PrismLauncher/launcher/InstanceList.cpp

1011 lines
30 KiB
C++
Raw Permalink Normal View History

// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
2013-01-14 23:42:38 +00:00
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
2013-10-06 00:13:40 +01:00
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
2013-01-14 23:42:38 +00:00
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
2013-01-14 23:42:38 +00:00
*/
#include <QDebug>
2013-02-19 18:15:22 +00:00
#include <QDir>
2018-07-24 00:24:06 +01:00
#include <QDirIterator>
2013-02-19 18:15:22 +00:00
#include <QFile>
2018-07-24 00:24:06 +01:00
#include <QFileSystemWatcher>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeData>
#include <QPair>
#include <QSet>
#include <QStack>
#include <QTextStream>
#include <QThread>
#include <QTimer>
#include <QUuid>
#include <QXmlStreamReader>
2013-02-19 18:15:22 +00:00
2015-02-09 00:51:14 +00:00
#include "BaseInstance.h"
#include "ExponentialSeries.h"
#include "FileSystem.h"
#include "InstanceList.h"
2018-07-24 00:24:06 +01:00
#include "InstanceTask.h"
#include "NullInstance.h"
#include "WatchLock.h"
#include "minecraft/MinecraftInstance.h"
#include "settings/INISettingsObject.h"
2018-07-24 00:24:06 +01:00
#ifdef Q_OS_WIN32
#include <Windows.h>
#endif
2018-07-24 00:24:06 +01:00
const static int GROUP_FILE_FORMAT_VERSION = 1;
2013-02-19 18:15:22 +00:00
InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent)
2018-07-24 00:24:06 +01:00
: QAbstractListModel(parent), m_globalSettings(settings)
2013-02-19 18:15:22 +00:00
{
2018-07-15 13:51:05 +01:00
resumeWatch();
2018-07-24 00:24:06 +01:00
// Create aand normalize path
if (!QDir::current().exists(instDir)) {
2018-07-24 00:24:06 +01:00
QDir::current().mkpath(instDir);
}
connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated);
// NOTE: canonicalPath requires the path to exist. Do not move this above the creation block!
m_instDir = QDir(instDir).canonicalPath();
m_watcher = new QFileSystemWatcher(this);
connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged);
m_watcher->addPath(m_instDir);
2013-02-19 18:15:22 +00:00
}
InstanceList::~InstanceList() {}
Qt::DropActions InstanceList::supportedDragActions() const
{
return Qt::MoveAction;
}
Qt::DropActions InstanceList::supportedDropActions() const
{
return Qt::MoveAction;
}
bool InstanceList::canDropMimeData(const QMimeData* data,
[[maybe_unused]] Qt::DropAction action,
[[maybe_unused]] int row,
[[maybe_unused]] int column,
[[maybe_unused]] const QModelIndex& parent) const
{
if (data && data->hasFormat("application/x-instanceid")) {
return true;
}
return false;
}
bool InstanceList::dropMimeData(const QMimeData* data,
[[maybe_unused]] Qt::DropAction action,
[[maybe_unused]] int row,
[[maybe_unused]] int column,
[[maybe_unused]] const QModelIndex& parent)
{
if (data && data->hasFormat("application/x-instanceid")) {
return true;
}
return false;
}
QStringList InstanceList::mimeTypes() const
{
auto types = QAbstractListModel::mimeTypes();
types.push_back("application/x-instanceid");
return types;
}
QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const
{
auto mimeData = QAbstractListModel::mimeData(indexes);
if (indexes.size() == 1) {
auto instanceId = data(indexes[0], InstanceIDRole).toString();
mimeData->setData("application/x-instanceid", instanceId.toUtf8());
}
return mimeData;
}
QStringList InstanceList::getLinkedInstancesById(const QString& id) const
{
QStringList linkedInstances;
for (auto inst : m_instances) {
if (inst->isLinkedToInstanceId(id))
linkedInstances.append(inst->id());
}
return linkedInstances;
}
int InstanceList::rowCount(const QModelIndex& parent) const
{
2018-07-15 13:51:05 +01:00
Q_UNUSED(parent);
return m_instances.count();
}
QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const
{
2018-07-15 13:51:05 +01:00
Q_UNUSED(parent);
if (row < 0 || row >= m_instances.size())
return QModelIndex();
return createIndex(row, column, (void*)m_instances.at(row).get());
}
QVariant InstanceList::data(const QModelIndex& index, int role) const
{
if (!index.isValid()) {
2018-07-15 13:51:05 +01:00
return QVariant();
}
BaseInstance* pdata = static_cast<BaseInstance*>(index.internalPointer());
switch (role) {
case InstancePointerRole: {
QVariant v = QVariant::fromValue((void*)pdata);
return v;
}
case InstanceIDRole: {
return pdata->id();
}
case Qt::EditRole:
case Qt::DisplayRole: {
return pdata->name();
}
case Qt::AccessibleTextRole: {
return tr("%1 Instance").arg(pdata->name());
}
case Qt::ToolTipRole: {
return pdata->instanceRoot();
}
case Qt::DecorationRole: {
return pdata->iconKey();
}
// HACK: see InstanceView.h in gui!
case GroupRole: {
return getInstanceGroup(pdata->id());
}
default:
break;
2018-07-15 13:51:05 +01:00
}
return QVariant();
}
2018-11-22 00:50:32 +00:00
bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (!index.isValid()) {
2018-11-22 00:50:32 +00:00
return false;
}
if (role != Qt::EditRole) {
2018-11-22 00:50:32 +00:00
return false;
}
BaseInstance* pdata = static_cast<BaseInstance*>(index.internalPointer());
2018-11-22 00:50:32 +00:00
auto newName = value.toString();
if (pdata->name() == newName) {
2018-11-22 00:50:32 +00:00
return true;
}
pdata->setName(newName);
return true;
}
Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const
{
2018-07-15 13:51:05 +01:00
Qt::ItemFlags f;
if (index.isValid()) {
2018-11-22 00:50:32 +00:00
f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
2018-07-15 13:51:05 +01:00
}
return f;
}
GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
{
auto inst = getInstanceById(id);
if (!inst) {
return GroupId();
}
auto iter = m_instanceGroupIndex.find(inst->id());
if (iter != m_instanceGroupIndex.end()) {
return *iter;
}
return GroupId();
}
void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name)
{
if (name.isEmpty() && !name.isNull())
name = QString();
auto inst = getInstanceById(id);
if (!inst) {
qDebug() << "Attempt to set a null instance's group";
return;
}
bool changed = false;
auto iter = m_instanceGroupIndex.find(inst->id());
if (iter != m_instanceGroupIndex.end()) {
if (*iter != name) {
decreaseGroupCount(*iter);
*iter = name;
changed = true;
}
} else {
changed = true;
m_instanceGroupIndex[id] = name;
}
if (changed) {
increaseGroupCount(name);
auto idx = getInstIndex(inst.get());
emit dataChanged(index(idx), index(idx), { GroupRole });
saveGroupList();
}
}
QStringList InstanceList::getGroups()
{
return m_groupNameCache.keys();
}
void InstanceList::deleteGroup(const GroupId& name)
{
m_groupNameCache.remove(name);
m_collapsedGroups.remove(name);
bool removed = false;
qDebug() << "Delete group" << name;
for (auto& instance : m_instances) {
const QString& instID = instance->id();
const QString instGroupName = getInstanceGroup(instID);
if (instGroupName == name) {
m_instanceGroupIndex.remove(instID);
qDebug() << "Remove" << instID << "from group" << name;
removed = true;
auto idx = getInstIndex(instance.get());
if (idx >= 0)
emit dataChanged(index(idx), index(idx), { GroupRole });
2018-07-15 13:51:05 +01:00
}
}
if (removed)
saveGroupList();
}
void InstanceList::renameGroup(const QString& src, const QString& dst)
{
m_groupNameCache.remove(src);
if (m_collapsedGroups.remove(src))
m_collapsedGroups.insert(dst);
bool modified = false;
qDebug() << "Rename group" << src << "to" << dst;
for (auto& instance : m_instances) {
const QString& instID = instance->id();
const QString instGroupName = getInstanceGroup(instID);
if (instGroupName == src) {
m_instanceGroupIndex[instID] = dst;
increaseGroupCount(dst);
qDebug() << "Set" << instID << "group to" << dst;
modified = true;
auto idx = getInstIndex(instance.get());
if (idx >= 0)
emit dataChanged(index(idx), index(idx), { GroupRole });
}
}
if (modified)
saveGroupList();
}
bool InstanceList::isGroupCollapsed(const QString& group)
{
return m_collapsedGroups.contains(group);
}
bool InstanceList::trashInstance(const InstanceId& id)
{
auto inst = getInstanceById(id);
if (!inst) {
qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?).";
return false;
}
QString cachedGroupId = m_instanceGroupIndex[id];
qDebug() << "Will trash instance" << id;
QString trashedLoc;
if (m_instanceGroupIndex.remove(id)) {
decreaseGroupCount(cachedGroupId);
saveGroupList();
}
if (!FS::trash(inst->instanceRoot(), &trashedLoc)) {
qDebug() << "Trash of instance" << id << "has not been completely successfully...";
return false;
}
qDebug() << "Instance" << id << "has been trashed by the launcher.";
m_trashHistory.push({ id, inst->instanceRoot(), trashedLoc, cachedGroupId });
return true;
}
bool InstanceList::trashedSomething()
{
return !m_trashHistory.empty();
}
void InstanceList::undoTrashInstance()
{
if (m_trashHistory.empty()) {
qWarning() << "Nothing to recover from trash.";
return;
}
auto top = m_trashHistory.pop();
while (QDir(top.polyPath).exists()) {
top.id += "1";
top.polyPath += "1";
}
qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath;
QFile(top.trashPath).rename(top.polyPath);
m_instanceGroupIndex[top.id] = top.groupName;
increaseGroupCount(top.groupName);
saveGroupList();
emit instancesChanged();
}
void InstanceList::deleteInstance(const InstanceId& id)
{
auto inst = getInstanceById(id);
if (!inst) {
qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?).";
return;
}
QString cachedGroupId = m_instanceGroupIndex[id];
if (m_instanceGroupIndex.remove(id)) {
decreaseGroupCount(cachedGroupId);
saveGroupList();
}
qDebug() << "Will delete instance" << id;
if (!FS::deletePath(inst->instanceRoot())) {
qWarning() << "Deletion of instance" << id << "has not been completely successful ...";
return;
}
2021-10-17 23:47:02 +01:00
qDebug() << "Instance" << id << "has been deleted by the launcher.";
}
static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr>& list)
{
2018-07-15 13:51:05 +01:00
QMap<InstanceId, InstanceLocator> out;
int i = 0;
for (auto& item : list) {
2018-07-15 13:51:05 +01:00
auto id = item->id();
if (out.contains(id)) {
2018-07-15 13:51:05 +01:00
qWarning() << "Duplicate ID" << id << "in instance list";
}
out[id] = std::make_pair(item, i);
i++;
}
return out;
2016-10-02 23:55:54 +01:00
}
QList<InstanceId> InstanceList::discoverInstances()
2018-07-24 00:24:06 +01:00
{
qDebug() << "Discovering instances in" << m_instDir;
QList<InstanceId> out;
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks);
while (iter.hasNext()) {
2018-07-24 00:24:06 +01:00
QString subDir = iter.next();
QFileInfo dirInfo(subDir);
if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists())
continue;
// if it is a symlink, ignore it if it goes to the instance folder
if (dirInfo.isSymLink()) {
2018-07-24 00:24:06 +01:00
QFileInfo targetInfo(dirInfo.symLinkTarget());
QFileInfo instDirInfo(m_instDir);
if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) {
2018-07-24 00:24:06 +01:00
qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder";
continue;
}
}
auto id = dirInfo.fileName();
out.append(id);
qDebug() << "Found instance ID" << id;
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
instanceSet = QSet<QString>(out.begin(), out.end());
#else
2018-07-24 00:24:06 +01:00
instanceSet = out.toSet();
#endif
2018-07-24 00:24:06 +01:00
m_instancesProbed = true;
return out;
}
2018-07-23 23:11:24 +01:00
InstanceList::InstListError InstanceList::loadList()
2016-10-02 23:55:54 +01:00
{
2018-07-15 13:51:05 +01:00
auto existingIds = getIdMapping(m_instances);
2018-07-15 13:51:05 +01:00
QList<InstancePtr> newList;
for (auto& id : discoverInstances()) {
if (existingIds.contains(id)) {
2018-07-23 23:11:24 +01:00
auto instPair = existingIds[id];
existingIds.remove(id);
qDebug() << "Should keep and soft-reload" << id;
} else {
2018-07-24 00:24:06 +01:00
InstancePtr instPtr = loadInstance(id);
if (instPtr) {
2018-07-23 23:11:24 +01:00
newList.append(instPtr);
}
2018-07-15 13:51:05 +01:00
}
}
2016-10-02 23:55:54 +01:00
2018-07-15 13:51:05 +01:00
// TODO: looks like a general algorithm with a few specifics inserted. Do something about it.
if (!existingIds.isEmpty()) {
2018-07-15 13:51:05 +01:00
// get the list of removed instances and sort it by their original index, from last to first
auto deadList = existingIds.values();
auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; };
2018-07-15 13:51:05 +01:00
std::sort(deadList.begin(), deadList.end(), orderSortPredicate);
// remove the contiguous ranges of rows
int front_bookmark = -1;
int back_bookmark = -1;
int currentItem = -1;
auto removeNow = [&]() {
2018-07-15 13:51:05 +01:00
beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark);
m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1);
endRemoveRows();
front_bookmark = -1;
back_bookmark = currentItem;
};
for (auto& removedItem : deadList) {
2018-07-15 13:51:05 +01:00
auto instPtr = removedItem.first;
instPtr->invalidate();
currentItem = removedItem.second;
if (back_bookmark == -1) {
2018-07-15 13:51:05 +01:00
// no bookmark yet
back_bookmark = currentItem;
} else if (currentItem == front_bookmark - 1) {
2018-07-15 13:51:05 +01:00
// part of contiguous sequence, continue
} else {
2018-07-15 13:51:05 +01:00
// seam between previous and current item
removeNow();
}
front_bookmark = currentItem;
}
if (back_bookmark != -1) {
2018-07-15 13:51:05 +01:00
removeNow();
}
}
if (newList.size()) {
2018-07-15 13:51:05 +01:00
add(newList);
}
2018-07-23 23:11:24 +01:00
m_dirty = false;
2019-07-10 03:23:24 +01:00
updateTotalPlayTime();
2018-07-15 13:51:05 +01:00
return NoError;
}
2019-07-10 03:23:24 +01:00
void InstanceList::updateTotalPlayTime()
{
totalPlayTime = 0;
for (auto const& itr : m_instances) {
2019-07-10 03:23:24 +01:00
totalPlayTime += itr.get()->totalTimePlayed();
}
}
void InstanceList::saveNow()
{
for (auto& item : m_instances) {
2018-07-15 13:51:05 +01:00
item->saveNow();
}
}
void InstanceList::add(const QList<InstancePtr>& t)
2013-01-14 23:42:38 +00:00
{
2018-07-15 13:51:05 +01:00
beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1);
m_instances.append(t);
for (auto& ptr : t) {
2018-07-15 13:51:05 +01:00
connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged);
}
endInsertRows();
2016-10-02 23:55:54 +01:00
}
2013-10-06 00:13:40 +01:00
2016-10-02 23:55:54 +01:00
void InstanceList::resumeWatch()
{
if (m_watchLevel > 0) {
2018-07-15 13:51:05 +01:00
qWarning() << "Bad suspend level resume in instance list";
return;
}
m_watchLevel++;
if (m_watchLevel > 0 && m_dirty) {
2018-07-15 13:51:05 +01:00
loadList();
}
}
2016-10-02 23:55:54 +01:00
void InstanceList::suspendWatch()
{
m_watchLevel--;
2016-10-02 23:55:54 +01:00
}
2013-10-06 00:13:40 +01:00
2016-10-02 23:55:54 +01:00
void InstanceList::providerUpdated()
{
2018-07-23 23:11:24 +01:00
m_dirty = true;
if (m_watchLevel == 1) {
2018-07-15 13:51:05 +01:00
loadList();
}
2013-10-06 00:13:40 +01:00
}
InstancePtr InstanceList::getInstanceById(QString instId) const
2013-03-18 22:00:46 +00:00
{
if (instId.isEmpty())
2018-07-15 13:51:05 +01:00
return InstancePtr();
for (auto& inst : m_instances) {
if (inst->id() == instId) {
2018-07-15 13:51:05 +01:00
return inst;
}
}
return InstancePtr();
}
InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) const
{
if (managed_name.isEmpty())
return {};
for (auto instance : m_instances) {
if (instance->getManagedPackName() == managed_name)
return instance;
}
return {};
}
QModelIndex InstanceList::getInstanceIndexById(const QString& id) const
{
2018-07-15 13:51:05 +01:00
return index(getInstIndex(getInstanceById(id).get()));
}
int InstanceList::getInstIndex(BaseInstance* inst) const
{
2018-07-15 13:51:05 +01:00
int count = m_instances.count();
for (int i = 0; i < count; i++) {
if (inst == m_instances[i].get()) {
2018-07-15 13:51:05 +01:00
return i;
}
}
return -1;
}
void InstanceList::propertiesChanged(BaseInstance* inst)
{
2018-07-15 13:51:05 +01:00
int i = getInstIndex(inst);
if (i != -1) {
2018-07-15 13:51:05 +01:00
emit dataChanged(index(i), index(i));
2019-07-10 03:23:24 +01:00
updateTotalPlayTime();
2018-07-15 13:51:05 +01:00
}
}
2018-07-24 00:24:06 +01:00
InstancePtr InstanceList::loadInstance(const InstanceId& id)
{
if (!m_groupsLoaded) {
2018-07-24 00:24:06 +01:00
loadGroupList();
}
auto instanceRoot = FS::PathCombine(m_instDir, id);
auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instanceRoot, "instance.cfg"));
InstancePtr inst;
2022-06-04 10:59:12 +01:00
instanceSettings->registerSetting("InstanceType", "");
QString inst_type = instanceSettings->get("InstanceType").toString();
// NOTE: Some PolyMC versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a OneSix
// instance
if (inst_type == "OneSix" || inst_type.isEmpty()) {
2022-06-04 10:59:12 +01:00
inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot));
} else {
2022-06-04 10:59:12 +01:00
inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot));
}
2018-07-24 00:24:06 +01:00
qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot();
return inst;
}
void InstanceList::increaseGroupCount(const QString& group)
{
if (group.isEmpty())
return;
++m_groupNameCache[group];
}
void InstanceList::decreaseGroupCount(const QString& group)
{
if (group.isEmpty())
return;
if (--m_groupNameCache[group] < 1) {
m_groupNameCache.remove(group);
m_collapsedGroups.remove(group);
}
}
2018-07-24 00:24:06 +01:00
void InstanceList::saveGroupList()
{
qDebug() << "Will save group list now.";
if (!m_instancesProbed) {
2018-07-24 00:24:06 +01:00
qDebug() << "Group saving prevented because we don't know the full list of instances yet.";
return;
}
WatchLock foo(m_watcher, m_instDir);
QString groupFileName = m_instDir + "/instgroups.json";
QMap<QString, QSet<QString>> reverseGroupMap;
for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) {
const QString& id = iter.key();
2018-07-24 00:24:06 +01:00
QString group = iter.value();
if (group.isEmpty())
continue;
if (!instanceSet.contains(id)) {
2018-07-24 00:24:06 +01:00
qDebug() << "Skipping saving missing instance" << id << "to groups list.";
continue;
}
if (!reverseGroupMap.count(group)) {
2018-07-24 00:24:06 +01:00
QSet<QString> set;
set.insert(id);
reverseGroupMap[group] = set;
} else {
QSet<QString>& set = reverseGroupMap[group];
2018-07-24 00:24:06 +01:00
set.insert(id);
}
}
QJsonObject toplevel;
toplevel.insert("formatVersion", QJsonValue(QString("1")));
QJsonObject groupsArr;
for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) {
2018-07-24 00:24:06 +01:00
auto list = iter.value();
auto name = iter.key();
QJsonObject groupObj;
QJsonArray instanceArr;
groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name)));
for (auto item : list) {
2018-07-24 00:24:06 +01:00
instanceArr.append(QJsonValue(item));
}
groupObj.insert("instances", instanceArr);
groupsArr.insert(name, groupObj);
}
toplevel.insert("groups", groupsArr);
QJsonDocument doc(toplevel);
try {
2018-07-24 00:24:06 +01:00
FS::write(groupFileName, doc.toJson());
qDebug() << "Group list saved.";
} catch (const FS::FileSystemException& e) {
2018-07-24 00:24:06 +01:00
qCritical() << "Failed to write instance group file :" << e.cause();
}
}
void InstanceList::loadGroupList()
{
qDebug() << "Will load group list now.";
QString groupFileName = m_instDir + "/instgroups.json";
// if there's no group file, fail
if (!QFileInfo(groupFileName).exists())
return;
QByteArray jsonData;
try {
2018-07-24 00:24:06 +01:00
jsonData = FS::read(groupFileName);
} catch (const FS::FileSystemException& e) {
2018-07-24 00:24:06 +01:00
qCritical() << "Failed to read instance group file :" << e.cause();
return;
}
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error);
// if the json was bad, fail
if (error.error != QJsonParseError::NoError) {
2018-07-24 00:24:06 +01:00
qCritical() << QString("Failed to parse instance group file: %1 at offset %2")
.arg(error.errorString(), QString::number(error.offset))
.toUtf8();
2018-07-24 00:24:06 +01:00
return;
}
// if the root of the json wasn't an object, fail
if (!jsonDoc.isObject()) {
2018-07-24 00:24:06 +01:00
qWarning() << "Invalid group file. Root entry should be an object.";
return;
}
QJsonObject rootObj = jsonDoc.object();
// Make sure the format version matches, otherwise fail.
if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION)
return;
// Get the groups. if it's not an object, fail
if (!rootObj.value("groups").isObject()) {
2018-07-24 00:24:06 +01:00
qWarning() << "Invalid group list JSON: 'groups' should be an object.";
return;
}
m_instanceGroupIndex.clear();
m_groupNameCache.clear();
2018-07-24 00:24:06 +01:00
// Iterate through all the groups.
QJsonObject groupMapping = rootObj.value("groups").toObject();
for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) {
2018-07-24 00:24:06 +01:00
QString groupName = iter.key();
if (iter.key().isEmpty()) {
qWarning() << "Redundant empty group found";
continue;
}
2018-07-24 00:24:06 +01:00
// If not an object, complain and skip to the next one.
if (!iter.value().isObject()) {
qWarning() << QString("Group '%1' in the group list should be an object").arg(groupName).toUtf8();
2018-07-24 00:24:06 +01:00
continue;
}
QJsonObject groupObj = iter.value().toObject();
if (!groupObj.value("instances").isArray()) {
qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.")
.arg(groupName)
.toUtf8();
2018-07-24 00:24:06 +01:00
continue;
}
auto hidden = groupObj.value("hidden").toBool(false);
if (hidden)
m_collapsedGroups.insert(groupName);
2018-07-24 00:24:06 +01:00
// Iterate through the list of instances in the group.
QJsonArray instancesArray = groupObj.value("instances").toArray();
for (auto value : instancesArray) {
m_instanceGroupIndex[value.toString()] = groupName;
increaseGroupCount(groupName);
}
2018-07-24 00:24:06 +01:00
}
m_groupsLoaded = true;
qDebug() << "Group list loaded.";
}
void InstanceList::instanceDirContentsChanged(const QString& path)
{
Q_UNUSED(path);
emit instancesChanged();
}
void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting, QVariant value)
2018-07-24 00:24:06 +01:00
{
QString newInstDir = QDir(value.toString()).canonicalPath();
if (newInstDir != m_instDir) {
if (m_groupsLoaded) {
2018-07-24 00:24:06 +01:00
saveGroupList();
}
m_instDir = newInstDir;
m_groupsLoaded = false;
emit instancesChanged();
}
}
void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
{
qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded");
if (collapsed) {
m_collapsedGroups.insert(group);
} else {
m_collapsedGroups.remove(group);
}
saveGroupList();
}
class InstanceStaging : public Task {
Q_OBJECT
2018-07-24 00:24:06 +01:00
const unsigned minBackoff = 1;
const unsigned maxBackoff = 16;
public:
InstanceStaging(InstanceList* parent, InstanceTask* child, QString stagingPath, InstanceName const& instanceName, QString groupName)
: m_parent(parent)
, backoff(minBackoff, maxBackoff)
, m_stagingPath(std::move(stagingPath))
, m_instance_name(std::move(instanceName))
, m_groupName(std::move(groupName))
2018-07-24 00:24:06 +01:00
{
m_child.reset(child);
connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded);
2018-07-24 00:24:06 +01:00
connect(child, &Task::failed, this, &InstanceStaging::childFailed);
connect(child, &Task::aborted, this, &InstanceStaging::childAborted);
connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable);
2018-07-24 00:24:06 +01:00
connect(child, &Task::status, this, &InstanceStaging::setStatus);
connect(child, &Task::details, this, &InstanceStaging::setDetails);
2018-07-24 00:24:06 +01:00
connect(child, &Task::progress, this, &InstanceStaging::setProgress);
connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress);
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded);
2018-07-24 00:24:06 +01:00
}
virtual ~InstanceStaging(){};
// FIXME/TODO: add ability to abort during instance commit retries
bool abort() override
{
if (!canAbort())
return false;
m_child->abort();
return Task::abort();
}
bool canAbort() const override { return (m_child && m_child->canAbort()); }
protected:
virtual void executeTask() override { m_child->start(); }
QStringList warnings() const override { return m_child->warnings(); }
2018-07-24 00:24:06 +01:00
private slots:
void childSucceeded()
2018-07-24 00:24:06 +01:00
{
unsigned sleepTime = backoff();
if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) {
2018-07-24 00:24:06 +01:00
emitSucceeded();
return;
}
// we actually failed, retry?
if (sleepTime == maxBackoff) {
2018-07-24 00:24:06 +01:00
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return;
}
qDebug() << "Failed to commit instance" << m_instance_name.name() << "Initiating backoff:" << sleepTime;
2018-07-24 00:24:06 +01:00
m_backoffTimer.start(sleepTime * 500);
}
void childFailed(const QString& reason)
2018-07-24 00:24:06 +01:00
{
m_parent->destroyStagingPath(m_stagingPath);
emitFailed(reason);
}
void childAborted() { emitAborted(); }
private:
InstanceList* m_parent;
2018-07-24 00:24:06 +01:00
/*
* WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows.
2021-10-17 23:47:02 +01:00
* Basically, it starts messing things up while the launcher is extracting/creating instances
2018-07-24 00:24:06 +01:00
* and causes that horrible failure that is NTFS to lock files in place because they are open.
*/
ExponentialSeries backoff;
QString m_stagingPath;
unique_qobject_ptr<InstanceTask> m_child;
InstanceName m_instance_name;
2018-07-24 00:24:06 +01:00
QString m_groupName;
QTimer m_backoffTimer;
};
Task* InstanceList::wrapInstanceTask(InstanceTask* task)
2018-07-24 00:24:06 +01:00
{
auto stagingPath = getStagedInstancePath();
task->setStagingPath(stagingPath);
task->setParentSettings(m_globalSettings);
return new InstanceStaging(this, task, stagingPath, *task, task->group());
2018-07-24 00:24:06 +01:00
}
QString InstanceList::getStagedInstancePath()
{
QString key = QUuid::createUuid().toString(QUuid::WithoutBraces);
QString tempDir = ".LAUNCHER_TEMP/";
QString relPath = FS::PathCombine(tempDir, key);
2018-07-24 00:24:06 +01:00
QDir rootPath(m_instDir);
auto path = FS::PathCombine(m_instDir, relPath);
if (!rootPath.mkpath(relPath)) {
2018-07-24 00:24:06 +01:00
return QString();
}
#ifdef Q_OS_WIN32
auto tempPath = FS::PathCombine(m_instDir, tempDir);
SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
#endif
2018-07-24 00:24:06 +01:00
return path;
}
bool InstanceList::commitStagedInstance(const QString& path,
InstanceName const& instanceName,
QString groupName,
InstanceTask const& commiting)
2018-07-24 00:24:06 +01:00
{
if (groupName.isEmpty() && !groupName.isNull())
groupName = QString();
2018-07-24 00:24:06 +01:00
QDir dir;
QString instID;
InstancePtr inst;
auto should_override = commiting.shouldOverride();
if (should_override) {
instID = commiting.originalInstanceID();
} else {
instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir);
}
Q_ASSERT(!instID.isEmpty());
2018-07-24 00:24:06 +01:00
{
WatchLock lock(m_watcher, m_instDir);
QString destination = FS::PathCombine(m_instDir, instID);
if (should_override) {
if (!FS::overrideFolder(destination, path)) {
qWarning() << "Failed to override" << path << "to" << destination;
return false;
}
} else {
if (!dir.rename(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination;
return false;
}
m_instanceGroupIndex[instID] = groupName;
increaseGroupCount(groupName);
2018-07-24 00:24:06 +01:00
}
instanceSet.insert(instID);
2018-07-24 00:24:06 +01:00
emit instancesChanged();
emit instanceSelectRequest(instID);
2018-07-24 00:24:06 +01:00
}
2018-07-24 00:24:06 +01:00
saveGroupList();
return true;
}
bool InstanceList::destroyStagingPath(const QString& keyPath)
{
return FS::deletePath(keyPath);
}
int InstanceList::getTotalPlayTime()
{
2019-07-10 03:23:24 +01:00
updateTotalPlayTime();
return totalPlayTime;
}
#include "InstanceList.moc"