Implement FUSE VFS as a very early implementation. It can, does, and will break your game.
Updated QML and C++ in various places to build and work with new Qt (6.4+) Minor bugfixes, possibly caused by the Qt version bump, and mostly in JS logic. Tried (again) to get mod sorting working by dragging them within the list. Initial support for Profiles. (To switch between FO4 and FOLON, for example.) Initial "Launch Game" stuff, but it's far from done. Don't use it (yet).
This commit is contained in:
parent
87cfc881e6
commit
1393a796b2
41 changed files with 3753 additions and 244 deletions
33
FuseMounter/FuseMounter.pro
Normal file
33
FuseMounter/FuseMounter.pro
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
QT += core sql dbus
|
||||
QT -= gui
|
||||
|
||||
CONFIG += c++17 cmdline link_pkgconfig
|
||||
|
||||
PKGCONFIG += fuse3 libunarr
|
||||
QMAKE_CXXFLAGS += -D_FILE_OFFSET_BITS=64 -O3
|
||||
#QMAKE_CXXFLAGS += -D_FILE_OFFSET_BITS=64 -g -O0
|
||||
|
||||
# You can make your code fail to compile if it uses deprecated APIs.
|
||||
# In order to do so, uncomment the following line.
|
||||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
SOURCES += main.cpp \
|
||||
archivemanager.cpp \
|
||||
database.cpp \
|
||||
fsproxy.cpp \
|
||||
fusestuff.cpp \
|
||||
modarchive.cpp \
|
||||
signalhandler.cpp
|
||||
|
||||
# Default rules for deployment.
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
!isEmpty(target.path): INSTALLS += target
|
||||
|
||||
HEADERS += \
|
||||
archivemanager.h \
|
||||
database.h \
|
||||
fsproxy.h \
|
||||
fusestuff.h \
|
||||
modarchive.h \
|
||||
signalhandler.h
|
||||
150
FuseMounter/archivemanager.cpp
Normal file
150
FuseMounter/archivemanager.cpp
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#include "archivemanager.h"
|
||||
#include "database.h"
|
||||
|
||||
#include <QDir>
|
||||
|
||||
ArchiveManager::ArchiveManager(Database *db, const QString &modsdir, int profileId, QObject *parent)
|
||||
: QObject{parent},
|
||||
m_database{db},
|
||||
m_modsdir{modsdir},
|
||||
m_profileId{profileId}
|
||||
{
|
||||
reloadArchives();
|
||||
|
||||
connect( &m_cacheTimer, &QTimer::timeout, this, &ArchiveManager::cleanCache );
|
||||
m_cacheTimer.setInterval(60000);
|
||||
m_cacheTimer.start();
|
||||
//QObject::connect( m_database, &Database::databaseModified, this, &ArchiveManager::reloadArchives );
|
||||
}
|
||||
|
||||
int ArchiveManager::reloadArchives()
|
||||
{
|
||||
qDebug() << "ArchiveManager::reloadArchives()";
|
||||
QHash< ModLibrary *, Archive * > archives;
|
||||
const QList<ModLibrary *> &entries = m_database->archiveList(m_profileId);
|
||||
for( ModLibrary *lib : entries )
|
||||
{
|
||||
Archive *arc = new Archive(lib->m_filename);
|
||||
//Archive *arc = new Archive(m_modsdir + QDir::separator() + lib->m_filename);
|
||||
archives.insert(lib, arc);
|
||||
}
|
||||
|
||||
// Release old:
|
||||
const QList< ModLibrary * > &libs = m_archives.keys();
|
||||
for( ModLibrary *lib : libs ) {
|
||||
Archive *arc = m_archives[lib];
|
||||
arc->killme();
|
||||
lib->deleteLater();
|
||||
}
|
||||
m_archives = archives;
|
||||
m_cache.clear();
|
||||
return m_archives.count();
|
||||
}
|
||||
|
||||
ArchiveManager::~ArchiveManager()
|
||||
{
|
||||
m_cacheTimer.stop();
|
||||
const QList< Archive * > &arcs = m_archives.values();
|
||||
for( Archive *arc : arcs )
|
||||
arc->deleteLater();
|
||||
}
|
||||
|
||||
QHash<ModLibrary *, Archive *> ArchiveManager::getArchives()
|
||||
{
|
||||
return m_archives;
|
||||
}
|
||||
|
||||
bool ArchiveManager::findFile(const QString &qpath, ModLibrary **library, ModEntry **modentry, Archive **archive)
|
||||
{
|
||||
if( m_cache.contains(qpath) )
|
||||
{
|
||||
//qDebug() << "ArchiveManager::findFile: Entry [" << qpath << "] cached (Valid:" << m_cache[qpath].m_valid << ")";
|
||||
*library = m_cache[qpath].m_library;
|
||||
*modentry = m_cache[qpath].m_entry;
|
||||
*archive = m_cache[qpath].m_archive;
|
||||
return m_cache[qpath].m_valid;
|
||||
}
|
||||
|
||||
qDebug() << "ArchiveManager::findFile: Searching for file" << qpath;
|
||||
ArchiveCachedEntry cacheEntry;
|
||||
QStringList qparts = qpath.split(QDir::separator());
|
||||
if( 0 == qpath.length() )
|
||||
qparts.clear();
|
||||
|
||||
const QList< ModLibrary * > &keys = m_archives.keys();
|
||||
for( ModLibrary *lib : keys )
|
||||
{
|
||||
//qDebug() << "ArchiveManager::findFile: Scanning library" << lib->m_filename;
|
||||
for( ModEntry *mod : lib->m_entries )
|
||||
{
|
||||
/*
|
||||
if( 0 != mod->m_installedPath.compare(qpath, Qt::CaseInsensitive) )
|
||||
continue;
|
||||
|
||||
//dolog("getattr (3): \"%s\" exists as \"%s\"\n", mod.m_archivePath.toStdString().c_str(), mod.m_installedPath.toStdString().c_str());
|
||||
dolog("getattr(4): Comparing: qpath=\"%s\" vs. path=\"%s\"...\n",
|
||||
qparts.join(QDir::separator()).toStdString().c_str(),
|
||||
parts.join(QDir::separator()).toStdString().c_str()
|
||||
);
|
||||
*/
|
||||
QStringList parts = mod->m_installedPath.split(QDir::separator());
|
||||
if( parts.length() < qparts.length() )
|
||||
continue;
|
||||
|
||||
bool skipme = false;
|
||||
for( int x=0; x < qparts.length(); x++ )
|
||||
{
|
||||
if( 0 != qparts.at(x).compare(parts.at(x), Qt::CaseInsensitive) )
|
||||
{
|
||||
skipme = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( skipme ) {
|
||||
//qDebug() << "ArchiveManager::findFile: Skipping" << mod->m_installedPath;
|
||||
continue;
|
||||
}
|
||||
|
||||
if( parts.length() > qparts.length() )
|
||||
{
|
||||
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...";
|
||||
m_cache[qpath] = cacheEntry;
|
||||
qDebug() << "Done.";
|
||||
return true;
|
||||
}
|
||||
|
||||
//QMap<QString, ArchiveCacheEntry *> *arc = m_archives[lib]->getEntries();
|
||||
*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();
|
||||
|
||||
cacheEntry.m_valid = true;
|
||||
cacheEntry.m_library = lib;
|
||||
cacheEntry.m_entry = mod;
|
||||
cacheEntry.m_archive = m_archives[lib];
|
||||
m_cache[qpath] = cacheEntry;
|
||||
//m_archives[lib]->cleanCache(time(NULL)-30);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
qDebug() << "ArchiveManager::findFile: Couldn't find file" << qpath << "(Cached)";
|
||||
m_cache[qpath] = cacheEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
void ArchiveManager::cleanCache()
|
||||
{
|
||||
//qDebug() << "ArchiveManager::cleanCache...";
|
||||
const QList< ModLibrary * > &keys = m_archives.keys();
|
||||
for( ModLibrary *lib : keys )
|
||||
m_archives[lib]->cleanCache(time(NULL)-300); // 5 mins seems like plenty
|
||||
}
|
||||
82
FuseMounter/archivemanager.h
Normal file
82
FuseMounter/archivemanager.h
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#ifndef ARCHIVEMANAGER_H
|
||||
#define ARCHIVEMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "modarchive.h"
|
||||
#include "database.h"
|
||||
|
||||
class ArchiveCachedEntry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
QObject *m_parent;
|
||||
|
||||
public:
|
||||
bool m_valid;
|
||||
//bool m_isdir;
|
||||
ModLibrary *m_library;
|
||||
ModEntry *m_entry;
|
||||
Archive *m_archive;
|
||||
|
||||
ArchiveCachedEntry(QObject *p=nullptr) : QObject(p)
|
||||
{
|
||||
m_valid = false;
|
||||
//m_isdir = false;
|
||||
m_library = NULL;
|
||||
m_entry = NULL;
|
||||
m_archive = NULL;
|
||||
}
|
||||
|
||||
ArchiveCachedEntry(const ArchiveCachedEntry &other)
|
||||
: QObject(other.parent())
|
||||
{
|
||||
m_valid = other.m_valid;
|
||||
//m_isdir = other.m_isdir;
|
||||
m_library = other.m_library;
|
||||
m_entry = other.m_entry;
|
||||
m_archive = other.m_archive;
|
||||
}
|
||||
ArchiveCachedEntry &operator=(const ArchiveCachedEntry &other)
|
||||
{
|
||||
//QObject(other.m_parent);
|
||||
m_valid = other.m_valid;
|
||||
//m_isdir = other.m_isdir;
|
||||
m_library = other.m_library;
|
||||
m_entry = other.m_entry;
|
||||
m_archive = other.m_archive;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
class ArchiveManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Database *m_database;
|
||||
QString m_modsdir;
|
||||
int m_profileId;
|
||||
|
||||
QTimer m_cacheTimer;
|
||||
QMap< QString, ArchiveCachedEntry > m_cache;
|
||||
|
||||
public slots:
|
||||
int reloadArchives();
|
||||
void cleanCache();
|
||||
|
||||
public:
|
||||
QHash< ModLibrary *, Archive * > m_archives;
|
||||
|
||||
explicit ArchiveManager(Database *db, const QString &modsdir, int profileId, QObject *parent = nullptr);
|
||||
~ArchiveManager();
|
||||
|
||||
QHash<ModLibrary *, Archive *> getArchives();
|
||||
|
||||
bool findFile(const QString &qpath, ModLibrary **library, ModEntry **modentry, Archive **archive);
|
||||
|
||||
signals:
|
||||
};
|
||||
|
||||
#endif // ARCHIVEMANAGER_H
|
||||
139
FuseMounter/database.cpp
Normal file
139
FuseMounter/database.cpp
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#include "database.h"
|
||||
|
||||
#include <QSqlError>
|
||||
#include <QSqlQuery>
|
||||
|
||||
Database::Database(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
m_database = QSqlDatabase::addDatabase("QSQLITE");
|
||||
}
|
||||
|
||||
void Database::setDatabase(QSqlDatabase &db)
|
||||
{
|
||||
m_database = db;
|
||||
}
|
||||
|
||||
bool Database::open(const QString &dbpath)
|
||||
{
|
||||
m_database.setDatabaseName(dbpath);
|
||||
m_open = m_database.open();
|
||||
if( !m_open ) {
|
||||
fprintf(stderr, "Failed to open the database: %s\n", m_database.lastError().text().toStdString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
readOverrides();
|
||||
return m_open;
|
||||
}
|
||||
|
||||
void Database::readOverrides()
|
||||
{
|
||||
QStringList overrides;
|
||||
QSqlQuery q = QSqlQuery("SELECT path FROM proxyOverrides", m_database);
|
||||
while( q.next() )
|
||||
{
|
||||
QString path = q.value(0).toString();
|
||||
overrides << path;
|
||||
}
|
||||
m_overrides = overrides;
|
||||
}
|
||||
|
||||
QList<ModLibrary *> Database::archiveList(int profileId)
|
||||
{
|
||||
/**
|
||||
* @brief Load in *reverse* order preference. Why? I'll tell ya. Go on, ask me. There's a grand reason.
|
||||
*
|
||||
* Y'see, users will intuitively order overrides or patches to load AFTER the original.
|
||||
*
|
||||
* Because we work on a "first match" policy, that means we MATCH on the LATEST version,
|
||||
* meaning that the last index containing a file reference will take preference for that
|
||||
* file over previous ones. Effectively, this "overrides" the older (lower index) version.
|
||||
*
|
||||
* 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);
|
||||
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);
|
||||
QList<ModLibrary *> library;
|
||||
while( q.next() )
|
||||
{
|
||||
ModLibrary *ent = new ModLibrary();
|
||||
ent->m_id = q.value(0).toInt();
|
||||
ent->m_filename = q.value(1).toString();
|
||||
library << ent;
|
||||
}
|
||||
|
||||
QList<ModLibrary *> results;
|
||||
for( ModLibrary *ent : library )
|
||||
{
|
||||
ent->m_entries = modManifest( ent->m_id );
|
||||
results << ent;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
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);
|
||||
qDebug() << qstr;
|
||||
QSqlQuery q = QSqlQuery(qstr, m_database);
|
||||
while( q.next() )
|
||||
{
|
||||
results << q.value(0).toString();
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
QList<ModEntry *> Database::modManifest(int id)
|
||||
{
|
||||
QList<ModEntry *> results;
|
||||
|
||||
QSqlQuery q = QSqlQuery(m_database);
|
||||
q.prepare("SELECT fileId, source, dest FROM files WHERE modId=?");
|
||||
q.bindValue(0, id);
|
||||
if( !q.exec() )
|
||||
return results;
|
||||
|
||||
while( q.next() )
|
||||
{
|
||||
ModEntry *ent = new ModEntry();
|
||||
ent->m_id = q.value(0).toInt();
|
||||
ent->m_archivePath = q.value(1).toString();
|
||||
ent->m_installedPath = q.value(2).toString();
|
||||
results << ent;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
bool Database::isProxyOverride(const QString &filepath)
|
||||
{
|
||||
return m_overrides.contains(filepath);
|
||||
}
|
||||
|
||||
bool Database::registerProxyOverride(const QString &filepath)
|
||||
{
|
||||
QSqlQuery q = QSqlQuery(m_database);
|
||||
q.prepare("INSERT INTO proxyOverrides (path)VALUES(?)");
|
||||
q.bindValue(0, filepath.toStdString().c_str());
|
||||
if( !q.exec() )
|
||||
return false;
|
||||
|
||||
m_overrides.append(filepath);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Database::unregisterProxyOverride(const QString &filepath)
|
||||
{
|
||||
QSqlQuery q = QSqlQuery(m_database);
|
||||
q.prepare("DELETE FROM proxyOverrides WHERE path LIKE ?");
|
||||
q.bindValue(0, filepath.toStdString().c_str());
|
||||
if( !q.exec() )
|
||||
return false;
|
||||
|
||||
m_overrides.removeAll(filepath);
|
||||
return true;
|
||||
}
|
||||
99
FuseMounter/database.h
Normal file
99
FuseMounter/database.h
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#ifndef DATABASE_H
|
||||
#define DATABASE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QSqlDatabase>
|
||||
|
||||
class ModEntry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
int m_id;
|
||||
QString m_archivePath;
|
||||
QString m_installedPath;
|
||||
|
||||
ModEntry() : m_id{0} {}
|
||||
/*
|
||||
ModEntry(const ModEntry &other)
|
||||
: QObject()
|
||||
{
|
||||
m_id = other.m_id;
|
||||
m_archivePath = other.m_archivePath;
|
||||
m_installedPath = other.m_installedPath;
|
||||
}
|
||||
|
||||
ModEntry &operator=(const ModEntry &other)
|
||||
{
|
||||
m_id = other.m_id;
|
||||
m_archivePath = other.m_archivePath;
|
||||
m_installedPath = other.m_installedPath;
|
||||
return *this;
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
class ModLibrary : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
int m_id;
|
||||
QString m_filename;
|
||||
QList<ModEntry *> m_entries;
|
||||
|
||||
ModLibrary() : m_id{0} {}
|
||||
~ModLibrary() {
|
||||
for( ModEntry *ent : m_entries )
|
||||
ent->deleteLater();
|
||||
}
|
||||
/*
|
||||
ModLibrary(const ModLibrary &other)
|
||||
: QObject()
|
||||
{
|
||||
m_id = other.m_id;
|
||||
m_filename = other.m_filename;
|
||||
m_entries = other.m_entries;
|
||||
}
|
||||
|
||||
ModLibrary &operator=(const ModLibrary &other)
|
||||
{
|
||||
m_id = other.m_id;
|
||||
m_filename = other.m_filename;
|
||||
m_entries = other.m_entries;
|
||||
return *this;
|
||||
}
|
||||
|
||||
friend bool operator<(const ModLibrary &a, const ModLibrary &b)
|
||||
{
|
||||
return a.m_id < b.m_id;
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
class Database : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
bool m_open;
|
||||
QSqlDatabase m_database;
|
||||
QStringList m_overrides;
|
||||
|
||||
public:
|
||||
explicit Database(QObject *parent = nullptr);
|
||||
//~Database();
|
||||
|
||||
void setDatabase(QSqlDatabase &db);
|
||||
bool open(const QString &dbpath);
|
||||
void readOverrides();
|
||||
QList<ModLibrary *> archiveList(int profileId);
|
||||
QStringList proxyList(int profileId);
|
||||
QList<ModEntry *> modManifest(int id);
|
||||
|
||||
bool isProxyOverride(const QString &filepath);
|
||||
bool registerProxyOverride(const QString &filepath);
|
||||
bool unregisterProxyOverride(const QString &filepath);
|
||||
};
|
||||
|
||||
#endif // DATABASE_H
|
||||
530
FuseMounter/fsproxy.cpp
Normal file
530
FuseMounter/fsproxy.cpp
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
#include "fsproxy.h"
|
||||
#include "database.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define CACHE_BLOCK_SIZE (1024*1024) // 1MB chunks
|
||||
|
||||
FSProxy::FSProxy(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
}
|
||||
|
||||
ProxyCacheEntry::ProxyCacheEntry(FSProxy *parent) :
|
||||
QObject(parent),
|
||||
m_parent{parent},
|
||||
m_accessCount{0},
|
||||
m_offset{0},
|
||||
m_lastAccess{0}
|
||||
{
|
||||
}
|
||||
|
||||
void ProxyCacheEntry::clear()
|
||||
{
|
||||
m_mutex.lock();
|
||||
for( ProxyCacheEntryChunk *c : m_chunks.values() )
|
||||
c->deleteLater();
|
||||
m_chunks.clear();
|
||||
m_mutex.unlock();
|
||||
}
|
||||
|
||||
void ProxyCacheEntry::clean()
|
||||
{
|
||||
time_t now = time(NULL) - 60; // 1 minute?
|
||||
m_mutex.lock();
|
||||
for( quint64 slot : m_chunks.keys() )
|
||||
{
|
||||
ProxyCacheEntryChunk *c = m_chunks[slot];
|
||||
if( c->m_lastAccess > now )
|
||||
continue;
|
||||
|
||||
qDebug() << "ProxyCacheEntry::clean(): Freeing chunk #" << slot;
|
||||
m_chunks.take(slot)->deleteLater();
|
||||
}
|
||||
m_mutex.unlock();
|
||||
}
|
||||
|
||||
FSProxyElement::FSProxyElement(const QString &path, QObject *parent)
|
||||
: QObject{parent},
|
||||
m_path{path}
|
||||
{
|
||||
}
|
||||
|
||||
QString FSProxyElement::resolvePath(const QString &qpath, int *matchlength=nullptr)
|
||||
{
|
||||
QStringList parts = qpath.split(QDir::separator());
|
||||
//QString filename = parts.takeLast();
|
||||
|
||||
int mlen = 0;
|
||||
QString bothparts = m_path;
|
||||
for( QString part : parts ) {
|
||||
QDir cur( bothparts );
|
||||
QStringList entries = cur.entryList(QDir::AllEntries|QDir::NoDotAndDotDot);
|
||||
|
||||
QString thisPiece = part;
|
||||
for( QString edir : entries )
|
||||
{
|
||||
if( 0 == part.compare(edir, Qt::CaseInsensitive) )
|
||||
{
|
||||
thisPiece = edir;
|
||||
mlen++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bothparts.append( QDir::separator() );
|
||||
bothparts.append( thisPiece );
|
||||
}
|
||||
|
||||
//qDebug() << "FSProxy::resolvePath:" << qpath << "=>" << bothparts;
|
||||
if( nullptr != matchlength )
|
||||
*matchlength = mlen;
|
||||
return bothparts;
|
||||
}
|
||||
|
||||
/*
|
||||
void FSProxy::setRoot(const QString &path)
|
||||
{
|
||||
qDebug() << "FSProxy::setRoot: Game root set to" << path;
|
||||
m_path = path;
|
||||
}
|
||||
*/
|
||||
void FSProxy::addRoot(const QString &path)
|
||||
{
|
||||
qDebug() << "FSProxy::setRoot: Adding proxy to" << path << "at position" << m_proxies.length();
|
||||
FSProxyElement *e = new FSProxyElement(path, this);
|
||||
//m_proxies.prepend( e );
|
||||
m_proxies << e;
|
||||
}
|
||||
|
||||
void FSProxy::clear()
|
||||
{
|
||||
qDebug() << "FSProxy: Resetting all...";
|
||||
m_filesMutex.lock();
|
||||
|
||||
QList< FSProxyElement * > proxies = m_proxies;
|
||||
m_proxies.clear();
|
||||
for( FSProxyElement *e : proxies )
|
||||
e->deleteLater();
|
||||
|
||||
QList<ProxyCacheEntry *> cache = m_cache.values();
|
||||
m_cache.clear();
|
||||
for( ProxyCacheEntry *e : cache )
|
||||
e->deleteLater();
|
||||
|
||||
m_filesMutex.unlock();
|
||||
}
|
||||
|
||||
void FSProxy::setup(Database *db, int profileId, const QString &gamename, const QString &gamedir)
|
||||
{
|
||||
m_db = db;
|
||||
m_profileId = profileId;
|
||||
m_gamename = gamename;
|
||||
m_gamedir = gamedir;
|
||||
reload();
|
||||
}
|
||||
|
||||
void FSProxy::reload() {
|
||||
clear();
|
||||
|
||||
addRoot( QString("/media/Sabrent/modding/%1/created/%2").arg(m_gamename).arg(m_profileId) );
|
||||
|
||||
QStringList others = m_db->proxyList(m_profileId);
|
||||
for( const QString &e : others )
|
||||
addRoot(e);
|
||||
|
||||
addRoot(m_gamedir);
|
||||
}
|
||||
|
||||
void FSProxy::cleanCache()
|
||||
{
|
||||
//m_filesMutex.lock();
|
||||
QList< ProxyCacheEntry * > ents = m_cache.values();
|
||||
for( ProxyCacheEntry *e : ents )
|
||||
e->clean();
|
||||
//m_filesMutex.unlock();
|
||||
}
|
||||
|
||||
ProxyCacheEntry *FSProxy::cacheFile(const QString &qpath)
|
||||
{
|
||||
int ret;
|
||||
struct stat st;
|
||||
for( FSProxyElement *e : m_proxies )
|
||||
{
|
||||
QString fullpath = e->resolvePath(qpath);
|
||||
ret = ::stat(fullpath.toStdString().c_str(), &st);
|
||||
if( 0 == ret ) {
|
||||
qDebug() << "FSProxy::getattr: Caching" << qpath << "to" << fullpath;
|
||||
ProxyCacheEntry *ne = new ProxyCacheEntry(this);
|
||||
ne->m_logicalpath = qpath;
|
||||
ne->m_filepath = fullpath;
|
||||
memcpy( &ne->m_stat, &st, sizeof(struct stat) );
|
||||
|
||||
//m_filesMutex.lock();
|
||||
m_cache.insert( qpath, ne );
|
||||
//m_filesMutex.unlock();
|
||||
|
||||
return ne;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
QStringList FSProxy::readdir(const QString &qpath)
|
||||
{
|
||||
QStringList results;
|
||||
for( FSProxyElement *e : m_proxies )
|
||||
{
|
||||
QString fullpath = e->resolvePath(qpath);
|
||||
|
||||
//qDebug() << "FSProxy::readdir(" << path << fullpath << ")";
|
||||
QDir dir(fullpath);
|
||||
QStringList ret = dir.entryList(QDir::AllEntries|QDir::NoDotAndDotDot);
|
||||
for( const QString &qs : ret ) {
|
||||
if( !results.contains(qs, Qt::CaseInsensitive) )
|
||||
results << qs;
|
||||
}
|
||||
//qDebug() << ret;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
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;
|
||||
memcpy( st, &ent->m_stat, sizeof(struct stat) );
|
||||
return true;
|
||||
}
|
||||
|
||||
ProxyCacheEntry *nent = cacheFile(qpath);
|
||||
if( !nent )
|
||||
return false;
|
||||
|
||||
memcpy( st, &nent->m_stat, sizeof(struct stat) );
|
||||
return true;
|
||||
}
|
||||
|
||||
int FSProxy::open(const QString &qpath, bool createIfNeeded)
|
||||
{
|
||||
if( m_cache.contains(qpath) )
|
||||
{
|
||||
ProxyCacheEntry *ent = m_cache[qpath];
|
||||
|
||||
QString fullpath = ent->m_filepath;
|
||||
QFile *f = new QFile(fullpath);
|
||||
//qDebug() << "FSProxy::open: (cached)" << fullpath;
|
||||
if( !f->open(QIODevice::ReadWrite) )
|
||||
{
|
||||
qDebug() << "FSProxy::open: failed:" << f->errorString();
|
||||
f->deleteLater();
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
int fd = f->handle();
|
||||
OpenFile *o = new OpenFile();
|
||||
o->m_logicalpath = qpath;
|
||||
o->m_filepath = fullpath;
|
||||
o->m_file = f;
|
||||
o->m_fd = fd;
|
||||
|
||||
m_filesMutex.lock();
|
||||
m_openFiles.insert( fd, o );
|
||||
m_filesMutex.unlock();
|
||||
|
||||
ent->m_accessCount++;
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
ProxyCacheEntry *cent = cacheFile( qpath );
|
||||
if( !cent && !createIfNeeded )
|
||||
{
|
||||
qDebug() << "FSProxy::open: 'createIfNeeded' not specified, file not found:" << qpath;
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
FSProxyElement *e = m_proxies.first();
|
||||
QString fullpath = e->resolvePath(qpath);
|
||||
QFile *f = new QFile(fullpath);
|
||||
|
||||
if( createIfNeeded ) {
|
||||
QStringList parts = fullpath.split(QDir::separator());
|
||||
parts.takeLast();
|
||||
QDir here;
|
||||
if( !here.mkpath(parts.join(QDir::separator())) )
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
qDebug() << "FSProxy::open:" << fullpath;
|
||||
if( !f->open(QIODevice::ReadWrite) )
|
||||
{
|
||||
qDebug() << "FSProxy::open: failed:" << f->errorString();
|
||||
f->deleteLater();
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
int fd = f->handle();
|
||||
OpenFile *o = new OpenFile();
|
||||
o->m_logicalpath = qpath;
|
||||
o->m_filepath = fullpath;
|
||||
o->m_file = f;
|
||||
o->m_fd = fd;
|
||||
|
||||
m_filesMutex.lock();
|
||||
m_openFiles.insert( fd, o );
|
||||
m_filesMutex.unlock();
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
bool FSProxy::close(int fd)
|
||||
{
|
||||
m_filesMutex.lock();
|
||||
if( !m_openFiles.contains(fd) ) {
|
||||
m_filesMutex.unlock();
|
||||
return false;
|
||||
}
|
||||
OpenFile *f = m_openFiles.take(fd);
|
||||
m_filesMutex.unlock();
|
||||
|
||||
f->m_mutex.lock();
|
||||
f->m_file->close();
|
||||
f->m_file->deleteLater();
|
||||
f->m_fd = -1;
|
||||
f->m_file = NULL;
|
||||
f->m_mutex.unlock();
|
||||
f->deleteLater();
|
||||
|
||||
cleanCache();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
qint64 ProxyCacheEntry::sparseRead(int fd, char *buf, size_t size, off_t offset)
|
||||
{
|
||||
m_accessCount++;
|
||||
m_lastAccess = time(NULL);
|
||||
|
||||
// chunk cache is aligned on CACHE_BLOCK_SIZE, and m_chunks is indexed on multiples therein.
|
||||
quint64 ipos = offset % CACHE_BLOCK_SIZE;
|
||||
quint64 cstart = (offset - ipos);
|
||||
quint64 cslot = cstart / CACHE_BLOCK_SIZE;
|
||||
|
||||
//fprintf(stderr, "ProxyCacheEntry::sparseRead: Calling, ipos=%llu / cstart=%llu / cslot=%llu / offset=%lu / size=%lu\n",
|
||||
// ipos, cstart, cslot, offset, size);
|
||||
|
||||
quint64 rlen = CACHE_BLOCK_SIZE - ipos;
|
||||
quint64 opos = 0, rtotal = 0;
|
||||
while( size > 0 )
|
||||
{
|
||||
ProxyCacheEntryChunk *chunk;
|
||||
if( m_chunks.contains(cslot) ) {
|
||||
qDebug() << "ProxyCacheEntry::sparseRead: Chunk #" << cslot << "already cached. Yay!";
|
||||
chunk = m_chunks[cslot];
|
||||
} else {
|
||||
if( !m_parent->m_openFiles.contains(fd) )
|
||||
{
|
||||
qDebug() << "ProxyCacheEntry::sparseRead: Attempt to read on an unknown FD!";
|
||||
errno = -EBADF;
|
||||
return -1;
|
||||
}
|
||||
|
||||
OpenFile *f = m_parent->m_openFiles[fd];
|
||||
if( !f->m_file->seek(cstart) )
|
||||
{
|
||||
qDebug() << "ProxyCacheEntry::sparseRead: Seek within file failed!";
|
||||
if( 0 == opos )
|
||||
{
|
||||
errno = -EINVAL;
|
||||
return -1;
|
||||
}
|
||||
return rtotal;
|
||||
}
|
||||
|
||||
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.";
|
||||
}
|
||||
|
||||
if( size < rlen )
|
||||
rlen = size;
|
||||
|
||||
//fprintf(stderr, " + sparseRead(size=%lu, offset=%lu)\n", size, offset);
|
||||
//fprintf(stderr, " - Copying %llu bytes of chunk+%llu to buf+%llu...\n", rlen, ipos, opos);
|
||||
memcpy( buf + opos, chunk->m_data.constData() + ipos, rlen );
|
||||
rtotal += rlen;
|
||||
|
||||
opos += rlen;
|
||||
size -= rlen;
|
||||
rlen = size > CACHE_BLOCK_SIZE ? CACHE_BLOCK_SIZE : size;
|
||||
ipos = 0;
|
||||
|
||||
cstart += CACHE_BLOCK_SIZE;
|
||||
cslot++;
|
||||
|
||||
chunk->m_accessCount++;
|
||||
chunk->m_lastAccess = time(NULL);
|
||||
}
|
||||
|
||||
//fprintf(stderr, " ~ sparseRead returning %llu bytes read.\n", rtotal);
|
||||
return rtotal;
|
||||
}
|
||||
|
||||
size_t FSProxy::read(int fd, char *buf, size_t size, off_t offset)
|
||||
{
|
||||
m_filesMutex.lock();
|
||||
if( !m_openFiles.contains(fd) ) {
|
||||
m_filesMutex.unlock();
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
OpenFile *f = m_openFiles[fd];
|
||||
f->m_mutex.lock();
|
||||
m_filesMutex.unlock();
|
||||
/*
|
||||
ProxyCacheEntry *ent;
|
||||
if( !m_cache.contains(f->m_logicalpath) )
|
||||
ent = cacheFile(f->m_logicalpath);
|
||||
else
|
||||
ent = m_cache[f->m_logicalpath];
|
||||
|
||||
//size_t ret = ent->sparseRead(fd, buf, size, offset);
|
||||
*/
|
||||
if( !f->m_file->seek(offset) )
|
||||
{
|
||||
errno = -EBADF;
|
||||
return -1;
|
||||
}
|
||||
size_t ret = f->m_file->read(buf, size);
|
||||
f->m_mutex.unlock();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int FSProxy::mknod(const QString &qpath, mode_t mode, dev_t dev)
|
||||
{
|
||||
FSProxyElement *e = m_proxies.first();
|
||||
QString fullpath = e->resolvePath(qpath);
|
||||
|
||||
QStringList parts = fullpath.split(QDir::separator());
|
||||
parts.takeLast();
|
||||
QDir here;
|
||||
QString parentdirs = parts.join(QDir::separator());
|
||||
qDebug() << "FSProxy::mknod: Creating parent directories:" << parentdirs;
|
||||
if( !here.mkpath(parentdirs) )
|
||||
return -EACCES;
|
||||
|
||||
qDebug() << "FSProxy::mknod: mknod:" << fullpath;
|
||||
return ::mknod(fullpath.toStdString().c_str(), mode, dev);
|
||||
}
|
||||
|
||||
int FSProxy::mkdir(const QString &qpath, mode_t mode)
|
||||
{
|
||||
FSProxyElement *e = m_proxies.first();
|
||||
QString fullpath = e->resolvePath(qpath);
|
||||
return ::mkdir(fullpath.toStdString().c_str(), mode);
|
||||
}
|
||||
|
||||
int FSProxy::unlink(const QString &qpath)
|
||||
{
|
||||
FSProxyElement *e = m_proxies.first();
|
||||
QString fullpath = e->resolvePath(qpath);
|
||||
if( m_cache.contains(qpath) )
|
||||
m_cache.take(qpath)->deleteLater();
|
||||
return ::unlink(fullpath.toStdString().c_str());
|
||||
}
|
||||
|
||||
int FSProxy::rmdir(const QString &qpath)
|
||||
{
|
||||
FSProxyElement *e = m_proxies.first();
|
||||
QString fullpath = e->resolvePath(qpath);
|
||||
if( m_cache.contains(qpath) )
|
||||
m_cache.take(qpath)->deleteLater();
|
||||
return ::rmdir(fullpath.toStdString().c_str());
|
||||
}
|
||||
|
||||
int FSProxy::rename(const QString &qpath, const QString &qnewname, unsigned int flags)
|
||||
{
|
||||
Q_UNUSED(flags)
|
||||
FSProxyElement *e = m_proxies.first();
|
||||
QString fullpathA = e->resolvePath(qpath);
|
||||
QString fullpathB = e->resolvePath(qnewname);
|
||||
if( flags & RENAME_EXCHANGE )
|
||||
return -ENOTSUP;
|
||||
if( flags & RENAME_NOREPLACE )
|
||||
return -ENOTSUP;
|
||||
if( m_cache.contains(qpath) )
|
||||
m_cache.take(qpath)->deleteLater();
|
||||
return ::rename(fullpathA.toStdString().c_str(), fullpathB.toStdString().c_str());
|
||||
}
|
||||
|
||||
int FSProxy::truncate(const QString &qpath, off_t offset)
|
||||
{
|
||||
FSProxyElement *e = m_proxies.first();
|
||||
QString fullpath = e->resolvePath(qpath);
|
||||
QFileInfo fi(fullpath);
|
||||
QDir dir = fi.absoluteDir();
|
||||
dir.mkdir( dir.path() );
|
||||
|
||||
return ::truncate(fullpath.toStdString().c_str(), offset);
|
||||
}
|
||||
|
||||
int FSProxy::write(int fd, const char *buf, size_t bufsz, off_t offset)
|
||||
{
|
||||
m_filesMutex.lock();
|
||||
if( !m_openFiles.contains(fd) ) {
|
||||
m_filesMutex.unlock();
|
||||
return -EINVAL;
|
||||
}
|
||||
OpenFile *f = m_openFiles[fd];
|
||||
f->m_mutex.lock();
|
||||
m_filesMutex.unlock();
|
||||
if( f->m_fd <= 0 )
|
||||
{
|
||||
f->m_mutex.unlock();
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
f->m_file->seek(offset);
|
||||
int ret = f->m_file->write(buf, bufsz);
|
||||
if( m_cache.contains(f->m_logicalpath) )
|
||||
m_cache.take(f->m_logicalpath)->deleteLater();
|
||||
f->m_mutex.unlock();
|
||||
return ret;
|
||||
}
|
||||
|
||||
off_t FSProxy::lseek(int fd, off_t off, int whence)
|
||||
{
|
||||
m_filesMutex.lock();
|
||||
if( !m_openFiles.contains(fd) ) {
|
||||
m_filesMutex.unlock();
|
||||
return -EINVAL;
|
||||
}
|
||||
OpenFile *f = m_openFiles[fd];
|
||||
f->m_mutex.lock();
|
||||
m_filesMutex.unlock();
|
||||
if( f->m_fd <= 0 )
|
||||
{
|
||||
f->m_mutex.unlock();
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
off_t ret;
|
||||
if( SEEK_CUR == whence )
|
||||
ret = f->m_file->seek( f->m_file->pos() + off );
|
||||
else if( SEEK_END == whence )
|
||||
ret = f->m_file->seek( f->m_file->size() + off );
|
||||
else if( SEEK_SET == whence )
|
||||
// Otherwise assume SEEK_SET
|
||||
ret = f->m_file->seek(off);
|
||||
else
|
||||
ret = -EINVAL;
|
||||
|
||||
f->m_mutex.unlock();
|
||||
return ret;
|
||||
}
|
||||
130
FuseMounter/fsproxy.h
Normal file
130
FuseMounter/fsproxy.h
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#ifndef FSPROXY_H
|
||||
#define FSPROXY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QFile>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
class Database;
|
||||
class OpenFile : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
QString m_logicalpath;
|
||||
QString m_filepath;
|
||||
|
||||
int m_fd;
|
||||
QFile *m_file;
|
||||
QMutex m_mutex;
|
||||
};
|
||||
|
||||
class ProxyCacheEntryChunk : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
quint64 m_start;
|
||||
QByteArray m_data;
|
||||
time_t m_lastAccess;
|
||||
quint32 m_accessCount;
|
||||
|
||||
ProxyCacheEntryChunk(quint64 start, QObject *parent=NULL) :
|
||||
QObject(parent),
|
||||
m_start{start},
|
||||
m_lastAccess{0},
|
||||
m_accessCount{0}
|
||||
{}
|
||||
};
|
||||
|
||||
class FSProxy;
|
||||
class ProxyCacheEntry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
FSProxy *m_parent;
|
||||
|
||||
public:
|
||||
QString m_logicalpath;
|
||||
QString m_filepath;
|
||||
|
||||
int m_accessCount;
|
||||
off_t m_offset;
|
||||
struct stat m_stat;
|
||||
time_t m_lastAccess;
|
||||
QMutex m_mutex;
|
||||
|
||||
QMap< quint64, ProxyCacheEntryChunk * > m_chunks;
|
||||
|
||||
ProxyCacheEntry(FSProxy *parent=NULL);
|
||||
~ProxyCacheEntry() { clear(); }
|
||||
|
||||
void clear();
|
||||
void clean();
|
||||
qint64 sparseRead(int fd, char *buf, size_t size, off_t offset);
|
||||
};
|
||||
|
||||
class FSProxyElement : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QString m_path;
|
||||
|
||||
public:
|
||||
explicit FSProxyElement(const QString &path, QObject *parent=nullptr);
|
||||
|
||||
QString resolvePath(const QString &path, int *matchlength);
|
||||
};
|
||||
|
||||
class FSProxy : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Database *m_db;
|
||||
int m_profileId;
|
||||
QString m_gamename;
|
||||
QString m_gamedir;
|
||||
|
||||
public:
|
||||
//QString m_path;
|
||||
QMap< QString, ProxyCacheEntry * > m_cache;
|
||||
//QMap< QString, QString > m_cache;
|
||||
QMap< int, OpenFile * > m_openFiles;
|
||||
QMutex m_filesMutex;
|
||||
QList< FSProxyElement * > m_proxies;
|
||||
|
||||
explicit FSProxy(QObject *parent = nullptr);
|
||||
|
||||
void setup(Database *db, int profileId, const QString &gamename, const QString &gamedir);
|
||||
|
||||
//void setRoot(const QString &path);
|
||||
QStringList readdir(const QString &path);
|
||||
bool getattr(const QString &path, struct stat *st);
|
||||
int open(const QString &path, bool createIfNeeded=false);
|
||||
bool close(int fd);
|
||||
size_t read(int fd, char *buf, size_t size, off_t offset);
|
||||
int mknod(const QString &qpath, mode_t mode, dev_t dev);
|
||||
int mkdir(const QString &qpath, mode_t mode);
|
||||
int unlink(const QString &qpath);
|
||||
int rmdir(const QString &qpath);
|
||||
int rename(const QString &qpath, const QString &qnewpath, unsigned int flags);
|
||||
int truncate(const QString &qpath, off_t offset);
|
||||
int write(int fd, const char *buf, size_t bufsz, off_t offset);
|
||||
off_t lseek(int fd, off_t off, int whence);
|
||||
|
||||
ProxyCacheEntry *cacheFile(const QString &qpath);
|
||||
|
||||
public slots:
|
||||
void addRoot(const QString &path);
|
||||
void clear();
|
||||
void reload();
|
||||
void cleanCache();
|
||||
|
||||
signals:
|
||||
};
|
||||
|
||||
#endif // FSPROXY_H
|
||||
678
FuseMounter/fusestuff.cpp
Normal file
678
FuseMounter/fusestuff.cpp
Normal file
|
|
@ -0,0 +1,678 @@
|
|||
#include "fusestuff.h"
|
||||
#include "archivemanager.h"
|
||||
#include "database.h"
|
||||
#include "fsproxy.h"
|
||||
#include "database.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QSocketNotifier>
|
||||
#include <QDateTime>
|
||||
|
||||
extern "C" {
|
||||
#define FUSE_USE_VERSION 31
|
||||
|
||||
#include <fuse3/fuse.h>
|
||||
#include <fuse3/fuse_lowlevel.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdarg.h>
|
||||
#include <unistd.h>
|
||||
}
|
||||
|
||||
static FILE *logfd = NULL;
|
||||
QMap< QString, QStringList > g_dircache;
|
||||
QMap<QString, struct stat *> g_shortcuts;
|
||||
QSocketNotifier *g_notifier;
|
||||
|
||||
void dolog(const char *fmt, ...)
|
||||
{
|
||||
//return;
|
||||
|
||||
char ostr[8192];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(ostr, sizeof(ostr), fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QString tstr = now.toString(Qt::ISODateWithMs);
|
||||
QString dstamp = QString("[%1] ").arg( tstr );
|
||||
|
||||
fwrite(dstamp.toStdString().c_str(), dstamp.length(), 1, stdout);
|
||||
fwrite(dstamp.toStdString().c_str(), dstamp.length(), 1, logfd);
|
||||
fwrite(ostr, strlen(ostr), 1, stdout);
|
||||
fwrite(ostr, strlen(ostr), 1, logfd);
|
||||
fflush(logfd);
|
||||
}
|
||||
|
||||
static QString cleanup_path(const char *path)
|
||||
{
|
||||
QString result = QString(path+1).replace("//", "/");
|
||||
return result;
|
||||
}
|
||||
|
||||
static ArchiveManager *g_archive = NULL;
|
||||
void qmfuse_set_archive(ArchiveManager *archive)
|
||||
{
|
||||
g_archive = archive;
|
||||
}
|
||||
|
||||
static FSProxy *g_proxy = NULL;
|
||||
void qmfuse_set_proxy(FSProxy *proxy)
|
||||
{
|
||||
g_proxy = proxy;
|
||||
}
|
||||
|
||||
static Database *g_database = NULL;
|
||||
void qmfuse_set_database(Database *db)
|
||||
{
|
||||
g_database = db;
|
||||
}
|
||||
|
||||
void qmfuse_reset()
|
||||
{
|
||||
QList< struct stat * > gsvals = g_shortcuts.values();
|
||||
g_dircache.clear();
|
||||
g_shortcuts.clear();
|
||||
for( struct stat *st : gsvals )
|
||||
delete st;
|
||||
}
|
||||
|
||||
static bool qmfuse_dircache_remove(const QString &qpath)
|
||||
{
|
||||
QStringList parts = qpath.split(QDir::separator());
|
||||
parts.takeLast();
|
||||
QString npath = parts.join(QDir::separator());
|
||||
if( !g_dircache.contains(npath) )
|
||||
return false;
|
||||
g_dircache.remove(npath);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void *qmfuse_init(struct fuse_conn_info *conn,
|
||||
struct fuse_config *cfg)
|
||||
{
|
||||
(void) conn;
|
||||
cfg->kernel_cache = 0;
|
||||
|
||||
logfd = fopen("/tmp/quickmod.txt", "w");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int qmfuse_getattr(const char *path, struct stat *stbuf,
|
||||
struct fuse_file_info *fi)
|
||||
{
|
||||
Q_UNUSED(fi)
|
||||
QString spath = path;
|
||||
if( g_shortcuts.contains(spath) )
|
||||
{
|
||||
if( NULL == g_shortcuts[spath] ) {
|
||||
//dolog("getattr: already know %s doesn't exist!\n", path);
|
||||
return -ENOENT;
|
||||
} else {
|
||||
struct stat *st = g_shortcuts[spath];
|
||||
memcpy( stbuf, st, sizeof(struct stat));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
dolog("getattr: %s\n", path);
|
||||
|
||||
QString qpath = cleanup_path(path);
|
||||
memset(stbuf, 0, sizeof(struct stat));
|
||||
|
||||
QStringList qparts = qpath.split(QDir::separator());
|
||||
if( 0 == qpath.length() )
|
||||
qparts.clear();
|
||||
/*
|
||||
if( qpath.endsWith(".ciopfs") )
|
||||
{
|
||||
stbuf->st_mode = S_IFREG | 0755;
|
||||
stbuf->st_nlink = 1;
|
||||
stbuf->st_uid = geteuid();
|
||||
stbuf->st_gid = getegid();
|
||||
return 0;
|
||||
}
|
||||
*/
|
||||
if( g_database->isProxyOverride(qpath.toLower()) )
|
||||
{
|
||||
dolog("getattr(1): override exists for %s!\n", path+1);
|
||||
if( g_proxy->getattr(qpath, stbuf) )
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* First the mods: */
|
||||
ModLibrary *library;
|
||||
ModEntry *mod;
|
||||
Archive *archive;
|
||||
if( g_archive->findFile(qpath, &library, &mod, &archive) )
|
||||
{
|
||||
if( !mod ) {
|
||||
dolog("getattr(4): returning that we're a directory\n");
|
||||
stbuf->st_mode = S_IFDIR | 0755;
|
||||
stbuf->st_nlink = 2;
|
||||
stbuf->st_uid = geteuid();
|
||||
stbuf->st_gid = getegid();
|
||||
time_t t = time(NULL);
|
||||
stbuf->st_atim.tv_sec = t;
|
||||
stbuf->st_ctim.tv_sec = t;
|
||||
stbuf->st_mtim.tv_sec = t;
|
||||
struct stat *st = new struct stat;
|
||||
memcpy(st, stbuf, sizeof(struct stat));
|
||||
g_shortcuts[spath] = st;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if( !archive ) {
|
||||
dolog("getattr(4): No archive entry for \"%s\"!\n", qpath.toStdString().c_str());
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
ArchiveCacheEntry *cacheEntry = archive->cache(mod->m_archivePath);
|
||||
if( !cacheEntry )
|
||||
{
|
||||
dolog("getattr(4): No cache entry for \"%s\"!\n", qpath.toStdString().c_str());
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
memcpy( stbuf, &cacheEntry->m_stat, sizeof(struct stat) );
|
||||
stbuf->st_uid = geteuid();
|
||||
stbuf->st_gid = getegid();
|
||||
struct stat *st = new struct stat;
|
||||
memcpy(st, stbuf, sizeof(struct stat));
|
||||
g_shortcuts[spath] = st;
|
||||
|
||||
dolog("getattr(4): returning that we're a file (\"%s\" => \"%s\")\n",
|
||||
mod->m_archivePath.toLower().toStdString().c_str(), mod->m_installedPath.toStdString().c_str() );
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Now check overlay (after): */
|
||||
if( g_proxy->getattr(qpath, stbuf) )
|
||||
return 0;
|
||||
|
||||
dolog("getattr (4xxx): !!! Couldn't find \"%s\" anywhere in our libraries!\n", path+1);
|
||||
g_shortcuts[spath] = NULL;
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
static int qmfuse_mknod(const char *path, mode_t mode, dev_t dev)
|
||||
{
|
||||
QString qpath = cleanup_path(path);
|
||||
dolog("mknod: \"%s\" %d %d\n", qpath.toStdString().c_str(), mode, dev);
|
||||
int ret = g_proxy->mknod(qpath, mode, dev);
|
||||
g_shortcuts.remove(QString(path));
|
||||
qmfuse_dircache_remove(qpath);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int qmfuse_mkdir(const char *path, mode_t mode)
|
||||
{
|
||||
QString qpath = cleanup_path(path);
|
||||
dolog("mkdir: %s\n", qpath.toStdString().c_str());
|
||||
g_shortcuts.remove(QString(path));
|
||||
qmfuse_dircache_remove(qpath);
|
||||
return g_proxy->mkdir(qpath, mode);
|
||||
}
|
||||
|
||||
static int qmfuse_unlink(const char *path)
|
||||
{
|
||||
QString qpath = cleanup_path(path);
|
||||
dolog("unlink: %s\n", qpath.toStdString().c_str());
|
||||
g_shortcuts.remove(QString(path));
|
||||
qmfuse_dircache_remove(qpath);
|
||||
g_database->unregisterProxyOverride(qpath);
|
||||
return g_proxy->unlink(qpath.toStdString().c_str());
|
||||
}
|
||||
|
||||
static int qmfuse_rmdir(const char *path)
|
||||
{
|
||||
QString qpath = cleanup_path(path);
|
||||
dolog("rmdir: %s\n", qpath.toStdString().c_str());
|
||||
g_shortcuts.remove(QString(path));
|
||||
qmfuse_dircache_remove(qpath);
|
||||
return g_proxy->rmdir(qpath);
|
||||
}
|
||||
|
||||
static int qmfuse_rename(const char *path, const char *newname, unsigned int flags)
|
||||
{
|
||||
QString qpathA = cleanup_path(path);
|
||||
QString qpathB = cleanup_path(newname);
|
||||
dolog("rename: %s => %s\n",
|
||||
qpathA.toStdString().c_str(),
|
||||
qpathB.toStdString().c_str()
|
||||
);
|
||||
g_shortcuts.remove(QString(path));
|
||||
g_shortcuts.remove(QString(newname));
|
||||
qmfuse_dircache_remove(qpathA);
|
||||
qmfuse_dircache_remove(qpathB);
|
||||
return g_proxy->rename(qpathA, qpathB, flags);
|
||||
}
|
||||
|
||||
static int qmfuse_truncate(const char *path, off_t offset, struct fuse_file_info *fi)
|
||||
{
|
||||
Q_UNUSED(fi);
|
||||
QString qpath = cleanup_path(path);
|
||||
dolog("truncate: %s\n", qpath.toStdString().c_str());
|
||||
int ret = g_proxy->truncate(qpath, offset);
|
||||
g_shortcuts.remove(QString(path));
|
||||
qmfuse_dircache_remove(qpath);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int qmfuse_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
||||
off_t offset, struct fuse_file_info *fi,
|
||||
enum fuse_readdir_flags flags)
|
||||
{
|
||||
(void) offset;
|
||||
(void) fi;
|
||||
(void) flags;
|
||||
|
||||
QString qpath = cleanup_path(path);
|
||||
if( g_dircache.contains(qpath) )
|
||||
{
|
||||
QStringList ents = g_dircache[qpath];
|
||||
for( const QString &e : ents )
|
||||
filler(buf, e.toStdString().c_str(), NULL, 0, FUSE_FILL_DIR_PLUS);
|
||||
return 0;
|
||||
}
|
||||
|
||||
dolog("readdir: %s\n", qpath.toStdString().c_str());
|
||||
QStringList filesshared;
|
||||
|
||||
/* First the overlay: */
|
||||
QStringList proxyentries = g_proxy->readdir(qpath);
|
||||
for( const QString &ent : proxyentries )
|
||||
{
|
||||
QStringList parts = ent.split(QDir::separator());
|
||||
QString nent = parts.last();
|
||||
//dolog("readdir(3): proxy: %s\n", nent.toStdString().c_str());
|
||||
filesshared.append(nent);
|
||||
filler(buf, nent.toStdString().c_str(), NULL, 0, FUSE_FILL_DIR_PLUS);
|
||||
}
|
||||
|
||||
/* Now the mods: */
|
||||
QStringList qparts = qpath.split(QDir::separator());
|
||||
if( 0 == qpath.length() )
|
||||
qparts.clear();
|
||||
|
||||
QHash< ModLibrary *, Archive * > entries = g_archive->getArchives();
|
||||
const QList< ModLibrary * > &ekeys = entries.keys();
|
||||
for( ModLibrary *library : ekeys )
|
||||
{
|
||||
//QMap<QString, ArchiveCacheEntry> arc = entries[library].getEntries();
|
||||
for( ModEntry *mod : library->m_entries )
|
||||
{
|
||||
/*
|
||||
dolog("readdir(2): \"%s\" startsWith \"%s\"?\n",
|
||||
mod.m_installedPath.toStdString().c_str(),
|
||||
qpath.toStdString().c_str());
|
||||
*/
|
||||
QStringList parts = mod->m_installedPath.split(QDir::separator());
|
||||
/*
|
||||
dolog("readdir(4): Comparing: qpath=\"%s\" vs. path=\"%s\"...\n",
|
||||
qparts.join(QDir::separator()).toStdString().c_str(),
|
||||
parts.join(QDir::separator()).toStdString().c_str()
|
||||
);
|
||||
*/
|
||||
if( parts.length() <= qparts.length() )
|
||||
continue;
|
||||
|
||||
bool skipme = false;
|
||||
for( int x=0; x < qparts.length(); x++ )
|
||||
{
|
||||
if( 0 == parts.at(x).length() )
|
||||
continue;
|
||||
|
||||
if( 0 == qparts.at(x).length() )
|
||||
continue;
|
||||
|
||||
if( 0 != qparts.at(x).compare(parts.at(x), Qt::CaseInsensitive) )
|
||||
{
|
||||
skipme = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( skipme )
|
||||
continue;
|
||||
|
||||
QString target = parts.at(qparts.length());
|
||||
if( filesshared.contains(target, Qt::CaseInsensitive) )
|
||||
continue;
|
||||
|
||||
//dolog("readdir(4b): \"%s\" => \"%s\"\n", mod.m_installedPath.toStdString().c_str(), qpath.toStdString().c_str());
|
||||
filesshared.append(target);
|
||||
filler(buf, target.toStdString().c_str(), NULL, 0, FUSE_FILL_DIR_PLUS);
|
||||
}
|
||||
}
|
||||
|
||||
g_dircache[qpath] = filesshared;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int qmfuse_write(const char *path, const char *buf, size_t bufsz, off_t offset,
|
||||
struct fuse_file_info *fi)
|
||||
{
|
||||
QString qpath = cleanup_path(path);
|
||||
dolog("write: \"%s\" %d bytes, %d offset\n", qpath.toStdString().c_str(), bufsz, offset);
|
||||
int ret = g_proxy->write(fi->fh, buf, bufsz, offset);
|
||||
//if( g_shortcuts.contains(qpath) )
|
||||
g_shortcuts.remove(QString(path));
|
||||
qmfuse_dircache_remove(qpath);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int qmfuse_open(const char *path, struct fuse_file_info *fi)
|
||||
{
|
||||
QString qpath = cleanup_path(path);
|
||||
dolog("open: %s\n", qpath.toStdString().c_str());
|
||||
|
||||
if( g_database->isProxyOverride(qpath.toLower()) )
|
||||
{
|
||||
dolog("open(1): override exists for %s!\n", qpath.toStdString().c_str());
|
||||
int fd = g_proxy->open(qpath);
|
||||
if( fd > 0 ) {
|
||||
fi->fh = fd;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* First the mods: */
|
||||
ModLibrary *library;
|
||||
ModEntry *mod;
|
||||
Archive *archive;
|
||||
if( g_archive->findFile(qpath, &library, &mod, &archive) )
|
||||
{
|
||||
if( !archive ) {
|
||||
dolog("open(1): attempt to open a directory? iunno: %s\n", qpath.toStdString().c_str());
|
||||
return -EINVAL;
|
||||
}
|
||||
/*
|
||||
if( !archive->contains(mod->m_archivePath) )
|
||||
{
|
||||
dolog("open(3): Archive does not contain \"%s\"!\n", qpath.toStdString().c_str());
|
||||
return -ENOENT;
|
||||
}
|
||||
*/
|
||||
|
||||
int fam = (fi->flags & O_ACCMODE);
|
||||
if( fam == O_RDWR || fam == O_WRONLY )
|
||||
{
|
||||
// Make a proxy override...
|
||||
dolog("open(2): trying to open for write: %s\n", qpath.toStdString().c_str());
|
||||
int fd = g_proxy->open(qpath, true);
|
||||
if( fd <= 0 ) {
|
||||
dolog("open(2): no dice, for some reason.\n");
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
g_database->registerProxyOverride(qpath.toLower());
|
||||
g_shortcuts.remove(qpath);
|
||||
fi->fh = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
dolog("open(2): opened \"%s\"\n", mod->m_installedPath.toStdString().c_str());
|
||||
fi->fh = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Now check overlay (after): */
|
||||
int fd = g_proxy->open(qpath);
|
||||
if( fd > 0 ) {
|
||||
fi->fh = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if( fi->flags & O_CREAT ) {
|
||||
fd = g_proxy->open(qpath, true);
|
||||
g_shortcuts.remove(qpath);
|
||||
fi->fh = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
static int qmfuse_release(const char *path, struct fuse_file_info *fi)
|
||||
{
|
||||
QString qpath = cleanup_path(path);
|
||||
dolog("release: %s\n", qpath.toStdString().c_str());
|
||||
if( fi->fh <= 0 ) {
|
||||
//g_archive->cleanCache();
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_proxy->close(fi->fh);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int qmfuse_read(const char *path, char *buf, size_t size, off_t offset,
|
||||
struct fuse_file_info *fi)
|
||||
{
|
||||
QString qpath = cleanup_path(path);
|
||||
|
||||
/* check overlay: */
|
||||
if( fi->fh > 0 ) {
|
||||
int ret = g_proxy->read(fi->fh, buf, size, offset);
|
||||
//if( ret != -EACCES )
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* mods after: */
|
||||
ModLibrary *library;
|
||||
ModEntry *mod;
|
||||
Archive *archive;
|
||||
if( g_archive->findFile(qpath, &library, &mod, &archive) )
|
||||
{
|
||||
/*
|
||||
if( !archive->contains(mod->m_archivePath) )
|
||||
{
|
||||
dolog("read(3): Archive does not contain \"%s\"!\n", qpath.toStdString().c_str());
|
||||
return -ENOENT;
|
||||
}
|
||||
*/
|
||||
if( !archive ) {
|
||||
dolog("open(1): attempt to read a directory? iunno: %s\n", qpath.toStdString().c_str());
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
ArchiveCacheEntry *cacheEntry = archive->cache(mod->m_archivePath);
|
||||
if( cacheEntry && cacheEntry->m_loading ) {
|
||||
qDebug() << QString("getEntry(%1) -> (Still Loading...)\n").arg(qpath);
|
||||
return -EAGAIN;
|
||||
}
|
||||
|
||||
QByteArray *src = archive->getEntry(mod->m_archivePath);
|
||||
if( !src || 0 == src->length() )
|
||||
{
|
||||
dolog("read(2): got 0-byte length while trying to read \"%s\"\n", qpath.toStdString().c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if( offset >= src->length() )
|
||||
{
|
||||
archive->releaseEntry(mod->m_archivePath);
|
||||
return -EOF;
|
||||
}
|
||||
|
||||
if (offset + size > (size_t)src->length())
|
||||
size = src->length() - offset;
|
||||
if( size > 0 )
|
||||
memcpy(buf, src->constData() + offset, size);
|
||||
int ret = size;
|
||||
if( ret < 0 )
|
||||
ret = 0;
|
||||
archive->releaseEntry(mod->m_archivePath);
|
||||
return ret;
|
||||
}
|
||||
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
static off_t qmfuse_lseek(const char *path, off_t off, int whence, struct fuse_file_info *fi)
|
||||
{
|
||||
/* check overlay: */
|
||||
if( fi->fh > 0 ) {
|
||||
int ret = g_proxy->lseek(fi->fh, off, whence);
|
||||
if( -EINVAL == ret )
|
||||
dolog("lseek(1): g_proxy->lseek on fd resulted in -EINVAL (whence is %d)\n", whence);
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString qpath = cleanup_path(path);
|
||||
|
||||
/* mods after: */
|
||||
ModLibrary *library;
|
||||
ModEntry *mod;
|
||||
Archive *archive;
|
||||
if( g_archive->findFile(qpath, &library, &mod, &archive) )
|
||||
{
|
||||
if( !archive )
|
||||
//if( !archive->contains(qpath) )
|
||||
{
|
||||
dolog("lseek(3): Archive does not contain \"%s\"!\n", qpath.toStdString().c_str());
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
ArchiveCacheEntry *cacheEntry = archive->cache(mod->m_archivePath);
|
||||
if( cacheEntry && cacheEntry->m_loading ) {
|
||||
qDebug() << QString("getEntry(%1) -> (Still Loading...)\n").arg(qpath);
|
||||
return -EAGAIN;
|
||||
}
|
||||
|
||||
if( SEEK_SET != whence )
|
||||
return -EINVAL;
|
||||
return off;
|
||||
/*
|
||||
if( cacheEntry->m_data.length() >= off )
|
||||
return off;
|
||||
return -EACCES;
|
||||
*/
|
||||
}
|
||||
|
||||
dolog("lseek: %s\n", path+1);
|
||||
return g_proxy->lseek(fi->fh, off, whence);
|
||||
}
|
||||
|
||||
|
||||
static const struct fuse_operations qmfuse_oper = {
|
||||
.getattr = qmfuse_getattr,
|
||||
.readlink = NULL,
|
||||
.mknod = qmfuse_mknod,
|
||||
.mkdir = qmfuse_mkdir,
|
||||
.unlink = qmfuse_unlink,
|
||||
.rmdir = qmfuse_rmdir,
|
||||
.symlink = NULL,
|
||||
.rename = qmfuse_rename,
|
||||
.link = NULL,
|
||||
.chmod = NULL,
|
||||
.chown = NULL,
|
||||
.truncate = qmfuse_truncate,
|
||||
.open = qmfuse_open,
|
||||
.read = qmfuse_read,
|
||||
.write = qmfuse_write,
|
||||
.statfs = NULL,
|
||||
.flush = NULL,
|
||||
.release = qmfuse_release,
|
||||
.fsync = NULL,
|
||||
.setxattr = NULL,
|
||||
.getxattr = NULL,
|
||||
.listxattr = NULL,
|
||||
.removexattr = NULL,
|
||||
.opendir = NULL,
|
||||
.readdir = qmfuse_readdir,
|
||||
.releasedir = NULL,
|
||||
.fsyncdir = NULL,
|
||||
.init = qmfuse_init,
|
||||
.destroy = NULL,
|
||||
.access = NULL,
|
||||
.create = NULL,
|
||||
.lock = NULL,
|
||||
.utimens = NULL,
|
||||
.bmap = NULL,
|
||||
.ioctl = NULL,
|
||||
.poll = NULL,
|
||||
.write_buf = NULL,
|
||||
.read_buf = NULL,
|
||||
.flock = NULL,
|
||||
.fallocate = NULL,
|
||||
.copy_file_range = NULL,
|
||||
.lseek = qmfuse_lseek,
|
||||
};
|
||||
|
||||
struct fuse *g_fuse;
|
||||
struct fuse_session *g_fuse_session;
|
||||
int qmfuse_main(const char *mountpoint)
|
||||
{
|
||||
/*
|
||||
char *argv[4];
|
||||
argv[0] = "quickmod";
|
||||
argv[1] = (char*)mountpoint;
|
||||
argv[2] = "-f";
|
||||
argv[3] = "-s";
|
||||
return fuse_main(3, argv, &qmfuse_oper, NULL);
|
||||
*/
|
||||
|
||||
char *argv[4];
|
||||
argv[0] = "quickmod";
|
||||
argv[1] = (char*)mountpoint;
|
||||
argv[2] = "-f";
|
||||
//argv[3] = "-s";
|
||||
//int argc = sizeof(*argv) / sizeof(argv[0]);
|
||||
//struct fuse_args args = FUSE_ARGS_INIT(4, argv);
|
||||
struct fuse_args args = FUSE_ARGS_INIT(3, argv);
|
||||
struct fuse_cmdline_opts opts;
|
||||
|
||||
if( 0 != fuse_parse_cmdline(&args, &opts) ) {
|
||||
fprintf(stderr, "Failed to parse options.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t op_size = sizeof(*(&qmfuse_oper));
|
||||
g_fuse = fuse_new(&args, &qmfuse_oper, op_size, NULL);
|
||||
if( !g_fuse ) {
|
||||
fprintf(stderr, "Failed to create new FUSE object.\n");
|
||||
return -2;
|
||||
}
|
||||
|
||||
if( 0 != fuse_mount(g_fuse, mountpoint) ) {
|
||||
fprintf(stderr, "Failed at fuse_mount!\n");
|
||||
return -3;
|
||||
}
|
||||
|
||||
g_fuse_session = fuse_get_session(g_fuse);
|
||||
if( 0 != fuse_set_signal_handlers(g_fuse_session) ) {
|
||||
fprintf(stderr, "Failed to set FUSE signal handlers!\n");
|
||||
return -4;
|
||||
}
|
||||
|
||||
//g_notifier = new QSocketNotifier(fuse_session_fd(g_fuse_session), QSocketNotifier::Read);
|
||||
return fuse_session_fd(g_fuse_session);
|
||||
}
|
||||
|
||||
int qmfuse_pump()
|
||||
{
|
||||
struct fuse_buf fbuf = { .mem = NULL };
|
||||
if( fuse_session_exited(g_fuse_session) ) {
|
||||
fprintf(stderr, "Feh. Session closed!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int res = fuse_session_receive_buf(g_fuse_session, &fbuf);
|
||||
if( -EINTR == res )
|
||||
return 0; // Nothing to do.
|
||||
|
||||
if( 0 >= res )
|
||||
return res;
|
||||
|
||||
fuse_session_process_buf(g_fuse_session, &fbuf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void qmfuse_unmount()
|
||||
{
|
||||
fuse_session_unmount(g_fuse_session);
|
||||
}
|
||||
18
FuseMounter/fusestuff.h
Normal file
18
FuseMounter/fusestuff.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#ifndef FUSESTUFF_H
|
||||
#define FUSESTUFF_H
|
||||
|
||||
class ArchiveManager;
|
||||
class FSProxy;
|
||||
class Database;
|
||||
void qmfuse_set_archive(ArchiveManager *archive);
|
||||
void qmfuse_set_proxy(FSProxy *proxy);
|
||||
void qmfuse_set_database(Database *db);
|
||||
//int qmfuse_main(int argc, char **argv);
|
||||
int qmfuse_main(const char *mountpoint);
|
||||
void qmfuse_reset();
|
||||
int qmfuse_pump();
|
||||
void qmfuse_unmount();
|
||||
|
||||
void dolog(const char *fmt, ...);
|
||||
|
||||
#endif // FUSESTUFF_H
|
||||
121
FuseMounter/main.cpp
Normal file
121
FuseMounter/main.cpp
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#include <QCoreApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mount.h>
|
||||
|
||||
#include "archivemanager.h"
|
||||
#include "database.h"
|
||||
#include "fusestuff.h"
|
||||
#include "fsproxy.h"
|
||||
#include "signalhandler.h"
|
||||
|
||||
QDebug operator<<(QDebug debug, const ModLibrary &c)
|
||||
{
|
||||
QDebugStateSaver saver(debug);
|
||||
debug.nospace() << "(ModLibrary:" << c.m_id << ", " << c.m_filename << ')';
|
||||
|
||||
return debug;
|
||||
}
|
||||
|
||||
QDebug operator<<(QDebug debug, const ModEntry &c)
|
||||
{
|
||||
QDebugStateSaver saver(debug);
|
||||
debug.nospace() << "(ModEntry:" << c.m_id << ", " << c.m_archivePath << ", " << c.m_installedPath << ')';
|
||||
|
||||
return debug;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
app.setOrganizationDomain("org.oneill");
|
||||
app.setOrganizationName("Quickmod");
|
||||
app.setApplicationName("quickmod");
|
||||
app.setApplicationVersion("1.0");
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription("FUSE-based mod-mounter for QuickMod");
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
parser.addPositionalArgument("game", QCoreApplication::translate("main", "Name of the game itself. Eg: \"Fallout 4\""));
|
||||
parser.addPositionalArgument("target", QCoreApplication::translate("main", "Full path to target mountpoint"));
|
||||
parser.addPositionalArgument("profile", QCoreApplication::translate("main", "Profile ID"));
|
||||
|
||||
// A boolean option with a single name (-l)
|
||||
QCommandLineOption showMountsOption("l", QCoreApplication::translate("main", "Show currently mounted games"));
|
||||
parser.addOption(showMountsOption);
|
||||
|
||||
parser.process(app);
|
||||
|
||||
bool force = parser.isSet(showMountsOption);
|
||||
if( force )
|
||||
{
|
||||
printf("This is where I'd list my currently mounted games. Unimplemented. Sowwy.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const QStringList args = parser.positionalArguments();
|
||||
if( args.length() < 2 )
|
||||
{
|
||||
parser.showHelp(-1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString gamename = args.at(0);
|
||||
QString targetdir = args.at(1);
|
||||
int profileId = args.at(2).toInt();
|
||||
|
||||
// Lazy unmount in case we were mounted and ... crashed, or who knows:
|
||||
::umount2( targetdir.toStdString().c_str(), MNT_DETACH );
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(gamename);
|
||||
QString dbpath = s.value("dbPath").toString();
|
||||
QString gamedir = s.value("gamePath").toString();
|
||||
QString modsdir = s.value("modsPath").toString();
|
||||
|
||||
Database db;
|
||||
if( !db.open(dbpath) )
|
||||
return -1;
|
||||
|
||||
FSProxy proxy;
|
||||
proxy.setup(&db, profileId, gamename, gamedir);
|
||||
|
||||
ArchiveManager manager(&db, modsdir, profileId);
|
||||
|
||||
SignalHandler sh;
|
||||
QObject::connect( &sh, &SignalHandler::hupSignalReceived, &manager, &ArchiveManager::reloadArchives );
|
||||
QObject::connect( &sh, &SignalHandler::hupSignalReceived, &proxy, &FSProxy::reload );
|
||||
sh.setup_unix_signal_handlers();
|
||||
|
||||
qmfuse_set_archive(&manager);
|
||||
qmfuse_set_proxy(&proxy);
|
||||
qmfuse_set_database(&db);
|
||||
QObject::connect( &sh, &SignalHandler::hupSignalReceived, &sh, []() {
|
||||
qmfuse_reset();
|
||||
});
|
||||
|
||||
int fuse_fd = qmfuse_main(targetdir.toStdString().c_str());
|
||||
QSocketNotifier fuse_notifier(fuse_fd, QSocketNotifier::Read);
|
||||
QObject::connect( &fuse_notifier, &QSocketNotifier::activated, &sh, [&fuse_notifier, &app](){
|
||||
int ret;
|
||||
do {
|
||||
ret = qmfuse_pump();
|
||||
} while(ret > 0);
|
||||
|
||||
if( ret < 0 ) {
|
||||
fuse_notifier.setEnabled(false);
|
||||
app.quit();
|
||||
}
|
||||
} );
|
||||
|
||||
return app.exec();
|
||||
//return qmfuse_main(targetdir.toStdString().c_str());
|
||||
//return qmfuse_main(argc, argv);
|
||||
}
|
||||
244
FuseMounter/modarchive.cpp
Normal file
244
FuseMounter/modarchive.cpp
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
#include "modarchive.h"
|
||||
|
||||
//#include <archive_entry.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#define BLOCKSIZE 10240
|
||||
|
||||
Archive::Archive(const QString &arcpath, QObject *parent)
|
||||
: QObject{parent},
|
||||
m_stream{NULL},
|
||||
m_archive{NULL},
|
||||
m_arcpath{arcpath},
|
||||
m_suicidal{false}
|
||||
{
|
||||
if( !this->open() )
|
||||
return;
|
||||
|
||||
qDebug() << "Archive:" << arcpath;
|
||||
while( ar_parse_entry(m_archive) )
|
||||
{
|
||||
const char *entryname = ar_entry_get_name(m_archive);
|
||||
if( strlen(entryname) <= 0 )
|
||||
continue;
|
||||
/*
|
||||
if( (st->st_mode & S_IFMT) == S_IFDIR )
|
||||
continue;
|
||||
*/
|
||||
time64_t filetime = ar_entry_get_filetime(m_archive) / 100000000;
|
||||
|
||||
ArchiveCacheEntry *cacheEntry = new ArchiveCacheEntry(this);
|
||||
cacheEntry->m_offset = ar_entry_get_offset(m_archive);
|
||||
memset( (void*)&cacheEntry->m_stat, 0, sizeof(struct stat) );
|
||||
cacheEntry->m_stat.st_size = ar_entry_get_size(m_archive);
|
||||
cacheEntry->m_stat.st_mode = S_IFREG | 0755;
|
||||
cacheEntry->m_stat.st_nlink = 1;
|
||||
cacheEntry->m_stat.st_uid = geteuid();
|
||||
cacheEntry->m_stat.st_gid = getegid();
|
||||
cacheEntry->m_stat.st_atim.tv_sec = filetime;
|
||||
cacheEntry->m_stat.st_mtim.tv_sec = filetime;
|
||||
cacheEntry->m_stat.st_ctim.tv_sec = filetime;
|
||||
QString lpath = QString(entryname).toLower();
|
||||
m_cache.insert( lpath, cacheEntry );
|
||||
//qDebug() << "\tAdding" << lpath << "/" << cacheEntry->m_stat.st_size << "bytes at offset" << cacheEntry->m_offset;
|
||||
}
|
||||
}
|
||||
|
||||
Archive::~Archive()
|
||||
{
|
||||
for( ArchiveCacheEntry *ent : m_cache.values() )
|
||||
ent->deleteLater();
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
bool Archive::open()
|
||||
{
|
||||
m_stream = ar_open_file(m_arcpath.toStdString().c_str());
|
||||
if( !m_stream ) {
|
||||
qDebug() << "Failed to open archive stream:" << m_arcpath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if( m_arcpath.endsWith("7z", Qt::CaseInsensitive) )
|
||||
m_archive = ar_open_7z_archive(m_stream);
|
||||
else if( m_arcpath.endsWith("rar", Qt::CaseInsensitive) )
|
||||
m_archive = ar_open_rar_archive(m_stream);
|
||||
else if( m_arcpath.endsWith("zip", Qt::CaseInsensitive) )
|
||||
m_archive = ar_open_zip_archive(m_stream, false);
|
||||
else {
|
||||
qDebug() << "Unsupported archive type:" << m_arcpath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if( !m_archive )
|
||||
qDebug() << "Failed to open archive:" << m_arcpath;
|
||||
return NULL != m_archive;
|
||||
}
|
||||
|
||||
bool Archive::close()
|
||||
{
|
||||
if( !m_archive )
|
||||
return false;
|
||||
|
||||
m_archive = NULL;
|
||||
m_stream = NULL;
|
||||
|
||||
ar_close_archive(m_archive);
|
||||
ar_close(m_stream);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Archive::contains(const QString &path)
|
||||
{
|
||||
QString lpath = path.toLower();
|
||||
return m_cache.contains(lpath);
|
||||
}
|
||||
|
||||
QMap< QString, ArchiveCacheEntry * > *Archive::getEntries()
|
||||
{
|
||||
return &m_cache;
|
||||
}
|
||||
|
||||
QByteArray *Archive::getEntry(const QString &path)
|
||||
{
|
||||
QString lpath = path.toLower();
|
||||
if( !m_cache.contains(lpath) ) {
|
||||
qDebug() << " 918273918273918273 NOT FOUND:" << lpath;
|
||||
return NULL; // Not found.
|
||||
}
|
||||
else if( m_cache[lpath]->m_loading ) {
|
||||
//qDebug() << QString("getEntry(%1) -> (Still Loading...)\n").arg(path);
|
||||
qDebug() << "Archive::getEntry: Already loading, chill.";
|
||||
return NULL;
|
||||
}
|
||||
else if( m_cache[lpath]->m_cached ) {
|
||||
//qDebug() << QString("getEntry(%1) -> CACHED\n").arg(path);
|
||||
m_cache[lpath]->m_lastAccess = time(NULL);
|
||||
m_cache[lpath]->m_accessCount++;
|
||||
m_cache[lpath]->m_locked = true;
|
||||
m_cache[lpath]->m_mutex.lock();
|
||||
return &m_cache[lpath]->m_data;
|
||||
}
|
||||
|
||||
m_cache[lpath]->m_loading = true;
|
||||
m_mutex.lock();
|
||||
qDebug() << "Cheaty skip to" << m_cache[lpath]->m_offset << "bytes...";
|
||||
if( !ar_parse_entry_at(m_archive, m_cache[lpath]->m_offset) ) {
|
||||
qDebug() << "Archive::getEntry: ar_parse_entry_at failed!";
|
||||
m_cache[lpath]->m_loading = false;
|
||||
m_mutex.unlock();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//int64_t entrysize = archive_entry_size(entry);
|
||||
QByteArray obuf;
|
||||
char buff[BLOCKSIZE * 256];
|
||||
size_t len = sizeof(buff);
|
||||
if( len > (size_t)m_cache[lpath]->m_stat.st_size )
|
||||
len = m_cache[lpath]->m_stat.st_size;
|
||||
|
||||
while( ar_entry_uncompress(m_archive, buff, len ) ) {
|
||||
obuf.append(buff, len);
|
||||
len = m_cache[lpath]->m_stat.st_size - obuf.length();
|
||||
if( len <= 0 )
|
||||
break;
|
||||
if( len > sizeof(buff) )
|
||||
len = sizeof(buff);
|
||||
}
|
||||
m_mutex.unlock();
|
||||
|
||||
if( 0 > obuf.length() ) {
|
||||
m_cache[lpath]->m_loading = false;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
m_cache[lpath]->m_data = obuf;
|
||||
m_cache[lpath]->m_cached = true;
|
||||
m_cache[lpath]->m_accessCount++;
|
||||
m_cache[lpath]->m_loading = false;
|
||||
m_cache[lpath]->m_lastAccess = time(NULL);
|
||||
m_cache[lpath]->m_locked = true;
|
||||
|
||||
m_cache[lpath]->m_mutex.lock();
|
||||
return &m_cache[lpath]->m_data;
|
||||
}
|
||||
|
||||
void Archive::releaseEntry(const QString &path)
|
||||
{
|
||||
QString lpath = path.toLower();
|
||||
if( !m_cache.contains(lpath) ) {
|
||||
qDebug() << " releaseEntry: ---1231231 NOT FOUND:" << lpath;
|
||||
return; // Not found.
|
||||
}
|
||||
/*
|
||||
m_cache[lpath]->m_cached = false;
|
||||
m_cache[lpath]->m_mutex.unlock();
|
||||
m_cache[lpath]->m_locked = false;
|
||||
m_cache[lpath]->m_data.clear();
|
||||
*/
|
||||
m_cache[lpath]->m_mutex.unlock();
|
||||
m_cache[lpath]->m_locked = false;
|
||||
|
||||
if( m_suicidal )
|
||||
checkIfLifeIsWorthIt();
|
||||
}
|
||||
|
||||
ArchiveCacheEntry *Archive::cache(const QString &path)
|
||||
{
|
||||
QString lpath = path.toLower();
|
||||
if( !m_cache.contains(lpath) )
|
||||
return NULL;
|
||||
|
||||
return m_cache[lpath];
|
||||
}
|
||||
|
||||
void Archive::cleanCache(time_t olderThan)
|
||||
{
|
||||
//return;
|
||||
QStringList keys = m_cache.keys();
|
||||
for( const QString &k : keys )
|
||||
{
|
||||
if( !m_cache[k]->m_cached )
|
||||
continue;
|
||||
|
||||
if( m_cache[k]->m_loading )
|
||||
continue;
|
||||
|
||||
if( m_cache[k]->m_locked )
|
||||
continue;
|
||||
|
||||
if( m_cache[k]->m_lastAccess > olderThan )
|
||||
continue;
|
||||
|
||||
qDebug() << "Archive::cleanCache: Clearing cache entry for:" << k;
|
||||
m_cache[k]->m_mutex.lock();
|
||||
m_cache[k]->m_cached = false;
|
||||
m_cache[k]->m_data.clear();
|
||||
m_cache[k]->m_mutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void Archive::killme()
|
||||
{
|
||||
m_suicidal = true;
|
||||
checkIfLifeIsWorthIt();
|
||||
}
|
||||
|
||||
void Archive::checkIfLifeIsWorthIt()
|
||||
{
|
||||
for( QString k : m_cache.keys() )
|
||||
{
|
||||
if( m_cache[k]->m_loading )
|
||||
return;
|
||||
|
||||
if( m_cache[k]->m_locked )
|
||||
return;
|
||||
}
|
||||
|
||||
deleteLater();
|
||||
}
|
||||
71
FuseMounter/modarchive.h
Normal file
71
FuseMounter/modarchive.h
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#ifndef MODARCHIVE_H
|
||||
#define MODARCHIVE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <unarr.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
class ArchiveCacheEntry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
QByteArray m_data;
|
||||
int m_accessCount;
|
||||
bool m_cached;
|
||||
bool m_loading;
|
||||
off_t m_offset;
|
||||
struct stat m_stat;
|
||||
time_t m_lastAccess;
|
||||
QMutex m_mutex;
|
||||
bool m_locked;
|
||||
|
||||
ArchiveCacheEntry(QObject *parent=NULL) :
|
||||
QObject(parent),
|
||||
m_accessCount{0},
|
||||
m_cached{false},
|
||||
m_loading{false},
|
||||
m_offset{0},
|
||||
m_lastAccess{0},
|
||||
m_locked{false}
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class Archive : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
ar_stream *m_stream;
|
||||
ar_archive *m_archive;
|
||||
QString m_arcpath;
|
||||
QMutex m_mutex;
|
||||
bool m_suicidal;
|
||||
|
||||
QMap< QString, ArchiveCacheEntry * > m_cache;
|
||||
|
||||
private:
|
||||
bool open();
|
||||
bool close();
|
||||
|
||||
public:
|
||||
explicit Archive(const QString &arcpath, QObject *parent = nullptr);
|
||||
~Archive();
|
||||
|
||||
QString getPath() { return m_arcpath; }
|
||||
QMap<QString, ArchiveCacheEntry *> *getEntries();
|
||||
QByteArray *getEntry(const QString &path);
|
||||
void releaseEntry(const QString &path);
|
||||
bool contains(const QString &path);
|
||||
ArchiveCacheEntry *cache(const QString &path);
|
||||
void cleanCache(time_t olderThan);
|
||||
|
||||
void killme();
|
||||
void checkIfLifeIsWorthIt();
|
||||
|
||||
signals:
|
||||
};
|
||||
|
||||
#endif // MODARCHIVE_H
|
||||
62
FuseMounter/signalhandler.cpp
Normal file
62
FuseMounter/signalhandler.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#include <QDebug>
|
||||
|
||||
#include "signalhandler.h"
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <signal.h>
|
||||
|
||||
static int sighupFd[2] = {0, 0};
|
||||
|
||||
SignalHandler::SignalHandler(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
sighupFd[0] = sighupFd[1] = 0;
|
||||
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sighupFd))
|
||||
qFatal("Couldn't create HUP socketpair");
|
||||
|
||||
snHup = new QSocketNotifier(sighupFd[0], QSocketNotifier::Read, this);
|
||||
connect(snHup, &QSocketNotifier::activated, this, &SignalHandler::handleSigHup);
|
||||
snHup->setEnabled(true);
|
||||
|
||||
qDebug() << "QSocketNotifier valid?" << snHup->isValid();
|
||||
}
|
||||
|
||||
int SignalHandler::setup_unix_signal_handlers()
|
||||
{
|
||||
struct sigaction hup;
|
||||
|
||||
hup.sa_handler = SignalHandler::hupSignalHandler;
|
||||
sigemptyset(&hup.sa_mask);
|
||||
hup.sa_flags = 0;
|
||||
hup.sa_flags |= SA_RESTART;
|
||||
|
||||
if (sigaction(SIGHUP, &hup, 0))
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SignalHandler::hupSignalHandler(int)
|
||||
{
|
||||
char a = 1;
|
||||
int len = ::write(sighupFd[1], &a, sizeof(a));
|
||||
fprintf(stderr, "SignalHandler::hupSignalHandler: Informing Qt process of HUP request, wrote %d bytes.\n", len);
|
||||
}
|
||||
|
||||
void SignalHandler::handleSigHup(QSocketDescriptor socket, QSocketNotifier::Type type)
|
||||
{
|
||||
Q_UNUSED(socket)
|
||||
Q_UNUSED(type)
|
||||
|
||||
snHup->setEnabled(false);
|
||||
|
||||
fprintf(stderr, "SignalHandler::handleSigHup: Reading the byte.\n");
|
||||
char tmp;
|
||||
int len = ::read(sighupFd[0], &tmp, sizeof(tmp));
|
||||
|
||||
// do Qt stuff
|
||||
fprintf(stderr, "SignalHandler::handleSigHup: HUP Received. Emitting 'hupSignalReceived', read %d bytes.\n", len);
|
||||
emit hupSignalReceived();
|
||||
|
||||
snHup->setEnabled(true);
|
||||
}
|
||||
31
FuseMounter/signalhandler.h
Normal file
31
FuseMounter/signalhandler.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#ifndef SIGNALHANDLER_H
|
||||
#define SIGNALHANDLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSocketNotifier>
|
||||
|
||||
class SignalHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SignalHandler(QObject *parent = nullptr);
|
||||
|
||||
// Unix signal handlers.
|
||||
static int setup_unix_signal_handlers();
|
||||
|
||||
protected:
|
||||
static void hupSignalHandler(int _unused);
|
||||
|
||||
public slots:
|
||||
// Qt signal handlers.
|
||||
void handleSigHup(QSocketDescriptor socket, QSocketNotifier::Type type);
|
||||
|
||||
private:
|
||||
//static int sighupFd[2];
|
||||
QSocketNotifier *snHup;
|
||||
|
||||
signals:
|
||||
void hupSignalReceived();
|
||||
};
|
||||
|
||||
#endif // SIGNALHANDLER_H
|
||||
33
README.md
33
README.md
|
|
@ -32,8 +32,10 @@ You can see for yourself by just downloading and opening [the archive in questio
|
|||
|
||||
### Requirements
|
||||
|
||||
* Qt 5.12+
|
||||
* Qt 6.4+
|
||||
* libarchive 3.6.0+
|
||||
* FUSE 3
|
||||
* [unarr](https://github.com/selmf/unarr) - I know, there's already a libarchive dependency, but unarr allows for direct file seeking which is needed by the VFS when loading a mod in situ.
|
||||
|
||||
### Building
|
||||
|
||||
|
|
@ -63,13 +65,20 @@ In the *File* menu open *Settings*:
|
|||
|
||||
From the *General* tab you can enter your personal Nexusmods API key, if you're willing to risk that. This key is stored in plaintext at **~/.config/Quickmod/quickmod.conf**
|
||||
|
||||
**Installation Method** - This will likely be changed. The best bang for the buck really depends on the mod itself, so this will simply become the "default". Until then, it does nothing.
|
||||
|
||||

|
||||
|
||||
Click the tab of the game you'd like to manage mods for. Each path must be specified:
|
||||
Click the tab of the game you'd like to manage mods for. Each path must be specified.
|
||||
|
||||
* **Mod Storage Directory** - Where the mod archive file is stored. I put mine in the game directory so it follows the game when it's installed on a microsd.
|
||||
* **Game Data Directory** - Where the actual game lives. Usually something like */DATA/SteamLibrary/steamapps/common/Skyrim Special Edition* or */home/leetguy/.steam/debian-installation/steamapps/common/Skyrim Special Edition*
|
||||
Where I put "/DATA" you'd likely want to use the same mountpoint as your Steam library, but it's entirely up to you. There's no restriction that the filesystem overlay or archives *must* reside on the same medium.
|
||||
|
||||
* **Mod Storage Directory** - Where the mod archive files shall be stored. I put mine on the same device as the game so it follows the game (it's installed on a microsd). Maybe something like */DATA/modding/Fallout 4 VR/mods*.
|
||||
* **Mod Staging Directory** - In cases where mod performance requires being extracted, the mod will be sandboxed in a subdirectory of the *Mod Staging Directory*. Something like */DATA/modding/Fallout 4 VR/staging*.
|
||||
* **Real Game Directory** - Where the actual vanilla game lives. Usually something like */DATA/SteamLibrary/steamapps/common/Skyrim Special Edition* or */home/leetguy/.steam/debian-installation/steamapps/common/Skyrim Special Edition* renamed to "Skyrim Special Edition - ACTUAL REAL DATA" or something. (See the VFS section about this. It's pretty important to understand how it works.)
|
||||
* **VFS Mountpoint** - This is where Steam or GOG *looks* for the game data. Y'know, the directory you renamed to whatever you put in the box above? Yeah, that path. (You may have to mkdir it after renaming the original.)
|
||||
* **User Data Directory** - This should be your compatdata user's account (usually *steamuser*). Something like */DATA/SteamLibrary/steamapps/compatdata/489830/pfx/drive_c/users/steamuser* or */home/leetguy/.steam/debian-installation/steamapps/compatdata/489830/pfx/drive_c/users/steamuser*
|
||||
* **Quickmod Database File** - This will be where your mod manifest stuff, profiles, selections, and so-on reside. Usually somewhere like */DATA/modding/Fallout 4 VR/Quickmod.sqlite*.
|
||||
|
||||

|
||||
|
||||
|
|
@ -77,10 +86,12 @@ Click *Save* and then select that game in the *Games* menu at the top.
|
|||
|
||||

|
||||
|
||||
Finally, you probably need to enable mods for the game by clicking *Enable mods in game* in the *File* menu. This is also known as "archive invalidation".
|
||||
For any mods to work you probably need to enable mods for the game by clicking *Enable mods in game* in the *File* menu. This is also known as "archive invalidation". If you're from the future and just using this for Cyberpunk 2077, you don't need to. (If it's grayed out that means it isn't needed.)
|
||||
|
||||

|
||||
|
||||
A default profile will be created for each game. You can add more in the sqlite3 database file, but... I do plan on adding a profile manager, eventually.
|
||||
|
||||
#### Handling nexusmods.com links
|
||||
|
||||
The simplest way is via Plasma's system settings, found in "Desktop Mode" on SteamOS:
|
||||
|
|
@ -97,6 +108,14 @@ The simplest way is via Plasma's system settings, found in "Desktop Mode" on Ste
|
|||
|
||||

|
||||
|
||||
#### Limitations
|
||||
|
||||
* The FUSE VFS requires *your* user account to have whatever permissions are needed to mount a filesystem in userspace. How do you do that? I don't know, kinda depends on your distro. Let me know how you did it!
|
||||
|
||||
* At the moment, *unarr* (and *maybe* libarchive) doesn't support RAR5 format, which a lot of mods are starting to use. I... I don't know what to even do about that, really. In my case I have a script that just repacks it. If it continues to be a headache, I'll... I dunno, figure something out? Until then, just... that's how it is.
|
||||
|
||||
* Many parts are still unimplemented, including Profile management, Launching a game, Download manager (pause, queues, speed limits, etcetera), along with error checking, additional games support, polish, and bug squishing. That said, it works well enough for me in this state to be a life saver with mods on Linux, even if I have to launch the game itself from Steam. (So what, I have to have Steam open for the game to launch anyway.)
|
||||
|
||||
#### Notes
|
||||
|
||||
* If your installation already has mods installed by some other means, they ... will just stay there and on unless you manually remove them (or do so via a different manager), so for best results start from a *clean* installation.
|
||||
|
|
@ -109,9 +128,9 @@ The simplest way is via Plasma's system settings, found in "Desktop Mode" on Ste
|
|||
|
||||
* ~~Mods are installed in order, and loaded in that order. This isn't what anybody wants, but for now there is no load order manager. You'll have to manually edit your loadorder.txt because of this (for now).~~
|
||||
|
||||
* There currently is no overwrite protection: the latest mod will simply overwrite any other files (including masters).
|
||||
* ~~There currently is no overwrite protection: the latest mod will simply overwrite any other files (including masters).~~ The new FUSE VFS basically solves this issue.
|
||||
|
||||
* File tracking is VERY crude: if a new mod overwrites files in a different mod, that file will simply be deleted when either mod is uninstalled.
|
||||
* ~~File tracking is VERY crude: if a new mod overwrites files in a different mod, that file will simply be deleted when either mod is uninstalled.~~ Same as above.
|
||||
|
||||
|
||||
I view this version (in its current state) as more of a learning exercise. Having written it to this stage (which actually works) I can see what I would change in a rewrite, which is my next big plan.
|
||||
|
|
|
|||
15
about.html
15
about.html
|
|
@ -1,19 +1,18 @@
|
|||
<center>
|
||||
<b>Version 1.0</b>
|
||||
<h3>made with love by mortanian</h3>
|
||||
<b>Version 2.0</b>
|
||||
<h3>made with <font color="#B39DDB">love</font> by <font color="#F48FB1">mortanian</font></h3>
|
||||
🔗 <a href="https://github.com/danieloneill/quickmod">https://github.com/danieloneill/quickmod</a>
|
||||
<hr></hr>
|
||||
</center>
|
||||
<p />
|
||||
<p>
|
||||
A basic mod manager for Steam + Proton on Linux (and I guess BSD).
|
||||
<br><br>
|
||||
</p><p>
|
||||
Currently supported games:
|
||||
<ul>
|
||||
<li>Skyrim Special Edition</li>
|
||||
<li>Skyrim VR</li>
|
||||
<li>Fallout 4</li>
|
||||
<li>Fallout 4 VR</li>
|
||||
<li>Fallout: New Vegas</li>
|
||||
</ul>
|
||||
<p />
|
||||
<center>
|
||||
<a href="https://github.com/danieloneill/quickmod">https://github.com/danieloneill/quickmod</a>
|
||||
</center>
|
||||
</p>
|
||||
|
|
|
|||
3
qml.qrc
3
qml.qrc
|
|
@ -12,6 +12,8 @@
|
|||
<file>about.html</file>
|
||||
<file>qml/Database.qml</file>
|
||||
<file>qml/ModsTable.qml</file>
|
||||
<file>qml/PluginsTable.qml</file>
|
||||
<file>qml/LaunchPage.qml</file>
|
||||
<file>qml/ProgressDialogue.qml</file>
|
||||
<file>qml/OverwriteDialogue.qml</file>
|
||||
<file>qml/downloader.js</file>
|
||||
|
|
@ -19,6 +21,5 @@
|
|||
<file>qml/game.js</file>
|
||||
<file>qml/mods.js</file>
|
||||
<file>qml/GameSettings.qml</file>
|
||||
<file>qml/PluginsTable.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ Dialog {
|
|||
title: qsTr('About Quickmod')
|
||||
modal: true
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: "#55000000" // Use whatever color/opacity you like
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
Button {
|
||||
text: qsTr("Close")
|
||||
|
|
@ -16,12 +20,15 @@ Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitHeight: aboutText.implicitHeight + 20
|
||||
implicitWidth: mainWin.width * 0.75
|
||||
//Item {
|
||||
//implicitHeight: aboutText.implicitHeight + 20
|
||||
//implicitWidth: mainWin.width * 0.75
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
//anchors.margins: 10
|
||||
TextArea {
|
||||
id: aboutText
|
||||
anchors.fill: parent
|
||||
readOnly: true
|
||||
textFormat: Text.RichText
|
||||
onLinkActivated: function(url) {
|
||||
|
|
@ -32,6 +39,7 @@ Dialog {
|
|||
ToolTip.text: qsTr("Open URL in browser...")
|
||||
}
|
||||
}
|
||||
//}
|
||||
|
||||
Component.onCompleted: aboutText.text = File.read(":/about.html");
|
||||
}
|
||||
|
|
|
|||
174
qml/Database.qml
174
qml/Database.qml
|
|
@ -40,15 +40,9 @@ Item {
|
|||
|
||||
function updateDatabase()
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return false;
|
||||
|
||||
if( !conn )
|
||||
{
|
||||
console.log(`Don't be a turdburglar, you isn't connectorated to no datumbass.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let q = conn.query("CREATE TABLE IF NOT EXISTS mods(modId INTEGER PRIMARY KEY NOT NULL, nexusGameCode TEXT, nexusId INTEGER, nexusFileId INTEGER, name TEXT NOT NULL, author TEXT, version VARCHAR(32), website TEXT, description TEXT, groups TEXT, installed INT NOT NULL DEFAULT 0, enabled INT NOT NULL DEFAULT 0, filename TEXT)", []);
|
||||
let q = conn.query("CREATE TABLE IF NOT EXISTS mods(modId INTEGER PRIMARY KEY NOT NULL, idx INTEGER, nexusGameCode TEXT, nexusId INTEGER, nexusFileId INTEGER, name TEXT NOT NULL, author TEXT, version VARCHAR(32), website TEXT, description TEXT, groups TEXT, installed INT NOT NULL DEFAULT 0, enabled INT NOT NULL DEFAULT 0, filename TEXT, moddir TEXT)", []);
|
||||
q.destroy();
|
||||
|
||||
q = conn.query("CREATE TABLE IF NOT EXISTS files(fileId INTEGER PRIMARY KEY NOT NULL, modId INTEGER NOT NULL, relative TEXT, source TEXT, dest TEXT, priority INTEGER)", []);
|
||||
|
|
@ -57,61 +51,84 @@ Item {
|
|||
q = conn.query("CREATE TABLE IF NOT EXISTS selections(modId INTEGER UNIQUE NOT NULL, json TEXT)", []);
|
||||
q.destroy();
|
||||
|
||||
q = conn.query("CREATE TABLE IF NOT EXISTS proxyOverrides(path TEXT)", []);
|
||||
q.destroy();
|
||||
|
||||
q = conn.query("CREATE TABLE IF NOT EXISTS profiles(name TEXT)", []);
|
||||
q.destroy();
|
||||
|
||||
q = conn.query("CREATE TABLE IF NOT EXISTS profile_selections(profileId INTEGER NOT NULL, modId INTEGER NOT NULL)", []);
|
||||
q.destroy();
|
||||
|
||||
if( !upgradeIfNeeded() )
|
||||
{
|
||||
q = conn.query("CREATE TABLE IF NOT EXISTS version(version INTEGER NOT NULL)", []);
|
||||
q.destroy();
|
||||
|
||||
q = conn.query("INSERT OR REPLACE INTO version (version)VALUES(1)", []);
|
||||
q = conn.query("INSERT OR REPLACE INTO version (version)VALUES(4)", []);
|
||||
q.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function upgradeIfNeeded()
|
||||
{
|
||||
if( !checkConnection() ) return false;
|
||||
|
||||
try {
|
||||
let q = conn.query("SELECT version FROM version", []);
|
||||
let v = q.toArray();
|
||||
q.destroy();
|
||||
|
||||
if( v.length === 1 )
|
||||
let version = 0;
|
||||
if( v.length > 0 )
|
||||
version = v[0]['version'];
|
||||
|
||||
if( version == 2 )
|
||||
{
|
||||
if( v[0]['version'] === 1 )
|
||||
return true;
|
||||
// okay, this is easy. we just need to add "Data/" to errthang:
|
||||
let nq = conn.query( "ALTER TABLE mods ADD idx INTEGER AFTER modId", []);
|
||||
nq.destroy();
|
||||
|
||||
console.log(`This database version (${v[0]['version']}) isn't one I'm aware of, probably made with a newer version of quickmod.`);
|
||||
return true;
|
||||
nq = conn.query( "UPDATE version SET version=3", []);
|
||||
nq.destroy();
|
||||
|
||||
version = 3;
|
||||
}
|
||||
|
||||
if( version == 3 )
|
||||
{
|
||||
// okay, this is easy. we just need to add "Data/" to errthang:
|
||||
let nq = conn.query( "ALTER TABLE mods ADD moddir TEXT AFTER filename", []);
|
||||
nq.destroy();
|
||||
|
||||
nq = conn.query( "UPDATE version SET version=4", []);
|
||||
nq.destroy();
|
||||
|
||||
version = 4;
|
||||
}
|
||||
|
||||
if( version == 4 )
|
||||
return true;
|
||||
|
||||
console.log(`This database version (${version}) isn't one I'm aware of, probably made with a newer version of quickmod.`);
|
||||
return true;
|
||||
} catch(err) {
|
||||
console.log("No version table, probably version 0.");
|
||||
}
|
||||
|
||||
try {
|
||||
// We're on version 0.
|
||||
let nq = conn.query( "ALTER TABLE mods DROP COLUMN nexusId", []);
|
||||
nq.destroy();
|
||||
|
||||
nq = conn.query( "ALTER TABLE mods ADD COLUMN nexusId INTEGER", []);
|
||||
nq.destroy();
|
||||
|
||||
nq = conn.query( "ALTER TABLE mods ADD COLUMN nexusFileId INTEGER", []);
|
||||
nq.destroy();
|
||||
|
||||
nq = conn.query( "ALTER TABLE mods ADD COLUMN nexusGameCode TEXT", []);
|
||||
nq.destroy();
|
||||
} catch(err) {
|
||||
console.log("Failed to upgrade to version 1, so we probably have no database.");
|
||||
console.log("No version table, looks like we don't have a database yet.");
|
||||
}
|
||||
|
||||
//updateDatabase();
|
||||
return false; // Because we didn't have a version table.
|
||||
}
|
||||
|
||||
// Mods
|
||||
function getMods()
|
||||
function getMods(profileId)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("SELECT modId, name, nexusGameCode, nexusId, nexusFileId, author, version, website, description, groups, enabled, installed, filename FROM mods");
|
||||
if( !profileId )
|
||||
profileId = 1;
|
||||
|
||||
let q = conn.query("SELECT m.modId, m.name, m.nexusGameCode, m.nexusId, m.nexusFileId, m.author, m.version, m.website, m.description, m.groups, (SELECT COUNT(ps.modId) FROM profile_selections ps WHERE ps.profileId=? AND ps.modId=m.modId) AS enabled, m.installed, m.filename FROM mods m", [profileId]);
|
||||
const results = q.toArray();
|
||||
q.destroy();
|
||||
|
||||
|
|
@ -126,24 +143,61 @@ Item {
|
|||
|
||||
function updateMod(modinfo)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
const tgroups = JSON.stringify(modinfo['groups']);
|
||||
|
||||
conn.transaction();
|
||||
let q = conn.query("UPDATE mods SET nexusGameCode=?, nexusId=?, nexusFileId=?, name=?, author=?, version=?, website=?, description=?, groups=?, installed=?, enabled=?, filename=? WHERE modId=?",
|
||||
[modinfo['nexusGameCode'], modinfo['nexusId'], modinfo['nexusFileId'], modinfo['name'], modinfo['author'], modinfo['version'], modinfo['website'], modinfo['description'], tgroups, modinfo['installed'], modinfo['enabled'], modinfo['filename'], modinfo['modId']]);
|
||||
conn.commit();
|
||||
|
||||
q.destroy();
|
||||
}
|
||||
|
||||
function updateModIndex(modId, pos)
|
||||
{
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
conn.transaction();
|
||||
let q = conn.query("UPDATE mods SET idx=? WHERE modId=?",
|
||||
[pos, modId]);
|
||||
conn.commit();
|
||||
|
||||
q.destroy();
|
||||
}
|
||||
|
||||
function updateModActive(modId, profileId, isActive)
|
||||
{
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
conn.transaction();
|
||||
let q;
|
||||
if( isActive ) {
|
||||
q = conn.query("INSERT INTO profile_selections (profileId, modId)VALUES(?, ?)",
|
||||
[profileId, modId]);
|
||||
} else {
|
||||
q = conn.query("DELETE FROM profile_selections WHERE profileId=? AND modId=?",
|
||||
[profileId, modId]);
|
||||
}
|
||||
|
||||
conn.commit();
|
||||
|
||||
q.destroy();
|
||||
}
|
||||
|
||||
function insertMod(modinfo)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
const tgroups = JSON.stringify(modinfo['groups']);
|
||||
|
||||
conn.transaction();
|
||||
let q = conn.query("INSERT INTO mods (nexusGameCode, nexusId, nexusFileId, name, author, version, website, description, groups, installed, enabled, filename)VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[modinfo['nexusGameCode'], modinfo['nexusId'], modinfo['nexusFileId'], modinfo['name'], modinfo['author'], modinfo['version'], modinfo['website'], modinfo['description'], tgroups, modinfo['installed'], modinfo['enabled'], modinfo['filename']]);
|
||||
modinfo['modId'] = q.lastInsertId();
|
||||
conn.commit();
|
||||
|
||||
q.destroy();
|
||||
|
||||
return modinfo;
|
||||
|
|
@ -151,16 +205,34 @@ Item {
|
|||
|
||||
function removeMod(modId)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("DELETE FROM mods WHERE modId=?", [modId]);
|
||||
q.destroy();
|
||||
}
|
||||
|
||||
// Profiles
|
||||
function getProfiles()
|
||||
{
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("SELECT _rowId_ AS id, name FROM profiles");
|
||||
const results = q.toArray();
|
||||
q.destroy();
|
||||
/*
|
||||
results.map( function(m) {
|
||||
if( m['groups'].length > 0 )
|
||||
m['groups'] = JSON.parse(m['groups']);
|
||||
else m['groups'] = [];
|
||||
} );
|
||||
*/
|
||||
return results;
|
||||
}
|
||||
|
||||
// Files
|
||||
function getFiles(modId)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("SELECT fileId, modId, relative, source, dest, priority FROM files WHERE modId=?", [modId]);
|
||||
const results = q.toArray();
|
||||
|
|
@ -171,7 +243,7 @@ Item {
|
|||
|
||||
function getFilesByDests(dests)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let qstr = "SELECT fileId, modId, relative, source, dest, priority FROM files";
|
||||
let token = ' WHERE ';
|
||||
|
|
@ -193,15 +265,15 @@ Item {
|
|||
|
||||
function getFilesEndingWith(suffixes)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let qstr = "SELECT fileId, modId, relative, source, dest, priority FROM files";
|
||||
let qstr = "SELECT f.fileId, f.modId, f.relative, f.source, f.dest, f.priority, m.enabled FROM files f JOIN mods m ON m.modId=f.modId";
|
||||
let token = ' WHERE ';
|
||||
let args = [];
|
||||
|
||||
// I has feels this is gonna get beat tf up...
|
||||
suffixes.forEach( function(d) {
|
||||
qstr += token + "dest LIKE '%' || ?";
|
||||
qstr += token + "LOWER(f.dest) LIKE '%' || LOWER(?)";
|
||||
token = ' OR ';
|
||||
args.push(d);
|
||||
} );
|
||||
|
|
@ -215,7 +287,7 @@ Item {
|
|||
|
||||
function insertFile(modId, fileInfo)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("INSERT INTO files (modId, relative, source, dest, priority)VALUES(?, ?, ?, ?, ?)",
|
||||
[modId, fileInfo['relative'], fileInfo['source'], fileInfo['dest'], fileInfo['priority']]);
|
||||
|
|
@ -227,7 +299,7 @@ Item {
|
|||
|
||||
function insertFiles(modId, fileInfos)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("BEGIN DEFERRED TRANSACTION", []);
|
||||
q.destroy();
|
||||
|
|
@ -244,7 +316,7 @@ Item {
|
|||
|
||||
function removeFile(fileId)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("DELETE FROM files WHERE fileId=?", [fileId]);
|
||||
q.destroy();
|
||||
|
|
@ -252,7 +324,7 @@ Item {
|
|||
|
||||
function removeModFiles(modId)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("DELETE FROM files WHERE modId=?", [modId]);
|
||||
q.destroy();
|
||||
|
|
@ -260,7 +332,7 @@ Item {
|
|||
|
||||
function removeFiles(fileIds)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("BEGIN DEFERRED TRANSACTION", []);
|
||||
q.destroy();
|
||||
|
|
@ -277,7 +349,7 @@ Item {
|
|||
// Selections
|
||||
function getSelections(modId)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("SELECT json FROM selections WHERE modId=?", [modId]);
|
||||
const results = q.toArray();
|
||||
|
|
@ -288,7 +360,7 @@ Item {
|
|||
|
||||
function updateSelections(modId, json)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("REPLACE INTO selections (modId, json)VALUES(?, ?)", [json, modId]);
|
||||
q.destroy();
|
||||
|
|
@ -296,7 +368,7 @@ Item {
|
|||
|
||||
function clearSelections(modId)
|
||||
{
|
||||
if( !checkConnection ) return;
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let q = conn.query("DELETE FROM selections WHERE modId=?", [modId]);
|
||||
q.destroy();
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ Item {
|
|||
Item {
|
||||
property alias enabled: intobj.enabled
|
||||
property alias gamePath: intobj.gamePath
|
||||
property alias vfsPath: intobj.vfsPath
|
||||
property alias modsPath: intobj.modsPath
|
||||
property alias modStagingPath: intobj.modStagingPath
|
||||
property alias userDataPath: intobj.userDataPath
|
||||
property alias dbPath: intobj.dbPath
|
||||
property alias customLaunchString: intobj.customLaunchString
|
||||
property alias profileId: intobj.profileId
|
||||
|
||||
Settings {
|
||||
id: intobj
|
||||
|
|
@ -20,9 +24,13 @@ Item {
|
|||
|
||||
property bool enabled: false
|
||||
property string gamePath
|
||||
property string vfsPath
|
||||
property string modsPath
|
||||
property string modStagingPath
|
||||
property string userDataPath
|
||||
property string dbPath
|
||||
property string customLaunchString
|
||||
property var profileId: 1
|
||||
}
|
||||
}
|
||||
model: gameDefinitions
|
||||
|
|
|
|||
107
qml/LaunchPage.qml
Normal file
107
qml/LaunchPage.qml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
Item {
|
||||
id: launchPage
|
||||
clip: true
|
||||
|
||||
property string currentGame
|
||||
property var launchOptions: [
|
||||
{ 'name':qsTr('No game selected!'), 'valid':false, 'launch':qsTr('(No launch path!)') }
|
||||
]
|
||||
|
||||
Flickable {
|
||||
id: pager
|
||||
anchors.fill: parent
|
||||
contentWidth: pagedItem.width
|
||||
contentHeight: pagedItem.height
|
||||
|
||||
Item {
|
||||
id: pagedItem
|
||||
width: pager.width > 600 ? pager.width : 600
|
||||
height: pagedColumn.height + 40
|
||||
|
||||
ColumnLayout {
|
||||
id: pagedColumn
|
||||
width: parent.width - 40
|
||||
x: 20
|
||||
y: 20
|
||||
spacing: 20
|
||||
|
||||
Repeater {
|
||||
model: launchPage.launchOptions
|
||||
|
||||
GridLayout {
|
||||
id: compGrid
|
||||
columns: 3
|
||||
Layout.fillWidth: true
|
||||
required property var modelData
|
||||
/*
|
||||
Component.onCompleted: argsToString();
|
||||
onModelDataChanged: argsToString();
|
||||
|
||||
function argsToString() {
|
||||
let outstr = compGrid.modelData['exe'];
|
||||
for( let a of compGrid.modelData['args'] )
|
||||
{
|
||||
if( a.indexOf(' ') >= 0 ) {
|
||||
let b = a.replace(new RegExp(/"/g), '\\"');
|
||||
outstr += ` "${b}"`;
|
||||
} else
|
||||
outstr += ' '+a;
|
||||
}
|
||||
fieldExe.text = outstr;
|
||||
}
|
||||
*/
|
||||
function stringToArgs() {
|
||||
let rx = new RegExp(/("[^"]+"|[^\s"]+)/g);
|
||||
let parts = fieldExe.text.match(rx);
|
||||
|
||||
/*
|
||||
// To remove surrounding quotes:
|
||||
for( let idx in parts )
|
||||
parts[idx] = parts[idx].replace(/^"(.*)"$/, '$1');
|
||||
*/
|
||||
|
||||
let exe = parts.shift();
|
||||
console.log(`Got exe: '${exe}' with args: ${JSON.stringify(parts,null,2)}`);
|
||||
return [ exe, parts ];
|
||||
}
|
||||
|
||||
Label {
|
||||
font.bold: true
|
||||
text: compGrid.modelData['name']
|
||||
Layout.columnSpan: 2
|
||||
}
|
||||
Label {
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: compGrid.modelData['valid'] ? qsTr('Okay') : qsTr('Invalid!')
|
||||
color: compGrid.modelData['valid'] ? 'green' : 'red'
|
||||
}
|
||||
|
||||
Button {
|
||||
text: compGrid.modelData['custom'] ? qsTr('Clear') : qsTr('Reset')
|
||||
}
|
||||
TextField {
|
||||
id: fieldExe
|
||||
placeholderText: qsTr('Launch string')
|
||||
Layout.fillWidth: true
|
||||
enabled: compGrid.modelData['valid']
|
||||
text: compGrid.modelData['launch']
|
||||
onAccepted: {
|
||||
//compGrid.stringToArgs();
|
||||
if( compGrid.modelData['custom'] )
|
||||
{
|
||||
let gset = gameSettings.objFor(currentGame);
|
||||
gset['customLaunchString'] = fieldExe.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // GridLayout
|
||||
} // Repeater
|
||||
} // ColumnLayout:pagedColumn
|
||||
} // Item:pagedItem
|
||||
} // Flickable:pager
|
||||
} // Item
|
||||
|
|
@ -77,6 +77,9 @@ Item {
|
|||
property: "model"
|
||||
value: model
|
||||
}
|
||||
Component.onCompleted: {
|
||||
console.log(`Loaded section for ${modelData['name']['Value']}.`);
|
||||
}
|
||||
}
|
||||
|
||||
//model: modConfigPage.page['optionalFileGroups']['group'] || []
|
||||
|
|
@ -89,7 +92,7 @@ Item {
|
|||
id: compSection
|
||||
ModConfigSection {
|
||||
property var model
|
||||
pageId: modConfigPage.page['uuid']
|
||||
pageId: modConfigPage.page['uuid'] || null
|
||||
sectionIndex: model.index
|
||||
sectionName: model.modelData['name']['Value']
|
||||
sectionType: model.modelData['type'] ? model.modelData['type']['Value'] : model.modelData['typeDescriptor']['type']['name']['Value']
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Item {
|
|||
|
||||
//property alias header: header
|
||||
|
||||
property alias model: modsTableModel
|
||||
property alias model: modsModel
|
||||
signal installMod(variant mod)
|
||||
signal uninstallMod(variant mod)
|
||||
signal enableMod(variant mod)
|
||||
|
|
@ -17,28 +17,45 @@ Item {
|
|||
signal reinstallMod(variant mod)
|
||||
signal deleteMod(variant mod)
|
||||
|
||||
signal writeRequested()
|
||||
|
||||
SplitView {
|
||||
id: header
|
||||
x: 0-modsList.contentX
|
||||
height: 32
|
||||
width: implicitWidth > parent.width ? implicitWidth * 2 : parent.width * 2
|
||||
|
||||
readonly property variant preferredWidths: [ 16, header.width*0.2, header.width*0.1, header.width*0.1, header.width*0.4, header.width*0.2, 16 ]
|
||||
readonly property variant preferredWidths: [ 50, 16, header.width*0.2, header.width*0.1, header.width*0.1, header.width*0.4, header.width*0.2, 16 ]
|
||||
property variant widths
|
||||
|
||||
Repeater {
|
||||
id: headerRepeater
|
||||
model: [ qsTr(''), qsTr('Name'), qsTr('Author'), qsTr('Version'), qsTr('Description'), qsTr('Website'), qsTr('') ]
|
||||
model: [ qsTr('Enabled'), qsTr('Idx'), qsTr('Name'), qsTr('Author'), qsTr('Version'), qsTr('Description'), qsTr('Website'), qsTr('') ]
|
||||
Label {
|
||||
SplitView.minimumWidth: 24
|
||||
text: modelData
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
onWidthChanged: modsList.forceLayout();
|
||||
onWidthChanged: header.updateSizes();
|
||||
font.pointSize: 10
|
||||
font.bold: true
|
||||
leftPadding: 5
|
||||
}
|
||||
}
|
||||
|
||||
function updateSizes() {
|
||||
var newWidths = [];
|
||||
for( let sidx=0; sidx < headerRepeater.count; sidx++ )
|
||||
{
|
||||
const hri = headerRepeater.itemAt(sidx);
|
||||
if( !hri )
|
||||
return;
|
||||
newWidths.push( hri.width );
|
||||
}
|
||||
|
||||
header.widths = newWidths;
|
||||
modsList.forceLayout();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
try {
|
||||
if( settings.modListColumnSizes )
|
||||
|
|
@ -60,6 +77,255 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowDelegate
|
||||
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
implicitHeight: row.height
|
||||
implicitWidth: row.width
|
||||
height: row.height
|
||||
width: row.width
|
||||
|
||||
readonly property var modent: modsModel.get(index)
|
||||
readonly property int rowIndex: index
|
||||
//readonly property int modelIndex: dmodel.items.get(rowIndex).model.index
|
||||
|
||||
property bool held: false
|
||||
property bool targeted: false
|
||||
property int previousIndex: -1
|
||||
drag.target: held ? row : undefined
|
||||
drag.axis: Drag.YAxis
|
||||
|
||||
onPressed: function(ev) { if( Qt.LeftButton === ev.button ) held = true; }
|
||||
onReleased: held = false
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: function(ev) {
|
||||
//if( Qt.RightButton === ev.button )
|
||||
cellMenu.popup();
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
/*
|
||||
Rectangle {
|
||||
border.color: 'red'
|
||||
anchors.fill: parent
|
||||
}
|
||||
*/
|
||||
function saveLoadOrder()
|
||||
{
|
||||
for( let a=0; a < modsModel.count; a++ )
|
||||
{
|
||||
const mod = modsModel.get(a);
|
||||
//console.log(`Update mod index: ${mod['modId']} moving to index ${a}!`);
|
||||
db.updateModIndex(mod['modId'], a);
|
||||
}
|
||||
|
||||
//modsTable.writeRequested();
|
||||
}
|
||||
|
||||
onHeldChanged: {
|
||||
console.log(`${dragArea.DelegateModel.itemsIndex} is ${ held ? 'held' : 'not held' }`);
|
||||
if( held )
|
||||
{
|
||||
dragArea.previousIndex = dragArea.DelegateModel.itemsIndex;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
//if( !checkOrder() && previousIndex > -1 )
|
||||
/*
|
||||
if( previousIndex > -1 )
|
||||
{
|
||||
console.log(`Reverting: ${dragArea.DelegateModel.itemsIndex} => ${previousIndex}`);
|
||||
dmodel.items.move(dragArea.DelegateModel.itemsIndex, previousIndex);
|
||||
} else
|
||||
*/
|
||||
modsModel.move(dragArea.previousIndex, dragArea.DelegateModel.itemsIndex, 1);
|
||||
|
||||
console.log(`I'd save the order now: ${dragArea.DelegateModel.itemsIndex} => ${previousIndex}`);
|
||||
saveLoadOrder();
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: row
|
||||
/*
|
||||
implicitHeight: model.column === 0 ? cellEnabled.height : cellText.implicitHeight + 15
|
||||
implicitWidth: model.column === 0 ? cellEnabled.width : cellText.implicitWidth
|
||||
*/
|
||||
|
||||
width: labelRow.implicitWidth
|
||||
height: labelRow.height
|
||||
|
||||
clip: true
|
||||
|
||||
color: (dragArea.rowIndex % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
|
||||
|
||||
Drag.active: dragArea.held
|
||||
Drag.source: dragArea
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
|
||||
Row {
|
||||
id: labelRow
|
||||
height: switchEnabled.implicitHeight * 2
|
||||
|
||||
Switch {
|
||||
id: switchEnabled
|
||||
y: height * 0.5
|
||||
width: header.widths ? header.widths[0] + 4 : 50
|
||||
enabled: dragArea.modent['installed']
|
||||
checked: dragArea.modent['enabled']
|
||||
onToggled: {
|
||||
if( !dragArea.modent['enabled'] )
|
||||
enableMod(dragArea.modent);
|
||||
else
|
||||
disableMod(dragArea.modent);
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['modId'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[1] + 4 : 100
|
||||
height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['name'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[2] + 4 : 100
|
||||
height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
id: cellText
|
||||
text: dragArea.modent ? dragArea.modent['author'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
height: parent.height
|
||||
width: header.widths ? header.widths[3] + 4 : 100
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['version'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[4] + 4 : 100
|
||||
height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['description'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
height: parent.height
|
||||
width: header.widths ? header.widths[5] + 4 : 100
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['website'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[6] + 4 : 100
|
||||
height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: cellMenu
|
||||
MenuItem {
|
||||
text: dragArea.modent && dragArea.modent['installed'] ? qsTr('Uninstall') : qsTr('Install')
|
||||
onTriggered: {
|
||||
if( dragArea.modent && dragArea.modent['installed'] )
|
||||
uninstallMod(dragArea.modent);
|
||||
else
|
||||
installMod(dragArea.modent);
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: dragArea.modent && dragArea.modent['enabled'] ? qsTr('Disable') : qsTr('Enable')
|
||||
enabled: dragArea.modent && dragArea.modent['installed'] ? true:false
|
||||
onTriggered: {
|
||||
if( !dragArea.modent['enabled'] )
|
||||
enableMod(dragArea.modent);
|
||||
else
|
||||
disableMod(dragArea.modent);
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr('Delete')
|
||||
onTriggered: deleteMod(dragArea.modent);
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr('Re-configure')
|
||||
enabled: dragArea.modent && dragArea.modent['installed'] ? true:false
|
||||
onTriggered: reinstallMod(dragArea.modent);
|
||||
}
|
||||
}
|
||||
|
||||
states: State {
|
||||
when: dragArea.held
|
||||
|
||||
ParentChange { target: row; parent: modsList }
|
||||
AnchorChanges {
|
||||
target: row
|
||||
anchors { horizontalCenter: undefined; verticalCenter: undefined }
|
||||
}
|
||||
}
|
||||
} // Rectangle
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
//enabled: !dragArea.held
|
||||
|
||||
onEntered: function(drag) {
|
||||
console.log(`Drag: ${drag.source.DelegateModel.itemsIndex} -> ${dragArea.rowIndex}`);
|
||||
dragArea.targeted = true;
|
||||
dmodel.items.move(drag.source.DelegateModel.itemsIndex, dragArea.DelegateModel.itemsIndex);
|
||||
}
|
||||
onExited: dragArea.targeted = false;
|
||||
}
|
||||
} // MouseArea
|
||||
}
|
||||
|
||||
DelegateModel {
|
||||
id: dmodel
|
||||
delegate: rowDelegate
|
||||
model: modsModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: modsModel
|
||||
//dynamicRoles: true
|
||||
function some( scb )
|
||||
{
|
||||
for( let a=0; a < count; a++ )
|
||||
{
|
||||
const ment = get(a);
|
||||
if( scb(ment) )
|
||||
return a;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors {
|
||||
|
|
@ -69,103 +335,24 @@ Item {
|
|||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
TableView {
|
||||
ListView {
|
||||
id: modsList
|
||||
|
||||
clip: true
|
||||
//boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
delegate: Rectangle {
|
||||
implicitHeight: model.column === 0 ? cellEnabled.height : cellText.implicitHeight + 15
|
||||
implicitWidth: model.column === 0 ? cellEnabled.width : cellText.implicitWidth
|
||||
clip: true
|
||||
|
||||
color: (model.row % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
|
||||
|
||||
Rectangle {
|
||||
id: cellEnabled
|
||||
visible: model.column === 0
|
||||
|
||||
readonly property var modent: modsTable.model.getRow(model.row)
|
||||
radius: 90
|
||||
color: modent ? ( modent['installed'] ? ( modsTable.model.getRow(model.row)['enabled'] ? 'green' : 'red' ) : 'gray' ) : ''
|
||||
anchors.centerIn: parent
|
||||
height: 12
|
||||
width: 12
|
||||
}
|
||||
|
||||
Label {
|
||||
id: cellText
|
||||
visible: model.column !== 0
|
||||
text: model.modelData
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
Menu {
|
||||
id: cellMenu
|
||||
readonly property var modent: modsTable.model.getRow(model.row)
|
||||
MenuItem {
|
||||
text: cellMenu.modent && cellMenu.modent['installed'] ? qsTr('Uninstall') : qsTr('Install')
|
||||
onTriggered: {
|
||||
if( cellMenu.modent['installed'] )
|
||||
uninstallMod(cellMenu.modent);
|
||||
else
|
||||
installMod(cellMenu.modent);
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: cellMenu.modent && cellMenu.modent['enabled'] ? qsTr('Disable') : qsTr('Enable')
|
||||
enabled: cellMenu.modent && cellMenu.modent['installed'] ? true:false
|
||||
onTriggered: {
|
||||
if( !cellMenu.modent['enabled'] )
|
||||
enableMod(cellMenu.modent);
|
||||
else
|
||||
disableMod(cellMenu.modent);
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr('Delete')
|
||||
onTriggered: deleteMod(cellMenu.modent);
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr('Reinstall')
|
||||
enabled: cellMenu.modent && cellMenu.modent['installed'] ? true:false
|
||||
onTriggered: reinstallMod(cellMenu.modent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: function(ev) {
|
||||
if( Qt.RightButton === ev.button )
|
||||
cellMenu.popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model: TableModel {
|
||||
id: modsTableModel
|
||||
TableModelColumn { display: "enabled" }
|
||||
TableModelColumn { display: "name" }
|
||||
TableModelColumn { display: "author" }
|
||||
TableModelColumn { display: "version" }
|
||||
TableModelColumn { display: "description" }
|
||||
TableModelColumn { display: "website" }
|
||||
//rows: modsTable.model
|
||||
}
|
||||
//delegate: rowDelegate
|
||||
|
||||
model: dmodel
|
||||
/*
|
||||
columnWidthProvider: function(col) {
|
||||
return headerRepeater.itemAt(col).width + 5;
|
||||
}
|
||||
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
*/
|
||||
move: Transition { SmoothedAnimation {} }
|
||||
} // TableView
|
||||
} // ScrollView
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ Item {
|
|||
spacing: 0
|
||||
|
||||
clip: true
|
||||
interactive: false
|
||||
//interactive: false
|
||||
|
||||
model: dmodel
|
||||
//delegate: pluginRowDelegate
|
||||
|
|
@ -175,7 +175,7 @@ Item {
|
|||
hoverEnabled: true
|
||||
|
||||
onHeldChanged: {
|
||||
//console.log(dragArea.DelegateModel.itemsIndex+" is "+(held?'held':'not held'));
|
||||
console.log(`${dragArea.DelegateModel.itemsIndex} is ${ held ? 'held' : 'not held' }`);
|
||||
if( held )
|
||||
{
|
||||
previousIndex = dragArea.DelegateModel.itemsIndex;
|
||||
|
|
@ -183,11 +183,15 @@ Item {
|
|||
}
|
||||
else
|
||||
{
|
||||
if( !checkOrder() && previousIndex > -1 )
|
||||
//if( !checkOrder() && previousIndex > -1 )
|
||||
/*
|
||||
if( previousIndex > -1 )
|
||||
{
|
||||
//console.log(`Reverting: ${dragArea.DelegateModel.itemsIndex} => ${previousIndex}`);
|
||||
console.log(`Reverting: ${dragArea.DelegateModel.itemsIndex} => ${previousIndex}`);
|
||||
dmodel.items.move(dragArea.DelegateModel.itemsIndex, previousIndex);
|
||||
} else
|
||||
*/
|
||||
pluginsModel.move(previousIndex, dragArea.DelegateModel.itemsIndex, 1);
|
||||
saveLoadOrder();
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +204,7 @@ Item {
|
|||
const midx = dmodel.items.get(a).model.index;
|
||||
const ent = pluginsModel.get(midx);
|
||||
const lcName = ent['filename'].toLowerCase();
|
||||
//console.log(`[${lcName}] => ${inMasters}`);
|
||||
console.log(`[${lcName}] midx=${midx} / vidx=${a} => ${inMasters}`);
|
||||
if( lcName.endsWith('.esm') && !inMasters )
|
||||
return false;
|
||||
else if( !lcName.endsWith('.esm') && !lcName.endsWith('.esl') )
|
||||
|
|
@ -216,6 +220,8 @@ Item {
|
|||
{
|
||||
const midx = dmodel.items.get(a).model.index;
|
||||
const ent = pluginsModel.get(midx);
|
||||
const lcName = ent['filename'].toLowerCase();
|
||||
console.log(`[${lcName}] midx=${midx} / vidx=${a}`);
|
||||
oplugins.push(ent);
|
||||
}
|
||||
pluginsTable.writeRequested(oplugins);
|
||||
|
|
@ -292,29 +298,29 @@ Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
width: header.widths ? header.widths[ 0 ] : 24
|
||||
height: 24
|
||||
width: header.widths ? header.widths[ 0 ] + 5 : 32
|
||||
height: 32
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: textCell
|
||||
property string modelText: dragArea.modent ? dragArea.modent['filename'] : '???'
|
||||
width: header.widths ? header.widths[ 1 ] : 24
|
||||
height: 24
|
||||
width: header.widths ? header.widths[ 1 ] + 5 : 24
|
||||
height: 32
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: textCell
|
||||
property string modelText: dragArea.modent ? dragArea.modent['name'] : '???'
|
||||
width: header.widths ? header.widths[ 2 ] : 24
|
||||
height: 24
|
||||
width: header.widths ? header.widths[ 2 ] + 5 : 24
|
||||
height: 32
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: textCell
|
||||
property string modelText: dragArea.modent ? dragArea.modent['description'] : '???'
|
||||
width: header.widths ? header.widths[ 3 ] : 24
|
||||
height: 24
|
||||
width: header.widths ? header.widths[ 3 ] + 5 : 24
|
||||
height: 32
|
||||
}
|
||||
} // Row
|
||||
|
||||
|
|
@ -347,7 +353,7 @@ Item {
|
|||
enabled: !dragArea.held
|
||||
|
||||
onEntered: function(drag) {
|
||||
//console.log(`Drag: ${drag.source.rowIndex} -> ${dragArea.rowIndex}`);
|
||||
console.log(`Drag: ${drag.source.DelegateModel.itemsIndex} -> ${dragArea.rowIndex}`);
|
||||
dragArea.targeted = true;
|
||||
dmodel.items.move(drag.source.DelegateModel.itemsIndex, dragArea.DelegateModel.itemsIndex);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ Dialog {
|
|||
modal: true
|
||||
clip: true
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: "#55000000" // Use whatever color/opacity you like
|
||||
}
|
||||
|
||||
onAboutToShow: {
|
||||
let pages = [ {'name':qsTr('General')} ];
|
||||
gameDefinitions.forEach( g => pages.push(g) );
|
||||
|
|
@ -31,7 +35,9 @@ Dialog {
|
|||
ent.modspath = sobj.modsPath || `/DATA/SteamLibrary/steamapps/common/${g['gamedir']}/Quickmods`;
|
||||
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.userpath = sobj.userDataPath || `/DATA/SteamLibrary/steamapps/compatdata/${g['steamid']}/pfx/drive_c/users/steamuser`;
|
||||
ent.dbpath = sobj.dbPath || `/DATA/modding/${g['gamedir']}/Quickmod.sqlite`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +53,9 @@ Dialog {
|
|||
sobj.modsPath = ent.modspath;
|
||||
sobj.modStagingPath = ent.modstagingpath;
|
||||
sobj.gamePath = ent.gamepath;
|
||||
sobj.vfsPath = ent.vfsPath;
|
||||
sobj.userDataPath = ent.userpath;
|
||||
sobj.dbPath = ent.dbpath;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +202,9 @@ Dialog {
|
|||
property alias modspath: modsPath.text
|
||||
property alias modstagingpath: modStagingPath.text
|
||||
property alias gamepath: gamePath.text
|
||||
property alias vfsPath: vfsPathEntry.text
|
||||
property alias userpath: userDataPath.text
|
||||
property alias dbpath: dbPath.text
|
||||
|
||||
property string steamid: modelData['steamid']
|
||||
property string gamename: modelData['name']
|
||||
|
|
@ -232,7 +242,12 @@ Dialog {
|
|||
}
|
||||
Label {
|
||||
height: gameItem.rowHeight
|
||||
text: qsTr('Game Directory:')
|
||||
text: qsTr('Real Game Directory:')
|
||||
enabled: cbEnabled.checked
|
||||
}
|
||||
Label {
|
||||
height: gameItem.rowHeight
|
||||
text: qsTr('VFS Mountpoint:')
|
||||
enabled: cbEnabled.checked
|
||||
}
|
||||
Label {
|
||||
|
|
@ -240,6 +255,10 @@ Dialog {
|
|||
text: qsTr('User Data Directory:')
|
||||
enabled: cbEnabled.checked
|
||||
}
|
||||
Label {
|
||||
height: gameItem.rowHeight
|
||||
text: qsTr('Quickmod Database File:')
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
|
|
@ -278,7 +297,16 @@ Dialog {
|
|||
width: columnEdits.width
|
||||
|
||||
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)
|
||||
}
|
||||
TextField {
|
||||
id: vfsPathEntry
|
||||
enabled: cbEnabled.checked
|
||||
height: gameItem.rowHeight
|
||||
width: columnEdits.width
|
||||
|
||||
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: userDataPath
|
||||
|
|
@ -289,6 +317,15 @@ Dialog {
|
|||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr('This is the root directory of wherever your data and preferences are stored.\n\nEg: /DATA/SteamLibrary/steamapps/compatdata/%1/pfx/drive_c/users/steamuser').arg(steamid)
|
||||
}
|
||||
TextField {
|
||||
id: dbPath
|
||||
enabled: cbEnabled.checked
|
||||
height: gameItem.rowHeight
|
||||
width: columnEdits.width
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr('Path to Quickmod.sqlite database.\n\nEg: /DATA/modding/%1/Quickmod.sqlite').arg(gamename)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
|
|
@ -339,6 +376,18 @@ Dialog {
|
|||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr('Where the game is installed.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1').arg(gamename)
|
||||
}
|
||||
Button {
|
||||
text: qsTr('Browse...')
|
||||
height: gameItem.rowHeight
|
||||
enabled: cbEnabled.checked
|
||||
onClicked: {
|
||||
vfsPathDialogue.currentFolder = 'file://' + vfsPathEntry.text;
|
||||
vfsPathDialogue.open();
|
||||
}
|
||||
|
||||
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 {
|
||||
height: gameItem.rowHeight
|
||||
text: qsTr('Browse...')
|
||||
|
|
@ -351,6 +400,18 @@ Dialog {
|
|||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr('This is the root directory of wherever your data and preferences are stored.\n\nEg: /DATA/SteamLibrary/steamapps/compatdata/%1/pfx/drive_c/users/steamuser').arg(steamid)
|
||||
}
|
||||
Button {
|
||||
height: gameItem.rowHeight
|
||||
text: qsTr('Browse...')
|
||||
enabled: cbEnabled.checked
|
||||
onClicked: {
|
||||
dbPathDialogue.currentFile = 'file://' + userDataPath.text;
|
||||
dbPathDialogue.open();
|
||||
}
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr('Path to Quickmod.sqlite database.\n\nEg: /DATA/modding/%1/Quickmod.sqlite').arg(gamename)
|
||||
}
|
||||
} // Column
|
||||
|
||||
Platform.FolderDialog {
|
||||
|
|
@ -380,6 +441,15 @@ Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
Platform.FolderDialog {
|
||||
id: vfsPathDialogue
|
||||
//visible: false
|
||||
title: qsTr("Select the game VFS path...")
|
||||
onAccepted: {
|
||||
vfsPathEntry.text = (''+folder).substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
Platform.FolderDialog {
|
||||
id: userDataPathDialogue
|
||||
|
||||
|
|
@ -389,6 +459,17 @@ Dialog {
|
|||
userDataPath.text = (''+folder).substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
Platform.FileDialog {
|
||||
id: dbPathDialogue
|
||||
|
||||
//visible: false
|
||||
title: qsTr("Select the Quickmod.sqlite database file...")
|
||||
nameFilters: ["Sqlite3 Database File (*.sqlite)"]
|
||||
onAccepted: {
|
||||
dbPath.text = (''+currentFile).substring(7);
|
||||
}
|
||||
}
|
||||
} // Item
|
||||
} // Repeater
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,13 @@ Dialog {
|
|||
closePolicy: Popup.NoAutoClose
|
||||
clip: true
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: "#55000000" // Use whatever color/opacity you like
|
||||
}
|
||||
|
||||
property alias text: label.text
|
||||
property int to: 0
|
||||
property int value: 0
|
||||
property real to: 0
|
||||
property real value: 0
|
||||
property bool showCancel: true
|
||||
property variant queue: []
|
||||
|
||||
|
|
@ -41,7 +45,7 @@ Dialog {
|
|||
visible: !progress.indeterminate
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: parseInt(progressPage.value) + " / " + parseInt(progressPage.to)
|
||||
text: progressPage.value.toFixed(0) + " / " + progressPage.to.toFixed(0)
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
|
|
|
|||
10
qml/game.js
10
qml/game.js
|
|
@ -10,7 +10,8 @@ function loadForGame()
|
|||
if( !ent )
|
||||
return;
|
||||
|
||||
const dbpath = `${ent['paths']['gamePath']}/Quickmod.sqlite`;
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
const dbpath = sobj.dbPath;
|
||||
db.open(dbpath);
|
||||
|
||||
//modMasterList = db.getMods();
|
||||
|
|
@ -20,6 +21,13 @@ function loadForGame()
|
|||
mainWin.title = qsTr('Quickmod - %1').arg(currentGame);
|
||||
|
||||
Plugins.readPlugins();
|
||||
|
||||
launchPage.currentGame = currentGame;
|
||||
launchPage.launchOptions = [
|
||||
{ 'name':'Custom', 'valid':true, 'custom':true, 'launch':sobj['customLaunchString'] },
|
||||
{ 'name':'Proton', 'valid':true, 'custom':false, 'launch':`protontricks-launch --appid ${currentGameEntry['steamid']} "${ent['paths']['gamePath']}/${currentGameEntry['gameexe']}"` },
|
||||
{ 'name':'WINE', 'valid':true, 'custom':false, 'launch':`/usr/bin/wine "${ent['paths']['gamePath']}/${currentGameEntry['gameexe']}"` },
|
||||
];
|
||||
}
|
||||
|
||||
function gameEntryByName(gamename)
|
||||
|
|
|
|||
117
qml/main.qml
117
qml/main.qml
|
|
@ -26,6 +26,8 @@ ApplicationWindow {
|
|||
|
||||
function installFromFilesystem(filePath, gamecode, nexusModId, nexusFileId)
|
||||
{
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
File.mkdir(sobj.modsPath, true);
|
||||
Mods.installFromFilesystem(filePath, function() {}, gamecode, nexusModId, nexusFileId);
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +84,7 @@ ApplicationWindow {
|
|||
id: gamesMenuRepeater
|
||||
model: []
|
||||
RadioButton {
|
||||
enabled: !cbVFS.checked
|
||||
rightPadding: 5
|
||||
leftPadding: 5
|
||||
topPadding: 5
|
||||
|
|
@ -117,24 +120,67 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
TabBar {
|
||||
id: tabSection
|
||||
RowLayout {
|
||||
id: tabBar
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr('Mods')
|
||||
TabBar {
|
||||
id: tabSection
|
||||
Layout.fillWidth: true
|
||||
TabButton {
|
||||
text: qsTr('Mods')
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr('Plugins')
|
||||
}
|
||||
}
|
||||
ComboBox {
|
||||
id: profilesMenu
|
||||
enabled: !cbVFS.checked
|
||||
Layout.preferredWidth: tabBar.width * 0.33
|
||||
textRole: 'name'
|
||||
valueRole: 'id'
|
||||
model: [ 'Profile...' ]
|
||||
onActivated: function(idx) {
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
sobj.profileId = currentValue;
|
||||
mainWin.profileChanged();
|
||||
Game.loadForGame();
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: cbVFS
|
||||
text: qsTr('VFS')
|
||||
icon.name: 'drive-harddisk'
|
||||
onToggled: {
|
||||
if( !checked )
|
||||
FUSEManager.unmount();
|
||||
else
|
||||
{
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: qsTr('Plugins / Load Order')
|
||||
text: qsTr('Launch Game')
|
||||
icon.name: 'system-run'
|
||||
enabled: cbVFS.checked
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
anchors {
|
||||
top: tabSection.bottom
|
||||
top: tabBar.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
|
|
@ -144,6 +190,7 @@ ApplicationWindow {
|
|||
|
||||
ModsTable {
|
||||
id: modTable
|
||||
enabled: !cbVFS.checked
|
||||
//model: mainWin.modMasterList
|
||||
onInstallMod: function(mod) { Mods.installMod(mod); }
|
||||
onUninstallMod: function(mod) { Mods.uninstallMod(mod); }
|
||||
|
|
@ -151,9 +198,15 @@ ApplicationWindow {
|
|||
onDisableMod: function(mod) { Mods.disableMod(mod); }
|
||||
onReinstallMod: function(mod) { Mods.reinstallMod(mod); }
|
||||
onDeleteMod: function(mod) { Mods.deleteMod(mod); }
|
||||
|
||||
onWriteRequested: function(plugins) {
|
||||
Plugins.writePlugins(plugins);
|
||||
Plugins.writeLoadOrder(plugins);
|
||||
}
|
||||
}
|
||||
PluginsTable {
|
||||
id: pluginsTable
|
||||
enabled: !cbVFS.checked
|
||||
|
||||
onEnableMod: function(mod) { Plugins.enableMod(mod); }
|
||||
onDisableMod: function(mod) { Plugins.disableMod(mod); }
|
||||
|
|
@ -163,6 +216,9 @@ ApplicationWindow {
|
|||
Plugins.writeLoadOrder(plugins);
|
||||
}
|
||||
}
|
||||
LaunchPage {
|
||||
id: launchPage
|
||||
}
|
||||
}
|
||||
|
||||
Settings {
|
||||
|
|
@ -197,6 +253,8 @@ ApplicationWindow {
|
|||
target: NXMHandler
|
||||
function onDownloadRequested(path)
|
||||
{
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
File.mkdir(sobj.modsPath, true);
|
||||
Downloader.downloadFile(path);
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +282,7 @@ ApplicationWindow {
|
|||
onRejected: {
|
||||
console.log("Canceled");
|
||||
}
|
||||
options: Platform.FileDialog.ReadOnly
|
||||
}
|
||||
|
||||
property alias currentGame: settings.currentGame
|
||||
|
|
@ -280,9 +339,23 @@ ApplicationWindow {
|
|||
console.log('`---');
|
||||
gamesMenuRepeater.model = gamesMenuOptions;
|
||||
Game.loadForGame();
|
||||
profileChanged();
|
||||
}
|
||||
|
||||
onCurrentGameChanged: if( loadComplete ) Game.loadForGame();
|
||||
function profileChanged()
|
||||
{
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
profilesMenu.model = db.getProfiles();
|
||||
profilesMenu.currentIndex = profilesMenu.indexOfValue(sobj.profileId);
|
||||
}
|
||||
|
||||
onCurrentGameChanged: {
|
||||
if( !loadComplete )
|
||||
return;
|
||||
|
||||
Game.loadForGame();
|
||||
profileChanged();
|
||||
}
|
||||
|
||||
PreferencesDialogue {
|
||||
id: preferencesDialogue
|
||||
|
|
@ -306,6 +379,8 @@ ApplicationWindow {
|
|||
AboutDialogue {
|
||||
id: aboutDialogue
|
||||
anchors.centerIn: parent
|
||||
width: parent.width * 0.75
|
||||
height: parent.height * 0.90
|
||||
}
|
||||
|
||||
GameSettings {
|
||||
|
|
@ -352,10 +427,28 @@ ApplicationWindow {
|
|||
|
||||
property var modMasterList: []
|
||||
onModMasterListChanged: {
|
||||
/*
|
||||
modTable.model.clear();
|
||||
for( let ent of modMasterList ) {
|
||||
modTable.model.appendRow(ent);
|
||||
modTable.model.append(ent);
|
||||
}
|
||||
*/
|
||||
for( let a=0; a < modMasterList.length; a++ )
|
||||
{
|
||||
let nent = modMasterList[a];
|
||||
modTable.model.set(a, nent);
|
||||
/*
|
||||
let oent = modTable.model.get(a);
|
||||
for( k of Object.keys(nent) )
|
||||
{
|
||||
if( oent[k] != nent[k] )
|
||||
modTable.model.set(a, nent);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
if( modMasterList.length < modTable.model.count )
|
||||
modTable.model.remove(modMasterList.length, modTable.model.count-modMasterList.length);
|
||||
}
|
||||
|
||||
readonly property variant gameDefinitions: [
|
||||
|
|
@ -363,6 +456,7 @@ ApplicationWindow {
|
|||
'steamid': '611670',
|
||||
'name': 'Skyrim VR',
|
||||
'gamedir': 'SkyrimVR',
|
||||
'gameexe': 'SkyrimVR.exe',
|
||||
'appdir': 'AppData/Local/Skyrim VR',
|
||||
'confdir': 'Documents/My Games/Skyrim VR',
|
||||
'ini': 'Skyrim.ini',
|
||||
|
|
@ -393,6 +487,7 @@ ApplicationWindow {
|
|||
'steamid': '489830',
|
||||
'name': 'Skyrim Special Edition',
|
||||
'gamedir': 'Skyrim Special Edition',
|
||||
'gameexe': 'SkyrimSE.exe',
|
||||
'appdir': 'AppData/Local/Skyrim Special Edition',
|
||||
'confdir': 'Documents/My Games/Skyrim Special Edition',
|
||||
'ini': 'Skyrim.ini',
|
||||
|
|
@ -422,6 +517,7 @@ ApplicationWindow {
|
|||
'steamid': '611660',
|
||||
'name': 'Fallout 4 VR',
|
||||
'gamedir': 'Fallout 4 VR',
|
||||
'gameexe': 'Fallout4VR.exe',
|
||||
'appdir': 'AppData/Local/Fallout4VR',
|
||||
'confdir': 'Documents/My Games/Fallout4VR',
|
||||
'ini': 'Fallout4.ini',
|
||||
|
|
@ -449,10 +545,11 @@ ApplicationWindow {
|
|||
{
|
||||
'steamid': '377160',
|
||||
'name': 'Fallout 4',
|
||||
'gameexe': 'Fallout4.exe',
|
||||
'gamedir': 'Fallout 4',
|
||||
'appdir': 'AppData/Local/Fallout4',
|
||||
'confdir': 'Documents/My Games/Fallout4',
|
||||
'ini': 'Fallout4.INI',
|
||||
'ini': 'Fallout4.ini',
|
||||
'datadir': 'Data',
|
||||
'plugins': 'Plugins.txt',
|
||||
'loadorder': 'DLCList.txt',
|
||||
|
|
@ -468,6 +565,7 @@ ApplicationWindow {
|
|||
'enableMods': function() {
|
||||
const adroot = `${this['paths']['userData']}/${this['confdir']}`;
|
||||
const inipath = `${adroot}/${this['ini']}`;
|
||||
console.log("Updating ini: "+inipath);
|
||||
Utils.configSet(inipath, 'Archive', 'bInvalidateOlderFiles', '1');
|
||||
Utils.configSet(inipath, 'Archive', 'sResourceDataDirsFinal', '');
|
||||
return true;
|
||||
|
|
@ -476,6 +574,7 @@ ApplicationWindow {
|
|||
{
|
||||
'steamid': '22380',
|
||||
'name': 'Fallout: New Vegas',
|
||||
'gameexe': 'FalloutNV.exe',
|
||||
'gamedir': 'Fallout New Vegas',
|
||||
'appdir': 'AppData/Local/FalloutNV',
|
||||
'confdir': 'Documents/my games/falloutnv',
|
||||
|
|
|
|||
169
qml/mods.js
169
qml/mods.js
|
|
@ -7,6 +7,7 @@ function manifestFromFomod(filepath, cb)
|
|||
if( filelist.length === 0 )
|
||||
{
|
||||
console.log("Empty file listing for archive, it's ... probably corrupted.");
|
||||
delete a;
|
||||
return false;
|
||||
}
|
||||
console.log("CONTENTS: "+JSON.stringify(filelist,null,2));
|
||||
|
|
@ -93,11 +94,12 @@ function manifestFromFomod(filepath, cb)
|
|||
}, function(sofar, total, latestSource, latestDest) {
|
||||
console.log(`Progress: [${sofar} / ${total}]: ${latestSource}\t => ${latestDest}`);
|
||||
});
|
||||
//File.extractBatch(filepath, toExtract);
|
||||
File.extractBatch(filepath, toExtract);
|
||||
}
|
||||
else
|
||||
finish();
|
||||
} );
|
||||
delete a;
|
||||
}
|
||||
|
||||
function installFromFilesystem(filepath, cb, gamecode, nexusModId, nexusFileId)
|
||||
|
|
@ -125,22 +127,33 @@ function installFromFilesystem(filepath, cb, gamecode, nexusModId, nexusFileId)
|
|||
|
||||
let ncb = function(result, filepath, ent) { cb(addMod(result, ent)) };
|
||||
|
||||
if( !result['config'] || !result['info'] )
|
||||
{
|
||||
if( result['info'] )
|
||||
try {
|
||||
if( !result['config'] || !result['info'] )
|
||||
{
|
||||
if( result['info']['fomod']['Name']['Characters'] )
|
||||
ent['name'] = result['info']['fomod']['Name']['Characters'];
|
||||
if( result['info'] && result['info']['fomod'] )
|
||||
{
|
||||
if( result['info']['fomod']['Name'] && result['info']['fomod']['Name']['Characters'] )
|
||||
ent['name'] = result['info']['fomod']['Name']['Characters'];
|
||||
|
||||
ent['author'] = result['info']['fomod']['Author']['Characters'];
|
||||
ent['version'] = result['info']['fomod']['Version']['Characters'];
|
||||
ent['description'] = result['info']['fomod']['Description']['Characters'];
|
||||
}
|
||||
if( result['info']['fomod']['Author'] && result['info']['fomod']['Author']['Characters'] )
|
||||
ent['author'] = result['info']['fomod']['Author']['Characters'];
|
||||
|
||||
statusBar.text = qsTr("Added basic mod \"%1\".").arg(filepath);
|
||||
ncb = function(result, filepath, ent) { cb( addMod2(filepath, ent)) };
|
||||
} else
|
||||
statusBar.text = qsTr("Adding mod \"%1\"...").arg(filepath);
|
||||
if( result['info']['fomod']['Version'] && result['info']['fomod']['Version']['Characters'] )
|
||||
ent['version'] = result['info']['fomod']['Version']['Characters'];
|
||||
|
||||
if( result['info']['fomod']['Description'] && result['info']['fomod']['Description']['Characters'] )
|
||||
ent['description'] = result['info']['fomod']['Description']['Characters'];
|
||||
}
|
||||
|
||||
statusBar.text = qsTr("Added basic mod \"%1\".").arg(filepath);
|
||||
ncb = function(result, filepath, ent) { cb( addMod2(filepath, ent)) };
|
||||
} else
|
||||
statusBar.text = qsTr("Adding mod \"%1\"...").arg(filepath);
|
||||
} catch(e) {
|
||||
statusBar.text = qsTr("Failed to add mod \"%1\" due to processing error: %2").arg(filepath).arg(e);
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if( gamecode && nexusModId && nexusFileId )
|
||||
{
|
||||
|
|
@ -223,13 +236,15 @@ function enableMod(mod)
|
|||
|
||||
statusBar.text = qsTr('Enabled "%1".').arg(mod['name']);
|
||||
mod['enabled'] = true;
|
||||
db.updateMod(mod);
|
||||
//db.updateMod(mod);
|
||||
db.updateModActive(mod['modId'], sobj.profileId, true);
|
||||
rereadMods();
|
||||
}
|
||||
|
||||
function rereadMods()
|
||||
{
|
||||
modMasterList = db.getMods();
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
modMasterList = db.getMods(sobj.profileId);
|
||||
}
|
||||
|
||||
function removePlugin(mod)
|
||||
|
|
@ -271,7 +286,8 @@ function disableMod(mod)
|
|||
statusBar.text = qsTr('Disabled "%1".').arg(mod['name']);
|
||||
|
||||
mod['enabled'] = false;
|
||||
db.updateMod(mod);
|
||||
//db.updateMod(mod);
|
||||
db.updateModActive(mod['modId'], sobj.profileId, false);
|
||||
rereadMods();
|
||||
}
|
||||
|
||||
|
|
@ -330,11 +346,11 @@ function addMod2(archive, ent)
|
|||
console.log(`Creating directory "${destPath}"...`);
|
||||
File.mkdir(destPath);
|
||||
|
||||
ent['filename'] = baseName;
|
||||
|
||||
let destFile = destPath+'/'+baseName;
|
||||
destFile = destFile.replace(/\.\./g, '');
|
||||
|
||||
ent['filename'] = destFile;
|
||||
|
||||
console.log(`Copying "${archive}" to "${destFile}"...`);
|
||||
File.copy(archive, destFile);
|
||||
|
||||
|
|
@ -361,7 +377,8 @@ function installMod(mod, cb)
|
|||
|
||||
console.log(`Installmod: ${JSON.stringify(mod,null,2)}`);
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
const filepath = sobj.modsPath + '/' + mod['filename'];
|
||||
//const filepath = sobj.modsPath + '/' + mod['filename'];
|
||||
const filepath = mod['filename'];
|
||||
|
||||
console.log(`Attempting to read manifest from fomod: ${filepath}`);
|
||||
manifestFromFomod(filepath, function(result) {
|
||||
|
|
@ -393,9 +410,10 @@ function uninstallMod(mod)
|
|||
|
||||
let toRemove = [];
|
||||
files.forEach( function(f) {
|
||||
const dpath = sobj.gamePath + '/' + currentGameEntry['datadir'] + '/' + f['dest'];
|
||||
const dpath = sobj.gamePath + '/' + f['dest'];
|
||||
console.log(`Deleting file "${f['dest']}" at "${dpath}"...`);
|
||||
File.rm(dpath, true);
|
||||
// HACK: not actually doing file stuff right now~
|
||||
//File.rm(dpath, true);
|
||||
toRemove.push( f['fileId'] );
|
||||
//db.removeFile(f['fileId']);
|
||||
} );
|
||||
|
|
@ -417,7 +435,8 @@ function deleteMod(mod)
|
|||
uninstallMod(mod);
|
||||
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
const filepath = sobj.modsPath + '/' + mod['filename'];
|
||||
//const filepath = sobj.modsPath + '/' + mod['filename'];
|
||||
const filepath = mod['filename'];
|
||||
File.rm(filepath);
|
||||
|
||||
db.removeMod(mod['modId']);
|
||||
|
|
@ -429,7 +448,8 @@ function deleteMod(mod)
|
|||
function installFancyMod(mod, files, folders, modinfo, flags)
|
||||
{
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
const filepath = sobj.modsPath + '/' + mod['filename'];
|
||||
//const filepath = sobj.modsPath + '/' + mod['filename'];
|
||||
const filepath = mod['filename'];
|
||||
|
||||
const relative = modinfo['relative'];
|
||||
|
||||
|
|
@ -512,6 +532,29 @@ function installFancyMod(mod, files, folders, modinfo, flags)
|
|||
console.log(`Adding required folders: ${JSON.stringify(rfi['folder'])}`);
|
||||
}
|
||||
|
||||
let inDataDir = true;
|
||||
for( let c=0; c < files.length; c++ )
|
||||
{
|
||||
let f = files[c];
|
||||
let parts = f['dest'].toLowerCase().split(/\//g);
|
||||
if( parts[0] === currentGameEntry['datadir'].toLowerCase() ) {
|
||||
inDataDir = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( inDataDir ) {
|
||||
for( let c=0; c < folders.length; c++ )
|
||||
{
|
||||
let f = folders[c];
|
||||
let parts = f['dest'].toLowerCase().split(/\//g);
|
||||
if( parts[0] === currentGameEntry['datadir'].toLowerCase() ) {
|
||||
inDataDir = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( !mod['archiveObject'] )
|
||||
mod['archiveObject'] = File.archive(filepath);
|
||||
|
||||
|
|
@ -521,6 +564,9 @@ function installFancyMod(mod, files, folders, modinfo, flags)
|
|||
for( let c=0; c < files.length; c++ )
|
||||
{
|
||||
let f = files[c];
|
||||
if( inDataDir )
|
||||
f['dest'] = currentGameEntry['datadir'] + '/' + f['dest'];
|
||||
|
||||
const sourceParts = f['source'].split(/\//g);
|
||||
let destParts = f['dest'].split(/\//g);
|
||||
|
||||
|
|
@ -532,7 +578,7 @@ function installFancyMod(mod, files, folders, modinfo, flags)
|
|||
fdest = sourceParts[ sourceParts.length-1 ];
|
||||
|
||||
const src = `${relative.length > 0 ? relative+"/" : ""}${f['source']}`;
|
||||
const dpath = `${sobj.gamePath}/${currentGameEntry['datadir']}/${fdest}`;
|
||||
const dpath = `${sobj.gamePath}/${fdest}`;
|
||||
console.log(`1: Extract "${src}" -> "${dpath}"`);
|
||||
|
||||
if( !filelist.find( e => { return e['pathname'].toLowerCase() === src.toLowerCase() && e['type'] === 'file' } ) )
|
||||
|
|
@ -552,6 +598,9 @@ function installFancyMod(mod, files, folders, modinfo, flags)
|
|||
for( let c=0; c < folders.length; c++ )
|
||||
{
|
||||
let f = folders[c];
|
||||
if( inDataDir )
|
||||
f['dest'] = currentGameEntry['datadir'] + '/' + f['dest'];
|
||||
|
||||
const fdest = f['dest'].replace(/\/\//g, '/').replace(/\\/g, '/');
|
||||
const src = `${relative.length > 0 ? relative+"/" : ""}${f['source']}`.replace(/\\/g, '/');
|
||||
const dpath = `${sobj.gamePath}/${currentGameEntry['datadir']}/${fdest}`;
|
||||
|
|
@ -600,8 +649,12 @@ function installFancyMod(mod, files, folders, modinfo, flags)
|
|||
console.log(` *** No folders found for extraction for path: ${src}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* HACK: Not needed for overlay FuseMounter:
|
||||
*
|
||||
// Avoid overwriting:
|
||||
const overwrites = checkOverwrites(fileMap);
|
||||
*/
|
||||
|
||||
console.log(`DB committing files: ${JSON.stringify(toCommit,null,2)}`);
|
||||
|
||||
|
|
@ -617,6 +670,9 @@ function installFancyMod(mod, files, folders, modinfo, flags)
|
|||
rereadMods();
|
||||
}
|
||||
|
||||
// HACK: Don't -actually- extract anything:
|
||||
return finished();
|
||||
|
||||
if( Object.keys(fileMap).length > 0 )
|
||||
extractFileMap(mod, fileMap, finished);
|
||||
else
|
||||
|
|
@ -660,7 +716,8 @@ function extractFileMap(mod, fileMap, successCallback)
|
|||
function installBasicMod(mod)
|
||||
{
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
const filepath = sobj.modsPath + '/' + mod['filename'];
|
||||
//const filepath = sobj.modsPath + '/' + mod['filename'];
|
||||
const filepath = mod['filename'];
|
||||
|
||||
if( !mod['archiveObject'] )
|
||||
mod['archiveObject'] = File.archive(filepath);
|
||||
|
|
@ -668,13 +725,37 @@ function installBasicMod(mod)
|
|||
mod['archiveObject'].list( function(result, filelist) {
|
||||
// If everything is in a subdirectory, the SAME subdirectory, the mod creator is killing us:
|
||||
let topdirs = [];
|
||||
let hasDataDir = false;
|
||||
let poptop = false;
|
||||
|
||||
filelist.forEach( function(e) {
|
||||
const p = e['pathname'].toLowerCase().split(/\//g);
|
||||
if( !topdirs.includes(p[0]) )
|
||||
topdirs.push(p[0]);
|
||||
if( p[0] === currentGameEntry['datadir'].toLowerCase() )
|
||||
hasDataDir = true;
|
||||
} );
|
||||
|
||||
let inDataDir = true;
|
||||
if( topdirs.length === 1 && !hasDataDir )
|
||||
{
|
||||
for( let a=0; a < filelist.length; a++ )
|
||||
{
|
||||
let e = filelist[a];
|
||||
const p = e['pathname'].toLowerCase().split(/\//g);
|
||||
console.log(`Looking for datadir (${currentGameEntry['datadir'].toLowerCase()}) in this mod (at ${p[1]}...`);
|
||||
if( p[1] === currentGameEntry['datadir'].toLowerCase() )
|
||||
{
|
||||
// Ex: "FNIS Behavior SE 7.6/Data/tools/GenerateFNIS_for_Users/GenerateFNISforUsers.exe"
|
||||
// -> "Data/tools/GenerateFNIS_for_Users/GenerateFNISforUsers.exe"
|
||||
console.log("Mod author is possibly murdering me, Smalls. Compensating...");
|
||||
poptop = true;
|
||||
hasDataDir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
filelist.forEach( function(e) {
|
||||
const p = e['pathname'].toLowerCase().split(/\//g);
|
||||
//console.log(`[TD=${topdirs.length}] Comparing "${p[0]}" with "${currentGameEntry['datadir'].toLowerCase()}"...`);
|
||||
|
|
@ -683,7 +764,7 @@ function installBasicMod(mod)
|
|||
else if( inDataDir && topdirs.length === 1 && p[1] === currentGameEntry['datadir'].toLowerCase() )
|
||||
inDataDir = false;
|
||||
} );
|
||||
|
||||
*/
|
||||
let fileMap = {};
|
||||
let toCommit = [];
|
||||
for( let a=0; a < filelist.length; a++ )
|
||||
|
|
@ -696,16 +777,26 @@ function installBasicMod(mod)
|
|||
const fpath = f['pathname'].replace(/\/\//g, '/');
|
||||
let fdpath = f['pathname'];
|
||||
|
||||
if( topdirs.length === 1 )
|
||||
if( poptop )
|
||||
{
|
||||
// Ex: "FNIS Behavior SE 7.6/Data/tools/GenerateFNIS_for_Users/GenerateFNISforUsers.exe"
|
||||
// -> "Data/tools/GenerateFNIS_for_Users/GenerateFNISforUsers.exe"
|
||||
console.log("Mod author is possibly murdering me, Smalls. Compensating...");
|
||||
parts.shift();
|
||||
fdpath = parts.join('/');
|
||||
console.log("Popping-top, mod author is spanking me, Smalls.");
|
||||
}
|
||||
|
||||
if( !hasDataDir )
|
||||
{
|
||||
// Ex: "MCM/Config/Snappy_HouseK/config.json"
|
||||
// -> "Data/MCM/Config/Snappy_HouseK/config.json"
|
||||
console.log("Mod author is possibly killing me, Smalls. Compensating...");
|
||||
parts.unshift(currentGameEntry['datadir'].toLowerCase());
|
||||
|
||||
fdpath = parts.join('/');
|
||||
}
|
||||
|
||||
/*
|
||||
if( !inDataDir )
|
||||
//if( parts[0].toLowerCase() === currentGameEntry['datadir'].toLowerCase() )
|
||||
{
|
||||
|
|
@ -719,15 +810,15 @@ function installBasicMod(mod)
|
|||
|
||||
fdpath = parts.join('/');
|
||||
}
|
||||
|
||||
*/
|
||||
if( fdpath.length === 0 )
|
||||
fdpath = parts.pop();
|
||||
if( !fdpath )
|
||||
fdpath = f['pathname'];
|
||||
|
||||
console.log(`PATH: "${sobj.gamePath}" + '/' + "${currentGameEntry['datadir']}" + '/' + "${fdpath}"`);
|
||||
console.log(`PATH: "${sobj.gamePath}" + '/' + "${fdpath}"`);
|
||||
|
||||
const dpath = (sobj.gamePath + '/' + currentGameEntry['datadir'] + '/' + fdpath).replace(/\/\//g, '/');
|
||||
const dpath = (sobj.gamePath + '/' + fdpath).replace(/\/\//g, '/');
|
||||
console.log(`Extract "${f['pathname']}" -> "${dpath}"`);
|
||||
fileMap[fpath] = dpath;
|
||||
//File.extractSourceDest(filepath, f['path'], dpath);
|
||||
|
|
@ -745,6 +836,9 @@ function installBasicMod(mod)
|
|||
rereadMods();
|
||||
}
|
||||
|
||||
// HACK: Don't -actually- extract anything:
|
||||
return finished();
|
||||
|
||||
if( Object.keys(fileMap).length > 0 )
|
||||
extractFileMap(mod, fileMap, finished);
|
||||
else
|
||||
|
|
@ -768,11 +862,14 @@ function configureMod(mod, modinfo)
|
|||
page['files'] = {};
|
||||
if( !( page['optionalFileGroups']['group'] instanceof Array ) )
|
||||
page['optionalFileGroups']['group'] = [ page['optionalFileGroups']['group'] ];
|
||||
page['optionalFileGroups']['group'].forEach( group => {
|
||||
|
||||
for( let y=0; y < page['optionalFileGroups']['group'].length; y++ ) {
|
||||
let group = page['optionalFileGroups']['group'][y];
|
||||
if( !( group['plugins']['plugin'] instanceof Array ) )
|
||||
group['plugins']['plugin'] = [ group['plugins']['plugin'] ];
|
||||
} );
|
||||
steps[x] = page;
|
||||
page['optionalFileGroups']['group'][y] = group;
|
||||
}
|
||||
steps[x] = page;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(steps,null,2));
|
||||
|
|
|
|||
134
qml/plugins.js
134
qml/plugins.js
|
|
@ -63,7 +63,7 @@ function readPlugins()
|
|||
plugins.push(nent);
|
||||
}
|
||||
|
||||
plugins = scanForLoose(plugins);
|
||||
plugins = consolidate(plugins);
|
||||
|
||||
plugins = updatePluginCache(plugins);
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ function readPlugins()
|
|||
const lcfname = ent['filename'].toLowerCase();
|
||||
|
||||
inorder.push(lcfname);
|
||||
|
||||
/*
|
||||
if( ent['plugin'] )
|
||||
{
|
||||
if( ent['plugin']['masters'] )
|
||||
|
|
@ -94,7 +94,7 @@ function readPlugins()
|
|||
if( ent['plugin']['description'] )
|
||||
ent['description'] = ent['plugin']['description'];
|
||||
}
|
||||
|
||||
*/
|
||||
//console.log(`${ent['filename']}: ${JSON.stringify(ent['plugin'],null,2)}`);
|
||||
} );
|
||||
|
||||
|
|
@ -148,40 +148,131 @@ function writePlugins(plugins)
|
|||
|
||||
return res;
|
||||
}
|
||||
/*
|
||||
function _pluginSort(a, b, fname)
|
||||
{
|
||||
const all = a[fname].charAt( a[fname].length-1 ).toLowerCase();
|
||||
const bll = b[fname].charAt( b[fname].length-1 ).toLowerCase();
|
||||
|
||||
function scanForLoose(plugins)
|
||||
if( all === bll )
|
||||
return 0;
|
||||
|
||||
if( 'm' === all )
|
||||
return -1;
|
||||
else if( 'm' === bll )
|
||||
return 1;
|
||||
else if( 'p' === all )
|
||||
return 1;
|
||||
else if( 'p' === bll )
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
*/
|
||||
function sporkSort(arr, fname)
|
||||
{
|
||||
let out = [];
|
||||
let addM = [];
|
||||
let addL = [];
|
||||
let addP = [];
|
||||
for( let i=0; i < arr.length; i++ )
|
||||
{
|
||||
let ent = arr[i];
|
||||
let ch = ent[fname].charAt( ent[fname].length-1 ).toLowerCase();
|
||||
switch( ch )
|
||||
{
|
||||
case 'm':
|
||||
addM.push(ent);
|
||||
break;
|
||||
case 'l':
|
||||
addL.push(ent);
|
||||
break;
|
||||
default:
|
||||
addP.push(ent);
|
||||
}
|
||||
}
|
||||
|
||||
for( let e of addM )
|
||||
out.push(e);
|
||||
for( let e of addL )
|
||||
out.push(e);
|
||||
for( let e of addP )
|
||||
out.push(e);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
function isBuiltIn(filename)
|
||||
{
|
||||
const afn = filename.toLowerCase();
|
||||
for( let bent of currentGameEntry['builtin'] )
|
||||
{
|
||||
const bfn = bent.toLowerCase();
|
||||
if( afn === bfn )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function consolidate(plugins)
|
||||
{
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
const path = `${sobj.gamePath}/${currentGameEntry['datadir']}`;
|
||||
const contents = File.dirContents(path);
|
||||
if( !contents )
|
||||
return plugins; // ... weird?
|
||||
|
||||
// Don't add files that belong to a mod:
|
||||
const justFiles = db.getFilesEndingWith(['.esm', '.esl', '.esp']).map( e => e['dest'].toLowerCase() );
|
||||
// Database tells us what we needs~
|
||||
let fileEnts = db.getFilesEndingWith(['.esm', '.esl', '.esp']);
|
||||
|
||||
let isAThingLC = [];
|
||||
|
||||
const modsLC = plugins.map( e => e['filename'].toLowerCase() );
|
||||
for( let fent of fileEnts )
|
||||
{
|
||||
let dparts = fent['dest'].split('/');
|
||||
let fpart = dparts.pop();
|
||||
let fpartLC = fpart.toLowerCase();
|
||||
isAThingLC.push( fpartLC );
|
||||
|
||||
if( !modsLC.includes( fpart.toLowerCase() ) )
|
||||
plugins.push( { 'enabled':(fent['enabled'] !== 0 ? true : false), 'filename':fpart } );
|
||||
}
|
||||
|
||||
let justFiles = plugins.map( e => e['filename'].toLowerCase() );
|
||||
|
||||
const contents = File.dirContents(path);
|
||||
for( let x=0; x < contents.length; x++ )
|
||||
{
|
||||
const ent = contents[x];
|
||||
const lcfn = ent['fileName'].toLowerCase();
|
||||
|
||||
if( !isAThingLC.includes(lcfn) )
|
||||
isAThingLC.push( lcfn );
|
||||
|
||||
if( justFiles.includes(lcfn) )
|
||||
continue;
|
||||
|
||||
if( lcfn.endsWith('.esl') || lcfn.endsWith('.esp') || lcfn.endsWith('.esm') )
|
||||
{
|
||||
if( !modsLC.includes(lcfn) )
|
||||
{
|
||||
// ...append them.
|
||||
let nent = { 'enabled':false, 'filename':ent['fileName'] };
|
||||
plugins.push(nent);
|
||||
modsLC.push(lcfn);
|
||||
}
|
||||
let nent = { 'enabled':true, 'filename':ent['fileName'] };
|
||||
plugins.push(nent);
|
||||
justFiles.push(lcfn);
|
||||
}
|
||||
}
|
||||
|
||||
return plugins;
|
||||
// Prune the crap
|
||||
let newplugs = [];
|
||||
for( let ent of plugins )
|
||||
{
|
||||
if( isAThingLC.includes(ent['filename'].toLowerCase()) )
|
||||
newplugs.push(ent);
|
||||
}
|
||||
|
||||
/*
|
||||
plugins.sort( function(a, b) { return _pluginSort(a, b, 'filename'); } );
|
||||
*/
|
||||
newplugs = sporkSort( newplugs, 'filename' );
|
||||
|
||||
return newplugs;
|
||||
}
|
||||
|
||||
function disableMod(mod)
|
||||
|
|
@ -220,6 +311,11 @@ function updatePluginsTable(plugins)
|
|||
let model = [];
|
||||
plugins.forEach( function(ent) {
|
||||
let nent = { 'enabled':ent['enabled'], 'filename':ent['filename'], 'name':'???', 'description':'???' };
|
||||
if( isBuiltIn(ent['filename']) )
|
||||
{
|
||||
nent['name'] = currentGameEntry['name'];
|
||||
nent['description'] = qsTr('Built-In');
|
||||
}
|
||||
|
||||
if( ent['description'] )
|
||||
nent['description'] = ent['description'];
|
||||
|
|
@ -227,8 +323,12 @@ function updatePluginsTable(plugins)
|
|||
if( ent['missing'] )
|
||||
nent['missing'] = ent['missing'];
|
||||
|
||||
/**
|
||||
* HACK We don't care, we're using a VFS now:
|
||||
**
|
||||
if( !ent['plugin'] )
|
||||
nent['notfound'] = true;
|
||||
*/
|
||||
|
||||
let done = false;
|
||||
for( let a=0; a < mods.length && !done; a++ )
|
||||
|
|
|
|||
41
quickmod.pro
41
quickmod.pro
|
|
@ -13,6 +13,7 @@ SOURCES += \
|
|||
src/fomodreader.cpp \
|
||||
src/modreader.cpp \
|
||||
src/nxmhandler.cpp \
|
||||
src/process.cpp \
|
||||
src/utils.cpp \
|
||||
src/sqldatabase.cpp \
|
||||
src/sqldatabasemodel.cpp
|
||||
|
|
@ -23,6 +24,7 @@ HEADERS += \
|
|||
src/http.h \
|
||||
src/modreader.h \
|
||||
src/nxmhandler.h \
|
||||
src/process.h \
|
||||
src/utils.h \
|
||||
src/sqldatabase.h \
|
||||
src/sqldatabasemodel.h
|
||||
|
|
@ -31,6 +33,45 @@ RESOURCES += qml.qrc
|
|||
|
||||
LIBS += -larchive
|
||||
|
||||
# /**
|
||||
# * For FUSE-based mod archive VFS:
|
||||
# **/
|
||||
DEFINES += WITH_FUSE
|
||||
CONFIG += c++17 cmdline link_pkgconfig
|
||||
|
||||
PKGCONFIG += fuse3 libunarr
|
||||
QMAKE_CXXFLAGS += -D_FILE_OFFSET_BITS=64 -O3
|
||||
#QMAKE_CXXFLAGS += -D_FILE_OFFSET_BITS=64 -g -O0
|
||||
|
||||
# You can make your code fail to compile if it uses deprecated APIs.
|
||||
# In order to do so, uncomment the following line.
|
||||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
SOURCES += \
|
||||
src/fusemanager.cpp \
|
||||
FuseMounter/archivemanager.cpp \
|
||||
FuseMounter/database.cpp \
|
||||
FuseMounter/fsproxy.cpp \
|
||||
FuseMounter/fusestuff.cpp \
|
||||
FuseMounter/modarchive.cpp
|
||||
|
||||
# Default rules for deployment.
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
!isEmpty(target.path): INSTALLS += target
|
||||
|
||||
HEADERS += \
|
||||
src/fusemanager.h \
|
||||
FuseMounter/archivemanager.h \
|
||||
FuseMounter/database.h \
|
||||
FuseMounter/fsproxy.h \
|
||||
FuseMounter/fusestuff.h \
|
||||
FuseMounter/modarchive.h
|
||||
|
||||
# /*************************/
|
||||
# /* End of FUSE-based VFS */
|
||||
# /*************************/
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <QtGlobal>
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
|
||||
FOMODReader::FOMODReader(QObject *parent)
|
||||
: QObject{parent}
|
||||
|
|
|
|||
127
src/fusemanager.cpp
Normal file
127
src/fusemanager.cpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#include "fusemanager.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mount.h>
|
||||
|
||||
#include "../FuseMounter/archivemanager.h"
|
||||
#include "../FuseMounter/database.h"
|
||||
#include "../FuseMounter/fusestuff.h"
|
||||
#include "../FuseMounter/fsproxy.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QSocketNotifier>
|
||||
#include <QSqlDatabase>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include "src/sqldatabase.h"
|
||||
|
||||
FUSEManager::FUSEManager(QObject *parent)
|
||||
: QObject{parent},
|
||||
m_mounted{false},
|
||||
m_fuse_fd{-1},
|
||||
m_fuse_notifier{nullptr},
|
||||
m_db{nullptr},
|
||||
m_proxy{nullptr},
|
||||
m_manager{nullptr}
|
||||
{
|
||||
}
|
||||
|
||||
FUSEManager::~FUSEManager()
|
||||
{
|
||||
unmount();
|
||||
if( m_db )
|
||||
delete m_db;
|
||||
if( m_proxy )
|
||||
delete m_proxy;
|
||||
if( m_manager )
|
||||
delete m_manager;
|
||||
}
|
||||
|
||||
bool FUSEManager::mount(const QVariant &dbconn, const QString &gameName, const QString &destPath, int profileId)
|
||||
{
|
||||
m_currentGameName = gameName;
|
||||
m_targetDir = destPath;
|
||||
m_profileId = profileId;
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(m_currentGameName);
|
||||
QString dbpath = s.value("dbPath").toString();
|
||||
QString gamedir = s.value("gamePath").toString();
|
||||
QString modsdir = s.value("modsPath").toString();
|
||||
|
||||
SqlDatabaseConnection *db = dbconn.value<SqlDatabaseConnection *>();
|
||||
|
||||
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);
|
||||
|
||||
if( m_manager ) {
|
||||
m_manager->deleteLater();
|
||||
m_manager = NULL;
|
||||
}
|
||||
|
||||
m_manager = new ArchiveManager(m_db, modsdir, m_profileId, this);
|
||||
|
||||
qmfuse_set_archive(m_manager);
|
||||
qmfuse_set_proxy(m_proxy);
|
||||
qmfuse_set_database(m_db);
|
||||
|
||||
m_fuse_fd = qmfuse_main(m_targetDir.toStdString().c_str());
|
||||
if( m_fuse_notifier )
|
||||
m_fuse_notifier->deleteLater();
|
||||
|
||||
m_fuse_notifier = new QSocketNotifier(m_fuse_fd, QSocketNotifier::Read, this);
|
||||
QObject::connect( m_fuse_notifier, &QSocketNotifier::activated, this, &FUSEManager::handle_fuse_event );
|
||||
|
||||
m_mounted = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FUSEManager::unmount()
|
||||
{
|
||||
qmfuse_unmount();
|
||||
|
||||
::umount2( m_targetDir.toStdString().c_str(), MNT_DETACH );
|
||||
|
||||
m_mounted = false;
|
||||
if( m_fuse_notifier ) {
|
||||
m_fuse_notifier->setEnabled(false);
|
||||
m_fuse_notifier->deleteLater();
|
||||
m_fuse_notifier = NULL;
|
||||
}
|
||||
|
||||
if( m_fuse_fd > 0 )
|
||||
::close(m_fuse_fd);
|
||||
|
||||
::umount2( m_targetDir.toStdString().c_str(), 0 );
|
||||
}
|
||||
|
||||
void FUSEManager::handle_fuse_event()
|
||||
{
|
||||
int ret;
|
||||
do {
|
||||
ret = qmfuse_pump();
|
||||
} while(ret > 0);
|
||||
|
||||
if( ret < 0 ) {
|
||||
unmount();
|
||||
}
|
||||
}
|
||||
41
src/fusemanager.h
Normal file
41
src/fusemanager.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#ifndef FUSEMANAGER_H
|
||||
#define FUSEMANAGER_H
|
||||
|
||||
#include <QJSValue>
|
||||
#include <QObject>
|
||||
#include <QSocketNotifier>
|
||||
|
||||
class ArchiveManager;
|
||||
class Database;
|
||||
class FSProxy;
|
||||
class SqlDatabaseConnection;
|
||||
class FUSEManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QString m_currentGameName;
|
||||
QString m_targetDir;
|
||||
int m_profileId;
|
||||
bool m_mounted;
|
||||
|
||||
int m_fuse_fd;
|
||||
QSocketNotifier *m_fuse_notifier;
|
||||
|
||||
Database *m_db;
|
||||
FSProxy *m_proxy;
|
||||
ArchiveManager *m_manager;
|
||||
|
||||
public:
|
||||
explicit FUSEManager(QObject *parent = nullptr);
|
||||
~FUSEManager();
|
||||
|
||||
Q_INVOKABLE bool mount(const QVariant &dbhandle, const QString &gameName, const QString &destPath, int profileId);
|
||||
Q_INVOKABLE void unmount();
|
||||
|
||||
private slots:
|
||||
void handle_fuse_event();
|
||||
|
||||
signals:
|
||||
};
|
||||
|
||||
#endif // FUSEMANAGER_H
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
#include "http.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QJSValue>
|
||||
#include <QNetworkReply>
|
||||
|
||||
HTTPHandle::HTTPHandle(HTTP *parent, QNetworkReply *reply)
|
||||
|
|
|
|||
12
src/main.cpp
12
src/main.cpp
|
|
@ -17,6 +17,9 @@
|
|||
#include "sqldatabase.h"
|
||||
#include "sqldatabasemodel.h"
|
||||
#include "utils.h"
|
||||
#ifdef WITH_FUSE
|
||||
# include "fusemanager.h"
|
||||
#endif
|
||||
|
||||
#define SERVICE_NAME "org.oneill.Quickmod"
|
||||
|
||||
|
|
@ -96,6 +99,11 @@ int main(int argc, char *argv[])
|
|||
engine.rootContext()->setContextProperty("HTTP", http);
|
||||
engine.rootContext()->setContextProperty("Args", args);
|
||||
|
||||
#ifdef WITH_FUSE
|
||||
FUSEManager *fuse = new FUSEManager();
|
||||
engine.rootContext()->setContextProperty("FUSEManager", fuse);
|
||||
#endif
|
||||
|
||||
qmlRegisterUncreatableType<ArchiveExtractToFileWorker>("org.ONeill.ArchiveExtractToFileWorker", 1, 0, "ArchiveExtractToFileWorker", "Instantiated with Archive.extract(matrix, funcFinished)");
|
||||
qmlRegisterUncreatableType<ArchiveExtractToMemoryWorker>("org.ONeill.ArchiveExtractToMemory", 1, 0, "ArchiveExtractToMemory", "Instantiated with Archive.get(target, funcFinished)");
|
||||
qmlRegisterUncreatableType<ArchiveListWorker>("org.ONeill.ArchiveList", 1, 0, "ArchiveList", "Instantiated with Archive.list(funcFinished)");
|
||||
|
|
@ -131,5 +139,9 @@ int main(int argc, char *argv[])
|
|||
delete nxmHandler;
|
||||
delete http;
|
||||
|
||||
#ifdef WITH_FUSE
|
||||
delete fuse;
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ QVariant ModReader::readSkyrimMod(const QString &inPath)
|
|||
QString path = m_file->resolveCase(inPath);
|
||||
|
||||
QFile f(path);
|
||||
if( !f.open(QIODevice::ReadOnly) )
|
||||
if( !f.open(QIODeviceBase::ReadOnly) )
|
||||
return false;
|
||||
|
||||
struct {
|
||||
|
|
|
|||
42
src/process.cpp
Normal file
42
src/process.cpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#include "process.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QProcessEnvironment>
|
||||
|
||||
Process::Process(QObject *parent)
|
||||
: QObject{parent}
|
||||
{}
|
||||
|
||||
Process::~Process()
|
||||
{
|
||||
}
|
||||
|
||||
ProcessObject *Process::launch(const QString &path, const QStringList &args, const QString &tmode, const QVariantMap &env)
|
||||
{
|
||||
ProcessObject *p = new ProcessObject(this);
|
||||
|
||||
QProcessEnvironment penv = QProcessEnvironment::systemEnvironment();
|
||||
for( QString k : env.keys() )
|
||||
penv.insert(k, env[k].toString());
|
||||
p->setProcessEnvironment(penv);
|
||||
|
||||
QIODevice::OpenMode mode = QIODevice::ReadOnly;
|
||||
if( tmode.contains("rw") )
|
||||
mode = QIODevice::ReadWrite;
|
||||
|
||||
p->start(path, args, mode);
|
||||
return p;
|
||||
}
|
||||
|
||||
QByteArray Process::exec(const QString &path,const QStringList &args, const QVariantMap &env)
|
||||
{
|
||||
ProcessObject *p = launch(path, args, "rw", env);
|
||||
p->waitForStarted(1000);
|
||||
p->waitForFinished(10000);
|
||||
|
||||
QByteArray result = p->readAllStandardOutput();
|
||||
p->deleteLater();
|
||||
|
||||
return result;
|
||||
}
|
||||
48
src/process.h
Normal file
48
src/process.h
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#ifndef PROCESS_H
|
||||
#define PROCESS_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QJSEngine>
|
||||
#include <QJSValue>
|
||||
#include <QVariant>
|
||||
|
||||
class ProcessObject : public QProcess
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ProcessObject(QObject *parent=0) : QProcess(parent) {}
|
||||
~ProcessObject() {}
|
||||
|
||||
Q_INVOKABLE void close() { QProcess::close(); }
|
||||
Q_INVOKABLE bool open(QIODevice::OpenMode mode=ReadWrite) { return QProcess::open(mode); }
|
||||
Q_INVOKABLE QByteArray read(qint64 maxlen) { return QProcess::read(maxlen); }
|
||||
Q_INVOKABLE QByteArray readAllStandardError() { return QProcess::readAllStandardError(); }
|
||||
Q_INVOKABLE QByteArray readAllStandardOutput() { return QProcess::readAllStandardOutput(); }
|
||||
Q_INVOKABLE bool atEnd() const { return QProcess::atEnd(); }
|
||||
Q_INVOKABLE bool waitForStarted(int msecs = 30000) { return QProcess::waitForStarted(msecs); }
|
||||
Q_INVOKABLE bool waitForReadyRead(int msecs = 30000) { return QProcess::waitForReadyRead(msecs); }
|
||||
Q_INVOKABLE void closeWriteChannel() { QProcess::closeWriteChannel(); }
|
||||
Q_INVOKABLE qint64 write(const QByteArray &data) { return QProcess::write(data); }
|
||||
Q_INVOKABLE qint64 bytesAvailable() const { return QProcess::bytesAvailable(); }
|
||||
Q_INVOKABLE bool waitForFinished(int msecs = 30000) { return QProcess::waitForFinished(msecs); }
|
||||
Q_INVOKABLE qint64 processId() { return QProcess::processId(); }
|
||||
};
|
||||
|
||||
class Process : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Process(QObject *parent);
|
||||
~Process();
|
||||
|
||||
public slots:
|
||||
Q_INVOKABLE ProcessObject *launch(const QString &path, const QStringList &args, const QString &tmode, const QVariantMap &env);
|
||||
Q_INVOKABLE QByteArray exec(const QString &path, const QStringList &args=QStringList(), const QVariantMap &env=QVariantMap());
|
||||
};
|
||||
|
||||
|
||||
#endif // PROCESS_H
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
#include "sqldatabase.h"
|
||||
#include "sqldatabasemodel.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QMutex>
|
||||
#include <QSqlQuery>
|
||||
#include <QString>
|
||||
|
||||
#define __DB_MAGIC "FusRohDah"
|
||||
#define __DB_Q_MAGIC "HerpDerp"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#include "sqldatabase.h"
|
||||
#include "sqldatabasemodel.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQueryModel>
|
||||
|
||||
SqlDatabaseModel::SqlDatabaseModel(QObject *parent)
|
||||
: QSqlQueryModel(parent)
|
||||
|
|
|
|||
Loading…
Reference in a new issue