summaryrefslogtreecommitdiffstats
path: root/logic
diff options
context:
space:
mode:
Diffstat (limited to 'logic')
-rw-r--r--logic/BaseInstance.h2
-rw-r--r--logic/JavaChecker.cpp75
-rw-r--r--logic/JavaChecker.h14
-rw-r--r--logic/JavaCheckerJob.cpp49
-rw-r--r--logic/JavaCheckerJob.h100
-rw-r--r--logic/JavaUtils.cpp75
-rw-r--r--logic/JavaUtils.h12
-rw-r--r--logic/LegacyInstance.cpp6
-rw-r--r--logic/LegacyInstance.h2
-rw-r--r--logic/LegacyUpdate.cpp41
-rw-r--r--logic/LegacyUpdate.h4
-rw-r--r--logic/MinecraftProcess.cpp22
-rw-r--r--logic/OneSixAssets.cpp127
-rw-r--r--logic/OneSixInstance.cpp85
-rw-r--r--logic/OneSixInstance.h4
-rw-r--r--logic/OneSixLibrary.cpp7
-rw-r--r--logic/OneSixLibrary.h3
-rw-r--r--logic/OneSixUpdate.cpp168
-rw-r--r--logic/OneSixUpdate.h15
-rw-r--r--logic/OneSixVersion.cpp16
-rw-r--r--logic/OneSixVersion.h2
-rw-r--r--logic/assets/AssetsUtils.cpp227
-rw-r--r--logic/assets/AssetsUtils.h (renamed from logic/OneSixAssets.h)42
-rw-r--r--logic/auth/MojangAccount.cpp236
-rw-r--r--logic/auth/MojangAccount.h210
-rw-r--r--logic/auth/MojangAccountList.cpp (renamed from logic/lists/MojangAccountList.cpp)66
-rw-r--r--logic/auth/MojangAccountList.h (renamed from logic/lists/MojangAccountList.h)7
-rw-r--r--logic/auth/YggdrasilTask.cpp171
-rw-r--r--logic/auth/YggdrasilTask.h62
-rw-r--r--logic/auth/flows/AuthenticateTask.cpp37
-rw-r--r--logic/auth/flows/AuthenticateTask.h2
-rw-r--r--logic/auth/flows/InvalidateTask.cpp0
-rw-r--r--logic/auth/flows/InvalidateTask.h0
-rw-r--r--logic/auth/flows/RefreshTask.cpp32
-rw-r--r--logic/auth/flows/RefreshTask.h2
-rw-r--r--logic/auth/flows/ValidateTask.cpp4
-rw-r--r--logic/auth/flows/ValidateTask.h6
-rw-r--r--logic/lists/JavaVersionList.cpp83
-rw-r--r--logic/lists/JavaVersionList.h5
-rw-r--r--logic/lists/MinecraftVersionList.cpp9
-rw-r--r--logic/net/MD5EtagDownload.cpp (renamed from logic/net/FileDownload.cpp)26
-rw-r--r--logic/net/MD5EtagDownload.h (renamed from logic/net/FileDownload.h)10
-rw-r--r--logic/net/NetJob.cpp2
-rw-r--r--logic/net/NetJob.h4
-rw-r--r--logic/net/PasteUpload.cpp84
-rw-r--r--logic/net/PasteUpload.h26
-rw-r--r--logic/net/S3ListBucket.cpp1
-rw-r--r--logic/net/URLConstants.h32
-rw-r--r--logic/tasks/ProgressProvider.h1
-rw-r--r--logic/tasks/Task.h1
-rw-r--r--logic/updater/DownloadUpdateTask.cpp404
-rw-r--r--logic/updater/DownloadUpdateTask.h192
-rw-r--r--logic/updater/UpdateChecker.cpp247
-rw-r--r--logic/updater/UpdateChecker.h106
54 files changed, 2435 insertions, 731 deletions
diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h
index 2d7537d6..93e57414 100644
--- a/logic/BaseInstance.h
+++ b/logic/BaseInstance.h
@@ -150,7 +150,7 @@ public:
virtual SettingsObject &settings() const;
/// returns a valid update task if update is needed, NULL otherwise
- virtual Task *doUpdate(bool prepare_for_launch) = 0;
+ virtual std::shared_ptr<Task> doUpdate(bool only_prepare) = 0;
/// returns a valid minecraft process, ready for launch with the given account.
virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) = 0;
diff --git a/logic/JavaChecker.cpp b/logic/JavaChecker.cpp
index daad7281..2b94fbb6 100644
--- a/logic/JavaChecker.cpp
+++ b/logic/JavaChecker.cpp
@@ -1,6 +1,8 @@
#include "JavaChecker.h"
#include <QFile>
#include <QProcess>
+#include <QMap>
+#include <QTemporaryFile>
#define CHECKER_FILE "JavaChecker.jar"
@@ -8,16 +10,17 @@ JavaChecker::JavaChecker(QObject *parent) : QObject(parent)
{
}
-void JavaChecker::performCheck(QString path)
+void JavaChecker::performCheck()
{
- if(QFile::exists(CHECKER_FILE))
- {
- QFile::remove(CHECKER_FILE);
- }
- // extract the checker
- QFile(":/java/checker.jar").copy(CHECKER_FILE);
+ checkerJar.setFileTemplate("checker_XXXXXX.jar");
+ checkerJar.open();
+ QFile inner(":/java/checker.jar");
+ inner.open(QIODevice::ReadOnly);
+ checkerJar.write(inner.readAll());
+ inner.close();
+ checkerJar.close();
- QStringList args = {"-jar", CHECKER_FILE};
+ QStringList args = {"-jar", checkerJar.fileName()};
process.reset(new QProcess());
process->setArguments(args);
@@ -39,31 +42,55 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
killTimer.stop();
QProcessPtr _process;
_process.swap(process);
+ checkerJar.remove();
+
+ JavaCheckResult result;
+ {
+ result.path = path;
+ }
if (status == QProcess::CrashExit || exitcode == 1)
{
- emit checkFinished({});
+ emit checkFinished(result);
return;
}
+ bool success = true;
QString p_stdout = _process->readAllStandardOutput();
- auto parts = p_stdout.split('=', QString::SkipEmptyParts);
- if (parts.size() != 2 || parts[0] != "os.arch")
+ QMap<QString, QString> results;
+ QStringList lines = p_stdout.split("\n", QString::SkipEmptyParts);
+ for(QString line : lines)
{
- emit checkFinished({});
+ line = line.trimmed();
+
+ auto parts = line.split('=', QString::SkipEmptyParts);
+ if(parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty())
+ {
+ success = false;
+ }
+ else
+ {
+ results.insert(parts[0], parts[1]);
+ }
+ }
+
+ if(!results.contains("os.arch") || !results.contains("java.version") || !success)
+ {
+ emit checkFinished(result);
return;
}
- auto os_arch = parts[1].remove('\n').remove('\r');
+ auto os_arch = results["os.arch"];
+ auto java_version = results["java.version"];
bool is_64 = os_arch == "x86_64" || os_arch == "amd64";
- JavaCheckResult result;
- {
- result.valid = true;
- result.is_64bit = is_64;
- result.mojangPlatform = is_64 ? "64" : "32";
- result.realPlatform = os_arch;
- }
+
+ result.valid = true;
+ result.is_64bit = is_64;
+ result.mojangPlatform = is_64 ? "64" : "32";
+ result.realPlatform = os_arch;
+ result.javaVersion = java_version;
+
emit checkFinished(result);
}
@@ -72,7 +99,13 @@ void JavaChecker::error(QProcess::ProcessError err)
if(err == QProcess::FailedToStart)
{
killTimer.stop();
- emit checkFinished({});
+
+ JavaCheckResult result;
+ {
+ result.path = path;
+ }
+
+ emit checkFinished(result);
return;
}
}
diff --git a/logic/JavaChecker.h b/logic/JavaChecker.h
index 34782383..291bf46c 100644
--- a/logic/JavaChecker.h
+++ b/logic/JavaChecker.h
@@ -1,29 +1,39 @@
#pragma once
#include <QProcess>
#include <QTimer>
+#include <QTemporaryFile>
#include <memory>
+class JavaChecker;
+
+
struct JavaCheckResult
{
+ QString path;
QString mojangPlatform;
QString realPlatform;
+ QString javaVersion;
bool valid = false;
bool is_64bit = false;
};
-typedef std::shared_ptr<QProcess> QProcessPtr;
+typedef std::shared_ptr<QProcess> QProcessPtr;
+typedef std::shared_ptr<JavaChecker> JavaCheckerPtr;
class JavaChecker : public QObject
{
Q_OBJECT
public:
explicit JavaChecker(QObject *parent = 0);
- void performCheck(QString path);
+ void performCheck();
+
+ QString path;
signals:
void checkFinished(JavaCheckResult result);
private:
QProcessPtr process;
QTimer killTimer;
+ QTemporaryFile checkerJar;
public
slots:
void timeout();
diff --git a/logic/JavaCheckerJob.cpp b/logic/JavaCheckerJob.cpp
new file mode 100644
index 00000000..36a8a050
--- /dev/null
+++ b/logic/JavaCheckerJob.cpp
@@ -0,0 +1,49 @@
+/* Copyright 2013 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 "JavaCheckerJob.h"
+#include "pathutils.h"
+#include "MultiMC.h"
+
+#include "logger/QsLog.h"
+
+void JavaCheckerJob::partFinished(JavaCheckResult result)
+{
+ num_finished++;
+ QLOG_INFO() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/"
+ << javacheckers.size();
+ emit progress(num_finished, javacheckers.size());
+
+ javaresults.append(result);
+ int result_size = javacheckers.size();
+
+ emit progress(num_finished, result_size);
+
+ if (num_finished == javacheckers.size())
+ {
+ emit finished(javaresults);
+ }
+}
+
+void JavaCheckerJob::start()
+{
+ QLOG_INFO() << m_job_name.toLocal8Bit() << " started.";
+ m_running = true;
+ for (auto iter : javacheckers)
+ {
+ connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult)));
+ iter->performCheck();
+ }
+}
diff --git a/logic/JavaCheckerJob.h b/logic/JavaCheckerJob.h
new file mode 100644
index 00000000..132a92d4
--- /dev/null
+++ b/logic/JavaCheckerJob.h
@@ -0,0 +1,100 @@
+/* Copyright 2013 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 <QtNetwork>
+#include <QLabel>
+#include "JavaChecker.h"
+#include "logic/tasks/ProgressProvider.h"
+
+class JavaCheckerJob;
+typedef std::shared_ptr<JavaCheckerJob> JavaCheckerJobPtr;
+
+class JavaCheckerJob : public ProgressProvider
+{
+ Q_OBJECT
+public:
+ explicit JavaCheckerJob(QString job_name) : ProgressProvider(), m_job_name(job_name) {};
+
+ bool addJavaCheckerAction(JavaCheckerPtr base)
+ {
+ javacheckers.append(base);
+ total_progress++;
+ // if this is already running, the action needs to be started right away!
+ if (isRunning())
+ {
+ emit progress(current_progress, total_progress);
+ connect(base.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult)));
+
+ base->performCheck();
+ }
+ return true;
+ }
+
+ JavaCheckerPtr operator[](int index)
+ {
+ return javacheckers[index];
+ }
+ ;
+ JavaCheckerPtr first()
+ {
+ if (javacheckers.size())
+ return javacheckers[0];
+ return JavaCheckerPtr();
+ }
+ int size() const
+ {
+ return javacheckers.size();
+ }
+ virtual void getProgress(qint64 &current, qint64 &total)
+ {
+ current = current_progress;
+ total = total_progress;
+ }
+ ;
+ virtual QString getStatus() const
+ {
+ return m_job_name;
+ }
+ ;
+ virtual bool isRunning() const
+ {
+ return m_running;
+ }
+ ;
+
+signals:
+ void started();
+ void progress(int current, int total);
+ void finished(QList<JavaCheckResult>);
+public
+slots:
+ virtual void start();
+ // FIXME: implement
+ virtual void abort() {};
+private
+slots:
+ void partFinished(JavaCheckResult result);
+
+private:
+ QString m_job_name;
+ QList<JavaCheckerPtr> javacheckers;
+ QList<JavaCheckResult> javaresults;
+ qint64 current_progress = 0;
+ qint64 total_progress = 0;
+ int num_finished = 0;
+ bool m_running = false;
+};
diff --git a/logic/JavaUtils.cpp b/logic/JavaUtils.cpp
index 61d0231a..e1b3bc64 100644
--- a/logic/JavaUtils.cpp
+++ b/logic/JavaUtils.cpp
@@ -26,11 +26,24 @@
#include "JavaUtils.h"
#include "logger/QsLog.h"
#include "gui/dialogs/VersionSelectDialog.h"
+#include "JavaCheckerJob.h"
+#include "lists/JavaVersionList.h"
JavaUtils::JavaUtils()
{
}
+JavaVersionPtr JavaUtils::MakeJavaPtr(QString path, QString id, QString arch)
+{
+ JavaVersionPtr javaVersion(new JavaVersion());
+
+ javaVersion->id = id;
+ javaVersion->arch = arch;
+ javaVersion->path = path;
+
+ return javaVersion;
+}
+
JavaVersionPtr JavaUtils::GetDefaultJava()
{
JavaVersionPtr javaVersion(new JavaVersion());
@@ -38,7 +51,6 @@ JavaVersionPtr JavaUtils::GetDefaultJava()
javaVersion->id = "java";
javaVersion->arch = "unknown";
javaVersion->path = "java";
- javaVersion->recommended = false;
return javaVersion;
}
@@ -112,7 +124,6 @@ QList<JavaVersionPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
javaVersion->arch = archType;
javaVersion->path =
QDir(PathCombine(value, "bin")).absoluteFilePath("java.exe");
- javaVersion->recommended = (recommended == subKeyName);
javas.append(javaVersion);
}
@@ -128,9 +139,9 @@ QList<JavaVersionPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
return javas;
}
-QList<JavaVersionPtr> JavaUtils::FindJavaPaths()
+QList<QString> JavaUtils::FindJavaPaths()
{
- QList<JavaVersionPtr> javas;
+ QList<JavaVersionPtr> java_candidates;
QList<JavaVersionPtr> JRE64s = this->FindJavaFromRegistryKey(
KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
@@ -141,58 +152,56 @@ QList<JavaVersionPtr> JavaUtils::FindJavaPaths()
QList<JavaVersionPtr> JDK32s = this->FindJavaFromRegistryKey(
KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit");
- javas.append(JRE64s);
- javas.append(JDK64s);
- javas.append(JRE32s);
- javas.append(JDK32s);
-
- if (javas.size() <= 0)
- {
- QLOG_WARN() << "Failed to find Java in the Windows registry - defaulting to \"java\"";
- javas.append(this->GetDefaultJava());
- return javas;
- }
-
- QLOG_INFO() << "Found the following Java installations (64 -> 32, JRE -> JDK): ";
-
- for (auto &java : javas)
+ java_candidates.append(JRE64s);
+ java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/java.exe"));
+ java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/java.exe"));
+ java_candidates.append(JDK64s);
+ java_candidates.append(JRE32s);
+ java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/java.exe"));
+ java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/java.exe"));
+ java_candidates.append(JDK32s);
+ java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path));
+
+ QList<QString> candidates;
+ for(JavaVersionPtr java_candidate : java_candidates)
{
- QString sRec;
- if (java->recommended)
- sRec = "(Recommended)";
- QLOG_INFO() << java->id << java->arch << " at " << java->path << sRec;
+ if(!candidates.contains(java_candidate->path))
+ {
+ candidates.append(java_candidate->path);
+ }
}
- return javas;
+ return candidates;
}
+
#elif OSX
-QList<JavaVersionPtr> JavaUtils::FindJavaPaths()
+QList<QString> JavaUtils::FindJavaPaths()
{
QLOG_INFO() << "OS X Java detection incomplete - defaulting to \"java\"";
- QList<JavaVersionPtr> javas;
- javas.append(this->GetDefaultJava());
+ QList<QString> javas;
+ javas.append(this->GetDefaultJava()->path);
return javas;
}
#elif LINUX
-QList<JavaVersionPtr> JavaUtils::FindJavaPaths()
+QList<QString> JavaUtils::FindJavaPaths()
{
QLOG_INFO() << "Linux Java detection incomplete - defaulting to \"java\"";
- QList<JavaVersionPtr> javas;
- javas.append(this->GetDefaultJava());
+ QList<QString> javas;
+ javas.append(this->GetDefaultJava()->path);
return javas;
}
#else
-QList<JavaVersionPtr> JavaUtils::FindJavaPaths()
+QList<QString> JavaUtils::FindJavaPaths()
{
QLOG_INFO() << "Unknown operating system build - defaulting to \"java\"";
- QList<JavaVersionPtr> javas;
- javas.append(this->GetDefaultJava());
+ QList<QString> javas;
+ javas.append(this->GetDefaultJava()->path);
return javas;
}
diff --git a/logic/JavaUtils.h b/logic/JavaUtils.h
index 44f576b4..22a68ef3 100644
--- a/logic/JavaUtils.h
+++ b/logic/JavaUtils.h
@@ -19,21 +19,23 @@
#include <QWidget>
#include <osutils.h>
-
-#include "logic/lists/JavaVersionList.h"
+#include "JavaCheckerJob.h"
+#include "JavaChecker.h"
+#include "lists/JavaVersionList.h"
#if WINDOWS
#include <windows.h>
#endif
-class JavaUtils
+class JavaUtils : public QObject
{
+ Q_OBJECT
public:
JavaUtils();
- QList<JavaVersionPtr> FindJavaPaths();
+ JavaVersionPtr MakeJavaPtr(QString path, QString id = "unknown", QString arch = "unknown");
+ QList<QString> FindJavaPaths();
JavaVersionPtr GetDefaultJava();
-private:
#if WINDOWS
QList<JavaVersionPtr> FindJavaFromRegistryKey(DWORD keyType, QString keyName);
diff --git a/logic/LegacyInstance.cpp b/logic/LegacyInstance.cpp
index 72b6c51a..fef27bcd 100644
--- a/logic/LegacyInstance.cpp
+++ b/logic/LegacyInstance.cpp
@@ -44,12 +44,12 @@ LegacyInstance::LegacyInstance(const QString &rootDir, SettingsObject *settings,
settings->registerSetting(new Setting("IntendedJarVersion", ""));
}
-Task *LegacyInstance::doUpdate(bool prepare_for_launch)
+std::shared_ptr<Task> LegacyInstance::doUpdate(bool only_prepare)
{
// make sure the jar mods list is initialized by asking for it.
auto list = jarModList();
// create an update task
- return new LegacyUpdate(this, prepare_for_launch , this);
+ return std::shared_ptr<Task> (new LegacyUpdate(this, only_prepare , this));
}
MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account)
@@ -105,7 +105,7 @@ MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account)
#endif
args << "-jar" << LAUNCHER_FILE;
- args << account->currentProfile()->name();
+ args << account->currentProfile()->name;
args << account->sessionId();
args << windowTitle;
args << windowSize;
diff --git a/logic/LegacyInstance.h b/logic/LegacyInstance.h
index a17ef281..1e7d9eb6 100644
--- a/logic/LegacyInstance.h
+++ b/logic/LegacyInstance.h
@@ -76,7 +76,7 @@ public:
virtual bool shouldUpdate() const override;
virtual void setShouldUpdate(bool val) override;
- virtual Task *doUpdate(bool prepare_for_launch) override;
+ virtual std::shared_ptr<Task> doUpdate(bool only_prepare) override;
virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) override;
virtual void cleanupAfterRun() override;
diff --git a/logic/LegacyUpdate.cpp b/logic/LegacyUpdate.cpp
index 3fc17351..e71b270e 100644
--- a/logic/LegacyUpdate.cpp
+++ b/logic/LegacyUpdate.cpp
@@ -25,15 +25,32 @@
#include <quazipfile.h>
#include <JlCompress.h>
#include "logger/QsLog.h"
+#include "logic/net/URLConstants.h"
-LegacyUpdate::LegacyUpdate(BaseInstance *inst, bool prepare_for_launch, QObject *parent)
- : Task(parent), m_inst(inst), m_prepare_for_launch(prepare_for_launch)
+LegacyUpdate::LegacyUpdate(BaseInstance *inst, bool only_prepare, QObject *parent)
+ : Task(parent), m_inst(inst), m_only_prepare(only_prepare)
{
}
void LegacyUpdate::executeTask()
{
- lwjglStart();
+ if(m_only_prepare)
+ {
+ // FIXME: think this through some more.
+ LegacyInstance *inst = (LegacyInstance *)m_inst;
+ if (!inst->shouldUpdate() || inst->shouldUseCustomBaseJar())
+ {
+ ModTheJar();
+ }
+ else
+ {
+ emitSucceeded();
+ }
+ }
+ else
+ {
+ lwjglStart();
+ }
}
void LegacyUpdate::lwjglStart()
@@ -245,16 +262,20 @@ void LegacyUpdate::jarStart()
// Build a list of URLs that will need to be downloaded.
setStatus("Downloading new minecraft.jar");
- QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/");
- QString intended_version_id = inst->intendedVersionId();
- urlstr += intended_version_id + "/" + intended_version_id + ".jar";
+ QString version_id = inst->intendedVersionId();
+ QString localPath = version_id + "/" + version_id + ".jar";
+ QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + localPath;
- auto dljob = new NetJob("Minecraft.jar for version " + intended_version_id);
- dljob->addNetAction(FileDownload::make(QUrl(urlstr), inst->defaultBaseJar()));
- legacyDownloadJob.reset(dljob);
+ auto dljob = new NetJob("Minecraft.jar for version " + version_id);
+
+
+ auto metacache = MMC->metacache();
+ auto entry = metacache->resolveEntry("versions", localPath);
+ dljob->addNetAction(CacheDownload::make(QUrl(urlstr), entry));
connect(dljob, SIGNAL(succeeded()), SLOT(jarFinished()));
connect(dljob, SIGNAL(failed()), SLOT(jarFailed()));
connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ legacyDownloadJob.reset(dljob);
legacyDownloadJob->start();
}
@@ -466,4 +487,4 @@ void LegacyUpdate::ModTheJar()
// inst->UpdateVersion(true);
emitSucceeded();
return;
-} \ No newline at end of file
+}
diff --git a/logic/LegacyUpdate.h b/logic/LegacyUpdate.h
index d753197f..0b573ca5 100644
--- a/logic/LegacyUpdate.h
+++ b/logic/LegacyUpdate.h
@@ -31,7 +31,7 @@ class LegacyUpdate : public Task
{
Q_OBJECT
public:
- explicit LegacyUpdate(BaseInstance *inst, bool prepare_for_launch, QObject *parent = 0);
+ explicit LegacyUpdate(BaseInstance *inst, bool only_prepare, QObject *parent = 0);
virtual void executeTask();
private
@@ -72,5 +72,5 @@ private:
private:
NetJobPtr legacyDownloadJob;
BaseInstance *m_inst = nullptr;
- bool m_prepare_for_launch = false;
+ bool m_only_prepare = false;
};
diff --git a/logic/MinecraftProcess.cpp b/logic/MinecraftProcess.cpp
index 5d99bfae..209929b7 100644
--- a/logic/MinecraftProcess.cpp
+++ b/logic/MinecraftProcess.cpp
@@ -75,20 +75,22 @@ QString MinecraftProcess::censorPrivateInfo(QString in)
{
if(!m_account)
return in;
- else
+
+ QString sessionId = m_account->sessionId();
+ QString accessToken = m_account->accessToken();
+ QString clientToken = m_account->clientToken();
+ in.replace(sessionId, "<SESSION ID>");
+ in.replace(accessToken, "<ACCESS TOKEN>");
+ in.replace(clientToken, "<CLIENT TOKEN>");
+ auto profile = m_account->currentProfile();
+ if(profile)
{
- QString sessionId = m_account->sessionId();
- QString accessToken = m_account->accessToken();
- QString clientToken = m_account->clientToken();
- QString profileId = m_account->currentProfile()->id();
- QString profileName = m_account->currentProfile()->name();
- in.replace(sessionId, "<SESSION ID>");
- in.replace(accessToken, "<ACCESS TOKEN>");
- in.replace(clientToken, "<CLIENT TOKEN>");
+ QString profileId = profile->id;
+ QString profileName = profile->name;
in.replace(profileId, "<PROFILE ID>");
in.replace(profileName, "<PROFILE NAME>");
- return in;
}
+ return in;
}
// console window
diff --git a/logic/OneSixAssets.cpp b/logic/OneSixAssets.cpp
deleted file mode 100644
index 400aff2c..00000000
--- a/logic/OneSixAssets.cpp
+++ /dev/null
@@ -1,127 +0,0 @@
-/* Copyright 2013 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 <QString>
-#include "logger/QsLog.h"
-#include <QtXml/QtXml>
-#include "OneSixAssets.h"
-#include "net/NetJob.h"
-#include "net/HttpMetaCache.h"
-#include "net/S3ListBucket.h"
-#include "MultiMC.h"
-
-#define ASSETS_URL "http://resources.download.minecraft.net/"
-
-class ThreadedDeleter : public QThread
-{
- Q_OBJECT
-public:
- void run()
- {
- QLOG_INFO() << "Cleaning up assets folder...";
- QDirIterator iter(m_base, QDirIterator::Subdirectories);
- int base_length = m_base.length();
- while (iter.hasNext())
- {
- QString filename = iter.next();
- QFileInfo current(filename);
- // we keep the dirs... whatever
- if (current.isDir())
- continue;
- QString trimmedf = filename;
- trimmedf.remove(0, base_length + 1);
- if (m_whitelist.contains(trimmedf))
- {
- QLOG_TRACE() << trimmedf << " gets to live";
- }
- else
- {
- // DO NOT TOLERATE JUNK
- QLOG_TRACE() << trimmedf << " dies";
- QFile f(filename);
- f.remove();
- }
- }
- }
- QString m_base;
- QStringList m_whitelist;
-};
-
-void OneSixAssets::downloadFinished()
-{
- deleter = new ThreadedDeleter();
- QDir dir("assets");
- deleter->m_base = dir.absolutePath();
- deleter->m_whitelist = nuke_whitelist;
- connect(deleter, SIGNAL(finished()), SIGNAL(finished()));
- deleter->start();
-}
-
-void OneSixAssets::S3BucketFinished()
-{
- QString prefix(ASSETS_URL);
- nuke_whitelist.clear();
-
- emit filesStarted();
-
- auto firstJob = index_job->first();
- auto objectList = std::dynamic_pointer_cast<S3ListBucket>(firstJob)->objects;
-
- NetJob *job = new NetJob("Assets");
-
- connect(job, SIGNAL(succeeded()), SLOT(downloadFinished()));
- connect(job, SIGNAL(failed()), SIGNAL(failed()));
- connect(job, SIGNAL(filesProgress(int, int, int)), SIGNAL(filesProgress(int, int, int)));
-
- auto metacache = MMC->metacache();
-
- for (auto object : objectList)
- {
- // Filter folder keys (zero size)
- if (object.size == 0)
- continue;
-
- nuke_whitelist.append(object.Key);
-
- auto entry = metacache->resolveEntry("assets", object.Key, object.ETag);
- if (entry->stale)
- {
- job->addNetAction(CacheDownload::make(QUrl(prefix + object.Key), entry));
- }
- }
- if (job->size())
- {
- files_job.reset(job);
- files_job->start();
- }
- else
- {
- delete job;
- emit finished();
- }
-}
-
-void OneSixAssets::start()
-{
- auto job = new NetJob("Assets index");
- job->addNetAction(S3ListBucket::make(QUrl(ASSETS_URL)));
- connect(job, SIGNAL(succeeded()), SLOT(S3BucketFinished()));
- connect(job, SIGNAL(failed()), SIGNAL(failed()));
- emit indexStarted();
- index_job.reset(job);
- job->start();
-}
-
-#include "OneSixAssets.moc"
diff --git a/logic/OneSixInstance.cpp b/logic/OneSixInstance.cpp
index 08b63bf9..fd41b9e5 100644
--- a/logic/OneSixInstance.cpp
+++ b/logic/OneSixInstance.cpp
@@ -26,6 +26,7 @@
#include <JlCompress.h>
#include "gui/dialogs/OneSixModEditDialog.h"
#include "logger/QsLog.h"
+#include "logic/assets/AssetsUtils.h"
OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *setting_obj,
QObject *parent)
@@ -37,9 +38,9 @@ OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *setting_o
reloadFullVersion();
}
-Task *OneSixInstance::doUpdate(bool prepare_for_launch)
+std::shared_ptr<Task> OneSixInstance::doUpdate(bool only_prepare)
{
- return new OneSixUpdate(this, prepare_for_launch);
+ return std::shared_ptr<Task> (new OneSixUpdate(this, only_prepare));
}
QString replaceTokensIn(QString text, QMap<QString, QString> with)
@@ -66,6 +67,63 @@ QString replaceTokensIn(QString text, QMap<QString, QString> with)
return result;
}
+QDir OneSixInstance::reconstructAssets(std::shared_ptr<OneSixVersion> version)
+{
+ QDir assetsDir = QDir("assets/");
+ QDir indexDir = QDir(PathCombine(assetsDir.path(), "indexes"));
+ QDir objectDir = QDir(PathCombine(assetsDir.path(), "objects"));
+ QDir virtualDir = QDir(PathCombine(assetsDir.path(), "virtual"));
+
+ QString indexPath = PathCombine(indexDir.path(), version->assets + ".json");
+ QFile indexFile(indexPath);
+ QDir virtualRoot(PathCombine(virtualDir.path(), version->assets));
+
+ if(!indexFile.exists())
+ {
+ QLOG_ERROR() << "No assets index file" << indexPath << "; can't reconstruct assets";
+ return virtualRoot;
+ }
+
+ QLOG_DEBUG() << "reconstructAssets" << assetsDir.path() << indexDir.path() << objectDir.path() << virtualDir.path() << virtualRoot.path();
+
+ AssetsIndex index;
+ bool loadAssetsIndex = AssetsUtils::loadAssetsIndexJson(indexPath, &index);
+
+ if(loadAssetsIndex)
+ {
+ if(index.isVirtual)
+ {
+ QLOG_INFO() << "Reconstructing virtual assets folder at" << virtualRoot.path();
+
+ for(QString map : index.objects.keys())
+ {
+ AssetObject asset_object = index.objects.value(map);
+ QString target_path = PathCombine(virtualRoot.path(), map);
+ QFile target(target_path);
+
+ QString tlk = asset_object.hash.left(2);
+
+ QString original_path = PathCombine(PathCombine(objectDir.path(), tlk), asset_object.hash);
+ QFile original(original_path);
+ if(!target.exists())
+ {
+ QFileInfo info(target_path);
+ QDir target_dir = info.dir();
+ //QLOG_DEBUG() << target_dir;
+ if(!target_dir.exists()) QDir("").mkpath(target_dir.path());
+
+ bool couldCopy = original.copy(target_path);
+ QLOG_DEBUG() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy);// << original.errorString();
+ }
+ }
+
+ // TODO: Write last used time to virtualRoot/.lastused
+ }
+ }
+
+ return virtualRoot;
+}
+
QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account)
{
I_D(OneSixInstance);
@@ -77,8 +135,8 @@ QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account)
token_mapping["auth_username"] = account->username();
token_mapping["auth_session"] = account->sessionId();
token_mapping["auth_access_token"] = account->accessToken();
- token_mapping["auth_player_name"] = account->currentProfile()->name();
- token_mapping["auth_uuid"] = account->currentProfile()->id();
+ token_mapping["auth_player_name"] = account->currentProfile()->name;
+ token_mapping["auth_uuid"] = account->currentProfile()->id;
// this is for offline?:
/*
@@ -93,9 +151,22 @@ QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account)
QString absRootDir = QDir(minecraftRoot()).absolutePath();
token_mapping["game_directory"] = absRootDir;
QString absAssetsDir = QDir("assets/").absolutePath();
- token_mapping["game_assets"] = absAssetsDir;
- //TODO: this is something new and not even fully implemented in the vanilla launcher.
- token_mapping["user_properties"] = "{ }";
+ token_mapping["game_assets"] = reconstructAssets(d->version).absolutePath();
+
+ auto user = account->user();
+ QJsonObject userAttrs;
+ for(auto key: user.properties.keys())
+ {
+ auto array = QJsonArray::fromStringList(user.properties.values(key));
+ userAttrs.insert(key, array);
+ }
+ QJsonDocument value(userAttrs);
+
+ token_mapping["user_properties"] = value.toJson(QJsonDocument::Compact);
+ token_mapping["user_type"] = account->currentProfile()->legacy ? "legacy" : "mojang";
+ // 1.7.3+ assets tokens
+ token_mapping["assets_root"] = absAssetsDir;
+ token_mapping["assets_index_name"] = version->assets;
QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts);
for (int i = 0; i < parts.length(); i++)
diff --git a/logic/OneSixInstance.h b/logic/OneSixInstance.h
index 042c104b..f869e345 100644
--- a/logic/OneSixInstance.h
+++ b/logic/OneSixInstance.h
@@ -16,6 +16,7 @@
#pragma once
#include <QStringList>
+#include <QDir>
#include "BaseInstance.h"
@@ -39,7 +40,7 @@ public:
QString loaderModsDir() const;
virtual QString instanceConfigFolder() const override;
- virtual Task *doUpdate(bool prepare_for_launch) override;
+ virtual std::shared_ptr<Task> doUpdate(bool only_prepare) override;
virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) override;
virtual void cleanupAfterRun() override;
@@ -73,4 +74,5 @@ public:
private:
QStringList processMinecraftArgs(MojangAccountPtr account);
+ QDir reconstructAssets(std::shared_ptr<OneSixVersion> version);
};
diff --git a/logic/OneSixLibrary.cpp b/logic/OneSixLibrary.cpp
index ad4aec44..4b6ed9dc 100644
--- a/logic/OneSixLibrary.cpp
+++ b/logic/OneSixLibrary.cpp
@@ -18,6 +18,7 @@
#include "OneSixLibrary.h"
#include "OneSixRule.h"
#include "OpSys.h"
+#include "logic/net/URLConstants.h"
void OneSixLibrary::finalize()
{
@@ -140,9 +141,9 @@ QJsonObject OneSixLibrary::toJson()
libRoot.insert("MMC-absoluteUrl", m_absolute_url);
if (m_hint.size())
libRoot.insert("MMC-hint", m_hint);
- if (m_base_url != "http://s3.amazonaws.com/Minecraft.Download/libraries/" &&
- m_base_url != "https://s3.amazonaws.com/Minecraft.Download/libraries/" &&
- m_base_url != "https://libraries.minecraft.net/")
+ if (m_base_url != "http://" + URLConstants::AWS_DOWNLOAD_LIBRARIES &&
+ m_base_url != "https://" + URLConstants::AWS_DOWNLOAD_LIBRARIES &&
+ m_base_url != "https://" + URLConstants::LIBRARY_BASE)
libRoot.insert("url", m_base_url);
if (isNative() && m_native_suffixes.size())
{
diff --git a/logic/OneSixLibrary.h b/logic/OneSixLibrary.h
index f8dc3aef..5cb867c2 100644
--- a/logic/OneSixLibrary.h
+++ b/logic/OneSixLibrary.h
@@ -21,6 +21,7 @@
#include <QJsonObject>
#include <memory>
+#include "logic/net/URLConstants.h"
#include "OpSys.h"
class Rule;
@@ -30,7 +31,7 @@ class OneSixLibrary
private:
// basic values used internally (so far)
QString m_name;
- QString m_base_url = "https://libraries.minecraft.net/";
+ QString m_base_url = "https://" + URLConstants::LIBRARY_BASE;
QList<std::shared_ptr<Rule>> m_rules;
// custom values
diff --git a/logic/OneSixUpdate.cpp b/logic/OneSixUpdate.cpp
index 25e16328..66950fc4 100644
--- a/logic/OneSixUpdate.cpp
+++ b/logic/OneSixUpdate.cpp
@@ -29,12 +29,14 @@
#include "OneSixLibrary.h"
#include "OneSixInstance.h"
#include "net/ForgeMirrors.h"
+#include "net/URLConstants.h"
+#include "assets/AssetsUtils.h"
#include "pathutils.h"
#include <JlCompress.h>
-OneSixUpdate::OneSixUpdate(BaseInstance *inst, bool prepare_for_launch, QObject *parent)
- : Task(parent), m_inst(inst), m_prepare_for_launch(prepare_for_launch)
+OneSixUpdate::OneSixUpdate(BaseInstance *inst, bool only_prepare, QObject *parent)
+ : Task(parent), m_inst(inst), m_only_prepare(only_prepare)
{
}
@@ -50,6 +52,24 @@ void OneSixUpdate::executeTask()
return;
}
+ if (m_only_prepare)
+ {
+ if (m_inst->shouldUpdate())
+ {
+ emitFailed("Unable to update instance in offline mode.");
+ return;
+ }
+ setStatus("Testing the Java installation.");
+ QString java_path = m_inst->settings().get("JavaPath").toString();
+
+ checker.reset(new JavaChecker());
+ connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
+ SLOT(checkFinishedOffline(JavaCheckResult)));
+ checker->path = java_path;
+ checker->performCheck();
+ return;
+ }
+
if (m_inst->shouldUpdate())
{
// Get a pointer to the version object that corresponds to the instance's version.
@@ -65,35 +85,44 @@ void OneSixUpdate::executeTask()
}
else
{
- checkJava();
+ checkJavaOnline();
}
}
-void OneSixUpdate::checkJava()
+void OneSixUpdate::checkJavaOnline()
{
- QLOG_INFO() << m_inst->name() << ": checking java binary";
setStatus("Testing the Java installation.");
- // TODO: cache this so we don't have to run an extra java process every time.
QString java_path = m_inst->settings().get("JavaPath").toString();
checker.reset(new JavaChecker());
connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
- SLOT(checkFinished(JavaCheckResult)));
- checker->performCheck(java_path);
+ SLOT(checkFinishedOnline(JavaCheckResult)));
+ checker->path = java_path;
+ checker->performCheck();
}
-void OneSixUpdate::checkFinished(JavaCheckResult result)
+void OneSixUpdate::checkFinishedOnline(JavaCheckResult result)
{
if (result.valid)
{
- QLOG_INFO() << m_inst->name() << ": java is "
- << (result.is_64bit ? "64 bit" : "32 bit");
java_is_64bit = result.is_64bit;
jarlibStart();
}
else
{
- QLOG_INFO() << m_inst->name() << ": java isn't valid";
+ emitFailed("The java binary doesn't work. Check the settings and correct the problem");
+ }
+}
+
+void OneSixUpdate::checkFinishedOffline(JavaCheckResult result)
+{
+ if (result.valid)
+ {
+ java_is_64bit = result.is_64bit;
+ prepareForLaunch();
+ }
+ else
+ {
emitFailed("The java binary doesn't work. Check the settings and correct the problem");
}
}
@@ -103,8 +132,7 @@ void OneSixUpdate::versionFileStart()
QLOG_INFO() << m_inst->name() << ": getting version file.";
setStatus("Getting the version files from Mojang.");
- QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/");
- urlstr += targetVersion->descriptor() + "/" + targetVersion->descriptor() + ".json";
+ QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + targetVersion->descriptor() + "/" + targetVersion->descriptor() + ".json";
auto job = new NetJob("Version index");
job->addNetAction(ByteArrayDownload::make(QUrl(urlstr)));
specificVersionDownloadJob.reset(job);
@@ -160,7 +188,7 @@ void OneSixUpdate::versionFileFinished()
}
inst->reloadFullVersion();
- checkJava();
+ checkJavaOnline();
}
void OneSixUpdate::versionFileFailed()
@@ -168,6 +196,90 @@ void OneSixUpdate::versionFileFailed()
emitFailed("Failed to download the version description. Try again.");
}
+void OneSixUpdate::assetIndexStart()
+{
+ setStatus("Updating asset index.");
+ OneSixInstance *inst = (OneSixInstance *)m_inst;
+ std::shared_ptr<OneSixVersion> version = inst->getFullVersion();
+ QString assetName = version->assets;
+ QUrl indexUrl = "http://" + URLConstants::AWS_DOWNLOAD_INDEXES + assetName + ".json";
+ QString localPath = assetName + ".json";
+ auto job = new NetJob("Asset index for " + inst->name());
+
+ auto metacache = MMC->metacache();
+ auto entry = metacache->resolveEntry("asset_indexes", localPath);
+ job->addNetAction(CacheDownload::make(indexUrl, entry));
+ jarlibDownloadJob.reset(job);
+
+ connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetIndexFinished()));
+ connect(jarlibDownloadJob.get(), SIGNAL(failed()), SLOT(assetIndexFailed()));
+ connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)),
+ SIGNAL(progress(qint64, qint64)));
+
+ jarlibDownloadJob->start();
+}
+
+void OneSixUpdate::assetIndexFinished()
+{
+ AssetsIndex index;
+
+ OneSixInstance *inst = (OneSixInstance *)m_inst;
+ std::shared_ptr<OneSixVersion> version = inst->getFullVersion();
+ QString assetName = version->assets;
+
+ QString asset_fname = "assets/indexes/" + assetName + ".json";
+ if (!AssetsUtils::loadAssetsIndexJson(asset_fname, &index))
+ {
+ emitFailed("Failed to read the assets index!");
+ }
+
+ QList<Md5EtagDownloadPtr> dls;
+ for (auto object : index.objects.values())
+ {
+ QString objectName = object.hash.left(2) + "/" + object.hash;
+ QFileInfo objectFile("assets/objects/" + objectName);
+ if ((!objectFile.isFile()) || (objectFile.size() != object.size))
+ {
+ auto objectDL = MD5EtagDownload::make(
+ QUrl("http://" + URLConstants::RESOURCE_BASE + objectName),
+ objectFile.filePath());
+ dls.append(objectDL);
+ }
+ }
+ if(dls.size())
+ {
+ setStatus("Getting the assets files from Mojang...");
+ auto job = new NetJob("Assets for " + inst->name());
+ for(auto dl: dls)
+ job->addNetAction(dl);
+ jarlibDownloadJob.reset(job);
+ connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetsFinished()));
+ connect(jarlibDownloadJob.get(), SIGNAL(failed()), SLOT(assetsFailed()));
+ connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)),
+ SIGNAL(progress(qint64, qint64)));
+ jarlibDownloadJob->start();
+ return;
+ }
+ assetsFinished();
+}
+
+void OneSixUpdate::assetIndexFailed()
+{
+ emitFailed("Failed to download the assets index!");
+}
+
+void OneSixUpdate::assetsFinished()
+{
+ prepareForLaunch();
+}
+
+void OneSixUpdate::assetsFailed()
+{
+ emitFailed("Failed to download assets!");
+}
+
+
+
void OneSixUpdate::jarlibStart()
{
setStatus("Getting the library files from Mojang.");
@@ -181,17 +293,22 @@ void OneSixUpdate::jarlibStart()
return;
}
+ // Build a list of URLs that will need to be downloaded.
std::shared_ptr<OneSixVersion> version = inst->getFullVersion();
+ // minecraft.jar for this version
+ {
+ QString version_id = version->id;
+ QString localPath = version_id + "/" + version_id + ".jar";
+ QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + localPath;
- // download the right jar, save it in versions/$version/$version.jar
- QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/");
- urlstr += version->id + "/" + version->id + ".jar";
- QString targetstr("versions/");
- targetstr += version->id + "/" + version->id + ".jar";
+ auto job = new NetJob("Libraries for instance " + inst->name());
- auto job = new NetJob("Libraries for instance " + inst->name());
- job->addNetAction(FileDownload::make(QUrl(urlstr), targetstr));
- jarlibDownloadJob.reset(job);
+ auto metacache = MMC->metacache();
+ auto entry = metacache->resolveEntry("versions", localPath);
+ job->addNetAction(CacheDownload::make(QUrl(urlstr), entry));
+
+ jarlibDownloadJob.reset(job);
+ }
auto libs = version->getActiveNativeLibs();
libs.append(version->getActiveNormalLibs());
@@ -240,10 +357,7 @@ void OneSixUpdate::jarlibStart()
void OneSixUpdate::jarlibFinished()
{
- if (m_prepare_for_launch)
- prepareForLaunch();
- else
- emitSucceeded();
+ assetIndexStart();
}
void OneSixUpdate::jarlibFailed()
diff --git a/logic/OneSixUpdate.h b/logic/OneSixUpdate.h
index b86c205f..00b769c7 100644
--- a/logic/OneSixUpdate.h
+++ b/logic/OneSixUpdate.h
@@ -43,11 +43,20 @@ slots:
void jarlibFinished();
void jarlibFailed();
- void checkJava();
- void checkFinished(JavaCheckResult result);
+ void assetIndexStart();
+ void assetIndexFinished();
+ void assetIndexFailed();
+
+ void assetsFinished();
+ void assetsFailed();
+
+ void checkJavaOnline();
+ void checkFinishedOnline(JavaCheckResult result);
+ void checkFinishedOffline(JavaCheckResult result);
// extract the appropriate libraries
void prepareForLaunch();
+
private:
NetJobPtr specificVersionDownloadJob;
NetJobPtr jarlibDownloadJob;
@@ -55,7 +64,7 @@ private:
// target version, determined during this task
std::shared_ptr<MinecraftVersion> targetVersion;
BaseInstance *m_inst = nullptr;
- bool m_prepare_for_launch = false;
+ bool m_only_prepare = false;
std::shared_ptr<JavaChecker> checker;
bool java_is_64bit = false;
diff --git a/logic/OneSixVersion.cpp b/logic/OneSixVersion.cpp
index cc5b1de1..8ae685f0 100644
--- a/logic/OneSixVersion.cpp
+++ b/logic/OneSixVersion.cpp
@@ -17,6 +17,8 @@
#include "logic/OneSixLibrary.h"
#include "logic/OneSixRule.h"
+#include "logger/QsLog.h"
+
std::shared_ptr<OneSixVersion> fromJsonV4(QJsonObject root,
std::shared_ptr<OneSixVersion> fullVersion)
{
@@ -60,6 +62,18 @@ std::shared_ptr<OneSixVersion> fromJsonV4(QJsonObject root,
fullVersion->releaseTime = root.value("releaseTime").toString();
fullVersion->time = root.value("time").toString();
+ auto assetsID = root.value("assets");
+ if (assetsID.isString())
+ {
+ fullVersion->assets = assetsID.toString();
+ }
+ else
+ {
+ fullVersion->assets = "legacy";
+ }
+
+ QLOG_DEBUG() << "Assets version:" << fullVersion->assets;
+
// Iterate through the list, if it's a list.
auto librariesValue = root.value("libraries");
if (!librariesValue.isArray())
@@ -151,7 +165,7 @@ std::shared_ptr<OneSixVersion> OneSixVersion::fromJson(QJsonObject root)
root.value("minimumLauncherVersion").toDouble();
// ADD MORE HERE :D
- if (launcher_ver > 0 && launcher_ver <= 11)
+ if (launcher_ver > 0 && launcher_ver <= 13)
return fromJsonV4(root, readVersion);
else
{
diff --git a/logic/OneSixVersion.h b/logic/OneSixVersion.h
index 5718dafe..036f3d53 100644
--- a/logic/OneSixVersion.h
+++ b/logic/OneSixVersion.h
@@ -56,6 +56,8 @@ public:
QString releaseTime;
/// Release type - "release" or "snapshot"
QString type;
+ /// Assets type - "legacy" or a version ID
+ QString assets;
/**
* DEPRECATED: Old versions of the new vanilla launcher used this
* ex: "username_session_version"
diff --git a/logic/assets/AssetsUtils.cpp b/logic/assets/AssetsUtils.cpp
new file mode 100644
index 00000000..11d928cf
--- /dev/null
+++ b/logic/assets/AssetsUtils.cpp
@@ -0,0 +1,227 @@
+/* Copyright 2013 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 <QDirIterator>
+#include <QCryptographicHash>
+#include <QJsonParseError>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include "AssetsUtils.h"
+#include "MultiMC.h"
+
+namespace AssetsUtils
+{
+void migrateOldAssets()
+{
+ QDir assets_dir("assets");
+ if (!assets_dir.exists())
+ return;
+ assets_dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
+ int base_length = assets_dir.path().length();
+
+ QList<QString> blacklist = {"indexes", "objects", "virtual"};
+
+ if (!assets_dir.exists("objects"))
+ assets_dir.mkdir("objects");
+ QDir objects_dir("assets/objects");
+
+ QDirIterator iterator(assets_dir, QDirIterator::Subdirectories);
+ int successes = 0;
+ int failures = 0;
+ while (iterator.hasNext())
+ {
+ QString currentDir = iterator.next();
+ currentDir = currentDir.remove(0, base_length + 1);
+
+ bool ignore = false;
+ for (QString blacklisted : blacklist)
+ {
+ if (currentDir.startsWith(blacklisted))
+ ignore = true;
+ }
+
+ if (!iterator.fileInfo().isDir() && !ignore)
+ {
+ QString filename = iterator.filePath();
+
+ QFile input(filename);
+ input.open(QIODevice::ReadOnly | QIODevice::WriteOnly);
+ QString sha1sum =
+ QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1)
+ .toHex()
+ .constData();
+
+ QString object_name = filename.remove(0, base_length + 1);
+ QLOG_DEBUG() << "Processing" << object_name << ":" << sha1sum << input.size();
+
+ QString object_tlk = sha1sum.left(2);
+ QString object_tlk_dir = objects_dir.path() + "/" + object_tlk;
+
+ QDir tlk_dir(object_tlk_dir);
+ if (!tlk_dir.exists())
+ objects_dir.mkdir(object_tlk);
+
+ QString new_filename = tlk_dir.path() + "/" + sha1sum;
+ QFile new_object(new_filename);
+ if (!new_object.exists())
+ {
+ bool rename_success = input.rename(new_filename);
+ QLOG_DEBUG() << " Doesn't exist, copying to" << new_filename << ":"
+ << QString::number(rename_success);
+ if (rename_success)
+ successes++;
+ else
+ failures++;
+ }
+ else
+ {
+ input.remove();
+ QLOG_DEBUG() << " Already exists, deleting original and not copying.";
+ }
+ }
+ }
+
+ if (successes + failures == 0)
+ {
+ QLOG_DEBUG() << "No legacy assets needed importing.";
+ }
+ else
+ {
+ QLOG_DEBUG() << "Finished copying legacy assets:" << successes << "successes and"
+ << failures << "failures.";
+
+ QDirIterator cleanup_iterator(assets_dir);
+
+ while (cleanup_iterator.hasNext())
+ {
+ QString currentDir = cleanup_iterator.next();
+ currentDir = currentDir.remove(0, base_length + 1);
+
+ bool ignore = false;
+ for (QString blacklisted : blacklist)
+ {
+ if (currentDir.startsWith(blacklisted))
+ ignore = true;
+ }
+
+ if (cleanup_iterator.fileInfo().isDir() && !ignore)
+ {
+ QString path = cleanup_iterator.filePath();
+ QDir folder(path);
+
+ QLOG_DEBUG() << "Cleaning up legacy assets folder:" << path;
+
+ folder.removeRecursively();
+ }
+ }
+ }
+}
+
+/*
+ * Returns true on success, with index populated
+ * index is undefined otherwise
+ */
+bool loadAssetsIndexJson(QString path, AssetsIndex *index)
+{
+ /*
+ {
+ "objects": {
+ "icons/icon_16x16.png": {
+ "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a",
+ "size": 3665
+ },
+ ...
+ }
+ }
+ }
+ */
+
+ QFile file(path);
+
+ // Try to open the file and fail if we can't.
+ // TODO: We should probably report this error to the user.
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ QLOG_ERROR() << "Failed to read assets index file" << path;
+ return false;
+ }
+
+ // Read the file and close it.
+ QByteArray jsonData = file.readAll();
+ file.close();
+
+ QJsonParseError parseError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
+
+ // Fail if the JSON is invalid.
+ if (parseError.error != QJsonParseError::NoError)
+ {
+ QLOG_ERROR() << "Failed to parse assets index file:" << parseError.errorString()
+ << "at offset " << QString::number(parseError.offset);
+ return false;
+ }
+
+ // Make sure the root is an object.
+ if (!jsonDoc.isObject())
+ {
+ QLOG_ERROR() << "Invalid assets index JSON: Root should be an array.";
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ QJsonValue isVirtual = root.value("virtual");
+ if (!isVirtual.isUndefined())
+ {
+ index->isVirtual = isVirtual.toBool(false);
+ }
+
+ QJsonValue objects = root.value("objects");
+ QVariantMap map = objects.toVariant().toMap();
+
+ for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter)
+ {
+ // QLOG_DEBUG() << iter.key();
+
+ QVariant variant = iter.value();
+ QVariantMap nested_objects = variant.toMap();
+
+ AssetObject object;
+
+ for (QVariantMap::const_iterator nested_iter = nested_objects.begin();
+ nested_iter != nested_objects.end(); ++nested_iter)
+ {
+ // QLOG_DEBUG() << nested_iter.key() << nested_iter.value().toString();
+ QString key = nested_iter.key();
+ QVariant value = nested_iter.value();
+
+ if (key == "hash")
+ {
+ object.hash = value.toString();
+ }
+ else if (key == "size")
+ {
+ object.size = value.toDouble();
+ }
+ }
+
+ index->objects.insert(iter.key(), object);
+ }
+
+ return true;
+}
+}
diff --git a/logic/OneSixAssets.h b/logic/assets/AssetsUtils.h
index 948068b8..5276d5a5 100644
--- a/logic/OneSixAssets.h
+++ b/logic/assets/AssetsUtils.h
@@ -14,32 +14,26 @@
*/
#pragma once
-#include "net/NetJob.h"
-class Private;
-class ThreadedDeleter;
+#include <QString>
+#include <QMap>
-class OneSixAssets : public QObject
-{
- Q_OBJECT
-signals:
- void failed();
- void finished();
- void indexStarted();
- void filesStarted();
- void filesProgress(int, int, int);
-
-public
-slots:
- void S3BucketFinished();
- void downloadFinished();
+class AssetObject;
-public:
- void start();
+struct AssetObject
+{
+ QString hash;
+ qint64 size;
+};
-private:
- ThreadedDeleter *deleter;
- QStringList nuke_whitelist;
- NetJobPtr index_job;
- NetJobPtr files_job;
+struct AssetsIndex
+{
+ QMap<QString, AssetObject> objects;
+ bool isVirtual = false;
};
+
+namespace AssetsUtils
+{
+void migrateOldAssets();
+bool loadAssetsIndexJson(QString file, AssetsIndex* index);
+}
diff --git a/logic/auth/MojangAccount.cpp b/logic/auth/MojangAccount.cpp
index 4a61cf19..bc6af98f 100644
--- a/logic/auth/MojangAccount.cpp
+++ b/logic/auth/MojangAccount.cpp
@@ -16,113 +16,17 @@
*/
#include "MojangAccount.h"
+#include "flows/RefreshTask.h"
+#include "flows/AuthenticateTask.h"
#include <QUuid>
#include <QJsonObject>
#include <QJsonArray>
+#include <QRegExp>
+#include <QStringList>
#include <logger/QsLog.h>
-MojangAccount::MojangAccount(const QString &username, QObject *parent) : QObject(parent)
-{
- // Generate a client token.
- m_clientToken = QUuid::createUuid().toString();
-
- m_username = username;
-
- m_currentProfile = -1;
-}
-
-MojangAccount::MojangAccount(const QString &username, const QString &clientToken,
- const QString &accessToken, QObject *parent)
- : QObject(parent)
-{
- m_username = username;
- m_clientToken = clientToken;
- m_accessToken = accessToken;
-
- m_currentProfile = -1;
-}
-
-MojangAccount::MojangAccount(const MojangAccount &other, QObject *parent)
-{
- m_username = other.username();
- m_clientToken = other.clientToken();
- m_accessToken = other.accessToken();
-
- m_profiles = other.m_profiles;
- m_currentProfile = other.m_currentProfile;
-}
-
-QString MojangAccount::username() const
-{
- return m_username;
-}
-
-QString MojangAccount::clientToken() const
-{
- return m_clientToken;
-}
-
-void MojangAccount::setClientToken(const QString &clientToken)
-{
- m_clientToken = clientToken;
-}
-
-QString MojangAccount::accessToken() const
-{
- return m_accessToken;
-}
-
-void MojangAccount::setAccessToken(const QString &accessToken)
-{
- m_accessToken = accessToken;
-}
-
-QString MojangAccount::sessionId() const
-{
- return "token:" + m_accessToken + ":" + currentProfile()->id();
-}
-
-const QList<AccountProfile> MojangAccount::profiles() const
-{
- return m_profiles;
-}
-
-const AccountProfile *MojangAccount::currentProfile() const
-{
- if (m_currentProfile < 0)
- {
- if (m_profiles.length() > 0)
- return &m_profiles.at(0);
- else
- return nullptr;
- }
- else
- return &m_profiles.at(m_currentProfile);
-}
-
-bool MojangAccount::setProfile(const QString &profileId)
-{
- const QList<AccountProfile> &profiles = this->profiles();
- for (int i = 0; i < profiles.length(); i++)
- {
- if (profiles.at(i).id() == profileId)
- {
- m_currentProfile = i;
- return true;
- }
- }
- return false;
-}
-
-void MojangAccount::loadProfiles(const ProfileList &profiles)
-{
- m_profiles.clear();
- for (auto profile : profiles)
- m_profiles.append(profile);
-}
-
MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
{
// The JSON object must at least have a username for it to be valid.
@@ -143,78 +47,160 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
return nullptr;
}
- ProfileList profiles;
+ QList<AccountProfile> profiles;
for (QJsonValue profileVal : profileArray)
{
QJsonObject profileObject = profileVal.toObject();
QString id = profileObject.value("id").toString("");
QString name = profileObject.value("name").toString("");
+ bool legacy = profileObject.value("legacy").toBool(false);
if (id.isEmpty() || name.isEmpty())
{
QLOG_WARN() << "Unable to load a profile because it was missing an ID or a name.";
continue;
}
- profiles.append(AccountProfile(id, name));
+ profiles.append({id, name, legacy});
}
- MojangAccountPtr account(new MojangAccount(username, clientToken, accessToken));
- account->loadProfiles(profiles);
+ MojangAccountPtr account(new MojangAccount());
+ if(object.value("user").isObject())
+ {
+ User u;
+ QJsonObject userStructure = object.value("user").toObject();
+ u.id = userStructure.value("id").toString();
+ /*
+ QJsonObject propMap = userStructure.value("properties").toObject();
+ for(auto key: propMap.keys())
+ {
+ auto values = propMap.operator[](key).toArray();
+ for(auto value: values)
+ u.properties.insert(key, value.toString());
+ }
+ */
+ account->m_user = u;
+ }
+ account->m_username = username;
+ account->m_clientToken = clientToken;
+ account->m_accessToken = accessToken;
+ account->m_profiles = profiles;
// Get the currently selected profile.
QString currentProfile = object.value("activeProfile").toString("");
if (!currentProfile.isEmpty())
- account->setProfile(currentProfile);
+ account->setCurrentProfile(currentProfile);
+
+ return account;
+}
+MojangAccountPtr MojangAccount::createFromUsername(const QString& username)
+{
+ MojangAccountPtr account(new MojangAccount());
+ account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
+ account->m_username = username;
return account;
}
-QJsonObject MojangAccount::saveToJson()
+QJsonObject MojangAccount::saveToJson() const
{
QJsonObject json;
- json.insert("username", username());
- json.insert("clientToken", clientToken());
- json.insert("accessToken", accessToken());
+ json.insert("username", m_username);
+ json.insert("clientToken", m_clientToken);
+ json.insert("accessToken", m_accessToken);
QJsonArray profileArray;
for (AccountProfile profile : m_profiles)
{
QJsonObject profileObj;
- profileObj.insert("id", profile.id());
- profileObj.insert("name", profile.name());
+ profileObj.insert("id", profile.id);
+ profileObj.insert("name", profile.name);
+ profileObj.insert("legacy", profile.legacy);
profileArray.append(profileObj);
}
json.insert("profiles", profileArray);
- if (currentProfile() != nullptr)
- json.insert("activeProfile", currentProfile()->id());
+ QJsonObject userStructure;
+ {
+ userStructure.insert("id", m_user.id);
+ /*
+ QJsonObject userAttrs;
+ for(auto key: m_user.properties.keys())
+ {
+ auto array = QJsonArray::fromStringList(m_user.properties.values(key));
+ userAttrs.insert(key, array);
+ }
+ userStructure.insert("properties", userAttrs);
+ */
+ }
+ json.insert("user", userStructure);
+
+ if (m_currentProfile != -1)
+ json.insert("activeProfile", currentProfile()->id);
return json;
}
-
-AccountProfile::AccountProfile(const QString& id, const QString& name)
+bool MojangAccount::setCurrentProfile(const QString &profileId)
{
- m_id = id;
- m_name = name;
+ for (int i = 0; i < m_profiles.length(); i++)
+ {
+ if (m_profiles[i].id == profileId)
+ {
+ m_currentProfile = i;
+ return true;
+ }
+ }
+ return false;
}
-AccountProfile::AccountProfile(const AccountProfile &other)
+const AccountProfile* MojangAccount::currentProfile() const
{
- m_id = other.m_id;
- m_name = other.m_name;
+ if(m_currentProfile == -1)
+ return nullptr;
+ return &m_profiles[m_currentProfile];
}
-QString AccountProfile::id() const
+AccountStatus MojangAccount::accountStatus() const
{
- return m_id;
+ if(m_accessToken.isEmpty())
+ return NotVerified;
+ if(!m_online)
+ return Verified;
+ return Online;
}
-QString AccountProfile::name() const
+std::shared_ptr<Task> MojangAccount::login(QString password)
{
- return m_name;
+ if(m_currentTask)
+ return m_currentTask;
+ if(password.isEmpty())
+ {
+ m_currentTask.reset(new RefreshTask(this));
+ }
+ else
+ {
+ m_currentTask.reset(new AuthenticateTask(this, password));
+ }
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ return m_currentTask;
}
-void MojangAccount::propagateChange()
+void MojangAccount::authSucceeded()
{
+ m_online = true;
+ m_currentTask.reset();
emit changed();
}
+
+void MojangAccount::authFailed(QString reason)
+{
+ // This is emitted when the yggdrasil tasks time out or are cancelled.
+ // -> we treat the error as no-op
+ if(reason != "Yggdrasil task cancelled.")
+ {
+ m_online = false;
+ m_accessToken = QString();
+ emit changed();
+ }
+ m_currentTask.reset();
+}
diff --git a/logic/auth/MojangAccount.h b/logic/auth/MojangAccount.h
index 25a85790..325aa826 100644
--- a/logic/auth/MojangAccount.h
+++ b/logic/auth/MojangAccount.h
@@ -20,43 +20,42 @@
#include <QList>
#include <QJsonObject>
#include <QPair>
+#include <QMap>
#include <memory>
+class Task;
+class YggdrasilTask;
class MojangAccount;
typedef std::shared_ptr<MojangAccount> MojangAccountPtr;
Q_DECLARE_METATYPE(MojangAccountPtr)
/**
- * Class that represents a profile within someone's Mojang account.
+ * A profile within someone's Mojang account.
*
* Currently, the profile system has not been implemented by Mojang yet,
* but we might as well add some things for it in MultiMC right now so
* we don't have to rip the code to pieces to add it later.
*/
-class AccountProfile
+struct AccountProfile
{
-public:
- AccountProfile(const QString &id, const QString &name);
- AccountProfile(const AccountProfile &other);
-
- QString id() const;
- QString name() const;
-
-protected:
- QString m_id;
- QString m_name;
+ QString id;
+ QString name;
+ bool legacy;
};
-typedef QList<AccountProfile> ProfileList;
-
struct User
{
QString id;
- // pair of key:value
- // we don't know if the keys:value mapping is 1:1, so a list is used.
- QList<QPair<QString, QString>> properties;
+ QMultiMap<QString,QString> properties;
+};
+
+enum AccountStatus
+{
+ NotVerified,
+ Verified,
+ Online
};
/**
@@ -68,106 +67,121 @@ struct User
class MojangAccount : public QObject
{
Q_OBJECT
-public:
- /**
- * Constructs a new MojangAccount with the given username.
- * The client token will be generated automatically and the access token will be blank.
- */
- explicit MojangAccount(const QString &username, QObject *parent = 0);
+public: /* construction */
+ //! Do not copy accounts. ever.
+ explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete;
- /**
- * Constructs a new MojangAccount with the given username, client token, and access token.
- */
- explicit MojangAccount(const QString &username, const QString &clientToken,
- const QString &accessToken, QObject *parent = 0);
+ //! Default constructor
+ explicit MojangAccount(QObject *parent = 0) : QObject(parent) {};
- /**
- * Constructs a new MojangAccount matching the given account.
- */
- MojangAccount(const MojangAccount &other, QObject *parent);
+ //! Creates an empty account for the specified user name.
+ static MojangAccountPtr createFromUsername(const QString &username);
- /**
- * Loads a MojangAccount from the given JSON object.
- */
+ //! Loads a MojangAccount from the given JSON object.
static MojangAccountPtr loadFromJson(const QJsonObject &json);
- /**
- * Saves a MojangAccount to a JSON object and returns it.
- */
- QJsonObject saveToJson();
+ //! Saves a MojangAccount to a JSON object and returns it.
+ QJsonObject saveToJson() const;
+public: /* manipulation */
/**
- * Update the account on disk and lists (it changed, for whatever reason)
- * This is called by various Yggdrasil tasks.
- */
- void propagateChange();
+ * Sets the currently selected profile to the profile with the given ID string.
+ * If profileId is not in the list of available profiles, the function will simply return
+ * false.
+ */
+ bool setCurrentProfile(const QString &profileId);
+
+ /**
+ * Attempt to login. Empty password means we use the token.
+ * If the attempt fails because we already are performing some task, it returns false.
+ */
+ std::shared_ptr<Task> login(QString password = QString());
+
+ void downgrade()
+ {
+ m_online = false;
+ }
+public: /* queries */
+ const QString &username() const
+ {
+ return m_username;
+ }
+
+ const QString &clientToken() const
+ {
+ return m_clientToken;
+ }
+
+ const QString &accessToken() const
+ {
+ return m_accessToken;
+ }
+
+ const QList<AccountProfile> &profiles() const
+ {
+ return m_profiles;
+ }
+
+ const User & user()
+ {
+ return m_user;
+ }
+
+ //! Get the session ID required for legacy Minecraft versions
+ QString sessionId() const
+ {
+ if (m_currentProfile != -1 && !m_accessToken.isEmpty())
+ return "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id;
+ return "-";
+ }
+
+ //! Returns the currently selected profile (if none, returns nullptr)
+ const AccountProfile *currentProfile() const;
- /**
- * This MojangAccount's username. May be an email address if the account is migrated.
- */
- QString username() const;
+ //! Returns whether the account is NotVerified, Verified or Online
+ AccountStatus accountStatus() const;
+signals:
/**
- * This MojangAccount's client token. This is a UUID used by Mojang's auth servers to identify this client.
- * This is unique for each MojangAccount.
+ * This signal is emitted when the account changes
*/
- QString clientToken() const;
+ void changed();
- /**
- * Sets the MojangAccount's client token to the given value.
- */
- void setClientToken(const QString &token);
+ // TODO: better signalling for the various possible state changes - especially errors
- /**
- * This MojangAccount's access token.
- * If the user has not chosen to stay logged in, this will be an empty string.
- */
- QString accessToken() const;
+protected: /* variables */
+ QString m_username;
- /**
- * Changes this MojangAccount's access token to the given value.
- */
- void setAccessToken(const QString &token);
+ // Used to identify the client - the user can have multiple clients for the same account
+ // Think: different launchers, all connecting to the same account/profile
+ QString m_clientToken;
- /**
- * Get full session ID
- */
- QString sessionId() const;
+ // Blank if not logged in.
+ QString m_accessToken;
- /**
- * Returns a list of the available account profiles.
- */
- const ProfileList profiles() const;
+ // Index of the selected profile within the list of available
+ // profiles. -1 if nothing is selected.
+ int m_currentProfile = -1;
- /**
- * Returns a pointer to the currently selected profile.
- * If no profile is selected, returns the first profile in the profile list or nullptr if there are none.
- */
- const AccountProfile *currentProfile() const;
+ // List of available profiles.
+ QList<AccountProfile> m_profiles;
- /**
- * Sets the currently selected profile to the profile with the given ID string.
- * If profileId is not in the list of available profiles, the function will simply return false.
- */
- bool setProfile(const QString &profileId);
+ // the user structure, whatever it is.
+ User m_user;
- /**
- * Clears the current account profile list and replaces it with the given profile list.
- */
- void loadProfiles(const ProfileList &profiles);
+ // true when the account is verified
+ bool m_online = false;
-signals:
- /**
- * This isgnal is emitted whrn the account changes
- */
- void changed();
+ // current task we are executing here
+ std::shared_ptr<YggdrasilTask> m_currentTask;
-protected:
- QString m_username;
- QString m_clientToken;
- QString m_accessToken; // Blank if not logged in.
- int m_currentProfile; // Index of the selected profile within the list of available
- // profiles. -1 if nothing is selected.
- ProfileList m_profiles; // List of available profiles.
- User m_user; // the user structure, whatever it is.
+private slots:
+ void authSucceeded();
+ void authFailed(QString reason);
+
+public:
+ friend class YggdrasilTask;
+ friend class AuthenticateTask;
+ friend class ValidateTask;
+ friend class RefreshTask;
};
diff --git a/logic/lists/MojangAccountList.cpp b/logic/auth/MojangAccountList.cpp
index 439b5da6..33990662 100644
--- a/logic/lists/MojangAccountList.cpp
+++ b/logic/auth/MojangAccountList.cpp
@@ -13,7 +13,7 @@
* limitations under the License.
*/
-#include "logic/lists/MojangAccountList.h"
+#include "logic/auth/MojangAccountList.h"
#include <QIODevice>
#include <QFile>
@@ -28,7 +28,7 @@
#include "logic/auth/MojangAccount.h"
-#define ACCOUNT_LIST_FORMAT_VERSION 1
+#define ACCOUNT_LIST_FORMAT_VERSION 2
MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent)
{
@@ -84,10 +84,7 @@ void MojangAccountList::removeAccount(QModelIndex index)
MojangAccountPtr MojangAccountList::activeAccount() const
{
- if (m_activeAccount.isEmpty())
- return nullptr;
- else
- return findAccount(m_activeAccount);
+ return m_activeAccount;
}
void MojangAccountList::setActiveAccount(const QString &username)
@@ -95,14 +92,14 @@ void MojangAccountList::setActiveAccount(const QString &username)
beginResetModel();
if (username.isEmpty())
{
- m_activeAccount = "";
+ m_activeAccount = nullptr;
}
else
{
for (MojangAccountPtr account : m_accounts)
{
if (account->username() == username)
- m_activeAccount = username;
+ m_activeAccount = account;
}
}
endResetModel();
@@ -152,9 +149,6 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const
case Qt::DisplayRole:
switch (index.column())
{
- case ActiveColumn:
- return account->username() == m_activeAccount;
-
case NameColumn:
return account->username();
@@ -168,6 +162,13 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const
case PointerRole:
return qVariantFromValue(account);
+ case Qt::CheckStateRole:
+ switch (index.column())
+ {
+ case ActiveColumn:
+ return account == m_activeAccount;
+ }
+
default:
return QVariant();
}
@@ -216,6 +217,36 @@ int MojangAccountList::columnCount(const QModelIndex &parent) const
return 2;
}
+Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const
+{
+ if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
+ {
+ return Qt::NoItemFlags;
+ }
+
+ return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+}
+
+bool MojangAccountList::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)
+ {
+ if(value == Qt::Checked)
+ {
+ MojangAccountPtr account = this->at(index.row());
+ this->setActiveAccount(account->username());
+ }
+ }
+
+ emit dataChanged(index, index);
+ return true;
+}
+
void MojangAccountList::updateListData(QList<MojangAccountPtr> versions)
{
beginResetModel();
@@ -303,11 +334,9 @@ bool MojangAccountList::loadList(const QString &filePath)
QLOG_WARN() << "Failed to load an account.";
}
}
- endResetModel();
-
// Load the active account.
- m_activeAccount = root.value("activeAccount").toString("");
-
+ m_activeAccount = findAccount(root.value("activeAccount").toString(""));
+ endResetModel();
return true;
}
@@ -347,8 +376,11 @@ bool MojangAccountList::saveList(const QString &filePath)
// Insert the account list into the root object.
root.insert("accounts", accounts);
- // Save the active account.
- root.insert("activeAccount", m_activeAccount);
+ if(m_activeAccount)
+ {
+ // Save the active account.
+ root.insert("activeAccount", m_activeAccount->username());
+ }
// Create a JSON document object to convert our JSON to bytes.
QJsonDocument doc(root);
diff --git a/logic/lists/MojangAccountList.h b/logic/auth/MojangAccountList.h
index 744f3c51..c7e30958 100644
--- a/logic/lists/MojangAccountList.h
+++ b/logic/auth/MojangAccountList.h
@@ -64,6 +64,8 @@ public:
virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
virtual int rowCount(const QModelIndex &parent) const;
virtual int columnCount(const QModelIndex &parent) const;
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const;
+ virtual bool setData(const QModelIndex &index, const QVariant &value, int role);
/*!
* Adds a the given Mojang account to the account list.
@@ -161,10 +163,9 @@ protected:
QList<MojangAccountPtr> m_accounts;
/*!
- * Username of the account that is currently active.
- * Empty string if no account is active.
+ * Account that is currently active.
*/
- QString m_activeAccount;
+ MojangAccountPtr m_activeAccount;
//! Path to the account list file. Empty string if there isn't one.
QString m_listFilePath;
diff --git a/logic/auth/YggdrasilTask.cpp b/logic/auth/YggdrasilTask.cpp
index 797e84cd..088e1fc0 100644
--- a/logic/auth/YggdrasilTask.cpp
+++ b/logic/auth/YggdrasilTask.cpp
@@ -24,17 +24,11 @@
#include <MultiMC.h>
#include <logic/auth/MojangAccount.h>
+#include <logic/net/URLConstants.h>
-YggdrasilTask::YggdrasilTask(MojangAccountPtr account, QObject *parent) : Task(parent)
+YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent)
+ : Task(parent), m_account(account)
{
- m_error = nullptr;
- m_account = account;
-}
-
-YggdrasilTask::~YggdrasilTask()
-{
- if (m_error)
- delete m_error;
}
void YggdrasilTask::executeTask()
@@ -45,97 +39,126 @@ void YggdrasilTask::executeTask()
QJsonDocument doc(getRequestContent());
auto worker = MMC->qnam();
- connect(worker.get(), SIGNAL(finished(QNetworkReply *)), this,
- SLOT(processReply(QNetworkReply *)));
-
- QUrl reqUrl("https://authserver.mojang.com/" + getEndpoint());
+ QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint());
QNetworkRequest netRequest(reqUrl);
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QByteArray requestData = doc.toJson();
m_netReply = worker->post(netRequest, requestData);
+ connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply);
+ connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers);
+ connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers);
+ timeout_keeper.setSingleShot(true);
+ timeout_keeper.start(timeout_max);
+ counter.setSingleShot(false);
+ counter.start(time_step);
+ progress(0, timeout_max);
+ connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abort);
+ connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat);
}
-void YggdrasilTask::processReply(QNetworkReply *reply)
+void YggdrasilTask::refreshTimers(qint64, qint64)
{
- setStatus(getStateMessage(STATE_PROCESSING_RESPONSE));
+ timeout_keeper.stop();
+ timeout_keeper.start(timeout_max);
+ progress(count = 0, timeout_max);
+}
+void YggdrasilTask::heartbeat()
+{
+ count += time_step;
+ progress(count, timeout_max);
+}
- if (m_netReply != reply)
- // Wrong reply for some reason...
- return;
+void YggdrasilTask::abort()
+{
+ progress(timeout_max, timeout_max);
+ m_netReply->abort();
+}
- if (reply->error() == QNetworkReply::OperationCanceledError)
+void YggdrasilTask::processReply()
+{
+ setStatus(getStateMessage(STATE_PROCESSING_RESPONSE));
+
+ // any network errors lead to offline mode right now
+ if (m_netReply->error() >= QNetworkReply::ConnectionRefusedError &&
+ m_netReply->error() <= QNetworkReply::UnknownNetworkError)
{
+ // WARNING/FIXME: the value here is used in MojangAccount to detect the cancel/timeout
emitFailed("Yggdrasil task cancelled.");
return;
}
- else
+
+ // Try to parse the response regardless of the response code.
+ // Sometimes the auth server will give more information and an error code.
+ QJsonParseError jsonError;
+ QByteArray replyData = m_netReply->readAll();
+ QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
+ // Check the response code.
+ int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (responseCode == 200)
{
- // Try to parse the response regardless of the response code.
- // Sometimes the auth server will give more information and an error code.
- QJsonParseError jsonError;
- QByteArray replyData = reply->readAll();
- QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
- // Check the response code.
- int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
- if (responseCode == 200)
+ // If the response code was 200, then there shouldn't be an error. Make sure
+ // anyways.
+ // Also, sometimes an empty reply indicates success. If there was no data received,
+ // pass an empty json object to the processResponse function.
+ if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0)
{
- // If the response code was 200, then there shouldn't be an error. Make sure anyways.
- // Also, sometimes an empty reply indicates success. If there was no data received,
- // pass an empty json object to the processResponse function.
- if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0)
- {
- if (!processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()))
- {
- YggdrasilTask::Error *err = getError();
- if (err)
- emitFailed(err->getErrorMessage());
- else
- emitFailed(tr("An unknown error occurred when processing the response "
- "from the authentication server."));
- }
- else
- {
- emitSucceeded();
- }
- }
- else
+ if (processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()))
{
- emitFailed(tr("Failed to parse Yggdrasil JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset));
+ emitSucceeded();
+ return;
}
+
+ // errors happened anyway?
+ emitFailed(m_error ? m_error->m_errorMessageVerbose
+ : tr("An unknown error occurred when processing the response "
+ "from the authentication server."));
}
else
{
- // If the response code was not 200, then Yggdrasil may have given us information about the error.
- // If we can parse the response, then get information from it. Otherwise just say there was an unknown error.
- if (jsonError.error == QJsonParseError::NoError)
- {
- // We were able to parse the server's response. Woo!
- // Call processError. If a subclass has overridden it then they'll handle their stuff there.
- QLOG_DEBUG() << "The request failed, but the server gave us an error message. Processing error.";
- emitFailed(processError(doc.object()));
- }
- else
- {
- // The server didn't say anything regarding the error. Give the user an unknown error.
- QLOG_DEBUG() << "The request failed and the server gave no error message. Unknown error.";
- emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(reply->errorString()));
- }
+ emitFailed(tr("Failed to parse Yggdrasil JSON response: %1 at offset %2.")
+ .arg(jsonError.errorString())
+ .arg(jsonError.offset));
}
+ return;
+ }
+
+ // If the response code was not 200, then Yggdrasil may have given us information
+ // about the error.
+ // If we can parse the response, then get information from it. Otherwise just say
+ // there was an unknown error.
+ if (jsonError.error == QJsonParseError::NoError)
+ {
+ // We were able to parse the server's response. Woo!
+ // Call processError. If a subclass has overridden it then they'll handle their
+ // stuff there.
+ QLOG_DEBUG() << "The request failed, but the server gave us an error message. "
+ "Processing error.";
+ emitFailed(processError(doc.object()));
+ }
+ else
+ {
+ // The server didn't say anything regarding the error. Give the user an unknown
+ // error.
+ QLOG_DEBUG() << "The request failed and the server gave no error message. "
+ "Unknown error.";
+ emitFailed(tr("An unknown error occurred when trying to communicate with the "
+ "authentication server: %1").arg(m_netReply->errorString()));
}
}
QString YggdrasilTask::processError(QJsonObject responseData)
{
QJsonValue errorVal = responseData.value("error");
- QJsonValue msgVal = responseData.value("errorMessage");
+ QJsonValue errorMessageValue = responseData.value("errorMessage");
QJsonValue causeVal = responseData.value("cause");
- if (errorVal.isString() && msgVal.isString())
+ if (errorVal.isString() && errorMessageValue.isString())
{
- m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString(""));
- return m_error->getDisplayMessage();
+ m_error = std::shared_ptr<Error>(new Error{
+ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")});
+ return m_error->m_errorMessageVerbose;
}
else
{
@@ -156,13 +179,3 @@ QString YggdrasilTask::getStateMessage(const YggdrasilTask::State state) const
return tr("Processing. Please wait.");
}
}
-
-YggdrasilTask::Error *YggdrasilTask::getError() const
-{
- return this->m_error;
-}
-
-MojangAccountPtr YggdrasilTask::getMojangAccount() const
-{
- return this->m_account;
-}
diff --git a/logic/auth/YggdrasilTask.h b/logic/auth/YggdrasilTask.h
index 62638c9d..1f81a2d0 100644
--- a/logic/auth/YggdrasilTask.h
+++ b/logic/auth/YggdrasilTask.h
@@ -19,6 +19,7 @@
#include <QString>
#include <QJsonObject>
+#include <QTimer>
#include "logic/auth/MojangAccount.h"
@@ -31,45 +32,18 @@ class YggdrasilTask : public Task
{
Q_OBJECT
public:
- explicit YggdrasilTask(MojangAccountPtr account, QObject *parent = 0);
- ~YggdrasilTask();
+ explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0);
/**
* Class describing a Yggdrasil error response.
*/
- class Error
+ struct Error
{
- public:
- Error(const QString& shortError, const QString& errorMessage, const QString& cause) :
- m_shortError(shortError), m_errorMessage(errorMessage), m_cause(cause) {}
-
- QString getShortError() const { return m_shortError; }
- QString getErrorMessage() const { return m_errorMessage; }
- QString getCause() const { return m_cause; }
-
- /// Gets the string to display in the GUI for describing this error.
- QString getDisplayMessage()
- {
- return getErrorMessage();
- }
-
- protected:
- QString m_shortError;
- QString m_errorMessage;
+ QString m_errorMessageShort;
+ QString m_errorMessageVerbose;
QString m_cause;
};
- /**
- * Gets the Mojang account that this task is operating on.
- */
- virtual MojangAccountPtr getMojangAccount() const;
-
- /**
- * Returns a pointer to a YggdrasilTask::Error object if an error has occurred.
- * If no error has occurred, returns a null pointer.
- */
- virtual Error *getError() const;
-
protected:
/**
* Enum for describing the state of the current task.
@@ -120,13 +94,25 @@ protected:
*/
virtual QString getStateMessage(const State state) const;
- MojangAccountPtr m_account;
-
- QNetworkReply *m_netReply;
-
- Error *m_error;
-
protected
slots:
- void processReply(QNetworkReply *reply);
+ void processReply();
+ void refreshTimers(qint64, qint64);
+ void heartbeat();
+
+public
+slots:
+ virtual void abort() override;
+
+protected:
+ // FIXME: segfault disaster waiting to happen
+ MojangAccount *m_account = nullptr;
+ QNetworkReply *m_netReply = nullptr;
+ std::shared_ptr<Error> m_error;
+ QTimer timeout_keeper;
+ QTimer counter;
+ int count = 0; // num msec since time reset
+
+ const int timeout_max = 10000;
+ const int time_step = 50;
};
diff --git a/logic/auth/flows/AuthenticateTask.cpp b/logic/auth/flows/AuthenticateTask.cpp
index ec2004d6..f60be35d 100644
--- a/logic/auth/flows/AuthenticateTask.cpp
+++ b/logic/auth/flows/AuthenticateTask.cpp
@@ -26,7 +26,7 @@
#include "logger/QsLog.h"
-AuthenticateTask::AuthenticateTask(MojangAccountPtr account, const QString &password,
+AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password,
QObject *parent)
: YggdrasilTask(account, parent), m_password(password)
{
@@ -59,14 +59,14 @@ QJsonObject AuthenticateTask::getRequestContent() const
req.insert("agent", agent);
}
- req.insert("username", getMojangAccount()->username());
+ req.insert("username", m_account->username());
req.insert("password", m_password);
req.insert("requestUser", true);
// If we already have a client token, give it to the server.
// Otherwise, let the server give us one.
- if (!getMojangAccount()->clientToken().isEmpty())
- req.insert("clientToken", getMojangAccount()->clientToken());
+ if (!m_account->m_clientToken.isEmpty())
+ req.insert("clientToken", m_account->m_clientToken);
return req;
}
@@ -76,7 +76,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData)
// Read the response data. We need to get the client token, access token, and the selected
// profile.
QLOG_DEBUG() << "Processing authentication response.";
-
+ // QLOG_DEBUG() << responseData;
// If we already have a client token, make sure the one the server gave us matches our
// existing one.
QLOG_DEBUG() << "Getting client token.";
@@ -88,8 +88,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData)
QLOG_ERROR() << "Server didn't send a client token.";
return false;
}
- if (!getMojangAccount()->clientToken().isEmpty() &&
- clientToken != getMojangAccount()->clientToken())
+ if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
{
// The server changed our client token! Obey its wishes, but complain. That's what I do
// for my parents, so...
@@ -97,7 +96,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData)
<< "'. This shouldn't happen, but it isn't really a big deal.";
}
// Set the client token.
- getMojangAccount()->setClientToken(clientToken);
+ m_account->m_clientToken = clientToken;
// Now, we set the access token.
QLOG_DEBUG() << "Getting access token.";
@@ -109,7 +108,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData)
QLOG_ERROR() << "Server didn't send an access token.";
}
// Set the access token.
- getMojangAccount()->setAccessToken(accessToken);
+ m_account->m_accessToken = accessToken;
// Now we load the list of available profiles.
// Mojang hasn't yet implemented the profile system,
@@ -117,13 +116,14 @@ bool AuthenticateTask::processResponse(QJsonObject responseData)
// don't have trouble implementing it later.
QLOG_DEBUG() << "Loading profile list.";
QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
- ProfileList loadedProfiles;
+ QList<AccountProfile> loadedProfiles;
for (auto iter : availableProfiles)
{
QJsonObject profile = iter.toObject();
// Profiles are easy, we just need their ID and name.
QString id = profile.value("id").toString("");
QString name = profile.value("name").toString("");
+ bool legacy = profile.value("legacy").toBool(false);
if (id.isEmpty() || name.isEmpty())
{
@@ -135,10 +135,10 @@ bool AuthenticateTask::processResponse(QJsonObject responseData)
}
// Now, add a new AccountProfile entry to the list.
- loadedProfiles.append(AccountProfile(id, name));
+ loadedProfiles.append({id, name, legacy});
}
// Put the list of profiles we loaded into the MojangAccount object.
- getMojangAccount()->loadProfiles(loadedProfiles);
+ m_account->m_profiles = loadedProfiles;
// Finally, we set the current profile to the correct value. This is pretty simple.
// We do need to make sure that the current profile that the server gave us
@@ -153,7 +153,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData)
QLOG_ERROR() << "Server didn't specify a currently selected profile.";
return false;
}
- if (!getMojangAccount()->setProfile(currentProfileId))
+ if (!m_account->setCurrentProfile(currentProfileId))
{
// TODO: Set an error to display to the user.
QLOG_ERROR() << "Server specified a selected profile that wasn't in the available "
@@ -162,23 +162,20 @@ bool AuthenticateTask::processResponse(QJsonObject responseData)
}
// this is what the vanilla launcher passes to the userProperties launch param
- // doesn't seem to be used for anything so far? I don't get any of this data on my account
- // (peterixxx)
- // is it a good idea to log this?
if (responseData.contains("user"))
{
+ User u;
auto obj = responseData.value("user").toObject();
- auto userId = obj.value("id").toString();
+ u.id = obj.value("id").toString();
auto propArray = obj.value("properties").toArray();
- QLOG_DEBUG() << "User ID: " << userId;
- QLOG_DEBUG() << "User Properties: ";
for (auto prop : propArray)
{
auto propTuple = prop.toObject();
auto name = propTuple.value("name").toString();
auto value = propTuple.value("value").toString();
- QLOG_DEBUG() << name << " : " << value;
+ u.properties.insert(name, value);
}
+ m_account->m_user = u;
}
// We've made it through the minefield of possible errors. Return true to indicate that
diff --git a/logic/auth/flows/AuthenticateTask.h b/logic/auth/flows/AuthenticateTask.h
index 3b99caad..b6564657 100644
--- a/logic/auth/flows/AuthenticateTask.h
+++ b/logic/auth/flows/AuthenticateTask.h
@@ -30,7 +30,7 @@ class AuthenticateTask : public YggdrasilTask
{
Q_OBJECT
public:
- AuthenticateTask(MojangAccountPtr account, const QString &password, QObject *parent = 0);
+ AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0);
protected:
virtual QJsonObject getRequestContent() const;
diff --git a/logic/auth/flows/InvalidateTask.cpp b/logic/auth/flows/InvalidateTask.cpp
deleted file mode 100644
index e69de29b..00000000
--- a/logic/auth/flows/InvalidateTask.cpp
+++ /dev/null
diff --git a/logic/auth/flows/InvalidateTask.h b/logic/auth/flows/InvalidateTask.h
deleted file mode 100644
index e69de29b..00000000
--- a/logic/auth/flows/InvalidateTask.h
+++ /dev/null
diff --git a/logic/auth/flows/RefreshTask.cpp b/logic/auth/flows/RefreshTask.cpp
index b56ed9bc..5f68ccc7 100644
--- a/logic/auth/flows/RefreshTask.cpp
+++ b/logic/auth/flows/RefreshTask.cpp
@@ -25,7 +25,7 @@
#include "logger/QsLog.h"
-RefreshTask::RefreshTask(MojangAccountPtr account, QObject *parent)
+RefreshTask::RefreshTask(MojangAccount *account, QObject *parent)
: YggdrasilTask(account, parent)
{
}
@@ -44,13 +44,12 @@ QJsonObject RefreshTask::getRequestContent() const
* "requestUser": true/false // request the user structure
* }
*/
- auto account = getMojangAccount();
QJsonObject req;
- req.insert("clientToken", account->clientToken());
- req.insert("accessToken", account->accessToken());
+ req.insert("clientToken", m_account->m_clientToken);
+ req.insert("accessToken", m_account->m_accessToken);
/*
{
- auto currentProfile = account->currentProfile();
+ auto currentProfile = m_account->currentProfile();
QJsonObject profile;
profile.insert("id", currentProfile->id());
profile.insert("name", currentProfile->name());
@@ -64,12 +63,11 @@ QJsonObject RefreshTask::getRequestContent() const
bool RefreshTask::processResponse(QJsonObject responseData)
{
- auto account = getMojangAccount();
-
// Read the response data. We need to get the client token, access token, and the selected
// profile.
QLOG_DEBUG() << "Processing authentication response.";
+ // QLOG_DEBUG() << responseData;
// If we already have a client token, make sure the one the server gave us matches our
// existing one.
QString clientToken = responseData.value("clientToken").toString("");
@@ -80,7 +78,7 @@ bool RefreshTask::processResponse(QJsonObject responseData)
QLOG_ERROR() << "Server didn't send a client token.";
return false;
}
- if (!account->clientToken().isEmpty() && clientToken != account->clientToken())
+ if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
{
// The server changed our client token! Obey its wishes, but complain. That's what I do
// for my parents, so...
@@ -104,7 +102,7 @@ bool RefreshTask::processResponse(QJsonObject responseData)
// profile)
QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
QString currentProfileId = currentProfile.value("id").toString("");
- if (account->currentProfile()->id() != currentProfileId)
+ if (m_account->currentProfile()->id != currentProfileId)
{
// TODO: Set an error to display to the user.
QLOG_ERROR() << "Server didn't specify the same selected profile as ours.";
@@ -114,26 +112,26 @@ bool RefreshTask::processResponse(QJsonObject responseData)
// this is what the vanilla launcher passes to the userProperties launch param
if (responseData.contains("user"))
{
+ User u;
auto obj = responseData.value("user").toObject();
- auto userId = obj.value("id").toString();
+ u.id = obj.value("id").toString();
auto propArray = obj.value("properties").toArray();
- QLOG_DEBUG() << "User ID: " << userId;
- QLOG_DEBUG() << "User Properties: ";
for (auto prop : propArray)
{
auto propTuple = prop.toObject();
auto name = propTuple.value("name").toString();
auto value = propTuple.value("value").toString();
- QLOG_DEBUG() << name << " : " << value;
+ u.properties.insert(name, value);
}
+ m_account->m_user = u;
}
+
// We've made it through the minefield of possible errors. Return true to indicate that
// we've succeeded.
QLOG_DEBUG() << "Finished reading refresh response.";
// Reset the access token.
- account->setAccessToken(accessToken);
- account->propagateChange();
+ m_account->m_accessToken = accessToken;
return true;
}
@@ -147,9 +145,9 @@ QString RefreshTask::getStateMessage(const YggdrasilTask::State state) const
switch (state)
{
case STATE_SENDING_REQUEST:
- return tr("Refreshing: Sending request.");
+ return tr("Refreshing login token.");
case STATE_PROCESSING_RESPONSE:
- return tr("Refreshing: Processing response.");
+ return tr("Refreshing login token: Processing response.");
default:
return YggdrasilTask::getStateMessage(state);
}
diff --git a/logic/auth/flows/RefreshTask.h b/logic/auth/flows/RefreshTask.h
index 2596f6c7..2fd50c60 100644
--- a/logic/auth/flows/RefreshTask.h
+++ b/logic/auth/flows/RefreshTask.h
@@ -30,7 +30,7 @@ class RefreshTask : public YggdrasilTask
{
Q_OBJECT
public:
- RefreshTask(MojangAccountPtr account, QObject *parent = 0);
+ RefreshTask(MojangAccount * account, QObject *parent = 0);
protected:
virtual QJsonObject getRequestContent() const;
diff --git a/logic/auth/flows/ValidateTask.cpp b/logic/auth/flows/ValidateTask.cpp
index d9e0e46b..84d5e703 100644
--- a/logic/auth/flows/ValidateTask.cpp
+++ b/logic/auth/flows/ValidateTask.cpp
@@ -26,7 +26,7 @@
#include "logger/QsLog.h"
-ValidateTask::ValidateTask(MojangAccountPtr account, QObject *parent)
+ValidateTask::ValidateTask(MojangAccount * account, QObject *parent)
: YggdrasilTask(account, parent)
{
}
@@ -34,7 +34,7 @@ ValidateTask::ValidateTask(MojangAccountPtr account, QObject *parent)
QJsonObject ValidateTask::getRequestContent() const
{
QJsonObject req;
- req.insert("accessToken", getMojangAccount()->accessToken());
+ req.insert("accessToken", m_account->m_accessToken);
return req;
}
diff --git a/logic/auth/flows/ValidateTask.h b/logic/auth/flows/ValidateTask.h
index 3ff78c6a..0e34f0c3 100644
--- a/logic/auth/flows/ValidateTask.h
+++ b/logic/auth/flows/ValidateTask.h
@@ -13,6 +13,10 @@
* limitations under the License.
*/
+/*
+ * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME:
+ */
+
#pragma once
#include <logic/auth/YggdrasilTask.h>
@@ -28,7 +32,7 @@ class ValidateTask : public YggdrasilTask
{
Q_OBJECT
public:
- ValidateTask(MojangAccountPtr account, QObject *parent = 0);
+ ValidateTask(MojangAccount *account, QObject *parent = 0);
protected:
virtual QJsonObject getRequestContent() const;
diff --git a/logic/lists/JavaVersionList.cpp b/logic/lists/JavaVersionList.cpp
index 3dc1969b..d2f0972c 100644
--- a/logic/lists/JavaVersionList.cpp
+++ b/logic/lists/JavaVersionList.cpp
@@ -21,7 +21,8 @@
#include <QRegExp>
#include "logger/QsLog.h"
-#include <logic/JavaUtils.h>
+#include "logic/JavaCheckerJob.h"
+#include "logic/JavaUtils.h"
JavaVersionList::JavaVersionList(QObject *parent) : BaseVersionList(parent)
{
@@ -49,7 +50,7 @@ int JavaVersionList::count() const
int JavaVersionList::columnCount(const QModelIndex &parent) const
{
- return 4;
+ return 3;
}
QVariant JavaVersionList::data(const QModelIndex &index, int role) const
@@ -75,9 +76,6 @@ QVariant JavaVersionList::data(const QModelIndex &index, int role) const
case 2:
return version->path;
- case 3:
- return version->recommended ? tr("Yes") : tr("No");
-
default:
return QVariant();
}
@@ -109,9 +107,6 @@ QVariant JavaVersionList::headerData(int section, Qt::Orientation orientation, i
case 2:
return "Path";
- case 3:
- return "Recommended";
-
default:
return QVariant();
}
@@ -128,9 +123,6 @@ QVariant JavaVersionList::headerData(int section, Qt::Orientation orientation, i
case 2:
return "Path to this Java version.";
- case 3:
- return "Whether the version is recommended or not.";
-
default:
return QVariant();
}
@@ -142,15 +134,15 @@ QVariant JavaVersionList::headerData(int section, Qt::Orientation orientation, i
BaseVersionPtr JavaVersionList::getTopRecommended() const
{
- for (int i = 0; i < m_vlist.length(); i++)
+ auto first = m_vlist.first();
+ if(first != nullptr)
{
- auto ver = std::dynamic_pointer_cast<JavaVersion>(m_vlist.at(i));
- if (ver->recommended)
- {
- return m_vlist.at(i);
- }
+ return first;
+ }
+ else
+ {
+ return BaseVersionPtr();
}
- return BaseVersionPtr();
}
void JavaVersionList::updateListData(QList<BaseVersionPtr> versions)
@@ -183,20 +175,63 @@ void JavaListLoadTask::executeTask()
setStatus("Detecting Java installations...");
JavaUtils ju;
- QList<JavaVersionPtr> javas = ju.FindJavaPaths();
+ QList<QString> candidate_paths = ju.FindJavaPaths();
+
+ auto job = new JavaCheckerJob("Java detection");
+ connect(job, SIGNAL(finished(QList<JavaCheckResult>)), this, SLOT(javaCheckerFinished(QList<JavaCheckResult>)));
+ connect(job, SIGNAL(progress(int, int)), this, SLOT(checkerProgress(int, int)));
+
+ QLOG_DEBUG() << "Probing the following Java paths: ";
+ for(QString candidate : candidate_paths)
+ {
+ QLOG_DEBUG() << " " << candidate;
+
+ auto candidate_checker = new JavaChecker();
+ candidate_checker->path = candidate;
+ job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker));
+ }
+
+ job->start();
+}
+
+void JavaListLoadTask::checkerProgress(int current, int total)
+{
+ float progress = (current * 100.0) / total;
+ this->setProgress((int) progress);
+}
+
+void JavaListLoadTask::javaCheckerFinished(QList<JavaCheckResult> results)
+{
+ QList<JavaVersionPtr> candidates;
+
+ QLOG_DEBUG() << "Found the following valid Java installations:";
+ for(JavaCheckResult result : results)
+ {
+ if(result.valid)
+ {
+ JavaVersionPtr javaVersion(new JavaVersion());
+
+ javaVersion->id = result.javaVersion;
+ javaVersion->arch = result.mojangPlatform;
+ javaVersion->path = result.path;
+ candidates.append(javaVersion);
+
+ QLOG_DEBUG() << " " << javaVersion->id << javaVersion->arch << javaVersion->path;
+ }
+ }
QList<BaseVersionPtr> javas_bvp;
- for (int i = 0; i < javas.length(); i++)
+ for (auto &java : candidates)
{
- BaseVersionPtr java = std::dynamic_pointer_cast<BaseVersion>(javas.at(i));
+ //QLOG_INFO() << java->id << java->arch << " at " << java->path;
+ BaseVersionPtr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
- if (java)
+ if (bp_java)
{
- javas_bvp.append(java);
+ javas_bvp.append(bp_java);
}
}
m_list->updateListData(javas_bvp);
-
emitSucceeded();
}
diff --git a/logic/lists/JavaVersionList.h b/logic/lists/JavaVersionList.h
index f816c932..879b2480 100644
--- a/logic/lists/JavaVersionList.h
+++ b/logic/lists/JavaVersionList.h
@@ -20,6 +20,7 @@
#include "BaseVersionList.h"
#include "logic/tasks/Task.h"
+#include "logic/JavaCheckerJob.h"
class JavaListLoadTask;
@@ -43,7 +44,6 @@ struct JavaVersion : public BaseVersion
QString id;
QString arch;
QString path;
- bool recommended;
};
typedef std::shared_ptr<JavaVersion> JavaVersionPtr;
@@ -85,6 +85,9 @@ public:
~JavaListLoadTask();
virtual void executeTask();
+public slots:
+ void javaCheckerFinished(QList<JavaCheckResult> results);
+ void checkerProgress(int current, int total);
protected:
JavaVersionList *m_list;
diff --git a/logic/lists/MinecraftVersionList.cpp b/logic/lists/MinecraftVersionList.cpp
index 2a9c0d3b..523b81ac 100644
--- a/logic/lists/MinecraftVersionList.cpp
+++ b/logic/lists/MinecraftVersionList.cpp
@@ -15,6 +15,7 @@
#include "MinecraftVersionList.h"
#include "MultiMC.h"
+#include "logic/net/URLConstants.h"
#include <QtXml>
@@ -28,10 +29,6 @@
#include <QtNetwork>
-#define MCVLIST_URLBASE "http://s3.amazonaws.com/Minecraft.Download/versions/"
-#define ASSETS_URLBASE "http://assets.minecraft.net/"
-#define MCN_URLBASE "http://sonicrules.org/mcnweb.py"
-
MinecraftVersionList::MinecraftVersionList(QObject *parent) : BaseVersionList(parent)
{
}
@@ -144,7 +141,7 @@ void MCVListLoadTask::executeTask()
{
setStatus("Loading instance version list...");
auto worker = MMC->qnam();
- vlistReply = worker->get(QNetworkRequest(QUrl(QString(MCVLIST_URLBASE) + "versions.json")));
+ vlistReply = worker->get(QNetworkRequest(QUrl("http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + "versions.json")));
connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded()));
}
@@ -270,7 +267,7 @@ void MCVListLoadTask::list_downloaded()
continue;
}
// Get the download URL.
- QString dlUrl = QString(MCVLIST_URLBASE) + versionID + "/";
+ QString dlUrl = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + versionID + "/";
// Now, we construct the version object and add it to the list.
std::shared_ptr<MinecraftVersion> mcVersion(new MinecraftVersion());
diff --git a/logic/net/FileDownload.cpp b/logic/net/MD5EtagDownload.cpp
index 239af351..4b9f52af 100644
--- a/logic/net/FileDownload.cpp
+++ b/logic/net/MD5EtagDownload.cpp
@@ -14,12 +14,12 @@
*/
#include "MultiMC.h"
-#include "FileDownload.h"
+#include "MD5EtagDownload.h"
#include <pathutils.h>
#include <QCryptographicHash>
#include "logger/QsLog.h"
-FileDownload::FileDownload(QUrl url, QString target_path) : NetAction()
+MD5EtagDownload::MD5EtagDownload(QUrl url, QString target_path) : NetAction()
{
m_url = url;
m_target_path = target_path;
@@ -27,7 +27,7 @@ FileDownload::FileDownload(QUrl url, QString target_path) : NetAction()
m_status = Job_NotStarted;
}
-void FileDownload::start()
+void MD5EtagDownload::start()
{
QString filename = m_target_path;
m_output_file.setFileName(filename);
@@ -58,11 +58,20 @@ void FileDownload::start()
return;
}
- QLOG_INFO() << "Downloading " << m_url.toString();
+ QLOG_INFO() << "Downloading " << m_url.toString() << " expecting " << m_expected_md5;
QNetworkRequest request(m_url);
request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1());
request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+ // Go ahead and try to open the file.
+ // If we don't do this, empty files won't be created, which breaks the updater.
+ // Plus, this way, we don't end up starting a download for a file we can't open.
+ if (!m_output_file.open(QIODevice::WriteOnly))
+ {
+ emit failed(index_within_job);
+ return;
+ }
+
auto worker = MMC->qnam();
QNetworkReply *rep = worker->get(request);
@@ -75,19 +84,19 @@ void FileDownload::start()
connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
}
-void FileDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+void MD5EtagDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
emit progress(index_within_job, bytesReceived, bytesTotal);
}
-void FileDownload::downloadError(QNetworkReply::NetworkError error)
+void MD5EtagDownload::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
// TODO: log the reason why
m_status = Job_Failed;
}
-void FileDownload::downloadFinished()
+void MD5EtagDownload::downloadFinished()
{
// if the download succeeded
if (m_status != Job_Failed)
@@ -96,6 +105,7 @@ void FileDownload::downloadFinished()
m_status = Job_Finished;
m_output_file.close();
+ QLOG_INFO() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData();
m_reply.reset();
emit succeeded(index_within_job);
return;
@@ -110,7 +120,7 @@ void FileDownload::downloadFinished()
}
}
-void FileDownload::downloadReadyRead()
+void MD5EtagDownload::downloadReadyRead()
{
if (!m_output_file.isOpen())
{
diff --git a/logic/net/FileDownload.h b/logic/net/MD5EtagDownload.h
index 58380e86..416ab9de 100644
--- a/logic/net/FileDownload.h
+++ b/logic/net/MD5EtagDownload.h
@@ -18,8 +18,8 @@
#include "NetAction.h"
#include <QFile>
-typedef std::shared_ptr<class FileDownload> FileDownloadPtr;
-class FileDownload : public NetAction
+typedef std::shared_ptr<class MD5EtagDownload> Md5EtagDownloadPtr;
+class MD5EtagDownload : public NetAction
{
Q_OBJECT
public:
@@ -35,10 +35,10 @@ public:
QFile m_output_file;
public:
- explicit FileDownload(QUrl url, QString target_path);
- static FileDownloadPtr make(QUrl url, QString target_path)
+ explicit MD5EtagDownload(QUrl url, QString target_path);
+ static Md5EtagDownloadPtr make(QUrl url, QString target_path)
{
- return FileDownloadPtr(new FileDownload(url, target_path));
+ return Md5EtagDownloadPtr(new MD5EtagDownload(url, target_path));
}
protected
slots:
diff --git a/logic/net/NetJob.cpp b/logic/net/NetJob.cpp
index 333cdcbf..8b79bc54 100644
--- a/logic/net/NetJob.cpp
+++ b/logic/net/NetJob.cpp
@@ -16,7 +16,7 @@
#include "NetJob.h"
#include "pathutils.h"
#include "MultiMC.h"
-#include "FileDownload.h"
+#include "MD5EtagDownload.h"
#include "ByteArrayDownload.h"
#include "CacheDownload.h"
diff --git a/logic/net/NetJob.h b/logic/net/NetJob.h
index 021a1550..6e2e7607 100644
--- a/logic/net/NetJob.h
+++ b/logic/net/NetJob.h
@@ -18,7 +18,7 @@
#include <QLabel>
#include "NetAction.h"
#include "ByteArrayDownload.h"
-#include "FileDownload.h"
+#include "MD5EtagDownload.h"
#include "CacheDownload.h"
#include "HttpMetaCache.h"
#include "ForgeXzDownload.h"
@@ -94,6 +94,8 @@ signals:
public
slots:
virtual void start();
+ // FIXME: implement
+ virtual void abort() {};
private
slots:
void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal);
diff --git a/logic/net/PasteUpload.cpp b/logic/net/PasteUpload.cpp
new file mode 100644
index 00000000..acf09291
--- /dev/null
+++ b/logic/net/PasteUpload.cpp
@@ -0,0 +1,84 @@
+#include "PasteUpload.h"
+#include "MultiMC.h"
+#include "logger/QsLog.h"
+#include <QJsonObject>
+#include <QJsonDocument>
+#include "gui/dialogs/CustomMessageBox.h"
+#include <QDesktopServices>
+
+PasteUpload::PasteUpload(QWidget *window, QString text) : m_text(text), m_window(window)
+{
+}
+
+void PasteUpload::executeTask()
+{
+ QNetworkRequest request(QUrl("http://paste.ee/api"));
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+ QByteArray content(
+ "key=public&description=MultiMC5+Log+File&language=plain&format=json&paste=" +
+ m_text.toUtf8());
+ request.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
+ request.setRawHeader("Content-Length", QByteArray::number(content.size()));
+
+ auto worker = MMC->qnam();
+ QNetworkReply *rep = worker->post(request, content);
+
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+ connect(rep, &QNetworkReply::downloadProgress, [&](qint64 value, qint64 max)
+ { setProgress(value / max * 100); });
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this,
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void PasteUpload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ QLOG_ERROR() << "Network error: " << error;
+ emitFailed(m_reply->errorString());
+}
+
+void PasteUpload::downloadFinished()
+{
+ // if the download succeeded
+ if (m_reply->error() == QNetworkReply::NetworkError::NoError)
+ {
+ QByteArray data = m_reply->readAll();
+ m_reply.reset();
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed(jsonError.errorString());
+ return;
+ }
+ QString error;
+ if (!parseResult(doc, &error))
+ {
+ emitFailed(error);
+ return;
+ }
+ }
+ // else the download failed
+ else
+ {
+ emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
+ m_reply.reset();
+ return;
+ }
+ emitSucceeded();
+}
+
+bool PasteUpload::parseResult(QJsonDocument doc, QString *parseError)
+{
+ auto object = doc.object();
+ auto status = object.value("status").toString("error");
+ if (status == "error")
+ {
+ parseError = new QString(object.value("error").toString());
+ return false;
+ }
+ QString pasteUrl = object.value("paste").toObject().value("link").toString();
+ QDesktopServices::openUrl(pasteUrl);
+ return true;
+}
diff --git a/logic/net/PasteUpload.h b/logic/net/PasteUpload.h
new file mode 100644
index 00000000..917a0016
--- /dev/null
+++ b/logic/net/PasteUpload.h
@@ -0,0 +1,26 @@
+#pragma once
+#include "logic/tasks/Task.h"
+#include <QMessageBox>
+#include <QNetworkReply>
+#include <memory>
+
+class PasteUpload : public Task
+{
+ Q_OBJECT
+public:
+ PasteUpload(QWidget *window, QString text);
+
+protected:
+ virtual void executeTask();
+
+private:
+ bool parseResult(QJsonDocument doc, QString *parseError);
+ QString m_text;
+ QString m_error;
+ QWidget *m_window;
+ std::shared_ptr<QNetworkReply> m_reply;
+public
+slots:
+ void downloadError(QNetworkReply::NetworkError);
+ void downloadFinished();
+};
diff --git a/logic/net/S3ListBucket.cpp b/logic/net/S3ListBucket.cpp
index 89dff951..439b7086 100644
--- a/logic/net/S3ListBucket.cpp
+++ b/logic/net/S3ListBucket.cpp
@@ -102,7 +102,6 @@ void S3ListBucket::processValidReply()
};
// nothing went wrong...
- QString prefix("http://s3.amazonaws.com/Minecraft.Resources/");
QByteArray ba = m_reply->readAll();
QString xmlErrorMsg;
diff --git a/logic/net/URLConstants.h b/logic/net/URLConstants.h
new file mode 100644
index 00000000..dcd5c2b1
--- /dev/null
+++ b/logic/net/URLConstants.h
@@ -0,0 +1,32 @@
+/* Copyright 2013 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 <QString>
+
+namespace URLConstants
+{
+const QString AWS_DOWNLOAD_BASE("s3.amazonaws.com/Minecraft.Download/");
+const QString AWS_DOWNLOAD_VERSIONS(AWS_DOWNLOAD_BASE + "versions/");
+const QString AWS_DOWNLOAD_LIBRARIES(AWS_DOWNLOAD_BASE + "libraries/");
+const QString AWS_DOWNLOAD_INDEXES(AWS_DOWNLOAD_BASE + "indexes/");
+const QString ASSETS_BASE("assets.minecraft.net/");
+//const QString MCN_BASE("sonicrules.org/mcnweb.py");
+const QString RESOURCE_BASE("resources.download.minecraft.net/");
+const QString LIBRARY_BASE("libraries.minecraft.net/");
+const QString SKINS_BASE("skins.minecraft.net/MinecraftSkins/");
+const QString AUTH_BASE("authserver.mojang.com/");
+}
diff --git a/logic/tasks/ProgressProvider.h b/logic/tasks/ProgressProvider.h
index f6f2906a..15e453a3 100644
--- a/logic/tasks/ProgressProvider.h
+++ b/logic/tasks/ProgressProvider.h
@@ -38,4 +38,5 @@ public:
public
slots:
virtual void start() = 0;
+ virtual void abort() = 0;
};
diff --git a/logic/tasks/Task.h b/logic/tasks/Task.h
index d08ef560..80d5e38b 100644
--- a/logic/tasks/Task.h
+++ b/logic/tasks/Task.h
@@ -44,6 +44,7 @@ public:
public
slots:
virtual void start();
+ virtual void abort() {};
protected:
virtual void executeTask() = 0;
diff --git a/logic/updater/DownloadUpdateTask.cpp b/logic/updater/DownloadUpdateTask.cpp
new file mode 100644
index 00000000..d9aab826
--- /dev/null
+++ b/logic/updater/DownloadUpdateTask.cpp
@@ -0,0 +1,404 @@
+/* Copyright 2013 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 "DownloadUpdateTask.h"
+
+#include "MultiMC.h"
+#include "logic/updater/UpdateChecker.h"
+#include "logic/net/NetJob.h"
+#include "pathutils.h"
+
+#include <QFile>
+#include <QTemporaryDir>
+#include <QCryptographicHash>
+
+#include <QDomDocument>
+
+
+DownloadUpdateTask::DownloadUpdateTask(QString repoUrl, int versionId, QObject* parent) :
+ Task(parent)
+{
+ m_cVersionId = MMC->version().build;
+
+ m_nRepoUrl = repoUrl;
+ m_nVersionId = versionId;
+
+ m_updateFilesDir.setAutoRemove(false);
+}
+
+void DownloadUpdateTask::executeTask()
+{
+ // GO!
+ // This will call the next step when it's done.
+ findCurrentVersionInfo();
+}
+
+void DownloadUpdateTask::findCurrentVersionInfo()
+{
+ setStatus(tr("Finding information about the current version."));
+
+ auto checker = MMC->updateChecker();
+
+ // This runs after we've tried loading the channel list.
+ // If the channel list doesn't need to be loaded, this will be called immediately.
+ // If the channel list does need to be loaded, this will be called when it's done.
+ auto processFunc = [this, &checker] () -> void
+ {
+ // Now, check the channel list again.
+ if (checker->hasChannels())
+ {
+ // We still couldn't load the channel list. Give up. Call loadVersionInfo and return.
+ QLOG_INFO() << "Reloading the channel list didn't work. Giving up.";
+ loadVersionInfo();
+ return;
+ }
+
+ QList<UpdateChecker::ChannelListEntry> channels = checker->getChannelList();
+ QString channelId = MMC->version().channel;
+
+ // Search through the channel list for a channel with the correct ID.
+ for (auto channel : channels)
+ {
+ if (channel.id == channelId)
+ {
+ QLOG_INFO() << "Found matching channel.";
+ m_cRepoUrl = channel.url;
+ break;
+ }
+ }
+
+ // Now that we've done that, load version info.
+ loadVersionInfo();
+ };
+
+ if (checker->hasChannels())
+ {
+ // Load the channel list and wait for it to finish loading.
+ QLOG_INFO() << "No channel list entries found. Will try reloading it.";
+
+ QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, processFunc);
+ checker->updateChanList();
+ }
+ else
+ {
+ processFunc();
+ }
+}
+
+void DownloadUpdateTask::loadVersionInfo()
+{
+ setStatus(tr("Loading version information."));
+
+ // Create the net job for loading version info.
+ NetJob* netJob = new NetJob("Version Info");
+
+ // Find the index URL.
+ QUrl newIndexUrl = QUrl(m_nRepoUrl).resolved(QString::number(m_nVersionId) + ".json");
+
+ // Add a net action to download the version info for the version we're updating to.
+ netJob->addNetAction(ByteArrayDownload::make(newIndexUrl));
+
+ // If we have a current version URL, get that one too.
+ if (!m_cRepoUrl.isEmpty())
+ {
+ QUrl cIndexUrl = QUrl(m_cRepoUrl).resolved(QString::number(m_cVersionId) + ".json");
+ netJob->addNetAction(ByteArrayDownload::make(cIndexUrl));
+ }
+
+ // Connect slots so we know when it's done.
+ QObject::connect(netJob, &NetJob::succeeded, this, &DownloadUpdateTask::vinfoDownloadFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::vinfoDownloadFailed);
+
+ // Store the NetJob in a class member. We don't want to lose it!
+ m_vinfoNetJob.reset(netJob);
+
+ // Finally, we start the network job and the thread's event loop to wait for it to finish.
+ netJob->start();
+}
+
+void DownloadUpdateTask::vinfoDownloadFinished()
+{
+ // Both downloads succeeded. OK. Parse stuff.
+ parseDownloadedVersionInfo();
+}
+
+void DownloadUpdateTask::vinfoDownloadFailed()
+{
+ // Something failed. We really need the second download (current version info), so parse downloads anyways as long as the first one succeeded.
+ if (m_vinfoNetJob->first()->m_status != Job_Failed)
+ {
+ parseDownloadedVersionInfo();
+ return;
+ }
+
+ // TODO: Give a more detailed error message.
+ QLOG_ERROR() << "Failed to download version info files.";
+ emitFailed(tr("Failed to download version info files."));
+}
+
+void DownloadUpdateTask::parseDownloadedVersionInfo()
+{
+ setStatus(tr("Reading file lists."));
+
+ parseVersionInfo(NEW_VERSION, &m_nVersionFileList);
+
+ // If there is a second entry in the network job's list, load it as the current version's info.
+ if (m_vinfoNetJob->size() >= 2 && m_vinfoNetJob->operator[](1)->m_status != Job_Failed)
+ {
+ parseVersionInfo(CURRENT_VERSION, &m_cVersionFileList);
+ }
+
+ // We don't need this any more.
+ m_vinfoNetJob.reset();
+
+ // Now that we're done loading version info, we can move on to the next step. Process file lists and download files.
+ processFileLists();
+}
+
+void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list)
+{
+ if (vfile == CURRENT_VERSION) setStatus(tr("Reading file list for current version."));
+ else if (vfile == NEW_VERSION) setStatus(tr("Reading file list for new version."));
+
+ QLOG_DEBUG() << "Reading file list for" << (vfile == NEW_VERSION ? "new" : "current") << "version.";
+
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(
+ vfile == NEW_VERSION ? m_vinfoNetJob->first() : m_vinfoNetJob->operator[](1));
+ data = dl->m_data;
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ QLOG_ERROR() << "Failed to parse version info JSON:" << jsonError.errorString() << "at" << jsonError.offset;
+ return;
+ }
+
+ QJsonObject json = jsonDoc.object();
+
+ QLOG_DEBUG() << "Loading version info from JSON.";
+ QJsonArray filesArray = json.value("Files").toArray();
+ for (QJsonValue fileValue : filesArray)
+ {
+ QJsonObject fileObj = fileValue.toObject();
+
+ VersionFileEntry file{
+ fileObj.value("Path").toString(),
+ fileObj.value("Perms").toVariant().toInt(),
+ FileSourceList(),
+ fileObj.value("MD5").toString(),
+ };
+ QLOG_DEBUG() << "File" << file.path << "with perms" << file.mode;
+
+ QJsonArray sourceArray = fileObj.value("Sources").toArray();
+ for (QJsonValue val : sourceArray)
+ {
+ QJsonObject sourceObj = val.toObject();
+
+ QString type = sourceObj.value("SourceType").toString();
+ if (type == "http")
+ {
+ file.sources.append(FileSource("http", sourceObj.value("Url").toString()));
+ }
+ else if (type == "httpc")
+ {
+ file.sources.append(FileSource("httpc", sourceObj.value("Url").toString(), sourceObj.value("CompressionType").toString()));
+ }
+ else
+ {
+ QLOG_WARN() << "Unknown source type" << type << "ignored.";
+ }
+ }
+
+ QLOG_DEBUG() << "Loaded info for" << file.path;
+
+ list->append(file);
+ }
+}
+
+void DownloadUpdateTask::processFileLists()
+{
+ setStatus(tr("Processing file lists. Figuring out how to install the update."));
+
+ // First, if we've loaded the current version's file list, we need to iterate through it and
+ // delete anything in the current one version's list that isn't in the new version's list.
+ for (VersionFileEntry entry : m_cVersionFileList)
+ {
+ bool keep = false;
+ for (VersionFileEntry newEntry : m_nVersionFileList)
+ {
+ if (newEntry.path == entry.path)
+ {
+ QLOG_DEBUG() << "Not deleting" << entry.path << "because it is still present in the new version.";
+ keep = true;
+ break;
+ }
+ }
+ // If the loop reaches the end and we didn't find a match, delete the file.
+ if(!keep)
+ m_operationList.append(UpdateOperation::DeleteOp(entry.path));
+ }
+
+ // Create a network job for downloading files.
+ NetJob* netJob = new NetJob("Update Files");
+
+ // Next, check each file in MultiMC's folder and see if we need to update them.
+ for (VersionFileEntry entry : m_nVersionFileList)
+ {
+ // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a way to do this in the background.
+ QString fileMD5;
+ QFile entryFile(entry.path);
+ if (entryFile.open(QFile::ReadOnly))
+ {
+ QCryptographicHash hash(QCryptographicHash::Md5);
+ hash.addData(entryFile.readAll());
+ fileMD5 = hash.result().toHex();
+ }
+
+ if (!entryFile.exists() || fileMD5.isEmpty() || fileMD5 != entry.md5)
+ {
+ QLOG_DEBUG() << "Found file" << entry.path << "that needs updating.";
+
+ // Go through the sources list and find one to use.
+ // TODO: Make a NetAction that takes a source list and tries each of them until one works. For now, we'll just use the first http one.
+ for (FileSource source : entry.sources)
+ {
+ if (source.type == "http")
+ {
+ QLOG_DEBUG() << "Will download" << entry.path << "from" << source.url;
+
+ // Download it to updatedir/<filepath>-<md5> where filepath is the file's path with slashes replaced by underscores.
+ QString dlPath = PathCombine(m_updateFilesDir.path(), QString(entry.path).replace("/", "_"));
+
+ // We need to download the file to the updatefiles folder and add a task to copy it to its install path.
+ auto download = MD5EtagDownload::make(source.url, dlPath);
+ download->m_check_md5 = true;
+ download->m_expected_md5 = entry.md5;
+ netJob->addNetAction(download);
+
+ // Now add a copy operation to our operations list to install the file.
+ m_operationList.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode));
+ }
+ }
+ }
+ }
+
+ // Add listeners to wait for the downloads to finish.
+ QObject::connect(netJob, &NetJob::succeeded, this, &DownloadUpdateTask::fileDownloadFinished);
+ QObject::connect(netJob, &NetJob::progress, this, &DownloadUpdateTask::fileDownloadProgressChanged);
+ QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::fileDownloadFailed);
+
+ // Now start the download.
+ setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
+ QLOG_DEBUG() << "Begin downloading update files to" << m_updateFilesDir.path();
+ m_filesNetJob.reset(netJob);
+ netJob->start();
+
+ writeInstallScript(m_operationList, PathCombine(m_updateFilesDir.path(), "file_list.xml"));
+}
+
+void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QString scriptFile)
+{
+ // Build the base structure of the XML document.
+ QDomDocument doc;
+
+ QDomElement root = doc.createElement("update");
+ root.setAttribute("version", "3");
+ doc.appendChild(root);
+
+ QDomElement installFiles = doc.createElement("install");
+ root.appendChild(installFiles);
+
+ QDomElement removeFiles = doc.createElement("uninstall");
+ root.appendChild(removeFiles);
+
+ // Write the operation list to the XML document.
+ for (UpdateOperation op : opsList)
+ {
+ QDomElement file = doc.createElement("file");
+
+ switch (op.type)
+ {
+ case UpdateOperation::OP_COPY:
+ {
+ // Install the file.
+ QDomElement name = doc.createElement("source");
+ QDomElement path = doc.createElement("dest");
+ QDomElement mode = doc.createElement("mode");
+ name.appendChild(doc.createTextNode(op.file));
+ path.appendChild(doc.createTextNode(op.dest));
+ // We need to add a 0 at the beginning here, because Qt doesn't convert to octal correctly.
+ mode.appendChild(doc.createTextNode("0" + QString::number(op.mode, 8)));
+ file.appendChild(name);
+ file.appendChild(path);
+ file.appendChild(mode);
+ installFiles.appendChild(file);
+ QLOG_DEBUG() << "Will install file" << op.file;
+ }
+ break;
+
+ case UpdateOperation::OP_DELETE:
+ {
+ // Delete the file.
+ file.appendChild(doc.createTextNode(op.file));
+ removeFiles.appendChild(file);
+ QLOG_DEBUG() << "Will remove file" << op.file;
+ }
+ break;
+
+ default:
+ QLOG_WARN() << "Can't write update operation of type" << op.type << "to file. Not implemented.";
+ continue;
+ }
+ }
+
+ // Write the XML document to the file.
+ QFile outFile(scriptFile);
+
+ if (outFile.open(QIODevice::WriteOnly))
+ {
+ outFile.write(doc.toByteArray());
+ }
+ else
+ {
+ emitFailed(tr("Failed to write update script file."));
+ }
+}
+
+void DownloadUpdateTask::fileDownloadFinished()
+{
+ emitSucceeded();
+}
+
+void DownloadUpdateTask::fileDownloadFailed()
+{
+ // TODO: Give more info about the failure.
+ QLOG_ERROR() << "Failed to download update files.";
+ emitFailed(tr("Failed to download update files."));
+}
+
+void DownloadUpdateTask::fileDownloadProgressChanged(qint64 current, qint64 total)
+{
+ setProgress((int)(((float)current / (float)total)*100));
+}
+
+QString DownloadUpdateTask::updateFilesDir()
+{
+ return m_updateFilesDir.path();
+}
+
diff --git a/logic/updater/DownloadUpdateTask.h b/logic/updater/DownloadUpdateTask.h
new file mode 100644
index 00000000..f5b23d12
--- /dev/null
+++ b/logic/updater/DownloadUpdateTask.h
@@ -0,0 +1,192 @@
+/* Copyright 2013 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 "logic/tasks/Task.h"
+#include "logic/net/NetJob.h"
+
+/*!
+ * The DownloadUpdateTask is a task that takes a given version ID and repository URL,
+ * downloads that version's files from the repository, and prepares to install them.
+ */
+class DownloadUpdateTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit DownloadUpdateTask(QString repoUrl, int versionId, QObject* parent=0);
+
+ /*!
+ * Gets the directory that contains the update files.
+ */
+ QString updateFilesDir();
+
+protected:
+ // TODO: We should probably put these data structures into a separate header...
+
+ /*!
+ * Struct that describes an entry in a VersionFileEntry's `Sources` list.
+ */
+ struct FileSource
+ {
+ FileSource(QString type, QString url, QString compression="")
+ {
+ this->type = type;
+ this->url = url;
+ this->compressionType = compression;
+ }
+
+ QString type;
+ QString url;
+ QString compressionType;
+ };
+
+ typedef QList<FileSource> FileSourceList;
+
+ /*!
+ * Structure that describes an entry in a GoUpdate version's `Files` list.
+ */
+ struct VersionFileEntry
+ {
+ QString path;
+ int mode;
+ FileSourceList sources;
+ QString md5;
+ };
+
+ typedef QList<VersionFileEntry> VersionFileList;
+
+
+ /*!
+ * Structure that describes an operation to perform when installing updates.
+ */
+ struct UpdateOperation
+ {
+ static UpdateOperation CopyOp(QString fsource, QString fdest, int fmode=0644) { return UpdateOperation{OP_COPY, fsource, fdest, fmode}; }
+ static UpdateOperation MoveOp(QString fsource, QString fdest, int fmode=0644) { return UpdateOperation{OP_MOVE, fsource, fdest, fmode}; }
+ static UpdateOperation DeleteOp(QString file) { return UpdateOperation{OP_DELETE, file, "", 0644}; }
+ static UpdateOperation ChmodOp(QString file, int fmode) { return UpdateOperation{OP_CHMOD, file, "", fmode}; }
+
+ //! Specifies the type of operation that this is.
+ enum Type
+ {
+ OP_COPY,
+ OP_DELETE,
+ OP_MOVE,
+ OP_CHMOD,
+ } type;
+
+ //! The file to operate on. If this is a DELETE or CHMOD operation, this is the file that will be modified.
+ QString file;
+
+ //! The destination file. If this is a DELETE or CHMOD operation, this field will be ignored.
+ QString dest;
+
+ //! The mode to change the source file to. Ignored if this isn't a CHMOD operation.
+ int mode;
+
+ // Yeah yeah, polymorphism blah blah inheritance, blah blah object oriented. I'm lazy, OK?
+ };
+
+ typedef QList<UpdateOperation> UpdateOperationList;
+
+ /*!
+ * Used for arguments to parseVersionInfo and friends to specify which version info file to parse.
+ */
+ enum VersionInfoFileEnum { NEW_VERSION, CURRENT_VERSION };
+
+
+ //! Entry point for tasks.
+ virtual void executeTask();
+
+ /*!
+ * Attempts to find the version ID and repository URL for the current version.
+ * The function will look up the repository URL in the UpdateChecker's channel list.
+ * If the repository URL can't be found, this function will return false.
+ */
+ virtual void findCurrentVersionInfo();
+
+ /*!
+ * Downloads the version info files from the repository.
+ * The files for both the current build, and the build that we're updating to need to be downloaded.
+ * If the current version's info file can't be found, MultiMC will not delete files that
+ * were removed between versions. It will still replace files that have changed, however.
+ * Note that although the repository URL for the current version is not given to the update task,
+ * the task will attempt to look it up in the UpdateChecker's channel list.
+ * If an error occurs here, the function will call emitFailed and return false.
+ */
+ virtual void loadVersionInfo();
+
+ /*!
+ * This function is called when version information is finished downloading.
+ * This handles parsing the JSON downloaded by the version info network job and then calls processFileLists.
+ * Note that this function will sometimes be called even if the version info download emits failed. If
+ * we couldn't download the current version's info file, we can still update. This will be called even if the
+ * current version's info file fails to download, as long as the new version's info file succeeded.
+ */
+ virtual void parseDownloadedVersionInfo();
+
+ /*!
+ * Loads the file list from the given version info JSON object into the given list.
+ */
+ virtual void parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list);
+
+ /*!
+ * Takes a list of file entries for the current version's files and the new version's files
+ * and populates the downloadList and operationList with information about how to download and install the update.
+ */
+ virtual void processFileLists();
+
+ /*!
+ * Takes the operations list and writes an install script for the updater to the update files directory.
+ */
+ virtual void writeInstallScript(UpdateOperationList& opsList, QString scriptFile);
+
+ VersionFileList m_downloadList;
+ UpdateOperationList m_operationList;
+
+ VersionFileList m_nVersionFileList;
+ VersionFileList m_cVersionFileList;
+
+ //! Network job for downloading version info files.
+ NetJobPtr m_vinfoNetJob;
+
+ //! Network job for downloading update files.
+ NetJobPtr m_filesNetJob;
+
+ // Version ID and repo URL for the new version.
+ int m_nVersionId;
+ QString m_nRepoUrl;
+
+ // Version ID and repo URL for the currently installed version.
+ int m_cVersionId;
+ QString m_cRepoUrl;
+
+ /*!
+ * Temporary directory to store update files in.
+ * This will be set to not auto delete. Task will fail if this fails to be created.
+ */
+ QTemporaryDir m_updateFilesDir;
+
+protected slots:
+ void vinfoDownloadFinished();
+ void vinfoDownloadFailed();
+
+ void fileDownloadFinished();
+ void fileDownloadFailed();
+ void fileDownloadProgressChanged(qint64 current, qint64 total);
+};
+
diff --git a/logic/updater/UpdateChecker.cpp b/logic/updater/UpdateChecker.cpp
new file mode 100644
index 00000000..5ff1898e
--- /dev/null
+++ b/logic/updater/UpdateChecker.cpp
@@ -0,0 +1,247 @@
+/* Copyright 2013 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 "UpdateChecker.h"
+
+#include "MultiMC.h"
+
+#include "config.h"
+#include "logger/QsLog.h"
+
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+
+#define API_VERSION 0
+#define CHANLIST_FORMAT 0
+
+UpdateChecker::UpdateChecker()
+{
+ m_currentChannel = VERSION_CHANNEL;
+ m_channelListUrl = CHANLIST_URL;
+ m_updateChecking = false;
+ m_chanListLoading = false;
+ m_checkUpdateWaiting = false;
+ m_chanListLoaded = false;
+}
+
+QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const
+{
+ return m_channels;
+}
+
+bool UpdateChecker::hasChannels() const
+{
+ return m_channels.isEmpty();
+}
+
+void UpdateChecker::checkForUpdate()
+{
+ QLOG_DEBUG() << "Checking for updates.";
+
+ // If the channel list hasn't loaded yet, load it and defer checking for updates until later.
+ if (!m_chanListLoaded)
+ {
+ QLOG_DEBUG() << "Channel list isn't loaded yet. Loading channel list and deferring update check.";
+ m_checkUpdateWaiting = true;
+ updateChanList();
+ return;
+ }
+
+ if (m_updateChecking)
+ {
+ QLOG_DEBUG() << "Ignoring update check request. Already checking for updates.";
+ return;
+ }
+
+ m_updateChecking = true;
+
+ // Get the URL for the channel we're using.
+ // TODO: Allow user to select channels. For now, we'll just use the current channel.
+ QString updateChannel = m_currentChannel;
+
+ // Find the desired channel within the channel list and get its repo URL. If if cannot be found, error.
+ m_repoUrl = "";
+ for (ChannelListEntry entry : m_channels)
+ {
+ if (entry.id == updateChannel)
+ m_repoUrl = entry.url;
+ }
+
+ // If we didn't find our channel, error.
+ if (m_repoUrl.isEmpty())
+ {
+ emit updateCheckFailed();
+ return;
+ }
+
+ QUrl indexUrl = QUrl(m_repoUrl).resolved(QUrl("index.json"));
+
+ auto job = new NetJob("GoUpdate Repository Index");
+ job->addNetAction(ByteArrayDownload::make(indexUrl));
+ connect(job, SIGNAL(succeeded()), SLOT(updateCheckFinished()));
+ connect(job, SIGNAL(failed()), SLOT(updateCheckFailed()));
+ indexJob.reset(job);
+ job->start();
+}
+
+void UpdateChecker::updateCheckFinished()
+{
+ QLOG_DEBUG() << "Finished downloading repo index. Checking for new versions.";
+
+ QJsonParseError jsonError;
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(indexJob->first());
+ data = dl->m_data;
+ indexJob.reset();
+ }
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject())
+ {
+ QLOG_ERROR() << "Failed to parse GoUpdate repository index. JSON error" << jsonError.errorString() << "at offset" << jsonError.offset;
+ return;
+ }
+
+ QJsonObject object = jsonDoc.object();
+
+ bool success = false;
+ int apiVersion = object.value("ApiVersion").toVariant().toInt(&success);
+ if (apiVersion != API_VERSION || !success)
+ {
+ QLOG_ERROR() << "Failed to check for updates. API version mismatch. We're using" << API_VERSION << "server has" << apiVersion;
+ return;
+ }
+
+ QLOG_DEBUG() << "Processing repository version list.";
+ QJsonObject newestVersion;
+ QJsonArray versions = object.value("Versions").toArray();
+ for (QJsonValue versionVal : versions)
+ {
+ QJsonObject version = versionVal.toObject();
+ if (newestVersion.value("Id").toVariant().toInt() < version.value("Id").toVariant().toInt())
+ {
+ QLOG_DEBUG() << "Found newer version with ID" << version.value("Id").toVariant().toInt();
+ newestVersion = version;
+ }
+ }
+
+ // We've got the version with the greatest ID number. Now compare it to our current build number and update if they're different.
+ int newBuildNumber = newestVersion.value("Id").toVariant().toInt();
+ if (newBuildNumber != MMC->version().build)
+ {
+ // Update!
+ emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(), newBuildNumber);
+ }
+
+ m_updateChecking = false;
+}
+
+void UpdateChecker::updateCheckFailed()
+{
+ // TODO: log errors better
+ QLOG_ERROR() << "Update check failed for reasons unknown.";
+}
+
+void UpdateChecker::updateChanList()
+{
+ QLOG_DEBUG() << "Loading the channel list.";
+
+ if (m_channelListUrl.isEmpty())
+ {
+ QLOG_ERROR() << "Failed to update channel list. No channel list URL set."
+ << "If you'd like to use MultiMC's update system, please pass the channel list URL to CMake at compile time.";
+ return;
+ }
+
+ m_chanListLoading = true;
+ NetJob* job = new NetJob("Update System Channel List");
+ job->addNetAction(ByteArrayDownload::make(QUrl(m_channelListUrl)));
+ QObject::connect(job, &NetJob::succeeded, this, &UpdateChecker::chanListDownloadFinished);
+ QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed);
+ chanListJob.reset(job);
+ job->start();
+}
+
+void UpdateChecker::chanListDownloadFinished()
+{
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(chanListJob->first());
+ data = dl->m_data;
+ chanListJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ // TODO: Report errors to the user.
+ QLOG_ERROR() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset;
+ return;
+ }
+
+ QJsonObject object = jsonDoc.object();
+
+ bool success = false;
+ int formatVersion = object.value("format_version").toVariant().toInt(&success);
+ if (formatVersion != CHANLIST_FORMAT || !success)
+ {
+ QLOG_ERROR() << "Failed to check for updates. Channel list format version mismatch. We're using" << CHANLIST_FORMAT << "server has" << formatVersion;
+ return;
+ }
+
+ // Load channels into a temporary array.
+ QList<ChannelListEntry> loadedChannels;
+ QJsonArray channelArray = object.value("channels").toArray();
+ for (QJsonValue chanVal : channelArray)
+ {
+ QJsonObject channelObj = chanVal.toObject();
+ ChannelListEntry entry{
+ channelObj.value("id").toVariant().toString(),
+ channelObj.value("name").toVariant().toString(),
+ channelObj.value("description").toVariant().toString(),
+ channelObj.value("url").toVariant().toString()
+ };
+ if (entry.id.isEmpty() || entry.name.isEmpty() || entry.url.isEmpty())
+ {
+ QLOG_ERROR() << "Channel list entry with empty ID, name, or URL. Skipping.";
+ continue;
+ }
+ loadedChannels.append(entry);
+ }
+
+ // Swap the channel list we just loaded into the object's channel list.
+ m_channels.swap(loadedChannels);
+
+ m_chanListLoading = false;
+ m_chanListLoaded = true;
+ QLOG_INFO() << "Successfully loaded UpdateChecker channel list.";
+
+ // If we're waiting to check for updates, do that now.
+ if (m_checkUpdateWaiting)
+ checkForUpdate();
+
+ emit channelListLoaded();
+}
+
+void UpdateChecker::chanListDownloadFailed()
+{
+ m_chanListLoading = false;
+ QLOG_ERROR() << "Failed to download channel list.";
+ emit channelListLoaded();
+}
+
diff --git a/logic/updater/UpdateChecker.h b/logic/updater/UpdateChecker.h
new file mode 100644
index 00000000..59fb8e47
--- /dev/null
+++ b/logic/updater/UpdateChecker.h
@@ -0,0 +1,106 @@
+/* Copyright 2013 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 "logic/net/NetJob.h"
+
+#include <QUrl>
+
+class UpdateChecker : public QObject
+{
+ Q_OBJECT
+
+public:
+ UpdateChecker();
+ void checkForUpdate();
+
+ /*!
+ * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake).
+ * If this isn't called before checkForUpdate(), it will automatically be called.
+ */
+ void updateChanList();
+
+ /*!
+ * An entry in the channel list.
+ */
+ struct ChannelListEntry
+ {
+ QString id;
+ QString name;
+ QString description;
+ QString url;
+ };
+
+ /*!
+ * Returns a the current channel list.
+ * If the channel list hasn't been loaded, this list will be empty.
+ */
+ QList<ChannelListEntry> getChannelList() const;
+
+ /*!
+ * Returns true if the channel list is empty.
+ */
+ bool hasChannels() const;
+
+signals:
+ //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version.
+ void updateAvailable(QString repoUrl, QString versionName, int versionId);
+
+ //! Signal emitted when the channel list finishes loading or fails to load.
+ void channelListLoaded();
+
+private slots:
+ void updateCheckFinished();
+ void updateCheckFailed();
+
+ void chanListDownloadFinished();
+ void chanListDownloadFailed();
+
+private:
+ NetJobPtr indexJob;
+ NetJobPtr chanListJob;
+
+ QString m_repoUrl;
+
+ QString m_channelListUrl;
+ QString m_currentChannel;
+
+ QList<ChannelListEntry> m_channels;
+
+ /*!
+ * True while the system is checking for updates.
+ * If checkForUpdate is called while this is true, it will be ignored.
+ */
+ bool m_updateChecking;
+
+ /*!
+ * True if the channel list has loaded.
+ * If this is false, trying to check for updates will call updateChanList first.
+ */
+ bool m_chanListLoaded;
+
+ /*!
+ * Set to true while the channel list is currently loading.
+ */
+ bool m_chanListLoading;
+
+ /*!
+ * Set to true when checkForUpdate is called while the channel list isn't loaded.
+ * When the channel list finishes loading, if this is true, the update checker will check for updates.
+ */
+ bool m_checkUpdateWaiting;
+};
+