diff options
Diffstat (limited to 'api')
48 files changed, 1079 insertions, 135 deletions
diff --git a/api/logic/BaseInstance.cpp b/api/logic/BaseInstance.cpp index d08ceb2b..46b45827 100644 --- a/api/logic/BaseInstance.cpp +++ b/api/logic/BaseInstance.cpp @@ -37,6 +37,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("notes", ""); m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); + m_settings->registerSetting("lastTimePlayed", 0); // Custom Commands auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); @@ -134,15 +135,24 @@ void BaseInstance::setRunning(bool running) m_isRunning = running; + if(!m_settings->get("RecordGameTime").toBool()) + { + emit runningStatusChanged(running); + return; + } + if(running) { m_timeStarted = QDateTime::currentDateTime(); } else { - qint64 current = settings()->get("totalTimePlayed").toLongLong(); QDateTime timeEnded = QDateTime::currentDateTime(); + + qint64 current = settings()->get("totalTimePlayed").toLongLong(); settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded)); + settings()->set("lastTimePlayed", m_timeStarted.secsTo(timeEnded)); + emit propertiesChanged(this); } @@ -160,9 +170,20 @@ int64_t BaseInstance::totalTimePlayed() const return current; } +int64_t BaseInstance::lastTimePlayed() const +{ + if(m_isRunning) + { + QDateTime timeNow = QDateTime::currentDateTime(); + return m_timeStarted.secsTo(timeNow); + } + return settings()->get("lastTimePlayed").toLongLong(); +} + void BaseInstance::resetTimePlayed() { settings()->reset("totalTimePlayed"); + settings()->reset("lastTimePlayed"); } QString BaseInstance::instanceType() const diff --git a/api/logic/BaseInstance.h b/api/logic/BaseInstance.h index bbeabb41..d250e03e 100644 --- a/api/logic/BaseInstance.h +++ b/api/logic/BaseInstance.h @@ -34,6 +34,8 @@ #include "multimc_logic_export.h" +#include "minecraft/launch/MinecraftServerTarget.h" + class QDir; class Task; class LaunchTask; @@ -85,6 +87,7 @@ public: void setRunning(bool running); bool isRunning() const; int64_t totalTimePlayed() const; + int64_t lastTimePlayed() const; void resetTimePlayed(); /// get the type of this instance @@ -145,7 +148,8 @@ public: virtual shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) = 0; /// returns a valid launcher (task container) - virtual shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) = 0; + virtual shared_qobject_ptr<LaunchTask> createLaunchTask( + AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) = 0; /// returns the current launch task (if any) shared_qobject_ptr<LaunchTask> getLaunchTask(); @@ -221,9 +225,9 @@ public: bool reloadSettings(); /** - * 'print' a verbose desription of the instance into a QStringList + * 'print' a verbose description of the instance into a QStringList */ - virtual QStringList verboseDescription(AuthSessionPtr session) = 0; + virtual QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) = 0; Status currentStatus() const; diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index beaa0c00..6d269714 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -124,6 +124,8 @@ set(NET_SOURCES # Game launch logic set(LAUNCH_SOURCES + launch/steps/LookupServerAddress.cpp + launch/steps/LookupServerAddress.h launch/steps/PostLaunchCommand.cpp launch/steps/PostLaunchCommand.h launch/steps/PreLaunchCommand.cpp @@ -236,12 +238,16 @@ set(MINECRAFT_SOURCES minecraft/launch/ExtractNatives.h minecraft/launch/LauncherPartLaunch.cpp minecraft/launch/LauncherPartLaunch.h + minecraft/launch/MinecraftServerTarget.cpp + minecraft/launch/MinecraftServerTarget.h minecraft/launch/PrintInstanceInfo.cpp minecraft/launch/PrintInstanceInfo.h minecraft/launch/ReconstructAssets.cpp minecraft/launch/ReconstructAssets.h minecraft/launch/ScanModFolders.cpp minecraft/launch/ScanModFolders.h + minecraft/launch/VerifyJavaInstall.cpp + minecraft/launch/VerifyJavaInstall.h minecraft/legacy/LegacyModList.h minecraft/legacy/LegacyModList.cpp @@ -298,6 +304,10 @@ set(MINECRAFT_SOURCES minecraft/mod/ModFolderLoadTask.cpp minecraft/mod/LocalModParseTask.h minecraft/mod/LocalModParseTask.cpp + minecraft/mod/ResourcePackFolderModel.h + minecraft/mod/ResourcePackFolderModel.cpp + minecraft/mod/TexturePackFolderModel.h + minecraft/mod/TexturePackFolderModel.cpp # Assets minecraft/AssetsUtils.h @@ -540,7 +550,7 @@ set_target_properties(MultiMC_logic PROPERTIES CXX_VISIBILITY_PRESET hidden VISI generate_export_header(MultiMC_logic) # Link -target_link_libraries(MultiMC_logic systeminfo MultiMC_quazip MultiMC_classparser ${NBT_NAME} ${ZLIB_LIBRARIES} optional-bare BuildConfig) +target_link_libraries(MultiMC_logic systeminfo MultiMC_quazip MultiMC_classparser ${NBT_NAME} ${ZLIB_LIBRARIES} optional-bare tomlc99 BuildConfig) target_link_libraries(MultiMC_logic Qt5::Core Qt5::Xml Qt5::Network Qt5::Concurrent) # Mark and export headers diff --git a/api/logic/InstanceImportTask.cpp b/api/logic/InstanceImportTask.cpp index fe2cdd75..3eac4d57 100644 --- a/api/logic/InstanceImportTask.cpp +++ b/api/logic/InstanceImportTask.cpp @@ -238,6 +238,7 @@ void InstanceImportTask::processFlame() } QString forgeVersion; + QString fabricVersion; for(auto &loader: pack.minecraft.modLoaders) { auto id = loader.id; @@ -247,6 +248,12 @@ void InstanceImportTask::processFlame() forgeVersion = id; continue; } + if(id.startsWith("fabric-")) + { + id.remove("fabric-"); + fabricVersion = id; + continue; + } logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); } @@ -281,6 +288,10 @@ void InstanceImportTask::processFlame() } components->setComponentVersion("net.minecraftforge", forgeVersion); } + if(!fabricVersion.isEmpty()) + { + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); + } if (m_instIcon != "default") { instance.setIconKey(m_instIcon); diff --git a/api/logic/NullInstance.h b/api/logic/NullInstance.h index e9ba1a13..94ed6c3a 100644 --- a/api/logic/NullInstance.h +++ b/api/logic/NullInstance.h @@ -27,7 +27,7 @@ public: { return instanceRoot(); }; - shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr) override + shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override { return nullptr; } @@ -67,7 +67,7 @@ public: { return false; } - QStringList verboseDescription(AuthSessionPtr session) override + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override { QStringList out; out << "Null instance - placeholder."; diff --git a/api/logic/java/JavaUtils.cpp b/api/logic/java/JavaUtils.cpp index 647711e5..4b231e6a 100644 --- a/api/logic/java/JavaUtils.cpp +++ b/api/logic/java/JavaUtils.cpp @@ -150,7 +150,7 @@ JavaInstallPtr JavaUtils::GetDefaultJava() } #if defined(Q_OS_WIN32) -QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName) +QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix) { QList<JavaInstallPtr> javas; @@ -175,8 +175,6 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz); } - QString recommended = value; - TCHAR subKeyName[255]; DWORD subKeyNameSize, numSubKeys, retCode; @@ -195,7 +193,7 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString if (retCode == ERROR_SUCCESS) { // Now open the registry key for the version that we just got. - QString newKeyName = keyName + "\\" + subKeyName; + QString newKeyName = keyName + "\\" + subKeyName + subkeySuffix; HKEY newKey; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, newKeyName.toStdString().c_str(), 0, @@ -204,11 +202,11 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString // Read the JavaHome value to find where Java is installed. value = new char[0]; valueSz = 0; - if (RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value, + if (RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, &valueSz) == ERROR_MORE_DATA) { value = new char[valueSz]; - RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value, + RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, &valueSz); // Now, we construct the version object and add it to the list. @@ -237,25 +235,78 @@ QList<QString> JavaUtils::FindJavaPaths() { QList<JavaInstallPtr> java_candidates; + // Oracle QList<JavaInstallPtr> JRE64s = this->FindJavaFromRegistryKey( - KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList<JavaInstallPtr> JDK64s = this->FindJavaFromRegistryKey( - KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); QList<JavaInstallPtr> JRE32s = this->FindJavaFromRegistryKey( - KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList<JavaInstallPtr> JDK32s = this->FindJavaFromRegistryKey( - KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); - + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); + + // Oracle for Java 9 and newer + QList<JavaInstallPtr> NEWJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); + QList<JavaInstallPtr> NEWJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); + QList<JavaInstallPtr> NEWJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); + QList<JavaInstallPtr> NEWJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); + + // AdoptOpenJDK + QList<JavaInstallPtr> ADOPTOPENJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); + QList<JavaInstallPtr> ADOPTOPENJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); + QList<JavaInstallPtr> ADOPTOPENJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); + QList<JavaInstallPtr> ADOPTOPENJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); + + // Microsoft + QList<JavaInstallPtr> MICROSOFTJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); + + // Azul Zulu + QList<JavaInstallPtr> ZULU64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); + QList<JavaInstallPtr> ZULU32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); + + // BellSoft Liberica + QList<JavaInstallPtr> LIBERICA64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); + QList<JavaInstallPtr> LIBERICA32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); + + // List x64 before x86 java_candidates.append(JRE64s); + java_candidates.append(NEWJRE64s); + java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK64s); + java_candidates.append(NEWJDK64s); + java_candidates.append(ADOPTOPENJDK64s); + java_candidates.append(MICROSOFTJDK64s); + java_candidates.append(ZULU64s); + java_candidates.append(LIBERICA64s); + java_candidates.append(JRE32s); + java_candidates.append(NEWJRE32s); + java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK32s); + java_candidates.append(NEWJDK32s); + java_candidates.append(ADOPTOPENJDK32s); + java_candidates.append(ZULU32s); + java_candidates.append(LIBERICA32s); + java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); QList<QString> candidates; @@ -330,6 +381,9 @@ QList<QString> JavaUtils::FindJavaPaths() scanJavaDir("/usr/lib32/jvm"); // javas stored in MultiMC's folder scanJavaDir("java"); + // manually installed JDKs in /opt + scanJavaDir("/opt/jdk"); + scanJavaDir("/opt/jdks"); return javas; } #else diff --git a/api/logic/java/JavaUtils.h b/api/logic/java/JavaUtils.h index 7474c494..206acf89 100644 --- a/api/logic/java/JavaUtils.h +++ b/api/logic/java/JavaUtils.h @@ -39,6 +39,6 @@ public: JavaInstallPtr GetDefaultJava(); #ifdef Q_OS_WIN - QList<JavaInstallPtr> FindJavaFromRegistryKey(DWORD keyType, QString keyName); + QList<JavaInstallPtr> FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix = ""); #endif }; diff --git a/api/logic/launch/steps/LookupServerAddress.cpp b/api/logic/launch/steps/LookupServerAddress.cpp new file mode 100644 index 00000000..de56c28a --- /dev/null +++ b/api/logic/launch/steps/LookupServerAddress.cpp @@ -0,0 +1,95 @@ +/* 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 "LookupServerAddress.h" + +#include <launch/LaunchTask.h> + +LookupServerAddress::LookupServerAddress(LaunchTask *parent) : + LaunchStep(parent), m_dnsLookup(new QDnsLookup(this)) +{ + connect(m_dnsLookup, &QDnsLookup::finished, this, &LookupServerAddress::on_dnsLookupFinished); + + m_dnsLookup->setType(QDnsLookup::SRV); +} + +void LookupServerAddress::setLookupAddress(const QString &lookupAddress) +{ + m_lookupAddress = lookupAddress; + m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress)); +} + +void LookupServerAddress::setOutputAddressPtr(MinecraftServerTargetPtr output) +{ + m_output = std::move(output); +} + +bool LookupServerAddress::abort() +{ + m_dnsLookup->abort(); + emitFailed("Aborted"); + return true; +} + +void LookupServerAddress::executeTask() +{ + m_dnsLookup->lookup(); +} + +void LookupServerAddress::on_dnsLookupFinished() +{ + if (isFinished()) + { + // Aborted + return; + } + + if (m_dnsLookup->error() != QDnsLookup::NoError) + { + emit logLine(QString("Failed to resolve server address (this is NOT an error!) %1: %2\n") + .arg(m_dnsLookup->name(), m_dnsLookup->errorString()), MessageLevel::MultiMC); + resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch + // and leave it up to minecraft to fail (or maybe not) when connecting + return; + } + + const auto records = m_dnsLookup->serviceRecords(); + if (records.empty()) + { + emit logLine( + QString("Failed to resolve server address %1: the DNS lookup succeeded, but no records were returned.\n") + .arg(m_dnsLookup->name()), MessageLevel::Warning); + resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch + // and leave it up to minecraft to fail (or maybe not) when connecting + return; + } + + const auto &firstRecord = records.at(0); + quint16 port = firstRecord.port(); + + emit logLine(QString("Resolved server address %1 to %2 with port %3\n").arg( + m_dnsLookup->name(), firstRecord.target(), QString::number(port)),MessageLevel::MultiMC); + resolve(firstRecord.target(), port); +} + +void LookupServerAddress::resolve(const QString &address, quint16 port) +{ + m_output->address = address; + m_output->port = port; + + emitSucceeded(); + m_dnsLookup->deleteLater(); +} diff --git a/api/logic/launch/steps/LookupServerAddress.h b/api/logic/launch/steps/LookupServerAddress.h new file mode 100644 index 00000000..5a5c3de1 --- /dev/null +++ b/api/logic/launch/steps/LookupServerAddress.h @@ -0,0 +1,49 @@ +/* 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 <QObjectPtr.h> +#include <QDnsLookup> + +#include "minecraft/launch/MinecraftServerTarget.h" + +class LookupServerAddress: public LaunchStep { +Q_OBJECT +public: + explicit LookupServerAddress(LaunchTask *parent); + virtual ~LookupServerAddress() {}; + + virtual void executeTask(); + virtual bool abort(); + virtual bool canAbort() const + { + return true; + } + + void setLookupAddress(const QString &lookupAddress); + void setOutputAddressPtr(MinecraftServerTargetPtr output); + +private slots: + void on_dnsLookupFinished(); + +private: + void resolve(const QString &address, quint16 port); + + QDnsLookup *m_dnsLookup; + QString m_lookupAddress; + MinecraftServerTargetPtr m_output; +}; diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp index 37cdece7..dbf9f816 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -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" @@ -22,12 +23,15 @@ #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 "mod/ModFolderModel.h" +#include "mod/ResourcePackFolderModel.h" +#include "mod/TexturePackFolderModel.h" #include "WorldList.h" #include "icons/IIconList.h" @@ -106,6 +110,15 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO 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", ""); @@ -390,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(); @@ -399,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) @@ -435,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; @@ -456,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"; } @@ -507,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() << ""; @@ -622,7 +651,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } - auto params = processMinecraftArgs(nullptr); + auto params = processMinecraftArgs(nullptr, serverToJoin); out << "Params:"; out << " " + params.join(' '); out << ""; @@ -769,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()) { @@ -796,7 +831,7 @@ shared_qobject_ptr<Task> MinecraftInstance::createUpdateTask(Net::Mode mode) return nullptr; } -shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session) +shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { // FIXME: get rid of shared_from_this ... auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this())); @@ -828,6 +863,21 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt 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()) { @@ -859,7 +909,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt // print some instance info here... { - process->appendStep(new PrintInstanceInfo(pptr, session)); + process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin)); } // extract native jars if needed @@ -872,6 +922,11 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(new ReconstructAssets(pptr)); } + // verify that minimum Java requirements are met + { + process->appendStep(new VerifyJavaInstall(pptr)); + } + { // actually launch the game auto method = launchMethod(); @@ -880,6 +935,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt auto step = new LauncherPartLaunch(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); + step->setServerToJoin(serverToJoin); process->appendStep(step); } else if (method == "DirectJava") @@ -887,6 +943,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt auto step = new DirectJavaLaunch(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); + step->setServerToJoin(serverToJoin); process->appendStep(step); } } @@ -943,7 +1000,7 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new ModFolderModel(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); } @@ -954,7 +1011,7 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new ModFolderModel(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); } diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h index 985a8a76..05600797 100644 --- a/api/logic/minecraft/MinecraftInstance.h +++ b/api/logic/minecraft/MinecraftInstance.h @@ -5,6 +5,7 @@ #include <QProcess> #include <QDir> #include "multimc_logic_export.h" +#include "minecraft/launch/MinecraftServerTarget.h" class ModFolderModel; class WorldList; @@ -76,11 +77,11 @@ public: ////// Launch stuff ////// shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) override; - shared_qobject_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,7 +108,7 @@ 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; 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/launch/CreateGameFolders.cpp b/api/logic/minecraft/launch/CreateGameFolders.cpp index 415b7e23..4081e72e 100644 --- a/api/logic/minecraft/launch/CreateGameFolders.cpp +++ b/api/logic/minecraft/launch/CreateGameFolders.cpp @@ -15,7 +15,7 @@ void CreateGameFolders::executeTask() if(!FS::ensureFolderPathExists(minecraftInstance->gameRoot())) { emit logLine("Couldn't create the main game folder", MessageLevel::Error); - emitFailed("Couldn't create the main game folder"); + emitFailed(tr("Couldn't create the main game folder")); return; } diff --git a/api/logic/minecraft/launch/DirectJavaLaunch.cpp b/api/logic/minecraft/launch/DirectJavaLaunch.cpp index d9841a43..2110384f 100644 --- a/api/logic/minecraft/launch/DirectJavaLaunch.cpp +++ b/api/logic/minecraft/launch/DirectJavaLaunch.cpp @@ -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 d9d9bdc2..58b119b8 100644 --- a/api/logic/minecraft/launch/DirectJavaLaunch.h +++ b/api/logic/minecraft/launch/DirectJavaLaunch.h @@ -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 d41cb8fd..d57499aa 100644 --- a/api/logic/minecraft/launch/ExtractNatives.cpp +++ b/api/logic/minecraft/launch/ExtractNatives.cpp @@ -94,9 +94,9 @@ void ExtractNatives::executeTask() { 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/LauncherPartLaunch.cpp b/api/logic/minecraft/launch/LauncherPartLaunch.cpp index 1408e6ad..ee469770 100644 --- a/api/logic/minecraft/launch/LauncherPartLaunch.cpp +++ b/api/logic/minecraft/launch/LauncherPartLaunch.cpp @@ -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 9e951849..6a7ee0e5 100644 --- a/api/logic/minecraft/launch/LauncherPartLaunch.h +++ b/api/logic/minecraft/launch/LauncherPartLaunch.h @@ -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/PrintInstanceInfo.cpp b/api/logic/minecraft/launch/PrintInstanceInfo.cpp index af0b5bbf..0b9611ad 100644 --- a/api/logic/minecraft/launch/PrintInstanceInfo.cpp +++ b/api/logic/minecraft/launch/PrintInstanceInfo.cpp @@ -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 168eff9d..fdc30f31 100644 --- a/api/logic/minecraft/launch/PrintInstanceInfo.h +++ b/api/logic/minecraft/launch/PrintInstanceInfo.h @@ -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/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 f86e5d05..9f9bda5a 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.cpp +++ b/api/logic/minecraft/legacy/LegacyInstance.cpp @@ -225,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 9caddcba..325bac7a 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.h +++ b/api/logic/minecraft/legacy/LegacyInstance.h @@ -111,7 +111,8 @@ public: { return false; } - shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override + shared_qobject_ptr<LaunchTask> createLaunchTask( + AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override { return nullptr; } @@ -125,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/mod/LocalModParseTask.cpp b/api/logic/minecraft/mod/LocalModParseTask.cpp index 22ebd7d4..0d6972fb 100644 --- a/api/logic/minecraft/mod/LocalModParseTask.cpp +++ b/api/logic/minecraft/mod/LocalModParseTask.cpp @@ -6,6 +6,7 @@ #include <QJsonValue> #include <quazip.h> #include <quazipfile.h> +#include <toml.h> #include "settings/INIFile.h" #include "FileSystem.h" @@ -90,6 +91,124 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents) 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) { @@ -198,7 +317,57 @@ void LocalModParseTask::processAsZip() QuaZipFile file(&zip); - if (zip.setCurrentFile("mcmod.info")) + 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)) { 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/update/FMLLibrariesTask.cpp b/api/logic/minecraft/update/FMLLibrariesTask.cpp index 85993e81..a05a7c2a 100644 --- a/api/logic/minecraft/update/FMLLibrariesTask.cpp +++ b/api/logic/minecraft/update/FMLLibrariesTask.cpp @@ -64,7 +64,7 @@ void FMLLibrariesTask::executeTask() for (auto &lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); - QString urlString = (lib.ours ? BuildConfig.FMLLIBS_OUR_BASE_URL : BuildConfig.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/modplatform/atlauncher/ATLPackInstallTask.cpp b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp index 12ceaccd..55087a27 100644 --- a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -4,6 +4,7 @@ #include <MMCZip.h> #include <minecraft/OneSixVersionFormat.h> #include <Version.h> +#include <net/ChecksumValidator.h> #include "ATLPackInstallTask.h" #include "BuildConfig.h" @@ -18,15 +19,20 @@ namespace ATLauncher { -PackInstallTask::PackInstallTask(QString pack, QString version) +PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) { + m_support = support; m_pack = pack; m_version_name = version; } bool PackInstallTask::abort() { - return true; + if(abortable) + { + return jobPtr->abort(); + } + return false; } void PackInstallTask::executeTask() @@ -154,23 +160,56 @@ QString PackInstallTask::getVersionForLoader(QString uid) auto vlist = ENV.metadataIndex()->get(uid); if(!vlist) { - emitFailed(tr("Failed to get local metadata index for ") + uid); + emitFailed(tr("Failed to get local metadata index for %1").arg(uid)); return Q_NULLPTR; } - // todo: filter by Minecraft version - - if(m_version.loader.recommended) { - return vlist.get()->getRecommended().get()->descriptor(); + if(!vlist->isLoaded()) { + vlist->load(Net::Mode::Online); } - else if(m_version.loader.latest) { - return vlist.get()->at(0)->descriptor(); + + if(m_version.loader.recommended || m_version.loader.latest) { + for (int i = 0; i < vlist->versions().size(); i++) { + auto version = vlist->versions().at(i); + auto reqs = version->requires(); + + // filter by minecraft version, if the loader depends on a certain version. + // not all mod loaders depend on a given Minecraft version, so we won't do this + // filtering for those loaders. + if (m_version.loader.type != "fabric") { + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) { + return req.uid == "net.minecraft"; + }); + if (iter == reqs.end()) continue; + if (iter->equalsVersion != m_version.minecraft) continue; + } + + if (m_version.loader.recommended) { + // first recommended build we find, we use. + if (!version->isRecommended()) continue; + } + + return version->descriptor(); + } + + emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); + return Q_NULLPTR; } else if(m_version.loader.choose) { - // todo: implement + // Fabric Loader doesn't depend on a given Minecraft version. + if (m_version.loader.type == "fabric") { + return m_support->chooseVersion(vlist, Q_NULLPTR); + } + + return m_support->chooseVersion(vlist, m_version.minecraft); } } + if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) { + emitFailed(tr("No loader version set for modpack!")); + return Q_NULLPTR; + } + return m_version.loader.version; } @@ -373,21 +412,29 @@ void PackInstallTask::installConfigs() auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", path); entry->setStale(true); - jobPtr->addNetAction(Net::Download::makeCached(url, entry)); + auto dl = Net::Download::makeCached(url, entry); + if (!m_version.configs.sha1.isEmpty()) { + auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + } + jobPtr->addNetAction(dl); archivePath = entry->getFullPath(); connect(jobPtr.get(), &NetJob::succeeded, this, [&]() { + abortable = false; jobPtr.reset(); extractConfigs(); }); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { + abortable = false; jobPtr.reset(); emitFailed(reason); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + abortable = true; setProgress(current, total); }); @@ -423,13 +470,31 @@ void PackInstallTask::extractConfigs() void PackInstallTask::downloadMods() { qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); + + QVector<ATLauncher::VersionMod> optionalMods; + for (const auto& mod : m_version.mods) { + if (mod.optional) { + optionalMods.push_back(mod); + } + } + + // Select optional mods, if pack contains any + QVector<QString> selectedMods; + if (!optionalMods.isEmpty()) { + setStatus(tr("Selecting optional mods...")); + selectedMods = m_support->chooseOptionalMods(optionalMods); + } + setStatus(tr("Downloading mods...")); jarmods.clear(); jobPtr.reset(new NetJob(tr("Mod download"))); for(const auto& mod : m_version.mods) { - // skip optional mods for now - if(mod.optional) continue; + // skip non-client mods + if(!mod.client) continue; + + // skip optional mods that were not selected + if(mod.optional && !selectedMods.contains(mod.name)) continue; QString url; switch(mod.download) { @@ -451,12 +516,15 @@ void PackInstallTask::downloadMods() auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { - auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); modsToExtract.insert(entry->getFullPath(), mod); auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } jobPtr->addNetAction(dl); } else if(mod.type == ModType::Decomp) { @@ -465,6 +533,10 @@ void PackInstallTask::downloadMods() modsToDecomp.insert(entry->getFullPath(), mod); auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } jobPtr->addNetAction(dl); } else { @@ -475,6 +547,10 @@ void PackInstallTask::downloadMods() entry->setStale(true); auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } jobPtr->addNetAction(dl); auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); @@ -507,11 +583,13 @@ void PackInstallTask::downloadMods() connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { + abortable = false; jobPtr.reset(); emitFailed(reason); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + abortable = true; setProgress(current, total); }); @@ -519,10 +597,12 @@ void PackInstallTask::downloadMods() } void PackInstallTask::onModsDownloaded() { + abortable = false; + qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId(); jobPtr.reset(); - if(modsToExtract.size() || modsToDecomp.size() || modsToCopy.size()) { + if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onModsExtracted); connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]() @@ -629,6 +709,7 @@ void PackInstallTask::install() // Use a component to add libraries BEFORE Minecraft if(!createLibrariesComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create libraries component")); return; } @@ -666,6 +747,7 @@ void PackInstallTask::install() // Use a component to fill in the rest of the data // todo: use more detection if(!createPackComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create pack component")); return; } diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h index 78544bab..15fd9b32 100644 --- a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h @@ -15,14 +15,31 @@ namespace ATLauncher { +class MULTIMC_LOGIC_EXPORT UserInteractionSupport { + +public: + /** + * Requests a user interaction to select which optional mods should be installed. + */ + virtual QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) = 0; + + /** + * Requests a user interaction to select a component version from a given version list + * and constrained to a given Minecraft version. + */ + virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0; + +}; + class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask { Q_OBJECT public: - explicit PackInstallTask(QString pack, QString version); + explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version); virtual ~PackInstallTask(){} + bool canAbort() const override { return true; } bool abort() override; protected: @@ -54,6 +71,10 @@ private: void install(); private: + UserInteractionSupport *m_support; + + bool abortable = false; + NetJobPtr jobPtr; QByteArray response; @@ -76,9 +97,6 @@ private: QFuture<bool> m_modExtractFuture; QFutureWatcher<bool> m_modExtractFutureWatcher; - QFuture<bool> m_decompFuture; - QFutureWatcher<bool> m_decompFutureWatcher; - }; } diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp index 84389330..e25d8346 100644 --- a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp +++ b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp @@ -81,12 +81,21 @@ static ATLauncher::ModType parseModType(QString rawType) { static void loadVersionLoader(ATLauncher::VersionLoader & p, QJsonObject & obj) { p.type = Json::requireString(obj, "type"); - p.latest = Json::ensureBoolean(obj, QString("latest"), false); p.choose = Json::ensureBoolean(obj, QString("choose"), false); - p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); auto metadata = Json::requireObject(obj, "metadata"); - p.version = Json::requireString(metadata, "version"); + p.latest = Json::ensureBoolean(metadata, QString("latest"), false); + p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false); + + // Minecraft Forge + if (p.type == "forge") { + p.version = Json::ensureString(metadata, "version", ""); + } + + // Fabric Loader + if (p.type == "fabric") { + p.version = Json::ensureString(metadata, "loader", ""); + } } static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj) { @@ -100,6 +109,11 @@ static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj p.server = Json::ensureString(obj, "server", ""); } +static void loadVersionConfigs(ATLauncher::VersionConfigs & p, QJsonObject & obj) { + p.filesize = Json::requireInteger(obj, "filesize"); + p.sha1 = Json::requireString(obj, "sha1"); +} + static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.name = Json::requireString(obj, "name"); p.version = Json::requireString(obj, "version"); @@ -134,7 +148,24 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.decompFile = Json::requireString(obj, "decompFile"); } + p.description = Json::ensureString(obj, QString("description"), ""); p.optional = Json::ensureBoolean(obj, QString("optional"), false); + p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); + p.selected = Json::ensureBoolean(obj, QString("selected"), false); + p.hidden = Json::ensureBoolean(obj, QString("hidden"), false); + p.library = Json::ensureBoolean(obj, QString("library"), false); + p.group = Json::ensureString(obj, QString("group"), ""); + if(obj.contains("depends")) { + auto dependsArr = Json::requireArray(obj, "depends"); + for (const auto depends : dependsArr) { + p.depends.append(Json::requireString(depends)); + } + } + + p.client = Json::ensureBoolean(obj, QString("client"), false); + + // computed + p.effectively_hidden = p.hidden || p.library; } void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) @@ -169,12 +200,19 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) } } - auto mods = Json::requireArray(obj, "mods"); - for (const auto modRaw : mods) - { - auto modObj = Json::requireObject(modRaw); - ATLauncher::VersionMod mod; - loadVersionMod(mod, modObj); - v.mods.append(mod); + if(obj.contains("mods")) { + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) + { + auto modObj = Json::requireObject(modRaw); + ATLauncher::VersionMod mod; + loadVersionMod(mod, modObj); + v.mods.append(mod); + } + } + + if(obj.contains("configs")) { + auto configsObj = Json::requireObject(obj, "configs"); + loadVersionConfigs(v.configs, configsObj); } } diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.h b/api/logic/modplatform/atlauncher/ATLPackManifest.h index 1adf889b..17821e4c 100644 --- a/api/logic/modplatform/atlauncher/ATLPackManifest.h +++ b/api/logic/modplatform/atlauncher/ATLPackManifest.h @@ -86,7 +86,25 @@ struct VersionMod QString decompType_raw; QString decompFile; + QString description; bool optional; + bool recommended; + bool selected; + bool hidden; + bool library; + QString group; + QVector<QString> depends; + + bool client; + + // computed + bool effectively_hidden; +}; + +struct VersionConfigs +{ + int filesize; + QString sha1; }; struct PackVersion @@ -100,6 +118,7 @@ struct PackVersion VersionLoader loader; QVector<VersionLibrary> libraries; QVector<VersionMod> mods; + VersionConfigs configs; }; MULTIMC_LOGIC_EXPORT void loadVersion(PackVersion & v, QJsonObject & obj); diff --git a/api/logic/modplatform/legacy_ftb/PackInstallTask.h b/api/logic/modplatform/legacy_ftb/PackInstallTask.h index 7868d1c4..f3515781 100644 --- a/api/logic/modplatform/legacy_ftb/PackInstallTask.h +++ b/api/logic/modplatform/legacy_ftb/PackInstallTask.h @@ -20,6 +20,7 @@ public: explicit PackInstallTask(Modpack pack, QString version); virtual ~PackInstallTask(){} + bool canAbort() const override { return true; } bool abort() override; protected: diff --git a/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp b/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp index 068e3592..f22373bc 100644 --- a/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,10 +1,12 @@ #include "FTBPackInstallTask.h" #include "BuildConfig.h" +#include "Env.h" #include "FileSystem.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" namespace ModpacksCH { @@ -17,7 +19,11 @@ PackInstallTask::PackInstallTask(Modpack pack, QString version) bool PackInstallTask::abort() { - return true; + if(abortable) + { + return jobPtr->abort(); + } + return false; } void PackInstallTask::executeTask() @@ -93,31 +99,41 @@ void PackInstallTask::downloadPack() for(auto file : m_version.files) { if(file.serverOnly) continue; + QFileInfo fileName(file.name); + auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix(); + + auto entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", cacheName); + entry->setStale(true); + auto relpath = FS::PathCombine("minecraft", file.path, file.name); auto path = FS::PathCombine(m_stagingPath, relpath); qDebug() << "Will download" << file.url << "to" << path; - auto dl = Net::Download::makeFile(file.url, path); + filesToCopy[entry->getFullPath()] = path; + + auto dl = Net::Download::makeCached(file.url, entry); + if (!file.sha1.isEmpty()) { + auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + } jobPtr->addNetAction(dl); } connect(jobPtr.get(), &NetJob::succeeded, this, [&]() { + abortable = false; jobPtr.reset(); install(); }); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { + abortable = false; jobPtr.reset(); - - // FIXME: Temporarily ignore file download failures (matching FTB's installer), - // while FTB's data is fucked. - qWarning() << "Failed to download files for modpack: " + reason; - - install(); + emitFailed(reason); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + abortable = true; setProgress(current, total); }); @@ -126,6 +142,19 @@ void PackInstallTask::downloadPack() void PackInstallTask::install() { + setStatus(tr("Copying modpack files")); + + for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) { + auto &from = iter.key(); + auto &to = iter.value(); + FS::copy fileCopyOperation(from, to); + if(!fileCopyOperation()) { + qWarning() << "Failed to copy" << from << "to" << to; + emitFailed(tr("Failed to copy files")); + return; + } + } + setStatus(tr("Installing modpack")); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); diff --git a/api/logic/modplatform/modpacksch/FTBPackInstallTask.h b/api/logic/modplatform/modpacksch/FTBPackInstallTask.h index 4f7786fd..55db3d3c 100644 --- a/api/logic/modplatform/modpacksch/FTBPackInstallTask.h +++ b/api/logic/modplatform/modpacksch/FTBPackInstallTask.h @@ -16,6 +16,7 @@ public: explicit PackInstallTask(Modpack pack, QString version); virtual ~PackInstallTask(){} + bool canAbort() const override { return true; } bool abort() override; protected: @@ -30,6 +31,8 @@ private: void install(); private: + bool abortable = false; + NetJobPtr jobPtr; QByteArray response; @@ -37,6 +40,8 @@ private: QString m_version_name; Version m_version; + QMap<QString, QString> filesToCopy; + }; } diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp index 96e1804d..dbce8e53 100644 --- a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp @@ -28,6 +28,14 @@ Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUr m_minecraftVersion = minecraftVersion; } +bool Technic::SingleZipPackInstallTask::abort() { + if(m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + void Technic::SingleZipPackInstallTask::executeTask() { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); @@ -47,6 +55,8 @@ void Technic::SingleZipPackInstallTask::executeTask() void Technic::SingleZipPackInstallTask::downloadSucceeded() { + m_abortable = false; + setStatus(tr("Extracting modpack")); QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft")); qDebug() << "Attempting to create instance from" << m_archivePath; @@ -67,12 +77,14 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded() void Technic::SingleZipPackInstallTask::downloadFailed(QString reason) { + m_abortable = false; emitFailed(reason); m_filesNetJob.reset(); } void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) { + m_abortable = true; setProgress(current / 2, total); } diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.h b/api/logic/modplatform/technic/SingleZipPackInstallTask.h index c56b9e46..ec2ff605 100644 --- a/api/logic/modplatform/technic/SingleZipPackInstallTask.h +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.h @@ -36,6 +36,9 @@ class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask public: SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + bool canAbort() const override { return true; } + bool abort() override; + protected: void executeTask() override; @@ -48,6 +51,8 @@ private slots: void extractAborted(); private: + bool m_abortable = false; + QUrl m_sourceUrl; QString m_minecraftVersion; QString m_archivePath; diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.cpp b/api/logic/modplatform/technic/SolderPackInstallTask.cpp index 1d17073c..1b4186d4 100644 --- a/api/logic/modplatform/technic/SolderPackInstallTask.cpp +++ b/api/logic/modplatform/technic/SolderPackInstallTask.cpp @@ -27,6 +27,14 @@ Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, con m_minecraftVersion = minecraftVersion; } +bool Technic::SolderPackInstallTask::abort() { + if(m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + void Technic::SolderPackInstallTask::executeTask() { setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString())); @@ -106,6 +114,8 @@ void Technic::SolderPackInstallTask::fileListSucceeded() void Technic::SolderPackInstallTask::downloadSucceeded() { + m_abortable = false; + setStatus(tr("Extracting modpack")); m_filesNetJob.reset(); m_extractFuture = QtConcurrent::run([this]() @@ -132,12 +142,14 @@ void Technic::SolderPackInstallTask::downloadSucceeded() void Technic::SolderPackInstallTask::downloadFailed(QString reason) { + m_abortable = false; emitFailed(reason); m_filesNetJob.reset(); } void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) { + m_abortable = true; setProgress(current / 2, total); } diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.h b/api/logic/modplatform/technic/SolderPackInstallTask.h index 0fe6cb83..9f0f20a9 100644 --- a/api/logic/modplatform/technic/SolderPackInstallTask.h +++ b/api/logic/modplatform/technic/SolderPackInstallTask.h @@ -29,6 +29,9 @@ namespace Technic public: explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + bool canAbort() const override { return true; } + bool abort() override; + protected: //! Entry point for tasks. virtual void executeTask() override; @@ -43,6 +46,8 @@ namespace Technic void extractAborted(); private: + bool m_abortable = false; + NetJobPtr m_filesNetJob; QUrl m_sourceUrl; QString m_minecraftVersion; diff --git a/api/logic/net/Download.cpp b/api/logic/net/Download.cpp index 340f8657..3f183b7d 100644 --- a/api/logic/net/Download.cpp +++ b/api/logic/net/Download.cpp @@ -15,6 +15,7 @@ #include "Download.h" +#include "BuildConfig.h" #include <QFileInfo> #include <QDateTime> #include <QDebug> @@ -94,7 +95,7 @@ void Download::start() return; } - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); QNetworkReply *rep = ENV.qnam().get(request); diff --git a/api/logic/net/PasteUpload.cpp b/api/logic/net/PasteUpload.cpp index 3526e207..cb470c49 100644 --- a/api/logic/net/PasteUpload.cpp +++ b/api/logic/net/PasteUpload.cpp @@ -5,6 +5,7 @@ #include <QJsonArray> #include <QJsonDocument> #include <QFile> +#include <BuildConfig.h> PasteUpload::PasteUpload(QWidget *window, QString text, QString key) : m_window(window) { @@ -34,7 +35,7 @@ bool PasteUpload::validateText() void PasteUpload::executeTask() { QNetworkRequest request(QUrl("https://api.paste.ee/v1/pastes")); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setRawHeader("Content-Type", "application/json"); request.setRawHeader("Content-Length", QByteArray::number(m_jsonContent.size())); diff --git a/api/logic/screenshots/ImgurAlbumCreation.cpp b/api/logic/screenshots/ImgurAlbumCreation.cpp index ff9ec6fd..1f195f00 100644 --- a/api/logic/screenshots/ImgurAlbumCreation.cpp +++ b/api/logic/screenshots/ImgurAlbumCreation.cpp @@ -20,9 +20,9 @@ void ImgurAlbumCreation::start() { m_status = Job_InProgress; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - request.setRawHeader("Authorization", "Client-ID 5b97b0713fba4a3"); + request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); QStringList hashes; diff --git a/api/logic/screenshots/ImgurUpload.cpp b/api/logic/screenshots/ImgurUpload.cpp index 1585b061..7e95d5ca 100644 --- a/api/logic/screenshots/ImgurUpload.cpp +++ b/api/logic/screenshots/ImgurUpload.cpp @@ -23,8 +23,8 @@ void ImgurUpload::start() finished = false; m_status = Job_InProgress; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); - request.setRawHeader("Authorization", "Client-ID 5b97b0713fba4a3"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); QFile f(m_shot->m_file.absoluteFilePath()); |