Compare commits

...
26 Commits
Author SHA1 Message Date
Chris Bessent 2a5dd6c7cf Bump to version 2.4.2 2021-05-11 06:25:20 -07:00
Chris Bessent ce2450885c Bump version to 2.4.2rc1.1 2021-05-01 19:38:14 -07:00
Chris Bessent d78cb58593 Update translation file 2021-05-01 17:53:37 -07:00
Chris Bessent 2c54d128b4 Add some more Microsoft Store errors 2021-05-01 17:10:27 -07:00
Chris Bessent f71288db16 Bump to 2.4.2 RC 1 2021-05-01 15:23:16 -07:00
Chris Bessent 8bac17c2c5 Merge pull request #1508 from LostDragonist/gamepass
Add log for Microsoft Store / Gamepass installs
2021-05-01 15:21:45 -07:00
Chris Bessent 4509911a8e Add log for Microsoft Store / Gamepass installs 2021-05-01 15:18:15 -07:00
Chris Bessent f098491306 Merge pull request #1503 from LostDragonist/filename
Sanitize download file names
2021-05-01 14:38:17 -07:00
Chris Bessent bcaceb9637 Merge pull request #1504 from LostDragonist/skipcwd
Ignore "." and ".." in archives
2021-05-01 14:38:10 -07:00
Chris Bessent b7e8e69f47 Merge pull request #1507 from LostDragonist/tutorials
Fix up tutorials
2021-05-01 14:37:59 -07:00
Chris Bessent a8bcbf6555 Fix up tutorials 2021-04-30 08:41:29 -07:00
Chris Bessent aafd523736 Merge pull request #1505 from LostDragonist/foresto
Add foresto as a contributor
2021-04-29 02:02:19 -07:00
Chris Bessent 4a9da205cd Merge pull request #1485 from foresto/patch-1
Reduce log level of harmless failures
2021-04-29 02:02:05 -07:00
Chris Bessent 68e7820154 Add foresto as a contributor 2021-04-29 02:01:38 -07:00
Chris Bessent 3c6190d713 Merge pull request #1470 from isanae/filtered-nav-crash
Crash for next/previous in mod info dialog
2021-04-29 01:59:05 -07:00
Chris Bessent 1197b562f6 Merge pull request #1496 from LostDragonist/nexus_links
Change the "Visit Nexus" button to support alternate sources
2021-04-29 01:57:13 -07:00
Chris Bessent 7df5329ea0 Merge pull request #1497 from LostDragonist/profile_fix
Fix selecting the profile in the dialog
2021-04-29 01:56:52 -07:00
Chris Bessent dc3dcd9500 Fix fixDirectoryName includes 2021-04-29 01:50:23 -07:00
Chris Bessent cc8fb9bd23 Ignore "." and ".." in archives 2021-04-29 01:29:38 -07:00
Chris Bessent 9638a797d1 Sanitize download file names
Also switch instance name sanitizer to MOBase version
2021-04-29 00:50:11 -07:00
Chris Bessent 8308dfcbff Fix selecting the profile in the dialog
Previously, when clicking the "select" button in the profiles
dialog (when the dialog was launched in the menu or toolbar),
everything would be refreshed without checking the selected
profile in the dialog.

Now, the selected profile is set with some signal blocking to
help prevent redundant refreshes from occurring.
2021-04-25 05:09:58 -07:00
Chris Bessent dfbaed0ade Add a separator after the primary game and sort the secondary games 2021-04-25 04:45:35 -07:00
Chris Bessent e475893ff4 Change the "Visit Nexus" button to support alternate sources
If a game plugin supports more than one Nexus site for downloads,
the "Visit Nexus" button will be turned into a drop-down that
lets you select each site.
2021-04-25 04:13:12 -07:00
Al 40a4bf1e3a Update issue templates 2021-04-08 16:51:11 +02:00
Forest 3ee2598533 Reduce log level of harmless failures
These two messages (along with their glaring red X icons) are cluttering up the log on linux/wine systems, misleading people who are trying to diagnose real problems. Let's reduce their log priority to make it clear that they're harmless.

As mentioned here:
https://github.com/ModOrganizer2/modorganizer/issues/372#issuecomment-575231673
2021-04-05 23:10:54 -07:00
isanae 234ee923fe model() can return null for invalid indexes
happens when looking at the mod info for a mod filtered out of the mod list
2021-03-20 14:51:44 -04:00
26 changed files with 429 additions and 294 deletions
@@ -2,7 +2,7 @@
name: Game support Request
about: Request support for a new game
title: Add support for game [GAME NAME]
labels: 'additional games support, Feature Request, area: mo2 game plugins'
labels: 'Feature Request, additional games support, area: mo2 game plugins'
assignees: ''
---
@@ -18,7 +18,7 @@ assignees: ''
- **Nexus ID [optional]:** ID of the game on Nexus (you can usually find this in Nexus URL).
- **Executable:** Name of the main executable for the game (relative to the game folder).
- **Launcher [optional]:** Name of the game launcher (relative to the game folder).
- **Data path:** Path to the data folder of the game (relative to the game folder).
- **Data path:** Path to the data folder of the game (relative to the game folder). Please note that the Virtual Files System often does not work for top level dlls or exe files.
- **Documents path:** Path to folder containing INI files, etc., for the game (usually under "My Games", or the game folder itself).
- **Saves directory [optional]:** Path to the folder containing save games (this default to the path above).
- **Save extension [optional]:** Extension of the saves
+5
View File
@@ -469,6 +469,11 @@
<string notr="true">Drew Warwick</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">foresto</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">GamerPoet</string>
+13 -6
View File
@@ -158,14 +158,14 @@ protected:
currentName = std::get<0>(p)[0];
}
// If the name is different, we need to create a directory from what we have
// If the name is different, we need to create a directory from what we have
// accumulated:
if (currentName != std::get<0>(p)[0]) {
// We may or may not have an index here, it depends on the type of archive (some archives list
// intermediate non-empty folders, some don't):
entries.push_back(std::make_shared<ArchiveFileTreeImpl>(parent, currentName, currentIndex, std::move(currentFiles)));
currentFiles.clear(); // Back to a valid state.
// Reset the index:
@@ -200,7 +200,7 @@ protected:
if (currentName != "") {
entries.push_back(std::make_shared<ArchiveFileTreeImpl>(parent, currentName, currentIndex, std::move(currentFiles)));
}
// Let the parent class sort the entries:
return false;
}
@@ -214,7 +214,7 @@ private:
mutable std::vector<File> m_Files;
};
std::shared_ptr<ArchiveFileTree> ArchiveFileTree::makeTree(Archive const& archive)
std::shared_ptr<ArchiveFileTree> ArchiveFileTree::makeTree(Archive const& archive)
{
auto const& data = archive.getFileList();
@@ -222,9 +222,16 @@ std::shared_ptr<ArchiveFileTree> ArchiveFileTree::makeTree(Archive const& archiv
files.reserve(data.size());
for (size_t i = 0; i < data.size(); ++i) {
// Ignore "." and ".." as they're useless and muck things up
if (data[i]->getArchiveFilePath().compare(L".") == 0 ||
data[i]->getArchiveFilePath().compare(L"..") == 0)
{
continue;
}
files.push_back(std::make_tuple(
QString::fromStdWString(data[i]->getArchiveFilePath()).replace("\\", "/").split("/", Qt::SkipEmptyParts),
data[i]->isDirectory(),
QString::fromStdWString(data[i]->getArchiveFilePath()).replace("\\", "/").split("/", Qt::SkipEmptyParts),
data[i]->isDirectory(),
(int) i));
}
+49 -4
View File
@@ -8,6 +8,7 @@
#include <iplugingame.h>
#include <report.h>
#include <utility.h>
#include "filesystemutilities.h"
namespace cid
{
@@ -292,7 +293,7 @@ void GamePage::select(IPluginGame* game, const QString& dir)
Game* checked = findGame(game);
if (checked) {
if (!checked->installed) {
if (!checked->installed || (detectMicrosoftStore(checked->dir) && !confirmMicrosoftStore(checked->dir, checked->game))) {
if (dir.isEmpty()) {
// the selected game has no installation directory and none was given,
// ask the user
@@ -304,6 +305,9 @@ void GamePage::select(IPluginGame* game, const QString& dir)
if (path.isEmpty()) {
// cancelled
checked = nullptr;
} else if (detectMicrosoftStore(path) && !confirmMicrosoftStore(path, game)) {
// cancelled
checked = nullptr;
} else {
// check whether a plugin supports the given directory; this can
// return the same plugin, a different one, or null
@@ -353,6 +357,13 @@ void GamePage::selectCustom()
return;
}
// Microsoft store games are not supported
if (detectMicrosoftStore(path) && !confirmMicrosoftStore(path, nullptr)) {
// reselect the previous button
selectButton(m_selection);
return;
}
// try to find a plugin that likes this directory
for (auto& g : m_games) {
if (g->game->looksValid(path)) {
@@ -593,6 +604,11 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
return g;
}
if (detectMicrosoftStore(path) && confirmMicrosoftStore(path, g->game)) {
// okay
return g;
}
// the selected game can't use that folder, find another one
IPluginGame* otherGame = nullptr;
@@ -639,6 +655,35 @@ GamePage::Game* GamePage::checkInstallation(const QString& path, Game* g)
return g;
}
bool GamePage::detectMicrosoftStore(const QString& path)
{
return path.contains("/ModifiableWindowsApps/") ||
path.contains("/WindowsApps/");
}
bool GamePage::confirmMicrosoftStore(const QString& path, IPluginGame* game)
{
const auto r = TaskDialog(&m_dlg)
.title(QObject::tr("Microsoft Store game"))
.main(QObject::tr("Microsoft Store game"))
.content(QObject::tr(
"The folder %1 seems to be a Microsoft Store game install. Games"
" installed through the Microsoft Store are not supported by Mod Organizer"
" and will not work properly.")
.arg(path))
.button({
game ? QObject::tr("Use this folder for %1").arg(game->gameName())
: QObject::tr("Use this folder"),
QObject::tr("I know what I'm doing"),
QMessageBox::Ignore})
.button({
QObject::tr("Cancel"),
QMessageBox::Cancel})
.exec();
return (r == QMessageBox::Ignore);
}
bool GamePage::confirmUnknown(const QString& path, IPluginGame* game)
{
const auto r = TaskDialog(&m_dlg)
@@ -856,7 +901,7 @@ QString NamePage::selectedInstanceName() const
}
const auto text = ui->instanceName->text().trimmed();
return InstanceManager::singleton().sanitizeInstanceName(text);
return MOBase::sanitizeFileName(text);
}
void NamePage::onChanged()
@@ -883,7 +928,7 @@ bool NamePage::checkName(QString parentDir, QString name)
if (name.isEmpty()) {
empty = true;
} else {
if (InstanceManager::singleton().validInstanceName(name)) {
if (MOBase::validFileName(name)) {
exists = QDir(parentDir).exists(name);
} else {
invalid = true;
@@ -1108,7 +1153,7 @@ bool PathsPage::checkPath(
} else {
const QDir d(path);
if (m.validInstanceName(d.dirName())) {
if (MOBase::validFileName(d.dirName())) {
if (m_dlg.rawCreationInfo().type == CreateInstanceDialog::Portable) {
// the default data path for a portable instance is the application
// directory, so it's not an error if it exists
+9
View File
@@ -354,6 +354,15 @@ private:
MOBase::IPluginGame* confirmOtherGame(
const QString& path,
MOBase::IPluginGame* selectedGame, MOBase::IPluginGame* guessedGame);
// detects if the given path likely contains a Microsoft Store game
//
bool detectMicrosoftStore(const QString& path);
// tells the user that the path probably contains a Microsoft Store game that
// is not supported, returns true if the user decides to accept anyway.
//
bool confirmMicrosoftStore(const QString& path, MOBase::IPluginGame* game);
};
+3 -2
View File
@@ -33,6 +33,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "shared/util.h"
#include <utility.h>
#include <report.h>
#include "filesystemutilities.h"
#include <QTimer>
#include <QFileInfo>
@@ -1456,7 +1457,7 @@ void DownloadManager::markUninstalled(QString fileName)
QString DownloadManager::getDownloadFileName(const QString &baseName, bool rename) const
{
QString fullPath = m_OutputDirectory + "/" + baseName;
QString fullPath = m_OutputDirectory + "/" + MOBase::sanitizeFileName(baseName);
if (QFile::exists(fullPath) && rename) {
int i = 1;
while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) {
@@ -1476,7 +1477,7 @@ QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply)
std::cmatch result;
if (std::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) {
return QString::fromUtf8(result.str(1).c_str());
return MOBase::sanitizeFileName(QString::fromUtf8(result.str(1).c_str()));
}
}
+2 -2
View File
@@ -130,7 +130,7 @@ Module::FileInfo Module::getFileInfo() const
return {};
}
log::error(
log::debug(
"GetFileVersionInfoSizeW() failed on '{}', {}",
m_path, formatSystemMessage(e));
@@ -268,7 +268,7 @@ QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const
if (h.get() == INVALID_HANDLE_VALUE) {
const auto e = GetLastError();
log::error(
log::debug(
"can't open file '{}' for timestamp, {}",
m_path, formatSystemMessage(e));
+1 -1
View File
@@ -21,7 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "installationmanager.h"
#include "utility.h"
#include "filesystemutilities.h"
#include "report.h"
#include "categories.h"
#include "questionboxmemory.h"
+2 -30
View File
@@ -32,6 +32,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <iplugingame.h>
#include <utility.h>
#include <log.h>
#include "filesystemutilities.h"
#include <QCoreApplication>
#include <QDir>
@@ -721,7 +722,7 @@ const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(
QString InstanceManager::makeUniqueName(const QString& instanceName) const
{
const QString sanitized = sanitizeInstanceName(instanceName);
const QString sanitized = MOBase::sanitizeFileName(instanceName);
// trying "name (N)"
QString name = sanitized;
@@ -742,35 +743,6 @@ bool InstanceManager::instanceExists(const QString& instanceName) const
return root.exists(instanceName);
}
QString InstanceManager::sanitizeInstanceName(const QString &name) const
{
QString new_name = name;
// Restrict the allowed characters
new_name = new_name.remove(QRegExp("[^A-Za-z0-9 _=+;!@#$%^'\\-\\.\\[\\]\\{\\}\\(\\)]"));
// Don't end in spaces and periods
new_name = new_name.remove(QRegExp("\\.*$"));
new_name = new_name.remove(QRegExp(" *$"));
// Recurse until stuff stops changing
if (new_name != name) {
return sanitizeInstanceName(new_name);
}
return new_name;
}
bool InstanceManager::validInstanceName(const QString& instanceName) const
{
if (instanceName.isEmpty()) {
return false;
}
return (instanceName == sanitizeInstanceName(instanceName));
}
std::unique_ptr<Instance> selectInstance()
{
auto& m = InstanceManager::singleton();
-10
View File
@@ -305,10 +305,6 @@ public:
//
std::vector<QString> globalInstancePaths() const;
// returns `name` modified so that it is a valid instance name
//
QString sanitizeInstanceName(const QString &name) const;
// sanitizes the given instance name and either
// 1) returns it if there is no instance with this name
// 2) tries to add " (N)" at the end until it works
@@ -321,12 +317,6 @@ public:
//
bool instanceExists(const QString& instanceName) const;
// returns whether the given instance name would be a valid name; this does
// not check whether the instance already exists, it's basiscally just a check
// against what sanitizeInstanceName() returns
//
bool validInstanceName(const QString& instanceName) const;
// returns the absolute path of a global instance with the given name; this
// does not check if the name is valid or if exists
//
+4 -3
View File
@@ -10,6 +10,7 @@
#include <utility.h>
#include <report.h>
#include <iplugingame.h>
#include "filesystemutilities.h"
using namespace MOBase;
@@ -109,10 +110,10 @@ QString getInstanceName(
if (text->text().isEmpty()) {
error->setText("");
} else if (!m.validInstanceName(text->text())) {
} else if (!MOBase::validFileName(text->text())) {
error->setText(QObject::tr("The instance name must be a valid folder name."));
} else {
const auto name = m.sanitizeInstanceName(text->text());
const auto name = MOBase::sanitizeFileName(text->text());
if ((name != oldName) && m.instanceExists(text->text())) {
error->setText(QObject::tr("An instance with this name already exists."));
@@ -136,7 +137,7 @@ QString getInstanceName(
return {};
}
return m.sanitizeInstanceName(text->text());
return MOBase::sanitizeFileName(text->text());
}
+52 -3
View File
@@ -1440,12 +1440,36 @@ void MainWindow::registerModPage(IPluginModPage *modPage)
ui->actionModPage->menu()->addAction(action);
}
bool MainWindow::registerNexusPage(const QString& gameName)
{
// Get the plugin
IPluginGame* plugin = m_OrganizerCore.getGame(gameName);
if (plugin == nullptr)
return false;
// Create an action
QAction* action = new QAction(
plugin->gameIcon(),
QObject::tr("Visit %1 on Nexus").arg(plugin->gameName()),
this);
// Bind the action
connect(action, &QAction::triggered, this, [this, gameName]() {
shell::Open(QUrl(NexusInterface::instance().getGameURL(gameName)));
}, Qt::QueuedConnection);
// Add the action
ui->actionModPage->menu()->addAction(action);
return true;
}
void MainWindow::updateModPageMenu()
{
// Clear the menu:
ui->actionModPage->menu()->clear();
ui->actionModPage->menu()->addAction(ui->actionNexus);
// Determine the loaded mod page plugins
std::vector<IPluginModPage*> modPagePlugins = m_PluginContainer.plugins<IPluginModPage>();
// Sort the plugins by display name
@@ -1466,14 +1490,30 @@ void MainWindow::updateModPageMenu()
registerModPage(modPagePlugin);
}
// Add the primary game (with a separator)
registerNexusPage(m_OrganizerCore.managedGame()->gameShortName());
ui->actionModPage->menu()->addSeparator();
// Add the secondary games (sorted)
bool secondaryGameAdded = false;
QStringList secondaryGames = m_OrganizerCore.managedGame()->validShortNames();
secondaryGames.sort(Qt::CaseInsensitive);
for (auto gameName : secondaryGames)
{
if (registerNexusPage(gameName)) {
secondaryGameAdded = true;
}
}
// No mod page plugin and the menu was visible:
if (modPagePlugins.empty()) {
bool keepOriginalAction = modPagePlugins.size() == 0 && !secondaryGameAdded;
if (keepOriginalAction) {
ui->toolBar->insertAction(ui->actionAdd_Profile, ui->actionNexus);
}
else {
ui->toolBar->removeAction(ui->actionNexus);
}
ui->actionModPage->setVisible(!modPagePlugins.empty());
ui->actionModPage->setVisible(!keepOriginalAction);
}
void MainWindow::startExeAction()
@@ -2148,6 +2188,15 @@ void MainWindow::on_actionAdd_Profile_triggered()
profilesDialog.exec();
m_SavesTab->refreshSaveList(); // since the save list may now be outdated we have to refresh it completely
if (profilesDialog.selectedProfile())
{
// Change profile while blocking signals to prevent extra signals being sent
// Doesn't matter much as refreshProfiles() is being called after this
ui->profileBox->blockSignals(true);
ui->profileBox->setCurrentText(profilesDialog.selectedProfile().value());
ui->profileBox->blockSignals(false);
}
if (refreshProfiles() && !profilesDialog.failed()) {
break;
}
+1
View File
@@ -187,6 +187,7 @@ private:
void setToolbarButtonStyle(Qt::ToolButtonStyle s);
void registerModPage(MOBase::IPluginModPage* modPage);
bool registerNexusPage(const QString& gameName);
void registerPluginTool(MOBase::IPluginTool* tool, QString name = QString(), QMenu* menu = nullptr);
void updateToolbarMenu();
+1 -1
View File
@@ -34,7 +34,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "shared/filesorigin.h"
#include "shared/appconfig.h"
#include <utility.h>
#include "filesystemutilities.h"
#include <report.h>
#include <QFileInfo>
+8 -1
View File
@@ -5,7 +5,7 @@
#include <widgetutility.h>
#include <utility.h>
#include "filesystemutilities.h"
#include <report.h>
#include "ui_mainwindow.h"
@@ -347,6 +347,9 @@ QModelIndexList ModListView::indexViewToModel(const QModelIndexList& index) cons
QModelIndex ModListView::nextIndex(const QModelIndex& index) const
{
auto* model = index.model();
if (!model) {
return {};
}
if (model->rowCount(index) > 0) {
return model->index(0, index.column(), index);
@@ -372,6 +375,10 @@ QModelIndex ModListView::prevIndex(const QModelIndex& index) const
}
auto* model = index.model();
if (!model) {
return {};
}
auto prev = model->index((index.row() - 1) % model->rowCount(index.parent()), index.column(), index.parent());
if (model->rowCount(prev) > 0) {
+1 -1
View File
@@ -8,7 +8,7 @@
#include <log.h>
#include <report.h>
#include <utility.h>
#include "filesystemutilities.h"
#include "categories.h"
#include "filedialogmemory.h"
+241 -217
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -21,7 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "modinfo.h"
#include "settings.h"
#include <utility.h>
#include "filesystemutilities.h"
#include "shared/appconfig.h"
#include <iplugingame.h>
#include <report.h>
+1 -1
View File
@@ -19,7 +19,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "profileinputdialog.h"
#include "ui_profileinputdialog.h"
#include <utility.h>
#include "filesystemutilities.h"
ProfileInputDialog::ProfileInputDialog(QWidget *parent) :
QDialog(parent),
+1 -1
View File
@@ -28,7 +28,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "profileinputdialog.h"
#include "report.h"
#include "transfersavesdialog.h"
#include "utility.h"
#include "filesystemutilities.h"
#include "settings.h"
#include "localsavegames.h"

Some files were not shown because too many files have changed in this diff Show More