diff options
Diffstat (limited to 'api/logic/minecraft')
108 files changed, 3190 insertions, 2493 deletions
diff --git a/api/logic/minecraft/AssetsUtils.cpp b/api/logic/minecraft/AssetsUtils.cpp index c6db2a40..c01733b6 100644 --- a/api/logic/minecraft/AssetsUtils.cpp +++ b/api/logic/minecraft/AssetsUtils.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -27,6 +27,34 @@ #include "FileSystem.h" #include "net/Download.h" #include "net/ChecksumValidator.h" +#include "BuildConfig.h" + +namespace { +QSet<QString> collectPathsFromDir(QString dirPath) +{ + QFileInfo dirInfo(dirPath); + + if (!dirInfo.exists()) + { + return {}; + } + + QSet<QString> out; + + QDirIterator iter(dirPath, QDirIterator::Subdirectories); + while (iter.hasNext()) + { + QString value = iter.next(); + QFileInfo info(value); + if(info.isFile()) + { + out.insert(value); + qDebug() << value; + } + } + return out; +} +} namespace AssetsUtils @@ -36,7 +64,7 @@ namespace AssetsUtils * Returns true on success, with index populated * index is undefined otherwise */ -bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) +bool loadAssetsIndexJson(const QString &assetsId, const QString &path, AssetsIndex& index) { /* { @@ -60,7 +88,7 @@ bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) qCritical() << "Failed to read assets index file" << path; return false; } - index->id = assetsId; + index.id = assetsId; // Read the file and close it. QByteArray jsonData = file.readAll(); @@ -89,7 +117,13 @@ bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) QJsonValue isVirtual = root.value("virtual"); if (!isVirtual.isUndefined()) { - index->isVirtual = isVirtual.toBool(false); + index.isVirtual = isVirtual.toBool(false); + } + + QJsonValue mapToResources = root.value("map_to_resources"); + if (!mapToResources.isUndefined()) + { + index.mapToResources = mapToResources.toBool(false); } QJsonValue objects = root.value("objects"); @@ -121,13 +155,14 @@ bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) } } - index->objects.insert(iter.key(), object); + index.objects.insert(iter.key(), object); } return true; } -QDir reconstructAssets(QString assetsId) +// FIXME: ugly code duplication +QDir getAssetsDir(const QString &assetsId, const QString &resourcesFolder) { QDir assetsDir = QDir("assets/"); QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); @@ -140,24 +175,77 @@ QDir reconstructAssets(QString assetsId) if (!indexFile.exists()) { - qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets"; + qCritical() << "No assets index file" << indexPath << "; can't determine assets path!"; return virtualRoot; } - qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() - << objectDir.path() << virtualDir.path() << virtualRoot.path(); + AssetsIndex index; + if(!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) + { + qCritical() << "Failed to load asset index file" << indexPath << "; can't determine assets path!"; + return virtualRoot; + } + + QString targetPath; + if(index.isVirtual) + { + return virtualRoot; + } + else if(index.mapToResources) + { + return QDir(resourcesFolder); + } + return virtualRoot; +} + +// FIXME: ugly code duplication +bool reconstructAssets(QString assetsId, QString resourcesFolder) +{ + QDir assetsDir = QDir("assets/"); + QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); + QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); + QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); + + QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets!"; + return false; + } + + qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() << objectDir.path() << virtualDir.path() << virtualRoot.path(); AssetsIndex index; - bool loadAssetsIndex = AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, &index); + if(!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) + { + qCritical() << "Failed to load asset index file" << indexPath << "; can't reconstruct assets!"; + return false; + } - if (loadAssetsIndex && index.isVirtual) + QString targetPath; + bool removeLeftovers = false; + if(index.isVirtual) + { + targetPath = virtualRoot.path(); + removeLeftovers = true; + qDebug() << "Reconstructing virtual assets folder at" << targetPath; + } + else if(index.mapToResources) { - qDebug() << "Reconstructing virtual assets folder at" << virtualRoot.path(); + targetPath = resourcesFolder; + qDebug() << "Reconstructing resources folder at" << targetPath; + } + if (!targetPath.isNull()) + { + auto presentFiles = collectPathsFromDir(targetPath); for (QString map : index.objects.keys()) { AssetObject asset_object = index.objects.value(map); - QString target_path = FS::PathCombine(virtualRoot.path(), map); + QString target_path = FS::PathCombine(targetPath, map); QFile target(target_path); QString tlk = asset_object.hash.left(2); @@ -166,24 +254,32 @@ QDir reconstructAssets(QString assetsId) QFile original(original_path); if (!original.exists()) continue; + + presentFiles.remove(target_path); + if (!target.exists()) { QFileInfo info(target_path); QDir target_dir = info.dir(); - // qDebug() << target_dir; - if (!target_dir.exists()) - QDir("").mkpath(target_dir.path()); + + qDebug() << target_dir.path(); + FS::ensureFolderPathExists(target_dir.path()); bool couldCopy = original.copy(target_path); - qDebug() << " Copying" << original_path << "to" << target_path - << QString::number(couldCopy); // << original.errorString(); + qDebug() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy); } } // TODO: Write last used time to virtualRoot/.lastused + if(removeLeftovers) + { + for(auto & file: presentFiles) + { + qDebug() << "Would remove" << file; + } + } } - - return virtualRoot; + return true; } } @@ -212,7 +308,7 @@ QString AssetObject::getLocalPath() QUrl AssetObject::getUrl() { - return QUrl("https://resources.download.minecraft.net/" + getRelPath()); + return BuildConfig.RESOURCE_BASE + getRelPath(); } QString AssetObject::getRelPath() diff --git a/api/logic/minecraft/AssetsUtils.h b/api/logic/minecraft/AssetsUtils.h index 063c1237..32e57060 100644 --- a/api/logic/minecraft/AssetsUtils.h +++ b/api/logic/minecraft/AssetsUtils.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -38,11 +38,16 @@ struct AssetsIndex QString id; QMap<QString, AssetObject> objects; bool isVirtual = false; + bool mapToResources = false; }; +/// FIXME: this is absolutely horrendous. REDO!!!! namespace AssetsUtils { -bool loadAssetsIndexJson(QString id, QString file, AssetsIndex* index); +bool loadAssetsIndexJson(const QString &id, const QString &file, AssetsIndex& index); + +QDir getAssetsDir(const QString &assetsId, const QString &resourcesFolder); + /// Reconstruct a virtual assets folder for the given assets ID and return the folder -QDir reconstructAssets(QString assetsId); +bool reconstructAssets(QString assetsId, QString resourcesFolder); } diff --git a/api/logic/minecraft/Component.cpp b/api/logic/minecraft/Component.cpp index 51957d17..92821065 100644 --- a/api/logic/minecraft/Component.cpp +++ b/api/logic/minecraft/Component.cpp @@ -5,13 +5,13 @@ #include "meta/Version.h" #include "VersionFile.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include <FileSystem.h> #include <QSaveFile> #include "OneSixVersionFormat.h" #include <assert.h> -Component::Component(ComponentList * parent, const QString& uid) +Component::Component(PackProfile * parent, const QString& uid) { assert(parent); m_parent = parent; @@ -19,7 +19,7 @@ Component::Component(ComponentList * parent, const QString& uid) m_uid = uid; } -Component::Component(ComponentList * parent, std::shared_ptr<Meta::Version> version) +Component::Component(PackProfile * parent, std::shared_ptr<Meta::Version> version) { assert(parent); m_parent = parent; @@ -31,7 +31,7 @@ Component::Component(ComponentList * parent, std::shared_ptr<Meta::Version> vers m_loaded = version->isLoaded(); } -Component::Component(ComponentList * parent, const QString& uid, std::shared_ptr<VersionFile> file) +Component::Component(PackProfile * parent, const QString& uid, std::shared_ptr<VersionFile> file) { assert(parent); m_parent = parent; diff --git a/api/logic/minecraft/Component.h b/api/logic/minecraft/Component.h index 6a0f86c8..cb202f7f 100644 --- a/api/logic/minecraft/Component.h +++ b/api/logic/minecraft/Component.h @@ -9,7 +9,7 @@ #include "QObjectPtr.h" #include "multimc_logic_export.h" -class ComponentList; +class PackProfile; class LaunchProfile; namespace Meta { @@ -22,11 +22,11 @@ class MULTIMC_LOGIC_EXPORT Component : public QObject, public ProblemProvider { Q_OBJECT public: - Component(ComponentList * parent, const QString &uid); + Component(PackProfile * parent, const QString &uid); // DEPRECATED: remove these constructors? - Component(ComponentList * parent, std::shared_ptr<Meta::Version> version); - Component(ComponentList * parent, const QString & uid, std::shared_ptr<VersionFile> file); + Component(PackProfile * parent, std::shared_ptr<Meta::Version> version); + Component(PackProfile * parent, const QString & uid, std::shared_ptr<VersionFile> file); virtual ~Component(){}; void applyTo(LaunchProfile *profile); @@ -73,7 +73,7 @@ signals: void dataChanged(); public: /* data */ - ComponentList * m_parent; + PackProfile * m_parent; // BEGIN: persistent component list properties /// ID of the component diff --git a/api/logic/minecraft/ComponentUpdateTask.cpp b/api/logic/minecraft/ComponentUpdateTask.cpp index 37cc488d..241d9a49 100644 --- a/api/logic/minecraft/ComponentUpdateTask.cpp +++ b/api/logic/minecraft/ComponentUpdateTask.cpp @@ -1,7 +1,7 @@ #include "ComponentUpdateTask.h" -#include "ComponentList_p.h" -#include "ComponentList.h" +#include "PackProfile_p.h" +#include "PackProfile.h" #include "Component.h" #include <Env.h> #include <meta/Index.h> @@ -22,16 +22,16 @@ * Really, it should be a reactor/state machine that receives input from the application * and dynamically adapts to changing requirements... * - * The reactor should be the only entry into manipulating the ComponentList. + * The reactor should be the only entry into manipulating the PackProfile. * See: https://en.wikipedia.org/wiki/Reactor_pattern */ /* - * Or make this operate on a snapshot of the ComponentList state, then merge results in as long as the snapshot and ComponentList didn't change? + * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and PackProfile didn't change? * If the component list changes, start over. */ -ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList* list, QObject* parent) +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent) : Task(parent) { d.reset(new ComponentUpdateTaskData); @@ -126,7 +126,7 @@ static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr<Task> // FIXME: dead code. determine if this can still be useful? /* -static LoadResult loadComponentList(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode) +static LoadResult loadPackProfile(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode) { if(component->m_loaded) { @@ -217,7 +217,7 @@ void ComponentUpdateTask::loadComponents() } case Mode::Resolution: { - singleResult = loadComponentList(component, loadTask, d->netmode); + singleResult = loadPackProfile(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::List; break; } @@ -244,7 +244,7 @@ void ComponentUpdateTask::loadComponents() }); RemoteLoadStatus status; status.type = loadType; - status.componentListIndex = componentIndex; + status.PackProfileIndex = componentIndex; d->remoteLoadStatusList.append(status); taskIndex++; } @@ -451,13 +451,17 @@ static bool getTrivialComponentChanges(const ComponentIndex & index, const Requi auto & comp = (*compIter); if(comp->getVersion() != req.equalsVersion) { - if(comp->m_dependencyOnly) - { - decision = Decision::VersionNotSame; - } - else - { + if(comp->isCustom()) { decision = Decision::LockedVersionNotSame; + } else { + if(comp->m_dependencyOnly) + { + decision = Decision::VersionNotSame; + } + else + { + decision = Decision::LockedVersionNotSame; + } } break; } @@ -491,7 +495,7 @@ static bool getTrivialComponentChanges(const ComponentIndex & index, const Requi } // FIXME, TODO: decouple dependency resolution from loading -// FIXME: This works directly with the ComponentList internals. It shouldn't! It needs richer data types than ComponentList uses. +// FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. // FIXME: throw all this away and use a graph void ComponentUpdateTask::resolveDependencies(bool checkOnly) { @@ -586,6 +590,15 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) { component->m_version = "3.1.2"; } + else if (add.uid == "net.fabricmc.intermediary") + { + auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){ + return cmp->getID() == "net.minecraft"; + }); + if(minecraft != components.end()) { + component->m_version = (*minecraft)->getVersion(); + } + } } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ @@ -635,7 +648,7 @@ void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) // update the cached data of the component from the downloaded version file. if (taskSlot.type == RemoteLoadStatus::Type::Version) { - auto component = d->m_list->getComponent(taskSlot.componentListIndex); + auto component = d->m_list->getComponent(taskSlot.PackProfileIndex); component->m_loaded = true; component->updateCachedData(); } diff --git a/api/logic/minecraft/ComponentUpdateTask.h b/api/logic/minecraft/ComponentUpdateTask.h index 4cb29a89..4274cabb 100644 --- a/api/logic/minecraft/ComponentUpdateTask.h +++ b/api/logic/minecraft/ComponentUpdateTask.h @@ -4,7 +4,7 @@ #include "net/Mode.h" #include <memory> -class ComponentList; +class PackProfile; struct ComponentUpdateTaskData; class ComponentUpdateTask : public Task @@ -18,7 +18,7 @@ public: }; public: - explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList * list, QObject *parent = 0); + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile * list, QObject *parent = 0); virtual ~ComponentUpdateTask(); protected: diff --git a/api/logic/minecraft/ComponentUpdateTask_p.h b/api/logic/minecraft/ComponentUpdateTask_p.h index 5989cf07..5b02431b 100644 --- a/api/logic/minecraft/ComponentUpdateTask_p.h +++ b/api/logic/minecraft/ComponentUpdateTask_p.h @@ -5,7 +5,7 @@ #include <QList> #include "net/Mode.h" -class ComponentList; +class PackProfile; struct RemoteLoadStatus { @@ -15,7 +15,7 @@ struct RemoteLoadStatus List, Version } type = Type::Version; - size_t componentListIndex = 0; + size_t PackProfileIndex = 0; bool finished = false; bool succeeded = false; QString error; @@ -23,7 +23,7 @@ struct RemoteLoadStatus struct ComponentUpdateTaskData { - ComponentList * m_list = nullptr; + PackProfile * m_list = nullptr; QList<RemoteLoadStatus> remoteLoadStatusList; bool remoteLoadSuccessful = true; size_t remoteTasksInProgress = 0; diff --git a/api/logic/minecraft/GradleSpecifier.h b/api/logic/minecraft/GradleSpecifier.h index 959325c6..60e0a726 100644 --- a/api/logic/minecraft/GradleSpecifier.h +++ b/api/logic/minecraft/GradleSpecifier.h @@ -18,32 +18,35 @@ struct GradleSpecifier { /* org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar - DEBUG 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" - DEBUG 1 "org.gradle.test.classifiers" - DEBUG 2 "service" - DEBUG 3 "1.0" - DEBUG 4 ":jdk15" - DEBUG 5 "jdk15" - DEBUG 6 "@jar" - DEBUG 7 "jar" + 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" + 1 "org.gradle.test.classifiers" + 2 "service" + 3 "1.0" + 4 "jdk15" + 5 "jar" */ - QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(:([^:@]+))?" "(@([^:@]+))?"); + QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?"); m_valid = matcher.exactMatch(value); + if(!m_valid) { + m_invalidValue = value; + return *this; + } auto elements = matcher.capturedTexts(); m_groupId = elements[1]; m_artifactId = elements[2]; m_version = elements[3]; - m_classifier = elements[5]; - if(!elements[7].isEmpty()) + m_classifier = elements[4]; + if(!elements[5].isEmpty()) { - m_extension = elements[7]; + m_extension = elements[5]; } return *this; } - operator QString() const + QString serialize() const { - if(!m_valid) - return "INVALID"; + if(!m_valid) { + return m_invalidValue; + } QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; if(!m_classifier.isEmpty()) { @@ -57,6 +60,9 @@ struct GradleSpecifier } QString getFileName() const { + if(!m_valid) { + return QString(); + } QString filename = m_artifactId + '-' + m_version; if(!m_classifier.isEmpty()) { @@ -67,8 +73,9 @@ struct GradleSpecifier } QString toPath(const QString & filenameOverride = QString()) const { - if(!m_valid) - return "INVALID"; + if(!m_valid) { + return QString(); + } QString filename; if(filenameOverride.isEmpty()) { @@ -134,6 +141,7 @@ struct GradleSpecifier return true; } private: + QString m_invalidValue; QString m_groupId; QString m_artifactId; QString m_version; diff --git a/api/logic/minecraft/GradleSpecifier_test.cpp b/api/logic/minecraft/GradleSpecifier_test.cpp index f49ec718..0900c9d8 100644 --- a/api/logic/minecraft/GradleSpecifier_test.cpp +++ b/api/logic/minecraft/GradleSpecifier_test.cpp @@ -31,7 +31,7 @@ slots: { QFETCH(QString, through); - QString converted = GradleSpecifier(through); + QString converted = GradleSpecifier(through).serialize(); QCOMPARE(converted, through); } @@ -68,7 +68,8 @@ slots: GradleSpecifier spec(input); QVERIFY(!spec.valid()); - QCOMPARE(spec.operator QString(), QString("INVALID")); + QCOMPARE(spec.serialize(), input); + QCOMPARE(spec.toPath(), QString()); } }; diff --git a/api/logic/minecraft/LaunchProfile.cpp b/api/logic/minecraft/LaunchProfile.cpp index c39bdf04..41705187 100644 --- a/api/logic/minecraft/LaunchProfile.cpp +++ b/api/logic/minecraft/LaunchProfile.cpp @@ -11,6 +11,7 @@ void LaunchProfile::clear() m_mainClass.clear(); m_appletClass.clear(); m_libraries.clear(); + m_mavenFiles.clear(); m_traits.clear(); m_jarMods.clear(); m_mainJar.reset(); @@ -157,6 +158,22 @@ void LaunchProfile::applyLibrary(LibraryPtr library) } } +void LaunchProfile::applyMavenFile(LibraryPtr mavenFile) +{ + if(!mavenFile->isActive()) + { + return; + } + + if(mavenFile->isNative()) + { + return; + } + + // unlike libraries, we do not keep only one version or try to dedupe them + m_mavenFiles.append(Library::limitedCopy(mavenFile)); +} + const LibraryPtr LaunchProfile::getMainJar() const { return m_mainJar; @@ -253,6 +270,11 @@ const QList<LibraryPtr> & LaunchProfile::getNativeLibraries() const return m_nativeLibraries; } +const QList<LibraryPtr> & LaunchProfile::getMavenFiles() const +{ + return m_mavenFiles; +} + void LaunchProfile::getLibraryFiles( const QString& architecture, QStringList& jars, diff --git a/api/logic/minecraft/LaunchProfile.h b/api/logic/minecraft/LaunchProfile.h index 77174079..c1752531 100644 --- a/api/logic/minecraft/LaunchProfile.h +++ b/api/logic/minecraft/LaunchProfile.h @@ -20,6 +20,7 @@ public: /* application of profile variables from patches */ void applyJarMods(const QList<LibraryPtr> &jarMods); void applyMods(const QList<LibraryPtr> &jarMods); void applyLibrary(LibraryPtr library); + void applyMavenFile(LibraryPtr library); void applyMainJar(LibraryPtr jar); void applyProblemSeverity(ProblemSeverity severity); /// clear the profile @@ -37,6 +38,7 @@ public: /* getters for profile variables */ const QList<LibraryPtr> & getJarMods() const; const QList<LibraryPtr> & getLibraries() const; const QList<LibraryPtr> & getNativeLibraries() const; + const QList<LibraryPtr> & getMavenFiles() const; const LibraryPtr getMainJar() const; void getLibraryFiles( const QString & architecture, @@ -79,10 +81,13 @@ private: /// the list of libraries QList<LibraryPtr> m_libraries; + /// the list of maven files to be placed in the libraries folder, but not acted upon + QList<LibraryPtr> m_mavenFiles; + /// the main jar LibraryPtr m_mainJar; - /// the list of libraries + /// the list of native libraries QList<LibraryPtr> m_nativeLibraries; /// traits, collected from all the version files (version files can only add) diff --git a/api/logic/minecraft/Library.cpp b/api/logic/minecraft/Library.cpp index a6ec0301..f2293679 100644 --- a/api/logic/minecraft/Library.cpp +++ b/api/logic/minecraft/Library.cpp @@ -3,9 +3,9 @@ #include <net/Download.h> #include <net/ChecksumValidator.h> -#include <minecraft/forge/ForgeXzDownload.h> #include <Env.h> #include <FileSystem.h> +#include <BuildConfig.h> void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& native, QStringList& native32, @@ -18,13 +18,7 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na if(local && !overridePath.isEmpty()) { QString fileName = out.fileName(); - auto fullPath = FS::PathCombine(overridePath, fileName); - qDebug() << fullPath; - QFileInfo fileinfo(fullPath); - if(fileinfo.exists()) - { - return fileinfo.absoluteFilePath(); - } + return QFileInfo(FS::PathCombine(overridePath, fileName)).absoluteFilePath(); } return out.absoluteFilePath(); }; @@ -51,77 +45,62 @@ void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& na } } -QList< std::shared_ptr< NetAction > > Library::getDownloads(OpSys system, class HttpMetaCache* cache, - QStringList& failedFiles, const QString & overridePath) const +QList< std::shared_ptr< NetAction > > Library::getDownloads( + OpSys system, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString & overridePath +) const { QList<NetActionPtr> out; - bool isAlwaysStale = (hint() == "always-stale"); + bool stale = isAlwaysStale(); bool local = isLocal(); - bool isForge = (hint() == "forge-pack-xz"); + + auto check_local_file = [&](QString storage) + { + QFileInfo fileinfo(storage); + QString fileName = fileinfo.fileName(); + auto fullPath = FS::PathCombine(overridePath, fileName); + QFileInfo localFileInfo(fullPath); + if(!localFileInfo.exists()) + { + failedLocalFiles.append(localFileInfo.filePath()); + return false; + } + return true; + }; auto add_download = [&](QString storage, QString url, QString sha1) { + if(local) + { + return check_local_file(storage); + } auto entry = cache->resolveEntry("libraries", storage); - if(isAlwaysStale) + if(stale) { entry->setStale(true); } if (!entry->isStale()) return true; - if(local) - { - if(!overridePath.isEmpty()) - { - QString fileName; - int position = storage.lastIndexOf('/'); - if(position == -1) - { - fileName = storage; - } - else - { - fileName = storage.mid(position); - } - auto fullPath = FS::PathCombine(overridePath, fileName); - QFileInfo fileinfo(fullPath); - if(fileinfo.exists()) - { - return true; - } - } - QFileInfo fileinfo(entry->getFullPath()); - if(!fileinfo.exists()) - { - failedFiles.append(entry->getFullPath()); - return false; - } - return true; - } Net::Download::Options options; - if(isAlwaysStale) + if(stale) { options |= Net::Download::Option::AcceptLocalFiles; } - if (isForge) + + if(sha1.size()) { - qDebug() << "XzDownload for:" << rawName() << "storage:" << storage << "url:" << url; - out.append(ForgeXzDownload::make(storage, entry)); + auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); + auto dl = Net::Download::makeCached(url, entry, options); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + out.append(dl); } else { - if(sha1.size()) - { - auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); - auto dl = Net::Download::makeCached(url, entry, options); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); - qDebug() << "Checksummed Download for:" << rawName() << "storage:" << storage << "url:" << url; - out.append(dl); - } - else - { - out.append(Net::Download::makeCached(url, entry, options)); - qDebug() << "Download for:" << rawName() << "storage:" << storage << "url:" << url; - } + out.append(Net::Download::makeCached(url, entry, options)); + qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; } return true; }; @@ -166,7 +145,7 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads(OpSys system, class } else { - qDebug() << "Ignoring native library" << m_name << "because it has no classifier for current OS"; + qDebug() << "Ignoring native library" << m_name.serialize() << "because it has no classifier for current OS"; } } else @@ -178,13 +157,14 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads(OpSys system, class } else { - qDebug() << "Ignoring java library" << m_name << "because it has no artifact"; + qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact"; } } } else { - auto raw_dl = [&](){ + auto raw_dl = [&]() + { if (!m_absoluteURL.isEmpty()) { return m_absoluteURL; @@ -192,7 +172,7 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads(OpSys system, class if (m_repositoryURL.isEmpty()) { - return QString("https://" + URLConstants::LIBRARY_BASE) + raw_storage; + return BuildConfig.LIBRARY_BASE + raw_storage; } if(m_repositoryURL.endsWith('/')) @@ -251,6 +231,11 @@ bool Library::isLocal() const return m_hint == "local"; } +bool Library::isAlwaysStale() const +{ + return m_hint == "always-stale"; +} + void Library::setStoragePrefix(QString prefix) { m_storagePrefix = prefix; diff --git a/api/logic/minecraft/Library.h b/api/logic/minecraft/Library.h index 5fcff316..acdd6c9c 100644 --- a/api/logic/minecraft/Library.h +++ b/api/logic/minecraft/Library.h @@ -12,7 +12,6 @@ #include "Rule.h" #include "minecraft/OpSys.h" #include "GradleSpecifier.h" -#include "net/URLConstants.h" #include "MojangDownloadInfo.h" #include "multimc_logic_export.h" @@ -148,9 +147,15 @@ public: /* methods */ /// Returns true if the library is contained in an instance and false if it is shared bool isLocal() const; + /// Returns true if the library is to always be checked for updates + bool isAlwaysStale() const; + + /// Return true if the library requires forge XZ hacks + bool isForge() const; + // Get a list of downloads for this library QList<NetActionPtr> getDownloads(OpSys system, class HttpMetaCache * cache, - QStringList & failedFiles, const QString & overridePath) const; + QStringList & failedLocalFiles, const QString & overridePath) const; private: /* methods */ /// the default storage prefix used by MultiMC diff --git a/api/logic/minecraft/Library_test.cpp b/api/logic/minecraft/Library_test.cpp index 4f9c8006..75bb4db1 100644 --- a/api/logic/minecraft/Library_test.cpp +++ b/api/logic/minecraft/Library_test.cpp @@ -18,7 +18,8 @@ private: jsonFile.open(QIODevice::ReadOnly); auto data = jsonFile.readAll(); jsonFile.close(); - return MojangVersionFormat::libraryFromJson(QJsonDocument::fromJson(data).object(), file); + ProblemContainer problems; + return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), file); } // get absolute path to expected storage, assuming default cache prefix QStringList getStorage(QString relative) @@ -65,7 +66,7 @@ slots: test.setHint("local"); auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString()); QCOMPARE(downloads.size(), 0); - QCOMPARE(failedFiles, getStorage("test/package/testname/testversion/testname-testversion.jar")); + QCOMPARE(failedFiles, {"testname-testversion.jar"}); } void test_legacy_url_local_override() { @@ -170,11 +171,11 @@ slots: QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, {QFileInfo("data/testname-testversion-linux-32.jar").absoluteFilePath()}); - QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar")); + QCOMPARE(native64, {QFileInfo("data/testname-testversion-linux-64.jar").absoluteFilePath()}); QStringList failedFiles; auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString("data")); QCOMPARE(dls.size(), 0); - QCOMPARE(failedFiles, {getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar")}); + QCOMPARE(failedFiles, {"data/testname-testversion-linux-64.jar"}); } } void test_onenine() diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp index 7c83b890..dbf9f816 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -1,5 +1,5 @@ #include "MinecraftInstance.h" -#include <minecraft/launch/CreateServerResourcePacksFolder.h> +#include <minecraft/launch/CreateGameFolders.h> #include <minecraft/launch/ExtractNatives.h> #include <minecraft/launch/PrintInstanceInfo.h> #include <settings/Setting.h> @@ -12,6 +12,7 @@ #include <java/JavaVersion.h> #include "launch/LaunchTask.h" +#include "launch/steps/LookupServerAddress.h" #include "launch/steps/PostLaunchCommand.h" #include "launch/steps/Update.h" #include "launch/steps/PreLaunchCommand.h" @@ -20,22 +21,28 @@ #include "minecraft/launch/DirectJavaLaunch.h" #include "minecraft/launch/ModMinecraftJar.h" #include "minecraft/launch/ClaimAccount.h" +#include "minecraft/launch/ReconstructAssets.h" +#include "minecraft/launch/ScanModFolders.h" +#include "minecraft/launch/VerifyJavaInstall.h" #include "java/launch/CheckJava.h" #include "java/JavaUtils.h" #include "meta/Index.h" #include "meta/VersionList.h" -#include "SimpleModList.h" -#include "ModsModel.h" +#include "mod/ModFolderModel.h" +#include "mod/ResourcePackFolderModel.h" +#include "mod/TexturePackFolderModel.h" #include "WorldList.h" #include "icons/IIconList.h" #include <QCoreApplication> -#include "ComponentList.h" +#include "PackProfile.h" #include "AssetsUtils.h" #include "MinecraftUpdate.h" #include "MinecraftLoadAndCheck.h" +#include <minecraft/gameoptions/GameOptions.h> +#include <minecraft/update/FoldersTask.h> #define IBUS "@im=ibus" @@ -98,13 +105,27 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false); m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride); + // Native library workarounds + auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); + m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); + + // Game time + auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); + m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); + m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride); + + // Join server on launch, this does not have a global override + m_settings->registerSetting("JoinServerOnLaunch", false); + m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // DEPRECATED: Read what versions the user configuration thinks should be used m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); m_settings->registerSetting("LWJGLVersion", ""); m_settings->registerSetting("ForgeVersion", ""); m_settings->registerSetting("LiteloaderVersion", ""); - m_components.reset(new ComponentList(this)); + m_components.reset(new PackProfile(this)); m_components->setOldConfigVersion("net.minecraft", m_settings->get("IntendedVersion").toString()); auto setting = m_settings->getSetting("LWJGLVersion"); m_components->setOldConfigVersion("org.lwjgl", m_settings->get("LWJGLVersion").toString()); @@ -122,14 +143,14 @@ QString MinecraftInstance::typeName() const return "Minecraft"; } -std::shared_ptr<ComponentList> MinecraftInstance::getComponentList() const +std::shared_ptr<PackProfile> MinecraftInstance::getPackProfile() const { return m_components; } QSet<QString> MinecraftInstance::traits() const { - auto components = getComponentList(); + auto components = getPackProfile(); if (!components) { return {"version-incomplete"}; @@ -170,6 +191,12 @@ QString MinecraftInstance::getLocalLibraryPath() const return libraries_dir.absolutePath(); } +QString MinecraftInstance::jarModsDir() const +{ + QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); + return jarmods_dir.absolutePath(); +} + QString MinecraftInstance::loaderModsDir() const { return FS::PathCombine(gameRoot(), "mods"); @@ -200,11 +227,6 @@ QString MinecraftInstance::instanceConfigFolder() const return FS::PathCombine(gameRoot(), "config"); } -QString MinecraftInstance::jarModsDir() const -{ - return FS::PathCombine(instanceRoot(), "jarmods"); -} - QString MinecraftInstance::libDir() const { return FS::PathCombine(gameRoot(), "lib"); @@ -215,6 +237,11 @@ QString MinecraftInstance::worldDir() const return FS::PathCombine(gameRoot(), "saves"); } +QString MinecraftInstance::resourcesDir() const +{ + return FS::PathCombine(gameRoot(), "resources"); +} + QDir MinecraftInstance::librariesPath() const { return QDir::current().absoluteFilePath("libraries"); @@ -257,7 +284,7 @@ QStringList MinecraftInstance::getNativeJars() const QStringList MinecraftInstance::extraArguments() const { auto list = BaseInstance::extraArguments(); - auto version = getComponentList(); + auto version = getPackProfile(); if (!version) return list; auto jarMods = getJarMods(); @@ -376,7 +403,8 @@ static QString replaceTokensIn(QString text, QMap<QString, QString> with) return result; } -QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) const +QStringList MinecraftInstance::processMinecraftArgs( + AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const { auto profile = m_components->getProfile(); QString args_pattern = profile->getMinecraftArguments(); @@ -385,6 +413,12 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) cons args_pattern += " --tweakClass " + tweaker; } + if (serverToJoin && !serverToJoin->address.isEmpty()) + { + args_pattern += " --server " + serverToJoin->address; + args_pattern += " --port " + QString::number(serverToJoin->port); + } + QMap<QString, QString> token_mapping; // yggdrasil! if(session) @@ -407,8 +441,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) cons token_mapping["game_directory"] = absRootDir; QString absAssetsDir = QDir("assets/").absolutePath(); auto assets = profile->getMinecraftAssets(); - // FIXME: this is wrong and should be run as an async task - token_mapping["game_assets"] = AssetsUtils::reconstructAssets(assets->id).absolutePath(); + token_mapping["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); // 1.7.3+ assets tokens token_mapping["assets_root"] = absAssetsDir; @@ -422,7 +455,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) cons return parts; } -QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) +QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QString launchScript; @@ -443,8 +476,17 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) launchScript += "appletClass " + appletClass + "\n"; } + if (serverToJoin && !serverToJoin->address.isEmpty()) + { + launchScript += "serverAddress " + serverToJoin->address + "\n"; + launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n"; + } + // generic minecraft params - for (auto param : processMinecraftArgs(session)) + for (auto param : processMinecraftArgs( + session, + nullptr /* When using a launch script, the server parameters are handled by it*/ + )) { launchScript += "param " + param + "\n"; } @@ -494,7 +536,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) return launchScript; } -QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) +QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QStringList out; out << "Main Class:" << " " + getMainClass() << ""; @@ -513,11 +555,23 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } + auto settings = this->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + if (nativeOpenAL || nativeGLFW) + { + if (nativeOpenAL) + out << "Using system OpenAL."; + if (nativeGLFW) + out << "Using system GLFW."; + out << ""; + } + // libraries and class path. { out << "Libraries:"; QStringList jars, nativeJars; - auto javaArchitecture = settings()->get("JavaArchitecture").toString(); + auto javaArchitecture = settings->get("JavaArchitecture").toString(); profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); auto printLibFile = [&](const QString & path) { @@ -544,37 +598,38 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } - if(loaderModList()->size()) - { - out << "Mods:"; - for(auto & mod: loaderModList()->allMods()) + auto printModList = [&](const QString & label, ModFolderModel & model) { + if(model.size()) { - if(!mod.enabled()) - continue; - if(mod.type() == Mod::MOD_FOLDER) - continue; - // TODO: proper implementation would need to descend into folders. + out << QString("%1:").arg(label); + auto modList = model.allMods(); + std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) { + auto aName = a.filename().completeBaseName(); + auto bName = b.filename().completeBaseName(); + return aName.localeAwareCompare(bName) < 0; + }); + for(auto & mod: modList) + { + if(mod.type() == Mod::MOD_FOLDER) + { + out << u8" [📁] " + mod.filename().completeBaseName() + " (folder)"; + continue; + } + + if(mod.enabled()) { + out << u8" [✔️] " + mod.filename().completeBaseName(); + } + else { + out << u8" [❌] " + mod.filename().completeBaseName() + " (disabled)"; + } - out << " " + mod.filename().completeBaseName(); + } + out << ""; } - out << ""; - } - - if(coreModList()->size()) - { - out << "Core Mods:"; - for(auto & coremod: coreModList()->allMods()) - { - if(!coremod.enabled()) - continue; - if(coremod.type() == Mod::MOD_FOLDER) - continue; - // TODO: proper implementation would need to descend into folders. + }; - out << " " + coremod.filename().completeBaseName(); - } - out << ""; - } + printModList("Mods", *(loaderModList().get())); + printModList("Core Mods", *(coreModList().get())); auto & jarMods = profile->getJarMods(); if(jarMods.size()) @@ -596,20 +651,20 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } - auto params = processMinecraftArgs(nullptr); + auto params = processMinecraftArgs(nullptr, serverToJoin); out << "Params:"; out << " " + params.join(' '); out << ""; QString windowParams; - if (settings()->get("LaunchMaximized").toBool()) + if (settings->get("LaunchMaximized").toBool()) { out << "Window size: max (if available)"; } else { - auto width = settings()->get("MinecraftWinWidth").toInt(); - auto height = settings()->get("MinecraftWinHeight").toInt(); + auto width = settings->get("MinecraftWinWidth").toInt(); + auto height = settings->get("MinecraftWinHeight").toInt(); out << "Window size: " + QString::number(width) + " x " + QString::number(height); } out << ""; @@ -642,8 +697,7 @@ QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSess auto i = sessionRef.u.properties.begin(); while (i != sessionRef.u.properties.end()) { - if(i.key() == "preferredLanguage") - { + if(i.value().length() <= 3) { ++i; continue; } @@ -744,9 +798,15 @@ QString MinecraftInstance::getStatusbarDescription() QString description; description.append(tr("Minecraft %1 (%2)").arg(m_components->getComponentVersion("net.minecraft")).arg(typeName())); - if(totalTimePlayed() > 0) + if(m_settings->get("ShowGameTime").toBool()) { - description.append(tr(", played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); + if (lastTimePlayed() > 0) { + description.append(tr(", last played for %1").arg(prettifyTimeDuration(lastTimePlayed()))); + } + + if (totalTimePlayed() > 0) { + description.append(tr(", total played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); + } } if(hasCrashed()) { @@ -765,28 +825,28 @@ shared_qobject_ptr<Task> MinecraftInstance::createUpdateTask(Net::Mode mode) } case Net::Mode::Online: { - return shared_qobject_ptr<Task>(new OneSixUpdate(this)); + return shared_qobject_ptr<Task>(new MinecraftUpdate(this)); } } return nullptr; } -std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session) +shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { - auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(getSharedPtr())); + // FIXME: get rid of shared_from_this ... + auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this())); auto pptr = process.get(); ENV.icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); // print a header { - process->appendStep(std::make_shared<TextPrint>(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::MultiMC)); + process->appendStep(new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::MultiMC)); } // check java { - auto step = std::make_shared<CheckJava>(pptr); - process->appendStep(step); + process->appendStep(new CheckJava(pptr)); } // check launch method @@ -794,14 +854,34 @@ std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr s QString method = launchMethod(); if(!validMethods.contains(method)) { - process->appendStep(std::make_shared<TextPrint>(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); + process->appendStep(new TextPrint(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); return process; } + // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) + { + process->appendStep(new CreateGameFolders(pptr)); + } + + if (!serverToJoin && m_settings->get("JoinServerOnLaunch").toBool()) + { + QString fullAddress = m_settings->get("JoinServerOnLaunchAddress").toString(); + serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress))); + } + + if(serverToJoin && serverToJoin->port == 25565) + { + // Resolve server address to join on launch + auto *step = new LookupServerAddress(pptr); + step->setLookupAddress(serverToJoin->address); + step->setOutputAddressPtr(serverToJoin); + process->appendStep(step); + } + // run pre-launch command if that's needed if(getPreLaunchCommand().size()) { - auto step = std::make_shared<PreLaunchCommand>(pptr); + auto step = new PreLaunchCommand(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } @@ -809,36 +889,42 @@ std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr s // if we aren't in offline mode,. if(session->status != AuthSession::PlayableOffline) { - process->appendStep(std::make_shared<ClaimAccount>(pptr, session)); - process->appendStep(std::make_shared<Update>(pptr, Net::Mode::Online)); + process->appendStep(new ClaimAccount(pptr, session)); + process->appendStep(new Update(pptr, Net::Mode::Online)); } else { - process->appendStep(std::make_shared<Update>(pptr, Net::Mode::Offline)); + process->appendStep(new Update(pptr, Net::Mode::Offline)); } // if there are any jar mods { - auto step = std::make_shared<ModMinecraftJar>(pptr); - process->appendStep(step); + process->appendStep(new ModMinecraftJar(pptr)); } - // print some instance info here... + // Scan mods folders for mods { - auto step = std::make_shared<PrintInstanceInfo>(pptr, session); - process->appendStep(step); + process->appendStep(new ScanModFolders(pptr)); } - // create the server-resource-packs folder (workaround for Minecraft bug MCL-3732) + // print some instance info here... { - auto step = std::make_shared<CreateServerResourcePacksFolder>(pptr); - process->appendStep(step); + process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin)); } // extract native jars if needed { - auto step = std::make_shared<ExtractNatives>(pptr); - process->appendStep(step); + process->appendStep(new ExtractNatives(pptr)); + } + + // reconstruct assets if needed + { + process->appendStep(new ReconstructAssets(pptr)); + } + + // verify that minimum Java requirements are met + { + process->appendStep(new VerifyJavaInstall(pptr)); } { @@ -846,16 +932,18 @@ std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr s auto method = launchMethod(); if(method == "LauncherPart") { - auto step = std::make_shared<LauncherPartLaunch>(pptr); + auto step = new LauncherPartLaunch(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); + step->setServerToJoin(serverToJoin); process->appendStep(step); } else if (method == "DirectJava") { - auto step = std::make_shared<DirectJavaLaunch>(pptr); + auto step = new DirectJavaLaunch(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); + step->setServerToJoin(serverToJoin); process->appendStep(step); } } @@ -863,7 +951,7 @@ std::shared_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr s // run post-exit command if that's needed if(getPostExitCommand().size()) { - auto step = std::make_shared<PostLaunchCommand>(pptr); + auto step = new PostLaunchCommand(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } @@ -886,53 +974,47 @@ JavaVersion MinecraftInstance::getJavaVersion() const return JavaVersion(settings()->get("JavaVersion").toString()); } -std::shared_ptr<SimpleModList> MinecraftInstance::loaderModList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) { - m_loader_mod_list.reset(new SimpleModList(loaderModsDir())); + m_loader_mod_list.reset(new ModFolderModel(loaderModsDir())); + m_loader_mod_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } - m_loader_mod_list->update(); return m_loader_mod_list; } -std::shared_ptr<ModsModel> MinecraftInstance::modsModel() const -{ - if (!m_mods_model) - { - m_mods_model.reset(new ModsModel(loaderModsDir(), coreModsDir(), modsCacheLocation())); - } - m_mods_model->update(); - return m_mods_model; -} - -std::shared_ptr<SimpleModList> MinecraftInstance::coreModList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const { if (!m_core_mod_list) { - m_core_mod_list.reset(new SimpleModList(coreModsDir())); + m_core_mod_list.reset(new ModFolderModel(coreModsDir())); + m_core_mod_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } - m_core_mod_list->update(); return m_core_mod_list; } -std::shared_ptr<SimpleModList> MinecraftInstance::resourcePackList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new SimpleModList(resourcePacksDir())); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); + m_resource_pack_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction); } - m_resource_pack_list->update(); return m_resource_pack_list; } -std::shared_ptr<SimpleModList> MinecraftInstance::texturePackList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new SimpleModList(texturePacksDir())); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir())); + m_texture_pack_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction); } - m_texture_pack_list->update(); return m_texture_pack_list; } @@ -945,6 +1027,15 @@ std::shared_ptr<WorldList> MinecraftInstance::worldList() const return m_world_list; } +std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const +{ + if (!m_game_options) + { + m_game_options.reset(new GameOptions(FS::PathCombine(gameRoot(), "options.txt"))); + } + return m_game_options; +} + QList< Mod > MinecraftInstance::getJarMods() const { auto profile = m_components->getProfile(); diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h index 5f0fa353..05600797 100644 --- a/api/logic/minecraft/MinecraftInstance.h +++ b/api/logic/minecraft/MinecraftInstance.h @@ -1,16 +1,17 @@ #pragma once #include "BaseInstance.h" #include <java/JavaVersion.h> -#include "minecraft/Mod.h" +#include "minecraft/mod/Mod.h" #include <QProcess> #include <QDir> #include "multimc_logic_export.h" +#include "minecraft/launch/MinecraftServerTarget.h" -class ModsModel; -class SimpleModList; +class ModFolderModel; class WorldList; +class GameOptions; class LaunchStep; -class ComponentList; +class PackProfile; class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance { @@ -44,6 +45,7 @@ public: QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; + QString resourcesDir() const; QDir jarmodsPath() const; QDir librariesPath() const; QDir versionsPath() const; @@ -63,24 +65,23 @@ public: ////// Profile management ////// - std::shared_ptr<ComponentList> getComponentList() const; + std::shared_ptr<PackProfile> getPackProfile() const; ////// Mod Lists ////// - std::shared_ptr<ModsModel> modsModel() const; - std::shared_ptr<SimpleModList> loaderModList() const; - std::shared_ptr<SimpleModList> coreModList() const; - std::shared_ptr<SimpleModList> resourcePackList() const; - std::shared_ptr<SimpleModList> texturePackList() const; + std::shared_ptr<ModFolderModel> loaderModList() const; + std::shared_ptr<ModFolderModel> coreModList() const; + std::shared_ptr<ModFolderModel> resourcePackList() const; + std::shared_ptr<ModFolderModel> texturePackList() const; std::shared_ptr<WorldList> worldList() const; - + std::shared_ptr<GameOptions> gameOptionsModel() const; ////// Launch stuff ////// shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) override; - std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override; + shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override; QStringList extraArguments() const override; - QStringList verboseDescription(AuthSessionPtr session) override; + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; QList<Mod> getJarMods() const; - QString createLaunchScript(AuthSessionPtr session); + QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); /// get arguments passed to java QStringList javaArguments() const; @@ -107,13 +108,10 @@ public: virtual QString getMainClass() const; // FIXME: remove - virtual QStringList processMinecraftArgs(AuthSessionPtr account) const; + virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const; virtual JavaVersion getJavaVersion() const; -signals: - void versionReloaded(); - protected: QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session); QStringList validLaunchMethods(); @@ -123,13 +121,13 @@ private: QString prettifyTimeDuration(int64_t duration); protected: // data - std::shared_ptr<ComponentList> m_components; - mutable std::shared_ptr<ModsModel> m_mods_model; - mutable std::shared_ptr<SimpleModList> m_loader_mod_list; - mutable std::shared_ptr<SimpleModList> m_core_mod_list; - mutable std::shared_ptr<SimpleModList> m_resource_pack_list; - mutable std::shared_ptr<SimpleModList> m_texture_pack_list; + std::shared_ptr<PackProfile> m_components; + mutable std::shared_ptr<ModFolderModel> m_loader_mod_list; + mutable std::shared_ptr<ModFolderModel> m_core_mod_list; + mutable std::shared_ptr<ModFolderModel> m_resource_pack_list; + mutable std::shared_ptr<ModFolderModel> m_texture_pack_list; mutable std::shared_ptr<WorldList> m_world_list; + mutable std::shared_ptr<GameOptions> m_game_options; }; typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr; diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.cpp b/api/logic/minecraft/MinecraftLoadAndCheck.cpp index a5052b53..79b0c484 100644 --- a/api/logic/minecraft/MinecraftLoadAndCheck.cpp +++ b/api/logic/minecraft/MinecraftLoadAndCheck.cpp @@ -1,6 +1,6 @@ #include "MinecraftLoadAndCheck.h" #include "MinecraftInstance.h" -#include "ComponentList.h" +#include "PackProfile.h" MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) { @@ -9,7 +9,7 @@ MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *p void MinecraftLoadAndCheck::executeTask() { // add offline metadata load task - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); components->reload(Net::Mode::Offline); m_task = components->getCurrentTask(); @@ -28,7 +28,7 @@ void MinecraftLoadAndCheck::subtaskSucceeded() { if(isFinished()) { - qCritical() << "OneSixUpdate: Subtask" << sender() << "succeeded, but work was already done!"; + qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; return; } emitSucceeded(); @@ -38,7 +38,7 @@ void MinecraftLoadAndCheck::subtaskFailed(QString error) { if(isFinished()) { - qCritical() << "OneSixUpdate: Subtask" << sender() << "failed, but work was already done!"; + qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; return; } emitFailed(error); diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.h b/api/logic/minecraft/MinecraftLoadAndCheck.h index 1f5c2018..3435b52b 100644 --- a/api/logic/minecraft/MinecraftLoadAndCheck.h +++ b/api/logic/minecraft/MinecraftLoadAndCheck.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/MinecraftUpdate.cpp b/api/logic/minecraft/MinecraftUpdate.cpp index e62ff745..8f1565b0 100644 --- a/api/logic/minecraft/MinecraftUpdate.cpp +++ b/api/logic/minecraft/MinecraftUpdate.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -14,7 +14,6 @@ */ #include "Env.h" -#include <minecraft/forge/ForgeXzDownload.h> #include "MinecraftUpdate.h" #include "MinecraftInstance.h" @@ -24,9 +23,8 @@ #include <QDataStream> #include "BaseInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "minecraft/Library.h" -#include "net/URLConstants.h" #include <FileSystem.h> #include "update/FoldersTask.h" @@ -37,11 +35,11 @@ #include <meta/Index.h> #include <meta/Version.h> -OneSixUpdate::OneSixUpdate(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +MinecraftUpdate::MinecraftUpdate(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) { } -void OneSixUpdate::executeTask() +void MinecraftUpdate::executeTask() { m_tasks.clear(); // create folders @@ -51,7 +49,7 @@ void OneSixUpdate::executeTask() // add metadata update task if necessary { - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); components->reload(Net::Mode::Online); auto task = components->getCurrentTask(); if(task) @@ -83,7 +81,7 @@ void OneSixUpdate::executeTask() next(); } -void OneSixUpdate::next() +void MinecraftUpdate::next() { if(m_abort) { @@ -99,10 +97,10 @@ void OneSixUpdate::next() if(m_currentTask > 0) { auto task = m_tasks[m_currentTask - 1]; - disconnect(task.get(), &Task::succeeded, this, &OneSixUpdate::subtaskSucceeded); - disconnect(task.get(), &Task::failed, this, &OneSixUpdate::subtaskFailed); - disconnect(task.get(), &Task::progress, this, &OneSixUpdate::progress); - disconnect(task.get(), &Task::status, this, &OneSixUpdate::setStatus); + disconnect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); + disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); + disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); + disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); } if(m_currentTask == m_tasks.size()) { @@ -113,13 +111,13 @@ void OneSixUpdate::next() // if the task is already finished by the time we look at it, skip it if(task->isFinished()) { - qCritical() << "OneSixUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get(); + qCritical() << "MinecraftUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get(); next(); } - connect(task.get(), &Task::succeeded, this, &OneSixUpdate::subtaskSucceeded); - connect(task.get(), &Task::failed, this, &OneSixUpdate::subtaskFailed); - connect(task.get(), &Task::progress, this, &OneSixUpdate::progress); - connect(task.get(), &Task::status, this, &OneSixUpdate::setStatus); + connect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); + connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); + connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); + connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); // if the task is already running, do not start it again if(!task->isRunning()) { @@ -127,35 +125,35 @@ void OneSixUpdate::next() } } -void OneSixUpdate::subtaskSucceeded() +void MinecraftUpdate::subtaskSucceeded() { if(isFinished()) { - qCritical() << "OneSixUpdate: Subtask" << sender() << "succeeded, but work was already done!"; + qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; return; } auto senderTask = QObject::sender(); auto currentTask = m_tasks[m_currentTask].get(); if(senderTask != currentTask) { - qDebug() << "OneSixUpdate: Subtask" << sender() << "succeeded out of order."; + qDebug() << "MinecraftUpdate: Subtask" << sender() << "succeeded out of order."; return; } next(); } -void OneSixUpdate::subtaskFailed(QString error) +void MinecraftUpdate::subtaskFailed(QString error) { if(isFinished()) { - qCritical() << "OneSixUpdate: Subtask" << sender() << "failed, but work was already done!"; + qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; return; } auto senderTask = QObject::sender(); auto currentTask = m_tasks[m_currentTask].get(); if(senderTask != currentTask) { - qDebug() << "OneSixUpdate: Subtask" << sender() << "failed out of order."; + qDebug() << "MinecraftUpdate: Subtask" << sender() << "failed out of order."; m_failed_out_of_order = true; m_fail_reason = error; return; @@ -164,7 +162,7 @@ void OneSixUpdate::subtaskFailed(QString error) } -bool OneSixUpdate::abort() +bool MinecraftUpdate::abort() { if(!m_abort) { @@ -178,7 +176,7 @@ bool OneSixUpdate::abort() return true; } -bool OneSixUpdate::canAbort() const +bool MinecraftUpdate::canAbort() const { return true; } diff --git a/api/logic/minecraft/MinecraftUpdate.h b/api/logic/minecraft/MinecraftUpdate.h index f7b37d73..fadebff9 100644 --- a/api/logic/minecraft/MinecraftUpdate.h +++ b/api/logic/minecraft/MinecraftUpdate.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -27,12 +27,12 @@ class MinecraftVersion; class MinecraftInstance; -class OneSixUpdate : public Task +class MinecraftUpdate : public Task { Q_OBJECT public: - explicit OneSixUpdate(MinecraftInstance *inst, QObject *parent = 0); - virtual ~OneSixUpdate() {}; + explicit MinecraftUpdate(MinecraftInstance *inst, QObject *parent = 0); + virtual ~MinecraftUpdate() {}; void executeTask() override; bool canAbort() const override; diff --git a/api/logic/minecraft/Mod.cpp b/api/logic/minecraft/Mod.cpp deleted file mode 100644 index bd209211..00000000 --- a/api/logic/minecraft/Mod.cpp +++ /dev/null @@ -1,378 +0,0 @@ -/* Copyright 2013-2018 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. - */ - -#include <QDir> -#include <QString> -#include <QJsonDocument> -#include <QJsonObject> -#include <QJsonArray> -#include <QJsonValue> -#include <quazip.h> -#include <quazipfile.h> - -#include "Mod.h" -#include "settings/INIFile.h" -#include <FileSystem.h> -#include <QDebug> - -Mod::Mod(const QFileInfo &file) -{ - repath(file); - m_changedDateTime = file.lastModified(); -} - -void Mod::repath(const QFileInfo &file) -{ - m_file = file; - QString name_base = file.fileName(); - - m_type = Mod::MOD_UNKNOWN; - - if (m_file.isDir()) - { - m_type = MOD_FOLDER; - m_name = name_base; - m_mmc_id = name_base; - } - else if (m_file.isFile()) - { - if (name_base.endsWith(".disabled")) - { - m_enabled = false; - name_base.chop(9); - } - else - { - m_enabled = true; - } - m_mmc_id = name_base; - if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) - { - m_type = MOD_ZIPFILE; - name_base.chop(4); - } - else if (name_base.endsWith(".litemod")) - { - m_type = MOD_LITEMOD; - name_base.chop(8); - } - else - { - m_type = MOD_SINGLEFILE; - } - m_name = name_base; - } - - if (m_type == MOD_ZIPFILE) - { - QuaZip zip(m_file.filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return; - - QuaZipFile file(&zip); - - if (zip.setCurrentFile("mcmod.info")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - ReadMCModInfo(file.readAll()); - file.close(); - zip.close(); - return; - } - else if (zip.setCurrentFile("forgeversion.properties")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - ReadForgeInfo(file.readAll()); - file.close(); - zip.close(); - return; - } - - zip.close(); - } - else if (m_type == MOD_FOLDER) - { - QFileInfo mcmod_info(FS::PathCombine(m_file.filePath(), "mcmod.info")); - if (mcmod_info.isFile()) - { - QFile mcmod(mcmod_info.filePath()); - if (!mcmod.open(QIODevice::ReadOnly)) - return; - auto data = mcmod.readAll(); - if (data.isEmpty() || data.isNull()) - return; - ReadMCModInfo(data); - } - } - else if (m_type == MOD_LITEMOD) - { - QuaZip zip(m_file.filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return; - - QuaZipFile file(&zip); - - if (zip.setCurrentFile("litemod.json")) - { - if (!file.open(QIODevice::ReadOnly)) - { - zip.close(); - return; - } - - ReadLiteModInfo(file.readAll()); - file.close(); - } - zip.close(); - } -} - -// NEW format -// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 - -// OLD format: -// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc -void Mod::ReadMCModInfo(QByteArray contents) -{ - auto getInfoFromArray = [&](QJsonArray arr)->void - { - if (!arr.at(0).isObject()) - return; - auto firstObj = arr.at(0).toObject(); - m_mod_id = firstObj.value("modid").toString(); - m_name = firstObj.value("name").toString(); - m_version = firstObj.value("version").toString(); - m_homeurl = firstObj.value("url").toString(); - m_updateurl = firstObj.value("updateUrl").toString(); - m_homeurl = m_homeurl.trimmed(); - if(!m_homeurl.isEmpty()) - { - // fix up url. - if (!m_homeurl.startsWith("http://") && !m_homeurl.startsWith("https://") && - !m_homeurl.startsWith("ftp://")) - { - m_homeurl.prepend("http://"); - } - } - m_description = firstObj.value("description").toString(); - QJsonArray authors = firstObj.value("authorList").toArray(); - if (authors.size() == 0) - authors = firstObj.value("authors").toArray(); - - if (authors.size() == 0) - m_authors = ""; - else if (authors.size() >= 1) - { - m_authors = authors.at(0).toString(); - for (int i = 1; i < authors.size(); i++) - { - m_authors += ", " + authors.at(i).toString(); - } - } - m_credits = firstObj.value("credits").toString(); - return; - } - ; - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); - // this is the very old format that had just the array - if (jsonDoc.isArray()) - { - getInfoFromArray(jsonDoc.array()); - } - else if (jsonDoc.isObject()) - { - auto val = jsonDoc.object().value("modinfoversion"); - if(val.isUndefined()) - val = jsonDoc.object().value("modListVersion"); - int version = val.toDouble(); - if (version != 2) - { - qCritical() << "BAD stuff happened to mod json:"; - qCritical() << contents; - return; - } - auto arrVal = jsonDoc.object().value("modlist"); - if(arrVal.isUndefined()) - arrVal = jsonDoc.object().value("modList"); - if (arrVal.isArray()) - { - getInfoFromArray(arrVal.toArray()); - } - } -} - -void Mod::ReadForgeInfo(QByteArray contents) -{ - // Read the data - m_name = "Minecraft Forge"; - m_mod_id = "Forge"; - m_homeurl = "http://www.minecraftforge.net/forum/"; - INIFile ini; - if (!ini.loadFile(contents)) - return; - - QString major = ini.get("forge.major.number", "0").toString(); - QString minor = ini.get("forge.minor.number", "0").toString(); - QString revision = ini.get("forge.revision.number", "0").toString(); - QString build = ini.get("forge.build.number", "0").toString(); - - m_version = major + "." + minor + "." + revision + "." + build; -} - -void Mod::ReadLiteModInfo(QByteArray contents) -{ - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); - auto object = jsonDoc.object(); - if (object.contains("name")) - { - m_mod_id = m_name = object.value("name").toString(); - } - if (object.contains("version")) - { - m_version = object.value("version").toString(""); - } - else - { - m_version = object.value("revision").toString(""); - } - m_mcversion = object.value("mcversion").toString(); - m_authors = object.value("author").toString(); - m_description = object.value("description").toString(); - m_homeurl = object.value("url").toString(); -} - -bool Mod::replace(Mod &with) -{ - if (!destroy()) - return false; - bool success = false; - auto t = with.type(); - - if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE || t == MOD_LITEMOD) - { - qDebug() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath(); - success = QFile::copy(with.m_file.filePath(), m_file.filePath()); - } - if (t == MOD_FOLDER) - { - success = FS::copy(with.m_file.filePath(), m_file.path())(); - } - if (success) - { - m_name = with.m_name; - m_mmc_id = with.m_mmc_id; - m_mod_id = with.m_mod_id; - m_version = with.m_version; - m_mcversion = with.m_mcversion; - m_description = with.m_description; - m_authors = with.m_authors; - m_credits = with.m_credits; - m_homeurl = with.m_homeurl; - m_type = with.m_type; - m_file.refresh(); - } - return success; -} - -bool Mod::destroy() -{ - if (m_type == MOD_FOLDER) - { - QDir d(m_file.filePath()); - if (d.removeRecursively()) - { - m_type = MOD_UNKNOWN; - return true; - } - return false; - } - else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD) - { - QFile f(m_file.filePath()); - if (f.remove()) - { - m_type = MOD_UNKNOWN; - return true; - } - return false; - } - return true; -} - -QString Mod::version() const -{ - switch (type()) - { - case MOD_ZIPFILE: - case MOD_LITEMOD: - return m_version; - case MOD_FOLDER: - return "Folder"; - case MOD_SINGLEFILE: - return "File"; - default: - return "VOID"; - } -} - -bool Mod::enable(bool value) -{ - if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) - return false; - - if (m_enabled == value) - return false; - - QString path = m_file.absoluteFilePath(); - if (value) - { - QFile foo(path); - if (!path.endsWith(".disabled")) - return false; - path.chop(9); - if (!foo.rename(path)) - return false; - } - else - { - QFile foo(path); - path += ".disabled"; - if (!foo.rename(path)) - return false; - } - m_file = QFileInfo(path); - m_enabled = value; - return true; -} -bool Mod::operator==(const Mod &other) const -{ - return mmc_id() == other.mmc_id(); -} -bool Mod::strongCompare(const Mod &other) const -{ - return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type(); -} diff --git a/api/logic/minecraft/ModsModel.cpp b/api/logic/minecraft/ModsModel.cpp deleted file mode 100644 index e401618a..00000000 --- a/api/logic/minecraft/ModsModel.cpp +++ /dev/null @@ -1,374 +0,0 @@ -/* Copyright 2013-2018 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. - */ - -#include "ModsModel.h" -#include <FileSystem.h> -#include <QMimeData> -#include <QUrl> -#include <QUuid> -#include <QString> -#include <QFileSystemWatcher> -#include <QDebug> - -ModsModel::ModsModel(const QString &mainDir, const QString &coreDir, const QString &cacheLocation) - :QAbstractListModel(), m_mainDir(mainDir), m_coreDir(coreDir) -{ - FS::ensureFolderPathExists(m_mainDir.absolutePath()); - m_mainDir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | - QDir::NoSymLinks); - m_mainDir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); - m_watcher = new QFileSystemWatcher(this); - connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); -} - -void ModsModel::startWatching() -{ - if(is_watching) - return; - - update(); - - is_watching = m_watcher->addPath(m_mainDir.absolutePath()); - if (is_watching) - { - qDebug() << "Started watching " << m_mainDir.absolutePath(); - } - else - { - qDebug() << "Failed to start watching " << m_mainDir.absolutePath(); - } -} - -void ModsModel::stopWatching() -{ - if(!is_watching) - return; - - is_watching = !m_watcher->removePath(m_mainDir.absolutePath()); - if (!is_watching) - { - qDebug() << "Stopped watching " << m_mainDir.absolutePath(); - } - else - { - qDebug() << "Failed to stop watching " << m_mainDir.absolutePath(); - } -} - -bool ModsModel::update() -{ - if (!isValid()) - return false; - - QList<Mod> orderedMods; - QList<Mod> newMods; - m_mainDir.refresh(); - auto folderContents = m_mainDir.entryInfoList(); - bool orderOrStateChanged = false; - - // if there are any untracked files... - if (folderContents.size()) - { - // the order surely changed! - for (auto entry : folderContents) - { - newMods.append(Mod(entry)); - } - orderedMods.append(newMods); - orderOrStateChanged = true; - } - // otherwise, if we were already tracking some mods - else if (mods.size()) - { - // if the number doesn't match, order changed. - if (mods.size() != orderedMods.size()) - orderOrStateChanged = true; - // if it does match, compare the mods themselves - else - for (int i = 0; i < mods.size(); i++) - { - if (!mods[i].strongCompare(orderedMods[i])) - { - orderOrStateChanged = true; - break; - } - } - } - beginResetModel(); - mods.swap(orderedMods); - endResetModel(); - if (orderOrStateChanged) - { - emit changed(); - } - return true; -} - -void ModsModel::directoryChanged(QString path) -{ - update(); -} - -bool ModsModel::isValid() -{ - return m_mainDir.exists() && m_mainDir.isReadable(); -} - -bool ModsModel::installMod(const QString &filename) -{ - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName - QFileInfo fileinfo(FS::NormalizePath(filename)); - - qDebug() << "installing: " << fileinfo.absoluteFilePath(); - - if (!fileinfo.exists() || !fileinfo.isReadable()) - { - return false; - } - Mod m(fileinfo); - if (!m.valid()) - return false; - - auto type = m.type(); - if (type == Mod::MOD_UNKNOWN) - return false; - if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) - { - QString newpath = FS::PathCombine(m_mainDir.path(), fileinfo.fileName()); - if (!QFile::copy(fileinfo.filePath(), newpath)) - return false; - FS::updateTimestamp(newpath); - m.repath(newpath); - update(); - return true; - } - else if (type == Mod::MOD_FOLDER) - { - QString from = fileinfo.filePath(); - QString to = FS::PathCombine(m_mainDir.path(), fileinfo.fileName()); - if (!FS::copy(from, to)()) - return false; - m.repath(to); - update(); - return true; - } - return false; -} - -bool ModsModel::enableMods(const QModelIndexList& indexes, bool enable) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.enable(enable); - emit dataChanged(i, i); - } - emit changed(); - return true; -} - -bool ModsModel::deleteMods(const QModelIndexList& indexes) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.destroy(); - } - emit changed(); - return true; -} - -int ModsModel::columnCount(const QModelIndex &parent) const -{ - return NUM_COLUMNS; -} - -QVariant ModsModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - int row = index.row(); - int column = index.column(); - - if (row < 0 || row >= mods.size()) - return QVariant(); - - switch (role) - { - case Qt::DisplayRole: - switch (column) - { - case NameColumn: - return mods[row].name(); - case VersionColumn: - return mods[row].version(); - case DateColumn: - return mods[row].dateTimeChanged(); - case LocationColumn: - return "Unknown"; - - default: - return QVariant(); - } - - case Qt::ToolTipRole: - return mods[row].mmc_id(); - - case Qt::CheckStateRole: - switch (column) - { - case ActiveColumn: - return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; - default: - return QVariant(); - } - default: - return QVariant(); - } -} - -bool ModsModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) - { - return false; - } - - if (role == Qt::CheckStateRole) - { - auto &mod = mods[index.row()]; - if (mod.enable(!mod.enabled())) - { - emit dataChanged(index, index); - return true; - } - } - return false; -} - -QVariant ModsModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - switch (role) - { - case Qt::DisplayRole: - switch (section) - { - case ActiveColumn: - return QString(); - case NameColumn: - return tr("Name"); - case VersionColumn: - return tr("Version"); - case DateColumn: - return tr("Last changed"); - case LocationColumn: - return tr("Location"); - default: - return QVariant(); - } - - case Qt::ToolTipRole: - switch (section) - { - case ActiveColumn: - return tr("Is the mod enabled?"); - case NameColumn: - return tr("The name of the mod."); - case VersionColumn: - return tr("The version of the mod."); - case DateColumn: - return tr("The date and time this mod was last changed (or added)."); - case LocationColumn: - return tr("Where the mod is located (inside or outside the instance)."); - default: - return QVariant(); - } - default: - return QVariant(); - } - return QVariant(); -} - -Qt::ItemFlags ModsModel::flags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - if (index.isValid()) - return Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled | - defaultFlags; - else - return Qt::ItemIsDropEnabled | defaultFlags; -} - -Qt::DropActions ModsModel::supportedDropActions() const -{ - // copy from outside, move from within and other mod lists - return Qt::CopyAction | Qt::MoveAction; -} - -QStringList ModsModel::mimeTypes() const -{ - QStringList types; - types << "text/uri-list"; - return types; -} - -bool ModsModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) -{ - if (action == Qt::IgnoreAction) - { - return true; - } - - // check if the action is supported - if (!data || !(action & supportedDropActions())) - { - return false; - } - - // files dropped from outside? - if (data->hasUrls()) - { - bool was_watching = is_watching; - if (was_watching) - { - stopWatching(); - } - auto urls = data->urls(); - for (auto url : urls) - { - // only local files may be dropped... - if (!url.isLocalFile()) - { - continue; - } - // TODO: implement not only copy, but also move - // FIXME: handle errors here - installMod(url.toLocalFile()); - } - if (was_watching) - { - startWatching(); - } - return true; - } - return false; -} diff --git a/api/logic/minecraft/ModsModel.h b/api/logic/minecraft/ModsModel.h deleted file mode 100644 index b8980ac4..00000000 --- a/api/logic/minecraft/ModsModel.h +++ /dev/null @@ -1,123 +0,0 @@ -/* Copyright 2013-2018 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. - */ - -#pragma once - -#include <QList> -#include <QString> -#include <QDir> -#include <QAbstractListModel> - -#include "minecraft/Mod.h" - -#include "multimc_logic_export.h" - -class LegacyInstance; -class BaseInstance; -class QFileSystemWatcher; - -/** - * A legacy mod list. - * Backed by a folder. - */ -class MULTIMC_LOGIC_EXPORT ModsModel : public QAbstractListModel -{ - Q_OBJECT -public: - enum Columns - { - ActiveColumn = 0, - NameColumn, - DateColumn, - VersionColumn, - LocationColumn, - NUM_COLUMNS - }; - ModsModel(const QString &mainDir, const QString &coreDir, const QString &cacheFile); - - virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - Qt::DropActions supportedDropActions() const override; - - /// flags, mostly to support drag&drop - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - QStringList mimeTypes() const override; - bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; - - virtual int rowCount(const QModelIndex &) const override - { - return size(); - } - ; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - virtual int columnCount(const QModelIndex &parent) const override; - - size_t size() const - { - return mods.size(); - } - ; - bool empty() const - { - return size() == 0; - } - Mod &operator[](size_t index) - { - return mods[index]; - } - - /// Reloads the mod list and returns true if the list changed. - virtual bool update(); - - /** - * Adds the given mod to the list at the given index - if the list supports custom ordering - */ - bool installMod(const QString& filename); - - /// Deletes all the selected mods - virtual bool deleteMods(const QModelIndexList &indexes); - - /// Enable or disable listed mods - virtual bool enableMods(const QModelIndexList &indexes, bool enable = true); - - void startWatching(); - void stopWatching(); - - virtual bool isValid(); - - QDir dir() - { - return m_mainDir; - } - - const QList<Mod> & allMods() - { - return mods; - } - -private -slots: - void directoryChanged(QString path); - -signals: - void changed(); - -protected: - QFileSystemWatcher *m_watcher; - bool is_watching = false; - QDir m_mainDir; - QDir m_coreDir; - QList<Mod> mods; -}; diff --git a/api/logic/minecraft/MojangVersionFormat.cpp b/api/logic/minecraft/MojangVersionFormat.cpp index 33d3c54c..f9cb2228 100644 --- a/api/logic/minecraft/MojangVersionFormat.cpp +++ b/api/logic/minecraft/MojangVersionFormat.cpp @@ -220,7 +220,7 @@ VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument &doc { auto libObj = requireObject(libVal); - auto lib = MojangVersionFormat::libraryFromJson(libObj, filename); + auto lib = MojangVersionFormat::libraryFromJson(*out, libObj, filename); out->libraries.append(lib); } } @@ -283,14 +283,18 @@ QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch } } -LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); } - out->m_name = libObj.value("name").toString(); + auto rawName = libObj.value("name").toString(); + out->m_name = rawName; + if(!out->m_name.valid()) { + problems.addProblem(ProblemSeverity::Error, QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName)); + } Bits::readString(libObj, "url", out->m_repositoryURL); if (libObj.contains("extract")) @@ -333,7 +337,7 @@ LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QJsonObject MojangVersionFormat::libraryToJson(Library *library) { QJsonObject libRoot; - libRoot.insert("name", (QString)library->m_name); + libRoot.insert("name", library->m_name.serialize()); if (!library->m_repositoryURL.isEmpty()) { libRoot.insert("url", library->m_repositoryURL); diff --git a/api/logic/minecraft/MojangVersionFormat.h b/api/logic/minecraft/MojangVersionFormat.h index 76c529e9..2871dae4 100644 --- a/api/logic/minecraft/MojangVersionFormat.h +++ b/api/logic/minecraft/MojangVersionFormat.h @@ -3,6 +3,7 @@ #include <minecraft/VersionFile.h> #include <minecraft/Library.h> #include <QJsonDocument> +#include <ProblemProvider.h> #include "multimc_logic_export.h" @@ -20,6 +21,6 @@ public: static QJsonDocument versionFileToJson(const VersionFilePtr &patch); // libraries - static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject libraryToJson(Library *library); }; diff --git a/api/logic/minecraft/OneSixVersionFormat.cpp b/api/logic/minecraft/OneSixVersionFormat.cpp index 6f3b926b..d6aaa790 100644 --- a/api/logic/minecraft/OneSixVersionFormat.cpp +++ b/api/logic/minecraft/OneSixVersionFormat.cpp @@ -13,9 +13,9 @@ static void readString(const QJsonObject &root, const QString &key, QString &var } } -LibraryPtr OneSixVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename) { - LibraryPtr out = MojangVersionFormat::libraryFromJson(libObj, filename); + LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename); readString(libObj, "MMC-hint", out->m_hint); readString(libObj, "MMC-absulute_url", out->m_absoluteURL); readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); @@ -115,7 +115,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::jarModFromJson(libObj, filename); + auto lib = OneSixVersionFormat::jarModFromJson(*out, libObj, filename); // and add to jar mods out->jarMods.append(lib); } @@ -126,7 +126,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::plusJarModFromJson(libObj, filename, out->name); + auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name); // and add to jar mods out->jarMods.append(lib); } @@ -138,20 +138,20 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::modFromJson(libObj, filename); + auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename); // and add to jar mods out->mods.append(lib); } } - auto readLibs = [&](const char * which) + auto readLibs = [&](const char * which, QList<LibraryPtr> & outList) { for (auto libVal : requireArray(root.value(which))) { QJsonObject libObj = requireObject(libVal); // parse the library - auto lib = libraryFromJson(libObj, filename); - out->libraries.append(lib); + auto lib = libraryFromJson(*out, libObj, filename); + outList.append(lib); } }; bool hasPlusLibs = root.contains("+libraries"); @@ -160,23 +160,27 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { out->addProblem(ProblemSeverity::Warning, QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); - readLibs("libraries"); - readLibs("+libraries"); + readLibs("libraries", out->libraries); + readLibs("+libraries", out->libraries); } else if (hasLibs) { - readLibs("libraries"); + readLibs("libraries", out->libraries); } else if(hasPlusLibs) { - readLibs("+libraries"); + readLibs("+libraries", out->libraries); + } + + if(root.contains("mavenFiles")) { + readLibs("mavenFiles", out->mavenFiles); } // if we have mainJar, just use it if(root.contains("mainJar")) { QJsonObject libObj = requireObject(root, "mainJar"); - out->mainJar = libraryFromJson(libObj, filename); + out->mainJar = libraryFromJson(*out, libObj, filename); } // else reconstruct it from downloads and id ... if that's available else if(!out->minecraftVersion.isEmpty()) @@ -194,7 +198,10 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc // FIXME: this will eventually break... else { - lib->setAbsoluteUrl(URLConstants::getLegacyJarUrl(out->minecraftVersion)); + out->addProblem( + ProblemSeverity::Error, + QObject::tr("URL for the main jar could not be determined - Mojang removed the server that we used as fallback.") + ); } out->mainJar = lib; } @@ -276,6 +283,15 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch } root.insert("libraries", array); } + if (!patch->mavenFiles.isEmpty()) + { + QJsonArray array; + for (auto value: patch->mavenFiles) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("mavenFiles", array); + } if (!patch->jarMods.isEmpty()) { QJsonArray array; @@ -314,8 +330,12 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch } } -LibraryPtr OneSixVersionFormat::plusJarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName) -{ +LibraryPtr OneSixVersionFormat::plusJarModFromJson( + ProblemContainer & problems, + const QJsonObject &libObj, + const QString &filename, + const QString &originalName +) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { @@ -350,9 +370,9 @@ LibraryPtr OneSixVersionFormat::plusJarModFromJson(const QJsonObject &libObj, co return out; } -LibraryPtr OneSixVersionFormat::jarModFromJson(const QJsonObject& libObj, const QString& filename) +LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename) { - return libraryFromJson(libObj, filename); + return libraryFromJson(problems, libObj, filename); } @@ -361,9 +381,9 @@ QJsonObject OneSixVersionFormat::jarModtoJson(Library *jarmod) return libraryToJson(jarmod); } -LibraryPtr OneSixVersionFormat::modFromJson(const QJsonObject& libObj, const QString& filename) +LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename) { - return libraryFromJson(libObj, filename); + return libraryFromJson(problems, libObj, filename); } QJsonObject OneSixVersionFormat::modtoJson(Library *jarmod) diff --git a/api/logic/minecraft/OneSixVersionFormat.h b/api/logic/minecraft/OneSixVersionFormat.h index 6bbebca0..1a091d88 100644 --- a/api/logic/minecraft/OneSixVersionFormat.h +++ b/api/logic/minecraft/OneSixVersionFormat.h @@ -1,9 +1,10 @@ #pragma once #include <minecraft/VersionFile.h> -#include <minecraft/ComponentList.h> +#include <minecraft/PackProfile.h> #include <minecraft/Library.h> #include <QJsonDocument> +#include <ProblemProvider.h> class OneSixVersionFormat { @@ -13,17 +14,17 @@ public: static QJsonDocument versionFileToJson(const VersionFilePtr &patch); // libraries - static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject libraryToJson(Library *library); // DEPRECATED: old 'plus' jar mods generated by the application - static LibraryPtr plusJarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName); + static LibraryPtr plusJarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename, const QString &originalName); // new jar mods derived from libraries - static LibraryPtr jarModFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr jarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject jarModtoJson(Library * jarmod); // mods, also derived from libraries - static LibraryPtr modFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr modFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject modtoJson(Library * jarmod); }; diff --git a/api/logic/minecraft/OpSys.cpp b/api/logic/minecraft/OpSys.cpp index 2e18634b..f6a4ed1c 100644 --- a/api/logic/minecraft/OpSys.cpp +++ b/api/logic/minecraft/OpSys.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/OpSys.h b/api/logic/minecraft/OpSys.h index 8ea84587..63c750b1 100644 --- a/api/logic/minecraft/OpSys.h +++ b/api/logic/minecraft/OpSys.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/ComponentList.cpp b/api/logic/minecraft/PackProfile.cpp index 7a865c60..f6918116 100644 --- a/api/logic/minecraft/ComponentList.cpp +++ b/api/logic/minecraft/PackProfile.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -32,21 +32,23 @@ #include <QTimer> #include <Json.h> -#include "ComponentList.h" -#include "ComponentList_p.h" +#include "PackProfile.h" +#include "PackProfile_p.h" #include "ComponentUpdateTask.h" -ComponentList::ComponentList(MinecraftInstance * instance) +PackProfile::PackProfile(MinecraftInstance * instance) : QAbstractListModel() { - d.reset(new ComponentListData); + d.reset(new PackProfileData); d->m_instance = instance; d->m_saveTimer.setSingleShot(true); d->m_saveTimer.setInterval(5000); - connect(&d->m_saveTimer, &QTimer::timeout, this, &ComponentList::save_internal); + d->interactionDisabled = instance->isRunning(); + connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction); + connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal); } -ComponentList::~ComponentList() +PackProfile::~PackProfile() { saveNow(); } @@ -95,7 +97,7 @@ static QJsonObject componentToJsonV1(ComponentPtr component) return obj; } -static ComponentPtr componentFromJsonV1(ComponentList * parent, const QString & componentJsonPattern, const QJsonObject &obj) +static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & componentJsonPattern, const QJsonObject &obj) { // critical auto uid = Json::requireString(obj.value("uid")); @@ -118,7 +120,7 @@ static ComponentPtr componentFromJsonV1(ComponentList * parent, const QString & } // Save the given component container data to a file -static bool saveComponentList(const QString & filename, const ComponentContainer & container) +static bool savePackProfile(const QString & filename, const ComponentContainer & container) { QJsonObject obj; obj.insert("formatVersion", currentComponentsFileVersion); @@ -151,7 +153,7 @@ static bool saveComponentList(const QString & filename, const ComponentContainer } // Read the given file into component containers -static bool loadComponentList(ComponentList * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container) +static bool loadPackProfile(PackProfile * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container) { QFile componentsFile(filename); if (!componentsFile.exists()) @@ -208,7 +210,7 @@ static bool loadComponentList(ComponentList * parent, const QString & filename, // BEGIN: save/load logic -void ComponentList::saveNow() +void PackProfile::saveNow() { if(saveIsScheduled()) { @@ -217,18 +219,18 @@ void ComponentList::saveNow() } } -bool ComponentList::saveIsScheduled() const +bool PackProfile::saveIsScheduled() const { return d->dirty; } -void ComponentList::buildingFromScratch() +void PackProfile::buildingFromScratch() { d->loaded = true; d->dirty = true; } -void ComponentList::scheduleSave() +void PackProfile::scheduleSave() { if(!d->loaded) { @@ -243,30 +245,30 @@ void ComponentList::scheduleSave() d->m_saveTimer.start(); } -QString ComponentList::componentsFilePath() const +QString PackProfile::componentsFilePath() const { return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); } -QString ComponentList::patchesPattern() const +QString PackProfile::patchesPattern() const { return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); } -QString ComponentList::patchFilePathForUid(const QString& uid) const +QString PackProfile::patchFilePathForUid(const QString& uid) const { return patchesPattern().arg(uid); } -void ComponentList::save_internal() +void PackProfile::save_internal() { qDebug() << "Component list save performed now for" << d->m_instance->name(); auto filename = componentsFilePath(); - saveComponentList(filename, d->components); + savePackProfile(filename, d->components); d->dirty = false; } -bool ComponentList::load() +bool PackProfile::load() { auto filename = componentsFilePath(); QFile componentsFile(filename); @@ -284,7 +286,7 @@ bool ComponentList::load() // load the new component list and swap it with the current one... ComponentContainer newComponents; - if(!loadComponentList(this, filename, patchesPattern(), newComponents)) + if(!loadPackProfile(this, filename, patchesPattern(), newComponents)) { qCritical() << "Failed to load the component config for instance" << d->m_instance->name(); return false; @@ -296,7 +298,7 @@ bool ComponentList::load() // disconnect all the old components for(auto component: d->components) { - disconnect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged); + disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); } d->components.clear(); d->componentIndex.clear(); @@ -307,7 +309,7 @@ bool ComponentList::load() qWarning() << "Ignoring duplicate component entry" << component->m_uid; continue; } - connect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged); + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); d->components.append(component); d->componentIndex[component->m_uid] = component; } @@ -317,7 +319,7 @@ bool ComponentList::load() } } -void ComponentList::reload(Net::Mode netmode) +void PackProfile::reload(Net::Mode netmode) { // Do not reload when the update/resolve task is running. It is in control. if(d->m_updateTask) @@ -337,29 +339,29 @@ void ComponentList::reload(Net::Mode netmode) } } -shared_qobject_ptr<Task> ComponentList::getCurrentTask() +shared_qobject_ptr<Task> PackProfile::getCurrentTask() { return d->m_updateTask; } -void ComponentList::resolve(Net::Mode netmode) +void PackProfile::resolve(Net::Mode netmode) { auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this); d->m_updateTask.reset(updateTask); - connect(updateTask, &ComponentUpdateTask::succeeded, this, &ComponentList::updateSucceeded); - connect(updateTask, &ComponentUpdateTask::failed, this, &ComponentList::updateFailed); + connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded); + connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed); d->m_updateTask->start(); } -void ComponentList::updateSucceeded() +void PackProfile::updateSucceeded() { qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name(); d->m_updateTask.reset(); invalidateLaunchProfile(); } -void ComponentList::updateFailed(const QString& error) +void PackProfile::updateFailed(const QString& error) { qDebug() << "Component list update/resolve task failed for" << d->m_instance->name() << "Reason:" << error; d->m_updateTask.reset(); @@ -429,7 +431,7 @@ static void upgradeDeprecatedFiles(QString root, QString instanceName) * - Part is taken from the old order.json file. * - Part is loaded from loose json files in the instance's `patches` directory. */ -bool ComponentList::migratePreComponentConfig() +bool PackProfile::migratePreComponentConfig() { // upgrade the very old files from the beginnings of MultiMC 5 upgradeDeprecatedFiles(d->m_instance->instanceRoot(), d->m_instance->name()); @@ -596,17 +598,17 @@ bool ComponentList::migratePreComponentConfig() } } // new we have a complete list of components... - return saveComponentList(componentsFilePath(), components); + return savePackProfile(componentsFilePath(), components); } // END: save/load -void ComponentList::appendComponent(ComponentPtr component) +void PackProfile::appendComponent(ComponentPtr component) { insertComponent(d->components.size(), component); } -void ComponentList::insertComponent(size_t index, ComponentPtr component) +void PackProfile::insertComponent(size_t index, ComponentPtr component) { auto id = component->getID(); if(id.isEmpty()) @@ -623,18 +625,21 @@ void ComponentList::insertComponent(size_t index, ComponentPtr component) d->components.insert(index, component); d->componentIndex[id] = component; endInsertRows(); - connect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged); + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); scheduleSave(); } -void ComponentList::componentDataChanged() +void PackProfile::componentDataChanged() { auto objPtr = qobject_cast<Component *>(sender()); if(!objPtr) { - qWarning() << "ComponentList got dataChenged signal from a non-Component!"; + qWarning() << "PackProfile got dataChenged signal from a non-Component!"; return; } + if(objPtr->getID() == "net.minecraft") { + emit minecraftChanged(); + } // figure out which one is it... in a seriously dumb way. int index = 0; for (auto component: d->components) @@ -647,10 +652,10 @@ void ComponentList::componentDataChanged() } index++; } - qWarning() << "ComponentList got dataChenged signal from a Component which does not belong to it!"; + qWarning() << "PackProfile got dataChenged signal from a Component which does not belong to it!"; } -bool ComponentList::remove(const int index) +bool PackProfile::remove(const int index) { auto patch = getComponent(index); if (!patch->isRemovable()) @@ -674,7 +679,7 @@ bool ComponentList::remove(const int index) return true; } -bool ComponentList::remove(const QString id) +bool PackProfile::remove(const QString id) { int i = 0; for (auto patch : d->components) @@ -688,7 +693,7 @@ bool ComponentList::remove(const QString id) return false; } -bool ComponentList::customize(int index) +bool PackProfile::customize(int index) { auto patch = getComponent(index); if (!patch->isCustomizable()) @@ -706,7 +711,7 @@ bool ComponentList::customize(int index) return true; } -bool ComponentList::revertToBase(int index) +bool PackProfile::revertToBase(int index) { auto patch = getComponent(index); if (!patch->isRevertible()) @@ -724,7 +729,7 @@ bool ComponentList::revertToBase(int index) return true; } -Component * ComponentList::getComponent(const QString &id) +Component * PackProfile::getComponent(const QString &id) { auto iter = d->componentIndex.find(id); if (iter == d->componentIndex.end()) @@ -734,7 +739,7 @@ Component * ComponentList::getComponent(const QString &id) return (*iter).get(); } -Component * ComponentList::getComponent(int index) +Component * PackProfile::getComponent(int index) { if(index < 0 || index >= d->components.size()) { @@ -743,7 +748,7 @@ Component * ComponentList::getComponent(int index) return d->components[index].get(); } -QVariant ComponentList::data(const QModelIndex &index, int role) const +QVariant PackProfile::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); @@ -762,8 +767,9 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const { switch (column) { - case NameColumn: - return d->components.at(row)->isEnabled() ? Qt::Checked : Qt::Unchecked; + case NameColumn: { + return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; + } default: return QVariant(); } @@ -773,7 +779,7 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const switch (column) { case NameColumn: - return d->components.at(row)->getName(); + return patch->getName(); case VersionColumn: { if(patch->isCustom()) @@ -816,7 +822,7 @@ QVariant ComponentList::data(const QModelIndex &index, int role) const return QVariant(); } -bool ComponentList::setData(const QModelIndex& index, const QVariant& value, int role) +bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role) { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index)) { @@ -834,7 +840,7 @@ bool ComponentList::setData(const QModelIndex& index, const QVariant& value, int return false; } -QVariant ComponentList::headerData(int section, Qt::Orientation orientation, int role) const +QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { @@ -853,38 +859,42 @@ QVariant ComponentList::headerData(int section, Qt::Orientation orientation, int } return QVariant(); } -Qt::ItemFlags ComponentList::flags(const QModelIndex &index) const + +// FIXME: zero precision mess +Qt::ItemFlags PackProfile::flags(const QModelIndex &index) const { - if (!index.isValid()) + if (!index.isValid()) { return Qt::NoItemFlags; + } Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; int row = index.row(); - if (row < 0 || row >= d->components.size()) + if (row < 0 || row >= d->components.size()) { return Qt::NoItemFlags; + } auto patch = d->components.at(row); // TODO: this will need fine-tuning later... - if(patch->canBeDisabled()) + if(patch->canBeDisabled() && !d->interactionDisabled) { outFlags |= Qt::ItemIsUserCheckable; } return outFlags; } -int ComponentList::rowCount(const QModelIndex &parent) const +int PackProfile::rowCount(const QModelIndex &parent) const { return d->components.size(); } -int ComponentList::columnCount(const QModelIndex &parent) const +int PackProfile::columnCount(const QModelIndex &parent) const { return NUM_COLUMNS; } -void ComponentList::move(const int index, const MoveDirection direction) +void PackProfile::move(const int index, const MoveDirection direction) { int theirIndex; if (direction == MoveUp) @@ -920,22 +930,22 @@ void ComponentList::move(const int index, const MoveDirection direction) scheduleSave(); } -void ComponentList::invalidateLaunchProfile() +void PackProfile::invalidateLaunchProfile() { d->m_profile.reset(); } -void ComponentList::installJarMods(QStringList selectedFiles) +void PackProfile::installJarMods(QStringList selectedFiles) { installJarMods_internal(selectedFiles); } -void ComponentList::installCustomJar(QString selectedFile) +void PackProfile::installCustomJar(QString selectedFile) { installCustomJar_internal(selectedFile); } -bool ComponentList::installEmpty(const QString& uid, const QString& name) +bool PackProfile::installEmpty(const QString& uid, const QString& name) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) @@ -963,7 +973,7 @@ bool ComponentList::installEmpty(const QString& uid, const QString& name) return true; } -bool ComponentList::removeComponent_internal(ComponentPtr patch) +bool PackProfile::removeComponent_internal(ComponentPtr patch) { bool ok = true; // first, remove the patch file. this ensures it's not used anymore @@ -1013,7 +1023,7 @@ bool ComponentList::removeComponent_internal(ComponentPtr patch) return ok; } -bool ComponentList::installJarMods_internal(QStringList filepaths) +bool PackProfile::installJarMods_internal(QStringList filepaths) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) @@ -1075,7 +1085,7 @@ bool ComponentList::installJarMods_internal(QStringList filepaths) return true; } -bool ComponentList::installCustomJar_internal(QString filepath) +bool PackProfile::installCustomJar_internal(QString filepath) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if(!FS::ensureFolderPathExists(patchDir)) @@ -1136,7 +1146,7 @@ bool ComponentList::installCustomJar_internal(QString filepath) return true; } -std::shared_ptr<LaunchProfile> ComponentList::getProfile() const +std::shared_ptr<LaunchProfile> PackProfile::getProfile() const { if(!d->m_profile) { @@ -1158,7 +1168,7 @@ std::shared_ptr<LaunchProfile> ComponentList::getProfile() const return d->m_profile; } -void ComponentList::setOldConfigVersion(const QString& uid, const QString& version) +void PackProfile::setOldConfigVersion(const QString& uid, const QString& version) { if(version.isEmpty()) { @@ -1167,7 +1177,7 @@ void ComponentList::setOldConfigVersion(const QString& uid, const QString& versi d->m_oldConfigVersions[uid] = version; } -bool ComponentList::setComponentVersion(const QString& uid, const QString& version, bool important) +bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important) { auto iter = d->componentIndex.find(uid); if(iter != d->componentIndex.end()) @@ -1193,7 +1203,7 @@ bool ComponentList::setComponentVersion(const QString& uid, const QString& versi } } -QString ComponentList::getComponentVersion(const QString& uid) const +QString PackProfile::getComponentVersion(const QString& uid) const { const auto iter = d->componentIndex.find(uid); if (iter != d->componentIndex.end()) @@ -1202,3 +1212,14 @@ QString ComponentList::getComponentVersion(const QString& uid) const } return QString(); } + +void PackProfile::disableInteraction(bool disable) +{ + if(d->interactionDisabled != disable) { + d->interactionDisabled = disable; + auto size = d->components.size(); + if(size) { + emit dataChanged(index(0), index(size - 1)); + } + } +} diff --git a/api/logic/minecraft/ComponentList.h b/api/logic/minecraft/PackProfile.h index 6ddc09eb..e55e6a58 100644 --- a/api/logic/minecraft/ComponentList.h +++ b/api/logic/minecraft/PackProfile.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -31,10 +31,10 @@ #include "net/Mode.h" class MinecraftInstance; -struct ComponentListData; +struct PackProfileData; class ComponentUpdateTask; -class MULTIMC_LOGIC_EXPORT ComponentList : public QAbstractListModel +class MULTIMC_LOGIC_EXPORT PackProfile : public QAbstractListModel { Q_OBJECT friend ComponentUpdateTask; @@ -46,8 +46,8 @@ public: NUM_COLUMNS }; - explicit ComponentList(MinecraftInstance * instance); - virtual ~ComponentList(); + explicit PackProfile(MinecraftInstance * instance); + virtual ~PackProfile(); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; @@ -104,6 +104,9 @@ public: /// if there is a save scheduled, do it now. void saveNow(); +signals: + void minecraftChanged(); + public: /// get the profile component by id Component * getComponent(const QString &id); @@ -111,6 +114,10 @@ public: /// get the profile component by index Component * getComponent(int index); + /// Add the component to the internal list of patches + // todo(merged): is this the best approach + void appendComponent(ComponentPtr component); + private: void scheduleSave(); bool saveIsScheduled() const; @@ -118,8 +125,6 @@ private: /// apply the component patches. Catches all the errors and returns true/false for success/failure void invalidateLaunchProfile(); - /// Add the component to the internal list of patches - void appendComponent(ComponentPtr component); /// insert component so that its index is ideally the specified one (returns real index) void insertComponent(size_t index, ComponentPtr component); @@ -131,6 +136,7 @@ private slots: void updateSucceeded(); void updateFailed(const QString & error); void componentDataChanged(); + void disableInteraction(bool disable); private: bool load(); @@ -142,5 +148,5 @@ private: private: /* data */ - std::unique_ptr<ComponentListData> d; + std::unique_ptr<PackProfileData> d; }; diff --git a/api/logic/minecraft/ComponentList_p.h b/api/logic/minecraft/PackProfile_p.h index aed65337..6cd2a4e5 100644 --- a/api/logic/minecraft/ComponentList_p.h +++ b/api/logic/minecraft/PackProfile_p.h @@ -9,9 +9,8 @@ class MinecraftInstance; using ComponentContainer = QList<ComponentPtr>; using ComponentIndex = QMap<QString, ComponentPtr>; -using ConnectionList = QList<QMetaObject::Connection>; -struct ComponentListData +struct PackProfileData { // the instance this belongs to MinecraftInstance *m_instance; @@ -38,5 +37,6 @@ struct ComponentListData QTimer m_saveTimer; shared_qobject_ptr<Task> m_updateTask; bool loaded = false; + bool interactionDisabled = true; }; diff --git a/api/logic/minecraft/ParseUtils_test.cpp b/api/logic/minecraft/ParseUtils_test.cpp index fde9cdbf..fcc137e5 100644 --- a/api/logic/minecraft/ParseUtils_test.cpp +++ b/api/logic/minecraft/ParseUtils_test.cpp @@ -33,7 +33,7 @@ slots: auto time_parsed = timeFromS3Time(timestamp); auto time_serialized = timeToS3Time(time_parsed); - + QCOMPARE(time_serialized, timestamp); } diff --git a/api/logic/minecraft/Rule.cpp b/api/logic/minecraft/Rule.cpp index a7640635..af2861e3 100644 --- a/api/logic/minecraft/Rule.cpp +++ b/api/logic/minecraft/Rule.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/Rule.h b/api/logic/minecraft/Rule.h index ec9b26bd..7aa34d96 100644 --- a/api/logic/minecraft/Rule.h +++ b/api/logic/minecraft/Rule.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/SimpleModList.cpp b/api/logic/minecraft/SimpleModList.cpp deleted file mode 100644 index a9fb42eb..00000000 --- a/api/logic/minecraft/SimpleModList.cpp +++ /dev/null @@ -1,367 +0,0 @@ -/* Copyright 2013-2018 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. - */ - -#include "SimpleModList.h" -#include <FileSystem.h> -#include <QMimeData> -#include <QUrl> -#include <QUuid> -#include <QString> -#include <QFileSystemWatcher> -#include <QDebug> - -SimpleModList::SimpleModList(const QString &dir) : QAbstractListModel(), m_dir(dir) -{ - FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | - QDir::NoSymLinks); - m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); - m_watcher = new QFileSystemWatcher(this); - connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); -} - -void SimpleModList::startWatching() -{ - if(is_watching) - return; - - update(); - - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) - { - qDebug() << "Started watching " << m_dir.absolutePath(); - } - else - { - qDebug() << "Failed to start watching " << m_dir.absolutePath(); - } -} - -void SimpleModList::stopWatching() -{ - if(!is_watching) - return; - - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) - { - qDebug() << "Stopped watching " << m_dir.absolutePath(); - } - else - { - qDebug() << "Failed to stop watching " << m_dir.absolutePath(); - } -} - -bool SimpleModList::update() -{ - if (!isValid()) - return false; - - QList<Mod> orderedMods; - QList<Mod> newMods; - m_dir.refresh(); - auto folderContents = m_dir.entryInfoList(); - bool orderOrStateChanged = false; - - // if there are any untracked files... - if (folderContents.size()) - { - // the order surely changed! - for (auto entry : folderContents) - { - newMods.append(Mod(entry)); - } - orderedMods.append(newMods); - orderOrStateChanged = true; - } - // otherwise, if we were already tracking some mods - else if (mods.size()) - { - // if the number doesn't match, order changed. - if (mods.size() != orderedMods.size()) - orderOrStateChanged = true; - // if it does match, compare the mods themselves - else - for (int i = 0; i < mods.size(); i++) - { - if (!mods[i].strongCompare(orderedMods[i])) - { - orderOrStateChanged = true; - break; - } - } - } - beginResetModel(); - mods.swap(orderedMods); - endResetModel(); - if (orderOrStateChanged) - { - emit changed(); - } - return true; -} - -void SimpleModList::directoryChanged(QString path) -{ - update(); -} - -bool SimpleModList::isValid() -{ - return m_dir.exists() && m_dir.isReadable(); -} - -bool SimpleModList::installMod(const QString &filename) -{ - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName - QFileInfo fileinfo(FS::NormalizePath(filename)); - - qDebug() << "installing: " << fileinfo.absoluteFilePath(); - - if (!fileinfo.exists() || !fileinfo.isReadable()) - { - return false; - } - Mod m(fileinfo); - if (!m.valid()) - return false; - - auto type = m.type(); - if (type == Mod::MOD_UNKNOWN) - return false; - if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) - { - QString newpath = FS::PathCombine(m_dir.path(), fileinfo.fileName()); - if (!QFile::copy(fileinfo.filePath(), newpath)) - return false; - FS::updateTimestamp(newpath); - m.repath(newpath); - update(); - return true; - } - else if (type == Mod::MOD_FOLDER) - { - QString from = fileinfo.filePath(); - QString to = FS::PathCombine(m_dir.path(), fileinfo.fileName()); - if (!FS::copy(from, to)()) - return false; - m.repath(to); - update(); - return true; - } - return false; -} - -bool SimpleModList::enableMods(const QModelIndexList& indexes, bool enable) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.enable(enable); - emit dataChanged(i, i); - } - emit changed(); - return true; -} - -bool SimpleModList::deleteMods(const QModelIndexList& indexes) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.destroy(); - } - emit changed(); - return true; -} - -int SimpleModList::columnCount(const QModelIndex &parent) const -{ - return NUM_COLUMNS; -} - -QVariant SimpleModList::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - int row = index.row(); - int column = index.column(); - - if (row < 0 || row >= mods.size()) - return QVariant(); - - switch (role) - { - case Qt::DisplayRole: - switch (column) - { - case NameColumn: - return mods[row].name(); - case VersionColumn: - return mods[row].version(); - case DateColumn: - return mods[row].dateTimeChanged(); - - default: - return QVariant(); - } - - case Qt::ToolTipRole: - return mods[row].mmc_id(); - - case Qt::CheckStateRole: - switch (column) - { - case ActiveColumn: - return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; - default: - return QVariant(); - } - default: - return QVariant(); - } -} - -bool SimpleModList::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) - { - return false; - } - - if (role == Qt::CheckStateRole) - { - auto &mod = mods[index.row()]; - if (mod.enable(!mod.enabled())) - { - emit dataChanged(index, index); - return true; - } - } - return false; -} - -QVariant SimpleModList::headerData(int section, Qt::Orientation orientation, int role) const -{ - switch (role) - { - case Qt::DisplayRole: - switch (section) - { - case ActiveColumn: - return QString(); - case NameColumn: - return tr("Name"); - case VersionColumn: - return tr("Version"); - case DateColumn: - return tr("Last changed"); - default: - return QVariant(); - } - - case Qt::ToolTipRole: - switch (section) - { - case ActiveColumn: - return tr("Is the mod enabled?"); - case NameColumn: - return tr("The name of the mod."); - case VersionColumn: - return tr("The version of the mod."); - case DateColumn: - return tr("The date and time this mod was last changed (or added)."); - default: - return QVariant(); - } - default: - return QVariant(); - } - return QVariant(); -} - -Qt::ItemFlags SimpleModList::flags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - if (index.isValid()) - return Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled | - defaultFlags; - else - return Qt::ItemIsDropEnabled | defaultFlags; -} - -Qt::DropActions SimpleModList::supportedDropActions() const -{ - // copy from outside, move from within and other mod lists - return Qt::CopyAction | Qt::MoveAction; -} - -QStringList SimpleModList::mimeTypes() const -{ - QStringList types; - types << "text/uri-list"; - return types; -} - -bool SimpleModList::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) -{ - if (action == Qt::IgnoreAction) - { - return true; - } - - // check if the action is supported - if (!data || !(action & supportedDropActions())) - { - return false; - } - - // files dropped from outside? - if (data->hasUrls()) - { - bool was_watching = is_watching; - if (was_watching) - { - stopWatching(); - } - auto urls = data->urls(); - for (auto url : urls) - { - // only local files may be dropped... - if (!url.isLocalFile()) - { - continue; - } - // TODO: implement not only copy, but also move - // FIXME: handle errors here - installMod(url.toLocalFile()); - } - if (was_watching) - { - startWatching(); - } - return true; - } - return false; -} diff --git a/api/logic/minecraft/VersionFile.cpp b/api/logic/minecraft/VersionFile.cpp index cfb9e504..d0a1a507 100644 --- a/api/logic/minecraft/VersionFile.cpp +++ b/api/logic/minecraft/VersionFile.cpp @@ -5,7 +5,7 @@ #include "minecraft/VersionFile.h" #include "minecraft/Library.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "ParseUtils.h" #include <Version.h> @@ -41,6 +41,10 @@ void VersionFile::applyTo(LaunchProfile *profile) { profile->applyLibrary(library); } + for (auto mavenFile : mavenFiles) + { + profile->applyMavenFile(mavenFile); + } profile->applyProblemSeverity(getProblemSeverity()); } @@ -53,4 +57,4 @@ void VersionFile::applyTo(LaunchProfile *profile) throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion); } } -*/
\ No newline at end of file +*/ diff --git a/api/logic/minecraft/VersionFile.h b/api/logic/minecraft/VersionFile.h index 3dc9db96..b79fcd4f 100644 --- a/api/logic/minecraft/VersionFile.h +++ b/api/logic/minecraft/VersionFile.h @@ -12,7 +12,7 @@ #include "Library.h" #include <meta/JsonFormat.h> -class ComponentList; +class PackProfile; class VersionFile; class LaunchProfile; struct MojangDownloadInfo; @@ -75,6 +75,9 @@ public: /* data */ /// Mojang: list of libraries to add to the version QList<LibraryPtr> libraries; + /// MultiMC: list of maven files to put in the libraries folder, but not in classpath + QList<LibraryPtr> mavenFiles; + /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; diff --git a/api/logic/minecraft/VersionFilterData.cpp b/api/logic/minecraft/VersionFilterData.cpp index 11f7eea9..38e7b60c 100644 --- a/api/logic/minecraft/VersionFilterData.cpp +++ b/api/logic/minecraft/VersionFilterData.cpp @@ -7,18 +7,18 @@ VersionFilterData::VersionFilterData() { // 1.3.* auto libs13 = - QList<FMLlib>{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, - {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, - {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}}; + QList<FMLlib>{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}}; fmlLibsMapping["1.3.2"] = libs13; // 1.4.* auto libs14 = QList<FMLlib>{ - {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, - {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, - {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}, - {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", false}}; + {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}, + {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb"}}; fmlLibsMapping["1.4"] = libs14; fmlLibsMapping["1.4.1"] = libs14; @@ -31,30 +31,30 @@ VersionFilterData::VersionFilterData() // 1.5 fmlLibsMapping["1.5"] = QList<FMLlib>{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // 1.5.1 fmlLibsMapping["1.5.1"] = QList<FMLlib>{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // 1.5.2 fmlLibsMapping["1.5.2"] = QList<FMLlib>{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // don't use installers for those. forgeInstallerBlacklist = QSet<QString>({"1.5.2"}); @@ -65,4 +65,7 @@ VersionFilterData::VersionFilterData() QSet<QString>{"net.java.jinput:jinput", "net.java.jinput:jinput-platform", "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl", "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform"}; + + java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); + java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); } diff --git a/api/logic/minecraft/VersionFilterData.h b/api/logic/minecraft/VersionFilterData.h index 88e91f11..d100acc3 100644 --- a/api/logic/minecraft/VersionFilterData.h +++ b/api/logic/minecraft/VersionFilterData.h @@ -10,7 +10,6 @@ struct FMLlib { QString filename; QString checksum; - bool ours; }; struct VersionFilterData @@ -24,5 +23,9 @@ struct VersionFilterData QDateTime legacyCutoffDate; // Libraries that belong to LWJGL QSet<QString> lwjglWhitelist; + // release date of first version to require Java 8 (17w13a) + QDateTime java8BeginsDate; + // release data of first version to require Java 16 (21w19a) + QDateTime java16BeginsDate; }; extern VersionFilterData MULTIMC_LOGIC_EXPORT g_VersionFilterData; diff --git a/api/logic/minecraft/World.cpp b/api/logic/minecraft/World.cpp index b39f940e..a2b4dac7 100644 --- a/api/logic/minecraft/World.cpp +++ b/api/logic/minecraft/World.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -30,6 +30,79 @@ #include <quazipfile.h> #include <quazipdir.h> +#include <QCoreApplication> + +#include <nonstd/optional> + +using nonstd::optional; +using nonstd::nullopt; + +GameType::GameType(nonstd::optional<int> original): + original(original) +{ + if(!original) { + return; + } + switch(*original) { + case 0: + type = GameType::Survival; + break; + case 1: + type = GameType::Creative; + break; + case 2: + type = GameType::Adventure; + break; + case 3: + type = GameType::Spectator; + break; + default: + break; + } +} + +QString GameType::toTranslatedString() const +{ + switch (type) + { + case GameType::Survival: + return QCoreApplication::translate("GameType", "Survival"); + case GameType::Creative: + return QCoreApplication::translate("GameType", "Creative"); + case GameType::Adventure: + return QCoreApplication::translate("GameType", "Adventure"); + case GameType::Spectator: + return QCoreApplication::translate("GameType", "Spectator"); + default: + break; + } + if(original) { + return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original); + } + return QCoreApplication::translate("GameType", "Undefined"); +} + +QString GameType::toLogString() const +{ + switch (type) + { + case GameType::Survival: + return "Survival"; + case GameType::Creative: + return "Creative"; + case GameType::Adventure: + return "Adventure"; + case GameType::Spectator: + return "Spectator"; + default: + break; + } + if(original) { + return QString("Unknown (%1)").arg(*original); + } + return "Undefined"; +} + std::unique_ptr <nbt::tag_compound> parseLevelDat(QByteArray data) { QByteArray output; @@ -38,15 +111,22 @@ std::unique_ptr <nbt::tag_compound> parseLevelDat(QByteArray data) return nullptr; } std::istringstream foo(std::string(output.constData(), output.size())); - auto pair = nbt::io::read_compound(foo); + try { + auto pair = nbt::io::read_compound(foo); - if(pair.first != "") - return nullptr; + if(pair.first != "") + return nullptr; - if(pair.second == nullptr) - return nullptr; + if(pair.second == nullptr) + return nullptr; - return std::move(pair.second); + return std::move(pair.second); + } + catch (const nbt::io::input_error &e) + { + qWarning() << "Unable to parse level.dat:" << e.what(); + return nullptr; + } } QByteArray serializeLevelDat(nbt::tag_compound * levelInfo) @@ -118,14 +198,31 @@ void World::repath(const QFileInfo &file) m_folderName = file.fileName(); if(file.isFile() && file.suffix() == "zip") { + m_iconFile = QString(); readFromZip(file); } else if(file.isDir()) { + QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); + if(assumedIconPath.exists()) { + m_iconFile = assumedIconPath.absoluteFilePath(); + } readFromFS(file); } } +bool World::resetIcon() +{ + if(m_iconFile.isNull()) { + return false; + } + if(QFile(m_iconFile).remove()) { + m_iconFile = QString(); + return true; + } + return false; +} + void World::readFromFS(const QFileInfo &file) { auto bytes = getLevelDatDataFromFS(file); @@ -192,7 +289,7 @@ bool World::install(const QString &to, const QString &name) { return false; } - ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath).isEmpty(); + ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); } else if(m_containerFile.isDir()) { @@ -251,14 +348,16 @@ bool World::rename(const QString &newName) return true; } -static QString read_string (nbt::value& parent, const char * name, const QString & fallback = QString()) +namespace { + +optional<QString> read_string (nbt::value& parent, const char * name) { try { auto &namedValue = parent.at(name); if(namedValue.get_type() != nbt::tag_type::String) { - return fallback; + return nullopt; } auto & tag_str = namedValue.as<nbt::tag_string>(); return QString::fromStdString(tag_str.get()); @@ -266,25 +365,25 @@ static QString read_string (nbt::value& parent, const char * name, const QString catch (const std::out_of_range &e) { // fallback for old world formats - qWarning() << "String NBT tag" << name << "could not be found. Defaulting to" << fallback; - return fallback; + qWarning() << "String NBT tag" << name << "could not be found."; + return nullopt; } catch (const std::bad_cast &e) { // type mismatch - qWarning() << "NBT tag" << name << "could not be converted to string. Defaulting to" << fallback; - return fallback; + qWarning() << "NBT tag" << name << "could not be converted to string."; + return nullopt; } } -static int64_t read_long (nbt::value& parent, const char * name, const int64_t & fallback = 0) +optional<int64_t> read_long (nbt::value& parent, const char * name) { try { auto &namedValue = parent.at(name); if(namedValue.get_type() != nbt::tag_type::Long) { - return fallback; + return nullopt; } auto & tag_str = namedValue.as<nbt::tag_long>(); return tag_str.get(); @@ -292,58 +391,98 @@ static int64_t read_long (nbt::value& parent, const char * name, const int64_t & catch (const std::out_of_range &e) { // fallback for old world formats - qWarning() << "Long NBT tag" << name << "could not be found. Defaulting to" << fallback; - return fallback; + qWarning() << "Long NBT tag" << name << "could not be found."; + return nullopt; } catch (const std::bad_cast &e) { // type mismatch - qWarning() << "NBT tag" << name << "could not be converted to long. Defaulting to" << fallback; - return fallback; + qWarning() << "NBT tag" << name << "could not be converted to long."; + return nullopt; } } -void World::loadFromLevelDat(QByteArray data) +optional<int> read_int (nbt::value& parent, const char * name) { try { - auto levelData = parseLevelDat(data); - if(!levelData) + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::Int) { - is_valid = false; - return; + return nullopt; } + auto & tag_str = namedValue.as<nbt::tag_int>(); + return tag_str.get(); + } + catch (const std::out_of_range &e) + { + // fallback for old world formats + qWarning() << "Int NBT tag" << name << "could not be found."; + return nullopt; + } + catch (const std::bad_cast &e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to int."; + return nullopt; + } +} - auto &val = levelData->at("Data"); - is_valid = val.get_type() == nbt::tag_type::Compound; - if(!is_valid) - return; - - m_actualName = read_string(val, "LevelName", m_folderName); - +GameType read_gametype(nbt::value& parent, const char * name) { + return GameType(read_int(parent, name)); +} - int64_t temp = read_long(val, "LastPlayed", 0); - if(temp == 0) - { - m_lastPlayed = levelDatTime; - } - else - { - m_lastPlayed = QDateTime::fromMSecsSinceEpoch(temp); - } +} - m_randomSeed = read_long(val, "RandomSeed", 0); +void World::loadFromLevelDat(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if(!levelData) + { + is_valid = false; + return; + } - qDebug() << "World Name:" << m_actualName; - qDebug() << "Last Played:" << m_lastPlayed.toString(); - qDebug() << "Seed:" << m_randomSeed; + nbt::value * valPtr = nullptr; + try { + valPtr = &levelData->at("Data"); } - catch (const nbt::io::input_error &e) - { - qWarning() << "Unable to load" << m_folderName << ":" << e.what(); + catch (const std::out_of_range &e) { + qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); is_valid = false; return; } + nbt::value &val = *valPtr; + + is_valid = val.get_type() == nbt::tag_type::Compound; + if(!is_valid) + return; + + auto name = read_string(val, "LevelName"); + m_actualName = name ? *name : m_folderName; + + auto timestamp = read_long(val, "LastPlayed"); + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime; + + m_gameType = read_gametype(val, "GameType"); + + optional<int64_t> randomSeed; + try { + auto &WorldGen_val = val.at("WorldGenSettings"); + randomSeed = read_long(WorldGen_val, "seed"); + } + catch (const std::out_of_range &) {} + if(!randomSeed) { + randomSeed = read_long(val, "RandomSeed"); + } + m_randomSeed = randomSeed ? *randomSeed : 0; + + qDebug() << "World Name:" << m_actualName; + qDebug() << "Last Played:" << m_lastPlayed.toString(); + if(randomSeed) { + qDebug() << "Seed:" << *randomSeed; + } + qDebug() << "GameType:" << m_gameType.toLogString(); } bool World::replace(World &with) @@ -379,7 +518,3 @@ bool World::operator==(const World &other) const { return is_valid == other.is_valid && folderName() == other.folderName(); } -bool World::strongCompare(const World &other) const -{ - return is_valid == other.is_valid && folderName() == other.folderName(); -} diff --git a/api/logic/minecraft/World.h b/api/logic/minecraft/World.h index 2cce85a2..1d94d54d 100644 --- a/api/logic/minecraft/World.h +++ b/api/logic/minecraft/World.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -16,9 +16,28 @@ #pragma once #include <QFileInfo> #include <QDateTime> +#include <nonstd/optional> #include "multimc_logic_export.h" +struct MULTIMC_LOGIC_EXPORT GameType { + GameType() = default; + GameType (nonstd::optional<int> original); + + QString toTranslatedString() const; + QString toLogString() const; + + enum + { + Unknown = -1, + Survival = 0, + Creative, + Adventure, + Spectator + } type = Unknown; + nonstd::optional<int> original; +}; + class MULTIMC_LOGIC_EXPORT World { public: @@ -31,10 +50,18 @@ public: { return m_actualName; } + QString iconFile() const + { + return m_iconFile; + } QDateTime lastPlayed() const { return m_lastPlayed; } + GameType gameType() const + { + return m_gameType; + } int64_t seed() const { return m_randomSeed; @@ -57,13 +84,14 @@ public: bool replace(World &with); // change the world's filesystem path (used by world lists for *MAGIC* purposes) void repath(const QFileInfo &file); + // remove the icon file, if any + bool resetIcon(); bool rename(const QString &to); bool install(const QString &to, const QString &name= QString()); // WEAK compare operator - used for replacing worlds bool operator==(const World &other) const; - bool strongCompare(const World &other) const; private: void readFromZip(const QFileInfo &file); @@ -76,8 +104,10 @@ protected: QString m_containerOffsetPath; QString m_folderName; QString m_actualName; + QString m_iconFile; QDateTime levelDatTime; QDateTime m_lastPlayed; int64_t m_randomSeed = 0; + GameType m_gameType; bool is_valid = false; }; diff --git a/api/logic/minecraft/WorldList.cpp b/api/logic/minecraft/WorldList.cpp index 79a5bf38..f6309dbd 100644 --- a/api/logic/minecraft/WorldList.cpp +++ b/api/logic/minecraft/WorldList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -136,9 +136,22 @@ bool WorldList::deleteWorlds(int first, int last) return true; } +bool WorldList::resetIcon(int row) +{ + if (row >= worlds.size() || row < 0) + return false; + World &m = worlds[row]; + if(m.resetIcon()) { + emit dataChanged(index(row), index(row), {WorldList::IconFileRole}); + return true; + } + return false; +} + + int WorldList::columnCount(const QModelIndex &parent) const { - return 2; + return 3; } QVariant WorldList::data(const QModelIndex &index, int role) const @@ -161,6 +174,9 @@ QVariant WorldList::data(const QModelIndex &index, int role) const case NameColumn: return world.name(); + case GameModeColumn: + return world.gameType().toTranslatedString(); + case LastPlayedColumn: return world.lastPlayed(); @@ -192,6 +208,10 @@ QVariant WorldList::data(const QModelIndex &index, int role) const { return world.lastPlayed(); } + case IconFileRole: + { + return world.iconFile(); + } default: return QVariant(); } @@ -206,6 +226,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol { case NameColumn: return tr("Name"); + case GameModeColumn: + return tr("Game Mode"); case LastPlayedColumn: return tr("Last Played"); default: @@ -217,6 +239,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol { case NameColumn: return tr("The name of the world."); + case GameModeColumn: + return tr("Game mode of the world."); case LastPlayedColumn: return tr("Date and time the world was last played."); default: diff --git a/api/logic/minecraft/WorldList.h b/api/logic/minecraft/WorldList.h index a1cd8f51..740b1461 100644 --- a/api/logic/minecraft/WorldList.h +++ b/api/logic/minecraft/WorldList.h @@ -1,4 +1,4 @@ -/* Copyright 2015-2018 MultiMC Contributors +/* Copyright 2015-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. @@ -33,6 +33,7 @@ public: enum Columns { NameColumn, + GameModeColumn, LastPlayedColumn }; @@ -42,7 +43,9 @@ public: FolderRole, SeedRole, NameRole, - LastPlayedRole + GameModeRole, + LastPlayedRole, + IconFileRole }; WorldList(const QString &dir); @@ -79,6 +82,9 @@ public: /// Deletes the mod at the given index. virtual bool deleteWorld(int index); + /// Removes the world icon, if any + virtual bool resetIcon(int index); + /// Deletes all the selected mods virtual bool deleteWorlds(int first, int last); diff --git a/api/logic/minecraft/auth/MojangAccount.cpp b/api/logic/minecraft/auth/MojangAccount.cpp index edf1e4e3..f5853fe3 100644 --- a/api/logic/minecraft/auth/MojangAccount.cpp +++ b/api/logic/minecraft/auth/MojangAccount.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan <orochimarufan.x3@gmail.com> * diff --git a/api/logic/minecraft/auth/MojangAccount.h b/api/logic/minecraft/auth/MojangAccount.h index 8f9bec95..30a5f2ff 100644 --- a/api/logic/minecraft/auth/MojangAccount.h +++ b/api/logic/minecraft/auth/MojangAccount.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/MojangAccountList.cpp b/api/logic/minecraft/auth/MojangAccountList.cpp index de671add..e584cb3b 100644 --- a/api/logic/minecraft/auth/MojangAccountList.cpp +++ b/api/logic/minecraft/auth/MojangAccountList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/MojangAccountList.h b/api/logic/minecraft/auth/MojangAccountList.h index 33018636..cc3a61a2 100644 --- a/api/logic/minecraft/auth/MojangAccountList.h +++ b/api/logic/minecraft/auth/MojangAccountList.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/YggdrasilTask.cpp b/api/logic/minecraft/auth/YggdrasilTask.cpp index 666e57d6..0857b46b 100644 --- a/api/logic/minecraft/auth/YggdrasilTask.cpp +++ b/api/logic/minecraft/auth/YggdrasilTask.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -25,7 +25,7 @@ #include <Env.h> -#include <net/URLConstants.h> +#include <BuildConfig.h> #include <QDebug> @@ -42,7 +42,7 @@ void YggdrasilTask::executeTask() // Get the content of the request we're going to send to the server. QJsonDocument doc(getRequestContent()); - QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint()); + QUrl reqUrl(BuildConfig.AUTH_BASE + getEndpoint()); QNetworkRequest netRequest(reqUrl); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -122,7 +122,7 @@ void YggdrasilTask::processReply() tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>" "<ul>" "<li>You use Windows XP and need to <a " - "href=\"http://www.microsoft.com/en-us/download/details.aspx?id=38918\">update " + "href=\"https://www.microsoft.com/en-us/download/details.aspx?id=38918\">update " "your root certificates</a></li>" "<li>Some device on your network is interfering with SSL traffic. In that case, " "you have bigger worries than Minecraft not starting.</li>" diff --git a/api/logic/minecraft/auth/YggdrasilTask.h b/api/logic/minecraft/auth/YggdrasilTask.h index 4e302195..8af2e132 100644 --- a/api/logic/minecraft/auth/YggdrasilTask.h +++ b/api/logic/minecraft/auth/YggdrasilTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.cpp b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp index 5e37f0ca..2e8dc859 100644 --- a/api/logic/minecraft/auth/flows/AuthenticateTask.cpp +++ b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp @@ -1,5 +1,5 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.h b/api/logic/minecraft/auth/flows/AuthenticateTask.h index 71f0eab7..4c14eec7 100644 --- a/api/logic/minecraft/auth/flows/AuthenticateTask.h +++ b/api/logic/minecraft/auth/flows/AuthenticateTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/RefreshTask.cpp b/api/logic/minecraft/auth/flows/RefreshTask.cpp index f6fe4757..ecba178d 100644 --- a/api/logic/minecraft/auth/flows/RefreshTask.cpp +++ b/api/logic/minecraft/auth/flows/RefreshTask.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/RefreshTask.h b/api/logic/minecraft/auth/flows/RefreshTask.h index ff586396..f0840dda 100644 --- a/api/logic/minecraft/auth/flows/RefreshTask.h +++ b/api/logic/minecraft/auth/flows/RefreshTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/ValidateTask.cpp b/api/logic/minecraft/auth/flows/ValidateTask.cpp index e82678c8..6b3f0a65 100644 --- a/api/logic/minecraft/auth/flows/ValidateTask.cpp +++ b/api/logic/minecraft/auth/flows/ValidateTask.cpp @@ -1,5 +1,5 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/auth/flows/ValidateTask.h b/api/logic/minecraft/auth/flows/ValidateTask.h index 87876fb8..986c2e9f 100644 --- a/api/logic/minecraft/auth/flows/ValidateTask.h +++ b/api/logic/minecraft/auth/flows/ValidateTask.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/forge/ForgeXzDownload.cpp b/api/logic/minecraft/forge/ForgeXzDownload.cpp deleted file mode 100644 index 4083bdea..00000000 --- a/api/logic/minecraft/forge/ForgeXzDownload.cpp +++ /dev/null @@ -1,393 +0,0 @@ -/* Copyright 2013-2018 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. - */ - -#include "Env.h" -#include "ForgeXzDownload.h" -#include <FileSystem.h> - -#include <QCryptographicHash> -#include <QFileInfo> -#include <QDateTime> -#include <QDir> -#include <QDebug> - -ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction() -{ - m_entry = entry; - m_target_path = entry->getFullPath(); - m_pack200_xz_file.setFileTemplate("./dl_temp.XXXXXX"); - m_status = Job_NotStarted; - m_url_path = relative_path; - m_url = "http://files.minecraftforge.net/maven/" + m_url_path + ".pack.xz"; -} - -void ForgeXzDownload::start() -{ - if(m_status == Job_Aborted) - { - qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); - emit aborted(m_index_within_job); - return; - } - m_status = Job_InProgress; - if (!m_entry->isStale()) - { - m_status = Job_Finished; - emit succeeded(m_index_within_job); - return; - } - // can we actually create the real, final file? - if (!FS::ensureFilePathExists(m_target_path)) - { - m_status = Job_Failed; - emit failed(m_index_within_job); - return; - } - - qDebug() << "Downloading " << m_url.toString(); - QNetworkRequest request(m_url); - request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)"); - - QNetworkReply *rep = ENV.qnam().get(request); - - m_reply.reset(rep); - connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); - connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); - connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); -} - -void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - m_total_progress = bytesTotal; - m_progress = bytesReceived; - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); -} - -void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error) -{ - if(error == QNetworkReply::OperationCanceledError) - { - qCritical() << "Aborted " << m_url.toString(); - m_status = Job_Aborted; - } - else - { - // error happened during download. - qCritical() << "Failed " << m_url.toString() << " with reason " << error; - m_status = Job_Failed; - } -} - -void ForgeXzDownload::failAndTryNextMirror() -{ - m_status = Job_Failed; - emit failed(m_index_within_job); -} - -void ForgeXzDownload::downloadFinished() -{ - // if the download succeeded - if (m_status != Job_Failed && m_status != Job_Aborted) - { - // nothing went wrong... - m_status = Job_Finished; - if (m_pack200_xz_file.isOpen()) - { - // we actually downloaded something! process and isntall it - decompressAndInstall(); - return; - } - else - { - // something bad happened -- on the local machine! - m_status = Job_Failed; - m_pack200_xz_file.remove(); - m_reply.reset(); - emit failed(m_index_within_job); - return; - } - } - else if(m_status == Job_Aborted) - { - m_pack200_xz_file.remove(); - m_reply.reset(); - emit failed(m_index_within_job); - emit aborted(m_index_within_job); - return; - } - // else the download failed - else - { - m_status = Job_Failed; - m_pack200_xz_file.close(); - m_pack200_xz_file.remove(); - m_reply.reset(); - failAndTryNextMirror(); - return; - } -} - -void ForgeXzDownload::downloadReadyRead() -{ - - if (!m_pack200_xz_file.isOpen()) - { - if (!m_pack200_xz_file.open()) - { - /* - * Can't open the file... the job failed - */ - m_reply->abort(); - emit failed(m_index_within_job); - return; - } - } - m_pack200_xz_file.write(m_reply->readAll()); -} - -#include "xz.h" -#include "unpack200.h" -#include <stdexcept> -#include <unistd.h> - -const size_t buffer_size = 8196; - -// NOTE: once this gets here, it can't be aborted anymore. we don't care. -void ForgeXzDownload::decompressAndInstall() -{ - // rewind the downloaded temp file - m_pack200_xz_file.seek(0); - // de-xz'd file - QTemporaryFile pack200_file("./dl_temp.XXXXXX"); - pack200_file.open(); - - bool xz_success = false; - // first, de-xz - { - uint8_t in[buffer_size]; - uint8_t out[buffer_size]; - struct xz_buf b; - struct xz_dec *s; - enum xz_ret ret; - xz_crc32_init(); - xz_crc64_init(); - s = xz_dec_init(XZ_DYNALLOC, 1 << 26); - if (s == nullptr) - { - xz_dec_end(s); - failAndTryNextMirror(); - return; - } - b.in = in; - b.in_pos = 0; - b.in_size = 0; - b.out = out; - b.out_pos = 0; - b.out_size = buffer_size; - while (!xz_success) - { - if (b.in_pos == b.in_size) - { - b.in_size = m_pack200_xz_file.read((char *)in, sizeof(in)); - b.in_pos = 0; - } - - ret = xz_dec_run(s, &b); - - if (b.out_pos == sizeof(out)) - { - auto wresult = pack200_file.write((char *)out, b.out_pos); - if (wresult < 0 || size_t(wresult) != b.out_pos) - { - // msg = "Write error\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - } - - b.out_pos = 0; - } - - if (ret == XZ_OK) - continue; - - if (ret == XZ_UNSUPPORTED_CHECK) - { - // unsupported check. this is OK, but we should log this - continue; - } - - auto wresult = pack200_file.write((char *)out, b.out_pos); - if (wresult < 0 || size_t(wresult) != b.out_pos) - { - // write error - pack200_file.close(); - xz_dec_end(s); - return; - } - - switch (ret) - { - case XZ_STREAM_END: - xz_dec_end(s); - xz_success = true; - break; - - case XZ_MEM_ERROR: - qCritical() << "Memory allocation failed\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - case XZ_MEMLIMIT_ERROR: - qCritical() << "Memory usage limit reached\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - case XZ_FORMAT_ERROR: - qCritical() << "Not a .xz file\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - case XZ_OPTIONS_ERROR: - qCritical() << "Unsupported options in the .xz headers\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - case XZ_DATA_ERROR: - case XZ_BUF_ERROR: - qCritical() << "File is corrupt\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - - default: - qCritical() << "Bug!\n"; - xz_dec_end(s); - failAndTryNextMirror(); - return; - } - } - } - m_pack200_xz_file.remove(); - - // revert pack200 - pack200_file.seek(0); - int handle_in = pack200_file.handle(); - // FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects. - if(handle_in == -1) - { - qCritical() << "Error reopening " << pack200_file.fileName(); - failAndTryNextMirror(); - return; - } - int handle_in_dup = dup (handle_in); - if(handle_in_dup == -1) - { - qCritical() << "Error reopening " << pack200_file.fileName(); - failAndTryNextMirror(); - return; - } - FILE *file_in = fdopen (handle_in_dup, "rb"); - if(!file_in) - { - qCritical() << "Error reopening " << pack200_file.fileName(); - failAndTryNextMirror(); - return; - } - QFile qfile_out(m_target_path); - if(!qfile_out.open(QIODevice::WriteOnly)) - { - qCritical() << "Error opening " << qfile_out.fileName(); - failAndTryNextMirror(); - return; - } - int handle_out = qfile_out.handle(); - if(handle_out == -1) - { - qCritical() << "Error opening " << qfile_out.fileName(); - failAndTryNextMirror(); - return; - } - int handle_out_dup = dup (handle_out); - if(handle_out_dup == -1) - { - qCritical() << "Error reopening " << qfile_out.fileName(); - failAndTryNextMirror(); - return; - } - FILE *file_out = fdopen (handle_out_dup, "wb"); - if(!file_out) - { - qCritical() << "Error opening " << qfile_out.fileName(); - failAndTryNextMirror(); - return; - } - try - { - // NOTE: this takes ownership of both FILE pointers. That's why we duplicate them above. - unpack_200(file_in, file_out); - } - catch (const std::runtime_error &err) - { - m_status = Job_Failed; - qCritical() << "Error unpacking " << pack200_file.fileName() << " : " << err.what(); - QFile f(m_target_path); - if (f.exists()) - f.remove(); - failAndTryNextMirror(); - return; - } - pack200_file.remove(); - - QFile jar_file(m_target_path); - - if (!jar_file.open(QIODevice::ReadOnly)) - { - jar_file.remove(); - failAndTryNextMirror(); - return; - } - auto hash = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5); - m_entry->setMD5Sum(hash.toHex().constData()); - jar_file.close(); - - QFileInfo output_file_info(m_target_path); - m_entry->setETag(m_reply->rawHeader("ETag").constData()); - m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); - m_entry->setStale(false); - ENV.metacache()->updateEntry(m_entry); - - m_reply.reset(); - emit succeeded(m_index_within_job); -} - -bool ForgeXzDownload::abort() -{ - if(m_reply) - m_reply->abort(); - m_status = Job_Aborted; - return true; -} - -bool ForgeXzDownload::canAbort() -{ - return true; -} diff --git a/api/logic/minecraft/forge/ForgeXzDownload.h b/api/logic/minecraft/forge/ForgeXzDownload.h deleted file mode 100644 index 728a7f7a..00000000 --- a/api/logic/minecraft/forge/ForgeXzDownload.h +++ /dev/null @@ -1,61 +0,0 @@ -/* Copyright 2013-2018 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. - */ - -#pragma once - -#include "net/NetAction.h" -#include "net/HttpMetaCache.h" -#include <QFile> -#include <QTemporaryFile> - -typedef std::shared_ptr<class ForgeXzDownload> ForgeXzDownloadPtr; - -class ForgeXzDownload : public NetAction -{ - Q_OBJECT -public: - MetaEntryPtr m_entry; - /// if saving to file, use the one specified in this string - QString m_target_path; - /// this is the output file, if any - QTemporaryFile m_pack200_xz_file; - /// path relative to the mirror base - QString m_url_path; - -public: - explicit ForgeXzDownload(QString relative_path, MetaEntryPtr entry); - static ForgeXzDownloadPtr make(QString relative_path, MetaEntryPtr entry) - { - return ForgeXzDownloadPtr(new ForgeXzDownload(relative_path, entry)); - } - virtual ~ForgeXzDownload(){}; - bool canAbort() override; - -protected -slots: - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; - void downloadError(QNetworkReply::NetworkError error) override; - void downloadFinished() override; - void downloadReadyRead() override; - -public -slots: - void start() override; - bool abort() override; - -private: - void decompressAndInstall(); - void failAndTryNextMirror(); -}; diff --git a/api/logic/minecraft/gameoptions/GameOptions.cpp b/api/logic/minecraft/gameoptions/GameOptions.cpp new file mode 100644 index 00000000..e547b32a --- /dev/null +++ b/api/logic/minecraft/gameoptions/GameOptions.cpp @@ -0,0 +1,144 @@ +#include "GameOptions.h" +#include "FileSystem.h" +#include <QDebug> +#include <QSaveFile> + +namespace { +bool load(const QString& path, std::vector<GameOptionItem> &contents, int & version) +{ + contents.clear(); + QFile file(path); + if (!file.open(QFile::ReadOnly)) + { + qWarning() << "Failed to read options file."; + return false; + } + version = 0; + while(!file.atEnd()) + { + auto line = file.readLine(); + if(line.endsWith('\n')) + { + line.chop(1); + } + auto separatorIndex = line.indexOf(':'); + if(separatorIndex == -1) + { + continue; + } + auto key = QString::fromUtf8(line.data(), separatorIndex); + auto value = QString::fromUtf8(line.data() + separatorIndex + 1, line.size() - 1 - separatorIndex); + qDebug() << "!!" << key << "!!"; + if(key == "version") + { + version = value.toInt(); + continue; + } + contents.emplace_back(GameOptionItem{key, value}); + } + qDebug() << "Loaded" << path << "with version:" << version; + return true; +} +bool save(const QString& path, std::vector<GameOptionItem> &mapping, int version) +{ + QSaveFile out(path); + if(!out.open(QIODevice::WriteOnly)) + { + return false; + } + if(version != 0) + { + QString versionLine = QString("version:%1\n").arg(version); + out.write(versionLine.toUtf8()); + } + auto iter = mapping.begin(); + while (iter != mapping.end()) + { + out.write(iter->key.toUtf8()); + out.write(":"); + out.write(iter->value.toUtf8()); + out.write("\n"); + iter++; + } + return out.commit(); +} +} + +GameOptions::GameOptions(const QString& path): + path(path) +{ + reload(); +} + +QVariant GameOptions::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role != Qt::DisplayRole) + { + return QAbstractListModel::headerData(section, orientation, role); + } + switch(section) + { + case 0: + return tr("Key"); + case 1: + return tr("Value"); + default: + return QVariant(); + } +} + +QVariant GameOptions::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= int(contents.size())) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + if(column == 0) + { + return contents[row].key; + } + else + { + return contents[row].value; + } + default: + return QVariant(); + } + return QVariant(); +} + +int GameOptions::rowCount(const QModelIndex&) const +{ + return contents.size(); +} + +int GameOptions::columnCount(const QModelIndex&) const +{ + return 2; +} + +bool GameOptions::isLoaded() const +{ + return loaded; +} + +bool GameOptions::reload() +{ + beginResetModel(); + loaded = load(path, contents, version); + endResetModel(); + return loaded; +} + +bool GameOptions::save() +{ + return ::save(path, contents, version); +} diff --git a/api/logic/minecraft/gameoptions/GameOptions.h b/api/logic/minecraft/gameoptions/GameOptions.h new file mode 100644 index 00000000..c6d25492 --- /dev/null +++ b/api/logic/minecraft/gameoptions/GameOptions.h @@ -0,0 +1,34 @@ +#pragma once + +#include <map> +#include <QString> +#include <QAbstractListModel> + +struct GameOptionItem +{ + QString key; + QString value; +}; + +class GameOptions : public QAbstractListModel +{ + Q_OBJECT +public: + explicit GameOptions(const QString& path); + virtual ~GameOptions() = default; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex & parent) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + bool isLoaded() const; + bool reload(); + bool save(); + +private: + std::vector<GameOptionItem> contents; + bool loaded = false; + QString path; + int version = 0; +}; diff --git a/api/logic/minecraft/launch/ClaimAccount.h b/api/logic/minecraft/launch/ClaimAccount.h index 64891406..c5bd75f3 100644 --- a/api/logic/minecraft/launch/ClaimAccount.h +++ b/api/logic/minecraft/launch/ClaimAccount.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/launch/CreateGameFolders.cpp b/api/logic/minecraft/launch/CreateGameFolders.cpp new file mode 100644 index 00000000..4081e72e --- /dev/null +++ b/api/logic/minecraft/launch/CreateGameFolders.cpp @@ -0,0 +1,28 @@ +#include "CreateGameFolders.h" +#include "minecraft/MinecraftInstance.h" +#include "launch/LaunchTask.h" +#include "FileSystem.h" + +CreateGameFolders::CreateGameFolders(LaunchTask* parent): LaunchStep(parent) +{ +} + +void CreateGameFolders::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); + + if(!FS::ensureFolderPathExists(minecraftInstance->gameRoot())) + { + emit logLine("Couldn't create the main game folder", MessageLevel::Error); + emitFailed(tr("Couldn't create the main game folder")); + return; + } + + // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. + if(!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs"))) + { + emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); + } + emitSucceeded(); +} diff --git a/api/logic/minecraft/launch/CreateServerResourcePacksFolder.h b/api/logic/minecraft/launch/CreateGameFolders.h index b29496c9..9c7d3c94 100644 --- a/api/logic/minecraft/launch/CreateServerResourcePacksFolder.h +++ b/api/logic/minecraft/launch/CreateGameFolders.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,13 +19,13 @@ #include <LoggedProcess.h> #include <minecraft/auth/AuthSession.h> -// HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. -class CreateServerResourcePacksFolder: public LaunchStep +// Create the main .minecraft for the instance and any other necessary folders +class CreateGameFolders: public LaunchStep { Q_OBJECT public: - explicit CreateServerResourcePacksFolder(LaunchTask *parent); - virtual ~CreateServerResourcePacksFolder() {}; + explicit CreateGameFolders(LaunchTask *parent); + virtual ~CreateGameFolders() {}; virtual void executeTask(); virtual bool canAbort() const diff --git a/api/logic/minecraft/launch/CreateServerResourcePacksFolder.cpp b/api/logic/minecraft/launch/CreateServerResourcePacksFolder.cpp deleted file mode 100644 index ae426e31..00000000 --- a/api/logic/minecraft/launch/CreateServerResourcePacksFolder.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "CreateServerResourcePacksFolder.h" -#include "minecraft/MinecraftInstance.h" -#include "launch/LaunchTask.h" -#include "FileSystem.h" - -CreateServerResourcePacksFolder::CreateServerResourcePacksFolder(LaunchTask* parent): LaunchStep(parent) -{ -} - -void CreateServerResourcePacksFolder::executeTask() -{ - auto instance = m_parent->instance(); - std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); - if(!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs"))) - { - emit logLine(tr("Couldn't create the 'server-resource-packs' folder"), MessageLevel::Error); - } - emitSucceeded(); -} diff --git a/api/logic/minecraft/launch/DirectJavaLaunch.cpp b/api/logic/minecraft/launch/DirectJavaLaunch.cpp index 7ead8324..2110384f 100644 --- a/api/logic/minecraft/launch/DirectJavaLaunch.cpp +++ b/api/logic/minecraft/launch/DirectJavaLaunch.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -55,7 +55,7 @@ void DirectJavaLaunch::executeTask() // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); - auto mcArgs = minecraftInstance->processMinecraftArgs(m_session); + auto mcArgs = minecraftInstance->processMinecraftArgs(m_session, m_serverToJoin); args.append(mcArgs); QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); @@ -66,9 +66,9 @@ void DirectJavaLaunch::executeTask() auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); if (realWrapperCommand.isEmpty()) { - QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); return; } emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::MultiMC); @@ -87,18 +87,17 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) { case LoggedProcess::FailedToStart: { - //: Error message displayed if instace can't start - QString reason = tr("Could not launch minecraft!"); + //: Error message displayed if instance can't start + const char *reason = QT_TR_NOOP("Could not launch minecraft!"); emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + emitFailed(tr(reason)); return; } case LoggedProcess::Aborted: case LoggedProcess::Crashed: - { m_parent->setPid(-1); - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } case LoggedProcess::Finished: @@ -108,7 +107,7 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) auto exitCode = m_process.exitCode(); if(exitCode != 0) { - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } //FIXME: make this work again @@ -118,7 +117,7 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) break; } case LoggedProcess::Running: - emit logLine(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); m_parent->setPid(m_process.processId()); m_parent->instance()->setLastLaunch(); break; diff --git a/api/logic/minecraft/launch/DirectJavaLaunch.h b/api/logic/minecraft/launch/DirectJavaLaunch.h index fb555e3e..58b119b8 100644 --- a/api/logic/minecraft/launch/DirectJavaLaunch.h +++ b/api/logic/minecraft/launch/DirectJavaLaunch.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,6 +19,8 @@ #include <LoggedProcess.h> #include <minecraft/auth/AuthSession.h> +#include "MinecraftServerTarget.h" + class DirectJavaLaunch: public LaunchStep { Q_OBJECT @@ -38,6 +40,12 @@ public: { m_session = session; } + + void setServerToJoin(MinecraftServerTargetPtr serverToJoin) + { + m_serverToJoin = std::move(serverToJoin); + } + private slots: void on_state(LoggedProcess::State state); @@ -45,5 +53,6 @@ private: LoggedProcess m_process; QString m_command; AuthSessionPtr m_session; + MinecraftServerTargetPtr m_serverToJoin; }; diff --git a/api/logic/minecraft/launch/ExtractNatives.cpp b/api/logic/minecraft/launch/ExtractNatives.cpp index 336eddbd..d57499aa 100644 --- a/api/logic/minecraft/launch/ExtractNatives.cpp +++ b/api/logic/minecraft/launch/ExtractNatives.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -33,7 +33,7 @@ static QString replaceSuffix (QString target, const QString &suffix, const QStri return target + replacement; } -static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) +static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack, bool nativeOpenAL, bool nativeGLFW) { QuaZip zip(source); if(!zip.open(QuaZip::mdUnzip)) @@ -48,6 +48,13 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH do { QString name = zip.getCurrentFileName(); + auto lowercase = name.toLower(); + if (nativeGLFW && name.contains("glfw")) { + continue; + } + if (nativeOpenAL && name.contains("openal")) { + continue; + } if(applyJnilibHack) { name = replaceSuffix(name, ".jnilib", ".dylib"); @@ -76,16 +83,20 @@ void ExtractNatives::executeTask() emitSucceeded(); return; } + auto settings = minecraftInstance->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + auto outputPath = minecraftInstance->getNativePath(); auto javaVersion = minecraftInstance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for(const auto &source: toExtract) { - if(!unzipNatives(source, outputPath, jniHackEnabled)) + if(!unzipNatives(source, outputPath, jniHackEnabled, nativeOpenAL, nativeGLFW)) { - auto reason = tr("Couldn't extract native jar '%1' to destination '%2'").arg(source, outputPath); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); + emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); + emitFailed(tr(reason).arg(source, outputPath)); } } emitSucceeded(); diff --git a/api/logic/minecraft/launch/ExtractNatives.h b/api/logic/minecraft/launch/ExtractNatives.h index d9587991..094fcd6b 100644 --- a/api/logic/minecraft/launch/ExtractNatives.h +++ b/api/logic/minecraft/launch/ExtractNatives.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/launch/LauncherPartLaunch.cpp b/api/logic/minecraft/launch/LauncherPartLaunch.cpp index 466c1e46..ee469770 100644 --- a/api/logic/minecraft/launch/LauncherPartLaunch.cpp +++ b/api/logic/minecraft/launch/LauncherPartLaunch.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -59,7 +59,7 @@ void LauncherPartLaunch::executeTask() auto instance = m_parent->instance(); std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); - m_launchScript = minecraftInstance->createLaunchScript(m_session); + m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin); QStringList args = minecraftInstance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::MultiMC); @@ -118,9 +118,9 @@ void LauncherPartLaunch::executeTask() auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); if (realWrapperCommand.isEmpty()) { - QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); return; } emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::MultiMC); @@ -140,17 +140,16 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) case LoggedProcess::FailedToStart: { //: Error message displayed if instace can't start - QString reason = tr("Could not launch minecraft!"); + const char *reason = QT_TR_NOOP("Could not launch minecraft!"); emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + emitFailed(tr(reason)); return; } case LoggedProcess::Aborted: case LoggedProcess::Crashed: - { m_parent->setPid(-1); - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } case LoggedProcess::Finished: @@ -160,7 +159,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) auto exitCode = m_process.exitCode(); if(exitCode != 0) { - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } //FIXME: make this work again @@ -170,7 +169,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) break; } case LoggedProcess::Running: - emit logLine(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); m_parent->setPid(m_process.processId()); m_parent->instance()->setLastLaunch(); // send the launch script to the launcher part diff --git a/api/logic/minecraft/launch/LauncherPartLaunch.h b/api/logic/minecraft/launch/LauncherPartLaunch.h index 7fadbd66..6a7ee0e5 100644 --- a/api/logic/minecraft/launch/LauncherPartLaunch.h +++ b/api/logic/minecraft/launch/LauncherPartLaunch.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,6 +19,8 @@ #include <LoggedProcess.h> #include <minecraft/auth/AuthSession.h> +#include "MinecraftServerTarget.h" + class LauncherPartLaunch: public LaunchStep { Q_OBJECT @@ -39,6 +41,11 @@ public: m_session = session; } + void setServerToJoin(MinecraftServerTargetPtr serverToJoin) + { + m_serverToJoin = std::move(serverToJoin); + } + private slots: void on_state(LoggedProcess::State state); @@ -47,5 +54,7 @@ private: QString m_command; AuthSessionPtr m_session; QString m_launchScript; + MinecraftServerTargetPtr m_serverToJoin; + bool mayProceed = false; }; diff --git a/api/logic/minecraft/launch/MinecraftServerTarget.cpp b/api/logic/minecraft/launch/MinecraftServerTarget.cpp new file mode 100644 index 00000000..569273b6 --- /dev/null +++ b/api/logic/minecraft/launch/MinecraftServerTarget.cpp @@ -0,0 +1,66 @@ +/* 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. + */ + +#include "MinecraftServerTarget.h" + +#include <QStringList> + +MinecraftServerTarget MinecraftServerTarget::parse(const QString &fullAddress) { + QStringList split = fullAddress.split(":"); + + // The logic below replicates the exact logic minecraft uses for parsing server addresses. + // While the conversion is not lossless and eats errors, it ensures the same behavior + // within Minecraft and MultiMC when entering server addresses. + if (fullAddress.startsWith("[")) + { + int bracket = fullAddress.indexOf("]"); + if (bracket > 0) + { + QString ipv6 = fullAddress.mid(1, bracket - 1); + QString port = fullAddress.mid(bracket + 1).trimmed(); + + if (port.startsWith(":") && !ipv6.isEmpty()) + { + port = port.mid(1); + split = QStringList({ ipv6, port }); + } + else + { + split = QStringList({ipv6}); + } + } + } + + if (split.size() > 2) + { + split = QStringList({fullAddress}); + } + + QString realAddress = split[0]; + + quint16 realPort = 25565; + if (split.size() > 1) + { + bool ok; + realPort = split[1].toUInt(&ok); + + if (!ok) + { + realPort = 25565; + } + } + + return MinecraftServerTarget { realAddress, realPort }; +} diff --git a/api/logic/minecraft/launch/MinecraftServerTarget.h b/api/logic/minecraft/launch/MinecraftServerTarget.h new file mode 100644 index 00000000..3c5786f4 --- /dev/null +++ b/api/logic/minecraft/launch/MinecraftServerTarget.h @@ -0,0 +1,30 @@ +/* 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. + */ + +#pragma once + +#include <memory> + +#include <QString> +#include <multimc_logic_export.h> + +struct MinecraftServerTarget { + QString address; + quint16 port; + + static MULTIMC_LOGIC_EXPORT MinecraftServerTarget parse(const QString &fullAddress); +}; + +typedef std::shared_ptr<MinecraftServerTarget> MinecraftServerTargetPtr; diff --git a/api/logic/minecraft/launch/ModMinecraftJar.cpp b/api/logic/minecraft/launch/ModMinecraftJar.cpp index 34472bb3..93de9d59 100644 --- a/api/logic/minecraft/launch/ModMinecraftJar.cpp +++ b/api/logic/minecraft/launch/ModMinecraftJar.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,7 +19,7 @@ #include "minecraft/OpSys.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" void ModMinecraftJar::executeTask() { @@ -43,7 +43,7 @@ void ModMinecraftJar::executeTask() } // create temporary modded jar, if needed - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto jarMods = m_inst->getJarMods(); if(jarMods.size()) diff --git a/api/logic/minecraft/launch/ModMinecraftJar.h b/api/logic/minecraft/launch/ModMinecraftJar.h index 48e11736..081c6a91 100644 --- a/api/logic/minecraft/launch/ModMinecraftJar.h +++ b/api/logic/minecraft/launch/ModMinecraftJar.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. diff --git a/api/logic/minecraft/launch/PrintInstanceInfo.cpp b/api/logic/minecraft/launch/PrintInstanceInfo.cpp index 6d5b93ae..0b9611ad 100644 --- a/api/logic/minecraft/launch/PrintInstanceInfo.cpp +++ b/api/logic/minecraft/launch/PrintInstanceInfo.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -101,6 +101,6 @@ void PrintInstanceInfo::executeTask() #endif logLines(log, MessageLevel::MultiMC); - logLines(instance->verboseDescription(m_session), MessageLevel::MultiMC); + logLines(instance->verboseDescription(m_session, m_serverToJoin), MessageLevel::MultiMC); emitSucceeded(); } diff --git a/api/logic/minecraft/launch/PrintInstanceInfo.h b/api/logic/minecraft/launch/PrintInstanceInfo.h index ae0a0400..fdc30f31 100644 --- a/api/logic/minecraft/launch/PrintInstanceInfo.h +++ b/api/logic/minecraft/launch/PrintInstanceInfo.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -18,13 +18,15 @@ #include <launch/LaunchStep.h> #include <memory> #include "minecraft/auth/AuthSession.h" +#include "minecraft/launch/MinecraftServerTarget.h" // FIXME: temporary wrapper for existing task. class PrintInstanceInfo: public LaunchStep { Q_OBJECT public: - explicit PrintInstanceInfo(LaunchTask *parent, AuthSessionPtr session) : LaunchStep(parent), m_session(session) {}; + explicit PrintInstanceInfo(LaunchTask *parent, AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) : + LaunchStep(parent), m_session(session), m_serverToJoin(serverToJoin) {}; virtual ~PrintInstanceInfo(){}; virtual void executeTask(); @@ -34,5 +36,6 @@ public: } private: AuthSessionPtr m_session; + MinecraftServerTargetPtr m_serverToJoin; }; diff --git a/api/logic/minecraft/launch/ReconstructAssets.cpp b/api/logic/minecraft/launch/ReconstructAssets.cpp new file mode 100644 index 00000000..4d206665 --- /dev/null +++ b/api/logic/minecraft/launch/ReconstructAssets.cpp @@ -0,0 +1,36 @@ +/* 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. + */ + +#include "ReconstructAssets.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/AssetsUtils.h" +#include "launch/LaunchTask.h" + +void ReconstructAssets::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); + auto components = minecraftInstance->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + + if(!AssetsUtils::reconstructAssets(assets->id, minecraftInstance->resourcesDir())) + { + emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); + } + + emitSucceeded(); +} diff --git a/api/logic/minecraft/launch/ReconstructAssets.h b/api/logic/minecraft/launch/ReconstructAssets.h new file mode 100644 index 00000000..58d7febd --- /dev/null +++ b/api/logic/minecraft/launch/ReconstructAssets.h @@ -0,0 +1,33 @@ +/* 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. + */ + +#pragma once + +#include <launch/LaunchStep.h> +#include <memory> + +class ReconstructAssets: public LaunchStep +{ + Q_OBJECT +public: + explicit ReconstructAssets(LaunchTask *parent) : LaunchStep(parent){}; + virtual ~ReconstructAssets(){}; + + void executeTask() override; + bool canAbort() const override + { + return false; + } +}; diff --git a/api/logic/minecraft/launch/ScanModFolders.cpp b/api/logic/minecraft/launch/ScanModFolders.cpp new file mode 100644 index 00000000..2a0e21b3 --- /dev/null +++ b/api/logic/minecraft/launch/ScanModFolders.cpp @@ -0,0 +1,59 @@ +/* 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. + */ + +#include "ScanModFolders.h" +#include "launch/LaunchTask.h" +#include "MMCZip.h" +#include "minecraft/OpSys.h" +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/mod/ModFolderModel.h" + +void ScanModFolders::executeTask() +{ + auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance()); + + auto loaders = m_inst->loaderModList(); + connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + if(!loaders->update()) { + m_modsDone = true; + } + + auto cores = m_inst->coreModList(); + connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + if(!cores->update()) { + m_coreModsDone = true; + } + checkDone(); +} + +void ScanModFolders::modsDone() +{ + m_modsDone = true; + checkDone(); +} + +void ScanModFolders::coreModsDone() +{ + m_coreModsDone = true; + checkDone(); +} + +void ScanModFolders::checkDone() +{ + if(m_modsDone && m_coreModsDone) { + emitSucceeded(); + } +} diff --git a/api/logic/minecraft/launch/ScanModFolders.h b/api/logic/minecraft/launch/ScanModFolders.h new file mode 100644 index 00000000..d5989170 --- /dev/null +++ b/api/logic/minecraft/launch/ScanModFolders.h @@ -0,0 +1,42 @@ +/* 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. + */ + +#pragma once + +#include <launch/LaunchStep.h> +#include <memory> + +class ScanModFolders: public LaunchStep +{ + Q_OBJECT +public: + explicit ScanModFolders(LaunchTask *parent) : LaunchStep(parent) {}; + virtual ~ScanModFolders(){}; + + virtual void executeTask() override; + virtual bool canAbort() const override + { + return false; + } +private slots: + void coreModsDone(); + void modsDone(); +private: + void checkDone(); + +private: // DATA + bool m_modsDone = false; + bool m_coreModsDone = false; +}; diff --git a/api/logic/minecraft/launch/VerifyJavaInstall.cpp b/api/logic/minecraft/launch/VerifyJavaInstall.cpp new file mode 100644 index 00000000..657669af --- /dev/null +++ b/api/logic/minecraft/launch/VerifyJavaInstall.cpp @@ -0,0 +1,34 @@ +#include "VerifyJavaInstall.h" + +#include <launch/LaunchTask.h> +#include <minecraft/MinecraftInstance.h> +#include <minecraft/PackProfile.h> +#include <minecraft/VersionFilterData.h> + +void VerifyJavaInstall::executeTask() { + auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance()); + + auto javaVersion = m_inst->getJavaVersion(); + auto minecraftComponent = m_inst->getPackProfile()->getComponent("net.minecraft"); + + // Java 16 requirement + if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate) { + if (javaVersion.major() < 16) { + emit logLine("Minecraft 21w19a and above require the use of Java 16", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 21w19a and above require the use of Java 16")); + return; + } + } + // Java 8 requirement + else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java8BeginsDate) { + if (javaVersion.major() < 8) { + emit logLine("Minecraft 17w13a and above require the use of Java 8", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 17w13a and above require the use of Java 8")); + return; + } + } + + emitSucceeded(); +} diff --git a/api/logic/minecraft/launch/VerifyJavaInstall.h b/api/logic/minecraft/launch/VerifyJavaInstall.h new file mode 100644 index 00000000..a553106d --- /dev/null +++ b/api/logic/minecraft/launch/VerifyJavaInstall.h @@ -0,0 +1,17 @@ +#pragma once + +#include <launch/LaunchStep.h> + +class VerifyJavaInstall : public LaunchStep { + Q_OBJECT + +public: + explicit VerifyJavaInstall(LaunchTask *parent) : LaunchStep(parent) { + }; + ~VerifyJavaInstall() override = default; + + void executeTask() override; + bool canAbort() const override { + return false; + } +}; diff --git a/api/logic/minecraft/legacy/LegacyInstance.cpp b/api/logic/minecraft/legacy/LegacyInstance.cpp index a35101e3..9f9bda5a 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.cpp +++ b/api/logic/minecraft/legacy/LegacyInstance.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -21,7 +21,6 @@ #include "LegacyInstance.h" #include "minecraft/legacy/LegacyModList.h" -#include "minecraft/SimpleModList.h" #include "minecraft/WorldList.h" #include <MMCZip.h> #include <FileSystem.h> @@ -107,11 +106,6 @@ std::shared_ptr<LegacyModList> LegacyInstance::jarModList() const return jar_mod_list; } -QList<Mod> LegacyInstance::getJarMods() const -{ - return jarModList()->allMods(); -} - QString LegacyInstance::gameRoot() const { QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); @@ -231,7 +225,7 @@ QString LegacyInstance::getStatusbarDescription() return tr("Instance from previous versions."); } -QStringList LegacyInstance::verboseDescription(AuthSessionPtr session) +QStringList LegacyInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QStringList out; diff --git a/api/logic/minecraft/legacy/LegacyInstance.h b/api/logic/minecraft/legacy/LegacyInstance.h index 619e2c83..325bac7a 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.h +++ b/api/logic/minecraft/legacy/LegacyInstance.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -16,11 +16,11 @@ #pragma once #include "BaseInstance.h" -#include "minecraft/Mod.h" +#include "launch/LaunchTask.h" #include "multimc_logic_export.h" -class SimpleModList; +class ModFolderModel; class LegacyModList; class WorldList; class Task; @@ -77,7 +77,6 @@ public: QString customBaseJar() const; std::shared_ptr<LegacyModList> jarModList() const; - QList<Mod> getJarMods() const; std::shared_ptr<WorldList> worldList() const; /*! @@ -112,7 +111,8 @@ public: { return false; } - std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override + shared_qobject_ptr<LaunchTask> createLaunchTask( + AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override { return nullptr; } @@ -126,7 +126,7 @@ public: } QString getStatusbarDescription() override; - QStringList verboseDescription(AuthSessionPtr session) override; + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; QProcessEnvironment createEnvironment() override { diff --git a/api/logic/minecraft/legacy/LegacyModList.cpp b/api/logic/minecraft/legacy/LegacyModList.cpp index ca171b08..7301eb8c 100644 --- a/api/logic/minecraft/legacy/LegacyModList.cpp +++ b/api/logic/minecraft/legacy/LegacyModList.cpp @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -22,8 +22,7 @@ LegacyModList::LegacyModList(const QString &dir, const QString &list_file) : m_dir(dir), m_list_file(list_file) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | - QDir::NoSymLinks); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); } @@ -34,15 +33,11 @@ LegacyModList::LegacyModList(const QString &dir, const QString &list_file) }; typedef QList<OrderItem> OrderList; -static void internalSort(QList<Mod> &what) +static void internalSort(QList<LegacyModList::Mod> &what) { - auto predicate = [](const Mod &left, const Mod &right) + auto predicate = [](const LegacyModList::Mod &left, const LegacyModList::Mod &right) { - if (left.name() == right.name()) - { - return left.mmc_id().localeAwareCompare(right.mmc_id()) < 0; - } - return left.name().localeAwareCompare(right.name()) < 0; + return left.fileName().localeAwareCompare(right.fileName()) < 0; }; std::sort(what.begin(), what.end(), predicate); } @@ -90,7 +85,6 @@ bool LegacyModList::update() QList<Mod> newMods; m_dir.refresh(); auto folderContents = m_dir.entryInfoList(); - bool orderOrStateChanged = false; // first, process the ordered items (if any) OrderList listOrder = readListFile(m_list_file); @@ -124,48 +118,19 @@ bool LegacyModList::update() // remove from the actual folder contents list folderContents.takeAt(idx); // append the new mod - orderedMods.append(Mod(info)); - if (isEnabled != item.enabled) - orderOrStateChanged = true; - } - else - { - orderOrStateChanged = true; + orderedMods.append(info); } } - // if there are any untracked files... + // if there are any untracked files... append them sorted at the end if (folderContents.size()) { - // the order surely changed! for (auto entry : folderContents) { - newMods.append(Mod(entry)); + newMods.append(entry); } internalSort(newMods); orderedMods.append(newMods); - orderOrStateChanged = true; - } - // otherwise, if we were already tracking some mods - else if (mods.size()) - { - // if the number doesn't match, order changed. - if (mods.size() != orderedMods.size()) - orderOrStateChanged = true; - // if it does match, compare the mods themselves - else - for (int i = 0; i < mods.size(); i++) - { - if (!mods[i].strongCompare(orderedMods[i])) - { - orderOrStateChanged = true; - break; - } - } } mods.swap(orderedMods); - if (orderOrStateChanged && !m_list_file.isEmpty()) - { - qDebug() << "Mod list " << m_list_file << " changed!"; - } return true; } diff --git a/api/logic/minecraft/legacy/LegacyModList.h b/api/logic/minecraft/legacy/LegacyModList.h index 8bc68b87..8881d471 100644 --- a/api/logic/minecraft/legacy/LegacyModList.h +++ b/api/logic/minecraft/legacy/LegacyModList.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -19,21 +19,14 @@ #include <QString> #include <QDir> -#include "minecraft/Mod.h" - #include "multimc_logic_export.h" -class LegacyInstance; -class BaseInstance; - -/** - * A legacy mod list. - * Backed by a folder. - */ class MULTIMC_LOGIC_EXPORT LegacyModList { public: + using Mod = QFileInfo; + LegacyModList(const QString &dir, const QString &list_file = QString()); /// Reloads the mod list and returns true if the list changed. diff --git a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp b/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp index 9a832a24..a4ea60cd 100644 --- a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp +++ b/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp @@ -6,7 +6,8 @@ #include <QtConcurrentRun> #include "LegacyInstance.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" +#include "LegacyModList.h" #include "classparser.h" LegacyUpgradeTask::LegacyUpgradeTask(InstancePtr origInstance) @@ -83,7 +84,7 @@ void LegacyUpgradeTask::copyFinished() } } } - auto components = inst.getComponentList(); + auto components = inst.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", preferredVersionNumber, true); @@ -96,10 +97,10 @@ void LegacyUpgradeTask::copyFinished() components->installCustomJar(jarPath); } - auto jarMods = legacyInst->getJarMods(); + auto jarMods = legacyInst->jarModList()->allMods(); for(auto & jarMod: jarMods) { - QString modPath = jarMod.filename().absoluteFilePath(); + QString modPath = jarMod.absoluteFilePath(); qDebug() << "jarMod: " << modPath; components->installJarMods({modPath}); } diff --git a/api/logic/minecraft/mod/LocalModParseTask.cpp b/api/logic/minecraft/mod/LocalModParseTask.cpp new file mode 100644 index 00000000..0d6972fb --- /dev/null +++ b/api/logic/minecraft/mod/LocalModParseTask.cpp @@ -0,0 +1,467 @@ +#include "LocalModParseTask.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <quazip.h> +#include <quazipfile.h> +#include <toml.h> + +#include "settings/INIFile.h" +#include "FileSystem.h" + +namespace { + +// NEW format +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 + +// OLD format: +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc +std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) +{ + auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails> + { + if (!arr.at(0).isObject()) { + return nullptr; + } + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + auto firstObj = arr.at(0).toObject(); + details->mod_id = firstObj.value("modid").toString(); + auto name = firstObj.value("name").toString(); + // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name + if(name != "Example Mod") { + details->name = name; + } + details->version = firstObj.value("version").toString(); + details->updateurl = firstObj.value("updateUrl").toString(); + auto homeurl = firstObj.value("url").toString().trimmed(); + if(!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + details->description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) { + // FIXME: what is the format of this? is there any? + authors = firstObj.value("authors").toArray(); + } + + for (auto author: authors) + { + details->authors.append(author.toString()); + } + details->credits = firstObj.value("credits").toString(); + return details; + }; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + return getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if(val.isUndefined()) { + val = jsonDoc.object().value("modListVersion"); + } + int version = val.toDouble(); + if (version != 2) + { + qCritical() << "BAD stuff happened to mod json:"; + qCritical() << contents; + return nullptr; + } + auto arrVal = jsonDoc.object().value("modlist"); + if(arrVal.isUndefined()) { + arrVal = jsonDoc.object().value("modList"); + } + if (arrVal.isArray()) + { + return getInfoFromArray(arrVal.toArray()); + } + } + return nullptr; +} + +// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md +std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents) +{ + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + + char errbuf[200]; + // top-level table + toml_table_t* tomlData = toml_parse(contents.data(), errbuf, sizeof(errbuf)); + + if(!tomlData) + { + return nullptr; + } + + // array defined by [[mods]] + toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods"); + // we only really care about the first element, since multiple mods in one file is not supported by us at the moment + toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0); + + // mandatory properties - always in [[mods]] + toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId"); + if(modIdDatum.ok) + { + details->mod_id = modIdDatum.u.s; + // library says this is required for strings + free(modIdDatum.u.s); + } + toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version"); + if(versionDatum.ok) + { + details->version = versionDatum.u.s; + free(versionDatum.u.s); + } + toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName"); + if(displayNameDatum.ok) + { + details->name = displayNameDatum.u.s; + free(displayNameDatum.u.s); + } + toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description"); + if(descriptionDatum.ok) + { + details->description = descriptionDatum.u.s; + free(descriptionDatum.u.s); + } + + // optional properties - can be in the root table or [[mods]] + toml_datum_t authorsDatum = toml_string_in(tomlData, "authors"); + QString authors = ""; + if(authorsDatum.ok) + { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + else + { + authorsDatum = toml_string_in(tomlModsTable0, "authors"); + if(authorsDatum.ok) + { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + } + if(!authors.isEmpty()) + { + // author information is stored as a string now, not a list + details->authors.append(authors); + } + // is credits even used anywhere? including this for completion/parity with old data version + toml_datum_t creditsDatum = toml_string_in(tomlData, "credits"); + QString credits = ""; + if(creditsDatum.ok) + { + authors = creditsDatum.u.s; + free(creditsDatum.u.s); + } + else + { + creditsDatum = toml_string_in(tomlModsTable0, "credits"); + if(creditsDatum.ok) + { + credits = creditsDatum.u.s; + free(creditsDatum.u.s); + } + } + details->credits = credits; + toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); + QString homeurl = ""; + if(homeurlDatum.ok) + { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + else + { + homeurlDatum = toml_string_in(tomlModsTable0, "displayURL"); + if(homeurlDatum.ok) + { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + } + if(!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + + // this seems to be recursive, so it should free everything + toml_free(tomlData); + + return details; +} + +// https://fabricmc.net/wiki/documentation:fabric_mod_json +std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; + + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + + details->mod_id = object.value("id").toString(); + details->version = object.value("version").toString(); + + details->name = object.contains("name") ? object.value("name").toString() : details->mod_id; + details->description = object.value("description").toString(); + + if (schemaVersion >= 1) + { + QJsonArray authors = object.value("authors").toArray(); + for (auto author: authors) + { + if(author.isObject()) { + details->authors.append(author.toObject().value("name").toString()); + } + else { + details->authors.append(author.toString()); + } + } + + if (object.contains("contact")) + { + QJsonObject contact = object.value("contact").toObject(); + + if (contact.contains("homepage")) + { + details->homeurl = contact.value("homepage").toString(); + } + } + } + return details; +} + +std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents) +{ + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + // Read the data + details->name = "Minecraft Forge"; + details->mod_id = "Forge"; + details->homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return details; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + details->version = major + "." + minor + "." + revision + "." + build; + return details; +} + +std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents) +{ + std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>(); + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) + { + details->mod_id = details->name = object.value("name").toString(); + } + if (object.contains("version")) + { + details->version = object.value("version").toString(""); + } + else + { + details->version = object.value("revision").toString(""); + } + details->mcversion = object.value("mcversion").toString(); + auto author = object.value("author").toString(); + if(!author.isEmpty()) { + details->authors.append(author); + } + details->description = object.value("description").toString(); + details->homeurl = object.value("url").toString(); + return details; +} + +} + +LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile): + m_token(token), + m_type(type), + m_modFile(modFile), + m_result(new Result()) +{ +} + +void LocalModParseTask::processAsZip() +{ + QuaZip zip(m_modFile.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("META-INF/mods.toml")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadMCModTOML(file.readAll()); + file.close(); + + // to replace ${file.jarVersion} with the actual version, as needed + if (m_result->details && m_result->details->version == "${file.jarVersion}") + { + if (zip.setCurrentFile("META-INF/MANIFEST.MF")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + // quick and dirty line-by-line parser + auto manifestLines = file.readAll().split('\n'); + QString manifestVersion = ""; + for (auto &line : manifestLines) + { + if (QString(line).startsWith("Implementation-Version: ")) + { + manifestVersion = QString(line).remove("Implementation-Version: "); + break; + } + } + + // some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF + // also keep with forge's behavior of setting the version to "NONE" if none is found + if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") + { + manifestVersion = "NONE"; + } + + m_result->details->version = manifestVersion; + + file.close(); + } + } + + zip.close(); + return; + } + else if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("fabric.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadFabricModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + + zip.close(); +} + +void LocalModParseTask::processAsFolder() +{ + QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); + if (mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return; + m_result->details = ReadMCModInfo(data); + } +} + +void LocalModParseTask::processAsLitemod() +{ + QuaZip zip(m_modFile.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadLiteModInfo(file.readAll()); + file.close(); + } + zip.close(); +} + +void LocalModParseTask::run() +{ + switch(m_type) + { + case Mod::MOD_ZIPFILE: + processAsZip(); + break; + case Mod::MOD_FOLDER: + processAsFolder(); + break; + case Mod::MOD_LITEMOD: + processAsLitemod(); + break; + default: + break; + } + emit finished(m_token); +} diff --git a/api/logic/minecraft/mod/LocalModParseTask.h b/api/logic/minecraft/mod/LocalModParseTask.h new file mode 100644 index 00000000..0f119ba6 --- /dev/null +++ b/api/logic/minecraft/mod/LocalModParseTask.h @@ -0,0 +1,37 @@ +#pragma once +#include <QRunnable> +#include <QDebug> +#include <QObject> +#include "Mod.h" +#include "ModDetails.h" + +class LocalModParseTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QString id; + std::shared_ptr<ModDetails> details; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const { + return m_result; + } + + LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile); + void run(); + +signals: + void finished(int token); + +private: + void processAsZip(); + void processAsFolder(); + void processAsLitemod(); + +private: + int m_token; + Mod::ModType m_type; + QFileInfo m_modFile; + ResultPtr m_result; +}; diff --git a/api/logic/minecraft/mod/Mod.cpp b/api/logic/minecraft/mod/Mod.cpp new file mode 100644 index 00000000..b6bff29b --- /dev/null +++ b/api/logic/minecraft/mod/Mod.cpp @@ -0,0 +1,151 @@ +/* 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. + */ + +#include <QDir> +#include <QString> + +#include "Mod.h" +#include <QDebug> +#include <FileSystem.h> + +namespace { + +ModDetails invalidDetails; + +} + + +Mod::Mod(const QFileInfo &file) +{ + repath(file); + m_changedDateTime = file.lastModified(); +} + +void Mod::repath(const QFileInfo &file) +{ + m_file = file; + QString name_base = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + + m_mmc_id = name_base; + + if (m_file.isDir()) + { + m_type = MOD_FOLDER; + m_name = name_base; + } + else if (m_file.isFile()) + { + if (name_base.endsWith(".disabled")) + { + m_enabled = false; + name_base.chop(9); + } + else + { + m_enabled = true; + } + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) + { + m_type = MOD_ZIPFILE; + name_base.chop(4); + } + else if (name_base.endsWith(".litemod")) + { + m_type = MOD_LITEMOD; + name_base.chop(8); + } + else + { + m_type = MOD_SINGLEFILE; + } + m_name = name_base; + } +} + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) + { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } + else + { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + repath(QFileInfo(path)); + m_enabled = value; + return true; +} + +bool Mod::destroy() +{ + m_type = MOD_UNKNOWN; + return FS::deletePath(m_file.filePath()); +} + + +const ModDetails & Mod::details() const +{ + if(!m_localDetails) + return invalidDetails; + return *m_localDetails; +} + + +QString Mod::version() const +{ + return details().version; +} + +QString Mod::name() const +{ + auto & d = details(); + if(!d.name.isEmpty()) { + return d.name; + } + return m_name; +} + +QString Mod::homeurl() const +{ + return details().homeurl; +} + +QString Mod::description() const +{ + return details().description; +} + +QStringList Mod::authors() const +{ + return details().authors; +} diff --git a/api/logic/minecraft/Mod.h b/api/logic/minecraft/mod/Mod.h index 0c1adf24..f77ffd41 100644 --- a/api/logic/minecraft/Mod.h +++ b/api/logic/minecraft/mod/Mod.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -16,8 +16,16 @@ #pragma once #include <QFileInfo> #include <QDateTime> +#include <QList> +#include <memory> -class Mod +#include "multimc_logic_export.h" + +#include "ModDetails.h" + + + +class MULTIMC_LOGIC_EXPORT Mod { public: enum ModType @@ -29,6 +37,7 @@ public: MOD_LITEMOD, //!< The mod is a litemod }; + Mod() = default; Mod(const QFileInfo &file); QFileInfo filename() const @@ -39,54 +48,14 @@ public: { return m_mmc_id; } - QString mod_id() const - { - return m_mod_id; - } ModType type() const { return m_type; } - QString mcversion() const - { - return m_mcversion; - } - ; bool valid() { return m_type != MOD_UNKNOWN; } - QString name() const - { - QString name = m_name.trimmed(); - if(name.isEmpty() || name == "Example Mod") - { - return m_mmc_id; - } - return m_name; - } - - QString version() const; - - QString homeurl() const - { - return m_homeurl; - } - - QString description() const - { - return m_description; - } - - QString authors() const - { - return m_authors; - } - - QString credits() const - { - return m_credits; - } QDateTime dateTimeChanged() const { @@ -98,45 +67,51 @@ public: return m_enabled; } + const ModDetails &details() const; + + QString name() const; + QString version() const; + QString homeurl() const; + QString description() const; + QStringList authors() const; + bool enable(bool value); // delete all the files of this mod bool destroy(); - // replace this mod with a copy of the other - bool replace(Mod &with); + // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void repath(const QFileInfo &file); - // WEAK compare operator - used for replacing mods - bool operator==(const Mod &other) const; - bool strongCompare(const Mod &other) const; - -private: - void ReadMCModInfo(QByteArray contents); - void ReadForgeInfo(QByteArray contents); - void ReadLiteModInfo(QByteArray contents); + bool shouldResolve() { + return !m_resolving && !m_resolved; + } + bool isResolving() { + return m_resolving; + } + int resolutionTicket() + { + return m_resolutionTicket; + } + void setResolving(bool resolving, int resolutionTicket) { + m_resolving = resolving; + m_resolutionTicket = resolutionTicket; + } + void finishResolvingWithDetails(std::shared_ptr<ModDetails> details){ + m_resolving = false; + m_resolved = true; + m_localDetails = details; + } protected: - - // FIXME: what do do with those? HMM... - /* - void ReadModInfoData(QString info); - void ReadForgeInfoData(QString infoFileData); - */ - QFileInfo m_file; QDateTime m_changedDateTime; QString m_mmc_id; - QString m_mod_id; - bool m_enabled = true; QString m_name; - QString m_version; - QString m_mcversion; - QString m_homeurl; - QString m_updateurl; - QString m_description; - QString m_authors; - QString m_credits; - - ModType m_type; + bool m_enabled = true; + bool m_resolving = false; + bool m_resolved = false; + int m_resolutionTicket = 0; + ModType m_type = MOD_UNKNOWN; + std::shared_ptr<ModDetails> m_localDetails; }; diff --git a/api/logic/minecraft/mod/ModDetails.h b/api/logic/minecraft/mod/ModDetails.h new file mode 100644 index 00000000..6ab4aee7 --- /dev/null +++ b/api/logic/minecraft/mod/ModDetails.h @@ -0,0 +1,17 @@ +#pragma once + +#include <QString> +#include <QStringList> + +struct ModDetails +{ + QString mod_id; + QString name; + QString version; + QString mcversion; + QString homeurl; + QString updateurl; + QString description; + QStringList authors; + QString credits; +}; diff --git a/api/logic/minecraft/mod/ModFolderLoadTask.cpp b/api/logic/minecraft/mod/ModFolderLoadTask.cpp new file mode 100644 index 00000000..88349877 --- /dev/null +++ b/api/logic/minecraft/mod/ModFolderLoadTask.cpp @@ -0,0 +1,18 @@ +#include "ModFolderLoadTask.h" +#include <QDebug> + +ModFolderLoadTask::ModFolderLoadTask(QDir dir) : + m_dir(dir), m_result(new Result()) +{ +} + +void ModFolderLoadTask::run() +{ + m_dir.refresh(); + for (auto entry : m_dir.entryInfoList()) + { + Mod m(entry); + m_result->mods[m.mmc_id()] = m; + } + emit succeeded(); +} diff --git a/api/logic/minecraft/mod/ModFolderLoadTask.h b/api/logic/minecraft/mod/ModFolderLoadTask.h new file mode 100644 index 00000000..8d720e65 --- /dev/null +++ b/api/logic/minecraft/mod/ModFolderLoadTask.h @@ -0,0 +1,29 @@ +#pragma once +#include <QRunnable> +#include <QObject> +#include <QDir> +#include <QMap> +#include "Mod.h" +#include <memory> + +class ModFolderLoadTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QMap<QString, Mod> mods; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const { + return m_result; + } + +public: + ModFolderLoadTask(QDir dir); + void run(); +signals: + void succeeded(); +private: + QDir m_dir; + ResultPtr m_result; +}; diff --git a/api/logic/minecraft/mod/ModFolderModel.cpp b/api/logic/minecraft/mod/ModFolderModel.cpp new file mode 100644 index 00000000..031eebe5 --- /dev/null +++ b/api/logic/minecraft/mod/ModFolderModel.cpp @@ -0,0 +1,554 @@ +/* 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. + */ + +#include "ModFolderModel.h" +#include <FileSystem.h> +#include <QMimeData> +#include <QUrl> +#include <QUuid> +#include <QString> +#include <QFileSystemWatcher> +#include <QDebug> +#include "ModFolderLoadTask.h" +#include <QThreadPool> +#include <algorithm> +#include "LocalModParseTask.h" + +ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); +} + +void ModFolderModel::startWatching() +{ + if(is_watching) + return; + + update(); + + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void ModFolderModel::stopWatching() +{ + if(!is_watching) + return; + + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool ModFolderModel::update() +{ + if (!isValid()) { + return false; + } + if(m_update) { + scheduled_update = true; + return true; + } + + auto task = new ModFolderLoadTask(m_dir); + m_update = task->result(); + QThreadPool *threadPool = QThreadPool::globalInstance(); + connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); + threadPool->start(task); + return true; +} + +void ModFolderModel::finishUpdate() +{ + QSet<QString> currentSet = modsIndex.keys().toSet(); + auto & newMods = m_update->mods; + QSet<QString> newSet = newMods.keys().toSet(); + + // see if the kept mods changed in some way + { + QSet<QString> kept = currentSet; + kept.intersect(newSet); + for(auto & keptMod: kept) { + auto & newMod = newMods[keptMod]; + auto row = modsIndex[keptMod]; + auto & currentMod = mods[row]; + if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) { + // no significant change, ignore... + continue; + } + auto & oldMod = mods[row]; + if(oldMod.isResolving()) { + activeTickets.remove(oldMod.resolutionTicket()); + } + oldMod = newMod; + resolveMod(mods[row]); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove mods no longer present + { + QSet<QString> removed = currentSet; + QList<int> removedRows; + removed.subtract(newSet); + for(auto & removedMod: removed) { + removedRows.append(modsIndex[removedMod]); + } + std::sort(removedRows.begin(), removedRows.end(), std::greater<int>()); + for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) { + int removedIndex = *iter; + beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + auto removedIter = mods.begin() + removedIndex; + if(removedIter->isResolving()) { + activeTickets.remove(removedIter->resolutionTicket()); + } + mods.erase(removedIter); + endRemoveRows(); + } + } + + // add new mods to the end + { + QSet<QString> added = newSet; + added.subtract(currentSet); + beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1); + for(auto & addedMod: added) { + mods.append(newMods[addedMod]); + resolveMod(mods.last()); + } + endInsertRows(); + } + + // update index + { + modsIndex.clear(); + int idx = 0; + for(auto & mod: mods) { + modsIndex[mod.mmc_id()] = idx; + idx++; + } + } + + m_update.reset(); + + emit updateFinished(); + + if(scheduled_update) { + scheduled_update = false; + update(); + } +} + +void ModFolderModel::resolveMod(Mod& m) +{ + if(!m.shouldResolve()) { + return; + } + + auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); + auto result = task->result(); + result->id = m.mmc_id(); + activeTickets.insert(nextResolutionTicket, result); + m.setResolving(true, nextResolutionTicket); + nextResolutionTicket++; + QThreadPool *threadPool = QThreadPool::globalInstance(); + connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse); + threadPool->start(task); +} + +void ModFolderModel::finishModParse(int token) +{ + auto iter = activeTickets.find(token); + if(iter == activeTickets.end()) { + return; + } + auto result = *iter; + activeTickets.remove(token); + int row = modsIndex[result->id]; + auto & mod = mods[row]; + mod.finishResolvingWithDetails(result->details); + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} + +void ModFolderModel::disableInteraction(bool disabled) +{ + if (interaction_disabled == disabled) { + return; + } + interaction_disabled = disabled; + if(size()) { + emit dataChanged(index(0), index(size() - 1)); + } +} + +void ModFolderModel::directoryChanged(QString path) +{ + update(); +} + +bool ModFolderModel::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +// FIXME: this does not take disabled mod (with extra .disable extension) into account... +bool ModFolderModel::installMod(const QString &filename) +{ + if(interaction_disabled) { + return false; + } + + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + auto originalPath = FS::NormalizePath(filename); + QFileInfo fileinfo(originalPath); + + if (!fileinfo.exists() || !fileinfo.isReadable()) + { + qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath; + return false; + } + qDebug() << "installing: " << fileinfo.absoluteFilePath(); + + Mod installedMod(fileinfo); + if (!installedMod.valid()) + { + qDebug() << originalPath << "is not a valid mod. Ignoring it."; + return false; + } + + auto type = installedMod.type(); + if (type == Mod::MOD_UNKNOWN) + { + qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it."; + return false; + } + + auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName())); + if(originalPath == newpath) + { + qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense..."; + return false; + } + + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) + { + if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled"))) + { + if(!QFile::remove(newpath)) + { + // FIXME: report error in a user-visible way + qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed."; + return false; + } + qDebug() << newpath << "has been deleted."; + } + if (!QFile::copy(fileinfo.filePath(), newpath)) + { + qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed."; + // FIXME: report error in a user-visible way + return false; + } + FS::updateTimestamp(newpath); + installedMod.repath(newpath); + update(); + return true; + } + else if (type == Mod::MOD_FOLDER) + { + QString from = fileinfo.filePath(); + if(QFile::exists(newpath)) + { + qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath; + return false; + } + + if (!FS::copy(from, newpath)()) + { + qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed."; + return false; + } + installedMod.repath(newpath); + update(); + return true; + } + return false; +} + +bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable) +{ + if(interaction_disabled) { + return false; + } + + if(indexes.isEmpty()) + return true; + + for (auto index: indexes) + { + if(index.column() != 0) { + continue; + } + setModStatus(index.row(), enable); + } + return true; +} + +bool ModFolderModel::deleteMods(const QModelIndexList& indexes) +{ + if(interaction_disabled) { + return false; + } + + if(indexes.isEmpty()) + return true; + + for (auto i: indexes) + { + Mod &m = mods[i.row()]; + m.destroy(); + } + return true; +} + +int ModFolderModel::columnCount(const QModelIndex &parent) const +{ + return NUM_COLUMNS; +} + +QVariant ModFolderModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= mods.size()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return mods[row].name(); + case VersionColumn: { + switch(mods[row].type()) { + case Mod::MOD_FOLDER: + return tr("Folder"); + case Mod::MOD_SINGLEFILE: + return tr("File"); + default: + break; + } + return mods[row].version(); + } + case DateColumn: + return mods[row].dateTimeChanged(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + return setModStatus(index.row(), Toggle); + } + return false; +} + +bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action) +{ + if(row < 0 || row >= mods.size()) { + return false; + } + + auto &mod = mods[row]; + bool desiredStatus; + switch(action) { + case Enable: + desiredStatus = true; + break; + case Disable: + desiredStatus = false; + break; + case Toggle: + default: + desiredStatus = !mod.enabled(); + break; + } + + if(desiredStatus == mod.enabled()) { + return true; + } + + // preserve the row, but change its ID + auto oldId = mod.mmc_id(); + if(!mod.enable(!mod.enabled())) { + return false; + } + auto newId = mod.mmc_id(); + if(modsIndex.contains(newId)) { + // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled + // But is it necessary? + } + modsIndex.remove(oldId); + modsIndex[newId] = row; + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + return true; +} + +QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + case DateColumn: + return tr("Last changed"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + case DateColumn: + return tr("The date and time this mod was last changed (or added)."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + auto flags = defaultFlags; + if(interaction_disabled) { + flags &= ~Qt::ItemIsDropEnabled; + } + else + { + flags |= Qt::ItemIsDropEnabled; + if(index.isValid()) { + flags |= Qt::ItemIsUserCheckable; + } + } + return flags; +} + +Qt::DropActions ModFolderModel::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList ModFolderModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +{ + if (action == Qt::IgnoreAction) + { + return true; + } + + // check if the action is supported + if (!data || !(action & supportedDropActions())) + { + return false; + } + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + { + continue; + } + // TODO: implement not only copy, but also move + // FIXME: handle errors here + installMod(url.toLocalFile()); + } + return true; + } + return false; +} diff --git a/api/logic/minecraft/SimpleModList.h b/api/logic/minecraft/mod/ModFolderModel.h index d623a295..b0a76121 100644 --- a/api/logic/minecraft/SimpleModList.h +++ b/api/logic/minecraft/mod/ModFolderModel.h @@ -1,4 +1,4 @@ -/* Copyright 2013-2018 MultiMC Contributors +/* 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. @@ -16,13 +16,17 @@ #pragma once #include <QList> +#include <QMap> +#include <QSet> #include <QString> #include <QDir> #include <QAbstractListModel> -#include "minecraft/Mod.h" +#include "Mod.h" #include "multimc_logic_export.h" +#include "ModFolderLoadTask.h" +#include "LocalModParseTask.h" class LegacyInstance; class BaseInstance; @@ -32,7 +36,7 @@ class QFileSystemWatcher; * A legacy mod list. * Backed by a folder. */ -class MULTIMC_LOGIC_EXPORT SimpleModList : public QAbstractListModel +class MULTIMC_LOGIC_EXPORT ModFolderModel : public QAbstractListModel { Q_OBJECT public: @@ -40,11 +44,16 @@ public: { ActiveColumn = 0, NameColumn, - DateColumn, VersionColumn, + DateColumn, NUM_COLUMNS }; - SimpleModList(const QString &dir); + enum ModStatusAction { + Disable, + Enable, + Toggle + }; + ModFolderModel(const QString &dir); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; @@ -59,7 +68,7 @@ public: { return size(); } - ; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; virtual int columnCount(const QModelIndex &parent) const override; @@ -76,9 +85,13 @@ public: { return mods[index]; } + const Mod &at(size_t index) const + { + return mods.at(index); + } /// Reloads the mod list and returns true if the list changed. - virtual bool update(); + bool update(); /** * Adds the given mod to the list at the given index - if the list supports custom ordering @@ -86,15 +99,15 @@ public: bool installMod(const QString& filename); /// Deletes all the selected mods - virtual bool deleteMods(const QModelIndexList &indexes); + bool deleteMods(const QModelIndexList &indexes); /// Enable or disable listed mods - virtual bool enableMods(const QModelIndexList &indexes, bool enable = true); + bool setModStatus(const QModelIndexList &indexes, ModStatusAction action); void startWatching(); void stopWatching(); - virtual bool isValid(); + bool isValid(); QDir dir() { @@ -106,16 +119,31 @@ public: return mods; } +public slots: + void disableInteraction(bool disabled); + private slots: void directoryChanged(QString path); + void finishUpdate(); + void finishModParse(int token); signals: - void changed(); + void updateFinished(); + +private: + void resolveMod(Mod& m); + bool setModStatus(int index, ModStatusAction action); protected: QFileSystemWatcher *m_watcher; bool is_watching = false; + ModFolderLoadTask::ResultPtr m_update; + bool scheduled_update = false; + bool interaction_disabled = false; QDir m_dir; + QMap<QString, int> modsIndex; + QMap<int, LocalModParseTask::ResultPtr> activeTickets; + int nextResolutionTicket = 0; QList<Mod> mods; }; diff --git a/api/logic/minecraft/SimpleModList_test.cpp b/api/logic/minecraft/mod/ModFolderModel_test.cpp index a100b539..76f16ed5 100644 --- a/api/logic/minecraft/SimpleModList_test.cpp +++ b/api/logic/minecraft/mod/ModFolderModel_test.cpp @@ -4,9 +4,9 @@ #include "TestUtil.h" #include "FileSystem.h" -#include "minecraft/SimpleModList.h" +#include "minecraft/mod/ModFolderModel.h" -class SimpleModListTest : public QObject +class ModFolderModelTest : public QObject { Q_OBJECT @@ -32,7 +32,7 @@ slots: { QString folder = source; QTemporaryDir tempDir; - SimpleModList m(tempDir.path()); + ModFolderModel m(tempDir.path()); m.installMod(folder); verify(tempDir.path()); } @@ -41,13 +41,13 @@ slots: { QString folder = source + '/'; QTemporaryDir tempDir; - SimpleModList m(tempDir.path()); + ModFolderModel m(tempDir.path()); m.installMod(folder); verify(tempDir.path()); } } }; -QTEST_GUILESS_MAIN(SimpleModListTest) +QTEST_GUILESS_MAIN(ModFolderModelTest) -#include "SimpleModList_test.moc" +#include "ModFolderModel_test.moc" diff --git a/api/logic/minecraft/mod/ResourcePackFolderModel.cpp b/api/logic/minecraft/mod/ResourcePackFolderModel.cpp new file mode 100644 index 00000000..f3d7f566 --- /dev/null +++ b/api/logic/minecraft/mod/ResourcePackFolderModel.cpp @@ -0,0 +1,23 @@ +#include "ResourcePackFolderModel.h" + +ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { +} + +QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::ToolTipRole) { + switch (section) { + case ActiveColumn: + return tr("Is the resource pack enabled?"); + case NameColumn: + return tr("The name of the resource pack."); + case VersionColumn: + return tr("The version of the resource pack."); + case DateColumn: + return tr("The date and time this resource pack was last changed (or added)."); + default: + return QVariant(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/api/logic/minecraft/mod/ResourcePackFolderModel.h b/api/logic/minecraft/mod/ResourcePackFolderModel.h new file mode 100644 index 00000000..47eb4bb2 --- /dev/null +++ b/api/logic/minecraft/mod/ResourcePackFolderModel.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ModFolderModel.h" + +class MULTIMC_LOGIC_EXPORT ResourcePackFolderModel : public ModFolderModel +{ + Q_OBJECT + +public: + explicit ResourcePackFolderModel(const QString &dir); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/api/logic/minecraft/mod/TexturePackFolderModel.cpp b/api/logic/minecraft/mod/TexturePackFolderModel.cpp new file mode 100644 index 00000000..d5956da1 --- /dev/null +++ b/api/logic/minecraft/mod/TexturePackFolderModel.cpp @@ -0,0 +1,23 @@ +#include "TexturePackFolderModel.h" + +TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { +} + +QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::ToolTipRole) { + switch (section) { + case ActiveColumn: + return tr("Is the texture pack enabled?"); + case NameColumn: + return tr("The name of the texture pack."); + case VersionColumn: + return tr("The version of the texture pack."); + case DateColumn: + return tr("The date and time this texture pack was last changed (or added)."); + default: + return QVariant(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/api/logic/minecraft/mod/TexturePackFolderModel.h b/api/logic/minecraft/mod/TexturePackFolderModel.h new file mode 100644 index 00000000..d773b17b --- /dev/null +++ b/api/logic/minecraft/mod/TexturePackFolderModel.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ModFolderModel.h" + +class MULTIMC_LOGIC_EXPORT TexturePackFolderModel : public ModFolderModel +{ + Q_OBJECT + +public: + explicit TexturePackFolderModel(const QString &dir); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/api/logic/minecraft/services/SkinDelete.cpp b/api/logic/minecraft/services/SkinDelete.cpp new file mode 100644 index 00000000..34977257 --- /dev/null +++ b/api/logic/minecraft/services/SkinDelete.cpp @@ -0,0 +1,42 @@ +#include "SkinDelete.h" +#include <QNetworkRequest> +#include <QHttpMultiPart> +#include <Env.h> + +SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session) + : Task(parent), m_session(session) +{ +} + +void SkinDelete::executeTask() +{ + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().deleteResource(request); + m_reply = std::shared_ptr<QNetworkReply>(rep); + + setStatus(tr("Deleting skin")); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void SkinDelete::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void SkinDelete::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} + diff --git a/api/logic/minecraft/services/SkinDelete.h b/api/logic/minecraft/services/SkinDelete.h new file mode 100644 index 00000000..705ce8ef --- /dev/null +++ b/api/logic/minecraft/services/SkinDelete.h @@ -0,0 +1,30 @@ +#pragma once + +#include <QFile> +#include <QtNetwork/QtNetwork> +#include <memory> +#include <minecraft/auth/AuthSession.h> +#include "tasks/Task.h" +#include "multimc_logic_export.h" + +typedef std::shared_ptr<class SkinDelete> SkinDeletePtr; + +class MULTIMC_LOGIC_EXPORT SkinDelete : public Task +{ + Q_OBJECT +public: + SkinDelete(QObject *parent, AuthSessionPtr session); + virtual ~SkinDelete() = default; + +private: + AuthSessionPtr m_session; + std::shared_ptr<QNetworkReply> m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; + diff --git a/api/logic/minecraft/SkinUpload.cpp b/api/logic/minecraft/services/SkinUpload.cpp index 83bdf592..4e5a1698 100644 --- a/api/logic/minecraft/SkinUpload.cpp +++ b/api/logic/minecraft/services/SkinUpload.cpp @@ -3,15 +3,14 @@ #include <QHttpMultiPart> #include <Env.h> -QByteArray getModelString(SkinUpload::Model model) { +QByteArray getVariant(SkinUpload::Model model) { switch (model) { - case SkinUpload::STEVE: - return ""; - case SkinUpload::ALEX: - return "slim"; default: qDebug() << "Unknown skin type!"; - return ""; + case SkinUpload::STEVE: + return "CLASSIC"; + case SkinUpload::ALEX: + return "SLIM"; } } @@ -22,25 +21,23 @@ SkinUpload::SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, void SkinUpload::executeTask() { - QNetworkRequest request(QUrl(QString("https://api.mojang.com/user/profile/%1/skin").arg(m_session->uuid))); - request.setRawHeader("Authorization", QString("Bearer: %1").arg(m_session->access_token).toLocal8Bit()); - + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - QHttpPart model; - model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"model\"")); - model.setBody(getModelString(m_model)); - QHttpPart skin; skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); - skin.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); + skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); skin.setBody(m_skin); - multiPart->append(model); + QHttpPart model; + model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); + model.setBody(getVariant(m_model)); + multiPart->append(skin); + multiPart->append(model); - QNetworkReply *rep = ENV.qnam().put(request, multiPart); + QNetworkReply *rep = ENV.qnam().post(request, multiPart); m_reply = std::shared_ptr<QNetworkReply>(rep); setStatus(tr("Uploading skin")); diff --git a/api/logic/minecraft/SkinUpload.h b/api/logic/minecraft/services/SkinUpload.h index c77abb03..c77abb03 100644 --- a/api/logic/minecraft/SkinUpload.h +++ b/api/logic/minecraft/services/SkinUpload.h diff --git a/api/logic/minecraft/update/AssetUpdateTask.cpp b/api/logic/minecraft/update/AssetUpdateTask.cpp index 1661822d..e26ab4ef 100644 --- a/api/logic/minecraft/update/AssetUpdateTask.cpp +++ b/api/logic/minecraft/update/AssetUpdateTask.cpp @@ -1,7 +1,7 @@ #include "Env.h" #include "AssetUpdateTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" #include "net/ChecksumValidator.h" #include "minecraft/AssetsUtils.h" @@ -17,7 +17,7 @@ AssetUpdateTask::~AssetUpdateTask() void AssetUpdateTask::executeTask() { setStatus(tr("Updating assets index...")); - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QUrl indexUrl = assets->url; @@ -54,13 +54,13 @@ void AssetUpdateTask::assetIndexFinished() AssetsIndex index; qDebug() << m_inst->name() << ": Finished asset index download"; - auto components = m_inst->getComponentList(); + auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QString asset_fname = "assets/indexes/" + assets->id + ".json"; // FIXME: this looks like a job for a generic validator based on json schema? - if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, &index)) + if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index)) { auto metacache = ENV.metacache(); auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); @@ -101,7 +101,7 @@ bool AssetUpdateTask::abort() } else { - qWarning() << "Prematurely aborted FMLLibrariesTask"; + qWarning() << "Prematurely aborted AssetUpdateTask"; } return true; } diff --git a/api/logic/minecraft/update/FMLLibrariesTask.cpp b/api/logic/minecraft/update/FMLLibrariesTask.cpp index 5b4975ab..a05a7c2a 100644 --- a/api/logic/minecraft/update/FMLLibrariesTask.cpp +++ b/api/logic/minecraft/update/FMLLibrariesTask.cpp @@ -3,7 +3,8 @@ #include <minecraft/VersionFilterData.h> #include "FMLLibrariesTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" +#include "BuildConfig.h" FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance * inst) { @@ -13,7 +14,7 @@ void FMLLibrariesTask::executeTask() { // Get the mod list MinecraftInstance *inst = (MinecraftInstance *)m_inst; - auto components = inst->getComponentList(); + auto components = inst->getPackProfile(); auto profile = components->getProfile(); if (!profile->hasTrait("legacyFML")) @@ -63,8 +64,7 @@ void FMLLibrariesTask::executeTask() for (auto &lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); - QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename - : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename; + QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry)); } diff --git a/api/logic/minecraft/update/LibrariesTask.cpp b/api/logic/minecraft/update/LibrariesTask.cpp index 6dcb149c..7f66a651 100644 --- a/api/logic/minecraft/update/LibrariesTask.cpp +++ b/api/logic/minecraft/update/LibrariesTask.cpp @@ -1,7 +1,7 @@ #include "Env.h" #include "LibrariesTask.h" #include "minecraft/MinecraftInstance.h" -#include "minecraft/ComponentList.h" +#include "minecraft/PackProfile.h" LibrariesTask::LibrariesTask(MinecraftInstance * inst) { @@ -15,51 +15,51 @@ void LibrariesTask::executeTask() MinecraftInstance *inst = (MinecraftInstance *)m_inst; // Build a list of URLs that will need to be downloaded. - auto components = inst->getComponentList(); + auto components = inst->getPackProfile(); auto profile = components->getProfile(); auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name())); downloadJob.reset(job); auto metacache = ENV.metacache(); - QList<LibraryPtr> brokenLocalLibs; - QStringList failedFiles; - auto createJob = [&](const LibraryPtr & lib) - { - if(!lib) - { - emitFailed(tr("Null jar is specified in the metadata, aborting.")); - return; - } - auto dls = lib->getDownloads(currentSystem, metacache.get(), failedFiles, inst->getLocalLibraryPath()); - for(auto dl : dls) - { - downloadJob->addNetAction(dl); - } - }; - auto createJobs = [&](const QList<LibraryPtr> & libs) + + auto processArtifactPool = [&](const QList<LibraryPtr> & pool, QStringList & errors, const QString & localPath) { - for (auto lib : libs) + for (auto lib : pool) { - createJob(lib); + if(!lib) + { + emitFailed(tr("Null jar is specified in the metadata, aborting.")); + return false; + } + auto dls = lib->getDownloads(currentSystem, metacache.get(), errors, localPath); + for(auto dl : dls) + { + downloadJob->addNetAction(dl); + } } + return true; }; - createJobs(profile->getLibraries()); - createJobs(profile->getNativeLibraries()); - createJobs(profile->getJarMods()); - createJob(profile->getMainJar()); - // FIXME: this is never filled!!!! - if (!brokenLocalLibs.empty()) + QStringList failedLocalLibraries; + QList<LibraryPtr> libArtifactPool; + libArtifactPool.append(profile->getLibraries()); + libArtifactPool.append(profile->getNativeLibraries()); + libArtifactPool.append(profile->getMavenFiles()); + libArtifactPool.append(profile->getMainJar()); + processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); + + QStringList failedLocalJarMods; + processArtifactPool(profile->getJarMods(), failedLocalJarMods, inst->jarModsDir()); + + if (!failedLocalJarMods.empty() || !failedLocalLibraries.empty()) { downloadJob.reset(); - QString failed_all = failedFiles.join("\n"); - emitFailed(tr("Some libraries marked as 'local' are missing their jar " - "files:\n%1\n\nYou'll have to correct this problem manually. If this is " - "an externally tracked instance, make sure to run it at least once " - "outside of MultiMC.").arg(failed_all)); + QString failed_all = (failedLocalLibraries + failedLocalJarMods).join("\n"); + emitFailed(tr("Some artifacts marked as 'local' are missing their files:\n%1\n\nYou need to either add the files, or removed the packages that require them.\nYou'll have to correct this problem manually.").arg(failed_all)); return; } + connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); |