From f918d50a136af68993070014fdd4c77c393ad442 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Sun, 3 May 2026 14:09:08 -0500 Subject: [PATCH 1/7] Add MODL schema handling - Extends entries with 'schema' attribute - Adds ability to init games with an arg string - MODL URIs will parse 'name' 'modname' 'version' and 'source' parameters - Utilizes replacement strings in the launch arg - Should automigrate old entries to 'nxm' schema - Should automatically ask to register for MODL --- src/addbinarydialog.cpp | 12 ++- src/addbinarydialog.h | 3 +- src/addbinarydialog.ui | 24 +++-- src/handlerstorage.cpp | 67 ++++++++---- src/handlerstorage.h | 19 ++-- src/handlerwindow.cpp | 14 +-- src/handlerwindow.ui | 18 +++- src/main.cpp | 231 +++++++++++++++++++++++++++++++--------- 8 files changed, 292 insertions(+), 96 deletions(-) diff --git a/src/addbinarydialog.cpp b/src/addbinarydialog.cpp index 0a0d70a..1774d9d 100644 --- a/src/addbinarydialog.cpp +++ b/src/addbinarydialog.cpp @@ -3,7 +3,8 @@ #include AddBinaryDialog::AddBinaryDialog( - const std::vector>& games, QWidget* parent) + const std::vector>& games, + const QStringList schemas, QWidget* parent) : QDialog(parent), ui(new Ui::AddBinaryDialog) { ui->setupUi(this); @@ -11,6 +12,10 @@ AddBinaryDialog::AddBinaryDialog( for (auto iter = games.begin(); iter != games.end(); ++iter) { addGame(std::get<0>(*iter), std::get<1>(*iter)); } + + for (auto schema : schemas) { + this->ui->schemaSelector->addItem(schema); + } } AddBinaryDialog::~AddBinaryDialog() @@ -45,6 +50,11 @@ QString AddBinaryDialog::arguments() return ui->argumentsEdit->text(); } +QString AddBinaryDialog::schema() +{ + return ui->schemaSelector->currentText(); +} + void AddBinaryDialog::on_browseButton_clicked() { ui->binaryEdit->setText(QFileDialog::getOpenFileName( diff --git a/src/addbinarydialog.h b/src/addbinarydialog.h index 6ec79ad..bbe5c24 100644 --- a/src/addbinarydialog.h +++ b/src/addbinarydialog.h @@ -16,11 +16,12 @@ class AddBinaryDialog : public QDialog public: explicit AddBinaryDialog( const std::vector>& handlers, - QWidget* parent = 0); + const QStringList schemas, QWidget* parent = 0); ~AddBinaryDialog(); QStringList gameIDs(); QString executable(); QString arguments(); + QString schema(); private slots: void on_browseButton_clicked(); diff --git a/src/addbinarydialog.ui b/src/addbinarydialog.ui index f84cab0..b3d7ba7 100644 --- a/src/addbinarydialog.ui +++ b/src/addbinarydialog.ui @@ -7,7 +7,7 @@ 0 0 575 - 160 + 219 @@ -28,7 +28,7 @@ - QAbstractItemView::ExtendedSelection + QAbstractItemView::SelectionMode::ExtendedSelection @@ -37,7 +37,7 @@ - + Select Handler Binary (i.e. ModOrganizer.exe) @@ -64,7 +64,17 @@ - + + + Assigned Schema + + + + + + + + Set Arguments (optional) @@ -76,7 +86,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -93,10 +103,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/src/handlerstorage.cpp b/src/handlerstorage.cpp index 759aead..3dff6c9 100644 --- a/src/handlerstorage.cpp +++ b/src/handlerstorage.cpp @@ -23,7 +23,7 @@ void HandlerStorage::clear() m_Handlers.clear(); } -void HandlerStorage::registerProxy(const QString& proxyPath) +void HandlerStorage::registerNxmProxy(const QString& proxyPath) { QSettings settings("HKEY_CURRENT_USER\\Software\\Classes\\nxm\\", QSettings::NativeFormat); @@ -35,17 +35,29 @@ void HandlerStorage::registerProxy(const QString& proxyPath) settings.sync(); } -void HandlerStorage::registerHandler(const QString& executable, +void HandlerStorage::registerModlProxy(const QString& proxyPath) +{ + QSettings settings("HKEY_CURRENT_USER\\Software\\Classes\\modl\\", + QSettings::NativeFormat); + QString myExe = + QString("\"%1\" ").arg(QDir::toNativeSeparators(proxyPath)).append("\"%1\""); + settings.setValue("Default", "URL:MODL Protocol"); + settings.setValue("URL Protocol", ""); + settings.setValue("shell/open/command/Default", myExe); + settings.sync(); +} + +void HandlerStorage::registerHandler(const QString& schema, const QString& executable, const QString& arguments, bool prepend) { QStringList games; for (const auto& game : this->knownGames()) { games.append(std::get<1>(game)); } - registerHandler(games, executable, arguments, prepend, false); + registerHandler(games, schema, executable, arguments, prepend, false); } -void HandlerStorage::registerHandler(const QStringList& games, +void HandlerStorage::registerHandler(const QStringList& games, const QString& schema, const QString& executable, const QString& arguments, bool prepend, bool rereg) { @@ -54,12 +66,14 @@ void HandlerStorage::registerHandler(const QStringList& games, gamesLower.append(game.toLower()); } for (auto iter = m_Handlers.begin(); iter != m_Handlers.end(); ++iter) { - if (iter->executable.compare(executable, Qt::CaseInsensitive) == 0) { - // executable already registered, update supported games and move it to top if - // requested + if (iter->executable.compare(executable, Qt::CaseInsensitive) == 0 && + iter->schema.compare(schema, Qt::CaseInsensitive) == 0) { + // executable already registered, update supported games and move it to + // top if requested if (rereg) { HandlerInfo info = *iter; info.games = gamesLower; + info.arguments = arguments; m_Handlers.erase(iter); if (prepend) { m_Handlers.push_front(info); @@ -69,6 +83,7 @@ void HandlerStorage::registerHandler(const QStringList& games, } else { iter->games.append(gamesLower); iter->games.removeDuplicates(); + iter->arguments = arguments; } return; // important: in the rereg-case we changed the list thus screwing up the // iterator @@ -79,6 +94,7 @@ void HandlerStorage::registerHandler(const QStringList& games, HandlerInfo info; info.ID = static_cast(m_Handlers.size()); info.games = gamesLower; + info.schema = schema; info.executable = executable; info.arguments = arguments; if (prepend) { @@ -88,7 +104,7 @@ void HandlerStorage::registerHandler(const QStringList& games, } } -QStringList HandlerStorage::getHandler(const QString& game) const +QStringList HandlerStorage::getHandler(const QString& game, const QString& schema) const { QString gameKey; QStringList results; @@ -102,12 +118,14 @@ QStringList HandlerStorage::getHandler(const QString& game) const } // look for an explictly registered handler for (const HandlerInfo& info : m_Handlers) { - for (auto handler : info.games) { - if (game.compare(handler, Qt::CaseInsensitive) == 0 || - gameKey.compare(handler, Qt::CaseInsensitive) == 0) { - results << info.executable; - results << info.arguments; - return results; + if (info.schema == schema) { + for (auto handler : info.games) { + if (game.compare(handler, Qt::CaseInsensitive) == 0 || + gameKey.compare(handler, Qt::CaseInsensitive) == 0) { + results << info.executable; + results << info.arguments; + return results; + } } } } @@ -115,10 +133,12 @@ QStringList HandlerStorage::getHandler(const QString& game) const // if no registered handler, look for the first "other" entry if (results.length() == 0) { for (const HandlerInfo& info : m_Handlers) { - if (info.games.contains("other", Qt::CaseInsensitive)) { - results << info.executable; - results << info.arguments; - return results; + if (info.schema == schema) { + if (info.games.contains("other", Qt::CaseInsensitive)) { + results << info.executable; + results << info.arguments; + return results; + } } } } @@ -136,18 +156,26 @@ std::vector> HandlerStorage::knownGames() return { std::make_tuple("Morrowind", "morrowind", "morrowind"), std::make_tuple("Oblivion", "oblivion", "oblivion"), + std::make_tuple( + "Oblivion Reloaded", "oblivionreloaded", "oblivionreloaded"), std::make_tuple("Fallout 3", "fallout3", "fallout3"), std::make_tuple("Fallout 4", "fallout4", "fallout4"), std::make_tuple("Fallout NV", "falloutnv", "newvegas"), std::make_tuple("Skyrim", "skyrim", "skyrim"), std::make_tuple("SkyrimSE", "skyrimse", "skyrimspecialedition"), + std::make_tuple("Starfield", "starfield", "starfield"), std::make_tuple("Enderal", "enderal", "enderal"), std::make_tuple("EnderalSE", "enderalse", "enderalspecialedition"), std::make_tuple("Other", "other", "other")}; } +QStringList HandlerStorage::availableSchemas() const +{ + return {"nxm", "modl"}; +} + QStringList HandlerStorage::stripCall(const QString& call) { // results[0] is binary, results[1..n] are optional arguments @@ -213,6 +241,7 @@ void HandlerStorage::loadStore() if (!gameList.isEmpty()) { info.games = gameList.split(","); } + info.schema = settings.value("schema", "nxm").toString(); info.executable = settings.value("executable").toString(); info.arguments = settings.value("arguments").toString(); if (QFile::exists(info.executable)) { @@ -234,6 +263,7 @@ void HandlerStorage::loadStore() ids.append(std::get<1>(*iter)); } info.games = QStringList() << ids; + info.schema = "nxm"; info.executable = handlerValues.front(); handlerValues.pop_front(); info.arguments = handlerValues.join(" "); @@ -261,6 +291,7 @@ void HandlerStorage::saveStore() for (auto iter = m_Handlers.begin(); iter != m_Handlers.end(); ++iter) { settings.setArrayIndex(i++); settings.setValue("games", iter->games.join(",")); + settings.setValue("schema", iter->schema); settings.setValue("executable", iter->executable); settings.setValue("arguments", iter->arguments); } diff --git a/src/handlerstorage.h b/src/handlerstorage.h index 577ad76..594ded3 100644 --- a/src/handlerstorage.h +++ b/src/handlerstorage.h @@ -10,6 +10,7 @@ struct HandlerInfo { int ID; QStringList games; + QString schema; QString executable; QString arguments; }; @@ -22,16 +23,20 @@ class HandlerStorage : public QObject ~HandlerStorage(); void clear(); - /// register the primary proxy handler - void registerProxy(const QString& proxyPath); + /// register the nxm proxy handler + void registerNxmProxy(const QString& proxyPath); + /// register the modl proxy handler + void registerModlProxy(const QString& proxyPath); /// register handler (for all games) - void registerHandler(const QString& executable, const QString& arguments, - bool prepend); + void registerHandler(const QString& schema, const QString& executable, + const QString& arguments, bool prepend); /// register handler for specified games - void registerHandler(const QStringList& games, const QString& executable, - const QString& arguments, bool prepend, bool rereg); - QStringList getHandler(const QString& game) const; + void registerHandler(const QStringList& games, const QString& schema, + const QString& executable, const QString& arguments, + bool prepend, bool rereg); + QStringList getHandler(const QString& game, const QString& schema) const; std::vector> knownGames() const; + QStringList availableSchemas() const; std::list handlers() const { return m_Handlers; } static QStringList stripCall(const QString& call); diff --git a/src/handlerwindow.cpp b/src/handlerwindow.cpp index a062c95..4482580 100644 --- a/src/handlerwindow.cpp +++ b/src/handlerwindow.cpp @@ -10,6 +10,7 @@ enum { COL_GAMES, + COL_SCHEMA, COL_BINARY, COL_ARGUMENTS }; @@ -48,7 +49,7 @@ void HandlerWindow::setHandlerStorage(HandlerStorage* storage) auto list = storage->handlers(); for (auto iter = list.begin(); iter != list.end(); ++iter) { QTreeWidgetItem* newItem = new QTreeWidgetItem( - QStringList() << iter->games.join(",") + QStringList() << iter->games.join(",") << iter->schema << QDir::toNativeSeparators(iter->executable) << iter->arguments); newItem->setFlags(newItem->flags() | Qt::ItemIsEditable); @@ -63,14 +64,14 @@ void HandlerWindow::closeEvent(QCloseEvent* event) for (int i = 0; i < ui->handlersWidget->topLevelItemCount(); ++i) { QTreeWidgetItem* item = ui->handlersWidget->topLevelItem(i); m_Storage->registerHandler(item->text(0).split(","), item->text(1), item->text(2), - false, false); + item->text(3), false, false); } QMainWindow::closeEvent(event); } void HandlerWindow::addBinaryDialog() { - AddBinaryDialog dialog(m_Storage->knownGames()); + AddBinaryDialog dialog(m_Storage->knownGames(), m_Storage->availableSchemas()); if (dialog.exec() == QDialog::Accepted) { bool executableKnown = false; for (int i = 0; i < ui->handlersWidget->topLevelItemCount(); ++i) { @@ -80,6 +81,7 @@ void HandlerWindow::addBinaryDialog() games.append(dialog.gameIDs()); games.removeDuplicates(); iterItem->setText(COL_GAMES, games.join(",")); + iterItem->setText(COL_SCHEMA, dialog.schema()); if (iterItem->text(COL_ARGUMENTS) .compare(dialog.arguments(), Qt::CaseInsensitive) != 0) { iterItem->setText(COL_ARGUMENTS, dialog.arguments()); @@ -90,8 +92,8 @@ void HandlerWindow::addBinaryDialog() if (!executableKnown) { QTreeWidgetItem* newItem = new QTreeWidgetItem( - QStringList() << dialog.gameIDs().join(",") << dialog.executable() - << dialog.arguments()); + QStringList() << dialog.gameIDs().join(",") << dialog.schema() + << dialog.executable() << dialog.arguments()); newItem->setFlags(newItem->flags() | Qt::ItemIsEditable); ui->handlersWidget->insertTopLevelItem(0, newItem); } @@ -133,6 +135,6 @@ void HandlerWindow::on_registerButton_clicked() ui->handlerView->setText(tr("")); ui->registerButton->setEnabled(false); - m_Storage->registerProxy(QCoreApplication::applicationFilePath()); + m_Storage->registerNxmProxy(QCoreApplication::applicationFilePath()); } } diff --git a/src/handlerwindow.ui b/src/handlerwindow.ui index 645b6e0..dc4d903 100644 --- a/src/handlerwindow.ui +++ b/src/handlerwindow.ui @@ -7,7 +7,7 @@ 0 0 647 - 347 + 362 @@ -28,13 +28,13 @@ - Qt::CustomContextMenu + Qt::ContextMenuPolicy::CustomContextMenu - QAbstractItemView::DragDrop + QAbstractItemView::DragDropMode::DragDrop - Qt::MoveAction + Qt::DropAction::MoveAction 0 @@ -42,6 +42,9 @@ false + + 4 + 150 @@ -50,6 +53,11 @@ Supported Games + + + Schema Handler + + Binary @@ -92,7 +100,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal diff --git a/src/main.cpp b/src/main.cpp index 3a6b126..215f758 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -43,8 +44,8 @@ void logHandler(QtMsgType type, const QMessageLogContext& context, QString("[%1] %2\n").arg(QDateTime::currentDateTime().toString()).arg(message))); } -void handleLink(const QString& executable, const QString& arguments, - const QString& link) +void handleNxmLink(const QString& executable, const QString& arguments, + const QString& link) { QString quotedExecutable(executable); if (!quotedExecutable.contains(QRegularExpression("^\".*\"$"))) { @@ -62,8 +63,49 @@ void handleLink(const QString& executable, const QString& arguments, SW_SHOWNORMAL); } -HandlerStorage* registerExecutable(const QDir& storagePath, const QString& handlerPath, - const QString& handlerArgs) +void handleModlLink(const QString& executable, const QString& arguments, + const QString& link) +{ + QString quotedExecutable(executable); + if (!quotedExecutable.contains(QRegularExpression("^\".*\"$"))) { + quotedExecutable = '"' + quotedExecutable + '"'; + } + + QString quotedLink(link); + if (!quotedLink.contains(QRegularExpression("^\".*\"$"))) { + quotedLink = '"' + quotedLink + '"'; + } + + ::ShellExecute(nullptr, TEXT("open"), ToWString(quotedExecutable).c_str(), + ToWString("download " + arguments + " " + quotedLink).c_str(), + ToWString(QFileInfo(quotedExecutable).absolutePath()).c_str(), + SW_SHOWNORMAL); +} + +HandlerStorage* registerNxmExecutable(const QDir& storagePath, + const QString& handlerPath, + const QString& handlerArgs) +{ + HandlerStorage* storage = nullptr; + if (!handlerPath.isEmpty() && + !handlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { + // a foreign or global nxm handler, register ourself and use that handler as + // an option - if this is another nxmhandler we could run into problems so + // skip it + storage = new HandlerStorage(storagePath.path()); + storage->registerHandler("nxm", handlerPath, handlerArgs, false); + storage->registerNxmProxy(QCoreApplication::applicationFilePath()); + } else { + // no handler registered yet or the existing handler is invalid -> overwrite + storage = new HandlerStorage(storagePath.path()); + storage->registerNxmProxy(QCoreApplication::applicationFilePath()); + } + return storage; +} + +HandlerStorage* registerModlExecutable(const QDir& storagePath, + const QString& handlerPath, + const QString& handlerArgs) { HandlerStorage* storage = nullptr; if (!handlerPath.isEmpty() && @@ -71,12 +113,12 @@ HandlerStorage* registerExecutable(const QDir& storagePath, const QString& handl // a foreign or global nxm handler, register ourself and use that handler as // an option - if this is another nxmhandler we could run into problems so skip it storage = new HandlerStorage(storagePath.path()); - storage->registerHandler(handlerPath, handlerArgs, false); - storage->registerProxy(QCoreApplication::applicationFilePath()); + storage->registerHandler("modl", handlerPath, handlerArgs, false); + storage->registerModlProxy(QCoreApplication::applicationFilePath()); } else { // no handler registered yet or the existing handler is invalid -> overwrite storage = new HandlerStorage(storagePath.path()); - storage->registerProxy(QCoreApplication::applicationFilePath()); + storage->registerModlProxy(QCoreApplication::applicationFilePath()); } return storage; } @@ -98,32 +140,42 @@ HandlerStorage* loadStorage(bool forceReg) baseDir = QDir(qApp->applicationDirPath()); } NxmHandler::LoggerInit(baseDir.filePath("nxmhandler.log")); - QSettings handlerReg("HKEY_CURRENT_USER\\Software\\Classes\\nxm\\", - QSettings::NativeFormat); - QStringList handlerVals = HandlerStorage::stripCall( - handlerReg.value("shell/open/command/Default").toString()); - QString handlerPath = handlerVals.front(); - handlerVals.pop_front(); - QString handlerArgs = handlerVals.join(" "); + QSettings nxmHandlerReg("HKEY_CURRENT_USER\\Software\\Classes\\nxm\\", + QSettings::NativeFormat); + QSettings modlHandlerReg("HKEY_CURRENT_USER\\Software\\Classes\\modl\\", + QSettings::NativeFormat); + QStringList nxmHandlerVals = HandlerStorage::stripCall( + nxmHandlerReg.value("shell/open/command/Default").toString()); + QStringList modlHandlerVals = HandlerStorage::stripCall( + modlHandlerReg.value("shell/open/command/Default").toString()); + QString nxmHandlerPath = nxmHandlerVals.front(); + QString modlHandlerPath = modlHandlerVals.front(); + nxmHandlerVals.pop_front(); + modlHandlerVals.pop_front(); + QString nxmHandlerArgs = nxmHandlerVals.join(" "); + QString modlHandlerArgs = modlHandlerVals.join(" "); - QDir handlerBaseDir = QFileInfo(handlerPath).absoluteDir(); + QDir nxmHandlerBaseDir = QFileInfo(nxmHandlerPath).absoluteDir(); + QDir modlHandlerBaseDir = QFileInfo(modlHandlerPath).absoluteDir(); QSettings settings(baseDir.absoluteFilePath("nxmhandler.ini"), QSettings::IniFormat); bool noRegister = settings.value("noregister", false).toBool(); if (globalStorage.exists("nxmhandler.ini") && - handlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && - QFile::exists(handlerPath)) { + nxmHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && + modlHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && + QFile::exists(nxmHandlerPath)) { // global configuration avaible - use it storage = new HandlerStorage(globalStorage.path()); - } else if (handlerBaseDir.exists("nxmhandler.ini") && - handlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && - QFile::exists(handlerPath)) { + } else if (nxmHandlerBaseDir.exists("nxmhandler.ini") && + nxmHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && + modlHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && + QFile::exists(nxmHandlerPath)) { // a portable installation is registered to handle links, use its // configuration - storage = new HandlerStorage(QFileInfo(handlerPath).absolutePath()); + storage = new HandlerStorage(QFileInfo(nxmHandlerPath).absolutePath()); if (forceReg && (QString::compare(QDir::toNativeSeparators( QCoreApplication::applicationFilePath()), - handlerPath, Qt::CaseInsensitive))) { + nxmHandlerPath, Qt::CaseInsensitive))) { if (QMessageBox::question( nullptr, QObject::tr("Change Handler?"), QObject::tr("A nxm handler from a different Mod Organizer " @@ -131,29 +183,54 @@ HandlerStorage* loadStorage(bool forceReg) "replace it? This is usually not necessary unless " "the other installation is defective."), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - storage->registerProxy(QCoreApplication::applicationFilePath()); + storage->registerNxmProxy(QCoreApplication::applicationFilePath()); } } } else if (!noRegister || forceReg) { - // no registration - QMessageBox registerBox( - QMessageBox::Question, QObject::tr("Register?"), - QObject::tr("Mod Organizer is not set up to handle nxm links. " - "Associate it with nxm links?"), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Save); - registerBox.button(QMessageBox::Save)->setText(QObject::tr("No, don't ask again")); - switch (registerBox.exec()) { - case QMessageBox::Yes: { - // base dir is either the global dir if it exists or the local application - // dir - storage = registerExecutable(baseDir, handlerPath, handlerArgs); - } break; - case QMessageBox::Save: { - settings.setValue("noregister", true); - } break; - case QMessageBox::No: { - settings.setValue("noregister", false); - } break; + // no nxm registration + if (!nxmHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { + QMessageBox registerBox( + QMessageBox::Question, QObject::tr("Register?"), + QObject::tr("Mod Organizer is not set up to handle nxm links. " + "Associate it with nxm links?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Save); + registerBox.button(QMessageBox::Save) + ->setText(QObject::tr("No, don't ask again")); + switch (registerBox.exec()) { + case QMessageBox::Yes: { + // base dir is either the global dir if it exists or the local application + // dir + storage = registerNxmExecutable(baseDir, nxmHandlerPath, nxmHandlerArgs); + } break; + case QMessageBox::Save: { + settings.setValue("noregister", true); + } break; + case QMessageBox::No: { + settings.setValue("noregister", false); + } break; + } + } + if (!modlHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { + QMessageBox registerBox( + QMessageBox::Question, QObject::tr("Register?"), + QObject::tr("Mod Organizer is not set up to handle modl links. " + "Associate it with modl links?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Save); + registerBox.button(QMessageBox::Save) + ->setText(QObject::tr("No, don't ask again")); + switch (registerBox.exec()) { + case QMessageBox::Yes: { + // base dir is either the global dir if it exists or the local application + // dir + storage = registerModlExecutable(baseDir, nxmHandlerPath, nxmHandlerArgs); + } break; + case QMessageBox::Save: { + settings.setValue("noregister", true); + } break; + case QMessageBox::No: { + settings.setValue("noregister", false); + } break; + } } } return storage; @@ -189,7 +266,7 @@ static void applyChromeFix() // excluded_schemes exists, protocol_handler existed as well if (handlers.contains("excluded_schemes")) { QJsonObject schemes = handlers["excluded_schemes"].toObject(); - if (schemes["nxm"].toBool(true)) { + if (schemes["nxm"].toBool(true) || schemes["modl"].toBool(true)) { if (QMessageBox::question(nullptr, "Apply Chrome fix", "Chrome may not support nexus links even though the " "association is set up correctly. " @@ -201,6 +278,7 @@ static void applyChromeFix() return; } schemes["nxm"] = false; + schemes["modl"] = false; handlers["excluded_schemes"] = schemes; docMap["protocol_handler"] = handlers; QByteArray result = QJsonDocument(docMap).toJson(); @@ -231,7 +309,7 @@ int main(int argc, char* argv[]) } // Log the arguments - qDebug(qUtf8Printable("\"" + args.join("\" \"") + "\"")); + qDebug() << qUtf8Printable("\"" + args.join("\" \"") + "\""); // No other logs, close the log NxmHandler::LoggerDeinit(); @@ -241,7 +319,8 @@ int main(int argc, char* argv[]) // nxmhandler.exe // forces registration and spawns handler window // - // nxmhandler.exe reg|forcereg game1,game2,game3 C:/path/to/binary + // nxmhandler.exe reg|forcereg schema game1,game2,game3 C:/path/to/binary + // [arguments] // reg: register if noregister==false // forcereg: force registration // @@ -250,10 +329,11 @@ int main(int argc, char* argv[]) if (args.count() > 1) { if ((args.at(1) == "reg") || (args.at(1) == "forcereg")) { - if (args.count() == 4) { - storage->registerHandler(args.at(2).split(",", Qt::SkipEmptyParts), - QDir::toNativeSeparators(args.at(3)), "", true, - forceReg); + if (args.count() == 5 || args.count() == 6) { + auto arguments = args.count() == 6 ? args.at(5) : ""; + storage->registerHandler(args.at(3).split(",", Qt::SkipEmptyParts), + args.at(2), QDir::toNativeSeparators(args.at(4)), + arguments, true, forceReg); if (forceReg) { applyChromeFix(); } @@ -264,12 +344,12 @@ int main(int argc, char* argv[]) } } else if (args.at(1).startsWith("nxm://")) { NXMUrl url(args.at(1)); - QStringList handlerVals = storage->getHandler(url.game()); + QStringList handlerVals = storage->getHandler(url.game(), "nxm"); QString executable = handlerVals.front(); handlerVals.pop_front(); QString arguments = handlerVals.join(" "); if (!executable.isEmpty()) { - handleLink(executable, arguments, args.at(1)); + handleNxmLink(executable, arguments, args.at(1)); return 0; } else { QMessageBox::warning( @@ -284,6 +364,55 @@ int main(int argc, char* argv[]) .arg(url.game())); return 1; } + } else if (args.at(1).startsWith("modl://")) { + QUrl url(args.at(1)); + QUrlQuery params(url.query()); + QStringList handlerVals = storage->getHandler(url.host(), "modl"); + QString executable = handlerVals.front(); + QString downloadUrl = params.queryItemValue("url", QUrl::FullyDecoded); + handlerVals.pop_front(); + auto argumentChunks = handlerVals.first().split(" "); + auto name = params.hasQueryItem("name") + ? params.queryItemValue("name", QUrl::FullyDecoded) + : ""; + auto modName = params.hasQueryItem("modname") + ? params.queryItemValue("modname", QUrl::FullyDecoded) + : ""; + auto version = params.hasQueryItem("version") + ? params.queryItemValue("version", QUrl::FullyDecoded) + : ""; + auto source = params.hasQueryItem("source") + ? params.queryItemValue("source", QUrl::FullyDecoded) + : ""; + if (argumentChunks.contains("%name%")) { + argumentChunks.replace(argumentChunks.indexOf("%name%"), "\"" + name + "\""); + } + if (argumentChunks.contains("%modname%")) { + argumentChunks.replace(argumentChunks.indexOf("%modname%"), + "\"" + modName + "\""); + } + if (argumentChunks.contains("%version%")) { + argumentChunks.replace(argumentChunks.indexOf("%version%"), + "\"" + version + "\""); + } + if (argumentChunks.contains("%source%")) { + argumentChunks.replace(argumentChunks.indexOf("%source%"), + "\"" + source + "\""); + } + QString arguments = argumentChunks.join(" "); + if (!executable.isEmpty()) { + handleModlLink(executable, arguments, downloadUrl); + return 0; + } else { + QMessageBox::warning( + nullptr, QObject::tr("No handler found"), + QObject::tr("No application registered to handle this game (%1).\n" + "If you expected Mod Organizer to handle the link, " + "you have to go to Settings and click the \"Associate " + "with MODL links\"-button.\n") + .arg(url.host())); + return 1; + } } else { QMessageBox::warning(nullptr, QObject::tr("Invalid Arguments"), QObject::tr("Invalid number of parameters")); From f3210aab88af538d10177cbe7e9d2294a5a14653 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Thu, 30 Apr 2026 23:45:03 -0500 Subject: [PATCH 2/7] Display and ensure MODL is registered --- src/handlerstorage.cpp | 2 +- src/handlerwindow.cpp | 59 +++++++++++++++++++++++++++++++----------- src/handlerwindow.h | 6 +++-- src/handlerwindow.ui | 27 ++++++++++++++++--- src/main.cpp | 33 +++++++++++++++++------ 5 files changed, 98 insertions(+), 29 deletions(-) diff --git a/src/handlerstorage.cpp b/src/handlerstorage.cpp index 3dff6c9..233f290 100644 --- a/src/handlerstorage.cpp +++ b/src/handlerstorage.cpp @@ -157,7 +157,7 @@ std::vector> HandlerStorage::knownGames() std::make_tuple("Morrowind", "morrowind", "morrowind"), std::make_tuple("Oblivion", "oblivion", "oblivion"), std::make_tuple( - "Oblivion Reloaded", "oblivionreloaded", "oblivionreloaded"), + "Oblivion Remastered", "oblivionremastered", "oblivionremastered"), std::make_tuple("Fallout 3", "fallout3", "fallout3"), std::make_tuple("Fallout 4", "fallout4", "fallout4"), std::make_tuple("Fallout NV", "falloutnv", "newvegas"), diff --git a/src/handlerwindow.cpp b/src/handlerwindow.cpp index 4482580..f955886 100644 --- a/src/handlerwindow.cpp +++ b/src/handlerwindow.cpp @@ -31,13 +31,23 @@ HandlerWindow::~HandlerWindow() delete ui; } -void HandlerWindow::setPrimaryHandler(const QString& handlerPath) +void HandlerWindow::setNXMHandler(const QString& handlerPath) { if (handlerPath == QCoreApplication::applicationFilePath()) { - ui->registerButton->setEnabled(false); - ui->handlerView->setText(tr("")); + ui->registerNXMButton->setEnabled(false); + ui->nxmHandlerView->setText(tr("")); } else { - ui->handlerView->setText(handlerPath); + ui->nxmHandlerView->setText(handlerPath); + } +} + +void HandlerWindow::setMODLHandler(const QString& handlerPath) +{ + if (handlerPath == QCoreApplication::applicationFilePath()) { + ui->registerMODLButton->setEnabled(false); + ui->modlHandlerView->setText(tr("")); + } else { + ui->modlHandlerView->setText(handlerPath); } } @@ -122,19 +132,38 @@ void HandlerWindow::on_handlersWidget_customContextMenuRequested(const QPoint& p contextMenu.exec(); } -void HandlerWindow::on_registerButton_clicked() +void HandlerWindow::on_registerNXMButton_clicked() { - if (QMessageBox::question(this, tr("Change handler registration?"), - tr("This will make the nxmhandler.exe you called the " - "primary handler registered in the system.\n" - "That has no immediate impact on how links are " - "handled.\nUse this if you moved Mod Organizer " - "or if you uninstalled the Mod Organizer installation " - "that was previously registered. Continue?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - ui->handlerView->setText(tr("")); - ui->registerButton->setEnabled(false); + if (QMessageBox::question( + this, tr("Change handler registration?"), + tr("This will make the nxmhandler.exe you called the NXM handler " + "registered in the system.\n" + "That has no immediate impact on how links are handled.\nUse this " + "if you moved Mod Organizer " + "or if you uninstalled the Mod Organizer installation that was " + "previously registered. Continue?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + ui->nxmHandlerView->setText(tr("")); + ui->registerNXMButton->setEnabled(false); m_Storage->registerNxmProxy(QCoreApplication::applicationFilePath()); } } + +void HandlerWindow::on_registerMODLButton_clicked() +{ + if (QMessageBox::question( + this, tr("Change handler registration?"), + tr("This will make the nxmhandler.exe you called the MODL handler " + "registered in the system.\n" + "That has no immediate impact on how links are handled.\nUse this " + "if you moved Mod Organizer " + "or if you uninstalled the Mod Organizer installation that was " + "previously registered. Continue?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + ui->modlHandlerView->setText(tr("")); + ui->registerMODLButton->setEnabled(false); + + m_Storage->registerModlProxy(QCoreApplication::applicationFilePath()); + } +} diff --git a/src/handlerwindow.h b/src/handlerwindow.h index 68c84bc..1273112 100644 --- a/src/handlerwindow.h +++ b/src/handlerwindow.h @@ -18,7 +18,8 @@ class HandlerWindow : public QMainWindow explicit HandlerWindow(QWidget* parent = 0); ~HandlerWindow(); - void setPrimaryHandler(const QString& handlerPath); + void setNXMHandler(const QString& handlerPath); + void setMODLHandler(const QString& handlerPath); void setHandlerStorage(HandlerStorage* storage); protected: @@ -27,7 +28,8 @@ private slots: void on_handlersWidget_customContextMenuRequested(const QPoint& pos); void addBinaryDialog(); void removeBinary(); - void on_registerButton_clicked(); + void on_registerNXMButton_clicked(); + void on_registerMODLButton_clicked(); private: Ui::HandlerWindow* ui; diff --git a/src/handlerwindow.ui b/src/handlerwindow.ui index dc4d903..72cb743 100644 --- a/src/handlerwindow.ui +++ b/src/handlerwindow.ui @@ -75,19 +75,40 @@ - Primary Handler + Current NXM Handler - + true - + + + Register active + + + + + + + + + + + Current MODL Handler + + + + + + + + Register active diff --git a/src/main.cpp b/src/main.cpp index 215f758..5d64675 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -186,6 +186,19 @@ HandlerStorage* loadStorage(bool forceReg) storage->registerNxmProxy(QCoreApplication::applicationFilePath()); } } + if (forceReg && (QString::compare(QDir::toNativeSeparators( + QCoreApplication::applicationFilePath()), + modlHandlerPath, Qt::CaseInsensitive))) { + if (QMessageBox::question( + nullptr, QObject::tr("Change Handler?"), + QObject::tr("A modl handler from a different Mod Organizer " + "installation has been registered. Do you want to " + "replace it? This is usually not necessary unless " + "the other installation is defective."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + storage->registerModlProxy(QCoreApplication::applicationFilePath()); + } + } } else if (!noRegister || forceReg) { // no nxm registration if (!nxmHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { @@ -422,14 +435,18 @@ int main(int argc, char* argv[]) } else { HandlerWindow win; win.setHandlerStorage(storage.get()); - QSettings handlerReg("HKEY_CURRENT_USER\\Software\\Classes\\nxm\\", - QSettings::NativeFormat); - QStringList handlerVals = HandlerStorage::stripCall( - handlerReg.value("shell/open/command/Default").toString()); - QString handlerPath = handlerVals.front(); - handlerVals.pop_front(); - QString handerArgs = handlerVals.join(" "); - win.setPrimaryHandler(handlerPath); + QSettings nxmHandlerReg("HKEY_CURRENT_USER\\Software\\Classes\\nxm\\", + QSettings::NativeFormat); + QStringList nxmHandlerVals = HandlerStorage::stripCall( + nxmHandlerReg.value("shell/open/command/Default").toString()); + QString nxmHandlerPath = nxmHandlerVals.front(); + win.setNXMHandler(nxmHandlerPath); + QSettings modlHandlerReg("HKEY_CURRENT_USER\\Software\\Classes\\modl\\", + QSettings::NativeFormat); + QStringList modlHandlerVals = HandlerStorage::stripCall( + modlHandlerReg.value("shell/open/command/Default").toString()); + QString modlHandlerPath = modlHandlerVals.front(); + win.setMODLHandler(modlHandlerPath); win.show(); return app.exec(); From 6cb7ed4bd0597e5d040942ac8eba8e00c8712b36 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Sat, 2 May 2026 00:08:57 -0500 Subject: [PATCH 3/7] Escape quotes in passthru arguments --- src/main.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 5d64675..91ce7c6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -398,19 +398,20 @@ int main(int argc, char* argv[]) ? params.queryItemValue("source", QUrl::FullyDecoded) : ""; if (argumentChunks.contains("%name%")) { - argumentChunks.replace(argumentChunks.indexOf("%name%"), "\"" + name + "\""); + argumentChunks.replace(argumentChunks.indexOf("%name%"), + "\"" + name.replace("\"", "\\\"") + "\""); } if (argumentChunks.contains("%modname%")) { argumentChunks.replace(argumentChunks.indexOf("%modname%"), - "\"" + modName + "\""); + "\"" + modName.replace("\"", "\\\"") + "\""); } if (argumentChunks.contains("%version%")) { argumentChunks.replace(argumentChunks.indexOf("%version%"), - "\"" + version + "\""); + "\"" + version.replace("\"", "\\\"") + "\""); } if (argumentChunks.contains("%source%")) { argumentChunks.replace(argumentChunks.indexOf("%source%"), - "\"" + source + "\""); + "\"" + source.replace("\"", "\\\"") + "\""); } QString arguments = argumentChunks.join(" "); if (!executable.isEmpty()) { From c3634f82a1375c9142b4cb7cceb8d71ac74ef378 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Sun, 3 May 2026 14:06:16 -0500 Subject: [PATCH 4/7] Unify various schema handlers --- src/handlerstorage.cpp | 19 +--- src/handlerstorage.h | 6 +- src/handlerwindow.cpp | 26 +++--- src/main.cpp | 207 +++++++++++++++-------------------------- 4 files changed, 94 insertions(+), 164 deletions(-) diff --git a/src/handlerstorage.cpp b/src/handlerstorage.cpp index 233f290..d23978d 100644 --- a/src/handlerstorage.cpp +++ b/src/handlerstorage.cpp @@ -23,25 +23,14 @@ void HandlerStorage::clear() m_Handlers.clear(); } -void HandlerStorage::registerNxmProxy(const QString& proxyPath) +void HandlerStorage::registerSchemaProxy(const QString& proxyPath, + const QString& schema) { - QSettings settings("HKEY_CURRENT_USER\\Software\\Classes\\nxm\\", + QSettings settings("HKEY_CURRENT_USER\\Software\\Classes\\" + schema + "\\", QSettings::NativeFormat); QString myExe = QString("\"%1\" ").arg(QDir::toNativeSeparators(proxyPath)).append("\"%1\""); - settings.setValue("Default", "URL:NXM Protocol"); - settings.setValue("URL Protocol", ""); - settings.setValue("shell/open/command/Default", myExe); - settings.sync(); -} - -void HandlerStorage::registerModlProxy(const QString& proxyPath) -{ - QSettings settings("HKEY_CURRENT_USER\\Software\\Classes\\modl\\", - QSettings::NativeFormat); - QString myExe = - QString("\"%1\" ").arg(QDir::toNativeSeparators(proxyPath)).append("\"%1\""); - settings.setValue("Default", "URL:MODL Protocol"); + settings.setValue("Default", "URL:" + schema.toUpper() + " Protocol"); settings.setValue("URL Protocol", ""); settings.setValue("shell/open/command/Default", myExe); settings.sync(); diff --git a/src/handlerstorage.h b/src/handlerstorage.h index 594ded3..e918eae 100644 --- a/src/handlerstorage.h +++ b/src/handlerstorage.h @@ -23,10 +23,8 @@ class HandlerStorage : public QObject ~HandlerStorage(); void clear(); - /// register the nxm proxy handler - void registerNxmProxy(const QString& proxyPath); - /// register the modl proxy handler - void registerModlProxy(const QString& proxyPath); + /// register a proxy handler + void registerSchemaProxy(const QString& proxyPath, const QString& schema); /// register handler (for all games) void registerHandler(const QString& schema, const QString& executable, const QString& arguments, bool prepend); diff --git a/src/handlerwindow.cpp b/src/handlerwindow.cpp index f955886..1aa06eb 100644 --- a/src/handlerwindow.cpp +++ b/src/handlerwindow.cpp @@ -136,17 +136,16 @@ void HandlerWindow::on_registerNXMButton_clicked() { if (QMessageBox::question( this, tr("Change handler registration?"), - tr("This will make the nxmhandler.exe you called the NXM handler " - "registered in the system.\n" - "That has no immediate impact on how links are handled.\nUse this " - "if you moved Mod Organizer " - "or if you uninstalled the Mod Organizer installation that was " - "previously registered. Continue?"), + tr("This will make the nxmhandler.exe you called the NXM handler registered " + "in the system.\n" + "That has no immediate impact on how links are handled.\n" + "Use this if you moved Mod Organizer or if you uninstalled the Mod " + "Organizer installation that was previously registered. Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { ui->nxmHandlerView->setText(tr("")); ui->registerNXMButton->setEnabled(false); - m_Storage->registerNxmProxy(QCoreApplication::applicationFilePath()); + m_Storage->registerSchemaProxy(QCoreApplication::applicationFilePath(), "nxm"); } } @@ -154,16 +153,15 @@ void HandlerWindow::on_registerMODLButton_clicked() { if (QMessageBox::question( this, tr("Change handler registration?"), - tr("This will make the nxmhandler.exe you called the MODL handler " - "registered in the system.\n" - "That has no immediate impact on how links are handled.\nUse this " - "if you moved Mod Organizer " - "or if you uninstalled the Mod Organizer installation that was " - "previously registered. Continue?"), + tr("This will make the nxmhandler.exe you called the MODL handler registered " + "in the system.\n" + "That has no immediate impact on how links are handled.\n" + "Use this if you moved Mod Organizer or if you uninstalled the Mod " + "Organizer installation that was previously registered. Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { ui->modlHandlerView->setText(tr("")); ui->registerMODLButton->setEnabled(false); - m_Storage->registerModlProxy(QCoreApplication::applicationFilePath()); + m_Storage->registerSchemaProxy(QCoreApplication::applicationFilePath(), "modl"); } } diff --git a/src/main.cpp b/src/main.cpp index 91ce7c6..2df1ca6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -82,30 +82,10 @@ void handleModlLink(const QString& executable, const QString& arguments, SW_SHOWNORMAL); } -HandlerStorage* registerNxmExecutable(const QDir& storagePath, - const QString& handlerPath, - const QString& handlerArgs) -{ - HandlerStorage* storage = nullptr; - if (!handlerPath.isEmpty() && - !handlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { - // a foreign or global nxm handler, register ourself and use that handler as - // an option - if this is another nxmhandler we could run into problems so - // skip it - storage = new HandlerStorage(storagePath.path()); - storage->registerHandler("nxm", handlerPath, handlerArgs, false); - storage->registerNxmProxy(QCoreApplication::applicationFilePath()); - } else { - // no handler registered yet or the existing handler is invalid -> overwrite - storage = new HandlerStorage(storagePath.path()); - storage->registerNxmProxy(QCoreApplication::applicationFilePath()); - } - return storage; -} - -HandlerStorage* registerModlExecutable(const QDir& storagePath, - const QString& handlerPath, - const QString& handlerArgs) +HandlerStorage* registerSchemaExecutable(const QDir& storagePath, + const QString& handlerPath, + const QString& schema, + const QString& handlerArgs) { HandlerStorage* storage = nullptr; if (!handlerPath.isEmpty() && @@ -113,23 +93,19 @@ HandlerStorage* registerModlExecutable(const QDir& storagePath, // a foreign or global nxm handler, register ourself and use that handler as // an option - if this is another nxmhandler we could run into problems so skip it storage = new HandlerStorage(storagePath.path()); - storage->registerHandler("modl", handlerPath, handlerArgs, false); - storage->registerModlProxy(QCoreApplication::applicationFilePath()); + storage->registerHandler(schema, handlerPath, handlerArgs, false); + storage->registerSchemaProxy(QCoreApplication::applicationFilePath(), schema); } else { // no handler registered yet or the existing handler is invalid -> overwrite storage = new HandlerStorage(storagePath.path()); - storage->registerModlProxy(QCoreApplication::applicationFilePath()); + storage->registerSchemaProxy(QCoreApplication::applicationFilePath(), schema); } return storage; } -// ensure a nxmhandler.exe is registered to handle nxm-links, then load the -// handler storage for that registered instance -// (even if it's different from the one actually being run) -HandlerStorage* loadStorage(bool forceReg) +HandlerStorage* registerHandler(HandlerStorage* storage, const QString& schema, + bool forceReg) { - HandlerStorage* storage = nullptr; - QDir globalStorage( QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); globalStorage.cd("../ModOrganizer"); @@ -139,95 +115,53 @@ HandlerStorage* loadStorage(bool forceReg) } else { baseDir = QDir(qApp->applicationDirPath()); } - NxmHandler::LoggerInit(baseDir.filePath("nxmhandler.log")); - QSettings nxmHandlerReg("HKEY_CURRENT_USER\\Software\\Classes\\nxm\\", - QSettings::NativeFormat); - QSettings modlHandlerReg("HKEY_CURRENT_USER\\Software\\Classes\\modl\\", - QSettings::NativeFormat); - QStringList nxmHandlerVals = HandlerStorage::stripCall( - nxmHandlerReg.value("shell/open/command/Default").toString()); - QStringList modlHandlerVals = HandlerStorage::stripCall( - modlHandlerReg.value("shell/open/command/Default").toString()); - QString nxmHandlerPath = nxmHandlerVals.front(); - QString modlHandlerPath = modlHandlerVals.front(); - nxmHandlerVals.pop_front(); - modlHandlerVals.pop_front(); - QString nxmHandlerArgs = nxmHandlerVals.join(" "); - QString modlHandlerArgs = modlHandlerVals.join(" "); - - QDir nxmHandlerBaseDir = QFileInfo(nxmHandlerPath).absoluteDir(); - QDir modlHandlerBaseDir = QFileInfo(modlHandlerPath).absoluteDir(); + QSettings handlerReg("HKEY_CURRENT_USER\\Software\\Classes\\" + schema + "\\", + QSettings::NativeFormat); + QStringList handlerVals = HandlerStorage::stripCall( + handlerReg.value("shell/open/command/Default").toString()); + QString handlerPath = handlerVals.front(); + handlerVals.pop_front(); + QString handlerArgs = handlerVals.join(" "); + + QDir handlerBaseDir = QFileInfo(handlerPath).absoluteDir(); QSettings settings(baseDir.absoluteFilePath("nxmhandler.ini"), QSettings::IniFormat); bool noRegister = settings.value("noregister", false).toBool(); if (globalStorage.exists("nxmhandler.ini") && - nxmHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && - modlHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && - QFile::exists(nxmHandlerPath)) { + handlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && + QFile::exists(handlerPath)) { // global configuration avaible - use it - storage = new HandlerStorage(globalStorage.path()); - } else if (nxmHandlerBaseDir.exists("nxmhandler.ini") && - nxmHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && - modlHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && - QFile::exists(nxmHandlerPath)) { + if (storage == nullptr) + storage = new HandlerStorage(globalStorage.path()); + } else if (handlerBaseDir.exists("nxmhandler.ini") && + handlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive) && + QFile::exists(handlerPath)) { // a portable installation is registered to handle links, use its // configuration - storage = new HandlerStorage(QFileInfo(nxmHandlerPath).absolutePath()); + if (storage == nullptr) + storage = new HandlerStorage(globalStorage.path()); if (forceReg && (QString::compare(QDir::toNativeSeparators( QCoreApplication::applicationFilePath()), - nxmHandlerPath, Qt::CaseInsensitive))) { + handlerPath, Qt::CaseInsensitive))) { if (QMessageBox::question( nullptr, QObject::tr("Change Handler?"), - QObject::tr("A nxm handler from a different Mod Organizer " + QObject::tr("A %1 handler from a different Mod Organizer " "installation has been registered. Do you want to " "replace it? This is usually not necessary unless " - "the other installation is defective."), + "the other installation is defective.") + .arg(schema), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - storage->registerNxmProxy(QCoreApplication::applicationFilePath()); - } - } - if (forceReg && (QString::compare(QDir::toNativeSeparators( - QCoreApplication::applicationFilePath()), - modlHandlerPath, Qt::CaseInsensitive))) { - if (QMessageBox::question( - nullptr, QObject::tr("Change Handler?"), - QObject::tr("A modl handler from a different Mod Organizer " - "installation has been registered. Do you want to " - "replace it? This is usually not necessary unless " - "the other installation is defective."), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - storage->registerModlProxy(QCoreApplication::applicationFilePath()); + storage->registerSchemaProxy(QCoreApplication::applicationFilePath(), schema); } } } else if (!noRegister || forceReg) { - // no nxm registration - if (!nxmHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { - QMessageBox registerBox( - QMessageBox::Question, QObject::tr("Register?"), - QObject::tr("Mod Organizer is not set up to handle nxm links. " - "Associate it with nxm links?"), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Save); - registerBox.button(QMessageBox::Save) - ->setText(QObject::tr("No, don't ask again")); - switch (registerBox.exec()) { - case QMessageBox::Yes: { - // base dir is either the global dir if it exists or the local application - // dir - storage = registerNxmExecutable(baseDir, nxmHandlerPath, nxmHandlerArgs); - } break; - case QMessageBox::Save: { - settings.setValue("noregister", true); - } break; - case QMessageBox::No: { - settings.setValue("noregister", false); - } break; - } - } - if (!modlHandlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { + // no handler registration + if (!handlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { QMessageBox registerBox( QMessageBox::Question, QObject::tr("Register?"), - QObject::tr("Mod Organizer is not set up to handle modl links. " - "Associate it with modl links?"), + QObject::tr("Mod Organizer is not set up to handle %1 links. " + "Associate it with %1 links?") + .arg(schema), QMessageBox::Yes | QMessageBox::No | QMessageBox::Save); registerBox.button(QMessageBox::Save) ->setText(QObject::tr("No, don't ask again")); @@ -235,7 +169,8 @@ HandlerStorage* loadStorage(bool forceReg) case QMessageBox::Yes: { // base dir is either the global dir if it exists or the local application // dir - storage = registerModlExecutable(baseDir, nxmHandlerPath, nxmHandlerArgs); + if (storage == nullptr) + storage = registerSchemaExecutable(baseDir, handlerPath, schema, handlerArgs); } break; case QMessageBox::Save: { settings.setValue("noregister", true); @@ -249,6 +184,30 @@ HandlerStorage* loadStorage(bool forceReg) return storage; } +// ensure a nxmhandler.exe is registered to handle links, then load the +// handler storage for that registered instance +// (even if it's different from the one actually being run) +HandlerStorage* loadStorage(bool forceReg) +{ + HandlerStorage* storage = nullptr; + + QDir globalStorage( + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); + globalStorage.cd("../ModOrganizer"); + QDir baseDir; + if (globalStorage.exists()) { + baseDir = globalStorage; + } else { + baseDir = QDir(qApp->applicationDirPath()); + } + NxmHandler::LoggerInit(baseDir.filePath("nxmhandler.log")); + QStringList schemas = {"nxm", "modl"}; + for (auto schema : schemas) { + storage = registerHandler(storage, schema, forceReg); + } + return storage; +} + static void applyChromeFix() { QString dataPath = QDir::fromNativeSeparators( @@ -384,34 +343,20 @@ int main(int argc, char* argv[]) QString executable = handlerVals.front(); QString downloadUrl = params.queryItemValue("url", QUrl::FullyDecoded); handlerVals.pop_front(); - auto argumentChunks = handlerVals.first().split(" "); - auto name = params.hasQueryItem("name") - ? params.queryItemValue("name", QUrl::FullyDecoded) - : ""; - auto modName = params.hasQueryItem("modname") - ? params.queryItemValue("modname", QUrl::FullyDecoded) - : ""; - auto version = params.hasQueryItem("version") - ? params.queryItemValue("version", QUrl::FullyDecoded) - : ""; - auto source = params.hasQueryItem("source") - ? params.queryItemValue("source", QUrl::FullyDecoded) - : ""; - if (argumentChunks.contains("%name%")) { - argumentChunks.replace(argumentChunks.indexOf("%name%"), - "\"" + name.replace("\"", "\\\"") + "\""); - } - if (argumentChunks.contains("%modname%")) { - argumentChunks.replace(argumentChunks.indexOf("%modname%"), - "\"" + modName.replace("\"", "\\\"") + "\""); + auto argumentChunks = handlerVals.first().split(" "); + QMap targetParams = { + {"name", ""}, {"modname", ""}, {"version", ""}, {"source", ""}}; + for (auto param : targetParams.asKeyValueRange()) { + auto value = params.hasQueryItem(param.first) + ? params.queryItemValue(param.first, QUrl::FullyDecoded) + : ""; + targetParams[param.first] = value; } - if (argumentChunks.contains("%version%")) { - argumentChunks.replace(argumentChunks.indexOf("%version%"), - "\"" + version.replace("\"", "\\\"") + "\""); - } - if (argumentChunks.contains("%source%")) { - argumentChunks.replace(argumentChunks.indexOf("%source%"), - "\"" + source.replace("\"", "\\\"") + "\""); + for (auto param : targetParams.asKeyValueRange()) { + if (argumentChunks.contains("%" + param.first + "%")) { + argumentChunks.replace(argumentChunks.indexOf("%" + param.first + "%"), + "\"" + param.second.replace("\"", "\\\"") + "\""); + } } QString arguments = argumentChunks.join(" "); if (!executable.isEmpty()) { From 3ac6eabdfbadcc1e41586199c18e5740d39cc703 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Sun, 3 May 2026 17:28:13 -0500 Subject: [PATCH 5/7] Bump version and rename app - We can decide if we want to rename the exe itself later --- src/handlerwindow.ui | 4 ++-- src/version.rc | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/handlerwindow.ui b/src/handlerwindow.ui index 72cb743..66c75df 100644 --- a/src/handlerwindow.ui +++ b/src/handlerwindow.ui @@ -11,14 +11,14 @@ - NXM Handler + MO2 Download Handler - Use this list to configure programs to handle nxm links. Different Programs can be set up to handle links for different games. If the same game is supported by multiple binaries, the top-most is used. + Use this list to configure programs to handle download links. Different programs can be set up to handle links for different games. If the same game is supported by multiple binaries, the top-most is used. true diff --git a/src/version.rc b/src/version.rc index e5cd977..ca52097 100644 --- a/src/version.rc +++ b/src/version.rc @@ -1,7 +1,7 @@ #include "Winver.h" -#define VER_FILEVERSION 1,2,0,0 -#define VER_FILEVERSION_STR "1,2,0,0\0" +#define VER_FILEVERSION 1,3,0,0 +#define VER_FILEVERSION_STR "1,3,0,0\0" VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION @@ -17,10 +17,10 @@ BEGIN BLOCK "040904B0" BEGIN VALUE "FileVersion", VER_FILEVERSION_STR - VALUE "CompanyName", "Tannin\0" - VALUE "FileDescription", "NXM Link Proxy\0" + VALUE "CompanyName", "Mod Organizer 2 Team\0" + VALUE "FileDescription", "MO2 Download Proxy\0" VALUE "OriginalFilename", "nxmhandler.exe\0" - VALUE "ProductName", "NXM Handler\0" + VALUE "ProductName", "MO2 Download Proxy\0" VALUE "ProductVersion", VER_FILEVERSION_STR END END From da4ab445635eda04c2a23d80bf901aeea8865dc5 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Sun, 3 May 2026 17:28:49 -0500 Subject: [PATCH 6/7] Pass game slug to MO2 download args --- src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.cpp b/src/main.cpp index 2df1ca6..7cd172f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -358,6 +358,7 @@ int main(int argc, char* argv[]) "\"" + param.second.replace("\"", "\\\"") + "\""); } } + argumentChunks.append("-g " + url.host()); QString arguments = argumentChunks.join(" "); if (!executable.isEmpty()) { handleModlLink(executable, arguments, downloadUrl); From e0a2fc8e92b86f20bf0c2df451d9159e31325aee Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Sun, 3 May 2026 17:29:50 -0500 Subject: [PATCH 7/7] Remove invalid check preventing init when missing ini --- src/main.cpp | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 7cd172f..98dc0d1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -156,29 +156,26 @@ HandlerStorage* registerHandler(HandlerStorage* storage, const QString& schema, } } else if (!noRegister || forceReg) { // no handler registration - if (!handlerPath.endsWith("nxmhandler.exe", Qt::CaseInsensitive)) { - QMessageBox registerBox( - QMessageBox::Question, QObject::tr("Register?"), - QObject::tr("Mod Organizer is not set up to handle %1 links. " - "Associate it with %1 links?") - .arg(schema), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Save); - registerBox.button(QMessageBox::Save) - ->setText(QObject::tr("No, don't ask again")); - switch (registerBox.exec()) { - case QMessageBox::Yes: { - // base dir is either the global dir if it exists or the local application - // dir - if (storage == nullptr) - storage = registerSchemaExecutable(baseDir, handlerPath, schema, handlerArgs); - } break; - case QMessageBox::Save: { - settings.setValue("noregister", true); - } break; - case QMessageBox::No: { - settings.setValue("noregister", false); - } break; - } + QMessageBox registerBox( + QMessageBox::Question, QObject::tr("Register?"), + QObject::tr("Mod Organizer is not set up to handle %1 links. " + "Associate it with %1 links?") + .arg(schema), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Save); + registerBox.button(QMessageBox::Save)->setText(QObject::tr("No, don't ask again")); + switch (registerBox.exec()) { + case QMessageBox::Yes: { + // base dir is either the global dir if it exists or the local application + // dir + if (storage == nullptr) + storage = registerSchemaExecutable(baseDir, handlerPath, schema, handlerArgs); + } break; + case QMessageBox::Save: { + settings.setValue("noregister", true); + } break; + case QMessageBox::No: { + settings.setValue("noregister", false); + } break; } } return storage;