summaryrefslogtreecommitdiffstats
path: root/api/logic
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2016-04-10 15:53:05 +0200
committerPetr Mrázek <peterix@gmail.com>2016-05-01 00:00:14 +0200
commitb6d455a02bd338e9dc0faa09d4d8177ecd8d569a (patch)
tree41982bca1ede50049f2f8c7109dd18edeefde6d0 /api/logic
parent47e37635f50c09b4f9a9ee7699e3120bab3e4088 (diff)
downloadMultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.tar
MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.tar.gz
MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.tar.lz
MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.tar.xz
MultiMC-b6d455a02bd338e9dc0faa09d4d8177ecd8d569a.zip
NOISSUE reorganize and document libraries
Diffstat (limited to 'api/logic')
-rw-r--r--api/logic/AbstractCommonModel.cpp133
-rw-r--r--api/logic/AbstractCommonModel.h462
-rw-r--r--api/logic/BaseConfigObject.cpp103
-rw-r--r--api/logic/BaseConfigObject.h50
-rw-r--r--api/logic/BaseInstaller.cpp61
-rw-r--r--api/logic/BaseInstaller.h46
-rw-r--r--api/logic/BaseInstance.cpp270
-rw-r--r--api/logic/BaseInstance.h243
-rw-r--r--api/logic/BaseVersion.h59
-rw-r--r--api/logic/BaseVersionList.cpp104
-rw-r--r--api/logic/BaseVersionList.h126
-rw-r--r--api/logic/CMakeLists.txt344
-rw-r--r--api/logic/Commandline.cpp483
-rw-r--r--api/logic/Commandline.h252
-rw-r--r--api/logic/DefaultVariable.h35
-rw-r--r--api/logic/Env.cpp222
-rw-r--r--api/logic/Env.h60
-rw-r--r--api/logic/Exception.h34
-rw-r--r--api/logic/FileSystem.cpp436
-rw-r--r--api/logic/FileSystem.h123
-rw-r--r--api/logic/GZip.cpp115
-rw-r--r--api/logic/GZip.h12
-rw-r--r--api/logic/InstanceList.cpp580
-rw-r--r--api/logic/InstanceList.h187
-rw-r--r--api/logic/Json.cpp272
-rw-r--r--api/logic/Json.h249
-rw-r--r--api/logic/MMCStrings.cpp76
-rw-r--r--api/logic/MMCStrings.h10
-rw-r--r--api/logic/MMCZip.cpp491
-rw-r--r--api/logic/MMCZip.h88
-rw-r--r--api/logic/NullInstance.h90
-rw-r--r--api/logic/QObjectPtr.h78
-rw-r--r--api/logic/RWStorage.h60
-rw-r--r--api/logic/RecursiveFileSystemWatcher.cpp111
-rw-r--r--api/logic/RecursiveFileSystemWatcher.h63
-rw-r--r--api/logic/SeparatorPrefixTree.h298
-rw-r--r--api/logic/TypeMagic.h37
-rw-r--r--api/logic/Version.cpp140
-rw-r--r--api/logic/Version.h110
-rw-r--r--api/logic/java/JavaChecker.cpp159
-rw-r--r--api/logic/java/JavaChecker.h54
-rw-r--r--api/logic/java/JavaCheckerJob.cpp45
-rw-r--r--api/logic/java/JavaCheckerJob.h84
-rw-r--r--api/logic/java/JavaInstall.cpp28
-rw-r--r--api/logic/java/JavaInstall.h38
-rw-r--r--api/logic/java/JavaInstallList.cpp186
-rw-r--r--api/logic/java/JavaInstallList.h71
-rw-r--r--api/logic/java/JavaUtils.cpp219
-rw-r--r--api/logic/java/JavaUtils.h43
-rw-r--r--api/logic/java/JavaVersion.cpp112
-rw-r--r--api/logic/java/JavaVersion.h30
-rw-r--r--api/logic/launch/LaunchStep.cpp27
-rw-r--r--api/logic/launch/LaunchStep.h48
-rw-r--r--api/logic/launch/LaunchTask.cpp228
-rw-r--r--api/logic/launch/LaunchTask.h122
-rw-r--r--api/logic/launch/LoggedProcess.cpp163
-rw-r--r--api/logic/launch/LoggedProcess.h76
-rw-r--r--api/logic/launch/MessageLevel.cpp36
-rw-r--r--api/logic/launch/MessageLevel.h28
-rw-r--r--api/logic/launch/steps/CheckJava.cpp92
-rw-r--r--api/logic/launch/steps/CheckJava.h41
-rw-r--r--api/logic/launch/steps/LaunchMinecraft.cpp155
-rw-r--r--api/logic/launch/steps/LaunchMinecraft.h48
-rw-r--r--api/logic/launch/steps/ModMinecraftJar.cpp44
-rw-r--r--api/logic/launch/steps/ModMinecraftJar.h39
-rw-r--r--api/logic/launch/steps/PostLaunchCommand.cpp84
-rw-r--r--api/logic/launch/steps/PostLaunchCommand.h39
-rw-r--r--api/logic/launch/steps/PreLaunchCommand.cpp85
-rw-r--r--api/logic/launch/steps/PreLaunchCommand.h39
-rw-r--r--api/logic/launch/steps/TextPrint.cpp29
-rw-r--r--api/logic/launch/steps/TextPrint.h43
-rw-r--r--api/logic/launch/steps/Update.cpp50
-rw-r--r--api/logic/launch/steps/Update.h41
-rw-r--r--api/logic/minecraft/AssetsUtils.cpp230
-rw-r--r--api/logic/minecraft/AssetsUtils.h48
-rw-r--r--api/logic/minecraft/GradleSpecifier.h129
-rw-r--r--api/logic/minecraft/JarMod.h12
-rw-r--r--api/logic/minecraft/Library.cpp239
-rw-r--r--api/logic/minecraft/Library.h184
-rw-r--r--api/logic/minecraft/MinecraftInstance.cpp369
-rw-r--r--api/logic/minecraft/MinecraftInstance.h69
-rw-r--r--api/logic/minecraft/MinecraftProfile.cpp610
-rw-r--r--api/logic/minecraft/MinecraftProfile.h200
-rw-r--r--api/logic/minecraft/MinecraftVersion.cpp215
-rw-r--r--api/logic/minecraft/MinecraftVersion.h119
-rw-r--r--api/logic/minecraft/MinecraftVersionList.cpp591
-rw-r--r--api/logic/minecraft/MinecraftVersionList.h72
-rw-r--r--api/logic/minecraft/Mod.cpp377
-rw-r--r--api/logic/minecraft/Mod.h134
-rw-r--r--api/logic/minecraft/ModList.cpp616
-rw-r--r--api/logic/minecraft/ModList.h160
-rw-r--r--api/logic/minecraft/MojangDownloadInfo.h71
-rw-r--r--api/logic/minecraft/MojangVersionFormat.cpp381
-rw-r--r--api/logic/minecraft/MojangVersionFormat.h25
-rw-r--r--api/logic/minecraft/OpSys.cpp42
-rw-r--r--api/logic/minecraft/OpSys.h37
-rw-r--r--api/logic/minecraft/ParseUtils.cpp34
-rw-r--r--api/logic/minecraft/ParseUtils.h11
-rw-r--r--api/logic/minecraft/ProfilePatch.h104
-rw-r--r--api/logic/minecraft/ProfileStrategy.h35
-rw-r--r--api/logic/minecraft/ProfileUtils.cpp191
-rw-r--r--api/logic/minecraft/ProfileUtils.h25
-rw-r--r--api/logic/minecraft/Rule.cpp93
-rw-r--r--api/logic/minecraft/Rule.h101
-rw-r--r--api/logic/minecraft/VersionBuildError.h58
-rw-r--r--api/logic/minecraft/VersionFile.cpp60
-rw-r--r--api/logic/minecraft/VersionFile.h195
-rw-r--r--api/logic/minecraft/VersionFilterData.cpp75
-rw-r--r--api/logic/minecraft/VersionFilterData.h32
-rw-r--r--api/logic/minecraft/World.cpp385
-rw-r--r--api/logic/minecraft/World.h83
-rw-r--r--api/logic/minecraft/WorldList.cpp355
-rw-r--r--api/logic/minecraft/WorldList.h125
-rw-r--r--api/logic/minecraft/auth/AuthSession.cpp30
-rw-r--r--api/logic/minecraft/auth/AuthSession.h51
-rw-r--r--api/logic/minecraft/auth/MojangAccount.cpp278
-rw-r--r--api/logic/minecraft/auth/MojangAccount.h173
-rw-r--r--api/logic/minecraft/auth/MojangAccountList.cpp427
-rw-r--r--api/logic/minecraft/auth/MojangAccountList.h201
-rw-r--r--api/logic/minecraft/auth/YggdrasilTask.cpp255
-rw-r--r--api/logic/minecraft/auth/YggdrasilTask.h150
-rw-r--r--api/logic/minecraft/auth/flows/AuthenticateTask.cpp202
-rw-r--r--api/logic/minecraft/auth/flows/AuthenticateTask.h46
-rw-r--r--api/logic/minecraft/auth/flows/RefreshTask.cpp144
-rw-r--r--api/logic/minecraft/auth/flows/RefreshTask.h44
-rw-r--r--api/logic/minecraft/auth/flows/ValidateTask.cpp61
-rw-r--r--api/logic/minecraft/auth/flows/ValidateTask.h47
-rw-r--r--api/logic/minecraft/forge/ForgeInstaller.cpp458
-rw-r--r--api/logic/minecraft/forge/ForgeInstaller.h52
-rw-r--r--api/logic/minecraft/forge/ForgeVersion.cpp55
-rw-r--r--api/logic/minecraft/forge/ForgeVersion.h42
-rw-r--r--api/logic/minecraft/forge/ForgeVersionList.cpp450
-rw-r--r--api/logic/minecraft/forge/ForgeVersionList.h90
-rw-r--r--api/logic/minecraft/forge/ForgeXzDownload.cpp358
-rw-r--r--api/logic/minecraft/forge/ForgeXzDownload.h59
-rw-r--r--api/logic/minecraft/forge/LegacyForge.cpp56
-rw-r--r--api/logic/minecraft/forge/LegacyForge.h25
-rw-r--r--api/logic/minecraft/ftb/FTBPlugin.cpp395
-rw-r--r--api/logic/minecraft/ftb/FTBPlugin.h13
-rw-r--r--api/logic/minecraft/ftb/FTBProfileStrategy.cpp128
-rw-r--r--api/logic/minecraft/ftb/FTBProfileStrategy.h21
-rw-r--r--api/logic/minecraft/ftb/FTBVersion.h32
-rw-r--r--api/logic/minecraft/ftb/LegacyFTBInstance.cpp27
-rw-r--r--api/logic/minecraft/ftb/LegacyFTBInstance.h17
-rw-r--r--api/logic/minecraft/ftb/OneSixFTBInstance.cpp138
-rw-r--r--api/logic/minecraft/ftb/OneSixFTBInstance.h30
-rw-r--r--api/logic/minecraft/legacy/LegacyInstance.cpp453
-rw-r--r--api/logic/minecraft/legacy/LegacyInstance.h142
-rw-r--r--api/logic/minecraft/legacy/LegacyUpdate.cpp393
-rw-r--r--api/logic/minecraft/legacy/LegacyUpdate.h70
-rw-r--r--api/logic/minecraft/legacy/LwjglVersionList.cpp189
-rw-r--r--api/logic/minecraft/legacy/LwjglVersionList.h156
-rw-r--r--api/logic/minecraft/liteloader/LiteLoaderInstaller.cpp142
-rw-r--r--api/logic/minecraft/liteloader/LiteLoaderInstaller.h39
-rw-r--r--api/logic/minecraft/liteloader/LiteLoaderVersionList.cpp276
-rw-r--r--api/logic/minecraft/liteloader/LiteLoaderVersionList.h119
-rw-r--r--api/logic/minecraft/onesix/OneSixInstance.cpp597
-rw-r--r--api/logic/minecraft/onesix/OneSixInstance.h117
-rw-r--r--api/logic/minecraft/onesix/OneSixProfileStrategy.cpp418
-rw-r--r--api/logic/minecraft/onesix/OneSixProfileStrategy.h26
-rw-r--r--api/logic/minecraft/onesix/OneSixUpdate.cpp342
-rw-r--r--api/logic/minecraft/onesix/OneSixUpdate.h67
-rw-r--r--api/logic/minecraft/onesix/OneSixVersionFormat.cpp225
-rw-r--r--api/logic/minecraft/onesix/OneSixVersionFormat.h22
-rw-r--r--api/logic/net/ByteArrayDownload.cpp105
-rw-r--r--api/logic/net/ByteArrayDownload.h48
-rw-r--r--api/logic/net/CacheDownload.cpp192
-rw-r--r--api/logic/net/CacheDownload.h63
-rw-r--r--api/logic/net/HttpMetaCache.cpp273
-rw-r--r--api/logic/net/HttpMetaCache.h125
-rw-r--r--api/logic/net/MD5EtagDownload.cpp155
-rw-r--r--api/logic/net/MD5EtagDownload.h52
-rw-r--r--api/logic/net/NetAction.h96
-rw-r--r--api/logic/net/NetJob.cpp125
-rw-r--r--api/logic/net/NetJob.h117
-rw-r--r--api/logic/net/PasteUpload.cpp99
-rw-r--r--api/logic/net/PasteUpload.h50
-rw-r--r--api/logic/net/URLConstants.cpp16
-rw-r--r--api/logic/net/URLConstants.h40
-rw-r--r--api/logic/news/NewsChecker.cpp135
-rw-r--r--api/logic/news/NewsChecker.h107
-rw-r--r--api/logic/news/NewsEntry.cpp77
-rw-r--r--api/logic/news/NewsEntry.h65
-rw-r--r--api/logic/notifications/NotificationChecker.cpp130
-rw-r--r--api/logic/notifications/NotificationChecker.h63
-rw-r--r--api/logic/pathmatcher/FSTreeMatcher.h21
-rw-r--r--api/logic/pathmatcher/IPathMatcher.h12
-rw-r--r--api/logic/pathmatcher/MultiMatcher.h31
-rw-r--r--api/logic/pathmatcher/RegexpMatcher.h42
-rw-r--r--api/logic/resources/Resource.cpp155
-rw-r--r--api/logic/resources/Resource.h132
-rw-r--r--api/logic/resources/ResourceHandler.cpp28
-rw-r--r--api/logic/resources/ResourceHandler.h36
-rw-r--r--api/logic/resources/ResourceObserver.cpp55
-rw-r--r--api/logic/resources/ResourceObserver.h73
-rw-r--r--api/logic/resources/ResourceProxyModel.cpp89
-rw-r--r--api/logic/resources/ResourceProxyModel.h39
-rw-r--r--api/logic/screenshots/ImgurAlbumCreation.cpp90
-rw-r--r--api/logic/screenshots/ImgurAlbumCreation.h44
-rw-r--r--api/logic/screenshots/ImgurUpload.cpp114
-rw-r--r--api/logic/screenshots/ImgurUpload.h33
-rw-r--r--api/logic/screenshots/Screenshot.h19
-rw-r--r--api/logic/settings/INIFile.cpp151
-rw-r--r--api/logic/settings/INIFile.h38
-rw-r--r--api/logic/settings/INISettingsObject.cpp107
-rw-r--r--api/logic/settings/INISettingsObject.h66
-rw-r--r--api/logic/settings/OverrideSetting.cpp54
-rw-r--r--api/logic/settings/OverrideSetting.h46
-rw-r--r--api/logic/settings/PassthroughSetting.cpp66
-rw-r--r--api/logic/settings/PassthroughSetting.h45
-rw-r--r--api/logic/settings/Setting.cpp53
-rw-r--r--api/logic/settings/Setting.h119
-rw-r--r--api/logic/settings/SettingsObject.cpp142
-rw-r--r--api/logic/settings/SettingsObject.h214
-rw-r--r--api/logic/status/StatusChecker.cpp153
-rw-r--r--api/logic/status/StatusChecker.h60
-rw-r--r--api/logic/tasks/SequentialTask.cpp55
-rw-r--r--api/logic/tasks/SequentialTask.h31
-rw-r--r--api/logic/tasks/Task.cpp88
-rw-r--r--api/logic/tasks/Task.h96
-rw-r--r--api/logic/tasks/ThreadTask.cpp41
-rw-r--r--api/logic/tasks/ThreadTask.h25
-rw-r--r--api/logic/tools/BaseExternalTool.cpp41
-rw-r--r--api/logic/tools/BaseExternalTool.h60
-rw-r--r--api/logic/tools/BaseProfiler.cpp35
-rw-r--r--api/logic/tools/BaseProfiler.h38
-rw-r--r--api/logic/tools/JProfiler.cpp116
-rw-r--r--api/logic/tools/JProfiler.h15
-rw-r--r--api/logic/tools/JVisualVM.cpp103
-rw-r--r--api/logic/tools/JVisualVM.h15
-rw-r--r--api/logic/tools/MCEditTool.cpp124
-rw-r--r--api/logic/tools/MCEditTool.h26
-rw-r--r--api/logic/trans/TranslationDownloader.cpp53
-rw-r--r--api/logic/trans/TranslationDownloader.h32
-rw-r--r--api/logic/updater/DownloadTask.cpp169
-rw-r--r--api/logic/updater/DownloadTask.h95
-rw-r--r--api/logic/updater/GoUpdate.cpp215
-rw-r--r--api/logic/updater/GoUpdate.h133
-rw-r--r--api/logic/updater/UpdateChecker.cpp269
-rw-r--r--api/logic/updater/UpdateChecker.h121
-rw-r--r--api/logic/wonko/BaseWonkoEntity.cpp39
-rw-r--r--api/logic/wonko/BaseWonkoEntity.h51
-rw-r--r--api/logic/wonko/WonkoIndex.cpp147
-rw-r--r--api/logic/wonko/WonkoIndex.h68
-rw-r--r--api/logic/wonko/WonkoReference.cpp44
-rw-r--r--api/logic/wonko/WonkoReference.h41
-rw-r--r--api/logic/wonko/WonkoUtil.cpp47
-rw-r--r--api/logic/wonko/WonkoUtil.h31
-rw-r--r--api/logic/wonko/WonkoVersion.cpp102
-rw-r--r--api/logic/wonko/WonkoVersion.h83
-rw-r--r--api/logic/wonko/WonkoVersionList.cpp283
-rw-r--r--api/logic/wonko/WonkoVersionList.h92
-rw-r--r--api/logic/wonko/format/WonkoFormat.cpp80
-rw-r--r--api/logic/wonko/format/WonkoFormat.h54
-rw-r--r--api/logic/wonko/format/WonkoFormatV1.cpp156
-rw-r--r--api/logic/wonko/format/WonkoFormatV1.h30
-rw-r--r--api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp117
-rw-r--r--api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h81
-rw-r--r--api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp126
-rw-r--r--api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h85
260 files changed, 32792 insertions, 0 deletions
diff --git a/api/logic/AbstractCommonModel.cpp b/api/logic/AbstractCommonModel.cpp
new file mode 100644
index 00000000..71d75829
--- /dev/null
+++ b/api/logic/AbstractCommonModel.cpp
@@ -0,0 +1,133 @@
+/* Copyright 2015 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 "AbstractCommonModel.h"
+
+BaseAbstractCommonModel::BaseAbstractCommonModel(const Qt::Orientation orientation, QObject *parent)
+ : QAbstractListModel(parent), m_orientation(orientation)
+{
+}
+
+int BaseAbstractCommonModel::rowCount(const QModelIndex &parent) const
+{
+ return m_orientation == Qt::Horizontal ? entryCount() : size();
+}
+int BaseAbstractCommonModel::columnCount(const QModelIndex &parent) const
+{
+ return m_orientation == Qt::Horizontal ? size() : entryCount();
+}
+QVariant BaseAbstractCommonModel::data(const QModelIndex &index, int role) const
+{
+ if (!hasIndex(index.row(), index.column(), index.parent()))
+ {
+ return QVariant();
+ }
+ const int i = m_orientation == Qt::Horizontal ? index.column() : index.row();
+ const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column();
+ return formatData(i, role, get(i, entry, role));
+}
+QVariant BaseAbstractCommonModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation != m_orientation && role == Qt::DisplayRole)
+ {
+ return entryTitle(section);
+ }
+ else
+ {
+ return QVariant();
+ }
+}
+bool BaseAbstractCommonModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ const int i = m_orientation == Qt::Horizontal ? index.column() : index.row();
+ const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column();
+ const bool result = set(i, entry, role, sanetizeData(i, role, value));
+ if (result)
+ {
+ emit dataChanged(index, index, QVector<int>() << role);
+ }
+ return result;
+}
+Qt::ItemFlags BaseAbstractCommonModel::flags(const QModelIndex &index) const
+{
+ if (!hasIndex(index.row(), index.column(), index.parent()))
+ {
+ return Qt::NoItemFlags;
+ }
+
+ const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column();
+ if (canSet(entry))
+ {
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+ }
+ else
+ {
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ }
+}
+
+void BaseAbstractCommonModel::notifyAboutToAddObject(const int at)
+{
+ if (m_orientation == Qt::Horizontal)
+ {
+ beginInsertColumns(QModelIndex(), at, at);
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), at, at);
+ }
+}
+void BaseAbstractCommonModel::notifyObjectAdded()
+{
+ if (m_orientation == Qt::Horizontal)
+ {
+ endInsertColumns();
+ }
+ else
+ {
+ endInsertRows();
+ }
+}
+void BaseAbstractCommonModel::notifyAboutToRemoveObject(const int at)
+{
+ if (m_orientation == Qt::Horizontal)
+ {
+ beginRemoveColumns(QModelIndex(), at, at);
+ }
+ else
+ {
+ beginRemoveRows(QModelIndex(), at, at);
+ }
+}
+void BaseAbstractCommonModel::notifyObjectRemoved()
+{
+ if (m_orientation == Qt::Horizontal)
+ {
+ endRemoveColumns();
+ }
+ else
+ {
+ endRemoveRows();
+ }
+}
+
+void BaseAbstractCommonModel::notifyBeginReset()
+{
+ beginResetModel();
+}
+void BaseAbstractCommonModel::notifyEndReset()
+{
+ endResetModel();
+}
diff --git a/api/logic/AbstractCommonModel.h b/api/logic/AbstractCommonModel.h
new file mode 100644
index 00000000..31b86a23
--- /dev/null
+++ b/api/logic/AbstractCommonModel.h
@@ -0,0 +1,462 @@
+/* Copyright 2015 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 <QAbstractListModel>
+#include <type_traits>
+#include <functional>
+#include <memory>
+
+class BaseAbstractCommonModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ explicit BaseAbstractCommonModel(const Qt::Orientation orientation, QObject *parent = nullptr);
+
+ // begin QAbstractItemModel interface
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ // end QAbstractItemModel interface
+
+ virtual int size() const = 0;
+ virtual int entryCount() const = 0;
+
+ virtual QVariant formatData(const int index, int role, const QVariant &data) const { return data; }
+ virtual QVariant sanetizeData(const int index, int role, const QVariant &data) const { return data; }
+
+protected:
+ virtual QVariant get(const int index, const int entry, const int role) const = 0;
+ virtual bool set(const int index, const int entry, const int role, const QVariant &value) = 0;
+ virtual bool canSet(const int entry) const = 0;
+ virtual QString entryTitle(const int entry) const = 0;
+
+ void notifyAboutToAddObject(const int at);
+ void notifyObjectAdded();
+ void notifyAboutToRemoveObject(const int at);
+ void notifyObjectRemoved();
+ void notifyBeginReset();
+ void notifyEndReset();
+
+ const Qt::Orientation m_orientation;
+};
+
+template<typename Object>
+class AbstractCommonModel : public BaseAbstractCommonModel
+{
+public:
+ explicit AbstractCommonModel(const Qt::Orientation orientation)
+ : BaseAbstractCommonModel(orientation) {}
+ virtual ~AbstractCommonModel() {}
+
+ int size() const override { return m_objects.size(); }
+ int entryCount() const override { return m_entries.size(); }
+
+ void append(const Object &object)
+ {
+ notifyAboutToAddObject(size());
+ m_objects.append(object);
+ notifyObjectAdded();
+ }
+ void prepend(const Object &object)
+ {
+ notifyAboutToAddObject(0);
+ m_objects.prepend(object);
+ notifyObjectAdded();
+ }
+ void insert(const Object &object, const int index)
+ {
+ if (index >= size())
+ {
+ prepend(object);
+ }
+ else if (index <= 0)
+ {
+ append(object);
+ }
+ else
+ {
+ notifyAboutToAddObject(index);
+ m_objects.insert(index, object);
+ notifyObjectAdded();
+ }
+ }
+ void remove(const int index)
+ {
+ notifyAboutToRemoveObject(index);
+ m_objects.removeAt(index);
+ notifyObjectRemoved();
+ }
+ Object get(const int index) const
+ {
+ return m_objects.at(index);
+ }
+
+private:
+ friend class CommonModel;
+ QVariant get(const int index, const int entry, const int role) const override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(role))
+ {
+ return QVariant();
+ }
+ return m_entries[entry].second.value(role)->get(m_objects.at(index));
+ }
+ bool set(const int index, const int entry, const int role, const QVariant &value) override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(role))
+ {
+ return false;
+ }
+ IEntry *e = m_entries[entry].second.value(role);
+ if (!e->canSet())
+ {
+ return false;
+ }
+ e->set(m_objects[index], value);
+ return true;
+ }
+ bool canSet(const int entry) const override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(Qt::EditRole))
+ {
+ return false;
+ }
+ IEntry *e = m_entries[entry].second.value(Qt::EditRole);
+ return e->canSet();
+ }
+
+ QString entryTitle(const int entry) const override
+ {
+ return m_entries.at(entry).first;
+ }
+
+private:
+ struct IEntry
+ {
+ virtual ~IEntry() {}
+ virtual void set(Object &object, const QVariant &value) = 0;
+ virtual QVariant get(const Object &object) const = 0;
+ virtual bool canSet() const = 0;
+ };
+ template<typename T>
+ struct VariableEntry : public IEntry
+ {
+ typedef T (Object::*Member);
+
+ explicit VariableEntry(Member member)
+ : m_member(member) {}
+
+ void set(Object &object, const QVariant &value) override
+ {
+ object.*m_member = value.value<T>();
+ }
+ QVariant get(const Object &object) const override
+ {
+ return QVariant::fromValue<T>(object.*m_member);
+ }
+ bool canSet() const override { return true; }
+
+ private:
+ Member m_member;
+ };
+ template<typename T>
+ struct FunctionEntry : public IEntry
+ {
+ typedef T (Object::*Getter)() const;
+ typedef void (Object::*Setter)(T);
+
+ explicit FunctionEntry(Getter getter, Setter setter)
+ : m_getter(m_getter), m_setter(m_setter) {}
+
+ void set(Object &object, const QVariant &value) override
+ {
+ object.*m_setter(value.value<T>());
+ }
+ QVariant get(const Object &object) const override
+ {
+ return QVariant::fromValue<T>(object.*m_getter());
+ }
+ bool canSet() const override { return !!m_setter; }
+
+ private:
+ Getter m_getter;
+ Setter m_setter;
+ };
+
+ QList<Object> m_objects;
+ QVector<QPair<QString, QMap<int, IEntry *>>> m_entries;
+
+ void addEntryInternal(IEntry *e, const int entry, const int role)
+ {
+ if (m_entries.size() <= entry)
+ {
+ m_entries.resize(entry + 1);
+ }
+ m_entries[entry].second.insert(role, e);
+ }
+
+protected:
+ template<typename Getter, typename Setter>
+ typename std::enable_if<std::is_member_function_pointer<Getter>::value && std::is_member_function_pointer<Getter>::value, void>::type
+ addEntry(Getter getter, Setter setter, const int entry, const int role)
+ {
+ addEntryInternal(new FunctionEntry<typename std::result_of<Getter>::type>(getter, setter), entry, role);
+ }
+ template<typename Getter>
+ typename std::enable_if<std::is_member_function_pointer<Getter>::value, void>::type
+ addEntry(Getter getter, const int entry, const int role)
+ {
+ addEntryInternal(new FunctionEntry<typename std::result_of<Getter>::type>(getter, nullptr), entry, role);
+ }
+ template<typename T>
+ typename std::enable_if<!std::is_member_function_pointer<T (Object::*)>::value, void>::type
+ addEntry(T (Object::*member), const int entry, const int role)
+ {
+ addEntryInternal(new VariableEntry<T>(member), entry, role);
+ }
+
+ void setEntryTitle(const int entry, const QString &title)
+ {
+ m_entries[entry].first = title;
+ }
+};
+template<typename Object>
+class AbstractCommonModel<Object *> : public BaseAbstractCommonModel
+{
+public:
+ explicit AbstractCommonModel(const Qt::Orientation orientation)
+ : BaseAbstractCommonModel(orientation) {}
+ virtual ~AbstractCommonModel()
+ {
+ qDeleteAll(m_objects);
+ }
+
+ int size() const override { return m_objects.size(); }
+ int entryCount() const override { return m_entries.size(); }
+
+ void append(Object *object)
+ {
+ notifyAboutToAddObject(size());
+ m_objects.append(object);
+ notifyObjectAdded();
+ }
+ void prepend(Object *object)
+ {
+ notifyAboutToAddObject(0);
+ m_objects.prepend(object);
+ notifyObjectAdded();
+ }
+ void insert(Object *object, const int index)
+ {
+ if (index >= size())
+ {
+ prepend(object);
+ }
+ else if (index <= 0)
+ {
+ append(object);
+ }
+ else
+ {
+ notifyAboutToAddObject(index);
+ m_objects.insert(index, object);
+ notifyObjectAdded();
+ }
+ }
+ void remove(const int index)
+ {
+ notifyAboutToRemoveObject(index);
+ m_objects.removeAt(index);
+ notifyObjectRemoved();
+ }
+ Object *get(const int index) const
+ {
+ return m_objects.at(index);
+ }
+ int find(Object * const obj) const
+ {
+ return m_objects.indexOf(obj);
+ }
+
+ QList<Object *> getAll() const
+ {
+ return m_objects;
+ }
+
+private:
+ friend class CommonModel;
+ QVariant get(const int index, const int entry, const int role) const override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(role))
+ {
+ return QVariant();
+ }
+ return m_entries[entry].second.value(role)->get(m_objects.at(index));
+ }
+ bool set(const int index, const int entry, const int role, const QVariant &value) override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(role))
+ {
+ return false;
+ }
+ IEntry *e = m_entries[entry].second.value(role);
+ if (!e->canSet())
+ {
+ return false;
+ }
+ e->set(m_objects[index], value);
+ return true;
+ }
+ bool canSet(const int entry) const override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(Qt::EditRole))
+ {
+ return false;
+ }
+ IEntry *e = m_entries[entry].second.value(Qt::EditRole);
+ return e->canSet();
+ }
+
+ QString entryTitle(const int entry) const override
+ {
+ return m_entries.at(entry).first;
+ }
+
+private:
+ struct IEntry
+ {
+ virtual ~IEntry() {}
+ virtual void set(Object *object, const QVariant &value) = 0;
+ virtual QVariant get(Object *object) const = 0;
+ virtual bool canSet() const = 0;
+ };
+ template<typename T>
+ struct VariableEntry : public IEntry
+ {
+ typedef T (Object::*Member);
+
+ explicit VariableEntry(Member member)
+ : m_member(member) {}
+
+ void set(Object *object, const QVariant &value) override
+ {
+ object->*m_member = value.value<T>();
+ }
+ QVariant get(Object *object) const override
+ {
+ return QVariant::fromValue<T>(object->*m_member);
+ }
+ bool canSet() const override { return true; }
+
+ private:
+ Member m_member;
+ };
+ template<typename T>
+ struct FunctionEntry : public IEntry
+ {
+ typedef T (Object::*Getter)() const;
+ typedef void (Object::*Setter)(T);
+
+ explicit FunctionEntry(Getter getter, Setter setter)
+ : m_getter(getter), m_setter(setter) {}
+
+ void set(Object *object, const QVariant &value) override
+ {
+ (object->*m_setter)(value.value<T>());
+ }
+ QVariant get(Object *object) const override
+ {
+ return QVariant::fromValue<T>((object->*m_getter)());
+ }
+ bool canSet() const override { return !!m_setter; }
+
+ private:
+ Getter m_getter;
+ Setter m_setter;
+ };
+ template<typename T>
+ struct LambdaEntry : public IEntry
+ {
+ using Getter = std::function<T(Object *)>;
+
+ explicit LambdaEntry(Getter getter)
+ : m_getter(getter) {}
+
+ void set(Object *object, const QVariant &value) override {}
+ QVariant get(Object *object) const override
+ {
+ return QVariant::fromValue<T>(m_getter(object));
+ }
+ bool canSet() const override { return false; }
+
+ private:
+ Getter m_getter;
+ };
+
+ QList<Object *> m_objects;
+ QVector<QPair<QString, QMap<int, IEntry *>>> m_entries;
+
+ void addEntryInternal(IEntry *e, const int entry, const int role)
+ {
+ if (m_entries.size() <= entry)
+ {
+ m_entries.resize(entry + 1);
+ }
+ m_entries[entry].second.insert(role, e);
+ }
+
+protected:
+ template<typename Getter, typename Setter>
+ typename std::enable_if<std::is_member_function_pointer<Getter>::value && std::is_member_function_pointer<Getter>::value, void>::type
+ addEntry(const int entry, const int role, Getter getter, Setter setter)
+ {
+ addEntryInternal(new FunctionEntry<typename std::result_of<Getter>::type>(getter, setter), entry, role);
+ }
+ template<typename T>
+ typename std::enable_if<std::is_member_function_pointer<typename FunctionEntry<T>::Getter>::value, void>::type
+ addEntry(const int entry, const int role, typename FunctionEntry<T>::Getter getter)
+ {
+ addEntryInternal(new FunctionEntry<T>(getter, nullptr), entry, role);
+ }
+ template<typename T>
+ typename std::enable_if<!std::is_member_function_pointer<T (Object::*)>::value, void>::type
+ addEntry(const int entry, const int role, T (Object::*member))
+ {
+ addEntryInternal(new VariableEntry<T>(member), entry, role);
+ }
+ template<typename T>
+ void addEntry(const int entry, const int role, typename LambdaEntry<T>::Getter lambda)
+ {
+ addEntryInternal(new LambdaEntry<T>(lambda), entry, role);
+ }
+
+ void setEntryTitle(const int entry, const QString &title)
+ {
+ m_entries[entry].first = title;
+ }
+
+ void setAll(const QList<Object *> objects)
+ {
+ notifyBeginReset();
+ qDeleteAll(m_objects);
+ m_objects = objects;
+ notifyEndReset();
+ }
+};
diff --git a/api/logic/BaseConfigObject.cpp b/api/logic/BaseConfigObject.cpp
new file mode 100644
index 00000000..3040ac2e
--- /dev/null
+++ b/api/logic/BaseConfigObject.cpp
@@ -0,0 +1,103 @@
+/* Copyright 2015 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 "BaseConfigObject.h"
+
+#include <QTimer>
+#include <QFile>
+#include <QCoreApplication>
+#include <QDebug>
+
+#include "Exception.h"
+#include "FileSystem.h"
+
+BaseConfigObject::BaseConfigObject(const QString &filename)
+ : m_filename(filename)
+{
+ m_saveTimer = new QTimer;
+ m_saveTimer->setSingleShot(true);
+ // cppcheck-suppress pureVirtualCall
+ QObject::connect(m_saveTimer, &QTimer::timeout, [this](){saveNow();});
+ setSaveTimeout(250);
+
+ m_initialReadTimer = new QTimer;
+ m_initialReadTimer->setSingleShot(true);
+ QObject::connect(m_initialReadTimer, &QTimer::timeout, [this]()
+ {
+ loadNow();
+ m_initialReadTimer->deleteLater();
+ m_initialReadTimer = 0;
+ });
+ m_initialReadTimer->start(0);
+
+ // cppcheck-suppress pureVirtualCall
+ m_appQuitConnection = QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this](){saveNow();});
+}
+BaseConfigObject::~BaseConfigObject()
+{
+ delete m_saveTimer;
+ if (m_initialReadTimer)
+ {
+ delete m_initialReadTimer;
+ }
+ QObject::disconnect(m_appQuitConnection);
+}
+
+void BaseConfigObject::setSaveTimeout(int msec)
+{
+ m_saveTimer->setInterval(msec);
+}
+
+void BaseConfigObject::scheduleSave()
+{
+ m_saveTimer->stop();
+ m_saveTimer->start();
+}
+void BaseConfigObject::saveNow()
+{
+ if (m_saveTimer->isActive())
+ {
+ m_saveTimer->stop();
+ }
+ if (m_disableSaving)
+ {
+ return;
+ }
+
+ try
+ {
+ FS::write(m_filename, doSave());
+ }
+ catch (Exception & e)
+ {
+ qCritical() << e.cause();
+ }
+}
+void BaseConfigObject::loadNow()
+{
+ if (m_saveTimer->isActive())
+ {
+ saveNow();
+ }
+
+ try
+ {
+ doLoad(FS::read(m_filename));
+ }
+ catch (Exception & e)
+ {
+ qWarning() << "Error loading" << m_filename << ":" << e.cause();
+ }
+}
diff --git a/api/logic/BaseConfigObject.h b/api/logic/BaseConfigObject.h
new file mode 100644
index 00000000..1c96b3d1
--- /dev/null
+++ b/api/logic/BaseConfigObject.h
@@ -0,0 +1,50 @@
+/* Copyright 2015 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 <QObject>
+
+class QTimer;
+
+class BaseConfigObject
+{
+public:
+ void setSaveTimeout(int msec);
+
+protected:
+ explicit BaseConfigObject(const QString &filename);
+ virtual ~BaseConfigObject();
+
+ // cppcheck-suppress pureVirtualCall
+ virtual QByteArray doSave() const = 0;
+ virtual void doLoad(const QByteArray &data) = 0;
+
+ void setSavingDisabled(bool savingDisabled) { m_disableSaving = savingDisabled; }
+
+ QString fileName() const { return m_filename; }
+
+public:
+ void scheduleSave();
+ void saveNow();
+ void loadNow();
+
+private:
+ QTimer *m_saveTimer;
+ QTimer *m_initialReadTimer;
+ QString m_filename;
+ QMetaObject::Connection m_appQuitConnection;
+ bool m_disableSaving = false;
+};
diff --git a/api/logic/BaseInstaller.cpp b/api/logic/BaseInstaller.cpp
new file mode 100644
index 00000000..cb762ebd
--- /dev/null
+++ b/api/logic/BaseInstaller.cpp
@@ -0,0 +1,61 @@
+/* Copyright 2013-2015 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 <QFile>
+
+#include "BaseInstaller.h"
+#include "minecraft/onesix/OneSixInstance.h"
+
+BaseInstaller::BaseInstaller()
+{
+
+}
+
+bool BaseInstaller::isApplied(OneSixInstance *on)
+{
+ return QFile::exists(filename(on->instanceRoot()));
+}
+
+bool BaseInstaller::add(OneSixInstance *to)
+{
+ if (!patchesDir(to->instanceRoot()).exists())
+ {
+ QDir(to->instanceRoot()).mkdir("patches");
+ }
+
+ if (isApplied(to))
+ {
+ if (!remove(to))
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool BaseInstaller::remove(OneSixInstance *from)
+{
+ return QFile::remove(filename(from->instanceRoot()));
+}
+
+QString BaseInstaller::filename(const QString &root) const
+{
+ return patchesDir(root).absoluteFilePath(id() + ".json");
+}
+QDir BaseInstaller::patchesDir(const QString &root) const
+{
+ return QDir(root + "/patches/");
+}
diff --git a/api/logic/BaseInstaller.h b/api/logic/BaseInstaller.h
new file mode 100644
index 00000000..a50c8cb1
--- /dev/null
+++ b/api/logic/BaseInstaller.h
@@ -0,0 +1,46 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class OneSixInstance;
+class QDir;
+class QString;
+class QObject;
+class Task;
+class BaseVersion;
+typedef std::shared_ptr<BaseVersion> BaseVersionPtr;
+
+class MULTIMC_LOGIC_EXPORT BaseInstaller
+{
+public:
+ BaseInstaller();
+ virtual ~BaseInstaller(){};
+ bool isApplied(OneSixInstance *on);
+
+ virtual bool add(OneSixInstance *to);
+ virtual bool remove(OneSixInstance *from);
+
+ virtual Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) = 0;
+
+protected:
+ virtual QString id() const = 0;
+ QString filename(const QString &root) const;
+ QDir patchesDir(const QString &root) const;
+};
diff --git a/api/logic/BaseInstance.cpp b/api/logic/BaseInstance.cpp
new file mode 100644
index 00000000..ce55d5e4
--- /dev/null
+++ b/api/logic/BaseInstance.cpp
@@ -0,0 +1,270 @@
+/* Copyright 2013-2015 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 "BaseInstance.h"
+
+#include <QFileInfo>
+#include <QDir>
+
+#include "settings/INISettingsObject.h"
+#include "settings/Setting.h"
+#include "settings/OverrideSetting.h"
+
+#include "minecraft/MinecraftVersionList.h"
+#include "FileSystem.h"
+#include "Commandline.h"
+
+BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
+ : QObject()
+{
+ m_settings = settings;
+ m_rootDir = rootDir;
+
+ m_settings->registerSetting("name", "Unnamed Instance");
+ m_settings->registerSetting("iconKey", "default");
+ m_settings->registerSetting("notes", "");
+ m_settings->registerSetting("lastLaunchTime", 0);
+ m_settings->registerSetting("totalTimePlayed", 0);
+
+ // Custom Commands
+ auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false);
+ m_settings->registerOverride(globalSettings->getSetting("PreLaunchCommand"), commandSetting);
+ m_settings->registerOverride(globalSettings->getSetting("WrapperCommand"), commandSetting);
+ m_settings->registerOverride(globalSettings->getSetting("PostExitCommand"), commandSetting);
+
+ // Console
+ auto consoleSetting = m_settings->registerSetting("OverrideConsole", false);
+ m_settings->registerOverride(globalSettings->getSetting("ShowConsole"), consoleSetting);
+ m_settings->registerOverride(globalSettings->getSetting("AutoCloseConsole"), consoleSetting);
+ m_settings->registerOverride(globalSettings->getSetting("LogPrePostOutput"), consoleSetting);
+}
+
+QString BaseInstance::getPreLaunchCommand()
+{
+ return settings()->get("PreLaunchCommand").toString();
+}
+
+QString BaseInstance::getWrapperCommand()
+{
+ return settings()->get("WrapperCommand").toString();
+}
+
+QString BaseInstance::getPostExitCommand()
+{
+ return settings()->get("PostExitCommand").toString();
+}
+
+void BaseInstance::iconUpdated(QString key)
+{
+ if(iconKey() == key)
+ {
+ emit propertiesChanged(this);
+ }
+}
+
+void BaseInstance::nuke()
+{
+ FS::deletePath(instanceRoot());
+ emit nuked(this);
+}
+
+QString BaseInstance::id() const
+{
+ return QFileInfo(instanceRoot()).fileName();
+}
+
+bool BaseInstance::isRunning() const
+{
+ return m_isRunning;
+}
+
+void BaseInstance::setRunning(bool running)
+{
+ if(running && !m_isRunning)
+ {
+ m_timeStarted = QDateTime::currentDateTime();
+ }
+ else if(!running && m_isRunning)
+ {
+ qint64 current = settings()->get("totalTimePlayed").toLongLong();
+ QDateTime timeEnded = QDateTime::currentDateTime();
+ settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded));
+ emit propertiesChanged(this);
+ }
+ m_isRunning = running;
+}
+
+int64_t BaseInstance::totalTimePlayed() const
+{
+ qint64 current = settings()->get("totalTimePlayed").toLongLong();
+ if(m_isRunning)
+ {
+ QDateTime timeNow = QDateTime::currentDateTime();
+ return current + m_timeStarted.secsTo(timeNow);
+ }
+ return current;
+}
+
+void BaseInstance::resetTimePlayed()
+{
+ settings()->reset("totalTimePlayed");
+}
+
+QString BaseInstance::instanceType() const
+{
+ return m_settings->get("InstanceType").toString();
+}
+
+QString BaseInstance::instanceRoot() const
+{
+ return m_rootDir;
+}
+
+InstancePtr BaseInstance::getSharedPtr()
+{
+ return shared_from_this();
+}
+
+SettingsObjectPtr BaseInstance::settings() const
+{
+ return m_settings;
+}
+
+BaseInstance::InstanceFlags BaseInstance::flags() const
+{
+ return m_flags;
+}
+
+void BaseInstance::setFlags(const InstanceFlags &flags)
+{
+ if (flags != m_flags)
+ {
+ m_flags = flags;
+ emit flagsChanged();
+ emit propertiesChanged(this);
+ }
+}
+
+void BaseInstance::setFlag(const BaseInstance::InstanceFlag flag)
+{
+ // nothing to set?
+ if(flag & m_flags)
+ return;
+ m_flags |= flag;
+ emit flagsChanged();
+ emit propertiesChanged(this);
+}
+
+void BaseInstance::unsetFlag(const BaseInstance::InstanceFlag flag)
+{
+ // nothing to unset?
+ if(!(flag & m_flags))
+ return;
+ m_flags &= ~flag;
+ emit flagsChanged();
+ emit propertiesChanged(this);
+}
+
+bool BaseInstance::canLaunch() const
+{
+ return !(flags() & VersionBrokenFlag);
+}
+
+bool BaseInstance::reload()
+{
+ return m_settings->reload();
+}
+
+qint64 BaseInstance::lastLaunch() const
+{
+ return m_settings->get("lastLaunchTime").value<qint64>();
+}
+
+void BaseInstance::setLastLaunch(qint64 val)
+{
+ //FIXME: if no change, do not set. setting involves saving a file.
+ m_settings->set("lastLaunchTime", val);
+ emit propertiesChanged(this);
+}
+
+void BaseInstance::setGroupInitial(QString val)
+{
+ if(m_group == val)
+ {
+ return;
+ }
+ m_group = val;
+ emit propertiesChanged(this);
+}
+
+void BaseInstance::setGroupPost(QString val)
+{
+ if(m_group == val)
+ {
+ return;
+ }
+ setGroupInitial(val);
+ emit groupChanged();
+}
+
+QString BaseInstance::group() const
+{
+ return m_group;
+}
+
+void BaseInstance::setNotes(QString val)
+{
+ //FIXME: if no change, do not set. setting involves saving a file.
+ m_settings->set("notes", val);
+}
+
+QString BaseInstance::notes() const
+{
+ return m_settings->get("notes").toString();
+}
+
+void BaseInstance::setIconKey(QString val)
+{
+ //FIXME: if no change, do not set. setting involves saving a file.
+ m_settings->set("iconKey", val);
+ emit propertiesChanged(this);
+}
+
+QString BaseInstance::iconKey() const
+{
+ return m_settings->get("iconKey").toString();
+}
+
+void BaseInstance::setName(QString val)
+{
+ //FIXME: if no change, do not set. setting involves saving a file.
+ m_settings->set("name", val);
+ emit propertiesChanged(this);
+}
+
+QString BaseInstance::name() const
+{
+ return m_settings->get("name").toString();
+}
+
+QString BaseInstance::windowTitle() const
+{
+ return "MultiMC: " + name();
+}
+
+QStringList BaseInstance::extraArguments() const
+{
+ return Commandline::splitArgs(settings()->get("JvmArgs").toString());
+}
diff --git a/api/logic/BaseInstance.h b/api/logic/BaseInstance.h
new file mode 100644
index 00000000..5e587c48
--- /dev/null
+++ b/api/logic/BaseInstance.h
@@ -0,0 +1,243 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QDateTime>
+#include <QSet>
+#include <QProcess>
+
+#include "settings/SettingsObject.h"
+
+#include "settings/INIFile.h"
+#include "BaseVersionList.h"
+#include "minecraft/auth/MojangAccount.h"
+#include "launch/MessageLevel.h"
+#include "pathmatcher/IPathMatcher.h"
+
+#include "multimc_logic_export.h"
+
+class QDir;
+class Task;
+class LaunchTask;
+class BaseInstance;
+
+// pointer for lazy people
+typedef std::shared_ptr<BaseInstance> InstancePtr;
+
+/*!
+ * \brief Base class for instances.
+ * This class implements many functions that are common between instances and
+ * provides a standard interface for all instances.
+ *
+ * To create a new instance type, create a new class inheriting from this class
+ * and implement the pure virtual functions.
+ */
+class MULTIMC_LOGIC_EXPORT BaseInstance : public QObject, public std::enable_shared_from_this<BaseInstance>
+{
+ Q_OBJECT
+protected:
+ /// no-touchy!
+ BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+
+public:
+ /// virtual destructor to make sure the destruction is COMPLETE
+ virtual ~BaseInstance() {};
+
+ virtual void copy(const QDir &newDir) {}
+
+ virtual void init() = 0;
+
+ /// nuke thoroughly - deletes the instance contents, notifies the list/model which is
+ /// responsible of cleaning up the husk
+ void nuke();
+
+ /// The instance's ID. The ID SHALL be determined by MMC internally. The ID IS guaranteed to
+ /// be unique.
+ virtual QString id() const;
+
+ void setRunning(bool running);
+ bool isRunning() const;
+ int64_t totalTimePlayed() const;
+ void resetTimePlayed();
+
+ /// get the type of this instance
+ QString instanceType() const;
+
+ /// Path to the instance's root directory.
+ QString instanceRoot() const;
+
+ QString name() const;
+ void setName(QString val);
+
+ /// Value used for instance window titles
+ QString windowTitle() const;
+
+ QString iconKey() const;
+ void setIconKey(QString val);
+
+ QString notes() const;
+ void setNotes(QString val);
+
+ QString group() const;
+ void setGroupInitial(QString val);
+ void setGroupPost(QString val);
+
+ QString getPreLaunchCommand();
+ QString getPostExitCommand();
+ QString getWrapperCommand();
+
+ /// guess log level from a line of game log
+ virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level)
+ {
+ return level;
+ };
+
+ virtual QStringList extraArguments() const;
+
+ virtual QString intendedVersionId() const = 0;
+ virtual bool setIntendedVersionId(QString version) = 0;
+
+ /*!
+ * The instance's current version.
+ * This value represents the instance's current version. If this value is
+ * different from the intendedVersion, the instance should be updated.
+ * \warning Don't change this value unless you know what you're doing.
+ */
+ virtual QString currentVersionId() const = 0;
+
+ /*!
+ * Whether or not 'the game' should be downloaded when the instance is launched.
+ */
+ virtual bool shouldUpdate() const = 0;
+ virtual void setShouldUpdate(bool val) = 0;
+
+ /// Traits. Normally inside the version, depends on instance implementation.
+ virtual QSet <QString> traits() = 0;
+
+ /**
+ * Gets the time that the instance was last launched.
+ * Stored in milliseconds since epoch.
+ */
+ qint64 lastLaunch() const;
+ /// Sets the last launched time to 'val' milliseconds since epoch
+ void setLastLaunch(qint64 val = QDateTime::currentMSecsSinceEpoch());
+
+ InstancePtr getSharedPtr();
+
+ /*!
+ * \brief Gets a pointer to this instance's version list.
+ * \return A pointer to the available version list for this instance.
+ */
+ virtual std::shared_ptr<BaseVersionList> versionList() const = 0;
+
+ /*!
+ * \brief Gets this instance's settings object.
+ * This settings object stores instance-specific settings.
+ * \return A pointer to this instance's settings object.
+ */
+ virtual SettingsObjectPtr settings() const;
+
+ /// returns a valid update task
+ virtual std::shared_ptr<Task> createUpdateTask() = 0;
+
+ /// returns a valid launcher (task container)
+ virtual std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) = 0;
+
+ /*!
+ * Returns a task that should be done right before launch
+ * This task should do any extra preparations needed
+ */
+ virtual std::shared_ptr<Task> createJarModdingTask() = 0;
+
+ /*!
+ * Create envrironment variables for running the instance
+ */
+ virtual QProcessEnvironment createEnvironment() = 0;
+
+ /*!
+ * Returns a matcher that can maps relative paths within the instance to whether they are 'log files'
+ */
+ virtual IPathMatcher::Ptr getLogFileMatcher() = 0;
+
+ /*!
+ * Returns the root folder to use for looking up log files
+ */
+ virtual QString getLogFileRoot() = 0;
+
+ /*!
+ * does any necessary cleanups after the instance finishes. also runs before\
+ * TODO: turn into a task that can run asynchronously
+ */
+ virtual void cleanupAfterRun() = 0;
+
+ virtual QString getStatusbarDescription() = 0;
+
+ /// FIXME: this really should be elsewhere...
+ virtual QString instanceConfigFolder() const = 0;
+
+ /// get variables this instance exports
+ virtual QMap<QString, QString> getVariables() const = 0;
+
+ virtual QString typeName() const = 0;
+
+ enum InstanceFlag
+ {
+ VersionBrokenFlag = 0x01,
+ UpdateAvailable = 0x02
+ };
+ Q_DECLARE_FLAGS(InstanceFlags, InstanceFlag)
+ InstanceFlags flags() const;
+ void setFlags(const InstanceFlags &flags);
+ void setFlag(const InstanceFlag flag);
+ void unsetFlag(const InstanceFlag flag);
+
+ bool canLaunch() const;
+ virtual bool canExport() const = 0;
+
+ virtual bool reload();
+
+signals:
+ /*!
+ * \brief Signal emitted when properties relevant to the instance view change
+ */
+ void propertiesChanged(BaseInstance *inst);
+ /*!
+ * \brief Signal emitted when groups are affected in any way
+ */
+ void groupChanged();
+ /*!
+ * \brief The instance just got nuked. Hurray!
+ */
+ void nuked(BaseInstance *inst);
+
+ void flagsChanged();
+
+protected slots:
+ void iconUpdated(QString key);
+
+protected:
+ QString m_rootDir;
+ QString m_group;
+ SettingsObjectPtr m_settings;
+ InstanceFlags m_flags;
+ bool m_isRunning = false;
+ QDateTime m_timeStarted;
+};
+
+Q_DECLARE_METATYPE(std::shared_ptr<BaseInstance>)
+Q_DECLARE_METATYPE(BaseInstance::InstanceFlag)
+Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags)
diff --git a/api/logic/BaseVersion.h b/api/logic/BaseVersion.h
new file mode 100644
index 00000000..80767518
--- /dev/null
+++ b/api/logic/BaseVersion.h
@@ -0,0 +1,59 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <QString>
+#include <QMetaType>
+
+/*!
+ * An abstract base class for versions.
+ */
+class BaseVersion
+{
+public:
+ virtual ~BaseVersion() {}
+ /*!
+ * A string used to identify this version in config files.
+ * This should be unique within the version list or shenanigans will occur.
+ */
+ virtual QString descriptor() = 0;
+
+ /*!
+ * The name of this version as it is displayed to the user.
+ * For example: "1.5.1"
+ */
+ virtual QString name() = 0;
+
+ /*!
+ * This should return a string that describes
+ * the kind of version this is (Stable, Beta, Snapshot, whatever)
+ */
+ virtual QString typeString() const = 0;
+
+ virtual bool operator<(BaseVersion &a)
+ {
+ return name() < a.name();
+ };
+ virtual bool operator>(BaseVersion &a)
+ {
+ return name() > a.name();
+ };
+};
+
+typedef std::shared_ptr<BaseVersion> BaseVersionPtr;
+
+Q_DECLARE_METATYPE(BaseVersionPtr)
diff --git a/api/logic/BaseVersionList.cpp b/api/logic/BaseVersionList.cpp
new file mode 100644
index 00000000..b34f318c
--- /dev/null
+++ b/api/logic/BaseVersionList.cpp
@@ -0,0 +1,104 @@
+/* Copyright 2013-2015 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 "BaseVersionList.h"
+#include "BaseVersion.h"
+
+BaseVersionList::BaseVersionList(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+BaseVersionPtr BaseVersionList::findVersion(const QString &descriptor)
+{
+ for (int i = 0; i < count(); i++)
+ {
+ if (at(i)->descriptor() == descriptor)
+ return at(i);
+ }
+ return BaseVersionPtr();
+}
+
+BaseVersionPtr BaseVersionList::getLatestStable() const
+{
+ if (count() <= 0)
+ return BaseVersionPtr();
+ else
+ return at(0);
+}
+
+BaseVersionPtr BaseVersionList::getRecommended() const
+{
+ return getLatestStable();
+}
+
+QVariant BaseVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ BaseVersionPtr version = at(index.row());
+
+ switch (role)
+ {
+ case VersionPointerRole:
+ return qVariantFromValue(version);
+
+ case VersionRole:
+ return version->name();
+
+ case VersionIdRole:
+ return version->descriptor();
+
+ case TypeRole:
+ return version->typeString();
+
+ default:
+ return QVariant();
+ }
+}
+
+BaseVersionList::RoleList BaseVersionList::providesRoles() const
+{
+ return {VersionPointerRole, VersionRole, VersionIdRole, TypeRole};
+}
+
+int BaseVersionList::rowCount(const QModelIndex &parent) const
+{
+ // Return count
+ return count();
+}
+
+int BaseVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QHash<int, QByteArray> BaseVersionList::roleNames() const
+{
+ QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
+ roles.insert(VersionRole, "version");
+ roles.insert(VersionIdRole, "versionId");
+ roles.insert(ParentGameVersionRole, "parentGameVersion");
+ roles.insert(RecommendedRole, "recommended");
+ roles.insert(LatestRole, "latest");
+ roles.insert(TypeRole, "type");
+ roles.insert(BranchRole, "branch");
+ roles.insert(PathRole, "path");
+ roles.insert(ArchitectureRole, "architecture");
+ return roles;
+}
diff --git a/api/logic/BaseVersionList.h b/api/logic/BaseVersionList.h
new file mode 100644
index 00000000..73d2ee1f
--- /dev/null
+++ b/api/logic/BaseVersionList.h
@@ -0,0 +1,126 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+
+#include "BaseVersion.h"
+#include "tasks/Task.h"
+#include "multimc_logic_export.h"
+
+/*!
+ * \brief Class that each instance type's version list derives from.
+ * Version lists are the lists that keep track of the available game versions
+ * for that instance. This list will not be loaded on startup. It will be loaded
+ * when the list's load function is called. Before using the version list, you
+ * should check to see if it has been loaded yet and if not, load the list.
+ *
+ * Note that this class also inherits from QAbstractListModel. Methods from that
+ * class determine how this version list shows up in a list view. Said methods
+ * all have a default implementation, but they can be overridden by plugins to
+ * change the behavior of the list.
+ */
+class MULTIMC_LOGIC_EXPORT BaseVersionList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum ModelRoles
+ {
+ VersionPointerRole = Qt::UserRole,
+ VersionRole,
+ VersionIdRole,
+ ParentGameVersionRole,
+ RecommendedRole,
+ LatestRole,
+ TypeRole,
+ BranchRole,
+ PathRole,
+ ArchitectureRole,
+ SortRole
+ };
+ typedef QList<int> RoleList;
+
+ explicit BaseVersionList(QObject *parent = 0);
+
+ /*!
+ * \brief Gets a task that will reload the version list.
+ * Simply execute the task to load the list.
+ * The task returned by this function should reset the model when it's done.
+ * \return A pointer to a task that reloads the version list.
+ */
+ virtual Task *getLoadTask() = 0;
+
+ //! Checks whether or not the list is loaded. If this returns false, the list should be
+ //loaded.
+ virtual bool isLoaded() = 0;
+
+ //! Gets the version at the given index.
+ virtual const BaseVersionPtr at(int i) const = 0;
+
+ //! Returns the number of versions in the list.
+ virtual int count() const = 0;
+
+ //////// List Model Functions ////////
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual int rowCount(const QModelIndex &parent) const;
+ virtual int columnCount(const QModelIndex &parent) const;
+ virtual QHash<int, QByteArray> roleNames() const override;
+
+ //! which roles are provided by this version list?
+ virtual RoleList providesRoles() const;
+
+ /*!
+ * \brief Finds a version by its descriptor.
+ * \param The descriptor of the version to find.
+ * \return A const pointer to the version with the given descriptor. NULL if
+ * one doesn't exist.
+ */
+ virtual BaseVersionPtr findVersion(const QString &descriptor);
+
+ /*!
+ * \brief Gets the latest stable version from this list
+ */
+ virtual BaseVersionPtr getLatestStable() const;
+
+ /*!
+ * \brief Gets the recommended version from this list
+ * If the list doesn't support recommended versions, this works exactly as getLatestStable
+ */
+ virtual BaseVersionPtr getRecommended() const;
+
+ /*!
+ * Sorts the version list.
+ */
+ virtual void sortVersions() = 0;
+
+protected
+slots:
+ /*!
+ * Updates this list with the given list of versions.
+ * This is done by copying each version in the given list and inserting it
+ * into this one.
+ * We need to do this so that we can set the parents of the versions are set to this
+ * version list. This can't be done in the load task, because the versions the load
+ * task creates are on the load task's thread and Qt won't allow their parents
+ * to be set to something created on another thread.
+ * To get around that problem, we invoke this method on the GUI thread, which
+ * then copies the versions and sets their parents correctly.
+ * \param versions List of versions whose parents should be set.
+ */
+ virtual void updateListData(QList<BaseVersionPtr> versions) = 0;
+};
diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt
new file mode 100644
index 00000000..317627d5
--- /dev/null
+++ b/api/logic/CMakeLists.txt
@@ -0,0 +1,344 @@
+project(MultiMC_logic)
+
+set(LOGIC_SOURCES
+ # LOGIC - Base classes and infrastructure
+ BaseInstaller.h
+ BaseInstaller.cpp
+ BaseVersionList.h
+ BaseVersionList.cpp
+ InstanceList.h
+ InstanceList.cpp
+ BaseVersion.h
+ BaseInstance.h
+ BaseInstance.cpp
+ NullInstance.h
+ MMCZip.h
+ MMCZip.cpp
+ MMCStrings.h
+ MMCStrings.cpp
+ BaseConfigObject.h
+ BaseConfigObject.cpp
+ AbstractCommonModel.h
+ AbstractCommonModel.cpp
+ TypeMagic.h
+
+ # Prefix tree where node names are strings between separators
+ SeparatorPrefixTree.h
+
+ # WARNING: globals live here
+ Env.h
+ Env.cpp
+
+ # JSON parsing helpers
+ Json.h
+ Json.cpp
+
+ FileSystem.h
+ FileSystem.cpp
+
+ Exception.h
+
+ # RW lock protected map
+ RWStorage.h
+
+ # A variable that has an implicit default value and keeps track of changes
+ DefaultVariable.h
+
+ # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms
+ QObjectPtr.h
+
+ # Resources
+ resources/Resource.cpp
+ resources/Resource.h
+ resources/ResourceHandler.cpp
+ resources/ResourceHandler.h
+ resources/ResourceObserver.cpp
+ resources/ResourceObserver.h
+ resources/ResourceProxyModel.h
+ resources/ResourceProxyModel.cpp
+
+ # Path matchers
+ pathmatcher/FSTreeMatcher.h
+ pathmatcher/IPathMatcher.h
+ pathmatcher/MultiMatcher.h
+ pathmatcher/RegexpMatcher.h
+
+ # Compression support
+ GZip.h
+ GZip.cpp
+
+ # Command line parameter parsing
+ Commandline.h
+ Commandline.cpp
+
+ # Version number string support
+ Version.h
+ Version.cpp
+
+ # network stuffs
+ net/NetAction.h
+ net/MD5EtagDownload.h
+ net/MD5EtagDownload.cpp
+ net/ByteArrayDownload.h
+ net/ByteArrayDownload.cpp
+ net/CacheDownload.h
+ net/CacheDownload.cpp
+ net/NetJob.h
+ net/NetJob.cpp
+ net/HttpMetaCache.h
+ net/HttpMetaCache.cpp
+ net/PasteUpload.h
+ net/PasteUpload.cpp
+ net/URLConstants.h
+ net/URLConstants.cpp
+
+ # Yggdrasil login stuff
+ minecraft/auth/AuthSession.h
+ minecraft/auth/AuthSession.cpp
+ minecraft/auth/MojangAccountList.h
+ minecraft/auth/MojangAccountList.cpp
+ minecraft/auth/MojangAccount.h
+ minecraft/auth/MojangAccount.cpp
+ minecraft/auth/YggdrasilTask.h
+ minecraft/auth/YggdrasilTask.cpp
+ minecraft/auth/flows/AuthenticateTask.h
+ minecraft/auth/flows/AuthenticateTask.cpp
+ minecraft/auth/flows/RefreshTask.cpp
+ minecraft/auth/flows/RefreshTask.cpp
+ minecraft/auth/flows/ValidateTask.h
+ minecraft/auth/flows/ValidateTask.cpp
+
+ # Game launch logic
+ launch/steps/CheckJava.cpp
+ launch/steps/CheckJava.h
+ launch/steps/LaunchMinecraft.cpp
+ launch/steps/LaunchMinecraft.h
+ launch/steps/ModMinecraftJar.cpp
+ launch/steps/ModMinecraftJar.h
+ launch/steps/PostLaunchCommand.cpp
+ launch/steps/PostLaunchCommand.h
+ launch/steps/PreLaunchCommand.cpp
+ launch/steps/PreLaunchCommand.h
+ launch/steps/TextPrint.cpp
+ launch/steps/TextPrint.h
+ launch/steps/Update.cpp
+ launch/steps/Update.h
+ launch/LaunchStep.cpp
+ launch/LaunchStep.h
+ launch/LaunchTask.cpp
+ launch/LaunchTask.h
+ launch/LoggedProcess.cpp
+ launch/LoggedProcess.h
+ launch/MessageLevel.cpp
+ launch/MessageLevel.h
+
+ # Update system
+ updater/GoUpdate.h
+ updater/GoUpdate.cpp
+ updater/UpdateChecker.h
+ updater/UpdateChecker.cpp
+ updater/DownloadTask.h
+ updater/DownloadTask.cpp
+
+ # Notifications - short warning messages
+ notifications/NotificationChecker.h
+ notifications/NotificationChecker.cpp
+
+ # News System
+ news/NewsChecker.h
+ news/NewsChecker.cpp
+ news/NewsEntry.h
+ news/NewsEntry.cpp
+
+ # Status system
+ status/StatusChecker.h
+ status/StatusChecker.cpp
+
+ # Minecraft support
+ minecraft/onesix/OneSixUpdate.h
+ minecraft/onesix/OneSixUpdate.cpp
+ minecraft/onesix/OneSixInstance.h
+ minecraft/onesix/OneSixInstance.cpp
+ minecraft/onesix/OneSixProfileStrategy.cpp
+ minecraft/onesix/OneSixProfileStrategy.h
+ minecraft/onesix/OneSixVersionFormat.cpp
+ minecraft/onesix/OneSixVersionFormat.h
+ minecraft/legacy/LegacyUpdate.h
+ minecraft/legacy/LegacyUpdate.cpp
+ minecraft/legacy/LegacyInstance.h
+ minecraft/legacy/LegacyInstance.cpp
+ minecraft/legacy/LwjglVersionList.h
+ minecraft/legacy/LwjglVersionList.cpp
+ minecraft/GradleSpecifier.h
+ minecraft/MinecraftProfile.cpp
+ minecraft/MinecraftProfile.h
+ minecraft/MojangVersionFormat.cpp
+ minecraft/MojangVersionFormat.h
+ minecraft/JarMod.h
+ minecraft/MinecraftInstance.cpp
+ minecraft/MinecraftInstance.h
+ minecraft/MinecraftVersion.cpp
+ minecraft/MinecraftVersion.h
+ minecraft/MinecraftVersionList.cpp
+ minecraft/MinecraftVersionList.h
+ minecraft/Rule.cpp
+ minecraft/Rule.h
+ minecraft/OpSys.cpp
+ minecraft/OpSys.h
+ minecraft/ParseUtils.cpp
+ minecraft/ParseUtils.h
+ minecraft/ProfileUtils.cpp
+ minecraft/ProfileUtils.h
+ minecraft/ProfileStrategy.h
+ minecraft/Library.cpp
+ minecraft/Library.h
+ minecraft/MojangDownloadInfo.h
+ minecraft/VersionBuildError.h
+ minecraft/VersionFile.cpp
+ minecraft/VersionFile.h
+ minecraft/ProfilePatch.h
+ minecraft/VersionFilterData.h
+ minecraft/VersionFilterData.cpp
+ minecraft/Mod.h
+ minecraft/Mod.cpp
+ minecraft/ModList.h
+ minecraft/ModList.cpp
+ minecraft/World.h
+ minecraft/World.cpp
+ minecraft/WorldList.h
+ minecraft/WorldList.cpp
+
+ # FTB
+ minecraft/ftb/OneSixFTBInstance.h
+ minecraft/ftb/OneSixFTBInstance.cpp
+ minecraft/ftb/LegacyFTBInstance.h
+ minecraft/ftb/LegacyFTBInstance.cpp
+ minecraft/ftb/FTBProfileStrategy.h
+ minecraft/ftb/FTBProfileStrategy.cpp
+ minecraft/ftb/FTBPlugin.h
+ minecraft/ftb/FTBPlugin.cpp
+
+ # A Recursive file system watcher
+ RecursiveFileSystemWatcher.h
+ RecursiveFileSystemWatcher.cpp
+
+ # the screenshots feature
+ screenshots/Screenshot.h
+ screenshots/ImgurUpload.h
+ screenshots/ImgurUpload.cpp
+ screenshots/ImgurAlbumCreation.h
+ screenshots/ImgurAlbumCreation.cpp
+
+ # Tasks
+ tasks/Task.h
+ tasks/Task.cpp
+ tasks/ThreadTask.h
+ tasks/ThreadTask.cpp
+ tasks/SequentialTask.h
+ tasks/SequentialTask.cpp
+
+ # Settings
+ settings/INIFile.cpp
+ settings/INIFile.h
+ settings/INISettingsObject.cpp
+ settings/INISettingsObject.h
+ settings/OverrideSetting.cpp
+ settings/OverrideSetting.h
+ settings/PassthroughSetting.cpp
+ settings/PassthroughSetting.h
+ settings/Setting.cpp
+ settings/Setting.h
+ settings/SettingsObject.cpp
+ settings/SettingsObject.h
+
+ # Java related code
+ java/JavaChecker.h
+ java/JavaChecker.cpp
+ java/JavaCheckerJob.h
+ java/JavaCheckerJob.cpp
+ java/JavaInstall.h
+ java/JavaInstall.cpp
+ java/JavaInstallList.h
+ java/JavaInstallList.cpp
+ java/JavaUtils.h
+ java/JavaUtils.cpp
+ java/JavaVersion.h
+ java/JavaVersion.cpp
+
+ # Assets
+ minecraft/AssetsUtils.h
+ minecraft/AssetsUtils.cpp
+
+ # Forge and all things forge related
+ minecraft/forge/ForgeVersion.h
+ minecraft/forge/ForgeVersion.cpp
+ minecraft/forge/ForgeVersionList.h
+ minecraft/forge/ForgeVersionList.cpp
+ minecraft/forge/ForgeXzDownload.h
+ minecraft/forge/ForgeXzDownload.cpp
+ minecraft/forge/LegacyForge.h
+ minecraft/forge/LegacyForge.cpp
+ minecraft/forge/ForgeInstaller.h
+ minecraft/forge/ForgeInstaller.cpp
+
+ # Liteloader and related things
+ minecraft/liteloader/LiteLoaderInstaller.h
+ minecraft/liteloader/LiteLoaderInstaller.cpp
+ minecraft/liteloader/LiteLoaderVersionList.h
+ minecraft/liteloader/LiteLoaderVersionList.cpp
+
+ # Translations
+ trans/TranslationDownloader.h
+ trans/TranslationDownloader.cpp
+
+ # Tools
+ tools/BaseExternalTool.cpp
+ tools/BaseExternalTool.h
+ tools/BaseProfiler.cpp
+ tools/BaseProfiler.h
+ tools/JProfiler.cpp
+ tools/JProfiler.h
+ tools/JVisualVM.cpp
+ tools/JVisualVM.h
+ tools/MCEditTool.cpp
+ tools/MCEditTool.h
+
+ # Wonko
+ wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp
+ wonko/tasks/BaseWonkoEntityRemoteLoadTask.h
+ wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp
+ wonko/tasks/BaseWonkoEntityLocalLoadTask.h
+ wonko/format/WonkoFormatV1.cpp
+ wonko/format/WonkoFormatV1.h
+ wonko/format/WonkoFormat.cpp
+ wonko/format/WonkoFormat.h
+ wonko/BaseWonkoEntity.cpp
+ wonko/BaseWonkoEntity.h
+ wonko/WonkoVersionList.cpp
+ wonko/WonkoVersionList.h
+ wonko/WonkoVersion.cpp
+ wonko/WonkoVersion.h
+ wonko/WonkoIndex.cpp
+ wonko/WonkoIndex.h
+ wonko/WonkoUtil.cpp
+ wonko/WonkoUtil.h
+ wonko/WonkoReference.cpp
+ wonko/WonkoReference.h
+)
+################################ COMPILE ################################
+
+# we need zlib
+find_package(ZLIB REQUIRED)
+
+add_library(MultiMC_logic SHARED ${LOGIC_SOURCES})
+set_target_properties(MultiMC_logic PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1)
+
+generate_export_header(MultiMC_logic)
+
+# Link
+target_link_libraries(MultiMC_logic xz-embedded unpack200 ${QUAZIP_LIBRARIES} nbt++ ${ZLIB_LIBRARIES})
+qt5_use_modules(MultiMC_logic Core Xml Network Concurrent)
+add_dependencies(MultiMC_logic QuaZIP)
+
+# Mark and export headers
+target_include_directories(MultiMC_logic PUBLIC "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}" PRIVATE "${ZLIB_INCLUDE_DIRS}")
diff --git a/api/logic/Commandline.cpp b/api/logic/Commandline.cpp
new file mode 100644
index 00000000..9a8ddbf1
--- /dev/null
+++ b/api/logic/Commandline.cpp
@@ -0,0 +1,483 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * 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 "Commandline.h"
+
+/**
+ * @file libutil/src/cmdutils.cpp
+ */
+
+namespace Commandline
+{
+
+// commandline splitter
+QStringList splitArgs(QString args)
+{
+ QStringList argv;
+ QString current;
+ bool escape = false;
+ QChar inquotes;
+ for (int i = 0; i < args.length(); i++)
+ {
+ QChar cchar = args.at(i);
+
+ // \ escaped
+ if (escape)
+ {
+ current += cchar;
+ escape = false;
+ // in "quotes"
+ }
+ else if (!inquotes.isNull())
+ {
+ if (cchar == 0x5C)
+ escape = true;
+ else if (cchar == inquotes)
+ inquotes = 0;
+ else
+ current += cchar;
+ // otherwise
+ }
+ else
+ {
+ if (cchar == 0x20)
+ {
+ if (!current.isEmpty())
+ {
+ argv << current;
+ current.clear();
+ }
+ }
+ else if (cchar == 0x22 || cchar == 0x27)
+ inquotes = cchar;
+ else
+ current += cchar;
+ }
+ }
+ if (!current.isEmpty())
+ argv << current;
+ return argv;
+}
+
+Parser::Parser(FlagStyle::Enum flagStyle, ArgumentStyle::Enum argStyle)
+{
+ m_flagStyle = flagStyle;
+ m_argStyle = argStyle;
+}
+
+// styles setter/getter
+void Parser::setArgumentStyle(ArgumentStyle::Enum style)
+{
+ m_argStyle = style;
+}
+ArgumentStyle::Enum Parser::argumentStyle()
+{
+ return m_argStyle;
+}
+
+void Parser::setFlagStyle(FlagStyle::Enum style)
+{
+ m_flagStyle = style;
+}
+FlagStyle::Enum Parser::flagStyle()
+{
+ return m_flagStyle;
+}
+
+// setup methods
+void Parser::addSwitch(QString name, bool def)
+{
+ if (m_params.contains(name))
+ throw "Name not unique";
+
+ OptionDef *param = new OptionDef;
+ param->type = otSwitch;
+ param->name = name;
+ param->metavar = QString("<%1>").arg(name);
+ param->def = def;
+
+ m_options[name] = param;
+ m_params[name] = (CommonDef *)param;
+ m_optionList.append(param);
+}
+
+void Parser::addOption(QString name, QVariant def)
+{
+ if (m_params.contains(name))
+ throw "Name not unique";
+
+ OptionDef *param = new OptionDef;
+ param->type = otOption;
+ param->name = name;
+ param->metavar = QString("<%1>").arg(name);
+ param->def = def;
+
+ m_options[name] = param;
+ m_params[name] = (CommonDef *)param;
+ m_optionList.append(param);
+}
+
+void Parser::addArgument(QString name, bool required, QVariant def)
+{
+ if (m_params.contains(name))
+ throw "Name not unique";
+
+ PositionalDef *param = new PositionalDef;
+ param->name = name;
+ param->def = def;
+ param->required = required;
+ param->metavar = name;
+
+ m_positionals.append(param);
+ m_params[name] = (CommonDef *)param;
+}
+
+void Parser::addDocumentation(QString name, QString doc, QString metavar)
+{
+ if (!m_params.contains(name))
+ throw "Name does not exist";
+
+ CommonDef *param = m_params[name];
+ param->doc = doc;
+ if (!metavar.isNull())
+ param->metavar = metavar;
+}
+
+void Parser::addShortOpt(QString name, QChar flag)
+{
+ if (!m_params.contains(name))
+ throw "Name does not exist";
+ if (!m_options.contains(name))
+ throw "Name is not an Option or Swtich";
+
+ OptionDef *param = m_options[name];
+ m_flags[flag] = param;
+ param->flag = flag;
+}
+
+// help methods
+QString Parser::compileHelp(QString progName, int helpIndent, bool useFlags)
+{
+ QStringList help;
+ help << compileUsage(progName, useFlags) << "\r\n";
+
+ // positionals
+ if (!m_positionals.isEmpty())
+ {
+ help << "\r\n";
+ help << "Positional arguments:\r\n";
+ QListIterator<PositionalDef *> it2(m_positionals);
+ while (it2.hasNext())
+ {
+ PositionalDef *param = it2.next();
+ help << " " << param->metavar;
+ help << " " << QString(helpIndent - param->metavar.length() - 1, ' ');
+ help << param->doc << "\r\n";
+ }
+ }
+
+ // Options
+ if (!m_optionList.isEmpty())
+ {
+ help << "\r\n";
+ QString optPrefix, flagPrefix;
+ getPrefix(optPrefix, flagPrefix);
+
+ help << "Options & Switches:\r\n";
+ QListIterator<OptionDef *> it(m_optionList);
+ while (it.hasNext())
+ {
+ OptionDef *option = it.next();
+ help << " ";
+ int nameLength = optPrefix.length() + option->name.length();
+ if (!option->flag.isNull())
+ {
+ nameLength += 3 + flagPrefix.length();
+ help << flagPrefix << option->flag << ", ";
+ }
+ help << optPrefix << option->name;
+ if (option->type == otOption)
+ {
+ QString arg = QString("%1%2").arg(
+ ((m_argStyle == ArgumentStyle::Equals) ? "=" : " "), option->metavar);
+ nameLength += arg.length();
+ help << arg;
+ }
+ help << " " << QString(helpIndent - nameLength - 1, ' ');
+ help << option->doc << "\r\n";
+ }
+ }
+
+ return help.join("");
+}
+
+QString Parser::compileUsage(QString progName, bool useFlags)
+{
+ QStringList usage;
+ usage << "Usage: " << progName;
+
+ QString optPrefix, flagPrefix;
+ getPrefix(optPrefix, flagPrefix);
+
+ // options
+ QListIterator<OptionDef *> it(m_optionList);
+ while (it.hasNext())
+ {
+ OptionDef *option = it.next();
+ usage << " [";
+ if (!option->flag.isNull() && useFlags)
+ usage << flagPrefix << option->flag;
+ else
+ usage << optPrefix << option->name;
+ if (option->type == otOption)
+ usage << ((m_argStyle == ArgumentStyle::Equals) ? "=" : " ") << option->metavar;
+ usage << "]";
+ }
+
+ // arguments
+ QListIterator<PositionalDef *> it2(m_positionals);
+ while (it2.hasNext())
+ {
+ PositionalDef *param = it2.next();
+ usage << " " << (param->required ? "<" : "[");
+ usage << param->metavar;
+ usage << (param->required ? ">" : "]");
+ }
+
+ return usage.join("");
+}
+
+// parsing
+QHash<QString, QVariant> Parser::parse(QStringList argv)
+{
+ QHash<QString, QVariant> map;
+
+ QStringListIterator it(argv);
+ QString programName = it.next();
+
+ QString optionPrefix;
+ QString flagPrefix;
+ QListIterator<PositionalDef *> positionals(m_positionals);
+ QStringList expecting;
+
+ getPrefix(optionPrefix, flagPrefix);
+
+ while (it.hasNext())
+ {
+ QString arg = it.next();
+
+ if (!expecting.isEmpty())
+ // we were expecting an argument
+ {
+ QString name = expecting.first();
+/*
+ if (map.contains(name))
+ throw ParsingError(
+ QString("Option %2%1 was given multiple times").arg(name, optionPrefix));
+*/
+ map[name] = QVariant(arg);
+
+ expecting.removeFirst();
+ continue;
+ }
+
+ if (arg.startsWith(optionPrefix))
+ // we have an option
+ {
+ // qDebug("Found option %s", qPrintable(arg));
+
+ QString name = arg.mid(optionPrefix.length());
+ QString equals;
+
+ if ((m_argStyle == ArgumentStyle::Equals ||
+ m_argStyle == ArgumentStyle::SpaceAndEquals) &&
+ name.contains("="))
+ {
+ int i = name.indexOf("=");
+ equals = name.mid(i + 1);
+ name = name.left(i);
+ }
+
+ if (m_options.contains(name))
+ {
+ /*
+ if (map.contains(name))
+ throw ParsingError(QString("Option %2%1 was given multiple times")
+ .arg(name, optionPrefix));
+*/
+ OptionDef *option = m_options[name];
+ if (option->type == otSwitch)
+ map[name] = true;
+ else // if (option->type == otOption)
+ {
+ if (m_argStyle == ArgumentStyle::Space)
+ expecting.append(name);
+ else if (!equals.isNull())
+ map[name] = equals;
+ else if (m_argStyle == ArgumentStyle::SpaceAndEquals)
+ expecting.append(name);
+ else
+ throw ParsingError(QString("Option %2%1 reqires an argument.")
+ .arg(name, optionPrefix));
+ }
+
+ continue;
+ }
+
+ throw ParsingError(QString("Unknown Option %2%1").arg(name, optionPrefix));
+ }
+
+ if (arg.startsWith(flagPrefix))
+ // we have (a) flag(s)
+ {
+ // qDebug("Found flags %s", qPrintable(arg));
+
+ QString flags = arg.mid(flagPrefix.length());
+ QString equals;
+
+ if ((m_argStyle == ArgumentStyle::Equals ||
+ m_argStyle == ArgumentStyle::SpaceAndEquals) &&
+ flags.contains("="))
+ {
+ int i = flags.indexOf("=");
+ equals = flags.mid(i + 1);
+ flags = flags.left(i);
+ }
+
+ for (int i = 0; i < flags.length(); i++)
+ {
+ QChar flag = flags.at(i);
+
+ if (!m_flags.contains(flag))
+ throw ParsingError(QString("Unknown flag %2%1").arg(flag, flagPrefix));
+
+ OptionDef *option = m_flags[flag];
+/*
+ if (map.contains(option->name))
+ throw ParsingError(QString("Option %2%1 was given multiple times")
+ .arg(option->name, optionPrefix));
+*/
+ if (option->type == otSwitch)
+ map[option->name] = true;
+ else // if (option->type == otOption)
+ {
+ if (m_argStyle == ArgumentStyle::Space)
+ expecting.append(option->name);
+ else if (!equals.isNull())
+ if (i == flags.length() - 1)
+ map[option->name] = equals;
+ else
+ throw ParsingError(QString("Flag %4%2 of Argument-requiring Option "
+ "%1 not last flag in %4%3")
+ .arg(option->name, flag, flags, flagPrefix));
+ else if (m_argStyle == ArgumentStyle::SpaceAndEquals)
+ expecting.append(option->name);
+ else
+ throw ParsingError(QString("Option %1 reqires an argument. (flag %3%2)")
+ .arg(option->name, flag, flagPrefix));
+ }
+ }
+
+ continue;
+ }
+
+ // must be a positional argument
+ if (!positionals.hasNext())
+ throw ParsingError(QString("Don't know what to do with '%1'").arg(arg));
+
+ PositionalDef *param = positionals.next();
+
+ map[param->name] = arg;
+ }
+
+ // check if we're missing something
+ if (!expecting.isEmpty())
+ throw ParsingError(QString("Was still expecting arguments for %2%1").arg(
+ expecting.join(QString(", ") + optionPrefix), optionPrefix));
+
+ while (positionals.hasNext())
+ {
+ PositionalDef *param = positionals.next();
+ if (param->required)
+ throw ParsingError(
+ QString("Missing required positional argument '%1'").arg(param->name));
+ else
+ map[param->name] = param->def;
+ }
+
+ // fill out gaps
+ QListIterator<OptionDef *> iter(m_optionList);
+ while (iter.hasNext())
+ {
+ OptionDef *option = iter.next();
+ if (!map.contains(option->name))
+ map[option->name] = option->def;
+ }
+
+ return map;
+}
+
+// clear defs
+void Parser::clear()
+{
+ m_flags.clear();
+ m_params.clear();
+ m_options.clear();
+
+ QMutableListIterator<OptionDef *> it(m_optionList);
+ while (it.hasNext())
+ {
+ OptionDef *option = it.next();
+ it.remove();
+ delete option;
+ }
+
+ QMutableListIterator<PositionalDef *> it2(m_positionals);
+ while (it2.hasNext())
+ {
+ PositionalDef *arg = it2.next();
+ it2.remove();
+ delete arg;
+ }
+}
+
+// Destructor
+Parser::~Parser()
+{
+ clear();
+}
+
+// getPrefix
+void Parser::getPrefix(QString &opt, QString &flag)
+{
+ if (m_flagStyle == FlagStyle::Windows)
+ opt = flag = "/";
+ else if (m_flagStyle == FlagStyle::Unix)
+ opt = flag = "-";
+ // else if (m_flagStyle == FlagStyle::GNU)
+ else
+ {
+ opt = "--";
+ flag = "-";
+ }
+}
+
+// ParsingError
+ParsingError::ParsingError(const QString &what) : std::runtime_error(what.toStdString())
+{
+}
+} \ No newline at end of file
diff --git a/api/logic/Commandline.h b/api/logic/Commandline.h
new file mode 100644
index 00000000..bee02bad
--- /dev/null
+++ b/api/logic/Commandline.h
@@ -0,0 +1,252 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * 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 <exception>
+#include <stdexcept>
+
+#include <QString>
+#include <QVariant>
+#include <QHash>
+#include <QStringList>
+
+#include "multimc_logic_export.h"
+
+/**
+ * @file libutil/include/cmdutils.h
+ * @brief commandline parsing and processing utilities
+ */
+
+namespace Commandline
+{
+
+/**
+ * @brief split a string into argv items like a shell would do
+ * @param args the argument string
+ * @return a QStringList containing all arguments
+ */
+MULTIMC_LOGIC_EXPORT QStringList splitArgs(QString args);
+
+/**
+ * @brief The FlagStyle enum
+ * Specifies how flags are decorated
+ */
+
+namespace FlagStyle
+{
+enum Enum
+{
+ GNU, /**< --option and -o (GNU Style) */
+ Unix, /**< -option and -o (Unix Style) */
+ Windows, /**< /option and /o (Windows Style) */
+#ifdef Q_OS_WIN32
+ Default = Windows
+#else
+ Default = GNU
+#endif
+};
+}
+
+/**
+ * @brief The ArgumentStyle enum
+ */
+namespace ArgumentStyle
+{
+enum Enum
+{
+ Space, /**< --option=value */
+ Equals, /**< --option value */
+ SpaceAndEquals, /**< --option[= ]value */
+#ifdef Q_OS_WIN32
+ Default = Equals
+#else
+ Default = SpaceAndEquals
+#endif
+};
+}
+
+/**
+ * @brief The ParsingError class
+ */
+class MULTIMC_LOGIC_EXPORT ParsingError : public std::runtime_error
+{
+public:
+ ParsingError(const QString &what);
+};
+
+/**
+ * @brief The Parser class
+ */
+class MULTIMC_LOGIC_EXPORT Parser
+{
+public:
+ /**
+ * @brief Parser constructor
+ * @param flagStyle the FlagStyle to use in this Parser
+ * @param argStyle the ArgumentStyle to use in this Parser
+ */
+ Parser(FlagStyle::Enum flagStyle = FlagStyle::Default,
+ ArgumentStyle::Enum argStyle = ArgumentStyle::Default);
+
+ /**
+ * @brief set the flag style
+ * @param style
+ */
+ void setFlagStyle(FlagStyle::Enum style);
+
+ /**
+ * @brief get the flag style
+ * @return
+ */
+ FlagStyle::Enum flagStyle();
+
+ /**
+ * @brief set the argument style
+ * @param style
+ */
+ void setArgumentStyle(ArgumentStyle::Enum style);
+
+ /**
+ * @brief get the argument style
+ * @return
+ */
+ ArgumentStyle::Enum argumentStyle();
+
+ /**
+ * @brief define a boolean switch
+ * @param name the parameter name
+ * @param def the default value
+ */
+ void addSwitch(QString name, bool def = false);
+
+ /**
+ * @brief define an option that takes an additional argument
+ * @param name the parameter name
+ * @param def the default value
+ */
+ void addOption(QString name, QVariant def = QVariant());
+
+ /**
+ * @brief define a positional argument
+ * @param name the parameter name
+ * @param required wether this argument is required
+ * @param def the default value
+ */
+ void addArgument(QString name, bool required = true, QVariant def = QVariant());
+
+ /**
+ * @brief adds a flag to an existing parameter
+ * @param name the (existing) parameter name
+ * @param flag the flag character
+ * @see addSwitch addArgument addOption
+ * Note: any one parameter can only have one flag
+ */
+ void addShortOpt(QString name, QChar flag);
+
+ /**
+ * @brief adds documentation to a Parameter
+ * @param name the parameter name
+ * @param metavar a string to be displayed as placeholder for the value
+ * @param doc a QString containing the documentation
+ * Note: on positional arguments, metavar replaces the name as displayed.
+ * on options , metavar replaces the value placeholder
+ */
+ void addDocumentation(QString name, QString doc, QString metavar = QString());
+
+ /**
+ * @brief generate a help message
+ * @param progName the program name to use in the help message
+ * @param helpIndent how much the parameter documentation should be indented
+ * @param flagsInUsage whether we should use flags instead of options in the usage
+ * @return a help message
+ */
+ QString compileHelp(QString progName, int helpIndent = 22, bool flagsInUsage = true);
+
+ /**
+ * @brief generate a short usage message
+ * @param progName the program name to use in the usage message
+ * @param useFlags whether we should use flags instead of options
+ * @return a usage message
+ */
+ QString compileUsage(QString progName, bool useFlags = true);
+
+ /**
+ * @brief parse
+ * @param argv a QStringList containing the program ARGV
+ * @return a QHash mapping argument names to their values
+ */
+ QHash<QString, QVariant> parse(QStringList argv);
+
+ /**
+ * @brief clear all definitions
+ */
+ void clear();
+
+ ~Parser();
+
+private:
+ FlagStyle::Enum m_flagStyle;
+ ArgumentStyle::Enum m_argStyle;
+
+ enum OptionType
+ {
+ otSwitch,
+ otOption
+ };
+
+ // Important: the common part MUST BE COMMON ON ALL THREE structs
+ struct CommonDef
+ {
+ QString name;
+ QString doc;
+ QString metavar;
+ QVariant def;
+ };
+
+ struct OptionDef
+ {
+ // common
+ QString name;
+ QString doc;
+ QString metavar;
+ QVariant def;
+ // option
+ OptionType type;
+ QChar flag;
+ };
+
+ struct PositionalDef
+ {
+ // common
+ QString name;
+ QString doc;
+ QString metavar;
+ QVariant def;
+ // positional
+ bool required;
+ };
+
+ QHash<QString, OptionDef *> m_options;
+ QHash<QChar, OptionDef *> m_flags;
+ QHash<QString, CommonDef *> m_params;
+ QList<PositionalDef *> m_positionals;
+ QList<OptionDef *> m_optionList;
+
+ void getPrefix(QString &opt, QString &flag);
+};
+}
diff --git a/api/logic/DefaultVariable.h b/api/logic/DefaultVariable.h
new file mode 100644
index 00000000..38d7ecc2
--- /dev/null
+++ b/api/logic/DefaultVariable.h
@@ -0,0 +1,35 @@
+#pragma once
+
+template <typename T>
+class DefaultVariable
+{
+public:
+ DefaultVariable(const T & value)
+ {
+ defaultValue = value;
+ }
+ DefaultVariable<T> & operator =(const T & value)
+ {
+ currentValue = value;
+ is_default = currentValue == defaultValue;
+ is_explicit = true;
+ return *this;
+ }
+ operator const T &() const
+ {
+ return is_default ? defaultValue : currentValue;
+ }
+ bool isDefault() const
+ {
+ return is_default;
+ }
+ bool isExplicit() const
+ {
+ return is_explicit;
+ }
+private:
+ T currentValue;
+ T defaultValue;
+ bool is_default = true;
+ bool is_explicit = false;
+};
diff --git a/api/logic/Env.cpp b/api/logic/Env.cpp
new file mode 100644
index 00000000..cc0c5981
--- /dev/null
+++ b/api/logic/Env.cpp
@@ -0,0 +1,222 @@
+#include "Env.h"
+#include "net/HttpMetaCache.h"
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+#include <QDir>
+#include <QNetworkProxy>
+#include <QNetworkAccessManager>
+#include <QDebug>
+#include "tasks/Task.h"
+#include "wonko/WonkoIndex.h"
+#include <QDebug>
+
+/*
+ * The *NEW* global rat nest of an object. Handle with care.
+ */
+
+Env::Env()
+{
+ m_qnam = std::make_shared<QNetworkAccessManager>();
+}
+
+void Env::destroy()
+{
+ m_metacache.reset();
+ m_qnam.reset();
+ m_versionLists.clear();
+}
+
+Env& Env::Env::getInstance()
+{
+ static Env instance;
+ return instance;
+}
+
+std::shared_ptr< HttpMetaCache > Env::metacache()
+{
+ Q_ASSERT(m_metacache != nullptr);
+ return m_metacache;
+}
+
+std::shared_ptr< QNetworkAccessManager > Env::qnam()
+{
+ return m_qnam;
+}
+
+/*
+class NullVersion : public BaseVersion
+{
+ Q_OBJECT
+public:
+ virtual QString name()
+ {
+ return "null";
+ }
+ virtual QString descriptor()
+ {
+ return "null";
+ }
+ virtual QString typeString() const
+ {
+ return "Null";
+ }
+};
+
+class NullTask: public Task
+{
+ Q_OBJECT
+public:
+ virtual void executeTask()
+ {
+ emitFailed(tr("Nothing to do."));
+ }
+};
+
+class NullVersionList: public BaseVersionList
+{
+ Q_OBJECT
+public:
+ virtual const BaseVersionPtr at(int i) const
+ {
+ return std::make_shared<NullVersion>();
+ }
+ virtual int count() const
+ {
+ return 0;
+ };
+ virtual Task* getLoadTask()
+ {
+ return new NullTask;
+ }
+ virtual bool isLoaded()
+ {
+ return false;
+ }
+ virtual void sort()
+ {
+ }
+ virtual void updateListData(QList< BaseVersionPtr >)
+ {
+ }
+};
+*/
+
+BaseVersionPtr Env::getVersion(QString component, QString version)
+{
+ auto list = getVersionList(component);
+ if(!list)
+ {
+ return nullptr;
+ }
+ return list->findVersion(version);
+}
+
+std::shared_ptr< BaseVersionList > Env::getVersionList(QString component)
+{
+ auto iter = m_versionLists.find(component);
+ if(iter != m_versionLists.end())
+ {
+ return *iter;
+ }
+ //return std::make_shared<NullVersionList>();
+ return nullptr;
+}
+
+void Env::registerVersionList(QString name, std::shared_ptr< BaseVersionList > vlist)
+{
+ m_versionLists[name] = vlist;
+}
+
+std::shared_ptr<WonkoIndex> Env::wonkoIndex()
+{
+ if (!m_wonkoIndex)
+ {
+ m_wonkoIndex = std::make_shared<WonkoIndex>();
+ }
+ return m_wonkoIndex;
+}
+
+
+void Env::initHttpMetaCache()
+{
+ m_metacache.reset(new HttpMetaCache("metacache"));
+ m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath());
+ m_metacache->addBase("asset_objects", QDir("assets/objects").absolutePath());
+ m_metacache->addBase("versions", QDir("versions").absolutePath());
+ m_metacache->addBase("libraries", QDir("libraries").absolutePath());
+ m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath());
+ m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath());
+ m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath());
+ m_metacache->addBase("general", QDir("cache").absolutePath());
+ m_metacache->addBase("skins", QDir("accounts/skins").absolutePath());
+ m_metacache->addBase("root", QDir::currentPath());
+ m_metacache->addBase("translations", QDir("translations").absolutePath());
+ m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
+ m_metacache->addBase("wonko", QDir("cache/wonko").absolutePath());
+ m_metacache->Load();
+}
+
+void Env::updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password)
+{
+ // Set the application proxy settings.
+ if (proxyTypeStr == "SOCKS5")
+ {
+ QNetworkProxy::setApplicationProxy(
+ QNetworkProxy(QNetworkProxy::Socks5Proxy, addr, port, user, password));
+ }
+ else if (proxyTypeStr == "HTTP")
+ {
+ QNetworkProxy::setApplicationProxy(
+ QNetworkProxy(QNetworkProxy::HttpProxy, addr, port, user, password));
+ }
+ else if (proxyTypeStr == "None")
+ {
+ // If we have no proxy set, set no proxy and return.
+ QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::NoProxy));
+ }
+ else
+ {
+ // If we have "Default" selected, set Qt to use the system proxy settings.
+ QNetworkProxyFactory::setUseSystemConfiguration(true);
+ }
+
+ qDebug() << "Detecting proxy settings...";
+ QNetworkProxy proxy = QNetworkProxy::applicationProxy();
+ if (m_qnam.get())
+ m_qnam->setProxy(proxy);
+ QString proxyDesc;
+ if (proxy.type() == QNetworkProxy::NoProxy)
+ {
+ qDebug() << "Using no proxy is an option!";
+ return;
+ }
+ switch (proxy.type())
+ {
+ case QNetworkProxy::DefaultProxy:
+ proxyDesc = "Default proxy: ";
+ break;
+ case QNetworkProxy::Socks5Proxy:
+ proxyDesc = "Socks5 proxy: ";
+ break;
+ case QNetworkProxy::HttpProxy:
+ proxyDesc = "HTTP proxy: ";
+ break;
+ case QNetworkProxy::HttpCachingProxy:
+ proxyDesc = "HTTP caching: ";
+ break;
+ case QNetworkProxy::FtpCachingProxy:
+ proxyDesc = "FTP caching: ";
+ break;
+ default:
+ proxyDesc = "DERP proxy: ";
+ break;
+ }
+ proxyDesc += QString("%3@%1:%2 pass %4")
+ .arg(proxy.hostName())
+ .arg(proxy.port())
+ .arg(proxy.user())
+ .arg(proxy.password());
+ qDebug() << proxyDesc;
+}
+
+#include "Env.moc"
diff --git a/api/logic/Env.h b/api/logic/Env.h
new file mode 100644
index 00000000..4d8945d7
--- /dev/null
+++ b/api/logic/Env.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include <memory>
+#include <QString>
+#include <QMap>
+
+#include "multimc_logic_export.h"
+
+class QNetworkAccessManager;
+class HttpMetaCache;
+class BaseVersionList;
+class BaseVersion;
+class WonkoIndex;
+
+#if defined(ENV)
+ #undef ENV
+#endif
+#define ENV (Env::getInstance())
+
+class MULTIMC_LOGIC_EXPORT Env
+{
+ friend class MultiMC;
+private:
+ Env();
+public:
+ static Env& getInstance();
+
+ // call when Qt stuff is being torn down
+ void destroy();
+
+ std::shared_ptr<QNetworkAccessManager> qnam();
+
+ std::shared_ptr<HttpMetaCache> metacache();
+
+ /// init the cache. FIXME: possible future hook point
+ void initHttpMetaCache();
+
+ /// Updates the application proxy settings from the settings object.
+ void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password);
+
+ /// get a version list by name
+ std::shared_ptr<BaseVersionList> getVersionList(QString component);
+
+ /// get a version by list name and version name
+ std::shared_ptr<BaseVersion> getVersion(QString component, QString version);
+
+ void registerVersionList(QString name, std::shared_ptr<BaseVersionList> vlist);
+
+ std::shared_ptr<WonkoIndex> wonkoIndex();
+
+ QString wonkoRootUrl() const { return m_wonkoRootUrl; }
+ void setWonkoRootUrl(const QString &url) { m_wonkoRootUrl = url; }
+
+protected:
+ std::shared_ptr<QNetworkAccessManager> m_qnam;
+ std::shared_ptr<HttpMetaCache> m_metacache;
+ QMap<QString, std::shared_ptr<BaseVersionList>> m_versionLists;
+ std::shared_ptr<WonkoIndex> m_wonkoIndex;
+ QString m_wonkoRootUrl;
+};
diff --git a/api/logic/Exception.h b/api/logic/Exception.h
new file mode 100644
index 00000000..30c7aa45
--- /dev/null
+++ b/api/logic/Exception.h
@@ -0,0 +1,34 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QString>
+#include <QDebug>
+#include <exception>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT Exception : public std::exception
+{
+public:
+ Exception(const QString &message) : std::exception(), m_message(message)
+ {
+ qCritical() << "Exception:" << message;
+ }
+ Exception(const Exception &other)
+ : std::exception(), m_message(other.cause())
+ {
+ }
+ virtual ~Exception() noexcept {}
+ const char *what() const noexcept
+ {
+ return m_message.toLatin1().constData();
+ }
+ QString cause() const
+ {
+ return m_message;
+ }
+
+private:
+ QString m_message;
+};
diff --git a/api/logic/FileSystem.cpp b/api/logic/FileSystem.cpp
new file mode 100644
index 00000000..049f1e38
--- /dev/null
+++ b/api/logic/FileSystem.cpp
@@ -0,0 +1,436 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "FileSystem.h"
+
+#include <QDir>
+#include <QSaveFile>
+#include <QFileInfo>
+#include <QDebug>
+#include <QUrl>
+#include <QStandardPaths>
+
+namespace FS {
+
+void ensureExists(const QDir &dir)
+{
+ if (!QDir().mkpath(dir.absolutePath()))
+ {
+ throw FileSystemException("Unable to create directory " + dir.dirName() + " (" +
+ dir.absolutePath() + ")");
+ }
+}
+
+void write(const QString &filename, const QByteArray &data)
+{
+ ensureExists(QFileInfo(filename).dir());
+ QSaveFile file(filename);
+ if (!file.open(QSaveFile::WriteOnly))
+ {
+ throw FileSystemException("Couldn't open " + filename + " for writing: " +
+ file.errorString());
+ }
+ if (data.size() != file.write(data))
+ {
+ throw FileSystemException("Error writing data to " + filename + ": " +
+ file.errorString());
+ }
+ if (!file.commit())
+ {
+ throw FileSystemException("Error while committing data to " + filename + ": " +
+ file.errorString());
+ }
+}
+
+QByteArray read(const QString &filename)
+{
+ QFile file(filename);
+ if (!file.open(QFile::ReadOnly))
+ {
+ throw FileSystemException("Unable to open " + filename + " for reading: " +
+ file.errorString());
+ }
+ const qint64 size = file.size();
+ QByteArray data(int(size), 0);
+ const qint64 ret = file.read(data.data(), size);
+ if (ret == -1 || ret != size)
+ {
+ throw FileSystemException("Error reading data from " + filename + ": " +
+ file.errorString());
+ }
+ return data;
+}
+
+bool ensureFilePathExists(QString filenamepath)
+{
+ QFileInfo a(filenamepath);
+ QDir dir;
+ QString ensuredPath = a.path();
+ bool success = dir.mkpath(ensuredPath);
+ return success;
+}
+
+bool ensureFolderPathExists(QString foldernamepath)
+{
+ QFileInfo a(foldernamepath);
+ QDir dir;
+ QString ensuredPath = a.filePath();
+ bool success = dir.mkpath(ensuredPath);
+ return success;
+}
+
+bool copy::operator()(const QString &offset)
+{
+ //NOTE always deep copy on windows. the alternatives are too messy.
+ #if defined Q_OS_WIN32
+ m_followSymlinks = true;
+ #endif
+
+ auto src = PathCombine(m_src.absolutePath(), offset);
+ auto dst = PathCombine(m_dst.absolutePath(), offset);
+
+ QFileInfo currentSrc(src);
+ if (!currentSrc.exists())
+ return false;
+
+ if(!m_followSymlinks && currentSrc.isSymLink())
+ {
+ qDebug() << "creating symlink" << src << " - " << dst;
+ if (!ensureFilePathExists(dst))
+ {
+ qWarning() << "Cannot create path!";
+ return false;
+ }
+ return QFile::link(currentSrc.symLinkTarget(), dst);
+ }
+ else if(currentSrc.isFile())
+ {
+ qDebug() << "copying file" << src << " - " << dst;
+ if (!ensureFilePathExists(dst))
+ {
+ qWarning() << "Cannot create path!";
+ return false;
+ }
+ return QFile::copy(src, dst);
+ }
+ else if(currentSrc.isDir())
+ {
+ qDebug() << "recursing" << offset;
+ if (!ensureFolderPathExists(dst))
+ {
+ qWarning() << "Cannot create path!";
+ return false;
+ }
+ QDir currentDir(src);
+ for(auto & f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System))
+ {
+ auto inner_offset = PathCombine(offset, f);
+ // ignore and skip stuff that matches the blacklist.
+ if(m_blacklist && m_blacklist->matches(inner_offset))
+ {
+ continue;
+ }
+ if(!operator()(inner_offset))
+ {
+ return false;
+ }
+ }
+ }
+ else
+ {
+ qCritical() << "Copy ERROR: Unknown filesystem object:" << src;
+ return false;
+ }
+ return true;
+}
+
+
+#if defined Q_OS_WIN32
+#include <windows.h>
+#include <string>
+#endif
+bool deletePath(QString path)
+{
+ bool OK = true;
+ QDir dir(path);
+
+ if (!dir.exists())
+ {
+ return OK;
+ }
+ auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
+ QDir::AllDirs | QDir::Files,
+ QDir::DirsFirst);
+
+ for(auto & info: allEntries)
+ {
+#if defined Q_OS_WIN32
+ QString nativePath = QDir::toNativeSeparators(info.absoluteFilePath());
+ auto wString = nativePath.toStdWString();
+ DWORD dwAttrs = GetFileAttributesW(wString.c_str());
+ // Windows: check for junctions, reparse points and other nasty things of that sort
+ if(dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT)
+ {
+ if (info.isFile())
+ {
+ OK &= QFile::remove(info.absoluteFilePath());
+ }
+ else if (info.isDir())
+ {
+ OK &= dir.rmdir(info.absoluteFilePath());
+ }
+ }
+#else
+ // We do not trust Qt with reparse points, but do trust it with unix symlinks.
+ if(info.isSymLink())
+ {
+ OK &= QFile::remove(info.absoluteFilePath());
+ }
+#endif
+ else if (info.isDir())
+ {
+ OK &= deletePath(info.absoluteFilePath());
+ }
+ else if (info.isFile())
+ {
+ OK &= QFile::remove(info.absoluteFilePath());
+ }
+ else
+ {
+ OK = false;
+ qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath();
+ }
+ }
+ OK &= dir.rmdir(dir.absolutePath());
+ return OK;
+}
+
+
+QString PathCombine(QString path1, QString path2)
+{
+ if(!path1.size())
+ return path2;
+ if(!path2.size())
+ return path1;
+ return QDir::cleanPath(path1 + QDir::separator() + path2);
+}
+
+QString PathCombine(QString path1, QString path2, QString path3)
+{
+ return PathCombine(PathCombine(path1, path2), path3);
+}
+
+QString AbsolutePath(QString path)
+{
+ return QFileInfo(path).absolutePath();
+}
+
+QString ResolveExecutable(QString path)
+{
+ if (path.isEmpty())
+ {
+ return QString();
+ }
+ if(!path.contains('/'))
+ {
+ path = QStandardPaths::findExecutable(path);
+ }
+ QFileInfo pathInfo(path);
+ if(!pathInfo.exists() || !pathInfo.isExecutable())
+ {
+ return QString();
+ }
+ return pathInfo.absoluteFilePath();
+}
+
+/**
+ * Normalize path
+ *
+ * Any paths inside the current directory will be normalized to relative paths (to current)
+ * Other paths will be made absolute
+ */
+QString NormalizePath(QString path)
+{
+ QDir a = QDir::currentPath();
+ QString currentAbsolute = a.absolutePath();
+
+ QDir b(path);
+ QString newAbsolute = b.absolutePath();
+
+ if (newAbsolute.startsWith(currentAbsolute))
+ {
+ return a.relativeFilePath(newAbsolute);
+ }
+ else
+ {
+ return newAbsolute;
+ }
+}
+
+QString badFilenameChars = "\"\\/?<>:*|!";
+
+QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
+{
+ for (int i = 0; i < string.length(); i++)
+ {
+ if (badFilenameChars.contains(string[i]))
+ {
+ string[i] = replaceWith;
+ }
+ }
+ return string;
+}
+
+QString DirNameFromString(QString string, QString inDir)
+{
+ int num = 0;
+ QString baseName = RemoveInvalidFilenameChars(string, '-');
+ QString dirName;
+ do
+ {
+ if(num == 0)
+ {
+ dirName = baseName;
+ }
+ else
+ {
+ dirName = baseName + QString::number(num);;
+ }
+
+ // If it's over 9000
+ if (num > 9000)
+ return "";
+ num++;
+ } while (QFileInfo(PathCombine(inDir, dirName)).exists());
+ return dirName;
+}
+
+// Does the directory path contain any '!'? If yes, return true, otherwise false.
+// (This is a problem for Java)
+bool checkProblemticPathJava(QDir folder)
+{
+ QString pathfoldername = folder.absolutePath();
+ return pathfoldername.contains("!", Qt::CaseInsensitive);
+}
+
+#include <QStandardPaths>
+#include <QFile>
+#include <QTextStream>
+
+// Win32 crap
+#if defined Q_OS_WIN
+
+#include <windows.h>
+#include <winnls.h>
+#include <shobjidl.h>
+#include <objbase.h>
+#include <objidl.h>
+#include <shlguid.h>
+#include <shlobj.h>
+
+bool called_coinit = false;
+
+HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args)
+{
+ HRESULT hres;
+
+ if (!called_coinit)
+ {
+ hres = CoInitialize(NULL);
+ called_coinit = true;
+
+ if (!SUCCEEDED(hres))
+ {
+ qWarning("Failed to initialize COM. Error 0x%08X", hres);
+ return hres;
+ }
+ }
+
+ IShellLink *link;
+ hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink,
+ (LPVOID *)&link);
+
+ if (SUCCEEDED(hres))
+ {
+ IPersistFile *persistFile;
+
+ link->SetPath(targetPath);
+ link->SetArguments(args);
+
+ hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile);
+ if (SUCCEEDED(hres))
+ {
+ WCHAR wstr[MAX_PATH];
+
+ MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH);
+
+ hres = persistFile->Save(wstr, TRUE);
+ persistFile->Release();
+ }
+ link->Release();
+ }
+ return hres;
+}
+
+#endif
+
+QString getDesktopDir()
+{
+ return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
+}
+
+// Cross-platform Shortcut creation
+bool createShortCut(QString location, QString dest, QStringList args, QString name,
+ QString icon)
+{
+#if defined Q_OS_LINUX
+ location = PathCombine(location, name + ".desktop");
+
+ QFile f(location);
+ f.open(QIODevice::WriteOnly | QIODevice::Text);
+ QTextStream stream(&f);
+
+ QString argstring;
+ if (!args.empty())
+ argstring = " '" + args.join("' '") + "'";
+
+ stream << "[Desktop Entry]"
+ << "\n";
+ stream << "Type=Application"
+ << "\n";
+ stream << "TryExec=" << dest.toLocal8Bit() << "\n";
+ stream << "Exec=" << dest.toLocal8Bit() << argstring.toLocal8Bit() << "\n";
+ stream << "Name=" << name.toLocal8Bit() << "\n";
+ stream << "Icon=" << icon.toLocal8Bit() << "\n";
+
+ stream.flush();
+ f.close();
+
+ f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup |
+ QFileDevice::ExeOther);
+
+ return true;
+#elif defined Q_OS_WIN
+ // TODO: Fix
+ // QFile file(PathCombine(location, name + ".lnk"));
+ // WCHAR *file_w;
+ // WCHAR *dest_w;
+ // WCHAR *args_w;
+ // file.fileName().toWCharArray(file_w);
+ // dest.toWCharArray(dest_w);
+
+ // QString argStr;
+ // for (int i = 0; i < args.count(); i++)
+ // {
+ // argStr.append(args[i]);
+ // argStr.append(" ");
+ // }
+ // argStr.toWCharArray(args_w);
+
+ // return SUCCEEDED(CreateLink(file_w, dest_w, args_w));
+ return false;
+#else
+ qWarning("Desktop Shortcuts not supported on your platform!");
+ return false;
+#endif
+}
+}
diff --git a/api/logic/FileSystem.h b/api/logic/FileSystem.h
new file mode 100644
index 00000000..80637f90
--- /dev/null
+++ b/api/logic/FileSystem.h
@@ -0,0 +1,123 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include "Exception.h"
+#include "pathmatcher/IPathMatcher.h"
+
+#include "multimc_logic_export.h"
+#include <QDir>
+#include <QFlags>
+
+namespace FS
+{
+
+class MULTIMC_LOGIC_EXPORT FileSystemException : public ::Exception
+{
+public:
+ FileSystemException(const QString &message) : Exception(message) {}
+};
+
+/**
+ * write data to a file safely
+ */
+MULTIMC_LOGIC_EXPORT void write(const QString &filename, const QByteArray &data);
+
+/**
+ * read data from a file safely\
+ */
+MULTIMC_LOGIC_EXPORT QByteArray read(const QString &filename);
+
+/**
+ * Creates all the folders in a path for the specified path
+ * last segment of the path is treated as a file name and is ignored!
+ */
+MULTIMC_LOGIC_EXPORT bool ensureFilePathExists(QString filenamepath);
+
+/**
+ * Creates all the folders in a path for the specified path
+ * last segment of the path is treated as a folder name and is created!
+ */
+MULTIMC_LOGIC_EXPORT bool ensureFolderPathExists(QString filenamepath);
+
+class MULTIMC_LOGIC_EXPORT copy
+{
+public:
+ copy(const copy&) = delete;
+ copy(const QString & src, const QString & dst)
+ {
+ m_src = src;
+ m_dst = dst;
+ }
+ copy & followSymlinks(const bool follow)
+ {
+ m_followSymlinks = follow;
+ return *this;
+ }
+ copy & blacklist(const IPathMatcher * filter)
+ {
+ m_blacklist = filter;
+ return *this;
+ }
+ bool operator()()
+ {
+ return operator()(QString());
+ }
+
+private:
+ bool operator()(const QString &offset);
+
+private:
+ bool m_followSymlinks = true;
+ const IPathMatcher * m_blacklist = nullptr;
+ QDir m_src;
+ QDir m_dst;
+};
+
+/**
+ * Delete a folder recursively
+ */
+MULTIMC_LOGIC_EXPORT bool deletePath(QString path);
+
+MULTIMC_LOGIC_EXPORT QString PathCombine(QString path1, QString path2);
+MULTIMC_LOGIC_EXPORT QString PathCombine(QString path1, QString path2, QString path3);
+
+MULTIMC_LOGIC_EXPORT QString AbsolutePath(QString path);
+
+/**
+ * Resolve an executable
+ *
+ * Will resolve:
+ * single executable (by name)
+ * relative path
+ * absolute path
+ *
+ * @return absolute path to executable or null string
+ */
+MULTIMC_LOGIC_EXPORT QString ResolveExecutable(QString path);
+
+/**
+ * Normalize path
+ *
+ * Any paths inside the current directory will be normalized to relative paths (to current)
+ * Other paths will be made absolute
+ *
+ * Returns false if the path logic somehow filed (and normalizedPath in invalid)
+ */
+MULTIMC_LOGIC_EXPORT QString NormalizePath(QString path);
+
+MULTIMC_LOGIC_EXPORT QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-');
+
+MULTIMC_LOGIC_EXPORT QString DirNameFromString(QString string, QString inDir = ".");
+
+/// Checks if the a given Path contains "!"
+MULTIMC_LOGIC_EXPORT bool checkProblemticPathJava(QDir folder);
+
+// Get the Directory representing the User's Desktop
+MULTIMC_LOGIC_EXPORT QString getDesktopDir();
+
+// Create a shortcut at *location*, pointing to *dest* called with the arguments *args*
+// call it *name* and assign it the icon *icon*
+// return true if operation succeeded
+MULTIMC_LOGIC_EXPORT bool createShortCut(QString location, QString dest, QStringList args, QString name, QString iconLocation);
+}
diff --git a/api/logic/GZip.cpp b/api/logic/GZip.cpp
new file mode 100644
index 00000000..38605df6
--- /dev/null
+++ b/api/logic/GZip.cpp
@@ -0,0 +1,115 @@
+#include "GZip.h"
+#include <zlib.h>
+#include <QByteArray>
+
+bool GZip::unzip(const QByteArray &compressedBytes, QByteArray &uncompressedBytes)
+{
+ if (compressedBytes.size() == 0)
+ {
+ uncompressedBytes = compressedBytes;
+ return true;
+ }
+
+ unsigned uncompLength = compressedBytes.size();
+ uncompressedBytes.clear();
+ uncompressedBytes.resize(uncompLength);
+
+ z_stream strm;
+ memset(&strm, 0, sizeof(strm));
+ strm.next_in = (Bytef *)compressedBytes.data();
+ strm.avail_in = compressedBytes.size();
+
+ bool done = false;
+
+ if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK)
+ {
+ return false;
+ }
+
+ int err = Z_OK;
+
+ while (!done)
+ {
+ // If our output buffer is too small
+ if (strm.total_out >= uncompLength)
+ {
+ uncompressedBytes.resize(uncompLength * 2);
+ uncompLength *= 2;
+ }
+
+ strm.next_out = (Bytef *)(uncompressedBytes.data() + strm.total_out);
+ strm.avail_out = uncompLength - strm.total_out;
+
+ // Inflate another chunk.
+ err = inflate(&strm, Z_SYNC_FLUSH);
+ if (err == Z_STREAM_END)
+ done = true;
+ else if (err != Z_OK)
+ {
+ break;
+ }
+ }
+
+ if (inflateEnd(&strm) != Z_OK || !done)
+ {
+ return false;
+ }
+
+ uncompressedBytes.resize(strm.total_out);
+ return true;
+}
+
+bool GZip::zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes)
+{
+ if (uncompressedBytes.size() == 0)
+ {
+ compressedBytes = uncompressedBytes;
+ return true;
+ }
+
+ unsigned compLength = std::min(uncompressedBytes.size(), 16);
+ compressedBytes.clear();
+ compressedBytes.resize(compLength);
+
+ z_stream zs;
+ memset(&zs, 0, sizeof(zs));
+
+ if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (16 + MAX_WBITS), 8, Z_DEFAULT_STRATEGY) != Z_OK)
+ {
+ return false;
+ }
+
+ zs.next_in = (Bytef*)uncompressedBytes.data();
+ zs.avail_in = uncompressedBytes.size();
+
+ int ret;
+ compressedBytes.resize(uncompressedBytes.size());
+
+ unsigned offset = 0;
+ unsigned temp = 0;
+ do
+ {
+ auto remaining = compressedBytes.size() - offset;
+ if(remaining < 1)
+ {
+ compressedBytes.resize(compressedBytes.size() * 2);
+ }
+ zs.next_out = (Bytef *) (compressedBytes.data() + offset);
+ temp = zs.avail_out = compressedBytes.size() - offset;
+ ret = deflate(&zs, Z_FINISH);
+ offset += temp - zs.avail_out;
+ } while (ret == Z_OK);
+
+ compressedBytes.resize(offset);
+
+ if (deflateEnd(&zs) != Z_OK)
+ {
+ return false;
+ }
+
+ if (ret != Z_STREAM_END)
+ {
+ return false;
+ }
+ return true;
+} \ No newline at end of file
diff --git a/api/logic/GZip.h b/api/logic/GZip.h
new file mode 100644
index 00000000..6993a222
--- /dev/null
+++ b/api/logic/GZip.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <QByteArray>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT GZip
+{
+public:
+ static bool unzip(const QByteArray &compressedBytes, QByteArray &uncompressedBytes);
+ static bool zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes);
+};
+
diff --git a/api/logic/InstanceList.cpp b/api/logic/InstanceList.cpp
new file mode 100644
index 00000000..783df660
--- /dev/null
+++ b/api/logic/InstanceList.cpp
@@ -0,0 +1,580 @@
+/* Copyright 2013-2015 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 <QSet>
+#include <QFile>
+#include <QDirIterator>
+#include <QThread>
+#include <QTextStream>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QXmlStreamReader>
+#include <QRegularExpression>
+#include <QDebug>
+
+#include "InstanceList.h"
+#include "BaseInstance.h"
+
+//FIXME: this really doesn't belong *here*
+#include "minecraft/onesix/OneSixInstance.h"
+#include "minecraft/legacy/LegacyInstance.h"
+#include "minecraft/ftb/FTBPlugin.h"
+#include "minecraft/MinecraftVersion.h"
+#include "settings/INISettingsObject.h"
+#include "NullInstance.h"
+#include "FileSystem.h"
+#include "pathmatcher/RegexpMatcher.h"
+
+const static int GROUP_FILE_FORMAT_VERSION = 1;
+
+InstanceList::InstanceList(SettingsObjectPtr globalSettings, const QString &instDir, QObject *parent)
+ : QAbstractListModel(parent), m_instDir(instDir)
+{
+ m_globalSettings = globalSettings;
+ if (!QDir::current().exists(m_instDir))
+ {
+ QDir::current().mkpath(m_instDir);
+ }
+}
+
+InstanceList::~InstanceList()
+{
+}
+
+int InstanceList::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return m_instances.count();
+}
+
+QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ if (row < 0 || row >= m_instances.size())
+ return QModelIndex();
+ return createIndex(row, column, (void *)m_instances.at(row).get());
+}
+
+QVariant InstanceList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ {
+ return QVariant();
+ }
+ BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
+ switch (role)
+ {
+ case InstancePointerRole:
+ {
+ QVariant v = qVariantFromValue((void *)pdata);
+ return v;
+ }
+ case InstanceIDRole:
+ {
+ return pdata->id();
+ }
+ case Qt::DisplayRole:
+ {
+ return pdata->name();
+ }
+ case Qt::ToolTipRole:
+ {
+ return pdata->instanceRoot();
+ }
+ case Qt::DecorationRole:
+ {
+ return pdata->iconKey();
+ }
+ // HACK: see GroupView.h in gui!
+ case GroupRole:
+ {
+ return pdata->group();
+ }
+ default:
+ break;
+ }
+ return QVariant();
+}
+
+Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
+{
+ Qt::ItemFlags f;
+ if (index.isValid())
+ {
+ f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+ }
+ return f;
+}
+
+void InstanceList::groupChanged()
+{
+ // save the groups. save all of them.
+ saveGroupList();
+}
+
+QStringList InstanceList::getGroups()
+{
+ return m_groups.toList();
+}
+
+void InstanceList::suspendGroupSaving()
+{
+ suspendedGroupSave = true;
+}
+
+void InstanceList::resumeGroupSaving()
+{
+ if(suspendedGroupSave)
+ {
+ suspendedGroupSave = false;
+ if(queuedGroupSave)
+ {
+ saveGroupList();
+ }
+ }
+}
+
+void InstanceList::deleteGroup(const QString& name)
+{
+ for(auto & instance: m_instances)
+ {
+ auto instGroupName = instance->group();
+ if(instGroupName == name)
+ {
+ instance->setGroupPost(QString());
+ }
+ }
+}
+
+void InstanceList::saveGroupList()
+{
+ if(suspendedGroupSave)
+ {
+ queuedGroupSave = true;
+ return;
+ }
+
+ QString groupFileName = m_instDir + "/instgroups.json";
+ QMap<QString, QSet<QString>> groupMap;
+ for (auto instance : m_instances)
+ {
+ QString id = instance->id();
+ QString group = instance->group();
+ if (group.isEmpty())
+ continue;
+
+ // keep a list/set of groups for choosing
+ m_groups.insert(group);
+
+ if (!groupMap.count(group))
+ {
+ QSet<QString> set;
+ set.insert(id);
+ groupMap[group] = set;
+ }
+ else
+ {
+ QSet<QString> &set = groupMap[group];
+ set.insert(id);
+ }
+ }
+ QJsonObject toplevel;
+ toplevel.insert("formatVersion", QJsonValue(QString("1")));
+ QJsonObject groupsArr;
+ for (auto iter = groupMap.begin(); iter != groupMap.end(); iter++)
+ {
+ auto list = iter.value();
+ auto name = iter.key();
+ QJsonObject groupObj;
+ QJsonArray instanceArr;
+ groupObj.insert("hidden", QJsonValue(QString("false")));
+ for (auto item : list)
+ {
+ instanceArr.append(QJsonValue(item));
+ }
+ groupObj.insert("instances", instanceArr);
+ groupsArr.insert(name, groupObj);
+ }
+ toplevel.insert("groups", groupsArr);
+ QJsonDocument doc(toplevel);
+ try
+ {
+ FS::write(groupFileName, doc.toJson());
+ }
+ catch(FS::FileSystemException & e)
+ {
+ qCritical() << "Failed to write instance group file :" << e.cause();
+ }
+}
+
+void InstanceList::loadGroupList(QMap<QString, QString> &groupMap)
+{
+ QString groupFileName = m_instDir + "/instgroups.json";
+
+ // if there's no group file, fail
+ if (!QFileInfo(groupFileName).exists())
+ return;
+
+ QByteArray jsonData;
+ try
+ {
+ jsonData = FS::read(groupFileName);
+ }
+ catch (FS::FileSystemException & e)
+ {
+ qCritical() << "Failed to read instance group file :" << e.cause();
+ return;
+ }
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error);
+
+ // if the json was bad, fail
+ if (error.error != QJsonParseError::NoError)
+ {
+ qCritical() << QString("Failed to parse instance group file: %1 at offset %2")
+ .arg(error.errorString(), QString::number(error.offset))
+ .toUtf8();
+ return;
+ }
+
+ // if the root of the json wasn't an object, fail
+ if (!jsonDoc.isObject())
+ {
+ qWarning() << "Invalid group file. Root entry should be an object.";
+ return;
+ }
+
+ QJsonObject rootObj = jsonDoc.object();
+
+ // Make sure the format version matches, otherwise fail.
+ if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION)
+ return;
+
+ // Get the groups. if it's not an object, fail
+ if (!rootObj.value("groups").isObject())
+ {
+ qWarning() << "Invalid group list JSON: 'groups' should be an object.";
+ return;
+ }
+
+ // Iterate through all the groups.
+ QJsonObject groupMapping = rootObj.value("groups").toObject();
+ for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++)
+ {
+ QString groupName = iter.key();
+
+ // If not an object, complain and skip to the next one.
+ if (!iter.value().isObject())
+ {
+ qWarning() << QString("Group '%1' in the group list should "
+ "be an object.")
+ .arg(groupName)
+ .toUtf8();
+ continue;
+ }
+
+ QJsonObject groupObj = iter.value().toObject();
+ if (!groupObj.value("instances").isArray())
+ {
+ qWarning() << QString("Group '%1' in the group list is invalid. "
+ "It should contain an array "
+ "called 'instances'.")
+ .arg(groupName)
+ .toUtf8();
+ continue;
+ }
+
+ // keep a list/set of groups for choosing
+ m_groups.insert(groupName);
+
+ // Iterate through the list of instances in the group.
+ QJsonArray instancesArray = groupObj.value("instances").toArray();
+
+ for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end();
+ iter2++)
+ {
+ groupMap[(*iter2).toString()] = groupName;
+ }
+ }
+}
+
+InstanceList::InstListError InstanceList::loadList()
+{
+ // load the instance groups
+ QMap<QString, QString> groupMap;
+ loadGroupList(groupMap);
+
+ QList<InstancePtr> tempList;
+ {
+ QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable,
+ QDirIterator::FollowSymlinks);
+ while (iter.hasNext())
+ {
+ QString subDir = iter.next();
+ if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists())
+ continue;
+ qDebug() << "Loading MultiMC instance from " << subDir;
+ InstancePtr instPtr;
+ auto error = loadInstance(instPtr, subDir);
+ if(!continueProcessInstance(instPtr, error, subDir, groupMap))
+ continue;
+ tempList.append(instPtr);
+ }
+ }
+
+ // FIXME: generalize
+ FTBPlugin::loadInstances(m_globalSettings, groupMap, tempList);
+
+ beginResetModel();
+ m_instances.clear();
+ for(auto inst: tempList)
+ {
+ inst->setParent(this);
+ connect(inst.get(), SIGNAL(propertiesChanged(BaseInstance *)), this,
+ SLOT(propertiesChanged(BaseInstance *)));
+ connect(inst.get(), SIGNAL(groupChanged()), this, SLOT(groupChanged()));
+ connect(inst.get(), SIGNAL(nuked(BaseInstance *)), this,
+ SLOT(instanceNuked(BaseInstance *)));
+ m_instances.append(inst);
+ }
+ endResetModel();
+ emit dataIsInvalid();
+ return NoError;
+}
+
+/// Clear all instances. Triggers notifications.
+void InstanceList::clear()
+{
+ beginResetModel();
+ saveGroupList();
+ m_instances.clear();
+ endResetModel();
+ emit dataIsInvalid();
+}
+
+void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
+{
+ m_instDir = value.toString();
+ loadList();
+}
+
+/// Add an instance. Triggers notifications, returns the new index
+int InstanceList::add(InstancePtr t)
+{
+ beginInsertRows(QModelIndex(), m_instances.size(), m_instances.size());
+ m_instances.append(t);
+ t->setParent(this);
+ connect(t.get(), SIGNAL(propertiesChanged(BaseInstance *)), this,
+ SLOT(propertiesChanged(BaseInstance *)));
+ connect(t.get(), SIGNAL(groupChanged()), this, SLOT(groupChanged()));
+ connect(t.get(), SIGNAL(nuked(BaseInstance *)), this, SLOT(instanceNuked(BaseInstance *)));
+ endInsertRows();
+ return count() - 1;
+}
+
+InstancePtr InstanceList::getInstanceById(QString instId) const
+{
+ if(instId.isEmpty())
+ return InstancePtr();
+ for(auto & inst: m_instances)
+ {
+ if (inst->id() == instId)
+ {
+ return inst;
+ }
+ }
+ return InstancePtr();
+}
+
+QModelIndex InstanceList::getInstanceIndexById(const QString &id) const
+{
+ return index(getInstIndex(getInstanceById(id).get()));
+}
+
+int InstanceList::getInstIndex(BaseInstance *inst) const
+{
+ int count = m_instances.count();
+ for (int i = 0; i < count; i++)
+ {
+ if (inst == m_instances[i].get())
+ {
+ return i;
+ }
+ }
+ return -1;
+}
+
+bool InstanceList::continueProcessInstance(InstancePtr instPtr, const int error,
+ const QDir &dir, QMap<QString, QString> &groupMap)
+{
+ if (error != InstanceList::NoLoadError && error != InstanceList::NotAnInstance)
+ {
+ QString errorMsg = QString("Failed to load instance %1: ")
+ .arg(QFileInfo(dir.absolutePath()).baseName())
+ .toUtf8();
+
+ switch (error)
+ {
+ default:
+ errorMsg += QString("Unknown instance loader error %1").arg(error);
+ break;
+ }
+ qCritical() << errorMsg.toUtf8();
+ return false;
+ }
+ else if (!instPtr)
+ {
+ qCritical() << QString("Error loading instance %1. Instance loader returned null.")
+ .arg(QFileInfo(dir.absolutePath()).baseName())
+ .toUtf8();
+ return false;
+ }
+ else
+ {
+ auto iter = groupMap.find(instPtr->id());
+ if (iter != groupMap.end())
+ {
+ instPtr->setGroupInitial((*iter));
+ }
+ qDebug() << "Loaded instance " << instPtr->name() << " from " << dir.absolutePath();
+ return true;
+ }
+}
+
+InstanceList::InstLoadError
+InstanceList::loadInstance(InstancePtr &inst, const QString &instDir)
+{
+ auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instDir, "instance.cfg"));
+
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+
+ QString inst_type = instanceSettings->get("InstanceType").toString();
+
+ // FIXME: replace with a map lookup, where instance classes register their types
+ if (inst_type == "OneSix" || inst_type == "Nostalgia")
+ {
+ inst.reset(new OneSixInstance(m_globalSettings, instanceSettings, instDir));
+ }
+ else if (inst_type == "Legacy")
+ {
+ inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instDir));
+ }
+ else
+ {
+ inst.reset(new NullInstance(m_globalSettings, instanceSettings, instDir));
+ }
+ inst->init();
+ return NoLoadError;
+}
+
+InstanceList::InstCreateError
+InstanceList::createInstance(InstancePtr &inst, BaseVersionPtr version, const QString &instDir)
+{
+ QDir rootDir(instDir);
+
+ qDebug() << instDir.toUtf8();
+ if (!rootDir.exists() && !rootDir.mkpath("."))
+ {
+ qCritical() << "Can't create instance folder" << instDir;
+ return InstanceList::CantCreateDir;
+ }
+
+ if (!version)
+ {
+ qCritical() << "Can't create instance for non-existing MC version";
+ return InstanceList::NoSuchVersion;
+ }
+
+ auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instDir, "instance.cfg"));
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+
+ auto minecraftVersion = std::dynamic_pointer_cast<MinecraftVersion>(version);
+ if(minecraftVersion)
+ {
+ auto mcVer = std::dynamic_pointer_cast<MinecraftVersion>(version);
+ instanceSettings->set("InstanceType", "OneSix");
+ inst.reset(new OneSixInstance(m_globalSettings, instanceSettings, instDir));
+ inst->setIntendedVersionId(version->descriptor());
+ inst->init();
+ return InstanceList::NoCreateError;
+ }
+ return InstanceList::NoSuchVersion;
+}
+
+InstanceList::InstCreateError
+InstanceList::copyInstance(InstancePtr &newInstance, InstancePtr &oldInstance, const QString &instDir, bool copySaves)
+{
+ QDir rootDir(instDir);
+ std::unique_ptr<IPathMatcher> matcher;
+ if(!copySaves)
+ {
+ auto matcherReal = new RegexpMatcher("[.]?minecraft/saves");
+ matcherReal->caseSensitive(false);
+ matcher.reset(matcherReal);
+ }
+
+ qDebug() << instDir.toUtf8();
+ FS::copy folderCopy(oldInstance->instanceRoot(), instDir);
+ folderCopy.followSymlinks(false).blacklist(matcher.get());
+ if (!folderCopy())
+ {
+ FS::deletePath(instDir);
+ return InstanceList::CantCreateDir;
+ }
+
+ INISettingsObject settings_obj(FS::PathCombine(instDir, "instance.cfg"));
+ settings_obj.registerSetting("InstanceType", "Legacy");
+ QString inst_type = settings_obj.get("InstanceType").toString();
+
+ oldInstance->copy(instDir);
+
+ auto error = loadInstance(newInstance, instDir);
+
+ switch (error)
+ {
+ case NoLoadError:
+ return NoCreateError;
+ case NotAnInstance:
+ rootDir.removeRecursively();
+ return CantCreateDir;
+ default:
+ case UnknownLoadError:
+ rootDir.removeRecursively();
+ return UnknownCreateError;
+ }
+}
+
+void InstanceList::instanceNuked(BaseInstance *inst)
+{
+ int i = getInstIndex(inst);
+ if (i != -1)
+ {
+ beginRemoveRows(QModelIndex(), i, i);
+ m_instances.removeAt(i);
+ endRemoveRows();
+ }
+}
+
+void InstanceList::propertiesChanged(BaseInstance *inst)
+{
+ int i = getInstIndex(inst);
+ if (i != -1)
+ {
+ emit dataChanged(index(i), index(i));
+ }
+}
diff --git a/api/logic/InstanceList.h b/api/logic/InstanceList.h
new file mode 100644
index 00000000..074cca7c
--- /dev/null
+++ b/api/logic/InstanceList.h
@@ -0,0 +1,187 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QAbstractListModel>
+#include <QSet>
+
+#include "BaseInstance.h"
+
+#include "multimc_logic_export.h"
+
+class BaseInstance;
+class QDir;
+
+class MULTIMC_LOGIC_EXPORT InstanceList : public QAbstractListModel
+{
+ Q_OBJECT
+private:
+ void loadGroupList(QMap<QString, QString> &groupList);
+ void suspendGroupSaving();
+ void resumeGroupSaving();
+
+public slots:
+ void saveGroupList();
+
+public:
+ explicit InstanceList(SettingsObjectPtr globalSettings, const QString &instDir, QObject *parent = 0);
+ virtual ~InstanceList();
+
+public:
+ QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role) const;
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+
+ enum AdditionalRoles
+ {
+ GroupRole = Qt::UserRole,
+ InstancePointerRole = 0x34B1CB48, ///< Return pointer to real instance
+ InstanceIDRole = 0x34B1CB49 ///< Return id if the instance
+ };
+ /*!
+ * \brief Error codes returned by functions in the InstanceList class.
+ * NoError Indicates that no error occurred.
+ * UnknownError indicates that an unspecified error occurred.
+ */
+ enum InstListError
+ {
+ NoError = 0,
+ UnknownError
+ };
+
+ enum InstLoadError
+ {
+ NoLoadError = 0,
+ UnknownLoadError,
+ NotAnInstance
+ };
+
+ enum InstCreateError
+ {
+ NoCreateError = 0,
+ NoSuchVersion,
+ UnknownCreateError,
+ InstExists,
+ CantCreateDir
+ };
+
+ QString instDir() const
+ {
+ return m_instDir;
+ }
+
+ /*!
+ * \brief Get the instance at index
+ */
+ InstancePtr at(int i) const
+ {
+ return m_instances.at(i);
+ }
+ ;
+
+ /*!
+ * \brief Get the count of loaded instances
+ */
+ int count() const
+ {
+ return m_instances.count();
+ }
+ ;
+
+ /// Clear all instances. Triggers notifications.
+ void clear();
+
+ /// Add an instance. Triggers notifications, returns the new index
+ int add(InstancePtr t);
+
+ /// Get an instance by ID
+ InstancePtr getInstanceById(QString id) const;
+
+ QModelIndex getInstanceIndexById(const QString &id) const;
+
+ // FIXME: instead of iterating through all instances and forming a set, keep the set around
+ QStringList getGroups();
+
+ void deleteGroup(const QString & name);
+
+ /*!
+ * \brief Creates a stub instance
+ *
+ * \param inst Pointer to store the created instance in.
+ * \param version Game version to use for the instance
+ * \param instDir The new instance's directory.
+ * \return An InstCreateError error code.
+ * - InstExists if the given instance directory is already an instance.
+ * - CantCreateDir if the given instance directory cannot be created.
+ */
+ InstCreateError createInstance(InstancePtr &inst, BaseVersionPtr version,
+ const QString &instDir);
+
+ /*!
+ * \brief Creates a copy of an existing instance with a new name
+ *
+ * \param newInstance Pointer to store the created instance in.
+ * \param oldInstance The instance to copy
+ * \param instDir The new instance's directory.
+ * \return An InstCreateError error code.
+ * - InstExists if the given instance directory is already an instance.
+ * - CantCreateDir if the given instance directory cannot be created.
+ */
+ InstCreateError copyInstance(InstancePtr &newInstance, InstancePtr &oldInstance,
+ const QString &instDir, bool copySaves);
+
+ /*!
+ * \brief Loads an instance from the given directory.
+ * Checks the instance's INI file to figure out what the instance's type is first.
+ * \param inst Pointer to store the loaded instance in.
+ * \param instDir The instance's directory.
+ * \return An InstLoadError error code.
+ * - NotAnInstance if the given instance directory isn't a valid instance.
+ */
+ InstLoadError loadInstance(InstancePtr &inst, const QString &instDir);
+
+signals:
+ void dataIsInvalid();
+
+public slots:
+ void on_InstFolderChanged(const Setting &setting, QVariant value);
+
+ /*!
+ * \brief Loads the instance list. Triggers notifications.
+ */
+ InstListError loadList();
+
+private slots:
+ void propertiesChanged(BaseInstance *inst);
+ void instanceNuked(BaseInstance *inst);
+ void groupChanged();
+
+private:
+ int getInstIndex(BaseInstance *inst) const;
+
+public:
+ static bool continueProcessInstance(InstancePtr instPtr, const int error, const QDir &dir, QMap<QString, QString> &groupMap);
+
+protected:
+ QString m_instDir;
+ QList<InstancePtr> m_instances;
+ QSet<QString> m_groups;
+ SettingsObjectPtr m_globalSettings;
+ bool suspendedGroupSave = false;
+ bool queuedGroupSave = false;
+};
diff --git a/api/logic/Json.cpp b/api/logic/Json.cpp
new file mode 100644
index 00000000..f2cbc8a3
--- /dev/null
+++ b/api/logic/Json.cpp
@@ -0,0 +1,272 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "Json.h"
+
+#include <QFile>
+
+#include "FileSystem.h"
+#include <math.h>
+
+namespace Json
+{
+void write(const QJsonDocument &doc, const QString &filename)
+{
+ FS::write(filename, doc.toJson());
+}
+void write(const QJsonObject &object, const QString &filename)
+{
+ write(QJsonDocument(object), filename);
+}
+void write(const QJsonArray &array, const QString &filename)
+{
+ write(QJsonDocument(array), filename);
+}
+
+QByteArray toBinary(const QJsonObject &obj)
+{
+ return QJsonDocument(obj).toBinaryData();
+}
+QByteArray toBinary(const QJsonArray &array)
+{
+ return QJsonDocument(array).toBinaryData();
+}
+QByteArray toText(const QJsonObject &obj)
+{
+ return QJsonDocument(obj).toJson(QJsonDocument::Compact);
+}
+QByteArray toText(const QJsonArray &array)
+{
+ return QJsonDocument(array).toJson(QJsonDocument::Compact);
+}
+
+static bool isBinaryJson(const QByteArray &data)
+{
+ decltype(QJsonDocument::BinaryFormatTag) tag = QJsonDocument::BinaryFormatTag;
+ return memcmp(data.constData(), &tag, sizeof(QJsonDocument::BinaryFormatTag)) == 0;
+}
+QJsonDocument requireDocument(const QByteArray &data, const QString &what)
+{
+ if (isBinaryJson(data))
+ {
+ QJsonDocument doc = QJsonDocument::fromBinaryData(data);
+ if (doc.isNull())
+ {
+ throw JsonException(what + ": Invalid JSON (binary JSON detected)");
+ }
+ return doc;
+ }
+ else
+ {
+ QJsonParseError error;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &error);
+ if (error.error != QJsonParseError::NoError)
+ {
+ throw JsonException(what + ": Error parsing JSON: " + error.errorString());
+ }
+ return doc;
+ }
+}
+QJsonDocument requireDocument(const QString &filename, const QString &what)
+{
+ return requireDocument(FS::read(filename), what);
+}
+QJsonObject requireObject(const QJsonDocument &doc, const QString &what)
+{
+ if (!doc.isObject())
+ {
+ throw JsonException(what + " is not an object");
+ }
+ return doc.object();
+}
+QJsonArray requireArray(const QJsonDocument &doc, const QString &what)
+{
+ if (!doc.isArray())
+ {
+ throw JsonException(what + " is not an array");
+ }
+ return doc.array();
+}
+
+void writeString(QJsonObject &to, const QString &key, const QString &value)
+{
+ if (!value.isEmpty())
+ {
+ to.insert(key, value);
+ }
+}
+
+void writeStringList(QJsonObject &to, const QString &key, const QStringList &values)
+{
+ if (!values.isEmpty())
+ {
+ QJsonArray array;
+ for(auto value: values)
+ {
+ array.append(value);
+ }
+ to.insert(key, array);
+ }
+}
+
+template<>
+QJsonValue toJson<QUrl>(const QUrl &url)
+{
+ return QJsonValue(url.toString(QUrl::FullyEncoded));
+}
+template<>
+QJsonValue toJson<QByteArray>(const QByteArray &data)
+{
+ return QJsonValue(QString::fromLatin1(data.toHex()));
+}
+template<>
+QJsonValue toJson<QDateTime>(const QDateTime &datetime)
+{
+ return QJsonValue(datetime.toString(Qt::ISODate));
+}
+template<>
+QJsonValue toJson<QDir>(const QDir &dir)
+{
+ return QDir::current().relativeFilePath(dir.absolutePath());
+}
+template<>
+QJsonValue toJson<QUuid>(const QUuid &uuid)
+{
+ return uuid.toString();
+}
+template<>
+QJsonValue toJson<QVariant>(const QVariant &variant)
+{
+ return QJsonValue::fromVariant(variant);
+}
+
+
+template<> QByteArray requireIsType<QByteArray>(const QJsonValue &value, const QString &what)
+{
+ const QString string = ensureIsType<QString>(value, what);
+ // ensure that the string can be safely cast to Latin1
+ if (string != QString::fromLatin1(string.toLatin1()))
+ {
+ throw JsonException(what + " is not encodable as Latin1");
+ }
+ return QByteArray::fromHex(string.toLatin1());
+}
+
+template<> QJsonArray requireIsType<QJsonArray>(const QJsonValue &value, const QString &what)
+{
+ if (!value.isArray())
+ {
+ throw JsonException(what + " is not an array");
+ }
+ return value.toArray();
+}
+
+
+template<> QString requireIsType<QString>(const QJsonValue &value, const QString &what)
+{
+ if (!value.isString())
+ {
+ throw JsonException(what + " is not a string");
+ }
+ return value.toString();
+}
+
+template<> bool requireIsType<bool>(const QJsonValue &value, const QString &what)
+{
+ if (!value.isBool())
+ {
+ throw JsonException(what + " is not a bool");
+ }
+ return value.toBool();
+}
+
+template<> double requireIsType<double>(const QJsonValue &value, const QString &what)
+{
+ if (!value.isDouble())
+ {
+ throw JsonException(what + " is not a double");
+ }
+ return value.toDouble();
+}
+
+template<> int requireIsType<int>(const QJsonValue &value, const QString &what)
+{
+ const double doubl = requireIsType<double>(value, what);
+ if (fmod(doubl, 1) != 0)
+ {
+ throw JsonException(what + " is not an integer");
+ }
+ return int(doubl);
+}
+
+template<> QDateTime requireIsType<QDateTime>(const QJsonValue &value, const QString &what)
+{
+ const QString string = requireIsType<QString>(value, what);
+ const QDateTime datetime = QDateTime::fromString(string, Qt::ISODate);
+ if (!datetime.isValid())
+ {
+ throw JsonException(what + " is not a ISO formatted date/time value");
+ }
+ return datetime;
+}
+
+template<> QUrl requireIsType<QUrl>(const QJsonValue &value, const QString &what)
+{
+ const QString string = ensureIsType<QString>(value, what);
+ if (string.isEmpty())
+ {
+ return QUrl();
+ }
+ const QUrl url = QUrl(string, QUrl::StrictMode);
+ if (!url.isValid())
+ {
+ throw JsonException(what + " is not a correctly formatted URL");
+ }
+ return url;
+}
+
+template<> QDir requireIsType<QDir>(const QJsonValue &value, const QString &what)
+{
+ const QString string = requireIsType<QString>(value, what);
+ // FIXME: does not handle invalid characters!
+ return QDir::current().absoluteFilePath(string);
+}
+
+template<> QUuid requireIsType<QUuid>(const QJsonValue &value, const QString &what)
+{
+ const QString string = requireIsType<QString>(value, what);
+ const QUuid uuid = QUuid(string);
+ if (uuid.toString() != string) // converts back => valid
+ {
+ throw JsonException(what + " is not a valid UUID");
+ }
+ return uuid;
+}
+
+template<> QJsonObject requireIsType<QJsonObject>(const QJsonValue &value, const QString &what)
+{
+ if (!value.isObject())
+ {
+ throw JsonException(what + " is not an object");
+ }
+ return value.toObject();
+}
+
+template<> QVariant requireIsType<QVariant>(const QJsonValue &value, const QString &what)
+{
+ if (value.isNull() || value.isUndefined())
+ {
+ throw JsonException(what + " is null or undefined");
+ }
+ return value.toVariant();
+}
+
+template<> QJsonValue requireIsType<QJsonValue>(const QJsonValue &value, const QString &what)
+{
+ if (value.isNull() || value.isUndefined())
+ {
+ throw JsonException(what + " is null or undefined");
+ }
+ return value;
+}
+
+}
diff --git a/api/logic/Json.h b/api/logic/Json.h
new file mode 100644
index 00000000..2cb60f0e
--- /dev/null
+++ b/api/logic/Json.h
@@ -0,0 +1,249 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QDateTime>
+#include <QUrl>
+#include <QDir>
+#include <QUuid>
+#include <QVariant>
+#include <memory>
+
+#include "Exception.h"
+
+namespace Json
+{
+class MULTIMC_LOGIC_EXPORT JsonException : public ::Exception
+{
+public:
+ JsonException(const QString &message) : Exception(message) {}
+};
+
+/// @throw FileSystemException
+void write(const QJsonDocument &doc, const QString &filename);
+/// @throw FileSystemException
+void write(const QJsonObject &object, const QString &filename);
+/// @throw FileSystemException
+void write(const QJsonArray &array, const QString &filename);
+
+QByteArray toBinary(const QJsonObject &obj);
+QByteArray toBinary(const QJsonArray &array);
+QByteArray toText(const QJsonObject &obj);
+QByteArray toText(const QJsonArray &array);
+
+/// @throw JsonException
+MULTIMC_LOGIC_EXPORT QJsonDocument requireDocument(const QByteArray &data, const QString &what = "Document");
+/// @throw JsonException
+MULTIMC_LOGIC_EXPORT QJsonDocument requireDocument(const QString &filename, const QString &what = "Document");
+/// @throw JsonException
+MULTIMC_LOGIC_EXPORT QJsonObject requireObject(const QJsonDocument &doc, const QString &what = "Document");
+/// @throw JsonException
+MULTIMC_LOGIC_EXPORT QJsonArray requireArray(const QJsonDocument &doc, const QString &what = "Document");
+
+/////////////////// WRITING ////////////////////
+
+void writeString(QJsonObject & to, const QString &key, const QString &value);
+void writeStringList(QJsonObject & to, const QString &key, const QStringList &values);
+
+template<typename T>
+QJsonValue toJson(const T &t)
+{
+ return QJsonValue(t);
+}
+template<>
+QJsonValue toJson<QUrl>(const QUrl &url);
+template<>
+QJsonValue toJson<QByteArray>(const QByteArray &data);
+template<>
+QJsonValue toJson<QDateTime>(const QDateTime &datetime);
+template<>
+QJsonValue toJson<QDir>(const QDir &dir);
+template<>
+QJsonValue toJson<QUuid>(const QUuid &uuid);
+template<>
+QJsonValue toJson<QVariant>(const QVariant &variant);
+
+template<typename T>
+QJsonArray toJsonArray(const QList<T> &container)
+{
+ QJsonArray array;
+ for (const T item : container)
+ {
+ array.append(toJson<T>(item));
+ }
+ return array;
+}
+
+////////////////// READING ////////////////////
+
+/// @throw JsonException
+template <typename T>
+T requireIsType(const QJsonValue &value, const QString &what = "Value");
+
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT double requireIsType<double>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT bool requireIsType<bool>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT int requireIsType<int>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QJsonObject requireIsType<QJsonObject>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QJsonArray requireIsType<QJsonArray>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QJsonValue requireIsType<QJsonValue>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QByteArray requireIsType<QByteArray>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QDateTime requireIsType<QDateTime>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QVariant requireIsType<QVariant>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QString requireIsType<QString>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QUuid requireIsType<QUuid>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QDir requireIsType<QDir>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> MULTIMC_LOGIC_EXPORT QUrl requireIsType<QUrl>(const QJsonValue &value, const QString &what);
+
+// the following functions are higher level functions, that make use of the above functions for
+// type conversion
+template <typename T>
+T ensureIsType(const QJsonValue &value, const T default_ = T(), const QString &what = "Value")
+{
+ if (value.isUndefined() || value.isNull())
+ {
+ return default_;
+ }
+ try
+ {
+ return requireIsType<T>(value, what);
+ }
+ catch (JsonException &)
+ {
+ return default_;
+ }
+}
+
+/// @throw JsonException
+template <typename T>
+T requireIsType(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__")
+{
+ const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
+ if (!parent.contains(key))
+ {
+ throw JsonException(localWhat + "s parent does not contain " + localWhat);
+ }
+ return requireIsType<T>(parent.value(key), localWhat);
+}
+
+template <typename T>
+T ensureIsType(const QJsonObject &parent, const QString &key, const T default_ = T(), const QString &what = "__placeholder__")
+{
+ const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
+ if (!parent.contains(key))
+ {
+ return default_;
+ }
+ return ensureIsType<T>(parent.value(key), default_, localWhat);
+}
+
+template <typename T>
+QVector<T> requireIsArrayOf(const QJsonDocument &doc)
+{
+ const QJsonArray array = requireArray(doc);
+ QVector<T> out;
+ for (const QJsonValue val : array)
+ {
+ out.append(requireIsType<T>(val, "Document"));
+ }
+ return out;
+}
+
+template <typename T>
+QVector<T> ensureIsArrayOf(const QJsonValue &value, const QString &what = "Value")
+{
+ const QJsonArray array = ensureIsType<QJsonArray>(value, QJsonArray(), what);
+ QVector<T> out;
+ for (const QJsonValue val : array)
+ {
+ out.append(requireIsType<T>(val, what));
+ }
+ return out;
+}
+
+template <typename T>
+QVector<T> ensureIsArrayOf(const QJsonValue &value, const QVector<T> default_, const QString &what = "Value")
+{
+ if (value.isUndefined())
+ {
+ return default_;
+ }
+ return ensureIsArrayOf<T>(value, what);
+}
+
+/// @throw JsonException
+template <typename T>
+QVector<T> requireIsArrayOf(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__")
+{
+ const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
+ if (!parent.contains(key))
+ {
+ throw JsonException(localWhat + "s parent does not contain " + localWhat);
+ }
+ return ensureIsArrayOf<T>(parent.value(key), localWhat);
+}
+
+template <typename T>
+QVector<T> ensureIsArrayOf(const QJsonObject &parent, const QString &key,
+ const QVector<T> &default_ = QVector<T>(), const QString &what = "__placeholder__")
+{
+ const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
+ if (!parent.contains(key))
+ {
+ return default_;
+ }
+ return ensureIsArrayOf<T>(parent.value(key), default_, localWhat);
+}
+
+// this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers
+#define JSON_HELPERFUNCTIONS(NAME, TYPE) \
+ inline TYPE require##NAME(const QJsonValue &value, const QString &what = "Value") \
+ { \
+ return requireIsType<TYPE>(value, what); \
+ } \
+ inline TYPE ensure##NAME(const QJsonValue &value, const TYPE default_ = TYPE(), const QString &what = "Value") \
+ { \
+ return ensureIsType<TYPE>(value, default_, what); \
+ } \
+ inline TYPE require##NAME(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__") \
+ { \
+ return requireIsType<TYPE>(parent, key, what); \
+ } \
+ inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const TYPE default_ = TYPE(), const QString &what = "__placeholder") \
+ { \
+ return ensureIsType<TYPE>(parent, key, default_, what); \
+ }
+
+JSON_HELPERFUNCTIONS(Array, QJsonArray)
+JSON_HELPERFUNCTIONS(Object, QJsonObject)
+JSON_HELPERFUNCTIONS(JsonValue, QJsonValue)
+JSON_HELPERFUNCTIONS(String, QString)
+JSON_HELPERFUNCTIONS(Boolean, bool)
+JSON_HELPERFUNCTIONS(Double, double)
+JSON_HELPERFUNCTIONS(Integer, int)
+JSON_HELPERFUNCTIONS(DateTime, QDateTime)
+JSON_HELPERFUNCTIONS(Url, QUrl)
+JSON_HELPERFUNCTIONS(ByteArray, QByteArray)
+JSON_HELPERFUNCTIONS(Dir, QDir)
+JSON_HELPERFUNCTIONS(Uuid, QUuid)
+JSON_HELPERFUNCTIONS(Variant, QVariant)
+
+#undef JSON_HELPERFUNCTIONS
+
+}
+using JSONValidationError = Json::JsonException;
diff --git a/api/logic/MMCStrings.cpp b/api/logic/MMCStrings.cpp
new file mode 100644
index 00000000..c50d596e
--- /dev/null
+++ b/api/logic/MMCStrings.cpp
@@ -0,0 +1,76 @@
+#include "MMCStrings.h"
+
+/// TAKEN FROM Qt, because it doesn't expose it intelligently
+static inline QChar getNextChar(const QString &s, int location)
+{
+ return (location < s.length()) ? s.at(location) : QChar();
+}
+
+/// TAKEN FROM Qt, because it doesn't expose it intelligently
+int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs)
+{
+ for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2)
+ {
+ // skip spaces, tabs and 0's
+ QChar c1 = getNextChar(s1, l1);
+ while (c1.isSpace())
+ c1 = getNextChar(s1, ++l1);
+ QChar c2 = getNextChar(s2, l2);
+ while (c2.isSpace())
+ c2 = getNextChar(s2, ++l2);
+
+ if (c1.isDigit() && c2.isDigit())
+ {
+ while (c1.digitValue() == 0)
+ c1 = getNextChar(s1, ++l1);
+ while (c2.digitValue() == 0)
+ c2 = getNextChar(s2, ++l2);
+
+ int lookAheadLocation1 = l1;
+ int lookAheadLocation2 = l2;
+ int currentReturnValue = 0;
+ // find the last digit, setting currentReturnValue as we go if it isn't equal
+ for (QChar lookAhead1 = c1, lookAhead2 = c2;
+ (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length());
+ lookAhead1 = getNextChar(s1, ++lookAheadLocation1),
+ lookAhead2 = getNextChar(s2, ++lookAheadLocation2))
+ {
+ bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit();
+ bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit();
+ if (!is1ADigit && !is2ADigit)
+ break;
+ if (!is1ADigit)
+ return -1;
+ if (!is2ADigit)
+ return 1;
+ if (currentReturnValue == 0)
+ {
+ if (lookAhead1 < lookAhead2)
+ {
+ currentReturnValue = -1;
+ }
+ else if (lookAhead1 > lookAhead2)
+ {
+ currentReturnValue = 1;
+ }
+ }
+ }
+ if (currentReturnValue != 0)
+ return currentReturnValue;
+ }
+ if (cs == Qt::CaseInsensitive)
+ {
+ if (!c1.isLower())
+ c1 = c1.toLower();
+ if (!c2.isLower())
+ c2 = c2.toLower();
+ }
+ int r = QString::localeAwareCompare(c1, c2);
+ if (r < 0)
+ return -1;
+ if (r > 0)
+ return 1;
+ }
+ // The two strings are the same (02 == 2) so fall back to the normal sort
+ return QString::compare(s1, s2, cs);
+}
diff --git a/api/logic/MMCStrings.h b/api/logic/MMCStrings.h
new file mode 100644
index 00000000..5606b909
--- /dev/null
+++ b/api/logic/MMCStrings.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <QString>
+
+#include "multimc_logic_export.h"
+
+namespace Strings
+{
+ int MULTIMC_LOGIC_EXPORT naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs);
+}
diff --git a/api/logic/MMCZip.cpp b/api/logic/MMCZip.cpp
new file mode 100644
index 00000000..0f35bc70
--- /dev/null
+++ b/api/logic/MMCZip.cpp
@@ -0,0 +1,491 @@
+/*
+Copyright (C) 2010 Roberto Pompermaier
+Copyright (C) 2005-2014 Sergey A. Tachenov
+
+Parts of this file were part of QuaZIP.
+
+QuaZIP is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 2.1 of the License, or
+(at your option) any later version.
+
+QuaZIP is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with QuaZIP. If not, see <http://www.gnu.org/licenses/>.
+
+See COPYING file for the full LGPL text.
+
+Original ZIP package is copyrighted by Gilles Vollant and contributors,
+see quazip/(un)MMCZip.h files for details. Basically it's the zlib license.
+*/
+
+#include <quazip.h>
+#include <JlCompress.h>
+#include <quazipdir.h>
+#include "MMCZip.h"
+#include "FileSystem.h"
+
+#include <QDebug>
+
+bool copyData(QIODevice &inFile, QIODevice &outFile)
+{
+ while (!inFile.atEnd())
+ {
+ char buf[4096];
+ qint64 readLen = inFile.read(buf, 4096);
+ if (readLen <= 0)
+ return false;
+ if (outFile.write(buf, readLen) != readLen)
+ return false;
+ }
+ return true;
+}
+
+QStringList MMCZip::extractDir(QString fileCompressed, QString dir)
+{
+ return JlCompress::extractDir(fileCompressed, dir);
+}
+
+bool compressFile(QuaZip *zip, QString fileName, QString fileDest)
+{
+ if (!zip)
+ {
+ return false;
+ }
+ if (zip->getMode() != QuaZip::mdCreate && zip->getMode() != QuaZip::mdAppend &&
+ zip->getMode() != QuaZip::mdAdd)
+ {
+ return false;
+ }
+
+ QFile inFile;
+ inFile.setFileName(fileName);
+ if (!inFile.open(QIODevice::ReadOnly))
+ {
+ return false;
+ }
+
+ QuaZipFile outFile(zip);
+ if (!outFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileDest, inFile.fileName())))
+ {
+ return false;
+ }
+
+ if (!copyData(inFile, outFile) || outFile.getZipError() != UNZ_OK)
+ {
+ return false;
+ }
+
+ outFile.close();
+ if (outFile.getZipError() != UNZ_OK)
+ {
+ return false;
+ }
+ inFile.close();
+
+ return true;
+}
+
+bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSet<QString>& added, QString prefix, const SeparatorPrefixTree <'/'> * blacklist)
+{
+ if (!zip) return false;
+ if (zip->getMode()!=QuaZip::mdCreate && zip->getMode()!=QuaZip::mdAppend && zip->getMode()!=QuaZip::mdAdd)
+ {
+ return false;
+ }
+
+ QDir directory(dir);
+ if (!directory.exists())
+ {
+ return false;
+ }
+
+ QDir origDirectory(origDir);
+ if (dir != origDir)
+ {
+ QString internalDirName = origDirectory.relativeFilePath(dir);
+ if(!blacklist || !blacklist->covers(internalDirName))
+ {
+ QuaZipFile dirZipFile(zip);
+ auto dirPrefix = FS::PathCombine(prefix, origDirectory.relativeFilePath(dir)) + "/";
+ if (!dirZipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(dirPrefix, dir), 0, 0, 0))
+ {
+ return false;
+ }
+ dirZipFile.close();
+ }
+ }
+
+ QFileInfoList files = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden);
+ for (auto file: files)
+ {
+ if(!file.isDir())
+ {
+ continue;
+ }
+ if(!compressSubDir(zip,file.absoluteFilePath(),origDir, added, prefix, blacklist))
+ {
+ return false;
+ }
+ }
+
+ files = directory.entryInfoList(QDir::Files);
+ for (auto file: files)
+ {
+ if(!file.isFile())
+ {
+ continue;
+ }
+
+ if(file.absoluteFilePath()==zip->getZipName())
+ {
+ continue;
+ }
+
+ QString filename = origDirectory.relativeFilePath(file.absoluteFilePath());
+ if(blacklist && blacklist->covers(filename))
+ {
+ continue;
+ }
+ if(prefix.size())
+ {
+ filename = FS::PathCombine(prefix, filename);
+ }
+ added.insert(filename);
+ if (!compressFile(zip,file.absoluteFilePath(),filename))
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained,
+ std::function<bool(QString)> filter)
+{
+ QuaZip modZip(from.filePath());
+ modZip.open(QuaZip::mdUnzip);
+
+ QuaZipFile fileInsideMod(&modZip);
+ QuaZipFile zipOutFile(into);
+ for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile())
+ {
+ QString filename = modZip.getCurrentFileName();
+ if (!filter(filename))
+ {
+ qDebug() << "Skipping file " << filename << " from "
+ << from.fileName() << " - filtered";
+ continue;
+ }
+ if (contained.contains(filename))
+ {
+ qDebug() << "Skipping already contained file " << filename << " from "
+ << from.fileName();
+ continue;
+ }
+ contained.insert(filename);
+
+ if (!fileInsideMod.open(QIODevice::ReadOnly))
+ {
+ qCritical() << "Failed to open " << filename << " from " << from.fileName();
+ return false;
+ }
+
+ QuaZipNewInfo info_out(fileInsideMod.getActualFileName());
+
+ if (!zipOutFile.open(QIODevice::WriteOnly, info_out))
+ {
+ qCritical() << "Failed to open " << filename << " in the jar";
+ fileInsideMod.close();
+ return false;
+ }
+ if (!copyData(fileInsideMod, zipOutFile))
+ {
+ zipOutFile.close();
+ fileInsideMod.close();
+ qCritical() << "Failed to copy data of " << filename << " into the jar";
+ return false;
+ }
+ zipOutFile.close();
+ fileInsideMod.close();
+ }
+ return true;
+}
+
+bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods)
+{
+ QuaZip zipOut(targetJarPath);
+ if (!zipOut.open(QuaZip::mdCreate))
+ {
+ QFile::remove(targetJarPath);
+ qCritical() << "Failed to open the minecraft.jar for modding";
+ return false;
+ }
+ // Files already added to the jar.
+ // These files will be skipped.
+ QSet<QString> addedFiles;
+
+ // Modify the jar
+ QListIterator<Mod> i(mods);
+ i.toBack();
+ while (i.hasPrevious())
+ {
+ const Mod &mod = i.previous();
+ // do not merge disabled mods.
+ if (!mod.enabled())
+ continue;
+ if (mod.type() == Mod::MOD_ZIPFILE)
+ {
+ if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles, noFilter))
+ {
+ zipOut.close();
+ QFile::remove(targetJarPath);
+ qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar.";
+ return false;
+ }
+ }
+ else if (mod.type() == Mod::MOD_SINGLEFILE)
+ {
+ auto filename = mod.filename();
+ if (!compressFile(&zipOut, filename.absoluteFilePath(),
+ filename.fileName()))
+ {
+ zipOut.close();
+ QFile::remove(targetJarPath);
+ qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar.";
+ return false;
+ }
+ addedFiles.insert(filename.fileName());
+ }
+ else if (mod.type() == Mod::MOD_FOLDER)
+ {
+ auto filename = mod.filename();
+ QString what_to_zip = filename.absoluteFilePath();
+ QDir dir(what_to_zip);
+ dir.cdUp();
+ QString parent_dir = dir.absolutePath();
+ if (!compressSubDir(&zipOut, what_to_zip, parent_dir, addedFiles))
+ {
+ zipOut.close();
+ QFile::remove(targetJarPath);
+ qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar.";
+ return false;
+ }
+ qDebug() << "Adding folder " << filename.fileName() << " from "
+ << filename.absoluteFilePath();
+ }
+ }
+
+ if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, metaInfFilter))
+ {
+ zipOut.close();
+ QFile::remove(targetJarPath);
+ qCritical() << "Failed to insert minecraft.jar contents.";
+ return false;
+ }
+
+ // Recompress the jar
+ zipOut.close();
+ if (zipOut.getZipError() != 0)
+ {
+ QFile::remove(targetJarPath);
+ qCritical() << "Failed to finalize minecraft.jar!";
+ return false;
+ }
+ return true;
+}
+
+bool MMCZip::noFilter(QString)
+{
+ return true;
+}
+
+bool MMCZip::metaInfFilter(QString key)
+{
+ if(key.contains("META-INF"))
+ {
+ return false;
+ }
+ return true;
+}
+
+bool MMCZip::compressDir(QString zipFile, QString dir, QString prefix, const SeparatorPrefixTree <'/'> * blacklist)
+{
+ QuaZip zip(zipFile);
+ QDir().mkpath(QFileInfo(zipFile).absolutePath());
+ if(!zip.open(QuaZip::mdCreate))
+ {
+ QFile::remove(zipFile);
+ return false;
+ }
+
+ QSet<QString> added;
+ if (!compressSubDir(&zip, dir, dir, added, prefix, blacklist))
+ {
+ QFile::remove(zipFile);
+ return false;
+ }
+ zip.close();
+ if(zip.getZipError()!=0)
+ {
+ QFile::remove(zipFile);
+ return false;
+ }
+ return true;
+}
+
+QString MMCZip::findFileInZip(QuaZip * zip, const QString & what, const QString &root)
+{
+ QuaZipDir rootDir(zip, root);
+ for(auto fileName: rootDir.entryList(QDir::Files))
+ {
+ if(fileName == what)
+ return root;
+ }
+ for(auto fileName: rootDir.entryList(QDir::Dirs))
+ {
+ QString result = findFileInZip(zip, what, root + fileName);
+ if(!result.isEmpty())
+ {
+ return result;
+ }
+ }
+ return QString();
+}
+
+bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root)
+{
+ QuaZipDir rootDir(zip, root);
+ for(auto fileName: rootDir.entryList(QDir::Files))
+ {
+ if(fileName == what)
+ {
+ result.append(root);
+ return true;
+ }
+ }
+ for(auto fileName: rootDir.entryList(QDir::Dirs))
+ {
+ findFilesInZip(zip, what, result, root + fileName);
+ }
+ return !result.isEmpty();
+}
+
+bool removeFile(QStringList listFile)
+{
+ bool ret = true;
+ for (int i = 0; i < listFile.count(); i++)
+ {
+ ret &= QFile::remove(listFile.at(i));
+ }
+ return ret;
+}
+
+bool MMCZip::extractFile(QuaZip *zip, const QString &fileName, const QString &fileDest)
+{
+ if(!zip)
+ return false;
+
+ if (zip->getMode() != QuaZip::mdUnzip)
+ return false;
+
+ if (!fileName.isEmpty())
+ zip->setCurrentFile(fileName);
+
+ QuaZipFile inFile(zip);
+ if (!inFile.open(QIODevice::ReadOnly) || inFile.getZipError() != UNZ_OK)
+ return false;
+
+ // Controllo esistenza cartella file risultato
+ QDir curDir;
+ if (fileDest.endsWith('/'))
+ {
+ if (!curDir.mkpath(fileDest))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!curDir.mkpath(QFileInfo(fileDest).absolutePath()))
+ {
+ return false;
+ }
+ }
+
+ QuaZipFileInfo64 info;
+ if (!zip->getCurrentFileInfo(&info))
+ return false;
+
+ QFile::Permissions srcPerm = info.getPermissions();
+ if (fileDest.endsWith('/') && QFileInfo(fileDest).isDir())
+ {
+ if (srcPerm != 0)
+ {
+ QFile(fileDest).setPermissions(srcPerm);
+ }
+ return true;
+ }
+
+ QFile outFile;
+ outFile.setFileName(fileDest);
+ if (!outFile.open(QIODevice::WriteOnly))
+ return false;
+
+ if (!copyData(inFile, outFile) || inFile.getZipError() != UNZ_OK)
+ {
+ outFile.close();
+ removeFile(QStringList(fileDest));
+ return false;
+ }
+ outFile.close();
+
+ inFile.close();
+ if (inFile.getZipError() != UNZ_OK)
+ {
+ removeFile(QStringList(fileDest));
+ return false;
+ }
+
+ if (srcPerm != 0)
+ {
+ outFile.setPermissions(srcPerm);
+ }
+ return true;
+}
+
+QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
+{
+ QDir directory(target);
+ QStringList extracted;
+ if (!zip->goToFirstFile())
+ {
+ return QStringList();
+ }
+ do
+ {
+ QString name = zip->getCurrentFileName();
+ if(!name.startsWith(subdir))
+ {
+ continue;
+ }
+ name.remove(0, subdir.size());
+ QString absFilePath = directory.absoluteFilePath(name);
+ if(name.isEmpty())
+ {
+ absFilePath += "/";
+ }
+ if (!extractFile(zip, "", absFilePath))
+ {
+ removeFile(extracted);
+ return QStringList();
+ }
+ extracted.append(absFilePath);
+ } while (zip->goToNextFile());
+ return extracted;
+}
diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h
new file mode 100644
index 00000000..f350e668
--- /dev/null
+++ b/api/logic/MMCZip.h
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <QString>
+#include <QFileInfo>
+#include <QSet>
+#include "minecraft/Mod.h"
+#include "SeparatorPrefixTree.h"
+#include <functional>
+
+#include "multimc_logic_export.h"
+
+class QuaZip;
+
+namespace MMCZip
+{
+ /**
+ * Compress a subdirectory.
+ * \param parentZip Opened zip containing the parent directory.
+ * \param dir The full path to the directory to pack.
+ * \param parentDir The full path to the directory corresponding to the root of the ZIP.
+ * \param recursive Whether to pack sub-directories as well or only files.
+ * \return true if success, false otherwise.
+ */
+ bool MULTIMC_LOGIC_EXPORT compressSubDir(QuaZip *zip, QString dir, QString origDir, QSet<QString> &added,
+ QString prefix = QString(), const SeparatorPrefixTree <'/'> * blacklist = nullptr);
+
+ /**
+ * Compress a whole directory.
+ * \param fileCompressed The name of the archive.
+ * \param dir The directory to compress.
+ * \param recursive Whether to pack the subdirectories as well, or just regular files.
+ * \return true if success, false otherwise.
+ */
+ bool MULTIMC_LOGIC_EXPORT compressDir(QString zipFile, QString dir, QString prefix = QString(), const SeparatorPrefixTree <'/'> * blacklist = nullptr);
+
+ /// filter function for @mergeZipFiles - passthrough
+ bool MULTIMC_LOGIC_EXPORT noFilter(QString key);
+
+ /// filter function for @mergeZipFiles - ignores METAINF
+ bool MULTIMC_LOGIC_EXPORT metaInfFilter(QString key);
+
+ /**
+ * Merge two zip files, using a filter function
+ */
+ bool MULTIMC_LOGIC_EXPORT mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, std::function<bool(QString)> filter);
+
+ /**
+ * take a source jar, add mods to it, resulting in target jar
+ */
+ bool MULTIMC_LOGIC_EXPORT createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods);
+
+ /**
+ * Extract a whole archive.
+ *
+ * \param fileCompressed The name of the archive.
+ * \param dir The directory to extract to, the current directory if
+ * left empty.
+ * \return The list of the full paths of the files extracted, empty on failure.
+ */
+ QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir = QString());
+
+ /**
+ * Find a single file in archive by file name (not path)
+ *
+ * \return the path prefix where the file is
+ */
+ QString MULTIMC_LOGIC_EXPORT findFileInZip(QuaZip * zip, const QString & what, const QString &root = QString());
+
+ /**
+ * Find a multiple files of the same name in archive by file name
+ * If a file is found in a path, no deeper paths are searched
+ *
+ * \return true if anything was found
+ */
+ bool MULTIMC_LOGIC_EXPORT findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString());
+
+ /**
+ * Extract a single file to a destination
+ *
+ * \return true if it succeeds
+ */
+ bool MULTIMC_LOGIC_EXPORT extractFile(QuaZip *zip, const QString &fileName, const QString &fileDest);
+
+ /**
+ * Extract a subdirectory from an archive
+ */
+ QStringList MULTIMC_LOGIC_EXPORT extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
+}
diff --git a/api/logic/NullInstance.h b/api/logic/NullInstance.h
new file mode 100644
index 00000000..fbb2d985
--- /dev/null
+++ b/api/logic/NullInstance.h
@@ -0,0 +1,90 @@
+#pragma once
+#include "BaseInstance.h"
+
+class NullInstance: public BaseInstance
+{
+public:
+ NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir)
+ :BaseInstance(globalSettings, settings, rootDir)
+ {
+ setFlag(BaseInstance::VersionBrokenFlag);
+ }
+ virtual ~NullInstance() {};
+ virtual bool setIntendedVersionId(QString) override
+ {
+ return false;
+ }
+ virtual void cleanupAfterRun() override
+ {
+ }
+ virtual QString currentVersionId() const override
+ {
+ return "Null";
+ };
+ virtual QString intendedVersionId() const override
+ {
+ return "Null";
+ };
+ virtual void init() override
+ {
+ };
+ virtual QString getStatusbarDescription() override
+ {
+ return tr("Unknown instance type");
+ };
+ virtual bool shouldUpdate() const override
+ {
+ return false;
+ };
+ virtual QSet< QString > traits() override
+ {
+ return {};
+ };
+ virtual QString instanceConfigFolder() const override
+ {
+ return instanceRoot();
+ };
+ virtual std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr) override
+ {
+ return nullptr;
+ }
+ virtual std::shared_ptr< Task > createUpdateTask() override
+ {
+ return nullptr;
+ }
+ virtual std::shared_ptr<Task> createJarModdingTask() override
+ {
+ return nullptr;
+ }
+ virtual void setShouldUpdate(bool) override
+ {
+ };
+ virtual std::shared_ptr< BaseVersionList > versionList() const override
+ {
+ return nullptr;
+ };
+ virtual QProcessEnvironment createEnvironment() override
+ {
+ return QProcessEnvironment();
+ }
+ virtual QMap<QString, QString> getVariables() const override
+ {
+ return QMap<QString, QString>();
+ }
+ virtual IPathMatcher::Ptr getLogFileMatcher() override
+ {
+ return nullptr;
+ }
+ virtual QString getLogFileRoot() override
+ {
+ return instanceRoot();
+ }
+ virtual QString typeName() const override
+ {
+ return "Null";
+ }
+ bool canExport() const override
+ {
+ return false;
+ }
+};
diff --git a/api/logic/QObjectPtr.h b/api/logic/QObjectPtr.h
new file mode 100644
index 00000000..b81b3234
--- /dev/null
+++ b/api/logic/QObjectPtr.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include <memory>
+#include <QObject>
+
+namespace details
+{
+struct DeleteQObjectLater
+{
+ void operator()(QObject *obj) const
+ {
+ obj->deleteLater();
+ }
+};
+}
+/**
+ * A unique pointer class with unique pointer semantics intended for derivates of QObject
+ * Calls deleteLater() instead of destroying the contained object immediately
+ */
+template<typename T> using unique_qobject_ptr = std::unique_ptr<T, details::DeleteQObjectLater>;
+
+/**
+ * A shared pointer class with shared pointer semantics intended for derivates of QObject
+ * Calls deleteLater() instead of destroying the contained object immediately
+ */
+template <typename T>
+class shared_qobject_ptr
+{
+public:
+ shared_qobject_ptr(){}
+ shared_qobject_ptr(T * wrap)
+ {
+ reset(wrap);
+ }
+ shared_qobject_ptr(const shared_qobject_ptr<T>& other)
+ {
+ m_ptr = other.m_ptr;
+ }
+ template<typename Derived>
+ shared_qobject_ptr(const shared_qobject_ptr<Derived> &other)
+ {
+ m_ptr = other.unwrap();
+ }
+
+public:
+ void reset(T * wrap)
+ {
+ using namespace std::placeholders;
+ m_ptr.reset(wrap, std::bind(&QObject::deleteLater, _1));
+ }
+ void reset()
+ {
+ m_ptr.reset();
+ }
+ T * get() const
+ {
+ return m_ptr.get();
+ }
+ T * operator->() const
+ {
+ return m_ptr.get();
+ }
+ T & operator*() const
+ {
+ return *m_ptr.get();
+ }
+ operator bool() const
+ {
+ return m_ptr.get() != nullptr;
+ }
+ const std::shared_ptr <T> unwrap() const
+ {
+ return m_ptr;
+ }
+
+private:
+ std::shared_ptr <T> m_ptr;
+};
diff --git a/api/logic/RWStorage.h b/api/logic/RWStorage.h
new file mode 100644
index 00000000..b1598ca4
--- /dev/null
+++ b/api/logic/RWStorage.h
@@ -0,0 +1,60 @@
+#pragma once
+template <typename K, typename V>
+class RWStorage
+{
+public:
+ void add(K key, V value)
+ {
+ QWriteLocker l(&lock);
+ cache[key] = value;
+ stale_entries.remove(key);
+ }
+ V get(K key)
+ {
+ QReadLocker l(&lock);
+ if(cache.contains(key))
+ {
+ return cache[key];
+ }
+ else return V();
+ }
+ bool get(K key, V& value)
+ {
+ QReadLocker l(&lock);
+ if(cache.contains(key))
+ {
+ value = cache[key];
+ return true;
+ }
+ else return false;
+ }
+ bool has(K key)
+ {
+ QReadLocker l(&lock);
+ return cache.contains(key);
+ }
+ bool stale(K key)
+ {
+ QReadLocker l(&lock);
+ if(!cache.contains(key))
+ return true;
+ return stale_entries.contains(key);
+ }
+ void setStale(K key)
+ {
+ QReadLocker l(&lock);
+ if(cache.contains(key))
+ {
+ stale_entries.insert(key);
+ }
+ }
+ void clear()
+ {
+ QWriteLocker l(&lock);
+ cache.clear();
+ }
+private:
+ QReadWriteLock lock;
+ QMap<K, V> cache;
+ QSet<K> stale_entries;
+}; \ No newline at end of file
diff --git a/api/logic/RecursiveFileSystemWatcher.cpp b/api/logic/RecursiveFileSystemWatcher.cpp
new file mode 100644
index 00000000..59c3f0f0
--- /dev/null
+++ b/api/logic/RecursiveFileSystemWatcher.cpp
@@ -0,0 +1,111 @@
+#include "RecursiveFileSystemWatcher.h"
+
+#include <QRegularExpression>
+#include <QDebug>
+
+RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject *parent)
+ : QObject(parent), m_watcher(new QFileSystemWatcher(this))
+{
+ connect(m_watcher, &QFileSystemWatcher::fileChanged, this,
+ &RecursiveFileSystemWatcher::fileChange);
+ connect(m_watcher, &QFileSystemWatcher::directoryChanged, this,
+ &RecursiveFileSystemWatcher::directoryChange);
+}
+
+void RecursiveFileSystemWatcher::setRootDir(const QDir &root)
+{
+ bool wasEnabled = m_isEnabled;
+ disable();
+ m_root = root;
+ setFiles(scanRecursive(m_root));
+ if (wasEnabled)
+ {
+ enable();
+ }
+}
+void RecursiveFileSystemWatcher::setWatchFiles(const bool watchFiles)
+{
+ bool wasEnabled = m_isEnabled;
+ disable();
+ m_watchFiles = watchFiles;
+ if (wasEnabled)
+ {
+ enable();
+ }
+}
+
+void RecursiveFileSystemWatcher::enable()
+{
+ if (m_isEnabled)
+ {
+ return;
+ }
+ Q_ASSERT(m_root != QDir::root());
+ addFilesToWatcherRecursive(m_root);
+ m_isEnabled = true;
+}
+void RecursiveFileSystemWatcher::disable()
+{
+ if (!m_isEnabled)
+ {
+ return;
+ }
+ m_isEnabled = false;
+ m_watcher->removePaths(m_watcher->files());
+ m_watcher->removePaths(m_watcher->directories());
+}
+
+void RecursiveFileSystemWatcher::setFiles(const QStringList &files)
+{
+ if (files != m_files)
+ {
+ m_files = files;
+ emit filesChanged();
+ }
+}
+
+void RecursiveFileSystemWatcher::addFilesToWatcherRecursive(const QDir &dir)
+{
+ m_watcher->addPath(dir.absolutePath());
+ for (const QString &directory : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
+ {
+ addFilesToWatcherRecursive(dir.absoluteFilePath(directory));
+ }
+ if (m_watchFiles)
+ {
+ for (const QFileInfo &info : dir.entryInfoList(QDir::Files))
+ {
+ m_watcher->addPath(info.absoluteFilePath());
+ }
+ }
+}
+QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir &directory)
+{
+ QStringList ret;
+ if(!m_matcher)
+ {
+ return {};
+ }
+ for (const QString &dir : directory.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden))
+ {
+ ret.append(scanRecursive(directory.absoluteFilePath(dir)));
+ }
+ for (const QString &file : directory.entryList(QDir::Files | QDir::Hidden))
+ {
+ auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file));
+ if (m_matcher->matches(relPath))
+ {
+ ret.append(relPath);
+ }
+ }
+ return ret;
+}
+
+void RecursiveFileSystemWatcher::fileChange(const QString &path)
+{
+ emit fileChanged(path);
+}
+void RecursiveFileSystemWatcher::directoryChange(const QString &path)
+{
+ setFiles(scanRecursive(m_root));
+}
diff --git a/api/logic/RecursiveFileSystemWatcher.h b/api/logic/RecursiveFileSystemWatcher.h
new file mode 100644
index 00000000..07bce0b9
--- /dev/null
+++ b/api/logic/RecursiveFileSystemWatcher.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <QFileSystemWatcher>
+#include <QDir>
+#include "pathmatcher/IPathMatcher.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT RecursiveFileSystemWatcher : public QObject
+{
+ Q_OBJECT
+public:
+ RecursiveFileSystemWatcher(QObject *parent);
+
+ void setRootDir(const QDir &root);
+ QDir rootDir() const
+ {
+ return m_root;
+ }
+
+ // WARNING: setting this to true may be bad for performance
+ void setWatchFiles(const bool watchFiles);
+ bool watchFiles() const
+ {
+ return m_watchFiles;
+ }
+
+ void setMatcher(IPathMatcher::Ptr matcher)
+ {
+ m_matcher = matcher;
+ }
+
+ QStringList files() const
+ {
+ return m_files;
+ }
+
+signals:
+ void filesChanged();
+ void fileChanged(const QString &path);
+
+public slots:
+ void enable();
+ void disable();
+
+private:
+ QDir m_root;
+ bool m_watchFiles = false;
+ bool m_isEnabled = false;
+ IPathMatcher::Ptr m_matcher;
+
+ QFileSystemWatcher *m_watcher;
+
+ QStringList m_files;
+ void setFiles(const QStringList &files);
+
+ void addFilesToWatcherRecursive(const QDir &dir);
+ QStringList scanRecursive(const QDir &dir);
+
+private slots:
+ void fileChange(const QString &path);
+ void directoryChange(const QString &path);
+};
diff --git a/api/logic/SeparatorPrefixTree.h b/api/logic/SeparatorPrefixTree.h
new file mode 100644
index 00000000..fd149af0
--- /dev/null
+++ b/api/logic/SeparatorPrefixTree.h
@@ -0,0 +1,298 @@
+#pragma once
+#include <QString>
+#include <QMap>
+#include <QStringList>
+
+template <char Tseparator>
+class SeparatorPrefixTree
+{
+public:
+ SeparatorPrefixTree(QStringList paths)
+ {
+ insert(paths);
+ }
+
+ SeparatorPrefixTree(bool contained = false)
+ {
+ m_contained = contained;
+ }
+
+ void insert(QStringList paths)
+ {
+ for(auto &path: paths)
+ {
+ insert(path);
+ }
+ }
+
+ /// insert an exact path into the tree
+ SeparatorPrefixTree & insert(QString path)
+ {
+ auto sepIndex = path.indexOf(Tseparator);
+ if(sepIndex == -1)
+ {
+ children[path] = SeparatorPrefixTree(true);
+ return children[path];
+ }
+ else
+ {
+ auto prefix = path.left(sepIndex);
+ if(!children.contains(prefix))
+ {
+ children[prefix] = SeparatorPrefixTree(false);
+ }
+ return children[prefix].insert(path.mid(sepIndex + 1));
+ }
+ }
+
+ /// is the path fully contained in the tree?
+ bool contains(QString path) const
+ {
+ auto node = find(path);
+ return node != nullptr;
+ }
+
+ /// does the tree cover a path? That means the prefix of the path is contained in the tree
+ bool covers(QString path) const
+ {
+ // if we found some valid node, it's good enough. the tree covers the path
+ if(m_contained)
+ {
+ return true;
+ }
+ auto sepIndex = path.indexOf(Tseparator);
+ if(sepIndex == -1)
+ {
+ auto found = children.find(path);
+ if(found == children.end())
+ {
+ return false;
+ }
+ return (*found).covers(QString());
+ }
+ else
+ {
+ auto prefix = path.left(sepIndex);
+ auto found = children.find(prefix);
+ if(found == children.end())
+ {
+ return false;
+ }
+ return (*found).covers(path.mid(sepIndex + 1));
+ }
+ }
+
+ /// return the contained path that covers the path specified
+ QString cover(QString path) const
+ {
+ // if we found some valid node, it's good enough. the tree covers the path
+ if(m_contained)
+ {
+ return QString("");
+ }
+ auto sepIndex = path.indexOf(Tseparator);
+ if(sepIndex == -1)
+ {
+ auto found = children.find(path);
+ if(found == children.end())
+ {
+ return QString();
+ }
+ auto nested = (*found).cover(QString());
+ if(nested.isNull())
+ {
+ return nested;
+ }
+ if(nested.isEmpty())
+ return path;
+ return path + Tseparator + nested;
+ }
+ else
+ {
+ auto prefix = path.left(sepIndex);
+ auto found = children.find(prefix);
+ if(found == children.end())
+ {
+ return QString();
+ }
+ auto nested = (*found).cover(path.mid(sepIndex + 1));
+ if(nested.isNull())
+ {
+ return nested;
+ }
+ if(nested.isEmpty())
+ return prefix;
+ return prefix + Tseparator + nested;
+ }
+ }
+
+ /// Does the path-specified node exist in the tree? It does not have to be contained.
+ bool exists(QString path) const
+ {
+ auto sepIndex = path.indexOf(Tseparator);
+ if(sepIndex == -1)
+ {
+ auto found = children.find(path);
+ if(found == children.end())
+ {
+ return false;
+ }
+ return true;
+ }
+ else
+ {
+ auto prefix = path.left(sepIndex);
+ auto found = children.find(prefix);
+ if(found == children.end())
+ {
+ return false;
+ }
+ return (*found).exists(path.mid(sepIndex + 1));
+ }
+ }
+
+ /// find a node in the tree by name
+ const SeparatorPrefixTree * find(QString path) const
+ {
+ auto sepIndex = path.indexOf(Tseparator);
+ if(sepIndex == -1)
+ {
+ auto found = children.find(path);
+ if(found == children.end())
+ {
+ return nullptr;
+ }
+ return &(*found);
+ }
+ else
+ {
+ auto prefix = path.left(sepIndex);
+ auto found = children.find(prefix);
+ if(found == children.end())
+ {
+ return nullptr;
+ }
+ return (*found).find(path.mid(sepIndex + 1));
+ }
+ }
+
+ /// is this a leaf node?
+ bool leaf() const
+ {
+ return children.isEmpty();
+ }
+
+ /// is this node actually contained in the tree, or is it purely structural?
+ bool contained() const
+ {
+ return m_contained;
+ }
+
+ /// Remove a path from the tree
+ bool remove(QString path)
+ {
+ return removeInternal(path) != Failed;
+ }
+
+ /// Clear all children of this node tree node
+ void clear()
+ {
+ children.clear();
+ }
+
+ QStringList toStringList() const
+ {
+ QStringList collected;
+ // collecting these is more expensive.
+ auto iter = children.begin();
+ while(iter != children.end())
+ {
+ QStringList list = iter.value().toStringList();
+ for(int i = 0; i < list.size(); i++)
+ {
+ list[i] = iter.key() + Tseparator + list[i];
+ }
+ collected.append(list);
+ if((*iter).m_contained)
+ {
+ collected.append(iter.key());
+ }
+ iter++;
+ }
+ return collected;
+ }
+private:
+ enum Removal
+ {
+ Failed,
+ Succeeded,
+ HasChildren
+ };
+ Removal removeInternal(QString path = QString())
+ {
+ if(path.isEmpty())
+ {
+ if(!m_contained)
+ {
+ // remove all children - we are removing a prefix
+ clear();
+ return Succeeded;
+ }
+ m_contained = false;
+ if(children.size())
+ {
+ return HasChildren;
+ }
+ return Succeeded;
+ }
+ Removal remStatus = Failed;
+ QString childToRemove;
+ auto sepIndex = path.indexOf(Tseparator);
+ if(sepIndex == -1)
+ {
+ childToRemove = path;
+ auto found = children.find(childToRemove);
+ if(found == children.end())
+ {
+ return Failed;
+ }
+ remStatus = (*found).removeInternal();
+ }
+ else
+ {
+ childToRemove = path.left(sepIndex);
+ auto found = children.find(childToRemove);
+ if(found == children.end())
+ {
+ return Failed;
+ }
+ remStatus = (*found).removeInternal(path.mid(sepIndex + 1));
+ }
+ switch (remStatus)
+ {
+ case Failed:
+ case HasChildren:
+ {
+ return remStatus;
+ }
+ case Succeeded:
+ {
+ children.remove(childToRemove);
+ if(m_contained)
+ {
+ return HasChildren;
+ }
+ if(children.size())
+ {
+ return HasChildren;
+ }
+ return Succeeded;
+ }
+ }
+ return Failed;
+ }
+
+private:
+ QMap<QString,SeparatorPrefixTree<Tseparator>> children;
+ bool m_contained = false;
+};
diff --git a/api/logic/TypeMagic.h b/api/logic/TypeMagic.h
new file mode 100644
index 00000000..fa9d12a9
--- /dev/null
+++ b/api/logic/TypeMagic.h
@@ -0,0 +1,37 @@
+#pragma once
+
+namespace TypeMagic
+{
+/** "Cleans" the given type T by stripping references (&) and cv-qualifiers (const, volatile) from it
+ * const int => int
+ * QString & => QString
+ * const unsigned long long & => unsigned long long
+ *
+ * Usage:
+ * using Cleaned = Detail::CleanType<const int>;
+ * static_assert(std::is_same<Cleaned, int>, "Cleaned == int");
+ */
+// the order of remove_cv and remove_reference matters!
+template <typename T>
+using CleanType = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
+
+/// For functors (structs with operator()), including lambdas, which in **most** cases are functors
+/// "Calls" Function<Ret(*)(Arg)> or Function<Ret(C::*)(Arg)>
+template <typename T> struct Function : public Function<decltype(&T::operator())> {};
+/// For function pointers (&function), including static members (&Class::member)
+template <typename Ret, typename Arg> struct Function<Ret(*)(Arg)> : public Function<Ret(Arg)> {};
+/// Default specialization used by others.
+template <typename Ret, typename Arg> struct Function<Ret(Arg)>
+{
+ using ReturnType = Ret;
+ using Argument = Arg;
+};
+/// For member functions. Also used by the lambda overload if the lambda captures [this]
+template <class C, typename Ret, typename Arg> struct Function<Ret(C::*)(Arg)> : public Function<Ret(Arg)> {};
+template <class C, typename Ret, typename Arg> struct Function<Ret(C::*)(Arg) const> : public Function<Ret(Arg)> {};
+/// Overload for references
+template <typename F> struct Function<F&> : public Function<F> {};
+/// Overload for rvalues
+template <typename F> struct Function<F&&> : public Function<F> {};
+// for more info: https://functionalcpp.wordpress.com/2013/08/05/function-traits/
+}
diff --git a/api/logic/Version.cpp b/api/logic/Version.cpp
new file mode 100644
index 00000000..3c4727ad
--- /dev/null
+++ b/api/logic/Version.cpp
@@ -0,0 +1,140 @@
+#include "Version.h"
+
+#include <QStringList>
+#include <QUrl>
+#include <QRegularExpression>
+#include <QRegularExpressionMatch>
+
+Version::Version(const QString &str) : m_string(str)
+{
+ parse();
+}
+
+bool Version::operator<(const Version &other) const
+{
+ const int size = qMax(m_sections.size(), other.m_sections.size());
+ for (int i = 0; i < size; ++i)
+ {
+ const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i);
+ const Section sec2 =
+ (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i);
+ if (sec1 != sec2)
+ {
+ return sec1 < sec2;
+ }
+ }
+
+ return false;
+}
+bool Version::operator<=(const Version &other) const
+{
+ return *this < other || *this == other;
+}
+bool Version::operator>(const Version &other) const
+{
+ const int size = qMax(m_sections.size(), other.m_sections.size());
+ for (int i = 0; i < size; ++i)
+ {
+ const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i);
+ const Section sec2 =
+ (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i);
+ if (sec1 != sec2)
+ {
+ return sec1 > sec2;
+ }
+ }
+
+ return false;
+}
+bool Version::operator>=(const Version &other) const
+{
+ return *this > other || *this == other;
+}
+bool Version::operator==(const Version &other) const
+{
+ const int size = qMax(m_sections.size(), other.m_sections.size());
+ for (int i = 0; i < size; ++i)
+ {
+ const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i);
+ const Section sec2 =
+ (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i);
+ if (sec1 != sec2)
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+bool Version::operator!=(const Version &other) const
+{
+ return !operator==(other);
+}
+
+void Version::parse()
+{
+ m_sections.clear();
+
+ QStringList parts = m_string.split('.');
+
+ for (const auto part : parts)
+ {
+ m_sections.append(Section(part));
+ }
+}
+
+bool versionIsInInterval(const QString &version, const QString &interval)
+{
+ return versionIsInInterval(Version(version), interval);
+}
+bool versionIsInInterval(const Version &version, const QString &interval)
+{
+ if (interval.isEmpty() || version.toString() == interval)
+ {
+ return true;
+ }
+
+ // Interval notation is used
+ QRegularExpression exp(
+ "(?<start>[\\[\\]\\(\\)])(?<bottom>.*?)(,(?<top>.*?))?(?<end>[\\[\\]\\(\\)]),?");
+ QRegularExpressionMatch match = exp.match(interval);
+ if (match.hasMatch())
+ {
+ const QChar start = match.captured("start").at(0);
+ const QChar end = match.captured("end").at(0);
+ const QString bottom = match.captured("bottom");
+ const QString top = match.captured("top");
+
+ // check if in range (bottom)
+ if (!bottom.isEmpty())
+ {
+ const auto bottomVersion = Version(bottom);
+ if ((start == '[') && !(version >= bottomVersion))
+ {
+ return false;
+ }
+ else if ((start == '(') && !(version > bottomVersion))
+ {
+ return false;
+ }
+ }
+
+ // check if in range (top)
+ if (!top.isEmpty())
+ {
+ const auto topVersion = Version(top);
+ if ((end == ']') && !(version <= topVersion))
+ {
+ return false;
+ }
+ else if ((end == ')') && !(version < topVersion))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
diff --git a/api/logic/Version.h b/api/logic/Version.h
new file mode 100644
index 00000000..b5946ced
--- /dev/null
+++ b/api/logic/Version.h
@@ -0,0 +1,110 @@
+#pragma once
+
+#include <QString>
+#include <QList>
+
+#include "multimc_logic_export.h"
+
+class QUrl;
+
+struct MULTIMC_LOGIC_EXPORT Version
+{
+ Version(const QString &str);
+ Version() {}
+
+ bool operator<(const Version &other) const;
+ bool operator<=(const Version &other) const;
+ bool operator>(const Version &other) const;
+ bool operator>=(const Version &other) const;
+ bool operator==(const Version &other) const;
+ bool operator!=(const Version &other) const;
+
+ QString toString() const
+ {
+ return m_string;
+ }
+
+private:
+ QString m_string;
+ struct Section
+ {
+ explicit Section(const QString &fullString)
+ {
+ m_fullString = fullString;
+ int cutoff = m_fullString.size();
+ for(int i = 0; i < m_fullString.size(); i++)
+ {
+ if(!m_fullString[i].isDigit())
+ {
+ cutoff = i;
+ break;
+ }
+ }
+ auto numPart = m_fullString.leftRef(cutoff);
+ if(numPart.size())
+ {
+ numValid = true;
+ m_numPart = numPart.toInt();
+ }
+ auto stringPart = m_fullString.midRef(cutoff);
+ if(stringPart.size())
+ {
+ m_stringPart = stringPart.toString();
+ }
+ }
+ explicit Section() {}
+ bool numValid = false;
+ int m_numPart = 0;
+ QString m_stringPart;
+ QString m_fullString;
+
+ inline bool operator!=(const Section &other) const
+ {
+ if(numValid && other.numValid)
+ {
+ return m_numPart != other.m_numPart || m_stringPart != other.m_stringPart;
+ }
+ else
+ {
+ return m_fullString != other.m_fullString;
+ }
+ }
+ inline bool operator<(const Section &other) const
+ {
+ if(numValid && other.numValid)
+ {
+ if(m_numPart < other.m_numPart)
+ return true;
+ if(m_numPart == other.m_numPart && m_stringPart < other.m_stringPart)
+ return true;
+ return false;
+ }
+ else
+ {
+ return m_fullString < other.m_fullString;
+ }
+ }
+ inline bool operator>(const Section &other) const
+ {
+ if(numValid && other.numValid)
+ {
+ if(m_numPart > other.m_numPart)
+ return true;
+ if(m_numPart == other.m_numPart && m_stringPart > other.m_stringPart)
+ return true;
+ return false;
+ }
+ else
+ {
+ return m_fullString > other.m_fullString;
+ }
+ }
+ };
+ QList<Section> m_sections;
+
+ void parse();
+};
+
+MULTIMC_LOGIC_EXPORT bool versionIsInInterval(const QString &version, const QString &interval);
+MULTIMC_LOGIC_EXPORT bool versionIsInInterval(const Version &version, const QString &interval);
+
diff --git a/api/logic/java/JavaChecker.cpp b/api/logic/java/JavaChecker.cpp
new file mode 100644
index 00000000..54d552a9
--- /dev/null
+++ b/api/logic/java/JavaChecker.cpp
@@ -0,0 +1,159 @@
+#include "JavaChecker.h"
+#include <FileSystem.h>
+#include <Commandline.h>
+#include <QFile>
+#include <QProcess>
+#include <QMap>
+#include <QCoreApplication>
+#include <QDebug>
+
+JavaChecker::JavaChecker(QObject *parent) : QObject(parent)
+{
+}
+
+void JavaChecker::performCheck()
+{
+ QString checkerJar = FS::PathCombine(QCoreApplication::applicationDirPath(), "jars", "JavaCheck.jar");
+
+ QStringList args;
+
+ process.reset(new QProcess());
+ if(m_args.size())
+ {
+ auto extraArgs = Commandline::splitArgs(m_args);
+ args.append(extraArgs);
+ }
+ if(m_minMem != 0)
+ {
+ args << QString("-Xms%1m").arg(m_minMem);
+ }
+ if(m_maxMem != 0)
+ {
+ args << QString("-Xmx%1m").arg(m_maxMem);
+ }
+ if(m_permGen != 64)
+ {
+ args << QString("-XX:PermSize=%1m").arg(m_permGen);
+ }
+
+ args.append({"-jar", checkerJar});
+ process->setArguments(args);
+ process->setProgram(m_path);
+ process->setProcessChannelMode(QProcess::SeparateChannels);
+ qDebug() << "Running java checker: " + m_path + args.join(" ");;
+
+ connect(process.get(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus)));
+ connect(process.get(), SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError)));
+ connect(process.get(), SIGNAL(readyReadStandardOutput()), this, SLOT(stdoutReady()));
+ connect(process.get(), SIGNAL(readyReadStandardError()), this, SLOT(stderrReady()));
+ connect(&killTimer, SIGNAL(timeout()), SLOT(timeout()));
+ killTimer.setSingleShot(true);
+ killTimer.start(15000);
+ process->start();
+}
+
+void JavaChecker::stdoutReady()
+{
+ QByteArray data = process->readAllStandardOutput();
+ QString added = QString::fromLocal8Bit(data);
+ added.remove('\r');
+ m_stdout += added;
+}
+
+void JavaChecker::stderrReady()
+{
+ QByteArray data = process->readAllStandardError();
+ QString added = QString::fromLocal8Bit(data);
+ added.remove('\r');
+ m_stderr += added;
+}
+
+void JavaChecker::finished(int exitcode, QProcess::ExitStatus status)
+{
+ killTimer.stop();
+ QProcessPtr _process;
+ _process.swap(process);
+
+ JavaCheckResult result;
+ {
+ result.path = m_path;
+ result.id = m_id;
+ }
+ result.errorLog = m_stderr;
+ qDebug() << "STDOUT" << m_stdout;
+ qWarning() << "STDERR" << m_stderr;
+ qDebug() << "Java checker finished with status " << status << " exit code " << exitcode;
+
+ if (status == QProcess::CrashExit || exitcode == 1)
+ {
+ qDebug() << "Java checker failed!";
+ emit checkFinished(result);
+ return;
+ }
+
+ bool success = true;
+
+ QMap<QString, QString> results;
+ QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts);
+ for(QString line : lines)
+ {
+ 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)
+ {
+ qDebug() << "Java checker failed - couldn't extract required information.";
+ emit checkFinished(result);
+ return;
+ }
+
+ auto os_arch = results["os.arch"];
+ auto java_version = results["java.version"];
+ bool is_64 = os_arch == "x86_64" || os_arch == "amd64";
+
+
+ result.valid = true;
+ result.is_64bit = is_64;
+ result.mojangPlatform = is_64 ? "64" : "32";
+ result.realPlatform = os_arch;
+ result.javaVersion = java_version;
+ qDebug() << "Java checker succeeded.";
+ emit checkFinished(result);
+}
+
+void JavaChecker::error(QProcess::ProcessError err)
+{
+ if(err == QProcess::FailedToStart)
+ {
+ killTimer.stop();
+ qDebug() << "Java checker has failed to start.";
+ JavaCheckResult result;
+ {
+ result.path = m_path;
+ result.id = m_id;
+ }
+
+ emit checkFinished(result);
+ return;
+ }
+}
+
+void JavaChecker::timeout()
+{
+ // NO MERCY. NO ABUSE.
+ if(process)
+ {
+ qDebug() << "Java checker has been killed by timeout.";
+ process->kill();
+ }
+}
diff --git a/api/logic/java/JavaChecker.h b/api/logic/java/JavaChecker.h
new file mode 100644
index 00000000..650e7ce3
--- /dev/null
+++ b/api/logic/java/JavaChecker.h
@@ -0,0 +1,54 @@
+#pragma once
+#include <QProcess>
+#include <QTimer>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+#include "JavaVersion.h"
+
+class JavaChecker;
+
+struct MULTIMC_LOGIC_EXPORT JavaCheckResult
+{
+ QString path;
+ QString mojangPlatform;
+ QString realPlatform;
+ JavaVersion javaVersion;
+ QString errorLog;
+ bool valid = false;
+ bool is_64bit = false;
+ int id;
+};
+
+typedef std::shared_ptr<QProcess> QProcessPtr;
+typedef std::shared_ptr<JavaChecker> JavaCheckerPtr;
+class MULTIMC_LOGIC_EXPORT JavaChecker : public QObject
+{
+ Q_OBJECT
+public:
+ explicit JavaChecker(QObject *parent = 0);
+ void performCheck();
+
+ QString m_path;
+ QString m_args;
+ int m_id = 0;
+ int m_minMem = 0;
+ int m_maxMem = 0;
+ int m_permGen = 64;
+
+signals:
+ void checkFinished(JavaCheckResult result);
+private:
+ QProcessPtr process;
+ QTimer killTimer;
+ QString m_stdout;
+ QString m_stderr;
+public
+slots:
+ void timeout();
+ void finished(int exitcode, QProcess::ExitStatus);
+ void error(QProcess::ProcessError);
+ void stdoutReady();
+ void stderrReady();
+};
diff --git a/api/logic/java/JavaCheckerJob.cpp b/api/logic/java/JavaCheckerJob.cpp
new file mode 100644
index 00000000..0b040e43
--- /dev/null
+++ b/api/logic/java/JavaCheckerJob.cpp
@@ -0,0 +1,45 @@
+/* Copyright 2013-2015 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 <QDebug>
+
+void JavaCheckerJob::partFinished(JavaCheckResult result)
+{
+ num_finished++;
+ qDebug() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/"
+ << javacheckers.size();
+ emit progress(num_finished, javacheckers.size());
+
+ javaresults.replace(result.id, result);
+
+ if (num_finished == javacheckers.size())
+ {
+ emit finished(javaresults);
+ }
+}
+
+void JavaCheckerJob::executeTask()
+{
+ qDebug() << m_job_name.toLocal8Bit() << " started.";
+ m_running = true;
+ for (auto iter : javacheckers)
+ {
+ javaresults.append(JavaCheckResult());
+ connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult)));
+ iter->performCheck();
+ }
+}
diff --git a/api/logic/java/JavaCheckerJob.h b/api/logic/java/JavaCheckerJob.h
new file mode 100644
index 00000000..aca0d02e
--- /dev/null
+++ b/api/logic/java/JavaCheckerJob.h
@@ -0,0 +1,84 @@
+/* Copyright 2013-2015 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 "JavaChecker.h"
+#include "tasks/Task.h"
+
+class JavaCheckerJob;
+typedef std::shared_ptr<JavaCheckerJob> JavaCheckerJobPtr;
+
+class JavaCheckerJob : public Task
+{
+ Q_OBJECT
+public:
+ explicit JavaCheckerJob(QString job_name) : Task(), 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())
+ {
+ setProgress(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 bool isRunning() const override
+ {
+ return m_running;
+ }
+
+signals:
+ void started();
+ void finished(QList<JavaCheckResult>);
+
+private slots:
+ void partFinished(JavaCheckResult result);
+
+protected:
+ virtual void executeTask() override;
+
+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/api/logic/java/JavaInstall.cpp b/api/logic/java/JavaInstall.cpp
new file mode 100644
index 00000000..bb262b6e
--- /dev/null
+++ b/api/logic/java/JavaInstall.cpp
@@ -0,0 +1,28 @@
+#include "JavaInstall.h"
+#include <MMCStrings.h>
+
+bool JavaInstall::operator<(const JavaInstall &rhs)
+{
+ auto archCompare = Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive);
+ if(archCompare != 0)
+ return archCompare < 0;
+ if(id < rhs.id)
+ {
+ return true;
+ }
+ if(id > rhs.id)
+ {
+ return false;
+ }
+ return Strings::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0;
+}
+
+bool JavaInstall::operator==(const JavaInstall &rhs)
+{
+ return arch == rhs.arch && id == rhs.id && path == rhs.path;
+}
+
+bool JavaInstall::operator>(const JavaInstall &rhs)
+{
+ return (!operator<(rhs)) && (!operator==(rhs));
+}
diff --git a/api/logic/java/JavaInstall.h b/api/logic/java/JavaInstall.h
new file mode 100644
index 00000000..882c7386
--- /dev/null
+++ b/api/logic/java/JavaInstall.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "BaseVersion.h"
+#include "JavaVersion.h"
+
+struct JavaInstall : public BaseVersion
+{
+ JavaInstall(){}
+ JavaInstall(QString id, QString arch, QString path)
+ : id(id), arch(arch), path(path)
+ {
+ }
+ virtual QString descriptor()
+ {
+ return id.toString();
+ }
+
+ virtual QString name()
+ {
+ return id.toString();
+ }
+
+ virtual QString typeString() const
+ {
+ return arch;
+ }
+
+ bool operator<(const JavaInstall & rhs);
+ bool operator==(const JavaInstall & rhs);
+ bool operator>(const JavaInstall & rhs);
+
+ JavaVersion id;
+ QString arch;
+ QString path;
+ bool recommended = false;
+};
+
+typedef std::shared_ptr<JavaInstall> JavaInstallPtr;
diff --git a/api/logic/java/JavaInstallList.cpp b/api/logic/java/JavaInstallList.cpp
new file mode 100644
index 00000000..c0729227
--- /dev/null
+++ b/api/logic/java/JavaInstallList.cpp
@@ -0,0 +1,186 @@
+/* Copyright 2013-2015 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 <QtNetwork>
+#include <QtXml>
+#include <QRegExp>
+
+#include <QDebug>
+
+#include "java/JavaInstallList.h"
+#include "java/JavaCheckerJob.h"
+#include "java/JavaUtils.h"
+#include "MMCStrings.h"
+#include "minecraft/VersionFilterData.h"
+
+JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent)
+{
+}
+
+Task *JavaInstallList::getLoadTask()
+{
+ return new JavaListLoadTask(this);
+}
+
+const BaseVersionPtr JavaInstallList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+bool JavaInstallList::isLoaded()
+{
+ return m_loaded;
+}
+
+int JavaInstallList::count() const
+{
+ return m_vlist.count();
+}
+
+QVariant JavaInstallList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<JavaInstall>(m_vlist[index.row()]);
+ switch (role)
+ {
+ case VersionPointerRole:
+ return qVariantFromValue(m_vlist[index.row()]);
+ case VersionIdRole:
+ return version->descriptor();
+ case VersionRole:
+ return version->id.toString();
+ case RecommendedRole:
+ return version->recommended;
+ case PathRole:
+ return version->path;
+ case ArchitectureRole:
+ return version->arch;
+ default:
+ return QVariant();
+ }
+}
+
+BaseVersionList::RoleList JavaInstallList::providesRoles() const
+{
+ return {VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, ArchitectureRole};
+}
+
+
+void JavaInstallList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ m_loaded = true;
+ sortVersions();
+ if(m_vlist.size())
+ {
+ auto best = std::dynamic_pointer_cast<JavaInstall>(m_vlist[0]);
+ best->recommended = true;
+ }
+ endResetModel();
+}
+
+bool sortJavas(BaseVersionPtr left, BaseVersionPtr right)
+{
+ auto rleft = std::dynamic_pointer_cast<JavaInstall>(left);
+ auto rright = std::dynamic_pointer_cast<JavaInstall>(right);
+ return (*rleft) > (*rright);
+}
+
+void JavaInstallList::sortVersions()
+{
+ beginResetModel();
+ std::sort(m_vlist.begin(), m_vlist.end(), sortJavas);
+ endResetModel();
+}
+
+JavaListLoadTask::JavaListLoadTask(JavaInstallList *vlist) : Task()
+{
+ m_list = vlist;
+ m_currentRecommended = NULL;
+}
+
+JavaListLoadTask::~JavaListLoadTask()
+{
+}
+
+void JavaListLoadTask::executeTask()
+{
+ setStatus(tr("Detecting Java installations..."));
+
+ JavaUtils ju;
+ QList<QString> candidate_paths = ju.FindJavaPaths();
+
+ m_job = std::shared_ptr<JavaCheckerJob>(new JavaCheckerJob("Java detection"));
+ connect(m_job.get(), SIGNAL(finished(QList<JavaCheckResult>)), this, SLOT(javaCheckerFinished(QList<JavaCheckResult>)));
+ connect(m_job.get(), &Task::progress, this, &Task::setProgress);
+
+ qDebug() << "Probing the following Java paths: ";
+ int id = 0;
+ for(QString candidate : candidate_paths)
+ {
+ qDebug() << " " << candidate;
+
+ auto candidate_checker = new JavaChecker();
+ candidate_checker->m_path = candidate;
+ candidate_checker->m_id = id;
+ m_job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker));
+
+ id++;
+ }
+
+ m_job->start();
+}
+
+void JavaListLoadTask::javaCheckerFinished(QList<JavaCheckResult> results)
+{
+ QList<JavaInstallPtr> candidates;
+
+ qDebug() << "Found the following valid Java installations:";
+ for(JavaCheckResult result : results)
+ {
+ if(result.valid)
+ {
+ JavaInstallPtr javaVersion(new JavaInstall());
+
+ javaVersion->id = result.javaVersion;
+ javaVersion->arch = result.mojangPlatform;
+ javaVersion->path = result.path;
+ candidates.append(javaVersion);
+
+ qDebug() << " " << javaVersion->id.toString() << javaVersion->arch << javaVersion->path;
+ }
+ }
+
+ QList<BaseVersionPtr> javas_bvp;
+ for (auto java : candidates)
+ {
+ //qDebug() << java->id << java->arch << " at " << java->path;
+ BaseVersionPtr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
+
+ if (bp_java)
+ {
+ javas_bvp.append(java);
+ }
+ }
+
+ m_list->updateListData(javas_bvp);
+ emitSucceeded();
+}
diff --git a/api/logic/java/JavaInstallList.h b/api/logic/java/JavaInstallList.h
new file mode 100644
index 00000000..cf0e5784
--- /dev/null
+++ b/api/logic/java/JavaInstallList.h
@@ -0,0 +1,71 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QAbstractListModel>
+
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+
+#include "JavaCheckerJob.h"
+#include "JavaInstall.h"
+
+#include "multimc_logic_export.h"
+
+class JavaListLoadTask;
+
+class MULTIMC_LOGIC_EXPORT JavaInstallList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ explicit JavaInstallList(QObject *parent = 0);
+
+ virtual Task *getLoadTask() override;
+ virtual bool isLoaded() override;
+ virtual const BaseVersionPtr at(int i) const override;
+ virtual int count() const override;
+ virtual void sortVersions() override;
+
+ virtual QVariant data(const QModelIndex &index, int role) const override;
+ virtual RoleList providesRoles() const override;
+
+public slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions) override;
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+
+ bool m_loaded = false;
+};
+
+class JavaListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit JavaListLoadTask(JavaInstallList *vlist);
+ ~JavaListLoadTask();
+
+ virtual void executeTask();
+public slots:
+ void javaCheckerFinished(QList<JavaCheckResult> results);
+
+protected:
+ std::shared_ptr<JavaCheckerJob> m_job;
+ JavaInstallList *m_list;
+ JavaInstall *m_currentRecommended;
+};
diff --git a/api/logic/java/JavaUtils.cpp b/api/logic/java/JavaUtils.cpp
new file mode 100644
index 00000000..88996e9f
--- /dev/null
+++ b/api/logic/java/JavaUtils.cpp
@@ -0,0 +1,219 @@
+/* Copyright 2013-2015 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 <QStringList>
+#include <QString>
+#include <QDir>
+#include <QStringList>
+
+#include <settings/Setting.h>
+
+#include <QDebug>
+#include "java/JavaUtils.h"
+#include "java/JavaCheckerJob.h"
+#include "java/JavaInstallList.h"
+#include "FileSystem.h"
+
+JavaUtils::JavaUtils()
+{
+}
+
+JavaInstallPtr JavaUtils::MakeJavaPtr(QString path, QString id, QString arch)
+{
+ JavaInstallPtr javaVersion(new JavaInstall());
+
+ javaVersion->id = id;
+ javaVersion->arch = arch;
+ javaVersion->path = path;
+
+ return javaVersion;
+}
+
+JavaInstallPtr JavaUtils::GetDefaultJava()
+{
+ JavaInstallPtr javaVersion(new JavaInstall());
+
+ javaVersion->id = "java";
+ javaVersion->arch = "unknown";
+ javaVersion->path = "java";
+
+ return javaVersion;
+}
+
+#if defined(Q_OS_WIN32)
+QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName)
+{
+ QList<JavaInstallPtr> javas;
+
+ QString archType = "unknown";
+ if (keyType == KEY_WOW64_64KEY)
+ archType = "64";
+ else if (keyType == KEY_WOW64_32KEY)
+ archType = "32";
+
+ HKEY jreKey;
+ if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, keyName.toStdString().c_str(), 0,
+ KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) == ERROR_SUCCESS)
+ {
+ // Read the current type version from the registry.
+ // This will be used to find any key that contains the JavaHome value.
+ char *value = new char[0];
+ DWORD valueSz = 0;
+ if (RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz) ==
+ ERROR_MORE_DATA)
+ {
+ value = new char[valueSz];
+ RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz);
+ }
+
+ QString recommended = value;
+
+ TCHAR subKeyName[255];
+ DWORD subKeyNameSize, numSubKeys, retCode;
+
+ // Get the number of subkeys
+ RegQueryInfoKey(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL);
+
+ // Iterate until RegEnumKeyEx fails
+ if (numSubKeys > 0)
+ {
+ for (int i = 0; i < numSubKeys; i++)
+ {
+ subKeyNameSize = 255;
+ retCode = RegEnumKeyEx(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL,
+ NULL);
+ if (retCode == ERROR_SUCCESS)
+ {
+ // Now open the registry key for the version that we just got.
+ QString newKeyName = keyName + "\\" + subKeyName;
+
+ HKEY newKey;
+ if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, newKeyName.toStdString().c_str(), 0,
+ KEY_READ | KEY_WOW64_64KEY, &newKey) == ERROR_SUCCESS)
+ {
+ // Read the JavaHome value to find where Java is installed.
+ value = new char[0];
+ valueSz = 0;
+ if (RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value,
+ &valueSz) == ERROR_MORE_DATA)
+ {
+ value = new char[valueSz];
+ RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value,
+ &valueSz);
+
+ // Now, we construct the version object and add it to the list.
+ JavaInstallPtr javaVersion(new JavaInstall());
+
+ javaVersion->id = subKeyName;
+ javaVersion->arch = archType;
+ javaVersion->path =
+ QDir(FS::PathCombine(value, "bin")).absoluteFilePath("javaw.exe");
+ javas.append(javaVersion);
+ }
+
+ RegCloseKey(newKey);
+ }
+ }
+ }
+ }
+
+ RegCloseKey(jreKey);
+ }
+
+ return javas;
+}
+
+QList<QString> JavaUtils::FindJavaPaths()
+{
+ QList<JavaInstallPtr> java_candidates;
+
+ QList<JavaInstallPtr> JRE64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
+ QList<JavaInstallPtr> JDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit");
+ QList<JavaInstallPtr> JRE32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
+ QList<JavaInstallPtr> JDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit");
+
+ java_candidates.append(JRE64s);
+ java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe"));
+ java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe"));
+ java_candidates.append(JDK64s);
+ java_candidates.append(JRE32s);
+ java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe"));
+ java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe"));
+ java_candidates.append(JDK32s);
+ java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path));
+
+ QList<QString> candidates;
+ for(JavaInstallPtr java_candidate : java_candidates)
+ {
+ if(!candidates.contains(java_candidate->path))
+ {
+ candidates.append(java_candidate->path);
+ }
+ }
+
+ return candidates;
+}
+
+#elif defined(Q_OS_MAC)
+QList<QString> JavaUtils::FindJavaPaths()
+{
+ QList<QString> javas;
+ javas.append(this->GetDefaultJava()->path);
+ javas.append("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java");
+ javas.append("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java");
+ javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java");
+ QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/");
+ QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
+ foreach (const QString &java, libraryJVMJavas) {
+ javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
+ javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java");
+ }
+ QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/");
+ QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
+ foreach (const QString &java, systemLibraryJVMJavas) {
+ javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java");
+ javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java");
+ }
+ return javas;
+}
+
+#elif defined(Q_OS_LINUX)
+QList<QString> JavaUtils::FindJavaPaths()
+{
+ qDebug() << "Linux Java detection incomplete - defaulting to \"java\"";
+
+ QList<QString> javas;
+ javas.append(this->GetDefaultJava()->path);
+ javas.append("/opt/java/bin/java");
+ javas.append("/usr/bin/java");
+
+ return javas;
+}
+#else
+QList<QString> JavaUtils::FindJavaPaths()
+{
+ qDebug() << "Unknown operating system build - defaulting to \"java\"";
+
+ QList<QString> javas;
+ javas.append(this->GetDefaultJava()->path);
+
+ return javas;
+}
+#endif
diff --git a/api/logic/java/JavaUtils.h b/api/logic/java/JavaUtils.h
new file mode 100644
index 00000000..3fb88341
--- /dev/null
+++ b/api/logic/java/JavaUtils.h
@@ -0,0 +1,43 @@
+/* Copyright 2013-2015 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 <QStringList>
+
+#include "JavaCheckerJob.h"
+#include "JavaChecker.h"
+#include "JavaInstallList.h"
+
+#ifdef Q_OS_WIN
+#include <windows.h>
+#endif
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT JavaUtils : public QObject
+{
+ Q_OBJECT
+public:
+ JavaUtils();
+
+ JavaInstallPtr MakeJavaPtr(QString path, QString id = "unknown", QString arch = "unknown");
+ QList<QString> FindJavaPaths();
+ JavaInstallPtr GetDefaultJava();
+
+#ifdef Q_OS_WIN
+ QList<JavaInstallPtr> FindJavaFromRegistryKey(DWORD keyType, QString keyName);
+#endif
+};
diff --git a/api/logic/java/JavaVersion.cpp b/api/logic/java/JavaVersion.cpp
new file mode 100644
index 00000000..84fc48a4
--- /dev/null
+++ b/api/logic/java/JavaVersion.cpp
@@ -0,0 +1,112 @@
+#include "JavaVersion.h"
+#include <MMCStrings.h>
+
+#include <QRegularExpression>
+#include <QString>
+
+JavaVersion & JavaVersion::operator=(const QString & javaVersionString)
+{
+ string = javaVersionString;
+
+ auto getCapturedInteger = [](const QRegularExpressionMatch & match, const QString &what) -> int
+ {
+ auto str = match.captured(what);
+ if(str.isEmpty())
+ {
+ return 0;
+ }
+ return str.toInt();
+ };
+
+ QRegularExpression pattern;
+ if(javaVersionString.startsWith("1."))
+ {
+ pattern = QRegularExpression ("1[.](?<major>[0-9]+)([.](?<minor>[0-9]+))?(_(?<security>[0-9]+)?)?(-(?<prerelease>[a-zA-Z0-9]+))?");
+ }
+ else
+ {
+ pattern = QRegularExpression("(?<major>[0-9]+)([.](?<minor>[0-9]+))?([.](?<security>[0-9]+))?(-(?<prerelease>[a-zA-Z0-9]+))?");
+ }
+
+ auto match = pattern.match(string);
+ parseable = match.hasMatch();
+ major = getCapturedInteger(match, "major");
+ minor = getCapturedInteger(match, "minor");
+ security = getCapturedInteger(match, "security");
+ prerelease = match.captured("prerelease");
+ return *this;
+}
+
+JavaVersion::JavaVersion(const QString &rhs)
+{
+ operator=(rhs);
+}
+
+QString JavaVersion::toString()
+{
+ return string;
+}
+
+bool JavaVersion::requiresPermGen()
+{
+ if(parseable)
+ {
+ return major < 8;
+ }
+ return true;
+}
+
+bool JavaVersion::operator<(const JavaVersion &rhs)
+{
+ if(parseable && rhs.parseable)
+ {
+ if(major < rhs.major)
+ return true;
+ if(major > rhs.major)
+ return false;
+ if(minor < rhs.minor)
+ return true;
+ if(minor > rhs.minor)
+ return false;
+ if(security < rhs.security)
+ return true;
+ if(security > rhs.security)
+ return false;
+
+ // everything else being equal, consider prerelease status
+ bool thisPre = !prerelease.isEmpty();
+ bool rhsPre = !rhs.prerelease.isEmpty();
+ if(thisPre && !rhsPre)
+ {
+ // this is a prerelease and the other one isn't -> lesser
+ return true;
+ }
+ else if(!thisPre && rhsPre)
+ {
+ // this isn't a prerelease and the other one is -> greater
+ return false;
+ }
+ else if(thisPre && rhsPre)
+ {
+ // both are prereleases - use natural compare...
+ return Strings::naturalCompare(prerelease, rhs.prerelease, Qt::CaseSensitive) < 0;
+ }
+ // neither is prerelease, so they are the same -> this cannot be less than rhs
+ return false;
+ }
+ else return Strings::naturalCompare(string, rhs.string, Qt::CaseSensitive) < 0;
+}
+
+bool JavaVersion::operator==(const JavaVersion &rhs)
+{
+ if(parseable && rhs.parseable)
+ {
+ return major == rhs.major && minor == rhs.minor && security == rhs.security && prerelease == rhs.prerelease;
+ }
+ return string == rhs.string;
+}
+
+bool JavaVersion::operator>(const JavaVersion &rhs)
+{
+ return (!operator<(rhs)) && (!operator==(rhs));
+}
diff --git a/api/logic/java/JavaVersion.h b/api/logic/java/JavaVersion.h
new file mode 100644
index 00000000..f9a733d3
--- /dev/null
+++ b/api/logic/java/JavaVersion.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "multimc_logic_export.h"
+#include <QString>
+
+class MULTIMC_LOGIC_EXPORT JavaVersion
+{
+ friend class JavaVersionTest;
+public:
+ JavaVersion() {};
+ JavaVersion(const QString & rhs);
+
+ JavaVersion & operator=(const QString & rhs);
+
+ bool operator<(const JavaVersion & rhs);
+ bool operator==(const JavaVersion & rhs);
+ bool operator>(const JavaVersion & rhs);
+
+ bool requiresPermGen();
+
+ QString toString();
+
+private:
+ QString string;
+ int major = 0;
+ int minor = 0;
+ int security = 0;
+ bool parseable = false;
+ QString prerelease;
+};
diff --git a/api/logic/launch/LaunchStep.cpp b/api/logic/launch/LaunchStep.cpp
new file mode 100644
index 00000000..3078043b
--- /dev/null
+++ b/api/logic/launch/LaunchStep.cpp
@@ -0,0 +1,27 @@
+/* Copyright 2013-2015 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 "LaunchStep.h"
+#include "LaunchTask.h"
+
+void LaunchStep::bind(LaunchTask *parent)
+{
+ m_parent = parent;
+ connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch);
+ connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine);
+ connect(this, &LaunchStep::logLines, parent, &LaunchTask::onLogLines);
+ connect(this, &LaunchStep::finished, parent, &LaunchTask::onStepFinished);
+ connect(this, &LaunchStep::progressReportingRequest, parent, &LaunchTask::onProgressReportingRequested);
+}
diff --git a/api/logic/launch/LaunchStep.h b/api/logic/launch/LaunchStep.h
new file mode 100644
index 00000000..ea472c0d
--- /dev/null
+++ b/api/logic/launch/LaunchStep.h
@@ -0,0 +1,48 @@
+/* Copyright 2013-2015 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 "tasks/Task.h"
+#include "MessageLevel.h"
+
+#include <QStringList>
+
+class LaunchTask;
+class LaunchStep: public Task
+{
+ Q_OBJECT
+public: /* methods */
+ explicit LaunchStep(LaunchTask *parent):Task(nullptr), m_parent(parent)
+ {
+ bind(parent);
+ };
+ virtual ~LaunchStep() {};
+
+protected: /* methods */
+ virtual void bind(LaunchTask *parent);
+
+signals:
+ void logLines(QStringList lines, MessageLevel::Enum level);
+ void logLine(QString line, MessageLevel::Enum level);
+ void readyForLaunch();
+ void progressReportingRequest();
+
+public slots:
+ virtual void proceed() {};
+
+protected: /* data */
+ LaunchTask *m_parent;
+}; \ No newline at end of file
diff --git a/api/logic/launch/LaunchTask.cpp b/api/logic/launch/LaunchTask.cpp
new file mode 100644
index 00000000..5b7ff182
--- /dev/null
+++ b/api/logic/launch/LaunchTask.cpp
@@ -0,0 +1,228 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * 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 "launch/LaunchTask.h"
+#include "MessageLevel.h"
+#include "MMCStrings.h"
+#include "java/JavaChecker.h"
+#include "tasks/Task.h"
+#include <QDebug>
+#include <QDir>
+#include <QEventLoop>
+#include <QRegularExpression>
+#include <QCoreApplication>
+#include <QStandardPaths>
+#include <assert.h>
+
+void LaunchTask::init()
+{
+ m_instance->setRunning(true);
+}
+
+std::shared_ptr<LaunchTask> LaunchTask::create(InstancePtr inst)
+{
+ std::shared_ptr<LaunchTask> proc(new LaunchTask(inst));
+ proc->init();
+ return proc;
+}
+
+LaunchTask::LaunchTask(InstancePtr instance): m_instance(instance)
+{
+}
+
+void LaunchTask::appendStep(std::shared_ptr<LaunchStep> step)
+{
+ m_steps.append(step);
+}
+
+void LaunchTask::prependStep(std::shared_ptr<LaunchStep> step)
+{
+ m_steps.prepend(step);
+}
+
+void LaunchTask::executeTask()
+{
+ if(!m_steps.size())
+ {
+ state = LaunchTask::Finished;
+ emitSucceeded();
+ }
+ state = LaunchTask::Running;
+ onStepFinished();
+}
+
+void LaunchTask::onReadyForLaunch()
+{
+ state = LaunchTask::Waiting;
+ emit readyForLaunch();
+}
+
+void LaunchTask::onStepFinished()
+{
+ // initial -> just start the first step
+ if(currentStep == -1)
+ {
+ currentStep ++;
+ m_steps[currentStep]->start();
+ return;
+ }
+
+ auto step = m_steps[currentStep];
+ if(step->successful())
+ {
+ // end?
+ if(currentStep == m_steps.size() - 1)
+ {
+ emitSucceeded();
+ }
+ else
+ {
+ currentStep ++;
+ step = m_steps[currentStep];
+ step->start();
+ }
+ }
+ else
+ {
+ emitFailed(step->failReason());
+ }
+}
+
+void LaunchTask::onProgressReportingRequested()
+{
+ state = LaunchTask::Waiting;
+ emit requestProgress(m_steps[currentStep].get());
+}
+
+void LaunchTask::setCensorFilter(QMap<QString, QString> filter)
+{
+ m_censorFilter = filter;
+}
+
+QString LaunchTask::censorPrivateInfo(QString in)
+{
+ auto iter = m_censorFilter.begin();
+ while (iter != m_censorFilter.end())
+ {
+ in.replace(iter.key(), iter.value());
+ iter++;
+ }
+ return in;
+}
+
+void LaunchTask::proceed()
+{
+ if(state != LaunchTask::Waiting)
+ {
+ return;
+ }
+ m_steps[currentStep]->proceed();
+}
+
+bool LaunchTask::abort()
+{
+ switch(state)
+ {
+ case LaunchTask::Aborted:
+ case LaunchTask::Failed:
+ case LaunchTask::Finished:
+ return true;
+ case LaunchTask::NotStarted:
+ {
+ state = LaunchTask::Aborted;
+ emitFailed("Aborted");
+ return true;
+ }
+ case LaunchTask::Running:
+ case LaunchTask::Waiting:
+ {
+ auto step = m_steps[currentStep];
+ if(!step->canAbort())
+ {
+ return false;
+ }
+ if(step->abort())
+ {
+ state = LaunchTask::Aborted;
+ return true;
+ }
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+void LaunchTask::onLogLines(const QStringList &lines, MessageLevel::Enum defaultLevel)
+{
+ for (auto & line: lines)
+ {
+ onLogLine(line, defaultLevel);
+ }
+}
+
+void LaunchTask::onLogLine(QString line, MessageLevel::Enum level)
+{
+ // if the launcher part set a log level, use it
+ auto innerLevel = MessageLevel::fromLine(line);
+ if(innerLevel != MessageLevel::Unknown)
+ {
+ level = innerLevel;
+ }
+
+ // If the level is still undetermined, guess level
+ if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown)
+ {
+ level = m_instance->guessLevel(line, level);
+ }
+
+ // censor private user info
+ line = censorPrivateInfo(line);
+
+ emit log(line, level);
+}
+
+void LaunchTask::emitSucceeded()
+{
+ m_instance->cleanupAfterRun();
+ m_instance->setRunning(false);
+ Task::emitSucceeded();
+}
+
+void LaunchTask::emitFailed(QString reason)
+{
+ m_instance->cleanupAfterRun();
+ m_instance->setRunning(false);
+ Task::emitFailed(reason);
+}
+
+QString LaunchTask::substituteVariables(const QString &cmd) const
+{
+ QString out = cmd;
+ auto variables = m_instance->getVariables();
+ for (auto it = variables.begin(); it != variables.end(); ++it)
+ {
+ out.replace("$" + it.key(), it.value());
+ }
+ auto env = QProcessEnvironment::systemEnvironment();
+ for (auto var : env.keys())
+ {
+ out.replace("$" + var, env.value(var));
+ }
+ return out;
+}
+
diff --git a/api/logic/launch/LaunchTask.h b/api/logic/launch/LaunchTask.h
new file mode 100644
index 00000000..447445ca
--- /dev/null
+++ b/api/logic/launch/LaunchTask.h
@@ -0,0 +1,122 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * 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 <QProcess>
+#include "BaseInstance.h"
+#include "MessageLevel.h"
+#include "LoggedProcess.h"
+#include "LaunchStep.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT LaunchTask: public Task
+{
+ Q_OBJECT
+protected:
+ explicit LaunchTask(InstancePtr instance);
+ void init();
+
+public:
+ enum State
+ {
+ NotStarted,
+ Running,
+ Waiting,
+ Failed,
+ Aborted,
+ Finished
+ };
+
+public: /* methods */
+ static std::shared_ptr<LaunchTask> create(InstancePtr inst);
+ virtual ~LaunchTask() {};
+
+ void appendStep(std::shared_ptr<LaunchStep> step);
+ void prependStep(std::shared_ptr<LaunchStep> step);
+ void setCensorFilter(QMap<QString, QString> filter);
+
+ InstancePtr instance()
+ {
+ return m_instance;
+ }
+
+ void setPid(qint64 pid)
+ {
+ m_pid = pid;
+ }
+
+ qint64 pid()
+ {
+ return m_pid;
+ }
+
+ /**
+ * @brief prepare the process for launch (for multi-stage launch)
+ */
+ virtual void executeTask() override;
+
+ /**
+ * @brief launch the armed instance
+ */
+ void proceed();
+
+ /**
+ * @brief abort launch
+ */
+ virtual bool abort() override;
+
+public:
+ QString substituteVariables(const QString &cmd) const;
+ QString censorPrivateInfo(QString in);
+
+protected: /* methods */
+ virtual void emitFailed(QString reason) override;
+ virtual void emitSucceeded() override;
+
+signals:
+ /**
+ * @brief emitted when the launch preparations are done
+ */
+ void readyForLaunch();
+
+ void requestProgress(Task *task);
+
+ void requestLogging();
+
+ /**
+ * @brief emitted when we want to log something
+ * @param text the text to log
+ * @param level the level to log at
+ */
+ void log(QString text, MessageLevel::Enum level = MessageLevel::MultiMC);
+
+public slots:
+ void onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel = MessageLevel::MultiMC);
+ void onLogLine(QString line, MessageLevel::Enum defaultLevel = MessageLevel::MultiMC);
+ void onReadyForLaunch();
+ void onStepFinished();
+ void onProgressReportingRequested();
+
+protected: /* data */
+ InstancePtr m_instance;
+ QList <std::shared_ptr<LaunchStep>> m_steps;
+ QMap<QString, QString> m_censorFilter;
+ int currentStep = -1;
+ State state = NotStarted;
+ qint64 m_pid = -1;
+};
diff --git a/api/logic/launch/LoggedProcess.cpp b/api/logic/launch/LoggedProcess.cpp
new file mode 100644
index 00000000..88ca40aa
--- /dev/null
+++ b/api/logic/launch/LoggedProcess.cpp
@@ -0,0 +1,163 @@
+#include "LoggedProcess.h"
+#include "MessageLevel.h"
+#include <QDebug>
+
+LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent)
+{
+ // QProcess has a strange interface... let's map a lot of those into a few.
+ connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut);
+ connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr);
+ connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(on_exit(int,QProcess::ExitStatus)));
+ connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError)));
+ connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange);
+}
+
+QStringList reprocess(const QByteArray & data, QString & leftover)
+{
+ QString str = leftover + QString::fromLocal8Bit(data);
+
+ str.remove('\r');
+ QStringList lines = str.split("\n");
+ leftover = lines.takeLast();
+ return lines;
+}
+
+void LoggedProcess::on_stdErr()
+{
+ auto lines = reprocess(readAllStandardError(), m_err_leftover);
+ emit log(lines, MessageLevel::StdErr);
+}
+
+void LoggedProcess::on_stdOut()
+{
+ auto lines = reprocess(readAllStandardOutput(), m_out_leftover);
+ emit log(lines, MessageLevel::StdOut);
+}
+
+void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status)
+{
+ // save the exit code
+ m_exit_code = exit_code;
+
+ // Flush console window
+ if (!m_err_leftover.isEmpty())
+ {
+ emit log({m_err_leftover}, MessageLevel::StdErr);
+ m_err_leftover.clear();
+ }
+ if (!m_out_leftover.isEmpty())
+ {
+ emit log({m_err_leftover}, MessageLevel::StdOut);
+ m_out_leftover.clear();
+ }
+
+ // based on state, send signals
+ if (!m_is_aborting)
+ {
+ if (status == QProcess::NormalExit)
+ {
+ //: Message displayed on instance exit
+ emit log({tr("Process exited with code %1.").arg(exit_code)}, MessageLevel::MultiMC);
+ changeState(LoggedProcess::Finished);
+ }
+ else
+ {
+ //: Message displayed on instance crashed
+ if(exit_code == -1)
+ emit log({tr("Process crashed.")}, MessageLevel::MultiMC);
+ else
+ emit log({tr("Process crashed with exitcode %1.").arg(exit_code)}, MessageLevel::MultiMC);
+ changeState(LoggedProcess::Crashed);
+ }
+ }
+ else
+ {
+ //: Message displayed after the instance exits due to kill request
+ emit log({tr("Process was killed by user.")}, MessageLevel::Error);
+ changeState(LoggedProcess::Aborted);
+ }
+}
+
+void LoggedProcess::on_error(QProcess::ProcessError error)
+{
+ switch(error)
+ {
+ case QProcess::FailedToStart:
+ {
+ emit log({tr("The process failed to start.")}, MessageLevel::Fatal);
+ changeState(LoggedProcess::FailedToStart);
+ break;
+ }
+ // we'll just ignore those... never needed them
+ case QProcess::Crashed:
+ case QProcess::ReadError:
+ case QProcess::Timedout:
+ case QProcess::UnknownError:
+ case QProcess::WriteError:
+ break;
+ }
+}
+
+void LoggedProcess::kill()
+{
+ m_is_aborting = true;
+ QProcess::kill();
+}
+
+int LoggedProcess::exitCode() const
+{
+ return m_exit_code;
+}
+
+void LoggedProcess::changeState(LoggedProcess::State state)
+{
+ if(state == m_state)
+ return;
+ m_state = state;
+ emit stateChanged(m_state);
+}
+
+LoggedProcess::State LoggedProcess::state() const
+{
+ return m_state;
+}
+
+void LoggedProcess::on_stateChange(QProcess::ProcessState state)
+{
+ switch(state)
+ {
+ case QProcess::NotRunning:
+ break; // let's not - there are too many that handle this already.
+ case QProcess::Starting:
+ {
+ if(m_state != LoggedProcess::NotRunning)
+ {
+ qWarning() << "Wrong state change for process from state" << m_state << "to" << (int) LoggedProcess::Starting;
+ }
+ changeState(LoggedProcess::Starting);
+ return;
+ }
+ case QProcess::Running:
+ {
+ if(m_state != LoggedProcess::Starting)
+ {
+ qWarning() << "Wrong state change for process from state" << m_state << "to" << (int) LoggedProcess::Running;
+ }
+ changeState(LoggedProcess::Running);
+ return;
+ }
+ }
+}
+
+#if defined Q_OS_WIN32
+#include <windows.h>
+#endif
+
+qint64 LoggedProcess::processId() const
+{
+#ifdef Q_OS_WIN
+ return pid() ? pid()->dwProcessId : 0;
+#else
+ return pid();
+#endif
+}
diff --git a/api/logic/launch/LoggedProcess.h b/api/logic/launch/LoggedProcess.h
new file mode 100644
index 00000000..baa53d79
--- /dev/null
+++ b/api/logic/launch/LoggedProcess.h
@@ -0,0 +1,76 @@
+/* Copyright 2013-2015 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 <QProcess>
+#include "MessageLevel.h"
+
+/*
+ * This is a basic process.
+ * It has line-based logging support and hides some of the nasty bits.
+ */
+class LoggedProcess : public QProcess
+{
+Q_OBJECT
+public:
+ enum State
+ {
+ NotRunning,
+ Starting,
+ FailedToStart,
+ Running,
+ Finished,
+ Crashed,
+ Aborted
+ };
+
+public:
+ explicit LoggedProcess(QObject* parent = 0);
+ virtual ~LoggedProcess() {};
+
+ State state() const;
+ int exitCode() const;
+ qint64 processId() const;
+
+signals:
+ void log(QStringList lines, MessageLevel::Enum level);
+ void stateChanged(LoggedProcess::State state);
+
+public slots:
+ /**
+ * @brief kill the process - equivalent to kill -9
+ */
+ void kill();
+
+
+private slots:
+ void on_stdErr();
+ void on_stdOut();
+ void on_exit(int exit_code, QProcess::ExitStatus status);
+ void on_error(QProcess::ProcessError error);
+ void on_stateChange(QProcess::ProcessState);
+
+private:
+ void changeState(LoggedProcess::State state);
+
+private:
+ QString m_err_leftover;
+ QString m_out_leftover;
+ bool m_killed = false;
+ State m_state = NotRunning;
+ int m_exit_code = 0;
+ bool m_is_aborting = false;
+};
diff --git a/api/logic/launch/MessageLevel.cpp b/api/logic/launch/MessageLevel.cpp
new file mode 100644
index 00000000..a5191290
--- /dev/null
+++ b/api/logic/launch/MessageLevel.cpp
@@ -0,0 +1,36 @@
+#include "MessageLevel.h"
+
+MessageLevel::Enum MessageLevel::getLevel(const QString& levelName)
+{
+ if (levelName == "MultiMC")
+ return MessageLevel::MultiMC;
+ else if (levelName == "Debug")
+ return MessageLevel::Debug;
+ else if (levelName == "Info")
+ return MessageLevel::Info;
+ else if (levelName == "Message")
+ return MessageLevel::Message;
+ else if (levelName == "Warning")
+ return MessageLevel::Warning;
+ else if (levelName == "Error")
+ return MessageLevel::Error;
+ else if (levelName == "Fatal")
+ return MessageLevel::Fatal;
+ // Skip PrePost, it's not exposed to !![]!
+ // Also skip StdErr and StdOut
+ else
+ return MessageLevel::Unknown;
+}
+
+MessageLevel::Enum MessageLevel::fromLine(QString &line)
+{
+ // Level prefix
+ int endmark = line.indexOf("]!");
+ if (line.startsWith("!![") && endmark != -1)
+ {
+ auto level = MessageLevel::getLevel(line.left(endmark).mid(3));
+ line = line.mid(endmark + 2);
+ return level;
+ }
+ return MessageLevel::Unknown;
+}
diff --git a/api/logic/launch/MessageLevel.h b/api/logic/launch/MessageLevel.h
new file mode 100644
index 00000000..0128148d
--- /dev/null
+++ b/api/logic/launch/MessageLevel.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <QString>
+
+/**
+ * @brief the MessageLevel Enum
+ * defines what level a log message is
+ */
+namespace MessageLevel
+{
+enum Enum
+{
+ Unknown, /**< No idea what this is or where it came from */
+ StdOut, /**< Undetermined stderr messages */
+ StdErr, /**< Undetermined stdout messages */
+ MultiMC, /**< MultiMC Messages */
+ Debug, /**< Debug Messages */
+ Info, /**< Info Messages */
+ Message, /**< Standard Messages */
+ Warning, /**< Warnings */
+ Error, /**< Errors */
+ Fatal, /**< Fatal Errors */
+};
+MessageLevel::Enum getLevel(const QString &levelName);
+
+/* Get message level from a line. Line is modified if it was successful. */
+MessageLevel::Enum fromLine(QString &line);
+}
diff --git a/api/logic/launch/steps/CheckJava.cpp b/api/logic/launch/steps/CheckJava.cpp
new file mode 100644
index 00000000..a4eaa307
--- /dev/null
+++ b/api/logic/launch/steps/CheckJava.cpp
@@ -0,0 +1,92 @@
+/* Copyright 2013-2015 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 "CheckJava.h"
+#include <launch/LaunchTask.h>
+#include <FileSystem.h>
+#include <QStandardPaths>
+#include <QFileInfo>
+
+void CheckJava::executeTask()
+{
+ auto instance = m_parent->instance();
+ auto settings = instance->settings();
+ m_javaPath = FS::ResolveExecutable(settings->get("JavaPath").toString());
+ bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool();
+
+ auto realJavaPath = QStandardPaths::findExecutable(m_javaPath);
+ if (realJavaPath.isEmpty())
+ {
+ if (perInstance)
+ {
+ emit logLine(
+ tr("The java binary \"%1\" couldn't be found. Please fix the java path "
+ "override in the instance's settings or disable it.").arg(m_javaPath),
+ MessageLevel::Warning);
+ }
+ else
+ {
+ emit logLine(tr("The java binary \"%1\" couldn't be found. Please set up java in "
+ "the settings.").arg(m_javaPath),
+ MessageLevel::Warning);
+ }
+ emitFailed(tr("Java path is not valid."));
+ return;
+ }
+ else
+ {
+ emit logLine("Java path is:\n" + m_javaPath + "\n\n", MessageLevel::MultiMC);
+ }
+
+ QFileInfo javaInfo(realJavaPath);
+ qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch();
+ auto storedUnixTime = settings->get("JavaTimestamp").toLongLong();
+ m_javaUnixTime = javaUnixTime;
+ // if they are not the same, check!
+ if (javaUnixTime != storedUnixTime)
+ {
+ m_JavaChecker = std::make_shared<JavaChecker>();
+ QString errorLog;
+ QString version;
+ emit logLine(tr("Checking Java version..."), MessageLevel::MultiMC);
+ connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this,
+ &CheckJava::checkJavaFinished);
+ m_JavaChecker->m_path = realJavaPath;
+ m_JavaChecker->performCheck();
+ return;
+ }
+ emitSucceeded();
+}
+
+void CheckJava::checkJavaFinished(JavaCheckResult result)
+{
+ if (!result.valid)
+ {
+ // Error message displayed if java can't start
+ emit logLine(tr("Could not start java:"), MessageLevel::Error);
+ emit logLines(result.errorLog.split('\n'), MessageLevel::Error);
+ emit logLine("\nCheck your MultiMC Java settings.", MessageLevel::MultiMC);
+ emitFailed(tr("Could not start java!"));
+ }
+ else
+ {
+ auto instance = m_parent->instance();
+ emit logLine(tr("Java version is %1!\n").arg(result.javaVersion.toString()),
+ MessageLevel::MultiMC);
+ instance->settings()->set("JavaVersion", result.javaVersion.toString());
+ instance->settings()->set("JavaTimestamp", m_javaUnixTime);
+ emitSucceeded();
+ }
+}
diff --git a/api/logic/launch/steps/CheckJava.h b/api/logic/launch/steps/CheckJava.h
new file mode 100644
index 00000000..b63dd4f4
--- /dev/null
+++ b/api/logic/launch/steps/CheckJava.h
@@ -0,0 +1,41 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <launch/LoggedProcess.h>
+#include <java/JavaChecker.h>
+
+class CheckJava: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit CheckJava(LaunchTask *parent) :LaunchStep(parent){};
+ virtual ~CheckJava() {};
+
+ virtual void executeTask();
+ virtual bool canAbort() const
+ {
+ return false;
+ }
+private slots:
+ void checkJavaFinished(JavaCheckResult result);
+
+private:
+ QString m_javaPath;
+ qlonglong m_javaUnixTime;
+ JavaCheckerPtr m_JavaChecker;
+};
diff --git a/api/logic/launch/steps/LaunchMinecraft.cpp b/api/logic/launch/steps/LaunchMinecraft.cpp
new file mode 100644
index 00000000..9b8cc0fb
--- /dev/null
+++ b/api/logic/launch/steps/LaunchMinecraft.cpp
@@ -0,0 +1,155 @@
+/* Copyright 2013-2015 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 "LaunchMinecraft.h"
+#include <launch/LaunchTask.h>
+#include <minecraft/MinecraftInstance.h>
+#include <FileSystem.h>
+#include <QStandardPaths>
+
+LaunchMinecraft::LaunchMinecraft(LaunchTask *parent) : LaunchStep(parent)
+{
+ connect(&m_process, &LoggedProcess::log, this, &LaunchMinecraft::logLines);
+ connect(&m_process, &LoggedProcess::stateChanged, this, &LaunchMinecraft::on_state);
+}
+
+void LaunchMinecraft::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance);
+
+ m_launchScript = minecraftInstance->createLaunchScript(m_session);
+
+ QStringList args = minecraftInstance->javaArguments();
+
+ // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' is created.
+ if(!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->minecraftRoot(), "server-resource-packs")))
+ {
+ emit logLine(tr("Couldn't create the 'server-resource-packs' folder"), MessageLevel::Error);
+ }
+
+ QString allArgs = args.join(", ");
+ emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::MultiMC);
+
+ auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString());
+
+ m_process.setProcessEnvironment(instance->createEnvironment());
+
+ QString wrapperCommand = instance->getWrapperCommand();
+ if(!wrapperCommand.isEmpty())
+ {
+ auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);
+ if (realWrapperCommand.isEmpty())
+ {
+ QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand);
+ emit logLine(reason, MessageLevel::Fatal);
+ emitFailed(reason);
+ return;
+ }
+ emit logLine("Wrapper command is:\n" + wrapperCommand + "\n\n", MessageLevel::MultiMC);
+ args.prepend(javaPath);
+ m_process.start(wrapperCommand, args);
+ }
+ else
+ {
+ m_process.start(javaPath, args);
+ }
+}
+
+void LaunchMinecraft::on_state(LoggedProcess::State state)
+{
+ switch(state)
+ {
+ case LoggedProcess::FailedToStart:
+ {
+ //: Error message displayed if instace can't start
+ QString reason = tr("Could not launch minecraft!");
+ emit logLine(reason, MessageLevel::Fatal);
+ emitFailed(reason);
+ return;
+ }
+ case LoggedProcess::Aborted:
+ case LoggedProcess::Crashed:
+
+ {
+ m_parent->setPid(-1);
+ emitFailed("Game crashed.");
+ return;
+ }
+ case LoggedProcess::Finished:
+ {
+ m_parent->setPid(-1);
+ // if the exit code wasn't 0, report this as a crash
+ auto exitCode = m_process.exitCode();
+ if(exitCode != 0)
+ {
+ emitFailed("Game crashed.");
+ return;
+ }
+ //FIXME: make this work again
+ // m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(exitCode));
+ // run post-exit
+ emitSucceeded();
+ break;
+ }
+ case LoggedProcess::Running:
+ emit logLine(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC);
+ m_parent->setPid(m_process.processId());
+ m_parent->instance()->setLastLaunch();
+ // send the launch script to the launcher part
+ m_process.write(m_launchScript.toUtf8());
+ qDebug() << m_launchScript;
+
+ mayProceed = true;
+ emit readyForLaunch();
+ break;
+ default:
+ break;
+ }
+}
+
+void LaunchMinecraft::setWorkingDirectory(const QString &wd)
+{
+ m_process.setWorkingDirectory(wd);
+}
+
+void LaunchMinecraft::proceed()
+{
+ if(mayProceed)
+ {
+ QString launchString("launch\n");
+ m_process.write(launchString.toUtf8());
+ mayProceed = false;
+ }
+}
+
+bool LaunchMinecraft::abort()
+{
+ if(mayProceed)
+ {
+ mayProceed = false;
+ QString launchString("abort\n");
+ m_process.write(launchString.toUtf8());
+ }
+ else
+ {
+ auto state = m_process.state();
+ if (state == LoggedProcess::Running || state == LoggedProcess::Starting)
+ {
+ m_process.kill();
+ }
+ }
+ return true;
+}
diff --git a/api/logic/launch/steps/LaunchMinecraft.h b/api/logic/launch/steps/LaunchMinecraft.h
new file mode 100644
index 00000000..6b9f7919
--- /dev/null
+++ b/api/logic/launch/steps/LaunchMinecraft.h
@@ -0,0 +1,48 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <launch/LoggedProcess.h>
+#include <minecraft/auth/AuthSession.h>
+
+class LaunchMinecraft: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit LaunchMinecraft(LaunchTask *parent);
+ virtual void executeTask();
+ virtual bool abort();
+ virtual void proceed();
+ virtual bool canAbort() const
+ {
+ return true;
+ }
+ void setWorkingDirectory(const QString &wd);
+ void setAuthSession(AuthSessionPtr session)
+ {
+ m_session = session;
+ }
+private slots:
+ void on_state(LoggedProcess::State state);
+
+private:
+ LoggedProcess m_process;
+ QString m_command;
+ QString m_launchScript;
+ AuthSessionPtr m_session;
+ bool mayProceed = false;
+};
diff --git a/api/logic/launch/steps/ModMinecraftJar.cpp b/api/logic/launch/steps/ModMinecraftJar.cpp
new file mode 100644
index 00000000..fce2d70a
--- /dev/null
+++ b/api/logic/launch/steps/ModMinecraftJar.cpp
@@ -0,0 +1,44 @@
+/* Copyright 2013-2015 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 "ModMinecraftJar.h"
+#include <launch/LaunchTask.h>
+#include <QStandardPaths>
+
+void ModMinecraftJar::executeTask()
+{
+ m_jarModTask = m_parent->instance()->createJarModdingTask();
+ if(m_jarModTask)
+ {
+ connect(m_jarModTask.get(), SIGNAL(finished()), this, SLOT(jarModdingFinished()));
+ m_jarModTask->start();
+ return;
+ }
+ emitSucceeded();
+}
+
+void ModMinecraftJar::jarModdingFinished()
+{
+ if(m_jarModTask->successful())
+ {
+ emitSucceeded();
+ }
+ else
+ {
+ QString reason = tr("jar modding failed because: %1.\n\n").arg(m_jarModTask->failReason());
+ emit logLine(reason, MessageLevel::Fatal);
+ emitFailed(reason);
+ }
+}
diff --git a/api/logic/launch/steps/ModMinecraftJar.h b/api/logic/launch/steps/ModMinecraftJar.h
new file mode 100644
index 00000000..b35dfafa
--- /dev/null
+++ b/api/logic/launch/steps/ModMinecraftJar.h
@@ -0,0 +1,39 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <memory>
+
+// FIXME: temporary wrapper for existing task.
+class ModMinecraftJar: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit ModMinecraftJar(LaunchTask *parent) : LaunchStep(parent) {};
+ virtual ~ModMinecraftJar(){};
+
+ virtual void executeTask();
+ virtual bool canAbort() const
+ {
+ return false;
+ }
+private slots:
+ void jarModdingFinished();
+
+private:
+ std::shared_ptr<Task> m_jarModTask;
+};
diff --git a/api/logic/launch/steps/PostLaunchCommand.cpp b/api/logic/launch/steps/PostLaunchCommand.cpp
new file mode 100644
index 00000000..29a45f1b
--- /dev/null
+++ b/api/logic/launch/steps/PostLaunchCommand.cpp
@@ -0,0 +1,84 @@
+/* Copyright 2013-2015 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 "PostLaunchCommand.h"
+#include <launch/LaunchTask.h>
+
+PostLaunchCommand::PostLaunchCommand(LaunchTask *parent) : LaunchStep(parent)
+{
+ auto instance = m_parent->instance();
+ m_command = instance->getPostExitCommand();
+ m_process.setProcessEnvironment(instance->createEnvironment());
+ connect(&m_process, &LoggedProcess::log, this, &PostLaunchCommand::logLines);
+ connect(&m_process, &LoggedProcess::stateChanged, this, &PostLaunchCommand::on_state);
+}
+
+void PostLaunchCommand::executeTask()
+{
+ QString postlaunch_cmd = m_parent->substituteVariables(m_command);
+ emit logLine(tr("Running Post-Launch command: %1").arg(postlaunch_cmd), MessageLevel::MultiMC);
+ m_process.start(postlaunch_cmd);
+}
+
+void PostLaunchCommand::on_state(LoggedProcess::State state)
+{
+ auto getError = [&]()
+ {
+ return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode());
+ };
+ switch(state)
+ {
+ case LoggedProcess::Aborted:
+ case LoggedProcess::Crashed:
+ case LoggedProcess::FailedToStart:
+ {
+ auto error = getError();
+ emit logLine(error, MessageLevel::Fatal);
+ emitFailed(error);
+ return;
+ }
+ case LoggedProcess::Finished:
+ {
+ if(m_process.exitCode() != 0)
+ {
+ auto error = getError();
+ emit logLine(error, MessageLevel::Fatal);
+ emitFailed(error);
+ }
+ else
+ {
+ emit logLine(tr("Post-Launch command ran successfully.\n\n"), MessageLevel::MultiMC);
+ emitSucceeded();
+ }
+ }
+ default:
+ break;
+ }
+}
+
+void PostLaunchCommand::setWorkingDirectory(const QString &wd)
+{
+ m_process.setWorkingDirectory(wd);
+}
+
+bool PostLaunchCommand::abort()
+{
+ auto state = m_process.state();
+ if (state == LoggedProcess::Running || state == LoggedProcess::Starting)
+ {
+ m_process.kill();
+ }
+ return true;
+}
diff --git a/api/logic/launch/steps/PostLaunchCommand.h b/api/logic/launch/steps/PostLaunchCommand.h
new file mode 100644
index 00000000..4d5b0a52
--- /dev/null
+++ b/api/logic/launch/steps/PostLaunchCommand.h
@@ -0,0 +1,39 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <launch/LoggedProcess.h>
+
+class PostLaunchCommand: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit PostLaunchCommand(LaunchTask *parent);
+ virtual void executeTask();
+ virtual bool abort();
+ virtual bool canAbort() const
+ {
+ return true;
+ }
+ void setWorkingDirectory(const QString &wd);
+private slots:
+ void on_state(LoggedProcess::State state);
+
+private:
+ LoggedProcess m_process;
+ QString m_command;
+};
diff --git a/api/logic/launch/steps/PreLaunchCommand.cpp b/api/logic/launch/steps/PreLaunchCommand.cpp
new file mode 100644
index 00000000..47197a82
--- /dev/null
+++ b/api/logic/launch/steps/PreLaunchCommand.cpp
@@ -0,0 +1,85 @@
+/* Copyright 2013-2015 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 "PreLaunchCommand.h"
+#include <launch/LaunchTask.h>
+
+PreLaunchCommand::PreLaunchCommand(LaunchTask *parent) : LaunchStep(parent)
+{
+ auto instance = m_parent->instance();
+ m_command = instance->getPreLaunchCommand();
+ m_process.setProcessEnvironment(instance->createEnvironment());
+ connect(&m_process, &LoggedProcess::log, this, &PreLaunchCommand::logLines);
+ connect(&m_process, &LoggedProcess::stateChanged, this, &PreLaunchCommand::on_state);
+}
+
+void PreLaunchCommand::executeTask()
+{
+ //FIXME: where to put this?
+ QString prelaunch_cmd = m_parent->substituteVariables(m_command);
+ emit logLine(tr("Running Pre-Launch command: %1").arg(prelaunch_cmd), MessageLevel::MultiMC);
+ m_process.start(prelaunch_cmd);
+}
+
+void PreLaunchCommand::on_state(LoggedProcess::State state)
+{
+ auto getError = [&]()
+ {
+ return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode());
+ };
+ switch(state)
+ {
+ case LoggedProcess::Aborted:
+ case LoggedProcess::Crashed:
+ case LoggedProcess::FailedToStart:
+ {
+ auto error = getError();
+ emit logLine(error, MessageLevel::Fatal);
+ emitFailed(error);
+ return;
+ }
+ case LoggedProcess::Finished:
+ {
+ if(m_process.exitCode() != 0)
+ {
+ auto error = getError();
+ emit logLine(error, MessageLevel::Fatal);
+ emitFailed(error);
+ }
+ else
+ {
+ emit logLine(tr("Pre-Launch command ran successfully.\n\n"), MessageLevel::MultiMC);
+ emitSucceeded();
+ }
+ }
+ default:
+ break;
+ }
+}
+
+void PreLaunchCommand::setWorkingDirectory(const QString &wd)
+{
+ m_process.setWorkingDirectory(wd);
+}
+
+bool PreLaunchCommand::abort()
+{
+ auto state = m_process.state();
+ if (state == LoggedProcess::Running || state == LoggedProcess::Starting)
+ {
+ m_process.kill();
+ }
+ return true;
+}
diff --git a/api/logic/launch/steps/PreLaunchCommand.h b/api/logic/launch/steps/PreLaunchCommand.h
new file mode 100644
index 00000000..077bdfca
--- /dev/null
+++ b/api/logic/launch/steps/PreLaunchCommand.h
@@ -0,0 +1,39 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <launch/LoggedProcess.h>
+
+class PreLaunchCommand: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit PreLaunchCommand(LaunchTask *parent);
+ virtual void executeTask();
+ virtual bool abort();
+ virtual bool canAbort() const
+ {
+ return true;
+ }
+ void setWorkingDirectory(const QString &wd);
+private slots:
+ void on_state(LoggedProcess::State state);
+
+private:
+ LoggedProcess m_process;
+ QString m_command;
+};
diff --git a/api/logic/launch/steps/TextPrint.cpp b/api/logic/launch/steps/TextPrint.cpp
new file mode 100644
index 00000000..f307b1fd
--- /dev/null
+++ b/api/logic/launch/steps/TextPrint.cpp
@@ -0,0 +1,29 @@
+#include "TextPrint.h"
+
+TextPrint::TextPrint(LaunchTask * parent, const QStringList &lines, MessageLevel::Enum level) : LaunchStep(parent)
+{
+ m_lines = lines;
+ m_level = level;
+}
+TextPrint::TextPrint(LaunchTask *parent, const QString &line, MessageLevel::Enum level) : LaunchStep(parent)
+{
+ m_lines.append(line);
+ m_level = level;
+}
+
+void TextPrint::executeTask()
+{
+ emit logLines(m_lines, m_level);
+ emitSucceeded();
+}
+
+bool TextPrint::canAbort() const
+{
+ return true;
+}
+
+bool TextPrint::abort()
+{
+ emitFailed("Aborted.");
+ return true;
+}
diff --git a/api/logic/launch/steps/TextPrint.h b/api/logic/launch/steps/TextPrint.h
new file mode 100644
index 00000000..fdd9014a
--- /dev/null
+++ b/api/logic/launch/steps/TextPrint.h
@@ -0,0 +1,43 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <launch/LoggedProcess.h>
+#include <java/JavaChecker.h>
+
+#include "multimc_logic_export.h"
+
+/*
+ * FIXME: maybe do not export
+ */
+
+class MULTIMC_LOGIC_EXPORT TextPrint: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit TextPrint(LaunchTask *parent, const QStringList &lines, MessageLevel::Enum level);
+ explicit TextPrint(LaunchTask *parent, const QString &line, MessageLevel::Enum level);
+ virtual ~TextPrint(){};
+
+ virtual void executeTask();
+ virtual bool canAbort() const;
+ virtual bool abort();
+
+private:
+ QStringList m_lines;
+ MessageLevel::Enum m_level;
+};
diff --git a/api/logic/launch/steps/Update.cpp b/api/logic/launch/steps/Update.cpp
new file mode 100644
index 00000000..4901f001
--- /dev/null
+++ b/api/logic/launch/steps/Update.cpp
@@ -0,0 +1,50 @@
+/* Copyright 2013-2015 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 "Update.h"
+#include <launch/LaunchTask.h>
+
+void Update::executeTask()
+{
+ m_updateTask = m_parent->instance()->createUpdateTask();
+ if(m_updateTask)
+ {
+ connect(m_updateTask.get(), SIGNAL(finished()), this, SLOT(updateFinished()));
+ connect(m_updateTask.get(), &Task::progress, this, &Task::setProgress);
+ connect(m_updateTask.get(), &Task::status, this, &Task::setStatus);
+ emit progressReportingRequest();
+ return;
+ }
+ emitSucceeded();
+}
+
+void Update::proceed()
+{
+ m_updateTask->start();
+}
+
+void Update::updateFinished()
+{
+ if(m_updateTask->successful())
+ {
+ emitSucceeded();
+ }
+ else
+ {
+ QString reason = tr("Instance update failed because: %1.\n\n").arg(m_updateTask->failReason());
+ emit logLine(reason, MessageLevel::Fatal);
+ emitFailed(reason);
+ }
+}
diff --git a/api/logic/launch/steps/Update.h b/api/logic/launch/steps/Update.h
new file mode 100644
index 00000000..14928253
--- /dev/null
+++ b/api/logic/launch/steps/Update.h
@@ -0,0 +1,41 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <launch/LoggedProcess.h>
+#include <java/JavaChecker.h>
+
+// FIXME: stupid. should be defined by the instance type? or even completely abstracted away...
+class Update: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit Update(LaunchTask *parent):LaunchStep(parent) {};
+ virtual ~Update() {};
+
+ virtual void executeTask();
+ virtual bool canAbort() const
+ {
+ return false;
+ }
+ virtual void proceed();
+private slots:
+ void updateFinished();
+
+private:
+ std::shared_ptr<Task> m_updateTask;
+};
diff --git a/api/logic/minecraft/AssetsUtils.cpp b/api/logic/minecraft/AssetsUtils.cpp
new file mode 100644
index 00000000..7a525abe
--- /dev/null
+++ b/api/logic/minecraft/AssetsUtils.cpp
@@ -0,0 +1,230 @@
+/* Copyright 2013-2015 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 <QFileInfo>
+#include <QDir>
+#include <QDirIterator>
+#include <QCryptographicHash>
+#include <QJsonParseError>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QVariant>
+#include <QDebug>
+
+#include "AssetsUtils.h"
+#include "FileSystem.h"
+#include "net/MD5EtagDownload.h"
+
+namespace AssetsUtils
+{
+
+/*
+ * Returns true on success, with index populated
+ * index is undefined otherwise
+ */
+bool loadAssetsIndexJson(QString assetsId, 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))
+ {
+ qCritical() << "Failed to read assets index file" << path;
+ return false;
+ }
+ index->id = assetsId;
+
+ // 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)
+ {
+ qCritical() << "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())
+ {
+ qCritical() << "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)
+ {
+ // qDebug() << 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)
+ {
+ // qDebug() << 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;
+}
+
+QDir reconstructAssets(QString assetsId)
+{
+ QDir assetsDir = QDir("assets/");
+ QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes"));
+ QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects"));
+ QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual"));
+
+ QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json");
+ QFile indexFile(indexPath);
+ QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId));
+
+ if (!indexFile.exists())
+ {
+ qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets";
+ return virtualRoot;
+ }
+
+ qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path()
+ << objectDir.path() << virtualDir.path() << virtualRoot.path();
+
+ AssetsIndex index;
+ bool loadAssetsIndex = AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, &index);
+
+ if (loadAssetsIndex && index.isVirtual)
+ {
+ qDebug() << "Reconstructing virtual assets folder at" << virtualRoot.path();
+
+ for (QString map : index.objects.keys())
+ {
+ AssetObject asset_object = index.objects.value(map);
+ QString target_path = FS::PathCombine(virtualRoot.path(), map);
+ QFile target(target_path);
+
+ QString tlk = asset_object.hash.left(2);
+
+ QString original_path = FS::PathCombine(objectDir.path(), tlk, asset_object.hash);
+ QFile original(original_path);
+ if (!original.exists())
+ continue;
+ if (!target.exists())
+ {
+ QFileInfo info(target_path);
+ QDir target_dir = info.dir();
+ // qDebug() << target_dir;
+ if (!target_dir.exists())
+ QDir("").mkpath(target_dir.path());
+
+ bool couldCopy = original.copy(target_path);
+ qDebug() << " Copying" << original_path << "to" << target_path
+ << QString::number(couldCopy); // << original.errorString();
+ }
+ }
+
+ // TODO: Write last used time to virtualRoot/.lastused
+ }
+
+ return virtualRoot;
+}
+
+}
+
+NetActionPtr AssetObject::getDownloadAction()
+{
+ QFileInfo objectFile(getLocalPath());
+ if ((!objectFile.isFile()) || (objectFile.size() != size))
+ {
+ auto objectDL = MD5EtagDownload::make(getUrl(), objectFile.filePath());
+ objectDL->m_total_progress = size;
+ return objectDL;
+ }
+ return nullptr;
+}
+
+QString AssetObject::getLocalPath()
+{
+ return "assets/objects/" + getRelPath();
+}
+
+QUrl AssetObject::getUrl()
+{
+ return QUrl("http://resources.download.minecraft.net/" + getRelPath());
+}
+
+QString AssetObject::getRelPath()
+{
+ return hash.left(2) + "/" + hash;
+}
+
+NetJobPtr AssetsIndex::getDownloadJob()
+{
+ auto job = new NetJob(QObject::tr("Assets for %1").arg(id));
+ for (auto &object : objects.values())
+ {
+ auto dl = object.getDownloadAction();
+ if(dl)
+ {
+ job->addNetAction(dl);
+ }
+ }
+ if(job->size())
+ return job;
+ return nullptr;
+}
diff --git a/api/logic/minecraft/AssetsUtils.h b/api/logic/minecraft/AssetsUtils.h
new file mode 100644
index 00000000..90251c2d
--- /dev/null
+++ b/api/logic/minecraft/AssetsUtils.h
@@ -0,0 +1,48 @@
+/* Copyright 2013-2015 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>
+#include <QMap>
+#include "net/NetAction.h"
+#include "net/NetJob.h"
+
+struct AssetObject
+{
+ QString getRelPath();
+ QUrl getUrl();
+ QString getLocalPath();
+ NetActionPtr getDownloadAction();
+
+ QString hash;
+ qint64 size;
+};
+
+struct AssetsIndex
+{
+ NetJobPtr getDownloadJob();
+
+ QString id;
+ QMap<QString, AssetObject> objects;
+ bool isVirtual = false;
+};
+
+namespace AssetsUtils
+{
+bool loadAssetsIndexJson(QString id, QString file, AssetsIndex* index);
+/// Reconstruct a virtual assets folder for the given assets ID and return the folder
+QDir reconstructAssets(QString assetsId);
+}
diff --git a/api/logic/minecraft/GradleSpecifier.h b/api/logic/minecraft/GradleSpecifier.h
new file mode 100644
index 00000000..18308537
--- /dev/null
+++ b/api/logic/minecraft/GradleSpecifier.h
@@ -0,0 +1,129 @@
+#pragma once
+
+#include <QString>
+#include <QStringList>
+#include "DefaultVariable.h"
+
+struct GradleSpecifier
+{
+ GradleSpecifier()
+ {
+ m_valid = false;
+ }
+ GradleSpecifier(QString value)
+ {
+ operator=(value);
+ }
+ GradleSpecifier & operator =(const QString & value)
+ {
+ /*
+ org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar
+ DEBUG 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar"
+ DEBUG 1 "org.gradle.test.classifiers"
+ DEBUG 2 "service"
+ DEBUG 3 "1.0"
+ DEBUG 4 ":jdk15"
+ DEBUG 5 "jdk15"
+ DEBUG 6 "@jar"
+ DEBUG 7 "jar"
+ */
+ QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(:([^:@]+))?" "(@([^:@]+))?");
+ m_valid = matcher.exactMatch(value);
+ auto elements = matcher.capturedTexts();
+ m_groupId = elements[1];
+ m_artifactId = elements[2];
+ m_version = elements[3];
+ m_classifier = elements[5];
+ if(!elements[7].isEmpty())
+ {
+ m_extension = elements[7];
+ }
+ return *this;
+ }
+ operator QString() const
+ {
+ if(!m_valid)
+ return "INVALID";
+ QString retval = m_groupId + ":" + m_artifactId + ":" + m_version;
+ if(!m_classifier.isEmpty())
+ {
+ retval += ":" + m_classifier;
+ }
+ if(m_extension.isExplicit())
+ {
+ retval += "@" + m_extension;
+ }
+ return retval;
+ }
+ QString toPath() const
+ {
+ if(!m_valid)
+ return "INVALID";
+ QString path = m_groupId;
+ path.replace('.', '/');
+ path += '/' + m_artifactId + '/' + m_version + '/' + m_artifactId + '-' + m_version;
+ if(!m_classifier.isEmpty())
+ {
+ path += "-" + m_classifier;
+ }
+ path += "." + m_extension;
+ return path;
+ }
+ inline bool valid() const
+ {
+ return m_valid;
+ }
+ inline QString version() const
+ {
+ return m_version;
+ }
+ inline QString groupId() const
+ {
+ return m_groupId;
+ }
+ inline QString artifactId() const
+ {
+ return m_artifactId;
+ }
+ inline void setClassifier(const QString & classifier)
+ {
+ m_classifier = classifier;
+ }
+ inline QString classifier() const
+ {
+ return m_classifier;
+ }
+ inline QString extension() const
+ {
+ return m_extension;
+ }
+ inline QString artifactPrefix() const
+ {
+ return m_groupId + ":" + m_artifactId;
+ }
+ bool matchName(const GradleSpecifier & other) const
+ {
+ return other.artifactId() == artifactId() && other.groupId() == groupId();
+ }
+ bool operator==(const GradleSpecifier & other) const
+ {
+ if(m_groupId != other.m_groupId)
+ return false;
+ if(m_artifactId != other.m_artifactId)
+ return false;
+ if(m_version != other.m_version)
+ return false;
+ if(m_classifier != other.m_classifier)
+ return false;
+ if(m_extension != other.m_extension)
+ return false;
+ return true;
+ }
+private:
+ QString m_groupId;
+ QString m_artifactId;
+ QString m_version;
+ QString m_classifier;
+ DefaultVariable<QString> m_extension = DefaultVariable<QString>("jar");
+ bool m_valid = false;
+};
diff --git a/api/logic/minecraft/JarMod.h b/api/logic/minecraft/JarMod.h
new file mode 100644
index 00000000..42d05da9
--- /dev/null
+++ b/api/logic/minecraft/JarMod.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <QString>
+#include <QJsonObject>
+#include <memory>
+class Jarmod;
+typedef std::shared_ptr<Jarmod> JarmodPtr;
+class Jarmod
+{
+public: /* data */
+ QString name;
+ QString originalName;
+};
diff --git a/api/logic/minecraft/Library.cpp b/api/logic/minecraft/Library.cpp
new file mode 100644
index 00000000..922db84e
--- /dev/null
+++ b/api/logic/minecraft/Library.cpp
@@ -0,0 +1,239 @@
+#include "Library.h"
+#include <net/CacheDownload.h>
+#include <minecraft/forge/ForgeXzDownload.h>
+#include <Env.h>
+#include <FileSystem.h>
+
+void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& native, QStringList& native32, QStringList& native64) const
+{
+ auto actualPath = [&](QString relPath)
+ {
+ QFileInfo out(FS::PathCombine(storagePrefix(), relPath));
+ return out.absoluteFilePath();
+ };
+ if(m_mojangDownloads)
+ {
+ if(m_mojangDownloads->artifact)
+ {
+ auto artifact = m_mojangDownloads->artifact;
+ jar += actualPath(artifact->path);
+ }
+ if(!isNative())
+ return;
+ if(m_nativeClassifiers.contains(system))
+ {
+ auto nativeClassifier = m_nativeClassifiers[system];
+ if(nativeClassifier.contains("${arch}"))
+ {
+ auto nat32Classifier = nativeClassifier;
+ nat32Classifier.replace("${arch}", "32");
+ auto nat64Classifier = nativeClassifier;
+ nat64Classifier.replace("${arch}", "64");
+ auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier);
+ if(nat32info)
+ native32 += actualPath(nat32info->path);
+ auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier);
+ if(nat64info)
+ native64 += actualPath(nat64info->path);
+ }
+ else
+ {
+ native += actualPath(m_mojangDownloads->getDownloadInfo(nativeClassifier)->path);
+ }
+ }
+ }
+ else
+ {
+ QString raw_storage = storageSuffix(system);
+ if(isNative())
+ {
+ if (raw_storage.contains("${arch}"))
+ {
+ auto nat32Storage = raw_storage;
+ nat32Storage.replace("${arch}", "32");
+ auto nat64Storage = raw_storage;
+ nat64Storage.replace("${arch}", "64");
+ native32 += actualPath(nat32Storage);
+ native64 += actualPath(nat64Storage);
+ }
+ else
+ {
+ native += actualPath(raw_storage);
+ }
+ }
+ else
+ {
+ jar += actualPath(raw_storage);
+ }
+ }
+}
+
+QList<NetActionPtr> Library::getDownloads(OpSys system, HttpMetaCache * cache, QStringList &failedFiles) const
+{
+ QList<NetActionPtr> out;
+ bool isLocal = (hint() == "local");
+ bool isForge = (hint() == "forge-pack-xz");
+
+ auto add_download = [&](QString storage, QString dl)
+ {
+ auto entry = cache->resolveEntry("libraries", storage);
+ if (!entry->isStale())
+ return true;
+ if(isLocal)
+ {
+ QFileInfo fileinfo(entry->getFullPath());
+ if(!fileinfo.exists())
+ {
+ failedFiles.append(entry->getFullPath());
+ return false;
+ }
+ return true;
+ }
+ if (isForge)
+ {
+ out.append(ForgeXzDownload::make(storage, entry));
+ }
+ else
+ {
+ out.append(CacheDownload::make(dl, entry));
+ }
+ return true;
+ };
+
+ if(m_mojangDownloads)
+ {
+ if(m_mojangDownloads->artifact)
+ {
+ auto artifact = m_mojangDownloads->artifact;
+ add_download(artifact->path, artifact->url);
+ }
+ if(m_nativeClassifiers.contains(system))
+ {
+ auto nativeClassifier = m_nativeClassifiers[system];
+ if(nativeClassifier.contains("${arch}"))
+ {
+ auto nat32Classifier = nativeClassifier;
+ nat32Classifier.replace("${arch}", "32");
+ auto nat64Classifier = nativeClassifier;
+ nat64Classifier.replace("${arch}", "64");
+ auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier);
+ if(nat32info)
+ add_download(nat32info->path, nat32info->url);
+ auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier);
+ if(nat64info)
+ add_download(nat64info->path, nat64info->url);
+ }
+ else
+ {
+ auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier);
+ if(info)
+ {
+ add_download(info->path, info->url);
+ }
+ }
+ }
+ }
+ else
+ {
+ QString raw_storage = storageSuffix(system);
+ auto raw_dl = [&](){
+ if (!m_absoluteURL.isEmpty())
+ {
+ return m_absoluteURL;
+ }
+
+ if (m_repositoryURL.isEmpty())
+ {
+ return QString("https://" + URLConstants::LIBRARY_BASE) + raw_storage;
+ }
+
+ if(m_repositoryURL.endsWith('/'))
+ {
+ return m_repositoryURL + raw_storage;
+ }
+ else
+ {
+ return m_repositoryURL + QChar('/') + raw_storage;
+ }
+ }();
+ if (raw_storage.contains("${arch}"))
+ {
+ QString cooked_storage = raw_storage;
+ QString cooked_dl = raw_dl;
+ add_download(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32"));
+ cooked_storage = raw_storage;
+ cooked_dl = raw_dl;
+ add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64"));
+ }
+ else
+ {
+ add_download(raw_storage, raw_dl);
+ }
+ }
+ return out;
+}
+
+bool Library::isActive() const
+{
+ bool result = true;
+ if (m_rules.empty())
+ {
+ result = true;
+ }
+ else
+ {
+ RuleAction ruleResult = Disallow;
+ for (auto rule : m_rules)
+ {
+ RuleAction temp = rule->apply(this);
+ if (temp != Defer)
+ ruleResult = temp;
+ }
+ result = result && (ruleResult == Allow);
+ }
+ if (isNative())
+ {
+ result = result && m_nativeClassifiers.contains(currentSystem);
+ }
+ return result;
+}
+
+void Library::setStoragePrefix(QString prefix)
+{
+ m_storagePrefix = prefix;
+}
+
+QString Library::defaultStoragePrefix()
+{
+ return "libraries/";
+}
+
+QString Library::storagePrefix() const
+{
+ if(m_storagePrefix.isEmpty())
+ {
+ return defaultStoragePrefix();
+ }
+ return m_storagePrefix;
+}
+
+QString Library::storageSuffix(OpSys system) const
+{
+ // non-native? use only the gradle specifier
+ if (!isNative())
+ {
+ return m_name.toPath();
+ }
+
+ // otherwise native, override classifiers. Mojang HACK!
+ GradleSpecifier nativeSpec = m_name;
+ if (m_nativeClassifiers.contains(system))
+ {
+ nativeSpec.setClassifier(m_nativeClassifiers[system]);
+ }
+ else
+ {
+ nativeSpec.setClassifier("INVALID");
+ }
+ return nativeSpec.toPath();
+}
diff --git a/api/logic/minecraft/Library.h b/api/logic/minecraft/Library.h
new file mode 100644
index 00000000..fdce93f3
--- /dev/null
+++ b/api/logic/minecraft/Library.h
@@ -0,0 +1,184 @@
+#pragma once
+#include <QString>
+#include <net/NetAction.h>
+#include <QPair>
+#include <QList>
+#include <QStringList>
+#include <QMap>
+#include <QDir>
+#include <QUrl>
+#include <memory>
+
+#include "Rule.h"
+#include "minecraft/OpSys.h"
+#include "GradleSpecifier.h"
+#include "net/URLConstants.h"
+#include "MojangDownloadInfo.h"
+
+#include "multimc_logic_export.h"
+
+class Library;
+
+typedef std::shared_ptr<Library> LibraryPtr;
+
+class MULTIMC_LOGIC_EXPORT Library
+{
+ friend class OneSixVersionFormat;
+ friend class MojangVersionFormat;
+ friend class LibraryTest;
+public:
+ Library()
+ {
+ }
+ Library(const QString &name)
+ {
+ m_name = name;
+ }
+ /// limited copy without some data. TODO: why?
+ static LibraryPtr limitedCopy(LibraryPtr base)
+ {
+ auto newlib = std::make_shared<Library>();
+ newlib->m_name = base->m_name;
+ newlib->m_repositoryURL = base->m_repositoryURL;
+ newlib->m_hint = base->m_hint;
+ newlib->m_absoluteURL = base->m_absoluteURL;
+ newlib->m_extractExcludes = base->m_extractExcludes;
+ newlib->m_nativeClassifiers = base->m_nativeClassifiers;
+ newlib->m_rules = base->m_rules;
+ newlib->m_storagePrefix = base->m_storagePrefix;
+ newlib->m_mojangDownloads = base->m_mojangDownloads;
+ return newlib;
+ }
+
+public: /* methods */
+ /// Returns the raw name field
+ const GradleSpecifier & rawName() const
+ {
+ return m_name;
+ }
+
+ void setRawName(const GradleSpecifier & spec)
+ {
+ m_name = spec;
+ }
+
+ void setClassifier(const QString & spec)
+ {
+ m_name.setClassifier(spec);
+ }
+
+ /// returns the full group and artifact prefix
+ QString artifactPrefix() const
+ {
+ return m_name.artifactPrefix();
+ }
+
+ /// get the artifact ID
+ QString artifactId() const
+ {
+ return m_name.artifactId();
+ }
+
+ /// get the artifact version
+ QString version() const
+ {
+ return m_name.version();
+ }
+
+ /// Returns true if the library is native
+ bool isNative() const
+ {
+ return m_nativeClassifiers.size() != 0;
+ }
+
+ void setStoragePrefix(QString prefix = QString());
+
+ /// Set the url base for downloads
+ void setRepositoryURL(const QString &base_url)
+ {
+ m_repositoryURL = base_url;
+ }
+
+ void getApplicableFiles(OpSys system, QStringList & jar, QStringList & native, QStringList & native32, QStringList & native64) const;
+
+ void setAbsoluteUrl(const QString &absolute_url)
+ {
+ m_absoluteURL = absolute_url;
+ }
+
+ void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info)
+ {
+ m_mojangDownloads = info;
+ }
+
+ void setHint(const QString &hint)
+ {
+ m_hint = hint;
+ }
+
+ /// Set the load rules
+ void setRules(QList<std::shared_ptr<Rule>> rules)
+ {
+ m_rules = rules;
+ }
+
+ /// Returns true if the library should be loaded (or extracted, in case of natives)
+ bool isActive() const;
+
+ // Get a list of downloads for this library
+ QList<NetActionPtr> getDownloads(OpSys system, class HttpMetaCache * cache, QStringList &failedFiles) const;
+
+private: /* methods */
+ /// the default storage prefix used by MultiMC
+ static QString defaultStoragePrefix();
+
+ /// Get the prefix - root of the storage to be used
+ QString storagePrefix() const;
+
+ /// Get the relative path where the library should be saved
+ QString storageSuffix(OpSys system) const;
+
+ QString hint() const
+ {
+ return m_hint;
+ }
+
+protected: /* data */
+ /// the basic gradle dependency specifier.
+ GradleSpecifier m_name;
+
+ /// DEPRECATED URL prefix of the maven repo where the file can be downloaded
+ QString m_repositoryURL;
+
+ /// DEPRECATED: MultiMC-specific absolute URL. takes precedence over the implicit maven repo URL, if defined
+ QString m_absoluteURL;
+
+ /**
+ * MultiMC-specific type hint - modifies how the library is treated
+ */
+ QString m_hint;
+
+ /**
+ * storage - by default the local libraries folder in multimc, but could be elsewhere
+ * MultiMC specific, because of FTB.
+ */
+ QString m_storagePrefix;
+
+ /// true if the library had an extract/excludes section (even empty)
+ bool m_hasExcludes = false;
+
+ /// a list of files that shouldn't be extracted from the library
+ QStringList m_extractExcludes;
+
+ /// native suffixes per OS
+ QMap<OpSys, QString> m_nativeClassifiers;
+
+ /// true if the library had a rules section (even empty)
+ bool applyRules = false;
+
+ /// rules associated with the library
+ QList<std::shared_ptr<Rule>> m_rules;
+
+ /// MOJANG: container with Mojang style download info
+ MojangLibraryDownloadInfo::Ptr m_mojangDownloads;
+};
diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp
new file mode 100644
index 00000000..405ccd26
--- /dev/null
+++ b/api/logic/minecraft/MinecraftInstance.cpp
@@ -0,0 +1,369 @@
+#include "MinecraftInstance.h"
+#include <settings/Setting.h>
+#include "settings/SettingsObject.h"
+#include "Env.h"
+#include "minecraft/MinecraftVersionList.h"
+#include <MMCStrings.h>
+#include <pathmatcher/RegexpMatcher.h>
+#include <pathmatcher/MultiMatcher.h>
+#include <FileSystem.h>
+#include <java/JavaVersion.h>
+
+#define IBUS "@im=ibus"
+
+// all of this because keeping things compatible with deprecated old settings
+// if either of the settings {a, b} is true, this also resolves to true
+class OrSetting : public Setting
+{
+ Q_OBJECT
+public:
+ OrSetting(QString id, std::shared_ptr<Setting> a, std::shared_ptr<Setting> b)
+ :Setting({id}, false), m_a(a), m_b(b)
+ {
+ }
+ virtual QVariant get() const
+ {
+ bool a = m_a->get().toBool();
+ bool b = m_b->get().toBool();
+ return a || b;
+ }
+ virtual void reset() {}
+ virtual void set(QVariant value) {}
+private:
+ std::shared_ptr<Setting> m_a;
+ std::shared_ptr<Setting> m_b;
+};
+
+MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
+ : BaseInstance(globalSettings, settings, rootDir)
+{
+ // Java Settings
+ auto javaOverride = m_settings->registerSetting("OverrideJava", false);
+ auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false);
+ auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false);
+
+ // combinations
+ auto javaOrLocation = std::make_shared<OrSetting>("JavaOrLocationOverride", javaOverride, locationOverride);
+ auto javaOrArgs = std::make_shared<OrSetting>("JavaOrArgsOverride", javaOverride, argsOverride);
+
+ m_settings->registerOverride(globalSettings->getSetting("JavaPath"), javaOrLocation);
+ m_settings->registerOverride(globalSettings->getSetting("JvmArgs"), javaOrArgs);
+
+ // special!
+ m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"), javaOrLocation);
+ m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"), javaOrLocation);
+
+ // Window Size
+ auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
+ m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"), windowSetting);
+ m_settings->registerOverride(globalSettings->getSetting("MinecraftWinWidth"), windowSetting);
+ m_settings->registerOverride(globalSettings->getSetting("MinecraftWinHeight"), windowSetting);
+
+ // Memory
+ auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
+ m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"), memorySetting);
+ m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"), memorySetting);
+ m_settings->registerOverride(globalSettings->getSetting("PermGen"), memorySetting);
+}
+
+QString MinecraftInstance::minecraftRoot() const
+{
+ QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
+ QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
+
+ if (dotMCDir.exists() && !mcDir.exists())
+ return dotMCDir.filePath();
+ else
+ return mcDir.filePath();
+}
+
+std::shared_ptr< BaseVersionList > MinecraftInstance::versionList() const
+{
+ return ENV.getVersionList("net.minecraft");
+}
+
+QStringList MinecraftInstance::javaArguments() const
+{
+ QStringList args;
+
+ // custom args go first. we want to override them if we have our own here.
+ args.append(extraArguments());
+
+ // OSX dock icon and name
+#ifdef Q_OS_MAC
+ args << "-Xdock:icon=icon.png";
+ args << QString("-Xdock:name=\"%1\"").arg(windowTitle());
+#endif
+
+ // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767
+#ifdef Q_OS_WIN32
+ args << QString("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_"
+ "minecraft.exe.heapdump");
+#endif
+
+ args << QString("-Xms%1m").arg(settings()->get("MinMemAlloc").toInt());
+ args << QString("-Xmx%1m").arg(settings()->get("MaxMemAlloc").toInt());
+
+ // No PermGen in newer java.
+ JavaVersion javaVersion(settings()->get("JavaVersion").toString());
+ if(javaVersion.requiresPermGen())
+ {
+ auto permgen = settings()->get("PermGen").toInt();
+ if (permgen != 64)
+ {
+ args << QString("-XX:PermSize=%1m").arg(permgen);
+ }
+ }
+
+ args << "-Duser.language=en";
+ args << "-jar" << FS::PathCombine(QCoreApplication::applicationDirPath(), "jars", "NewLaunch.jar");
+
+ return args;
+}
+
+QMap<QString, QString> MinecraftInstance::getVariables() const
+{
+ QMap<QString, QString> out;
+ out.insert("INST_NAME", name());
+ out.insert("INST_ID", id());
+ out.insert("INST_DIR", QDir(instanceRoot()).absolutePath());
+ out.insert("INST_MC_DIR", QDir(minecraftRoot()).absolutePath());
+ out.insert("INST_JAVA", settings()->get("JavaPath").toString());
+ out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
+ return out;
+}
+
+static QString processLD_LIBRARY_PATH(const QString & LD_LIBRARY_PATH)
+{
+ QDir mmcBin(QCoreApplication::applicationDirPath());
+ auto items = LD_LIBRARY_PATH.split(':');
+ QStringList final;
+ for(auto & item: items)
+ {
+ QDir test(item);
+ if(test == mmcBin)
+ {
+ qDebug() << "Env:LD_LIBRARY_PATH ignoring path" << item;
+ continue;
+ }
+ final.append(item);
+ }
+ return final.join(':');
+}
+
+QProcessEnvironment MinecraftInstance::createEnvironment()
+{
+ // prepare the process environment
+ QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment();
+ QProcessEnvironment env;
+
+ QStringList ignored =
+ {
+ "JAVA_ARGS",
+ "CLASSPATH",
+ "CONFIGPATH",
+ "JAVA_HOME",
+ "JRE_HOME",
+ "_JAVA_OPTIONS",
+ "JAVA_OPTIONS",
+ "JAVA_TOOL_OPTIONS"
+ };
+ for(auto key: rawenv.keys())
+ {
+ auto value = rawenv.value(key);
+ // filter out dangerous java crap
+ if(ignored.contains(key))
+ {
+ qDebug() << "Env: ignoring" << key << value;
+ continue;
+ }
+ // filter MultiMC-related things
+ if(key.startsWith("QT_"))
+ {
+ qDebug() << "Env: ignoring" << key << value;
+ continue;
+ }
+#ifdef Q_OS_LINUX
+ // Do not pass LD_* variables to java. They were intended for MultiMC
+ if(key.startsWith("LD_"))
+ {
+ qDebug() << "Env: ignoring" << key << value;
+ continue;
+ }
+ // Strip IBus
+ // IBus is a Linux IME framework. For some reason, it breaks MC?
+ if (key == "XMODIFIERS" && value.contains(IBUS))
+ {
+ QString save = value;
+ value.replace(IBUS, "");
+ qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value;
+ }
+ if(key == "GAME_PRELOAD")
+ {
+ env.insert("LD_PRELOAD", value);
+ continue;
+ }
+ if(key == "GAME_LIBRARY_PATH")
+ {
+ env.insert("LD_LIBRARY_PATH", processLD_LIBRARY_PATH(value));
+ continue;
+ }
+#endif
+ qDebug() << "Env: " << key << value;
+ env.insert(key, value);
+ }
+#ifdef Q_OS_LINUX
+ // HACK: Workaround for QTBUG42500
+ if(!env.contains("LD_LIBRARY_PATH"))
+ {
+ env.insert("LD_LIBRARY_PATH", "");
+ }
+#endif
+
+ // export some infos
+ auto variables = getVariables();
+ for (auto it = variables.begin(); it != variables.end(); ++it)
+ {
+ env.insert(it.key(), it.value());
+ }
+ return env;
+}
+
+QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSessionPtr session)
+{
+ if(!session)
+ {
+ return QMap<QString, QString>();
+ }
+ auto & sessionRef = *session.get();
+ QMap<QString, QString> filter;
+ auto addToFilter = [&filter](QString key, QString value)
+ {
+ if(key.trimmed().size())
+ {
+ filter[key] = value;
+ }
+ };
+ if (sessionRef.session != "-")
+ {
+ addToFilter(sessionRef.session, tr("<SESSION ID>"));
+ }
+ addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
+ addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>"));
+ addToFilter(sessionRef.uuid, tr("<PROFILE ID>"));
+ addToFilter(sessionRef.player_name, tr("<PROFILE NAME>"));
+
+ auto i = sessionRef.u.properties.begin();
+ while (i != sessionRef.u.properties.end())
+ {
+ addToFilter(i.value(), "<" + i.key().toUpper() + ">");
+ ++i;
+ }
+ return filter;
+}
+
+MessageLevel::Enum MinecraftInstance::guessLevel(const QString &line, MessageLevel::Enum level)
+{
+ QRegularExpression re("\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]");
+ auto match = re.match(line);
+ if(match.hasMatch())
+ {
+ // New style logs from log4j
+ QString timestamp = match.captured("timestamp");
+ QString levelStr = match.captured("level");
+ if(levelStr == "INFO")
+ level = MessageLevel::Message;
+ if(levelStr == "WARN")
+ level = MessageLevel::Warning;
+ if(levelStr == "ERROR")
+ level = MessageLevel::Error;
+ if(levelStr == "FATAL")
+ level = MessageLevel::Fatal;
+ if(levelStr == "TRACE" || levelStr == "DEBUG")
+ level = MessageLevel::Debug;
+ }
+ else
+ {
+ // Old style forge logs
+ if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") ||
+ line.contains("[FINER]") || line.contains("[FINEST]"))
+ level = MessageLevel::Message;
+ if (line.contains("[SEVERE]") || line.contains("[STDERR]"))
+ level = MessageLevel::Error;
+ if (line.contains("[WARNING]"))
+ level = MessageLevel::Warning;
+ if (line.contains("[DEBUG]"))
+ level = MessageLevel::Debug;
+ }
+ if (line.contains("overwriting existing"))
+ return MessageLevel::Fatal;
+ //NOTE: this diverges from the real regexp. no unicode, the first section is + instead of *
+ static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*";
+ if (line.contains("Exception in thread")
+ || line.contains(QRegularExpression("\\s+at " + javaSymbol))
+ || line.contains(QRegularExpression("Caused by: " + javaSymbol))
+ || line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)"))
+ || line.contains(QRegularExpression("... \\d+ more$"))
+ )
+ return MessageLevel::Error;
+ return level;
+}
+
+IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher()
+{
+ auto combined = std::make_shared<MultiMatcher>();
+ combined->add(std::make_shared<RegexpMatcher>(".*\\.log(\\.[0-9]*)?(\\.gz)?$"));
+ combined->add(std::make_shared<RegexpMatcher>("crash-.*\\.txt"));
+ combined->add(std::make_shared<RegexpMatcher>("IDMap dump.*\\.txt$"));
+ combined->add(std::make_shared<RegexpMatcher>("ModLoader\\.txt(\\..*)?$"));
+ return combined;
+}
+
+QString MinecraftInstance::getLogFileRoot()
+{
+ return minecraftRoot();
+}
+
+QString MinecraftInstance::prettifyTimeDuration(int64_t duration)
+{
+ int seconds = (int) (duration % 60);
+ duration /= 60;
+ int minutes = (int) (duration % 60);
+ duration /= 60;
+ int hours = (int) (duration % 24);
+ int days = (int) (duration / 24);
+ if((hours == 0)&&(days == 0))
+ {
+ return tr("%1m %2s").arg(minutes).arg(seconds);
+ }
+ if (days == 0)
+ {
+ return tr("%1h %2m").arg(hours).arg(minutes);
+ }
+ return tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes);
+}
+
+QString MinecraftInstance::getStatusbarDescription()
+{
+ QStringList traits;
+ if (flags() & VersionBrokenFlag)
+ {
+ traits.append(tr("broken"));
+ }
+
+ QString description;
+ description.append(tr("Minecraft %1 (%2)").arg(intendedVersionId()).arg(typeName()));
+ if(totalTimePlayed() > 0)
+ {
+ description.append(tr(", played for %1").arg(prettifyTimeDuration(totalTimePlayed())));
+ }
+ /*
+ if(traits.size())
+ {
+ description.append(QString(" (%1)").arg(traits.join(", ")));
+ }
+ */
+ return description;
+}
+
+#include "MinecraftInstance.moc"
diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h
new file mode 100644
index 00000000..cd3a8d90
--- /dev/null
+++ b/api/logic/minecraft/MinecraftInstance.h
@@ -0,0 +1,69 @@
+#pragma once
+#include "BaseInstance.h"
+#include "minecraft/Mod.h"
+#include <QProcess>
+
+#include "multimc_logic_export.h"
+
+class ModList;
+class WorldList;
+
+class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance
+{
+public:
+ MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+ virtual ~MinecraftInstance() {};
+
+ /// Path to the instance's minecraft directory.
+ QString minecraftRoot() const;
+
+ ////// Mod Lists //////
+ virtual std::shared_ptr<ModList> resourcePackList() const
+ {
+ return nullptr;
+ }
+ virtual std::shared_ptr<ModList> texturePackList() const
+ {
+ return nullptr;
+ }
+ virtual std::shared_ptr<WorldList> worldList() const
+ {
+ return nullptr;
+ }
+ /// get all jar mods applicable to this instance's jar
+ virtual QList<Mod> getJarMods() const
+ {
+ return QList<Mod>();
+ }
+
+ /// get the launch script to be used with this
+ virtual QString createLaunchScript(AuthSessionPtr session) = 0;
+
+ //FIXME: nuke?
+ virtual std::shared_ptr<BaseVersionList> versionList() const override;
+
+ /// get arguments passed to java
+ QStringList javaArguments() const;
+
+ /// get variables for launch command variable substitution/environment
+ virtual QMap<QString, QString> getVariables() const override;
+
+ /// create an environment for launching processes
+ virtual QProcessEnvironment createEnvironment() override;
+
+ /// guess log level from a line of minecraft log
+ virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) override;
+
+ virtual IPathMatcher::Ptr getLogFileMatcher() override;
+
+ virtual QString getLogFileRoot() override;
+
+ virtual QString getStatusbarDescription() override;
+
+protected:
+ QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
+private:
+ QString prettifyTimeDuration(int64_t duration);
+};
+
+typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr;
diff --git a/api/logic/minecraft/MinecraftProfile.cpp b/api/logic/minecraft/MinecraftProfile.cpp
new file mode 100644
index 00000000..70d0cee4
--- /dev/null
+++ b/api/logic/minecraft/MinecraftProfile.cpp
@@ -0,0 +1,610 @@
+/* Copyright 2013-2015 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 <QFile>
+#include <QCryptographicHash>
+#include <Version.h>
+#include <QDir>
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QDebug>
+
+#include "minecraft/MinecraftProfile.h"
+#include "ProfileUtils.h"
+#include "ProfileStrategy.h"
+#include "Exception.h"
+
+MinecraftProfile::MinecraftProfile(ProfileStrategy *strategy)
+ : QAbstractListModel()
+{
+ setStrategy(strategy);
+ clear();
+}
+
+void MinecraftProfile::setStrategy(ProfileStrategy* strategy)
+{
+ Q_ASSERT(strategy != nullptr);
+
+ if(m_strategy != nullptr)
+ {
+ delete m_strategy;
+ m_strategy = nullptr;
+ }
+ m_strategy = strategy;
+ m_strategy->profile = this;
+}
+
+ProfileStrategy* MinecraftProfile::strategy()
+{
+ return m_strategy;
+}
+
+void MinecraftProfile::reload()
+{
+ beginResetModel();
+ m_strategy->load();
+ reapplyPatches();
+ endResetModel();
+}
+
+void MinecraftProfile::clear()
+{
+ m_minecraftVersion.clear();
+ m_minecraftVersionType.clear();
+ m_minecraftAssets.reset();
+ m_minecraftArguments.clear();
+ m_tweakers.clear();
+ m_mainClass.clear();
+ m_appletClass.clear();
+ m_libraries.clear();
+ m_traits.clear();
+ m_jarMods.clear();
+ mojangDownloads.clear();
+ m_problemSeverity = ProblemSeverity::PROBLEM_NONE;
+}
+
+void MinecraftProfile::clearPatches()
+{
+ beginResetModel();
+ m_patches.clear();
+ endResetModel();
+}
+
+void MinecraftProfile::appendPatch(ProfilePatchPtr patch)
+{
+ int index = m_patches.size();
+ beginInsertRows(QModelIndex(), index, index);
+ m_patches.append(patch);
+ endInsertRows();
+}
+
+bool MinecraftProfile::remove(const int index)
+{
+ auto patch = versionPatch(index);
+ if (!patch->isRemovable())
+ {
+ qDebug() << "Patch" << patch->getID() << "is non-removable";
+ return false;
+ }
+
+ if(!m_strategy->removePatch(patch))
+ {
+ qCritical() << "Patch" << patch->getID() << "could not be removed";
+ return false;
+ }
+
+ beginRemoveRows(QModelIndex(), index, index);
+ m_patches.removeAt(index);
+ endRemoveRows();
+ reapplyPatches();
+ saveCurrentOrder();
+ return true;
+}
+
+bool MinecraftProfile::remove(const QString id)
+{
+ int i = 0;
+ for (auto patch : m_patches)
+ {
+ if (patch->getID() == id)
+ {
+ return remove(i);
+ }
+ i++;
+ }
+ return false;
+}
+
+bool MinecraftProfile::customize(int index)
+{
+ auto patch = versionPatch(index);
+ if (!patch->isCustomizable())
+ {
+ qDebug() << "Patch" << patch->getID() << "is not customizable";
+ return false;
+ }
+ if(!m_strategy->customizePatch(patch))
+ {
+ qCritical() << "Patch" << patch->getID() << "could not be customized";
+ return false;
+ }
+ reapplyPatches();
+ saveCurrentOrder();
+ // FIXME: maybe later in unstable
+ // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1));
+ return true;
+}
+
+bool MinecraftProfile::revertToBase(int index)
+{
+ auto patch = versionPatch(index);
+ if (!patch->isRevertible())
+ {
+ qDebug() << "Patch" << patch->getID() << "is not revertible";
+ return false;
+ }
+ if(!m_strategy->revertPatch(patch))
+ {
+ qCritical() << "Patch" << patch->getID() << "could not be reverted";
+ return false;
+ }
+ reapplyPatches();
+ saveCurrentOrder();
+ // FIXME: maybe later in unstable
+ // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1));
+ return true;
+}
+
+ProfilePatchPtr MinecraftProfile::versionPatch(const QString &id)
+{
+ for (auto file : m_patches)
+ {
+ if (file->getID() == id)
+ {
+ return file;
+ }
+ }
+ return nullptr;
+}
+
+ProfilePatchPtr MinecraftProfile::versionPatch(int index)
+{
+ if(index < 0 || index >= m_patches.size())
+ return nullptr;
+ return m_patches[index];
+}
+
+bool MinecraftProfile::isVanilla()
+{
+ for(auto patchptr: m_patches)
+ {
+ if(patchptr->isCustom())
+ return false;
+ }
+ return true;
+}
+
+bool MinecraftProfile::revertToVanilla()
+{
+ // remove patches, if present
+ auto VersionPatchesCopy = m_patches;
+ for(auto & it: VersionPatchesCopy)
+ {
+ if (!it->isCustom())
+ {
+ continue;
+ }
+ if(it->isRevertible() || it->isRemovable())
+ {
+ if(!remove(it->getID()))
+ {
+ qWarning() << "Couldn't remove" << it->getID() << "from profile!";
+ reapplyPatches();
+ saveCurrentOrder();
+ return false;
+ }
+ }
+ }
+ reapplyPatches();
+ saveCurrentOrder();
+ return true;
+}
+
+QVariant MinecraftProfile::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+
+ if (row < 0 || row >= m_patches.size())
+ return QVariant();
+
+ auto patch = m_patches.at(row);
+
+ if (role == Qt::DisplayRole)
+ {
+ switch (column)
+ {
+ case 0:
+ return m_patches.at(row)->getName();
+ case 1:
+ {
+ if(patch->isCustom())
+ {
+ return QString("%1 (Custom)").arg(patch->getVersion());
+ }
+ else
+ {
+ return patch->getVersion();
+ }
+ }
+ default:
+ return QVariant();
+ }
+ }
+ if(role == Qt::DecorationRole)
+ {
+ switch(column)
+ {
+ case 0:
+ {
+ auto severity = patch->getProblemSeverity();
+ switch (severity)
+ {
+ case PROBLEM_WARNING:
+ return "warning";
+ case PROBLEM_ERROR:
+ return "error";
+ default:
+ return QVariant();
+ }
+ }
+ default:
+ {
+ return QVariant();
+ }
+ }
+ }
+ return QVariant();
+}
+QVariant MinecraftProfile::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation == Qt::Horizontal)
+ {
+ if (role == Qt::DisplayRole)
+ {
+ switch (section)
+ {
+ case 0:
+ return tr("Name");
+ case 1:
+ return tr("Version");
+ default:
+ return QVariant();
+ }
+ }
+ }
+ return QVariant();
+}
+Qt::ItemFlags MinecraftProfile::flags(const QModelIndex &index) const
+{
+ if (!index.isValid())
+ return Qt::NoItemFlags;
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+}
+
+int MinecraftProfile::rowCount(const QModelIndex &parent) const
+{
+ return m_patches.size();
+}
+
+int MinecraftProfile::columnCount(const QModelIndex &parent) const
+{
+ return 2;
+}
+
+void MinecraftProfile::saveCurrentOrder() const
+{
+ ProfileUtils::PatchOrder order;
+ for(auto item: m_patches)
+ {
+ if(!item->isMoveable())
+ continue;
+ order.append(item->getID());
+ }
+ m_strategy->saveOrder(order);
+}
+
+void MinecraftProfile::move(const int index, const MoveDirection direction)
+{
+ int theirIndex;
+ if (direction == MoveUp)
+ {
+ theirIndex = index - 1;
+ }
+ else
+ {
+ theirIndex = index + 1;
+ }
+
+ if (index < 0 || index >= m_patches.size())
+ return;
+ if (theirIndex >= rowCount())
+ theirIndex = rowCount() - 1;
+ if (theirIndex == -1)
+ theirIndex = rowCount() - 1;
+ if (index == theirIndex)
+ return;
+ int togap = theirIndex > index ? theirIndex + 1 : theirIndex;
+
+ auto from = versionPatch(index);
+ auto to = versionPatch(theirIndex);
+
+ if (!from || !to || !to->isMoveable() || !from->isMoveable())
+ {
+ return;
+ }
+ beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap);
+ m_patches.swap(index, theirIndex);
+ endMoveRows();
+ reapplyPatches();
+ saveCurrentOrder();
+}
+void MinecraftProfile::resetOrder()
+{
+ m_strategy->resetOrder();
+ reload();
+}
+
+bool MinecraftProfile::reapplyPatches()
+{
+ try
+ {
+ clear();
+ for(auto file: m_patches)
+ {
+ file->applyTo(this);
+ }
+ }
+ catch (Exception & error)
+ {
+ clear();
+ qWarning() << "Couldn't apply profile patches because: " << error.cause();
+ return false;
+ }
+ return true;
+}
+
+static void applyString(const QString & from, QString & to)
+{
+ if(from.isEmpty())
+ return;
+ to = from;
+}
+
+void MinecraftProfile::applyMinecraftVersion(const QString& id)
+{
+ applyString(id, this->m_minecraftVersion);
+}
+
+void MinecraftProfile::applyAppletClass(const QString& appletClass)
+{
+ applyString(appletClass, this->m_appletClass);
+}
+
+void MinecraftProfile::applyMainClass(const QString& mainClass)
+{
+ applyString(mainClass, this->m_mainClass);
+}
+
+void MinecraftProfile::applyMinecraftArguments(const QString& minecraftArguments)
+{
+ applyString(minecraftArguments, this->m_minecraftArguments);
+}
+
+void MinecraftProfile::applyMinecraftVersionType(const QString& type)
+{
+ applyString(type, this->m_minecraftVersionType);
+}
+
+void MinecraftProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets)
+{
+ if(assets)
+ {
+ m_minecraftAssets = assets;
+ }
+}
+
+void MinecraftProfile::applyMojangDownload(const QString &key, MojangDownloadInfo::Ptr download)
+{
+ if(download)
+ {
+ mojangDownloads[key] = download;
+ }
+ else
+ {
+ mojangDownloads.remove(key);
+ }
+}
+
+void MinecraftProfile::applyTraits(const QSet<QString>& traits)
+{
+ this->m_traits.unite(traits);
+}
+
+void MinecraftProfile::applyTweakers(const QStringList& tweakers)
+{
+ // FIXME: check for dupes?
+ // FIXME: does order matter?
+ for (auto tweaker : tweakers)
+ {
+ this->m_tweakers += tweaker;
+ }
+}
+
+void MinecraftProfile::applyJarMods(const QList<JarmodPtr>& jarMods)
+{
+ this->m_jarMods.append(jarMods);
+}
+
+static int findLibraryByName(QList<LibraryPtr> haystack, const GradleSpecifier &needle)
+{
+ int retval = -1;
+ for (int i = 0; i < haystack.size(); ++i)
+ {
+ if (haystack.at(i)->rawName().matchName(needle))
+ {
+ // only one is allowed.
+ if (retval != -1)
+ return -1;
+ retval = i;
+ }
+ }
+ return retval;
+}
+
+void MinecraftProfile::applyLibrary(LibraryPtr library)
+{
+ if(!library->isActive())
+ {
+ return;
+ }
+ // find the library by name.
+ const int index = findLibraryByName(m_libraries, library->rawName());
+ // library not found? just add it.
+ if (index < 0)
+ {
+ m_libraries.append(Library::limitedCopy(library));
+ return;
+ }
+ auto existingLibrary = m_libraries.at(index);
+ // if we are higher it means we should update
+ if (Version(library->version()) > Version(existingLibrary->version()))
+ {
+ auto libraryCopy = Library::limitedCopy(library);
+ m_libraries.replace(index, libraryCopy);
+ }
+}
+
+void MinecraftProfile::applyProblemSeverity(ProblemSeverity severity)
+{
+ if (m_problemSeverity < severity)
+ {
+ m_problemSeverity = severity;
+ }
+}
+
+
+QString MinecraftProfile::getMinecraftVersion() const
+{
+ return m_minecraftVersion;
+}
+
+QString MinecraftProfile::getAppletClass() const
+{
+ return m_appletClass;
+}
+
+QString MinecraftProfile::getMainClass() const
+{
+ return m_mainClass;
+}
+
+const QSet<QString> &MinecraftProfile::getTraits() const
+{
+ return m_traits;
+}
+
+const QStringList & MinecraftProfile::getTweakers() const
+{
+ return m_tweakers;
+}
+
+bool MinecraftProfile::hasTrait(const QString& trait) const
+{
+ return m_traits.contains(trait);
+}
+
+ProblemSeverity MinecraftProfile::getProblemSeverity() const
+{
+ return m_problemSeverity;
+}
+
+QString MinecraftProfile::getMinecraftVersionType() const
+{
+ return m_minecraftVersionType;
+}
+
+std::shared_ptr<MojangAssetIndexInfo> MinecraftProfile::getMinecraftAssets() const
+{
+ if(!m_minecraftAssets)
+ {
+ return std::make_shared<MojangAssetIndexInfo>("legacy");
+ }
+ return m_minecraftAssets;
+}
+
+QString MinecraftProfile::getMinecraftArguments() const
+{
+ return m_minecraftArguments;
+}
+
+const QList<JarmodPtr> & MinecraftProfile::getJarMods() const
+{
+ return m_jarMods;
+}
+
+const QList<LibraryPtr> & MinecraftProfile::getLibraries() const
+{
+ return m_libraries;
+}
+
+QString MinecraftProfile::getMainJarUrl() const
+{
+ auto iter = mojangDownloads.find("client");
+ if(iter != mojangDownloads.end())
+ {
+ // current
+ return iter.value()->url;
+ }
+ else
+ {
+ // legacy fallback
+ return URLConstants::getLegacyJarUrl(getMinecraftVersion());
+ }
+}
+
+void MinecraftProfile::installJarMods(QStringList selectedFiles)
+{
+ m_strategy->installJarMods(selectedFiles);
+}
+
+/*
+ * TODO: get rid of this. Get rid of all order numbers.
+ */
+int MinecraftProfile::getFreeOrderNumber()
+{
+ int largest = 100;
+ // yes, I do realize this is dumb. The order thing itself is dumb. and to be removed next.
+ for(auto thing: m_patches)
+ {
+ int order = thing->getOrder();
+ if(order > largest)
+ largest = order;
+ }
+ return largest + 1;
+}
diff --git a/api/logic/minecraft/MinecraftProfile.h b/api/logic/minecraft/MinecraftProfile.h
new file mode 100644
index 00000000..ca9288ad
--- /dev/null
+++ b/api/logic/minecraft/MinecraftProfile.h
@@ -0,0 +1,200 @@
+/* Copyright 2013-2015 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 <QAbstractListModel>
+
+#include <QString>
+#include <QList>
+#include <memory>
+
+#include "Library.h"
+#include "VersionFile.h"
+#include "JarMod.h"
+#include "MojangDownloadInfo.h"
+
+#include "multimc_logic_export.h"
+
+class ProfileStrategy;
+class OneSixInstance;
+
+
+class MULTIMC_LOGIC_EXPORT MinecraftProfile : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ explicit MinecraftProfile(ProfileStrategy *strategy);
+
+ void setStrategy(ProfileStrategy * strategy);
+ ProfileStrategy *strategy();
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ virtual int columnCount(const QModelIndex &parent) const override;
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ /// is this version unchanged by the user?
+ bool isVanilla();
+
+ /// remove any customizations on top of whatever 'vanilla' means
+ bool revertToVanilla();
+
+ /// install more jar mods
+ void installJarMods(QStringList selectedFiles);
+
+ /// DEPRECATED, remove ASAP
+ int getFreeOrderNumber();
+
+ enum MoveDirection { MoveUp, MoveDown };
+ /// move patch file # up or down the list
+ void move(const int index, const MoveDirection direction);
+
+ /// remove patch file # - including files/records
+ bool remove(const int index);
+
+ /// remove patch file by id - including files/records
+ bool remove(const QString id);
+
+ bool customize(int index);
+
+ bool revertToBase(int index);
+
+ void resetOrder();
+
+ /// reload all profile patches from storage, clear the profile and apply the patches
+ void reload();
+
+ /// clear the profile
+ void clear();
+
+ /// apply the patches. Catches all the errors and returns true/false for success/failure
+ bool reapplyPatches();
+
+public: /* application of profile variables from patches */
+ void applyMinecraftVersion(const QString& id);
+ void applyMainClass(const QString& mainClass);
+ void applyAppletClass(const QString& appletClass);
+ void applyMinecraftArguments(const QString& minecraftArguments);
+ void applyMinecraftVersionType(const QString& type);
+ void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets);
+ void applyTraits(const QSet<QString> &traits);
+ void applyTweakers(const QStringList &tweakers);
+ void applyJarMods(const QList<JarmodPtr> &jarMods);
+ void applyLibrary(LibraryPtr library);
+ void applyProblemSeverity(ProblemSeverity severity);
+ void applyMojangDownload(const QString & key, MojangDownloadInfo::Ptr download);
+
+public: /* getters for profile variables */
+ QString getMinecraftVersion() const;
+ QString getMainClass() const;
+ QString getAppletClass() const;
+ QString getMinecraftVersionType() const;
+ MojangAssetIndexInfo::Ptr getMinecraftAssets() const;
+ QString getMinecraftArguments() const;
+ const QSet<QString> & getTraits() const;
+ const QStringList & getTweakers() const;
+ const QList<JarmodPtr> & getJarMods() const;
+ const QList<LibraryPtr> & getLibraries() const;
+ QString getMainJarUrl() const;
+ bool hasTrait(const QString & trait) const;
+ ProblemSeverity getProblemSeverity() const;
+
+public:
+ /// get the profile patch by id
+ ProfilePatchPtr versionPatch(const QString &id);
+
+ /// get the profile patch by index
+ ProfilePatchPtr versionPatch(int index);
+
+ /// save the current patch order
+ void saveCurrentOrder() const;
+
+ /// Remove all the patches
+ void clearPatches();
+
+ /// Add the patch object to the internal list of patches
+ void appendPatch(ProfilePatchPtr patch);
+
+private: /* data */
+ /// the version of Minecraft - jar to use
+ QString m_minecraftVersion;
+
+ /// Release type - "release" or "snapshot"
+ QString m_minecraftVersionType;
+
+ /// Assets type - "legacy" or a version ID
+ MojangAssetIndexInfo::Ptr m_minecraftAssets;
+
+ // Mojang: list of 'downloads' - client jar, server jar, windows server exe, maybe more.
+ QMap <QString, std::shared_ptr<MojangDownloadInfo>> mojangDownloads;
+
+ /**
+ * arguments that should be used for launching minecraft
+ *
+ * ex: "--username ${auth_player_name} --session ${auth_session}
+ * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}"
+ */
+ QString m_minecraftArguments;
+
+ /// A list of all tweaker classes
+ QStringList m_tweakers;
+
+ /// The main class to load first
+ QString m_mainClass;
+
+ /// The applet class, for some very old minecraft releases
+ QString m_appletClass;
+
+ /// the list of libraries
+ QList<LibraryPtr> m_libraries;
+
+ /// traits, collected from all the version files (version files can only add)
+ QSet<QString> m_traits;
+
+ /// A list of jar mods. version files can add those.
+ QList<JarmodPtr> m_jarMods;
+
+ ProblemSeverity m_problemSeverity = PROBLEM_NONE;
+
+ /*
+ FIXME: add support for those rules here? Looks like a pile of quick hacks to me though.
+
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx",
+ "version": "^10\\.5\\.\\d$"
+ }
+ }
+ ],
+ "incompatibilityReason": "There is a bug in LWJGL which makes it incompatible with OSX
+ 10.5.8. Please go to New Profile and use 1.5.2 for now. Sorry!"
+ }
+ */
+ // QList<Rule> rules;
+
+ /// list of attached profile patches
+ QList<ProfilePatchPtr> m_patches;
+
+ /// strategy used for profile operations
+ ProfileStrategy *m_strategy = nullptr;
+};
diff --git a/api/logic/minecraft/MinecraftVersion.cpp b/api/logic/minecraft/MinecraftVersion.cpp
new file mode 100644
index 00000000..1e1d273c
--- /dev/null
+++ b/api/logic/minecraft/MinecraftVersion.cpp
@@ -0,0 +1,215 @@
+#include "MinecraftVersion.h"
+#include "MinecraftProfile.h"
+#include "VersionBuildError.h"
+#include "ProfileUtils.h"
+#include "settings/SettingsObject.h"
+#include "minecraft/VersionFilterData.h"
+
+bool MinecraftVersion::usesLegacyLauncher()
+{
+ return getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate;
+}
+
+
+QString MinecraftVersion::descriptor()
+{
+ return m_version;
+}
+
+QString MinecraftVersion::name()
+{
+ return m_version;
+}
+
+QString MinecraftVersion::typeString() const
+{
+ if(m_type == "snapshot")
+ {
+ return QObject::tr("Snapshot");
+ }
+ else if (m_type == "release")
+ {
+ return QObject::tr("Regular release");
+ }
+ else if (m_type == "old_alpha")
+ {
+ return QObject::tr("Alpha");
+ }
+ else if (m_type == "old_beta")
+ {
+ return QObject::tr("Beta");
+ }
+ else
+ {
+ return QString();
+ }
+}
+
+VersionSource MinecraftVersion::getVersionSource()
+{
+ return m_versionSource;
+}
+
+bool MinecraftVersion::hasJarMods()
+{
+ return false;
+}
+
+bool MinecraftVersion::isMinecraftVersion()
+{
+ return true;
+}
+
+void MinecraftVersion::applyFileTo(MinecraftProfile *profile)
+{
+ if(m_versionSource == Local && getVersionFile())
+ {
+ getVersionFile()->applyTo(profile);
+ }
+ else
+ {
+ throw VersionIncomplete(QObject::tr("Can't apply incomplete/builtin Minecraft version %1").arg(m_version));
+ }
+}
+
+QString MinecraftVersion::getUrl() const
+{
+ // legacy fallback
+ if(m_versionFileURL.isEmpty())
+ {
+ return QString("http://") + URLConstants::AWS_DOWNLOAD_VERSIONS + m_version + "/" + m_version + ".json";
+ }
+ // current
+ return m_versionFileURL;
+}
+
+VersionFilePtr MinecraftVersion::getVersionFile()
+{
+ QFileInfo versionFile(QString("versions/%1/%1.dat").arg(m_version));
+ m_problems.clear();
+ if(!versionFile.exists())
+ {
+ if(m_loadedVersionFile)
+ {
+ m_loadedVersionFile.reset();
+ }
+ addProblem(PROBLEM_WARNING, QObject::tr("The patch file doesn't exist locally. It's possible it just needs to be downloaded."));
+ }
+ else
+ {
+ try
+ {
+ if(versionFile.lastModified() != m_loadedVersionFileTimestamp)
+ {
+ auto loadedVersionFile = ProfileUtils::parseBinaryJsonFile(versionFile);
+ loadedVersionFile->name = "Minecraft";
+ loadedVersionFile->setCustomizable(true);
+ m_loadedVersionFileTimestamp = versionFile.lastModified();
+ m_loadedVersionFile = loadedVersionFile;
+ }
+ }
+ catch(Exception e)
+ {
+ m_loadedVersionFile.reset();
+ addProblem(PROBLEM_ERROR, QObject::tr("The patch file couldn't be read:\n%1").arg(e.cause()));
+ }
+ }
+ return m_loadedVersionFile;
+}
+
+bool MinecraftVersion::isCustomizable()
+{
+ switch(m_versionSource)
+ {
+ case Local:
+ case Remote:
+ // locally cached file, or a remote file that we can acquire can be customized
+ return true;
+ default:
+ // Everything else is undefined and therefore not customizable.
+ return false;
+ }
+ return false;
+}
+
+const QList<PatchProblem> &MinecraftVersion::getProblems()
+{
+ if(getVersionFile())
+ {
+ return getVersionFile()->getProblems();
+ }
+ return ProfilePatch::getProblems();
+}
+
+ProblemSeverity MinecraftVersion::getProblemSeverity()
+{
+ if(getVersionFile())
+ {
+ return getVersionFile()->getProblemSeverity();
+ }
+ return ProfilePatch::getProblemSeverity();
+}
+
+void MinecraftVersion::applyTo(MinecraftProfile *profile)
+{
+ // do we have this one cached?
+ if (m_versionSource == Local)
+ {
+ applyFileTo(profile);
+ return;
+ }
+ throw VersionIncomplete(QObject::tr("Minecraft version %1 could not be applied: version files are missing.").arg(m_version));
+}
+
+int MinecraftVersion::getOrder()
+{
+ return order;
+}
+
+void MinecraftVersion::setOrder(int order)
+{
+ this->order = order;
+}
+
+QList<JarmodPtr> MinecraftVersion::getJarMods()
+{
+ return QList<JarmodPtr>();
+}
+
+QString MinecraftVersion::getName()
+{
+ return "Minecraft";
+}
+QString MinecraftVersion::getVersion()
+{
+ return m_version;
+}
+QString MinecraftVersion::getID()
+{
+ return "net.minecraft";
+}
+QString MinecraftVersion::getFilename()
+{
+ return QString();
+}
+QDateTime MinecraftVersion::getReleaseDateTime()
+{
+ return m_releaseTime;
+}
+
+
+bool MinecraftVersion::needsUpdate()
+{
+ return m_versionSource == Remote || hasUpdate();
+}
+
+bool MinecraftVersion::hasUpdate()
+{
+ return m_versionSource == Remote || (m_versionSource == Local && upstreamUpdate);
+}
+
+bool MinecraftVersion::isCustom()
+{
+ // if we add any other source types, this will evaluate to false for them.
+ return m_versionSource != Local && m_versionSource != Remote;
+}
diff --git a/api/logic/minecraft/MinecraftVersion.h b/api/logic/minecraft/MinecraftVersion.h
new file mode 100644
index 00000000..b21427d9
--- /dev/null
+++ b/api/logic/minecraft/MinecraftVersion.h
@@ -0,0 +1,119 @@
+/* Copyright 2013-2015 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 <QStringList>
+#include <QSet>
+#include <QDateTime>
+
+#include "BaseVersion.h"
+#include "ProfilePatch.h"
+#include "VersionFile.h"
+
+#include "multimc_logic_export.h"
+
+class MinecraftProfile;
+class MinecraftVersion;
+typedef std::shared_ptr<MinecraftVersion> MinecraftVersionPtr;
+
+class MULTIMC_LOGIC_EXPORT MinecraftVersion : public BaseVersion, public ProfilePatch
+{
+friend class MinecraftVersionList;
+
+public: /* methods */
+ // FIXME: nuke this.
+ bool usesLegacyLauncher();
+
+ virtual QString descriptor() override;
+ virtual QString name() override;
+ virtual QString typeString() const override;
+ virtual bool hasJarMods() override;
+ virtual bool isMinecraftVersion() override;
+ virtual void applyTo(MinecraftProfile *profile) override;
+ virtual int getOrder() override;
+ virtual void setOrder(int order) override;
+ virtual QList<JarmodPtr> getJarMods() override;
+ virtual QString getID() override;
+ virtual QString getVersion() override;
+ virtual QString getName() override;
+ virtual QString getFilename() override;
+ QDateTime getReleaseDateTime() override;
+ VersionSource getVersionSource() override;
+
+ bool needsUpdate();
+ bool hasUpdate();
+ virtual bool isCustom() override;
+ virtual bool isMoveable() override
+ {
+ return false;
+ }
+ virtual bool isCustomizable() override;
+ virtual bool isRemovable() override
+ {
+ return false;
+ }
+ virtual bool isRevertible() override
+ {
+ return false;
+ }
+ virtual bool isEditable() override
+ {
+ return false;
+ }
+ virtual bool isVersionChangeable() override
+ {
+ return true;
+ }
+
+ virtual VersionFilePtr getVersionFile() override;
+
+ // virtual QJsonDocument toJson(bool saveOrder) override;
+
+ QString getUrl() const;
+
+ virtual const QList<PatchProblem> &getProblems() override;
+ virtual ProblemSeverity getProblemSeverity() override;
+
+private: /* methods */
+ void applyFileTo(MinecraftProfile *profile);
+
+protected: /* data */
+ VersionSource m_versionSource = Remote;
+
+ /// The URL that this version will be downloaded from.
+ QString m_versionFileURL;
+
+ /// the human readable version name
+ QString m_version;
+
+ /// The type of this release
+ QString m_type;
+
+ /// the time this version was actually released by Mojang
+ QDateTime m_releaseTime;
+
+ /// the time this version was last updated by Mojang
+ QDateTime m_updateTime;
+
+ /// order of this file... default = -2
+ int order = -2;
+
+ /// an update available from Mojang
+ MinecraftVersionPtr upstreamUpdate;
+
+ QDateTime m_loadedVersionFileTimestamp;
+ mutable VersionFilePtr m_loadedVersionFile;
+};
diff --git a/api/logic/minecraft/MinecraftVersionList.cpp b/api/logic/minecraft/MinecraftVersionList.cpp
new file mode 100644
index 00000000..a5cc3a39
--- /dev/null
+++ b/api/logic/minecraft/MinecraftVersionList.cpp
@@ -0,0 +1,591 @@
+/* Copyright 2013-2015 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 <QtXml>
+#include "Json.h"
+#include <QtAlgorithms>
+#include <QtNetwork>
+
+#include "Env.h"
+#include "Exception.h"
+
+#include "MinecraftVersionList.h"
+#include "net/URLConstants.h"
+
+#include "ParseUtils.h"
+#include "ProfileUtils.h"
+#include "VersionFilterData.h"
+#include "onesix/OneSixVersionFormat.h"
+#include "MojangVersionFormat.h"
+#include <FileSystem.h>
+
+static const char * localVersionCache = "versions/versions.dat";
+
+class MCVListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit MCVListLoadTask(MinecraftVersionList *vlist);
+ virtual ~MCVListLoadTask() override{};
+
+ virtual void executeTask() override;
+
+protected
+slots:
+ void list_downloaded();
+
+protected:
+ QNetworkReply *vlistReply;
+ MinecraftVersionList *m_list;
+ MinecraftVersion *m_currentStable;
+};
+
+class MCVListVersionUpdateTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit MCVListVersionUpdateTask(MinecraftVersionList *vlist, std::shared_ptr<MinecraftVersion> updatedVersion);
+ virtual ~MCVListVersionUpdateTask() override{};
+ virtual void executeTask() override;
+
+protected
+slots:
+ void json_downloaded();
+
+protected:
+ NetJobPtr specificVersionDownloadJob;
+ std::shared_ptr<MinecraftVersion> updatedVersion;
+ MinecraftVersionList *m_list;
+};
+
+class ListLoadError : public Exception
+{
+public:
+ ListLoadError(QString cause) : Exception(cause) {};
+ virtual ~ListLoadError() noexcept
+ {
+ }
+};
+
+MinecraftVersionList::MinecraftVersionList(QObject *parent) : BaseVersionList(parent)
+{
+ loadCachedList();
+}
+
+Task *MinecraftVersionList::getLoadTask()
+{
+ return new MCVListLoadTask(this);
+}
+
+bool MinecraftVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+const BaseVersionPtr MinecraftVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+int MinecraftVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+static bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second)
+{
+ auto left = std::dynamic_pointer_cast<MinecraftVersion>(first);
+ auto right = std::dynamic_pointer_cast<MinecraftVersion>(second);
+ return left->getReleaseDateTime() > right->getReleaseDateTime();
+}
+
+void MinecraftVersionList::sortInternal()
+{
+ qSort(m_vlist.begin(), m_vlist.end(), cmpVersions);
+}
+
+void MinecraftVersionList::loadCachedList()
+{
+ QFile localIndex(localVersionCache);
+ if (!localIndex.exists())
+ {
+ return;
+ }
+ if (!localIndex.open(QIODevice::ReadOnly))
+ {
+ // FIXME: this is actually a very bad thing! How do we deal with this?
+ qCritical() << "The minecraft version cache can't be read.";
+ return;
+ }
+ auto data = localIndex.readAll();
+ try
+ {
+ localIndex.close();
+ QJsonDocument jsonDoc = QJsonDocument::fromBinaryData(data);
+ if (jsonDoc.isNull())
+ {
+ throw ListLoadError(tr("Error reading the version list."));
+ }
+ loadList(jsonDoc, Local);
+ }
+ catch (Exception &e)
+ {
+ // the cache has gone bad for some reason... flush it.
+ qCritical() << "The minecraft version cache is corrupted. Flushing cache.";
+ localIndex.remove();
+ return;
+ }
+ m_hasLocalIndex = true;
+}
+
+void MinecraftVersionList::loadList(QJsonDocument jsonDoc, VersionSource source)
+{
+ qDebug() << "Loading" << ((source == Remote) ? "remote" : "local") << "version list.";
+
+ if (!jsonDoc.isObject())
+ {
+ throw ListLoadError(tr("Error parsing version list JSON: jsonDoc is not an object"));
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ try
+ {
+ QJsonObject latest = Json::requireObject(root.value("latest"));
+ m_latestReleaseID = Json::requireString(latest.value("release"));
+ m_latestSnapshotID = Json::requireString(latest.value("snapshot"));
+ }
+ catch (Exception &err)
+ {
+ qCritical()
+ << tr("Error parsing version list JSON: couldn't determine latest versions");
+ }
+
+ // Now, get the array of versions.
+ if (!root.value("versions").isArray())
+ {
+ throw ListLoadError(tr("Error parsing version list JSON: version list object is "
+ "missing 'versions' array"));
+ }
+ QJsonArray versions = root.value("versions").toArray();
+
+ QList<BaseVersionPtr> tempList;
+ for (auto version : versions)
+ {
+ // Load the version info.
+ if (!version.isObject())
+ {
+ qCritical() << "Error while parsing version list : invalid JSON structure";
+ continue;
+ }
+
+ QJsonObject versionObj = version.toObject();
+ QString versionID = versionObj.value("id").toString("");
+ if (versionID.isEmpty())
+ {
+ qCritical() << "Error while parsing version : version ID is missing";
+ continue;
+ }
+
+ if (g_VersionFilterData.legacyBlacklist.contains(versionID))
+ {
+ qWarning() << "Blacklisted legacy version ignored: " << versionID;
+ continue;
+ }
+
+ // Now, we construct the version object and add it to the list.
+ std::shared_ptr<MinecraftVersion> mcVersion(new MinecraftVersion());
+ mcVersion->m_version = versionID;
+
+ mcVersion->m_releaseTime = timeFromS3Time(versionObj.value("releaseTime").toString(""));
+ mcVersion->m_updateTime = timeFromS3Time(versionObj.value("time").toString(""));
+
+ // depends on where we load the version from -- network request or local file?
+ mcVersion->m_versionSource = source;
+ mcVersion->m_versionFileURL = versionObj.value("url").toString("");
+ QString versionTypeStr = versionObj.value("type").toString("");
+ if (versionTypeStr.isEmpty())
+ {
+ qCritical() << "Ignoring" << versionID
+ << "because it doesn't have the version type set.";
+ continue;
+ }
+ // OneSix or Legacy. use filter to determine type
+ if (versionTypeStr == "release")
+ {
+ }
+ else if (versionTypeStr == "snapshot") // It's a snapshot... yay
+ {
+ }
+ else if (versionTypeStr == "old_alpha")
+ {
+ }
+ else if (versionTypeStr == "old_beta")
+ {
+ }
+ else
+ {
+ qCritical() << "Ignoring" << versionID
+ << "because it has an invalid version type.";
+ continue;
+ }
+ mcVersion->m_type = versionTypeStr;
+ qDebug() << "Loaded version" << versionID << "from"
+ << ((source == Remote) ? "remote" : "local") << "version list.";
+ tempList.append(mcVersion);
+ }
+ updateListData(tempList);
+ if(source == Remote)
+ {
+ m_loaded = true;
+ }
+}
+
+void MinecraftVersionList::sortVersions()
+{
+ beginResetModel();
+ sortInternal();
+ endResetModel();
+}
+
+QVariant MinecraftVersionList::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<MinecraftVersion>(m_vlist[index.row()]);
+ switch (role)
+ {
+ case VersionPointerRole:
+ return qVariantFromValue(m_vlist[index.row()]);
+
+ case VersionRole:
+ return version->name();
+
+ case VersionIdRole:
+ return version->descriptor();
+
+ case RecommendedRole:
+ return version->descriptor() == m_latestReleaseID;
+
+ case LatestRole:
+ {
+ if(version->descriptor() != m_latestSnapshotID)
+ return false;
+ MinecraftVersionPtr latestRelease = std::dynamic_pointer_cast<MinecraftVersion>(getLatestStable());
+ /*
+ if(latestRelease && latestRelease->m_releaseTime > version->m_releaseTime)
+ {
+ return false;
+ }
+ */
+ return true;
+ }
+
+ case TypeRole:
+ return version->typeString();
+
+ default:
+ return QVariant();
+ }
+}
+
+BaseVersionList::RoleList MinecraftVersionList::providesRoles() const
+{
+ return {VersionPointerRole, VersionRole, VersionIdRole, RecommendedRole, LatestRole, TypeRole};
+}
+
+BaseVersionPtr MinecraftVersionList::getLatestStable() const
+{
+ if(m_lookup.contains(m_latestReleaseID))
+ return m_lookup[m_latestReleaseID];
+ return BaseVersionPtr();
+}
+
+BaseVersionPtr MinecraftVersionList::getRecommended() const
+{
+ return getLatestStable();
+}
+
+void MinecraftVersionList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ for (auto version : versions)
+ {
+ auto descr = version->descriptor();
+
+ if (!m_lookup.contains(descr))
+ {
+ m_lookup[version->descriptor()] = version;
+ m_vlist.append(version);
+ continue;
+ }
+ auto orig = std::dynamic_pointer_cast<MinecraftVersion>(m_lookup[descr]);
+ auto added = std::dynamic_pointer_cast<MinecraftVersion>(version);
+ // updateListData is called after Mojang list loads. those can be local or remote
+ // remote comes always after local
+ // any other options are ignored
+ if (orig->m_versionSource != Local || added->m_versionSource != Remote)
+ {
+ continue;
+ }
+ // alright, it's an update. put it inside the original, for further processing.
+ orig->upstreamUpdate = added;
+ }
+ sortInternal();
+ endResetModel();
+}
+
+MCVListLoadTask::MCVListLoadTask(MinecraftVersionList *vlist)
+{
+ m_list = vlist;
+ m_currentStable = NULL;
+ vlistReply = nullptr;
+}
+
+void MCVListLoadTask::executeTask()
+{
+ setStatus(tr("Loading instance version list..."));
+ auto worker = ENV.qnam();
+ vlistReply = worker->get(QNetworkRequest(QUrl("https://launchermeta.mojang.com/mc/game/version_manifest.json")));
+ connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded()));
+}
+
+void MCVListLoadTask::list_downloaded()
+{
+ if (vlistReply->error() != QNetworkReply::NoError)
+ {
+ vlistReply->deleteLater();
+ emitFailed("Failed to load Minecraft main version list" + vlistReply->errorString());
+ return;
+ }
+
+ auto data = vlistReply->readAll();
+ vlistReply->deleteLater();
+ try
+ {
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ throw ListLoadError(
+ tr("Error parsing version list JSON: %1").arg(jsonError.errorString()));
+ }
+ m_list->loadList(jsonDoc, Remote);
+ }
+ catch (Exception &e)
+ {
+ emitFailed(e.cause());
+ return;
+ }
+
+ emitSucceeded();
+ return;
+}
+
+MCVListVersionUpdateTask::MCVListVersionUpdateTask(MinecraftVersionList *vlist, std::shared_ptr<MinecraftVersion> updatedVersion)
+ : Task()
+{
+ m_list = vlist;
+ this->updatedVersion = updatedVersion;
+}
+
+void MCVListVersionUpdateTask::executeTask()
+{
+ auto job = new NetJob("Version index");
+ job->addNetAction(ByteArrayDownload::make(QUrl(updatedVersion->getUrl())));
+ specificVersionDownloadJob.reset(job);
+ connect(specificVersionDownloadJob.get(), SIGNAL(succeeded()), SLOT(json_downloaded()));
+ connect(specificVersionDownloadJob.get(), SIGNAL(failed(QString)), SIGNAL(failed(QString)));
+ connect(specificVersionDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ specificVersionDownloadJob->start();
+}
+
+void MCVListVersionUpdateTask::json_downloaded()
+{
+ NetActionPtr DlJob = specificVersionDownloadJob->first();
+ auto data = std::dynamic_pointer_cast<ByteArrayDownload>(DlJob)->m_data;
+ specificVersionDownloadJob.reset();
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed(tr("The download version file is not valid."));
+ return;
+ }
+ VersionFilePtr file;
+ try
+ {
+ file = MojangVersionFormat::versionFileFromJson(jsonDoc, "net.minecraft.json");
+ }
+ catch (Exception &e)
+ {
+ emitFailed(tr("Couldn't process version file: %1").arg(e.cause()));
+ return;
+ }
+
+ // Strip LWJGL from the version file. We use our own.
+ ProfileUtils::removeLwjglFromPatch(file);
+
+ file->fileId = "net.minecraft";
+
+ // now dump the file to disk
+ auto doc = OneSixVersionFormat::versionFileToJson(file, false);
+ auto newdata = doc.toBinaryData();
+ auto id = updatedVersion->descriptor();
+ QString targetPath = "versions/" + id + "/" + id + ".dat";
+ FS::ensureFilePathExists(targetPath);
+ QSaveFile vfile1(targetPath);
+ if (!vfile1.open(QIODevice::Truncate | QIODevice::WriteOnly))
+ {
+ emitFailed(tr("Can't open %1 for writing.").arg(targetPath));
+ return;
+ }
+ qint64 actual = 0;
+ if ((actual = vfile1.write(newdata)) != newdata.size())
+ {
+ emitFailed(tr("Failed to write into %1. Written %2 out of %3.")
+ .arg(targetPath)
+ .arg(actual)
+ .arg(newdata.size()));
+ return;
+ }
+ if (!vfile1.commit())
+ {
+ emitFailed(tr("Can't commit changes to %1").arg(targetPath));
+ return;
+ }
+
+ m_list->finalizeUpdate(id);
+ emitSucceeded();
+}
+
+std::shared_ptr<Task> MinecraftVersionList::createUpdateTask(QString version)
+{
+ auto iter = m_lookup.find(version);
+ if(iter == m_lookup.end())
+ return nullptr;
+
+ auto mcversion = std::dynamic_pointer_cast<MinecraftVersion>(*iter);
+ if(!mcversion)
+ {
+ return nullptr;
+ }
+
+ return std::shared_ptr<Task>(new MCVListVersionUpdateTask(this, mcversion));
+}
+
+void MinecraftVersionList::saveCachedList()
+{
+ // FIXME: throw.
+ if (!FS::ensureFilePathExists(localVersionCache))
+ return;
+ QSaveFile tfile(localVersionCache);
+ if (!tfile.open(QIODevice::WriteOnly | QIODevice::Truncate))
+ return;
+ QJsonObject toplevel;
+ QJsonArray entriesArr;
+ for (auto version : m_vlist)
+ {
+ auto mcversion = std::dynamic_pointer_cast<MinecraftVersion>(version);
+ // do not save the remote versions.
+ if (mcversion->m_versionSource != Local)
+ continue;
+ QJsonObject entryObj;
+
+ entryObj.insert("id", mcversion->descriptor());
+ entryObj.insert("version", mcversion->descriptor());
+ entryObj.insert("time", timeToS3Time(mcversion->m_updateTime));
+ entryObj.insert("releaseTime", timeToS3Time(mcversion->m_releaseTime));
+ entryObj.insert("url", mcversion->m_versionFileURL);
+ entryObj.insert("type", mcversion->m_type);
+ entriesArr.append(entryObj);
+ }
+ toplevel.insert("versions", entriesArr);
+
+ {
+ bool someLatest = false;
+ QJsonObject latestObj;
+ if(!m_latestReleaseID.isNull())
+ {
+ latestObj.insert("release", m_latestReleaseID);
+ someLatest = true;
+ }
+ if(!m_latestSnapshotID.isNull())
+ {
+ latestObj.insert("snapshot", m_latestSnapshotID);
+ someLatest = true;
+ }
+ if(someLatest)
+ {
+ toplevel.insert("latest", latestObj);
+ }
+ }
+
+ QJsonDocument doc(toplevel);
+ QByteArray jsonData = doc.toBinaryData();
+ qint64 result = tfile.write(jsonData);
+ if (result == -1)
+ return;
+ if (result != jsonData.size())
+ return;
+ tfile.commit();
+}
+
+void MinecraftVersionList::finalizeUpdate(QString version)
+{
+ int idx = -1;
+ for (int i = 0; i < m_vlist.size(); i++)
+ {
+ if (version == m_vlist[i]->descriptor())
+ {
+ idx = i;
+ break;
+ }
+ }
+ if (idx == -1)
+ {
+ return;
+ }
+
+ auto updatedVersion = std::dynamic_pointer_cast<MinecraftVersion>(m_vlist[idx]);
+
+ // if we have an update for the version, replace it, make the update local
+ if (updatedVersion->upstreamUpdate)
+ {
+ auto updatedWith = updatedVersion->upstreamUpdate;
+ updatedWith->m_versionSource = Local;
+ m_vlist[idx] = updatedWith;
+ m_lookup[version] = updatedWith;
+ }
+ else
+ {
+ // otherwise, just set the version as local;
+ updatedVersion->m_versionSource = Local;
+ }
+
+ dataChanged(index(idx), index(idx));
+
+ saveCachedList();
+}
+
+#include "MinecraftVersionList.moc"
diff --git a/api/logic/minecraft/MinecraftVersionList.h b/api/logic/minecraft/MinecraftVersionList.h
new file mode 100644
index 00000000..0fca02a7
--- /dev/null
+++ b/api/logic/minecraft/MinecraftVersionList.h
@@ -0,0 +1,72 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QList>
+#include <QSet>
+
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+#include "minecraft/MinecraftVersion.h"
+#include <net/NetJob.h>
+
+#include "multimc_logic_export.h"
+
+class MCVListLoadTask;
+class MCVListVersionUpdateTask;
+
+class MULTIMC_LOGIC_EXPORT MinecraftVersionList : public BaseVersionList
+{
+ Q_OBJECT
+private:
+ void sortInternal();
+ void loadList(QJsonDocument jsonDoc, VersionSource source);
+ void loadCachedList();
+ void saveCachedList();
+ void finalizeUpdate(QString version);
+public:
+ friend class MCVListLoadTask;
+ friend class MCVListVersionUpdateTask;
+
+ explicit MinecraftVersionList(QObject *parent = 0);
+
+ std::shared_ptr<Task> createUpdateTask(QString version);
+
+ virtual Task *getLoadTask() override;
+ virtual bool isLoaded() override;
+ virtual const BaseVersionPtr at(int i) const override;
+ virtual int count() const override;
+ virtual void sortVersions() override;
+ virtual QVariant data(const QModelIndex & index, int role) const override;
+ virtual RoleList providesRoles() const override;
+
+ virtual BaseVersionPtr getLatestStable() const override;
+ virtual BaseVersionPtr getRecommended() const override;
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+ QMap<QString, BaseVersionPtr> m_lookup;
+
+ bool m_loaded = false;
+ bool m_hasLocalIndex = false;
+ QString m_latestReleaseID = "INVALID";
+ QString m_latestSnapshotID = "INVALID";
+
+protected
+slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions) override;
+};
diff --git a/api/logic/minecraft/Mod.cpp b/api/logic/minecraft/Mod.cpp
new file mode 100644
index 00000000..9b9f76f9
--- /dev/null
+++ b/api/logic/minecraft/Mod.cpp
@@ -0,0 +1,377 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QDir>
+#include <QString>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <quazip.h>
+#include <quazipfile.h>
+
+#include "Mod.h"
+#include "settings/INIFile.h"
+#include <FileSystem.h>
+#include <QDebug>
+
+Mod::Mod(const QFileInfo &file)
+{
+ repath(file);
+}
+
+void Mod::repath(const QFileInfo &file)
+{
+ m_file = file;
+ QString name_base = file.fileName();
+
+ m_type = Mod::MOD_UNKNOWN;
+
+ if (m_file.isDir())
+ {
+ m_type = MOD_FOLDER;
+ m_name = name_base;
+ m_mmc_id = name_base;
+ }
+ else if (m_file.isFile())
+ {
+ if (name_base.endsWith(".disabled"))
+ {
+ m_enabled = false;
+ name_base.chop(9);
+ }
+ else
+ {
+ m_enabled = true;
+ }
+ m_mmc_id = name_base;
+ if (name_base.endsWith(".zip") || name_base.endsWith(".jar"))
+ {
+ m_type = MOD_ZIPFILE;
+ name_base.chop(4);
+ }
+ else if (name_base.endsWith(".litemod"))
+ {
+ m_type = MOD_LITEMOD;
+ name_base.chop(8);
+ }
+ else
+ {
+ m_type = MOD_SINGLEFILE;
+ }
+ m_name = name_base;
+ }
+
+ if (m_type == MOD_ZIPFILE)
+ {
+ QuaZip zip(m_file.filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return;
+
+ QuaZipFile file(&zip);
+
+ if (zip.setCurrentFile("mcmod.info"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ ReadMCModInfo(file.readAll());
+ file.close();
+ zip.close();
+ return;
+ }
+ else if (zip.setCurrentFile("forgeversion.properties"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ ReadForgeInfo(file.readAll());
+ file.close();
+ zip.close();
+ return;
+ }
+
+ zip.close();
+ }
+ else if (m_type == MOD_FOLDER)
+ {
+ QFileInfo mcmod_info(FS::PathCombine(m_file.filePath(), "mcmod.info"));
+ if (mcmod_info.isFile())
+ {
+ QFile mcmod(mcmod_info.filePath());
+ if (!mcmod.open(QIODevice::ReadOnly))
+ return;
+ auto data = mcmod.readAll();
+ if (data.isEmpty() || data.isNull())
+ return;
+ ReadMCModInfo(data);
+ }
+ }
+ else if (m_type == MOD_LITEMOD)
+ {
+ QuaZip zip(m_file.filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return;
+
+ QuaZipFile file(&zip);
+
+ if (zip.setCurrentFile("litemod.json"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ ReadLiteModInfo(file.readAll());
+ file.close();
+ }
+ zip.close();
+ }
+}
+
+// NEW format
+// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3
+
+// OLD format:
+// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
+void Mod::ReadMCModInfo(QByteArray contents)
+{
+ auto getInfoFromArray = [&](QJsonArray arr)->void
+ {
+ if (!arr.at(0).isObject())
+ return;
+ auto firstObj = arr.at(0).toObject();
+ m_mod_id = firstObj.value("modid").toString();
+ m_name = firstObj.value("name").toString();
+ m_version = firstObj.value("version").toString();
+ m_homeurl = firstObj.value("url").toString();
+ m_updateurl = firstObj.value("updateUrl").toString();
+ m_homeurl = m_homeurl.trimmed();
+ if(!m_homeurl.isEmpty())
+ {
+ // fix up url.
+ if (!m_homeurl.startsWith("http://") && !m_homeurl.startsWith("https://") &&
+ !m_homeurl.startsWith("ftp://"))
+ {
+ m_homeurl.prepend("http://");
+ }
+ }
+ m_description = firstObj.value("description").toString();
+ QJsonArray authors = firstObj.value("authorList").toArray();
+ if (authors.size() == 0)
+ authors = firstObj.value("authors").toArray();
+
+ if (authors.size() == 0)
+ m_authors = "";
+ else if (authors.size() >= 1)
+ {
+ m_authors = authors.at(0).toString();
+ for (int i = 1; i < authors.size(); i++)
+ {
+ m_authors += ", " + authors.at(i).toString();
+ }
+ }
+ m_credits = firstObj.value("credits").toString();
+ return;
+ }
+ ;
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
+ // this is the very old format that had just the array
+ if (jsonDoc.isArray())
+ {
+ getInfoFromArray(jsonDoc.array());
+ }
+ else if (jsonDoc.isObject())
+ {
+ auto val = jsonDoc.object().value("modinfoversion");
+ if(val.isUndefined())
+ val = jsonDoc.object().value("modListVersion");
+ int version = val.toDouble();
+ if (version != 2)
+ {
+ qCritical() << "BAD stuff happened to mod json:";
+ qCritical() << contents;
+ return;
+ }
+ auto arrVal = jsonDoc.object().value("modlist");
+ if(arrVal.isUndefined())
+ arrVal = jsonDoc.object().value("modList");
+ if (arrVal.isArray())
+ {
+ getInfoFromArray(arrVal.toArray());
+ }
+ }
+}
+
+void Mod::ReadForgeInfo(QByteArray contents)
+{
+ // Read the data
+ m_name = "Minecraft Forge";
+ m_mod_id = "Forge";
+ m_homeurl = "http://www.minecraftforge.net/forum/";
+ INIFile ini;
+ if (!ini.loadFile(contents))
+ return;
+
+ QString major = ini.get("forge.major.number", "0").toString();
+ QString minor = ini.get("forge.minor.number", "0").toString();
+ QString revision = ini.get("forge.revision.number", "0").toString();
+ QString build = ini.get("forge.build.number", "0").toString();
+
+ m_version = major + "." + minor + "." + revision + "." + build;
+}
+
+void Mod::ReadLiteModInfo(QByteArray contents)
+{
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
+ auto object = jsonDoc.object();
+ if (object.contains("name"))
+ {
+ m_mod_id = m_name = object.value("name").toString();
+ }
+ if (object.contains("version"))
+ {
+ m_version = object.value("version").toString("");
+ }
+ else
+ {
+ m_version = object.value("revision").toString("");
+ }
+ m_mcversion = object.value("mcversion").toString();
+ m_authors = object.value("author").toString();
+ m_description = object.value("description").toString();
+ m_homeurl = object.value("url").toString();
+}
+
+bool Mod::replace(Mod &with)
+{
+ if (!destroy())
+ return false;
+ bool success = false;
+ auto t = with.type();
+
+ if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE || t == MOD_LITEMOD)
+ {
+ qDebug() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath();
+ success = QFile::copy(with.m_file.filePath(), m_file.filePath());
+ }
+ if (t == MOD_FOLDER)
+ {
+ success = FS::copy(with.m_file.filePath(), m_file.path())();
+ }
+ if (success)
+ {
+ m_name = with.m_name;
+ m_mmc_id = with.m_mmc_id;
+ m_mod_id = with.m_mod_id;
+ m_version = with.m_version;
+ m_mcversion = with.m_mcversion;
+ m_description = with.m_description;
+ m_authors = with.m_authors;
+ m_credits = with.m_credits;
+ m_homeurl = with.m_homeurl;
+ m_type = with.m_type;
+ m_file.refresh();
+ }
+ return success;
+}
+
+bool Mod::destroy()
+{
+ if (m_type == MOD_FOLDER)
+ {
+ QDir d(m_file.filePath());
+ if (d.removeRecursively())
+ {
+ m_type = MOD_UNKNOWN;
+ return true;
+ }
+ return false;
+ }
+ else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD)
+ {
+ QFile f(m_file.filePath());
+ if (f.remove())
+ {
+ m_type = MOD_UNKNOWN;
+ return true;
+ }
+ return false;
+ }
+ return true;
+}
+
+QString Mod::version() const
+{
+ switch (type())
+ {
+ case MOD_ZIPFILE:
+ case MOD_LITEMOD:
+ return m_version;
+ case MOD_FOLDER:
+ return "Folder";
+ case MOD_SINGLEFILE:
+ return "File";
+ default:
+ return "VOID";
+ }
+}
+
+bool Mod::enable(bool value)
+{
+ if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
+ return false;
+
+ if (m_enabled == value)
+ return false;
+
+ QString path = m_file.absoluteFilePath();
+ if (value)
+ {
+ QFile foo(path);
+ if (!path.endsWith(".disabled"))
+ return false;
+ path.chop(9);
+ if (!foo.rename(path))
+ return false;
+ }
+ else
+ {
+ QFile foo(path);
+ path += ".disabled";
+ if (!foo.rename(path))
+ return false;
+ }
+ m_file = QFileInfo(path);
+ m_enabled = value;
+ return true;
+}
+bool Mod::operator==(const Mod &other) const
+{
+ return mmc_id() == other.mmc_id();
+}
+bool Mod::strongCompare(const Mod &other) const
+{
+ return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type();
+}
diff --git a/api/logic/minecraft/Mod.h b/api/logic/minecraft/Mod.h
new file mode 100644
index 00000000..19f4c740
--- /dev/null
+++ b/api/logic/minecraft/Mod.h
@@ -0,0 +1,134 @@
+/* Copyright 2013-2015 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 <QFileInfo>
+
+class Mod
+{
+public:
+ enum ModType
+ {
+ MOD_UNKNOWN, //!< Indicates an unspecified mod type.
+ MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files.
+ MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
+ MOD_FOLDER, //!< The mod is in a folder on the filesystem.
+ MOD_LITEMOD, //!< The mod is a litemod
+ };
+
+ Mod(const QFileInfo &file);
+
+ QFileInfo filename() const
+ {
+ return m_file;
+ }
+ QString mmc_id() const
+ {
+ return m_mmc_id;
+ }
+ QString mod_id() const
+ {
+ return m_mod_id;
+ }
+ ModType type() const
+ {
+ return m_type;
+ }
+ QString mcversion() const
+ {
+ return m_mcversion;
+ }
+ ;
+ bool valid()
+ {
+ return m_type != MOD_UNKNOWN;
+ }
+ QString name() const
+ {
+ if(m_name.trimmed().isEmpty())
+ {
+ return m_mmc_id;
+ }
+ return m_name;
+ }
+
+ QString version() const;
+
+ QString homeurl() const
+ {
+ return m_homeurl;
+ }
+
+ QString description() const
+ {
+ return m_description;
+ }
+
+ QString authors() const
+ {
+ return m_authors;
+ }
+
+ QString credits() const
+ {
+ return m_credits;
+ }
+
+ bool enabled() const
+ {
+ return m_enabled;
+ }
+
+ bool enable(bool value);
+
+ // delete all the files of this mod
+ bool destroy();
+ // replace this mod with a copy of the other
+ bool replace(Mod &with);
+ // change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
+ void repath(const QFileInfo &file);
+
+ // WEAK compare operator - used for replacing mods
+ bool operator==(const Mod &other) const;
+ bool strongCompare(const Mod &other) const;
+
+private:
+ void ReadMCModInfo(QByteArray contents);
+ void ReadForgeInfo(QByteArray contents);
+ void ReadLiteModInfo(QByteArray contents);
+
+protected:
+
+ // FIXME: what do do with those? HMM...
+ /*
+ void ReadModInfoData(QString info);
+ void ReadForgeInfoData(QString infoFileData);
+ */
+
+ QFileInfo m_file;
+ QString m_mmc_id;
+ QString m_mod_id;
+ bool m_enabled = true;
+ QString m_name;
+ QString m_version;
+ QString m_mcversion;
+ QString m_homeurl;
+ QString m_updateurl;
+ QString m_description;
+ QString m_authors;
+ QString m_credits;
+
+ ModType m_type;
+};
diff --git a/api/logic/minecraft/ModList.cpp b/api/logic/minecraft/ModList.cpp
new file mode 100644
index 00000000..d9ed4886
--- /dev/null
+++ b/api/logic/minecraft/ModList.cpp
@@ -0,0 +1,616 @@
+/* Copyright 2013-2015 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 "ModList.h"
+#include <FileSystem.h>
+#include <QMimeData>
+#include <QUrl>
+#include <QUuid>
+#include <QString>
+#include <QFileSystemWatcher>
+#include <QDebug>
+
+ModList::ModList(const QString &dir, const QString &list_file)
+ : QAbstractListModel(), m_dir(dir), m_list_file(list_file)
+{
+ FS::ensureFolderPathExists(m_dir.absolutePath());
+ m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs |
+ QDir::NoSymLinks);
+ m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
+ m_list_id = QUuid::createUuid().toString();
+ m_watcher = new QFileSystemWatcher(this);
+ is_watching = false;
+ connect(m_watcher, SIGNAL(directoryChanged(QString)), this,
+ SLOT(directoryChanged(QString)));
+}
+
+void ModList::startWatching()
+{
+ update();
+ is_watching = m_watcher->addPath(m_dir.absolutePath());
+ if (is_watching)
+ {
+ qDebug() << "Started watching " << m_dir.absolutePath();
+ }
+ else
+ {
+ qDebug() << "Failed to start watching " << m_dir.absolutePath();
+ }
+}
+
+void ModList::stopWatching()
+{
+ is_watching = !m_watcher->removePath(m_dir.absolutePath());
+ if (!is_watching)
+ {
+ qDebug() << "Stopped watching " << m_dir.absolutePath();
+ }
+ else
+ {
+ qDebug() << "Failed to stop watching " << m_dir.absolutePath();
+ }
+}
+
+void ModList::internalSort(QList<Mod> &what)
+{
+ auto predicate = [](const Mod &left, const Mod &right)
+ {
+ if (left.name() == right.name())
+ {
+ return left.mmc_id().localeAwareCompare(right.mmc_id()) < 0;
+ }
+ return left.name().localeAwareCompare(right.name()) < 0;
+ };
+ std::sort(what.begin(), what.end(), predicate);
+}
+
+bool ModList::update()
+{
+ if (!isValid())
+ return false;
+
+ QList<Mod> orderedMods;
+ QList<Mod> newMods;
+ m_dir.refresh();
+ auto folderContents = m_dir.entryInfoList();
+ bool orderOrStateChanged = false;
+
+ // first, process the ordered items (if any)
+ OrderList listOrder = readListFile();
+ for (auto item : listOrder)
+ {
+ QFileInfo infoEnabled(m_dir.filePath(item.id));
+ QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled"));
+ int idxEnabled = folderContents.indexOf(infoEnabled);
+ int idxDisabled = folderContents.indexOf(infoDisabled);
+ bool isEnabled;
+ // if both enabled and disabled versions are present, it's a special case...
+ if (idxEnabled >= 0 && idxDisabled >= 0)
+ {
+ // we only process the one we actually have in the order file.
+ // and exactly as we have it.
+ // THIS IS A CORNER CASE
+ isEnabled = item.enabled;
+ }
+ else
+ {
+ // only one is present.
+ // we pick the one that we found.
+ // we assume the mod was enabled/disabled by external means
+ isEnabled = idxEnabled >= 0;
+ }
+ int idx = isEnabled ? idxEnabled : idxDisabled;
+ QFileInfo &info = isEnabled ? infoEnabled : infoDisabled;
+ // if the file from the index file exists
+ if (idx != -1)
+ {
+ // remove from the actual folder contents list
+ folderContents.takeAt(idx);
+ // append the new mod
+ orderedMods.append(Mod(info));
+ if (isEnabled != item.enabled)
+ orderOrStateChanged = true;
+ }
+ else
+ {
+ orderOrStateChanged = true;
+ }
+ }
+ // if there are any untracked files...
+ if (folderContents.size())
+ {
+ // the order surely changed!
+ for (auto entry : folderContents)
+ {
+ newMods.append(Mod(entry));
+ }
+ internalSort(newMods);
+ orderedMods.append(newMods);
+ orderOrStateChanged = true;
+ }
+ // otherwise, if we were already tracking some mods
+ else if (mods.size())
+ {
+ // if the number doesn't match, order changed.
+ if (mods.size() != orderedMods.size())
+ orderOrStateChanged = true;
+ // if it does match, compare the mods themselves
+ else
+ for (int i = 0; i < mods.size(); i++)
+ {
+ if (!mods[i].strongCompare(orderedMods[i]))
+ {
+ orderOrStateChanged = true;
+ break;
+ }
+ }
+ }
+ beginResetModel();
+ mods.swap(orderedMods);
+ endResetModel();
+ if (orderOrStateChanged && !m_list_file.isEmpty())
+ {
+ qDebug() << "Mod list " << m_list_file << " changed!";
+ saveListFile();
+ emit changed();
+ }
+ return true;
+}
+
+void ModList::directoryChanged(QString path)
+{
+ update();
+}
+
+ModList::OrderList ModList::readListFile()
+{
+ OrderList itemList;
+ if (m_list_file.isNull() || m_list_file.isEmpty())
+ return itemList;
+
+ QFile textFile(m_list_file);
+ if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text))
+ return OrderList();
+
+ QTextStream textStream;
+ textStream.setAutoDetectUnicode(true);
+ textStream.setDevice(&textFile);
+ while (true)
+ {
+ QString line = textStream.readLine();
+ if (line.isNull() || line.isEmpty())
+ break;
+ else
+ {
+ OrderItem it;
+ it.enabled = !line.endsWith(".disabled");
+ if (!it.enabled)
+ {
+ line.chop(9);
+ }
+ it.id = line;
+ itemList.append(it);
+ }
+ }
+ textFile.close();
+ return itemList;
+}
+
+bool ModList::saveListFile()
+{
+ if (m_list_file.isNull() || m_list_file.isEmpty())
+ return false;
+ QFile textFile(m_list_file);
+ if (!textFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
+ return false;
+ QTextStream textStream;
+ textStream.setGenerateByteOrderMark(true);
+ textStream.setCodec("UTF-8");
+ textStream.setDevice(&textFile);
+ for (auto mod : mods)
+ {
+ textStream << mod.mmc_id();
+ if (!mod.enabled())
+ textStream << ".disabled";
+ textStream << endl;
+ }
+ textFile.close();
+ return false;
+}
+
+bool ModList::isValid()
+{
+ return m_dir.exists() && m_dir.isReadable();
+}
+
+bool ModList::installMod(const QString &filename, int index)
+{
+ // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
+ QFileInfo fileinfo(FS::NormalizePath(filename));
+
+ qDebug() << "installing: " << fileinfo.absoluteFilePath();
+
+ if (!fileinfo.exists() || !fileinfo.isReadable() || index < 0)
+ {
+ return false;
+ }
+ Mod m(fileinfo);
+ if (!m.valid())
+ return false;
+
+ // if it's already there, replace the original mod (in place)
+ int idx = mods.indexOf(m);
+ if (idx != -1)
+ {
+ int idx2 = mods.indexOf(m, idx + 1);
+ if (idx2 != -1)
+ return false;
+ if (mods[idx].replace(m))
+ {
+
+ auto left = this->index(index);
+ auto right = this->index(index, columnCount(QModelIndex()) - 1);
+ emit dataChanged(left, right);
+ saveListFile();
+ update();
+ return true;
+ }
+ return false;
+ }
+
+ auto type = m.type();
+ if (type == Mod::MOD_UNKNOWN)
+ return false;
+ if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
+ {
+ QString newpath = FS::PathCombine(m_dir.path(), fileinfo.fileName());
+ if (!QFile::copy(fileinfo.filePath(), newpath))
+ return false;
+ m.repath(newpath);
+ beginInsertRows(QModelIndex(), index, index);
+ mods.insert(index, m);
+ endInsertRows();
+ saveListFile();
+ update();
+ return true;
+ }
+ else if (type == Mod::MOD_FOLDER)
+ {
+
+ QString from = fileinfo.filePath();
+ QString to = FS::PathCombine(m_dir.path(), fileinfo.fileName());
+ if (!FS::copy(from, to)())
+ return false;
+ m.repath(to);
+ beginInsertRows(QModelIndex(), index, index);
+ mods.insert(index, m);
+ endInsertRows();
+ saveListFile();
+ update();
+ return true;
+ }
+ return false;
+}
+
+bool ModList::deleteMod(int index)
+{
+ if (index >= mods.size() || index < 0)
+ return false;
+ Mod &m = mods[index];
+ if (m.destroy())
+ {
+ beginRemoveRows(QModelIndex(), index, index);
+ mods.removeAt(index);
+ endRemoveRows();
+ saveListFile();
+ emit changed();
+ return true;
+ }
+ return false;
+}
+
+bool ModList::deleteMods(int first, int last)
+{
+ for (int i = first; i <= last; i++)
+ {
+ Mod &m = mods[i];
+ m.destroy();
+ }
+ beginRemoveRows(QModelIndex(), first, last);
+ mods.erase(mods.begin() + first, mods.begin() + last + 1);
+ endRemoveRows();
+ saveListFile();
+ emit changed();
+ return true;
+}
+
+bool ModList::moveModTo(int from, int to)
+{
+ if (from < 0 || from >= mods.size())
+ return false;
+ if (to >= rowCount())
+ to = rowCount() - 1;
+ if (to == -1)
+ to = rowCount() - 1;
+ if (from == to)
+ return false;
+ int togap = to > from ? to + 1 : to;
+ beginMoveRows(QModelIndex(), from, from, QModelIndex(), togap);
+ mods.move(from, to);
+ endMoveRows();
+ saveListFile();
+ emit changed();
+ return true;
+}
+
+bool ModList::moveModUp(int from)
+{
+ if (from > 0)
+ return moveModTo(from, from - 1);
+ return false;
+}
+
+bool ModList::moveModsUp(int first, int last)
+{
+ if (first == 0)
+ return false;
+
+ beginMoveRows(QModelIndex(), first, last, QModelIndex(), first - 1);
+ mods.move(first - 1, last);
+ endMoveRows();
+ saveListFile();
+ emit changed();
+ return true;
+}
+
+bool ModList::moveModDown(int from)
+{
+ if (from < 0)
+ return false;
+ if (from < mods.size() - 1)
+ return moveModTo(from, from + 1);
+ return false;
+}
+
+bool ModList::moveModsDown(int first, int last)
+{
+ if (last == mods.size() - 1)
+ return false;
+
+ beginMoveRows(QModelIndex(), first, last, QModelIndex(), last + 2);
+ mods.move(last + 1, first);
+ endMoveRows();
+ saveListFile();
+ emit changed();
+ return true;
+}
+
+int ModList::columnCount(const QModelIndex &parent) const
+{
+ return 3;
+}
+
+QVariant ModList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+
+ if (row < 0 || row >= mods.size())
+ return QVariant();
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (column)
+ {
+ case NameColumn:
+ return mods[row].name();
+ case VersionColumn:
+ return mods[row].version();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return mods[row].mmc_id();
+
+ case Qt::CheckStateRole:
+ switch (column)
+ {
+ case ActiveColumn:
+ return mods[row].enabled() ? Qt::Checked : Qt::Unchecked;
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+}
+
+bool ModList::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
+ {
+ return false;
+ }
+
+ if (role == Qt::CheckStateRole)
+ {
+ auto &mod = mods[index.row()];
+ if (mod.enable(!mod.enabled()))
+ {
+ emit dataChanged(index, index);
+ return true;
+ }
+ }
+ return false;
+}
+
+QVariant ModList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case ActiveColumn:
+ return QString();
+ case NameColumn:
+ return tr("Name");
+ case VersionColumn:
+ return tr("Version");
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case ActiveColumn:
+ return tr("Is the mod enabled?");
+ case NameColumn:
+ return tr("The name of the mod.");
+ case VersionColumn:
+ return tr("The version of the mod.");
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ return QVariant();
+}
+
+Qt::ItemFlags ModList::flags(const QModelIndex &index) const
+{
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ if (index.isValid())
+ return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled |
+ defaultFlags;
+ else
+ return Qt::ItemIsDropEnabled | defaultFlags;
+}
+
+QStringList ModList::mimeTypes() const
+{
+ QStringList types;
+ types << "text/uri-list";
+ types << "text/plain";
+ return types;
+}
+
+Qt::DropActions ModList::supportedDropActions() const
+{
+ // copy from outside, move from within and other mod lists
+ return Qt::CopyAction | Qt::MoveAction;
+}
+
+Qt::DropActions ModList::supportedDragActions() const
+{
+ // move to other mod lists or VOID
+ return Qt::MoveAction;
+}
+
+QMimeData *ModList::mimeData(const QModelIndexList &indexes) const
+{
+ QMimeData *data = new QMimeData();
+
+ if (indexes.size() == 0)
+ return data;
+
+ auto idx = indexes[0];
+ int row = idx.row();
+ if (row < 0 || row >= mods.size())
+ return data;
+
+ QStringList params;
+ params << m_list_id << QString::number(row);
+ data->setText(params.join('|'));
+ return data;
+}
+
+bool ModList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
+ const QModelIndex &parent)
+{
+ if (action == Qt::IgnoreAction)
+ return true;
+ // check if the action is supported
+ if (!data || !(action & supportedDropActions()))
+ return false;
+ if (parent.isValid())
+ {
+ row = parent.row();
+ column = parent.column();
+ }
+
+ if (row > rowCount())
+ row = rowCount();
+ if (row == -1)
+ row = rowCount();
+ if (column == -1)
+ column = 0;
+ qDebug() << "Drop row: " << row << " column: " << column;
+
+ // files dropped from outside?
+ if (data->hasUrls())
+ {
+ bool was_watching = is_watching;
+ if (was_watching)
+ stopWatching();
+ auto urls = data->urls();
+ for (auto url : urls)
+ {
+ // only local files may be dropped...
+ if (!url.isLocalFile())
+ continue;
+ QString filename = url.toLocalFile();
+ installMod(filename, row);
+ // if there is no ordering, re-sort the list
+ if (m_list_file.isEmpty())
+ {
+ beginResetModel();
+ internalSort(mods);
+ endResetModel();
+ }
+ }
+ if (was_watching)
+ startWatching();
+ return true;
+ }
+ else if (data->hasText())
+ {
+ QString sourcestr = data->text();
+ auto list = sourcestr.split('|');
+ if (list.size() != 2)
+ return false;
+ QString remoteId = list[0];
+ int remoteIndex = list[1].toInt();
+ qDebug() << "move: " << sourcestr;
+ // no moving of things between two lists
+ if (remoteId != m_list_id)
+ return false;
+ // no point moving to the same place...
+ if (row == remoteIndex)
+ return false;
+ // otherwise, move the mod :D
+ moveModTo(remoteIndex, row);
+ return true;
+ }
+ return false;
+}
diff --git a/api/logic/minecraft/ModList.h b/api/logic/minecraft/ModList.h
new file mode 100644
index 00000000..05ada8ee
--- /dev/null
+++ b/api/logic/minecraft/ModList.h
@@ -0,0 +1,160 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QList>
+#include <QString>
+#include <QDir>
+#include <QAbstractListModel>
+
+#include "minecraft/Mod.h"
+
+#include "multimc_logic_export.h"
+
+class LegacyInstance;
+class BaseInstance;
+class QFileSystemWatcher;
+
+/**
+ * A legacy mod list.
+ * Backed by a folder.
+ */
+class MULTIMC_LOGIC_EXPORT ModList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Columns
+ {
+ ActiveColumn = 0,
+ NameColumn,
+ VersionColumn
+ };
+ ModList(const QString &dir, const QString &list_file = QString());
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+ virtual bool setData(const QModelIndex &index, const QVariant &value,
+ int role = Qt::EditRole);
+
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const
+ {
+ return size();
+ }
+ ;
+ virtual QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const;
+ virtual int columnCount(const QModelIndex &parent) const;
+
+ size_t size() const
+ {
+ return mods.size();
+ }
+ ;
+ bool empty() const
+ {
+ return size() == 0;
+ }
+ Mod &operator[](size_t index)
+ {
+ return mods[index];
+ }
+
+ /// Reloads the mod list and returns true if the list changed.
+ virtual bool update();
+
+ /**
+ * Adds the given mod to the list at the given index - if the list supports custom ordering
+ */
+ virtual bool installMod(const QString & filename, int index = 0);
+
+ /// Deletes the mod at the given index.
+ virtual bool deleteMod(int index);
+
+ /// Deletes all the selected mods
+ virtual bool deleteMods(int first, int last);
+
+ /**
+ * move the mod at index to the position N
+ * 0 is the beginning of the list, length() is the end of the list.
+ */
+ virtual bool moveModTo(int from, int to);
+
+ /**
+ * move the mod at index one position upwards
+ */
+ virtual bool moveModUp(int from);
+ virtual bool moveModsUp(int first, int last);
+
+ /**
+ * move the mod at index one position downwards
+ */
+ virtual bool moveModDown(int from);
+ virtual bool moveModsDown(int first, int last);
+
+ /// flags, mostly to support drag&drop
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const;
+ /// get data for drag action
+ virtual QMimeData *mimeData(const QModelIndexList &indexes) const;
+ /// get the supported mime types
+ virtual QStringList mimeTypes() const;
+ /// process data from drop action
+ virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
+ const QModelIndex &parent);
+ /// what drag actions do we support?
+ virtual Qt::DropActions supportedDragActions() const;
+
+ /// what drop actions do we support?
+ virtual Qt::DropActions supportedDropActions() const;
+
+ void startWatching();
+ void stopWatching();
+
+ virtual bool isValid();
+
+ QDir dir()
+ {
+ return m_dir;
+ }
+
+ const QList<Mod> & allMods()
+ {
+ return mods;
+ }
+
+private:
+ void internalSort(QList<Mod> & what);
+ struct OrderItem
+ {
+ QString id;
+ bool enabled = false;
+ };
+ typedef QList<OrderItem> OrderList;
+ OrderList readListFile();
+ bool saveListFile();
+private
+slots:
+ void directoryChanged(QString path);
+
+signals:
+ void changed();
+
+protected:
+ QFileSystemWatcher *m_watcher;
+ bool is_watching;
+ QDir m_dir;
+ QString m_list_file;
+ QString m_list_id;
+ QList<Mod> mods;
+};
diff --git a/api/logic/minecraft/MojangDownloadInfo.h b/api/logic/minecraft/MojangDownloadInfo.h
new file mode 100644
index 00000000..1f3306e0
--- /dev/null
+++ b/api/logic/minecraft/MojangDownloadInfo.h
@@ -0,0 +1,71 @@
+#pragma once
+#include <QString>
+#include <QMap>
+#include <memory>
+
+struct MojangDownloadInfo
+{
+ // types
+ typedef std::shared_ptr<MojangDownloadInfo> Ptr;
+
+ // data
+ /// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested!
+ QString path;
+ /// absolute URL of this file
+ QString url;
+ /// sha-1 checksum of the file
+ QString sha1;
+ /// size of the file in bytes
+ int size;
+};
+
+
+
+struct MojangLibraryDownloadInfo
+{
+ MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact): artifact(artifact) {};
+ MojangLibraryDownloadInfo() {};
+
+ // types
+ typedef std::shared_ptr<MojangLibraryDownloadInfo> Ptr;
+
+ // methods
+ MojangDownloadInfo *getDownloadInfo(QString classifier)
+ {
+ if (classifier.isNull())
+ {
+ return artifact.get();
+ }
+
+ return classifiers[classifier].get();
+ }
+
+ // data
+ MojangDownloadInfo::Ptr artifact;
+ QMap<QString, MojangDownloadInfo::Ptr> classifiers;
+};
+
+
+
+struct MojangAssetIndexInfo : public MojangDownloadInfo
+{
+ // types
+ typedef std::shared_ptr<MojangAssetIndexInfo> Ptr;
+
+ // methods
+ MojangAssetIndexInfo()
+ {
+ }
+
+ MojangAssetIndexInfo(QString id)
+ {
+ this->id = id;
+ url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id + ".json";
+ known = false;
+ }
+
+ // data
+ int totalSize;
+ QString id;
+ bool known = true;
+};
diff --git a/api/logic/minecraft/MojangVersionFormat.cpp b/api/logic/minecraft/MojangVersionFormat.cpp
new file mode 100644
index 00000000..34129c9e
--- /dev/null
+++ b/api/logic/minecraft/MojangVersionFormat.cpp
@@ -0,0 +1,381 @@
+#include "MojangVersionFormat.h"
+#include "onesix/OneSixVersionFormat.h"
+#include "MinecraftVersion.h"
+#include "VersionBuildError.h"
+#include "MojangDownloadInfo.h"
+
+#include "Json.h"
+using namespace Json;
+#include "ParseUtils.h"
+
+static const int CURRENT_MINIMUM_LAUNCHER_VERSION = 18;
+
+static MojangAssetIndexInfo::Ptr assetIndexFromJson (const QJsonObject &obj);
+static MojangDownloadInfo::Ptr downloadInfoFromJson (const QJsonObject &obj);
+static MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson (const QJsonObject &libObj);
+static QJsonObject assetIndexToJson (MojangAssetIndexInfo::Ptr assetidxinfo);
+static QJsonObject libDownloadInfoToJson (MojangLibraryDownloadInfo::Ptr libinfo);
+static QJsonObject downloadInfoToJson (MojangDownloadInfo::Ptr info);
+
+namespace Bits
+{
+static void readString(const QJsonObject &root, const QString &key, QString &variable)
+{
+ if (root.contains(key))
+ {
+ variable = requireString(root.value(key));
+ }
+}
+
+static void readDownloadInfo(MojangDownloadInfo::Ptr out, const QJsonObject &obj)
+{
+ // optional, not used
+ readString(obj, "path", out->path);
+ // required!
+ out->sha1 = requireString(obj, "sha1");
+ out->url = requireString(obj, "url");
+ out->size = requireInteger(obj, "size");
+}
+
+static void readAssetIndex(MojangAssetIndexInfo::Ptr out, const QJsonObject &obj)
+{
+ out->totalSize = requireInteger(obj, "totalSize");
+ out->id = requireString(obj, "id");
+ // out->known = true;
+}
+}
+
+MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject &obj)
+{
+ auto out = std::make_shared<MojangDownloadInfo>();
+ Bits::readDownloadInfo(out, obj);
+ return out;
+}
+
+MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject &obj)
+{
+ auto out = std::make_shared<MojangAssetIndexInfo>();
+ Bits::readDownloadInfo(out, obj);
+ Bits::readAssetIndex(out, obj);
+ return out;
+}
+
+QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info)
+{
+ QJsonObject out;
+ if(!info->path.isNull())
+ {
+ out.insert("path", info->path);
+ }
+ out.insert("sha1", info->sha1);
+ out.insert("size", info->size);
+ out.insert("url", info->url);
+ return out;
+}
+
+MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject &libObj)
+{
+ auto out = std::make_shared<MojangLibraryDownloadInfo>();
+ auto dlObj = requireObject(libObj.value("downloads"));
+ if(dlObj.contains("artifact"))
+ {
+ out->artifact = downloadInfoFromJson(requireObject(dlObj, "artifact"));
+ }
+ if(dlObj.contains("classifiers"))
+ {
+ auto classifiersObj = requireObject(dlObj, "classifiers");
+ for(auto iter = classifiersObj.begin(); iter != classifiersObj.end(); iter++)
+ {
+ auto classifier = iter.key();
+ auto classifierObj = requireObject(iter.value());
+ out->classifiers[classifier] = downloadInfoFromJson(classifierObj);
+ }
+ }
+ return out;
+}
+
+QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo)
+{
+ QJsonObject out;
+ if(libinfo->artifact)
+ {
+ out.insert("artifact", downloadInfoToJson(libinfo->artifact));
+ }
+ if(libinfo->classifiers.size())
+ {
+ QJsonObject classifiersOut;
+ for(auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++)
+ {
+ classifiersOut.insert(iter.key(), downloadInfoToJson(iter.value()));
+ }
+ out.insert("classifiers", classifiersOut);
+ }
+ return out;
+}
+
+QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr info)
+{
+ QJsonObject out;
+ if(!info->path.isNull())
+ {
+ out.insert("path", info->path);
+ }
+ out.insert("sha1", info->sha1);
+ out.insert("size", info->size);
+ out.insert("url", info->url);
+ out.insert("totalSize", info->totalSize);
+ out.insert("id", info->id);
+ return out;
+}
+
+void MojangVersionFormat::readVersionProperties(const QJsonObject &in, VersionFile *out)
+{
+ Bits::readString(in, "id", out->minecraftVersion);
+ Bits::readString(in, "mainClass", out->mainClass);
+ Bits::readString(in, "minecraftArguments", out->minecraftArguments);
+ if(out->minecraftArguments.isEmpty())
+ {
+ QString processArguments;
+ Bits::readString(in, "processArguments", processArguments);
+ QString toCompare = processArguments.toLower();
+ if (toCompare == "legacy")
+ {
+ out->minecraftArguments = " ${auth_player_name} ${auth_session}";
+ }
+ else if (toCompare == "username_session")
+ {
+ out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}";
+ }
+ else if (toCompare == "username_session_version")
+ {
+ out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}";
+ }
+ else if (!toCompare.isEmpty())
+ {
+ out->addProblem(PROBLEM_ERROR, QObject::tr("processArguments is set to unknown value '%1'").arg(processArguments));
+ }
+ }
+ Bits::readString(in, "type", out->type);
+
+ Bits::readString(in, "assets", out->assets);
+ if(in.contains("assetIndex"))
+ {
+ out->mojangAssetIndex = assetIndexFromJson(requireObject(in, "assetIndex"));
+ }
+ else if (!out->assets.isNull())
+ {
+ out->mojangAssetIndex = std::make_shared<MojangAssetIndexInfo>(out->assets);
+ }
+
+ out->m_releaseTime = timeFromS3Time(in.value("releaseTime").toString(""));
+ out->m_updateTime = timeFromS3Time(in.value("time").toString(""));
+
+ if (in.contains("minimumLauncherVersion"))
+ {
+ out->minimumLauncherVersion = requireInteger(in.value("minimumLauncherVersion"));
+ if (out->minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION)
+ {
+ out->addProblem(
+ PROBLEM_WARNING,
+ QObject::tr("The 'minimumLauncherVersion' value of this version (%1) is higher than supported by MultiMC (%2). It might not work properly!")
+ .arg(out->minimumLauncherVersion)
+ .arg(CURRENT_MINIMUM_LAUNCHER_VERSION));
+ }
+ }
+ if(in.contains("downloads"))
+ {
+ auto downloadsObj = requireObject(in, "downloads");
+ for(auto iter = downloadsObj.begin(); iter != downloadsObj.end(); iter++)
+ {
+ auto classifier = iter.key();
+ auto classifierObj = requireObject(iter.value());
+ out->mojangDownloads[classifier] = downloadInfoFromJson(classifierObj);
+ }
+ }
+}
+
+VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument &doc, const QString &filename)
+{
+ VersionFilePtr out(new VersionFile());
+ if (doc.isEmpty() || doc.isNull())
+ {
+ throw JSONValidationError(filename + " is empty or null");
+ }
+ if (!doc.isObject())
+ {
+ throw JSONValidationError(filename + " is not an object");
+ }
+
+ QJsonObject root = doc.object();
+
+ readVersionProperties(root, out.get());
+
+ out->name = "Minecraft";
+ out->fileId = "net.minecraft";
+ out->version = out->minecraftVersion;
+ out->filename = filename;
+
+
+ if (root.contains("libraries"))
+ {
+ for (auto libVal : requireArray(root.value("libraries")))
+ {
+ auto libObj = requireObject(libVal);
+
+ auto lib = MojangVersionFormat::libraryFromJson(libObj, filename);
+ out->libraries.append(lib);
+ }
+ }
+ return out;
+}
+
+void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObject& out)
+{
+ writeString(out, "id", in->minecraftVersion);
+ writeString(out, "mainClass", in->mainClass);
+ writeString(out, "minecraftArguments", in->minecraftArguments);
+ writeString(out, "type", in->type);
+ if(!in->m_releaseTime.isNull())
+ {
+ writeString(out, "releaseTime", timeToS3Time(in->m_releaseTime));
+ }
+ if(!in->m_updateTime.isNull())
+ {
+ writeString(out, "time", timeToS3Time(in->m_updateTime));
+ }
+ if(in->minimumLauncherVersion != -1)
+ {
+ out.insert("minimumLauncherVersion", in->minimumLauncherVersion);
+ }
+ writeString(out, "assets", in->assets);
+ if(in->mojangAssetIndex && in->mojangAssetIndex->known)
+ {
+ out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex));
+ }
+ if(in->mojangDownloads.size())
+ {
+ QJsonObject downloadsOut;
+ for(auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++)
+ {
+ downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value()));
+ }
+ out.insert("downloads", downloadsOut);
+ }
+}
+
+QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch)
+{
+ QJsonObject root;
+ writeVersionProperties(patch.get(), root);
+ if (!patch->libraries.isEmpty())
+ {
+ QJsonArray array;
+ for (auto value: patch->libraries)
+ {
+ array.append(MojangVersionFormat::libraryToJson(value.get()));
+ }
+ root.insert("libraries", array);
+ }
+
+ // write the contents to a json document.
+ {
+ QJsonDocument out;
+ out.setObject(root);
+ return out;
+ }
+}
+
+LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename)
+{
+ LibraryPtr out(new Library());
+ if (!libObj.contains("name"))
+ {
+ throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field");
+ }
+ out->m_name = libObj.value("name").toString();
+
+ Bits::readString(libObj, "url", out->m_repositoryURL);
+ if (libObj.contains("extract"))
+ {
+ out->m_hasExcludes = true;
+ auto extractObj = requireObject(libObj.value("extract"));
+ for (auto excludeVal : requireArray(extractObj.value("exclude")))
+ {
+ out->m_extractExcludes.append(requireString(excludeVal));
+ }
+ }
+ if (libObj.contains("natives"))
+ {
+ QJsonObject nativesObj = requireObject(libObj.value("natives"));
+ for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it)
+ {
+ if (!it.value().isString())
+ {
+ qWarning() << filename << "contains an invalid native (skipping)";
+ }
+ OpSys opSys = OpSys_fromString(it.key());
+ if (opSys != Os_Other)
+ {
+ out->m_nativeClassifiers[opSys] = it.value().toString();
+ }
+ }
+ }
+ if (libObj.contains("rules"))
+ {
+ out->applyRules = true;
+ out->m_rules = rulesFromJsonV4(libObj);
+ }
+ if (libObj.contains("downloads"))
+ {
+ out->m_mojangDownloads = libDownloadInfoFromJson(libObj);
+ }
+ return out;
+}
+
+QJsonObject MojangVersionFormat::libraryToJson(Library *library)
+{
+ QJsonObject libRoot;
+ libRoot.insert("name", (QString)library->m_name);
+ if (!library->m_repositoryURL.isEmpty())
+ {
+ libRoot.insert("url", library->m_repositoryURL);
+ }
+ if (library->isNative())
+ {
+ QJsonObject nativeList;
+ auto iter = library->m_nativeClassifiers.begin();
+ while (iter != library->m_nativeClassifiers.end())
+ {
+ nativeList.insert(OpSys_toString(iter.key()), iter.value());
+ iter++;
+ }
+ libRoot.insert("natives", nativeList);
+ if (library->m_extractExcludes.size())
+ {
+ QJsonArray excludes;
+ QJsonObject extract;
+ for (auto exclude : library->m_extractExcludes)
+ {
+ excludes.append(exclude);
+ }
+ extract.insert("exclude", excludes);
+ libRoot.insert("extract", extract);
+ }
+ }
+ if (library->m_rules.size())
+ {
+ QJsonArray allRules;
+ for (auto &rule : library->m_rules)
+ {
+ QJsonObject ruleObj = rule->toJson();
+ allRules.append(ruleObj);
+ }
+ libRoot.insert("rules", allRules);
+ }
+ if(library->m_mojangDownloads)
+ {
+ auto downloadsObj = libDownloadInfoToJson(library->m_mojangDownloads);
+ libRoot.insert("downloads", downloadsObj);
+ }
+ return libRoot;
+}
diff --git a/api/logic/minecraft/MojangVersionFormat.h b/api/logic/minecraft/MojangVersionFormat.h
new file mode 100644
index 00000000..4e141088
--- /dev/null
+++ b/api/logic/minecraft/MojangVersionFormat.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <minecraft/VersionFile.h>
+#include <minecraft/Library.h>
+#include <QJsonDocument>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT MojangVersionFormat
+{
+friend class OneSixVersionFormat;
+protected:
+ // does not include libraries
+ static void readVersionProperties(const QJsonObject& in, VersionFile* out);
+ // does not include libraries
+ static void writeVersionProperties(const VersionFile* in, QJsonObject& out);
+public:
+ // version files / profile patches
+ static VersionFilePtr versionFileFromJson(const QJsonDocument &doc, const QString &filename);
+ static QJsonDocument versionFileToJson(const VersionFilePtr &patch);
+
+ // libraries
+ static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename);
+ static QJsonObject libraryToJson(Library *library);
+};
diff --git a/api/logic/minecraft/OpSys.cpp b/api/logic/minecraft/OpSys.cpp
new file mode 100644
index 00000000..4c2a236d
--- /dev/null
+++ b/api/logic/minecraft/OpSys.cpp
@@ -0,0 +1,42 @@
+/* Copyright 2013-2015 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 "OpSys.h"
+
+OpSys OpSys_fromString(QString name)
+{
+ if (name == "linux")
+ return Os_Linux;
+ if (name == "windows")
+ return Os_Windows;
+ if (name == "osx")
+ return Os_OSX;
+ return Os_Other;
+}
+
+QString OpSys_toString(OpSys name)
+{
+ switch (name)
+ {
+ case Os_Linux:
+ return "linux";
+ case Os_OSX:
+ return "osx";
+ case Os_Windows:
+ return "windows";
+ default:
+ return "other";
+ }
+} \ No newline at end of file
diff --git a/api/logic/minecraft/OpSys.h b/api/logic/minecraft/OpSys.h
new file mode 100644
index 00000000..9ebea3de
--- /dev/null
+++ b/api/logic/minecraft/OpSys.h
@@ -0,0 +1,37 @@
+/* Copyright 2013-2015 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>
+enum OpSys
+{
+ Os_Windows,
+ Os_Linux,
+ Os_OSX,
+ Os_Other
+};
+
+OpSys OpSys_fromString(QString);
+QString OpSys_toString(OpSys);
+
+#ifdef Q_OS_WIN32
+#define currentSystem Os_Windows
+#else
+#ifdef Q_OS_MAC
+#define currentSystem Os_OSX
+#else
+#define currentSystem Os_Linux
+#endif
+#endif \ No newline at end of file
diff --git a/api/logic/minecraft/ParseUtils.cpp b/api/logic/minecraft/ParseUtils.cpp
new file mode 100644
index 00000000..ca188432
--- /dev/null
+++ b/api/logic/minecraft/ParseUtils.cpp
@@ -0,0 +1,34 @@
+#include <QDateTime>
+#include <QString>
+#include "ParseUtils.h"
+#include <QDebug>
+#include <cstdlib>
+
+QDateTime timeFromS3Time(QString str)
+{
+ return QDateTime::fromString(str, Qt::ISODate);
+}
+
+QString timeToS3Time(QDateTime time)
+{
+ // this all because Qt can't format timestamps right.
+ int offsetRaw = time.offsetFromUtc();
+ bool negative = offsetRaw < 0;
+ int offsetAbs = std::abs(offsetRaw);
+
+ int offsetSeconds = offsetAbs % 60;
+ offsetAbs -= offsetSeconds;
+
+ int offsetMinutes = offsetAbs % 3600;
+ offsetAbs -= offsetMinutes;
+ offsetMinutes /= 60;
+
+ int offsetHours = offsetAbs / 3600;
+
+ QString raw = time.toString("yyyy-MM-ddTHH:mm:ss");
+ raw += (negative ? QChar('-') : QChar('+'));
+ raw += QString("%1").arg(offsetHours, 2, 10, QChar('0'));
+ raw += ":";
+ raw += QString("%1").arg(offsetMinutes, 2, 10, QChar('0'));
+ return raw;
+}
diff --git a/api/logic/minecraft/ParseUtils.h b/api/logic/minecraft/ParseUtils.h
new file mode 100644
index 00000000..2b367a10
--- /dev/null
+++ b/api/logic/minecraft/ParseUtils.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <QString>
+#include <QDateTime>
+
+#include "multimc_logic_export.h"
+
+/// take the timestamp used by S3 and turn it into QDateTime
+MULTIMC_LOGIC_EXPORT QDateTime timeFromS3Time(QString str);
+
+/// take a timestamp and convert it into an S3 timestamp
+MULTIMC_LOGIC_EXPORT QString timeToS3Time(QDateTime);
diff --git a/api/logic/minecraft/ProfilePatch.h b/api/logic/minecraft/ProfilePatch.h
new file mode 100644
index 00000000..f0c65360
--- /dev/null
+++ b/api/logic/minecraft/ProfilePatch.h
@@ -0,0 +1,104 @@
+#pragma once
+
+#include <memory>
+#include <QList>
+#include <QJsonDocument>
+#include <QDateTime>
+#include "JarMod.h"
+
+class MinecraftProfile;
+
+enum ProblemSeverity
+{
+ PROBLEM_NONE,
+ PROBLEM_WARNING,
+ PROBLEM_ERROR
+};
+
+/// where is a version from?
+enum VersionSource
+{
+ Local, //!< version loaded from a file in the cache.
+ Remote, //!< incomplete version on a remote server.
+};
+
+class PatchProblem
+{
+public:
+ PatchProblem(ProblemSeverity severity, const QString & description)
+ {
+ m_severity = severity;
+ m_description = description;
+ }
+ const QString & getDescription() const
+ {
+ return m_description;
+ }
+ const ProblemSeverity getSeverity() const
+ {
+ return m_severity;
+ }
+private:
+ ProblemSeverity m_severity;
+ QString m_description;
+};
+
+class ProfilePatch : public std::enable_shared_from_this<ProfilePatch>
+{
+public:
+ virtual ~ProfilePatch(){};
+ virtual void applyTo(MinecraftProfile *profile) = 0;
+
+ virtual bool isMinecraftVersion() = 0;
+ virtual bool hasJarMods() = 0;
+ virtual QList<JarmodPtr> getJarMods() = 0;
+
+ virtual bool isMoveable() = 0;
+ virtual bool isCustomizable() = 0;
+ virtual bool isRevertible() = 0;
+ virtual bool isRemovable() = 0;
+ virtual bool isCustom() = 0;
+ virtual bool isEditable() = 0;
+ virtual bool isVersionChangeable() = 0;
+
+ virtual void setOrder(int order) = 0;
+ virtual int getOrder() = 0;
+
+ virtual QString getID() = 0;
+ virtual QString getName() = 0;
+ virtual QString getVersion() = 0;
+ virtual QDateTime getReleaseDateTime() = 0;
+
+ virtual QString getFilename() = 0;
+
+ virtual VersionSource getVersionSource() = 0;
+
+ virtual std::shared_ptr<class VersionFile> getVersionFile() = 0;
+
+ virtual const QList<PatchProblem>& getProblems()
+ {
+ return m_problems;
+ }
+ virtual void addProblem(ProblemSeverity severity, const QString &description)
+ {
+ if(severity > m_problemSeverity)
+ {
+ m_problemSeverity = severity;
+ }
+ m_problems.append(PatchProblem(severity, description));
+ }
+ virtual ProblemSeverity getProblemSeverity()
+ {
+ return m_problemSeverity;
+ }
+ virtual bool hasFailed()
+ {
+ return getProblemSeverity() == PROBLEM_ERROR;
+ }
+
+protected:
+ QList<PatchProblem> m_problems;
+ ProblemSeverity m_problemSeverity = PROBLEM_NONE;
+};
+
+typedef std::shared_ptr<ProfilePatch> ProfilePatchPtr;
diff --git a/api/logic/minecraft/ProfileStrategy.h b/api/logic/minecraft/ProfileStrategy.h
new file mode 100644
index 00000000..b4dfc4b3
--- /dev/null
+++ b/api/logic/minecraft/ProfileStrategy.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "ProfileUtils.h"
+
+class MinecraftProfile;
+
+class ProfileStrategy
+{
+ friend class MinecraftProfile;
+public:
+ virtual ~ProfileStrategy(){};
+
+ /// load the patch files into the profile
+ virtual void load() = 0;
+
+ /// reset the order of patches
+ virtual bool resetOrder() = 0;
+
+ /// save the order of patches, given the order
+ virtual bool saveOrder(ProfileUtils::PatchOrder order) = 0;
+
+ /// install a list of jar mods into the instance
+ virtual bool installJarMods(QStringList filepaths) = 0;
+
+ /// remove any files or records that constitute the version patch
+ virtual bool removePatch(ProfilePatchPtr jarMod) = 0;
+
+ /// make the patch custom, if possible
+ virtual bool customizePatch(ProfilePatchPtr patch) = 0;
+
+ /// revert the custom patch to 'vanilla', if possible
+ virtual bool revertPatch(ProfilePatchPtr patch) = 0;
+protected:
+ MinecraftProfile *profile;
+};
diff --git a/api/logic/minecraft/ProfileUtils.cpp b/api/logic/minecraft/ProfileUtils.cpp
new file mode 100644
index 00000000..ef9b3b28
--- /dev/null
+++ b/api/logic/minecraft/ProfileUtils.cpp
@@ -0,0 +1,191 @@
+#include "ProfileUtils.h"
+#include "minecraft/VersionFilterData.h"
+#include "minecraft/onesix/OneSixVersionFormat.h"
+#include "Json.h"
+#include <QDebug>
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QRegularExpression>
+#include <QSaveFile>
+
+namespace ProfileUtils
+{
+
+static const int currentOrderFileVersion = 1;
+
+bool writeOverrideOrders(QString path, const PatchOrder &order)
+{
+ QJsonObject obj;
+ obj.insert("version", currentOrderFileVersion);
+ QJsonArray orderArray;
+ for(auto str: order)
+ {
+ orderArray.append(str);
+ }
+ obj.insert("order", orderArray);
+ QSaveFile orderFile(path);
+ if (!orderFile.open(QFile::WriteOnly))
+ {
+ qCritical() << "Couldn't open" << orderFile.fileName()
+ << "for writing:" << orderFile.errorString();
+ return false;
+ }
+ auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented);
+ if(orderFile.write(data) != data.size())
+ {
+ qCritical() << "Couldn't write all the data into" << orderFile.fileName()
+ << "because:" << orderFile.errorString();
+ return false;
+ }
+ if(!orderFile.commit())
+ {
+ qCritical() << "Couldn't save" << orderFile.fileName()
+ << "because:" << orderFile.errorString();
+ }
+ return true;
+}
+
+bool readOverrideOrders(QString path, PatchOrder &order)
+{
+ QFile orderFile(path);
+ if (!orderFile.exists())
+ {
+ qWarning() << "Order file doesn't exist. Ignoring.";
+ return false;
+ }
+ if (!orderFile.open(QFile::ReadOnly))
+ {
+ qCritical() << "Couldn't open" << orderFile.fileName()
+ << " for reading:" << orderFile.errorString();
+ qWarning() << "Ignoring overriden order";
+ return false;
+ }
+
+ // and it's valid JSON
+ QJsonParseError error;
+ QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error);
+ if (error.error != QJsonParseError::NoError)
+ {
+ qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString();
+ qWarning() << "Ignoring overriden order";
+ return false;
+ }
+
+ // and then read it and process it if all above is true.
+ try
+ {
+ auto obj = Json::requireObject(doc);
+ // check order file version.
+ auto version = Json::requireInteger(obj.value("version"));
+ if (version != currentOrderFileVersion)
+ {
+ throw JSONValidationError(QObject::tr("Invalid order file version, expected %1")
+ .arg(currentOrderFileVersion));
+ }
+ auto orderArray = Json::requireArray(obj.value("order"));
+ for(auto item: orderArray)
+ {
+ order.append(Json::requireString(item));
+ }
+ }
+ catch (JSONValidationError &err)
+ {
+ qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format";
+ qWarning() << "Ignoring overriden order";
+ order.clear();
+ return false;
+ }
+ return true;
+}
+
+static VersionFilePtr createErrorVersionFile(QString fileId, QString filepath, QString error)
+{
+ auto outError = std::make_shared<VersionFile>();
+ outError->fileId = outError->name = fileId;
+ outError->filename = filepath;
+ outError->addProblem(PROBLEM_ERROR, error);
+ return outError;
+}
+
+static VersionFilePtr guardedParseJson(const QJsonDocument & doc,const QString &fileId,const QString &filepath,const bool &requireOrder)
+{
+ try
+ {
+ return OneSixVersionFormat::versionFileFromJson(doc, filepath, requireOrder);
+ }
+ catch (Exception & e)
+ {
+ return createErrorVersionFile(fileId, filepath, e.cause());
+ }
+}
+
+VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder)
+{
+ QFile file(fileInfo.absoluteFilePath());
+ if (!file.open(QFile::ReadOnly))
+ {
+ auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString());
+ return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr);
+ }
+ QJsonParseError error;
+ auto data = file.readAll();
+ QJsonDocument doc = QJsonDocument::fromJson(data, &error);
+ file.close();
+ if (error.error != QJsonParseError::NoError)
+ {
+ int line = 1;
+ int column = 0;
+ for(int i = 0; i < error.offset; i++)
+ {
+ if(data[i] == '\n')
+ {
+ line++;
+ column = 0;
+ continue;
+ }
+ column++;
+ }
+ auto errorStr = QObject::tr("Unable to process the version file %1: %2 at line %3 column %4.")
+ .arg(fileInfo.fileName(), error.errorString())
+ .arg(line).arg(column);
+ return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr);
+ }
+ return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder);
+}
+
+VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo)
+{
+ QFile file(fileInfo.absoluteFilePath());
+ if (!file.open(QFile::ReadOnly))
+ {
+ auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString());
+ return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr);
+ }
+ QJsonDocument doc = QJsonDocument::fromBinaryData(file.readAll());
+ file.close();
+ if (doc.isNull())
+ {
+ file.remove();
+ throw JSONValidationError(QObject::tr("Unable to process the version file %1.").arg(fileInfo.fileName()));
+ }
+ return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), false);
+}
+
+void removeLwjglFromPatch(VersionFilePtr patch)
+{
+ auto filter = [](QList<LibraryPtr>& libs)
+ {
+ QList<LibraryPtr> filteredLibs;
+ for (auto lib : libs)
+ {
+ if (!g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix()))
+ {
+ filteredLibs.append(lib);
+ }
+ }
+ libs = filteredLibs;
+ };
+ filter(patch->libraries);
+}
+}
diff --git a/api/logic/minecraft/ProfileUtils.h b/api/logic/minecraft/ProfileUtils.h
new file mode 100644
index 00000000..267fd42b
--- /dev/null
+++ b/api/logic/minecraft/ProfileUtils.h
@@ -0,0 +1,25 @@
+#pragma once
+#include "Library.h"
+#include "VersionFile.h"
+
+namespace ProfileUtils
+{
+typedef QStringList PatchOrder;
+
+/// Read and parse a OneSix format order file
+bool readOverrideOrders(QString path, PatchOrder &order);
+
+/// Write a OneSix format order file
+bool writeOverrideOrders(QString path, const PatchOrder &order);
+
+
+/// Parse a version file in JSON format
+VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder);
+
+/// Parse a version file in binary JSON format
+VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo);
+
+/// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files.
+void removeLwjglFromPatch(VersionFilePtr patch);
+
+}
diff --git a/api/logic/minecraft/Rule.cpp b/api/logic/minecraft/Rule.cpp
new file mode 100644
index 00000000..c8ba297b
--- /dev/null
+++ b/api/logic/minecraft/Rule.cpp
@@ -0,0 +1,93 @@
+/* Copyright 2013-2015 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 <QJsonObject>
+#include <QJsonArray>
+
+#include "Rule.h"
+
+RuleAction RuleAction_fromString(QString name)
+{
+ if (name == "allow")
+ return Allow;
+ if (name == "disallow")
+ return Disallow;
+ return Defer;
+}
+
+QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject &objectWithRules)
+{
+ QList<std::shared_ptr<Rule>> rules;
+ auto rulesVal = objectWithRules.value("rules");
+ if (!rulesVal.isArray())
+ return rules;
+
+ QJsonArray ruleList = rulesVal.toArray();
+ for (auto ruleVal : ruleList)
+ {
+ std::shared_ptr<Rule> rule;
+ if (!ruleVal.isObject())
+ continue;
+ auto ruleObj = ruleVal.toObject();
+ auto actionVal = ruleObj.value("action");
+ if (!actionVal.isString())
+ continue;
+ auto action = RuleAction_fromString(actionVal.toString());
+ if (action == Defer)
+ continue;
+
+ auto osVal = ruleObj.value("os");
+ if (!osVal.isObject())
+ {
+ // add a new implicit action rule
+ rules.append(ImplicitRule::create(action));
+ continue;
+ }
+
+ auto osObj = osVal.toObject();
+ auto osNameVal = osObj.value("name");
+ if (!osNameVal.isString())
+ continue;
+ OpSys requiredOs = OpSys_fromString(osNameVal.toString());
+ QString versionRegex = osObj.value("version").toString();
+ // add a new OS rule
+ rules.append(OsRule::create(action, requiredOs, versionRegex));
+ }
+ return rules;
+}
+
+QJsonObject ImplicitRule::toJson()
+{
+ QJsonObject ruleObj;
+ ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow"));
+ return ruleObj;
+}
+
+QJsonObject OsRule::toJson()
+{
+ QJsonObject ruleObj;
+ ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow"));
+ QJsonObject osObj;
+ {
+ osObj.insert("name", OpSys_toString(m_system));
+ if(!m_version_regexp.isEmpty())
+ {
+ osObj.insert("version", m_version_regexp);
+ }
+ }
+ ruleObj.insert("os", osObj);
+ return ruleObj;
+}
+
diff --git a/api/logic/minecraft/Rule.h b/api/logic/minecraft/Rule.h
new file mode 100644
index 00000000..c8bf6eaa
--- /dev/null
+++ b/api/logic/minecraft/Rule.h
@@ -0,0 +1,101 @@
+/* Copyright 2013-2015 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>
+#include <QList>
+#include <QJsonObject>
+#include <memory>
+#include "OpSys.h"
+
+class Library;
+class Rule;
+
+enum RuleAction
+{
+ Allow,
+ Disallow,
+ Defer
+};
+
+QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject &objectWithRules);
+
+class Rule
+{
+protected:
+ RuleAction m_result;
+ virtual bool applies(const Library *parent) = 0;
+
+public:
+ Rule(RuleAction result) : m_result(result)
+ {
+ }
+ virtual ~Rule() {};
+ virtual QJsonObject toJson() = 0;
+ RuleAction apply(const Library *parent)
+ {
+ if (applies(parent))
+ return m_result;
+ else
+ return Defer;
+ }
+};
+
+class OsRule : public Rule
+{
+private:
+ // the OS
+ OpSys m_system;
+ // the OS version regexp
+ QString m_version_regexp;
+
+protected:
+ virtual bool applies(const Library *)
+ {
+ return (m_system == currentSystem);
+ }
+ OsRule(RuleAction result, OpSys system, QString version_regexp)
+ : Rule(result), m_system(system), m_version_regexp(version_regexp)
+ {
+ }
+
+public:
+ virtual QJsonObject toJson();
+ static std::shared_ptr<OsRule> create(RuleAction result, OpSys system,
+ QString version_regexp)
+ {
+ return std::shared_ptr<OsRule>(new OsRule(result, system, version_regexp));
+ }
+};
+
+class ImplicitRule : public Rule
+{
+protected:
+ virtual bool applies(const Library *)
+ {
+ return true;
+ }
+ ImplicitRule(RuleAction result) : Rule(result)
+ {
+ }
+
+public:
+ virtual QJsonObject toJson();
+ static std::shared_ptr<ImplicitRule> create(RuleAction result)
+ {
+ return std::shared_ptr<ImplicitRule>(new ImplicitRule(result));
+ }
+};
diff --git a/api/logic/minecraft/VersionBuildError.h b/api/logic/minecraft/VersionBuildError.h
new file mode 100644
index 00000000..fda453e5
--- /dev/null
+++ b/api/logic/minecraft/VersionBuildError.h
@@ -0,0 +1,58 @@
+#include "Exception.h"
+
+class VersionBuildError : public Exception
+{
+public:
+ explicit VersionBuildError(QString cause) : Exception(cause) {}
+ virtual ~VersionBuildError() noexcept
+ {
+ }
+};
+
+/**
+ * the base version file was meant for a newer version of the vanilla launcher than we support
+ */
+class LauncherVersionError : public VersionBuildError
+{
+public:
+ LauncherVersionError(int actual, int supported)
+ : VersionBuildError(QObject::tr(
+ "The base version file of this instance was meant for a newer (%1) "
+ "version of the vanilla launcher than this version of MultiMC supports (%2).")
+ .arg(actual)
+ .arg(supported)) {};
+ virtual ~LauncherVersionError() noexcept
+ {
+ }
+};
+
+/**
+ * some patch was intended for a different version of minecraft
+ */
+class MinecraftVersionMismatch : public VersionBuildError
+{
+public:
+ MinecraftVersionMismatch(QString fileId, QString mcVersion, QString parentMcVersion)
+ : VersionBuildError(QObject::tr("The patch %1 is for a different version of Minecraft "
+ "(%2) than that of the instance (%3).")
+ .arg(fileId)
+ .arg(mcVersion)
+ .arg(parentMcVersion)) {};
+ virtual ~MinecraftVersionMismatch() noexcept
+ {
+ }
+};
+
+/**
+ * files required for the version are not (yet?) present
+ */
+class VersionIncomplete : public VersionBuildError
+{
+public:
+ VersionIncomplete(QString missingPatch)
+ : VersionBuildError(QObject::tr("Version is incomplete: missing %1.")
+ .arg(missingPatch)) {};
+ virtual ~VersionIncomplete() noexcept
+ {
+ }
+};
diff --git a/api/logic/minecraft/VersionFile.cpp b/api/logic/minecraft/VersionFile.cpp
new file mode 100644
index 00000000..573c4cb4
--- /dev/null
+++ b/api/logic/minecraft/VersionFile.cpp
@@ -0,0 +1,60 @@
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+#include "minecraft/VersionFile.h"
+#include "minecraft/Library.h"
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/JarMod.h"
+#include "ParseUtils.h"
+
+#include "VersionBuildError.h"
+#include <Version.h>
+
+bool VersionFile::isMinecraftVersion()
+{
+ return fileId == "net.minecraft";
+}
+
+bool VersionFile::hasJarMods()
+{
+ return !jarMods.isEmpty();
+}
+
+void VersionFile::applyTo(MinecraftProfile *profile)
+{
+ auto theirVersion = profile->getMinecraftVersion();
+ if (!theirVersion.isNull() && !dependsOnMinecraftVersion.isNull())
+ {
+ if (QRegExp(dependsOnMinecraftVersion, Qt::CaseInsensitive, QRegExp::Wildcard).indexIn(theirVersion) == -1)
+ {
+ throw MinecraftVersionMismatch(fileId, dependsOnMinecraftVersion, theirVersion);
+ }
+ }
+ profile->applyMinecraftVersion(minecraftVersion);
+ profile->applyMainClass(mainClass);
+ profile->applyAppletClass(appletClass);
+ profile->applyMinecraftArguments(minecraftArguments);
+ if (isMinecraftVersion())
+ {
+ profile->applyMinecraftVersionType(type);
+ }
+ profile->applyMinecraftAssets(mojangAssetIndex);
+ profile->applyTweakers(addTweakers);
+
+ profile->applyJarMods(jarMods);
+ profile->applyTraits(traits);
+
+ for (auto library : libraries)
+ {
+ profile->applyLibrary(library);
+ }
+ profile->applyProblemSeverity(getProblemSeverity());
+ auto iter = mojangDownloads.begin();
+ while(iter != mojangDownloads.end())
+ {
+ profile->applyMojangDownload(iter.key(), iter.value());
+ iter++;
+ }
+}
diff --git a/api/logic/minecraft/VersionFile.h b/api/logic/minecraft/VersionFile.h
new file mode 100644
index 00000000..1b692f0f
--- /dev/null
+++ b/api/logic/minecraft/VersionFile.h
@@ -0,0 +1,195 @@
+#pragma once
+
+#include <QString>
+#include <QStringList>
+#include <QDateTime>
+#include <QSet>
+
+#include <memory>
+#include "minecraft/OpSys.h"
+#include "minecraft/Rule.h"
+#include "ProfilePatch.h"
+#include "Library.h"
+#include "JarMod.h"
+
+class MinecraftProfile;
+class VersionFile;
+struct MojangDownloadInfo;
+struct MojangAssetIndexInfo;
+
+typedef std::shared_ptr<VersionFile> VersionFilePtr;
+class VersionFile : public ProfilePatch
+{
+ friend class MojangVersionFormat;
+ friend class OneSixVersionFormat;
+public: /* methods */
+ virtual void applyTo(MinecraftProfile *profile) override;
+ virtual bool isMinecraftVersion() override;
+ virtual bool hasJarMods() override;
+ virtual int getOrder() override
+ {
+ return order;
+ }
+ virtual void setOrder(int order) override
+ {
+ this->order = order;
+ }
+ virtual QList<JarmodPtr> getJarMods() override
+ {
+ return jarMods;
+ }
+ virtual QString getID() override
+ {
+ return fileId;
+ }
+ virtual QString getName() override
+ {
+ return name;
+ }
+ virtual QString getVersion() override
+ {
+ return version;
+ }
+ virtual QString getFilename() override
+ {
+ return filename;
+ }
+ virtual QDateTime getReleaseDateTime() override
+ {
+ return m_releaseTime;
+ }
+ VersionSource getVersionSource() override
+ {
+ return Local;
+ }
+
+ std::shared_ptr<class VersionFile> getVersionFile() override
+ {
+ return std::dynamic_pointer_cast<VersionFile>(shared_from_this());
+ }
+
+ virtual bool isCustom() override
+ {
+ return !m_isVanilla;
+ };
+ virtual bool isCustomizable() override
+ {
+ return m_isCustomizable;
+ }
+ virtual bool isRemovable() override
+ {
+ return m_isRemovable;
+ }
+ virtual bool isRevertible() override
+ {
+ return m_isRevertible;
+ }
+ virtual bool isMoveable() override
+ {
+ return m_isMovable;
+ }
+ virtual bool isEditable() override
+ {
+ return isCustom();
+ }
+ virtual bool isVersionChangeable() override
+ {
+ return false;
+ }
+
+ void setVanilla (bool state)
+ {
+ m_isVanilla = state;
+ }
+ void setRemovable (bool state)
+ {
+ m_isRemovable = state;
+ }
+ void setRevertible (bool state)
+ {
+ m_isRevertible = state;
+ }
+ void setCustomizable (bool state)
+ {
+ m_isCustomizable = state;
+ }
+ void setMovable (bool state)
+ {
+ m_isMovable = state;
+ }
+
+
+public: /* data */
+ /// MultiMC: order hint for this version file if no explicit order is set
+ int order = 0;
+
+ // Flags for UI and version file manipulation in general
+ bool m_isVanilla = false;
+ bool m_isRemovable = false;
+ bool m_isRevertible = false;
+ bool m_isCustomizable = false;
+ bool m_isMovable = false;
+
+ /// MultiMC: filename of the file this was loaded from
+ QString filename;
+
+ /// MultiMC: human readable name of this package
+ QString name;
+
+ /// MultiMC: package ID of this package
+ QString fileId;
+
+ /// MultiMC: version of this package
+ QString version;
+
+ /// MultiMC: dependency on a Minecraft version
+ QString dependsOnMinecraftVersion;
+
+ /// Mojang: used to version the Mojang version format
+ int minimumLauncherVersion = -1;
+
+ /// Mojang: version of Minecraft this is
+ QString minecraftVersion;
+
+ /// Mojang: class to launch Minecraft with
+ QString mainClass;
+
+ /// MultiMC: DEPRECATED class to launch legacy Minecraft with (ambed in a custom window)
+ QString appletClass;
+
+ /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution)
+ QString minecraftArguments;
+
+ /// Mojang: type of the Minecraft version
+ QString type;
+
+ /// Mojang: the time this version was actually released by Mojang
+ QDateTime m_releaseTime;
+
+ /// Mojang: the time this version was last updated by Mojang
+ QDateTime m_updateTime;
+
+ /// Mojang: DEPRECATED asset group to be used with Minecraft
+ QString assets;
+
+ /// MultiMC: list of tweaker mod arguments for launchwrapper
+ QStringList addTweakers;
+
+ /// Mojang: list of libraries to add to the version
+ QList<LibraryPtr> libraries;
+
+ /// MultiMC: list of attached traits of this version file - used to enable features
+ QSet<QString> traits;
+
+ /// MultiMC: list of jar mods added to this version
+ QList<JarmodPtr> jarMods;
+
+public:
+ // Mojang: list of 'downloads' - client jar, server jar, windows server exe, maybe more.
+ QMap <QString, std::shared_ptr<MojangDownloadInfo>> mojangDownloads;
+
+ // Mojang: extended asset index download information
+ std::shared_ptr<MojangAssetIndexInfo> mojangAssetIndex;
+};
+
+
diff --git a/api/logic/minecraft/VersionFilterData.cpp b/api/logic/minecraft/VersionFilterData.cpp
new file mode 100644
index 00000000..0c4a6e3d
--- /dev/null
+++ b/api/logic/minecraft/VersionFilterData.cpp
@@ -0,0 +1,75 @@
+#include "VersionFilterData.h"
+#include "ParseUtils.h"
+
+VersionFilterData g_VersionFilterData = VersionFilterData();
+
+VersionFilterData::VersionFilterData()
+{
+ // 1.3.*
+ auto libs13 =
+ QList<FMLlib>{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false},
+ {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false},
+ {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}};
+
+ fmlLibsMapping["1.3.2"] = libs13;
+
+ // 1.4.*
+ auto libs14 = QList<FMLlib>{
+ {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false},
+ {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false},
+ {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false},
+ {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", false}};
+
+ fmlLibsMapping["1.4"] = libs14;
+ fmlLibsMapping["1.4.1"] = libs14;
+ fmlLibsMapping["1.4.2"] = libs14;
+ fmlLibsMapping["1.4.3"] = libs14;
+ fmlLibsMapping["1.4.4"] = libs14;
+ fmlLibsMapping["1.4.5"] = libs14;
+ fmlLibsMapping["1.4.6"] = libs14;
+ fmlLibsMapping["1.4.7"] = libs14;
+
+ // 1.5
+ fmlLibsMapping["1.5"] = QList<FMLlib>{
+ {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false},
+ {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false},
+ {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false},
+ {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true},
+ {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", false},
+ {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}};
+
+ // 1.5.1
+ fmlLibsMapping["1.5.1"] = QList<FMLlib>{
+ {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false},
+ {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false},
+ {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false},
+ {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true},
+ {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", false},
+ {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}};
+
+ // 1.5.2
+ fmlLibsMapping["1.5.2"] = QList<FMLlib>{
+ {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false},
+ {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false},
+ {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false},
+ {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true},
+ {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", false},
+ {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}};
+
+ // don't use installers for those.
+ forgeInstallerBlacklist = QSet<QString>({"1.5.2"});
+ // these won't show up in version lists because they are extremely bad and dangerous
+ legacyBlacklist = QSet<QString>({"rd-160052"});
+ /*
+ * nothing older than this will be accepted from Mojang servers
+ * (these versions need to be tested by us first)
+ */
+ legacyCutoffDate = timeFromS3Time("2013-06-25T15:08:56+02:00");
+ lwjglWhitelist =
+ QSet<QString>{"net.java.jinput:jinput", "net.java.jinput:jinput-platform",
+ "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl",
+ "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform"};
+
+ // Version list magic
+ recommendedMinecraftVersion = "1.7.10";
+}
diff --git a/api/logic/minecraft/VersionFilterData.h b/api/logic/minecraft/VersionFilterData.h
new file mode 100644
index 00000000..f7d4ebe7
--- /dev/null
+++ b/api/logic/minecraft/VersionFilterData.h
@@ -0,0 +1,32 @@
+#pragma once
+#include <QMap>
+#include <QString>
+#include <QSet>
+#include <QDateTime>
+
+#include "multimc_logic_export.h"
+
+struct FMLlib
+{
+ QString filename;
+ QString checksum;
+ bool ours;
+};
+
+struct VersionFilterData
+{
+ VersionFilterData();
+ // mapping between minecraft versions and FML libraries required
+ QMap<QString, QList<FMLlib>> fmlLibsMapping;
+ // set of minecraft versions for which using forge installers is blacklisted
+ QSet<QString> forgeInstallerBlacklist;
+ // set of 'legacy' versions that will not show up in the version lists.
+ QSet<QString> legacyBlacklist;
+ // no new versions below this date will be accepted from Mojang servers
+ QDateTime legacyCutoffDate;
+ // Libraries that belong to LWJGL
+ QSet<QString> lwjglWhitelist;
+ // Currently recommended minecraft version
+ QString recommendedMinecraftVersion;
+};
+extern VersionFilterData MULTIMC_LOGIC_EXPORT g_VersionFilterData;
diff --git a/api/logic/minecraft/World.cpp b/api/logic/minecraft/World.cpp
new file mode 100644
index 00000000..6081a8ec
--- /dev/null
+++ b/api/logic/minecraft/World.cpp
@@ -0,0 +1,385 @@
+/* Copyright 2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QDir>
+#include <QString>
+#include <QDebug>
+#include <QSaveFile>
+#include "World.h"
+
+#include "GZip.h"
+#include <MMCZip.h>
+#include <FileSystem.h>
+#include <sstream>
+#include <io/stream_reader.h>
+#include <tag_string.h>
+#include <tag_primitive.h>
+#include <quazip.h>
+#include <quazipfile.h>
+#include <quazipdir.h>
+
+std::unique_ptr <nbt::tag_compound> parseLevelDat(QByteArray data)
+{
+ QByteArray output;
+ if(!GZip::unzip(data, output))
+ {
+ return nullptr;
+ }
+ std::istringstream foo(std::string(output.constData(), output.size()));
+ auto pair = nbt::io::read_compound(foo);
+
+ if(pair.first != "")
+ return nullptr;
+
+ if(pair.second == nullptr)
+ return nullptr;
+
+ return std::move(pair.second);
+}
+
+QByteArray serializeLevelDat(nbt::tag_compound * levelInfo)
+{
+ std::ostringstream s;
+ nbt::io::write_tag("", *levelInfo, s);
+ QByteArray val( s.str().data(), (int) s.str().size() );
+ return val;
+}
+
+QString getLevelDatFromFS(const QFileInfo &file)
+{
+ QDir worldDir(file.filePath());
+ if(!file.isDir() || !worldDir.exists("level.dat"))
+ {
+ return QString();
+ }
+ return worldDir.absoluteFilePath("level.dat");
+}
+
+QByteArray getLevelDatDataFromFS(const QFileInfo &file)
+{
+ auto fullFilePath = getLevelDatFromFS(file);
+ if(fullFilePath.isNull())
+ {
+ return QByteArray();
+ }
+ QFile f(fullFilePath);
+ if(!f.open(QIODevice::ReadOnly))
+ {
+ return QByteArray();
+ }
+ return f.readAll();
+}
+
+bool putLevelDatDataToFS(const QFileInfo &file, QByteArray & data)
+{
+ auto fullFilePath = getLevelDatFromFS(file);
+ if(fullFilePath.isNull())
+ {
+ return false;
+ }
+ QSaveFile f(fullFilePath);
+ if(!f.open(QIODevice::WriteOnly))
+ {
+ return false;
+ }
+ QByteArray compressed;
+ if(!GZip::zip(data, compressed))
+ {
+ return false;
+ }
+ if(f.write(compressed) != compressed.size())
+ {
+ f.cancelWriting();
+ return false;
+ }
+ return f.commit();
+}
+
+World::World(const QFileInfo &file)
+{
+ repath(file);
+}
+
+void World::repath(const QFileInfo &file)
+{
+ m_containerFile = file;
+ m_folderName = file.fileName();
+ if(file.isFile() && file.suffix() == "zip")
+ {
+ readFromZip(file);
+ }
+ else if(file.isDir())
+ {
+ readFromFS(file);
+ }
+}
+
+void World::readFromFS(const QFileInfo &file)
+{
+ auto bytes = getLevelDatDataFromFS(file);
+ if(bytes.isEmpty())
+ {
+ is_valid = false;
+ return;
+ }
+ loadFromLevelDat(bytes);
+ levelDatTime = file.lastModified();
+}
+
+void World::readFromZip(const QFileInfo &file)
+{
+ QuaZip zip(file.absoluteFilePath());
+ is_valid = zip.open(QuaZip::mdUnzip);
+ if (!is_valid)
+ {
+ return;
+ }
+ auto location = MMCZip::findFileInZip(&zip, "level.dat");
+ is_valid = !location.isEmpty();
+ if (!is_valid)
+ {
+ return;
+ }
+ m_containerOffsetPath = location;
+ QuaZipFile zippedFile(&zip);
+ // read the install profile
+ is_valid = zip.setCurrentFile(location + "level.dat");
+ if (!is_valid)
+ {
+ return;
+ }
+ is_valid = zippedFile.open(QIODevice::ReadOnly);
+ QuaZipFileInfo64 levelDatInfo;
+ zippedFile.getFileInfo(&levelDatInfo);
+ auto modTime = levelDatInfo.getNTFSmTime();
+ if(!modTime.isValid())
+ {
+ modTime = levelDatInfo.dateTime;
+ }
+ levelDatTime = modTime;
+ if (!is_valid)
+ {
+ return;
+ }
+ loadFromLevelDat(zippedFile.readAll());
+ zippedFile.close();
+}
+
+bool World::install(const QString &to, const QString &name)
+{
+ auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to));
+ if(!FS::ensureFolderPathExists(finalPath))
+ {
+ return false;
+ }
+ bool ok = false;
+ if(m_containerFile.isFile())
+ {
+ QuaZip zip(m_containerFile.absoluteFilePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ {
+ return false;
+ }
+ ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath).isEmpty();
+ }
+ else if(m_containerFile.isDir())
+ {
+ QString from = m_containerFile.filePath();
+ ok = FS::copy(from, finalPath)();
+ }
+
+ if(ok && !name.isEmpty() && m_actualName != name)
+ {
+ World newWorld(finalPath);
+ if(newWorld.isValid())
+ {
+ newWorld.rename(name);
+ }
+ }
+ return ok;
+}
+
+bool World::rename(const QString &newName)
+{
+ if(m_containerFile.isFile())
+ {
+ return false;
+ }
+
+ auto data = getLevelDatDataFromFS(m_containerFile);
+ if(data.isEmpty())
+ {
+ return false;
+ }
+
+ auto worldData = parseLevelDat(data);
+ if(!worldData)
+ {
+ return false;
+ }
+ auto &val = worldData->at("Data");
+ if(val.get_type() != nbt::tag_type::Compound)
+ {
+ return false;
+ }
+ auto &dataCompound = val.as<nbt::tag_compound>();
+ dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data()));
+ data = serializeLevelDat(worldData.get());
+
+ putLevelDatDataToFS(m_containerFile, data);
+
+ m_actualName = newName;
+
+ QDir parentDir(m_containerFile.absoluteFilePath());
+ parentDir.cdUp();
+ QFile container(m_containerFile.absoluteFilePath());
+ auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath());
+ container.rename(parentDir.absoluteFilePath(dirName));
+
+ return true;
+}
+
+static QString read_string (nbt::value& parent, const char * name, const QString & fallback = QString())
+{
+ try
+ {
+ auto &namedValue = parent.at(name);
+ if(namedValue.get_type() != nbt::tag_type::String)
+ {
+ return fallback;
+ }
+ auto & tag_str = namedValue.as<nbt::tag_string>();
+ return QString::fromStdString(tag_str.get());
+ }
+ catch(std::out_of_range e)
+ {
+ // fallback for old world formats
+ qWarning() << "String NBT tag" << name << "could not be found. Defaulting to" << fallback;
+ return fallback;
+ }
+ catch(std::bad_cast e)
+ {
+ // type mismatch
+ qWarning() << "NBT tag" << name << "could not be converted to string. Defaulting to" << fallback;
+ return fallback;
+ }
+};
+
+static int64_t read_long (nbt::value& parent, const char * name, const int64_t & fallback = 0)
+{
+ try
+ {
+ auto &namedValue = parent.at(name);
+ if(namedValue.get_type() != nbt::tag_type::Long)
+ {
+ return fallback;
+ }
+ auto & tag_str = namedValue.as<nbt::tag_long>();
+ return tag_str.get();
+ }
+ catch(std::out_of_range e)
+ {
+ // fallback for old world formats
+ qWarning() << "Long NBT tag" << name << "could not be found. Defaulting to" << fallback;
+ return fallback;
+ }
+ catch(std::bad_cast e)
+ {
+ // type mismatch
+ qWarning() << "NBT tag" << name << "could not be converted to long. Defaulting to" << fallback;
+ return fallback;
+ }
+};
+
+void World::loadFromLevelDat(QByteArray data)
+{
+ try
+ {
+ auto levelData = parseLevelDat(data);
+ if(!levelData)
+ {
+ is_valid = false;
+ return;
+ }
+
+ auto &val = levelData->at("Data");
+ is_valid = val.get_type() == nbt::tag_type::Compound;
+ if(!is_valid)
+ return;
+
+ m_actualName = read_string(val, "LevelName", m_folderName);
+
+
+ int64_t temp = read_long(val, "LastPlayed", 0);
+ if(temp == 0)
+ {
+ m_lastPlayed = levelDatTime;
+ }
+ else
+ {
+ m_lastPlayed = QDateTime::fromMSecsSinceEpoch(temp);
+ }
+
+ m_randomSeed = read_long(val, "RandomSeed", 0);
+
+ qDebug() << "World Name:" << m_actualName;
+ qDebug() << "Last Played:" << m_lastPlayed.toString();
+ qDebug() << "Seed:" << m_randomSeed;
+ }
+ catch (nbt::io::input_error e)
+ {
+ qWarning() << "Unable to load" << m_folderName << ":" << e.what();
+ is_valid = false;
+ return;
+ }
+}
+
+bool World::replace(World &with)
+{
+ if (!destroy())
+ return false;
+ bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())();
+ if (success)
+ {
+ m_folderName = with.m_folderName;
+ m_containerFile.refresh();
+ }
+ return success;
+}
+
+bool World::destroy()
+{
+ if(!is_valid) return false;
+ if (m_containerFile.isDir())
+ {
+ QDir d(m_containerFile.filePath());
+ return d.removeRecursively();
+ }
+ else if(m_containerFile.isFile())
+ {
+ QFile file(m_containerFile.absoluteFilePath());
+ return file.remove();
+ }
+ return true;
+}
+
+bool World::operator==(const World &other) const
+{
+ return is_valid == other.is_valid && folderName() == other.folderName();
+}
+bool World::strongCompare(const World &other) const
+{
+ return is_valid == other.is_valid && folderName() == other.folderName();
+}
diff --git a/api/logic/minecraft/World.h b/api/logic/minecraft/World.h
new file mode 100644
index 00000000..3cde5ea4
--- /dev/null
+++ b/api/logic/minecraft/World.h
@@ -0,0 +1,83 @@
+/* Copyright 2015 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 <QFileInfo>
+#include <QDateTime>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT World
+{
+public:
+ World(const QFileInfo &file);
+ QString folderName() const
+ {
+ return m_folderName;
+ }
+ QString name() const
+ {
+ return m_actualName;
+ }
+ QDateTime lastPlayed() const
+ {
+ return m_lastPlayed;
+ }
+ int64_t seed() const
+ {
+ return m_randomSeed;
+ }
+ bool isValid() const
+ {
+ return is_valid;
+ }
+ bool isOnFS() const
+ {
+ return m_containerFile.isDir();
+ }
+ QFileInfo container() const
+ {
+ return m_containerFile;
+ }
+ // delete all the files of this world
+ bool destroy();
+ // replace this world with a copy of the other
+ bool replace(World &with);
+ // change the world's filesystem path (used by world lists for *MAGIC* purposes)
+ void repath(const QFileInfo &file);
+
+ bool rename(const QString &to);
+ bool install(const QString &to, const QString &name= QString());
+
+ // WEAK compare operator - used for replacing worlds
+ bool operator==(const World &other) const;
+ bool strongCompare(const World &other) const;
+
+private:
+ void readFromZip(const QFileInfo &file);
+ void readFromFS(const QFileInfo &file);
+ void loadFromLevelDat(QByteArray data);
+
+protected:
+
+ QFileInfo m_containerFile;
+ QString m_containerOffsetPath;
+ QString m_folderName;
+ QString m_actualName;
+ QDateTime levelDatTime;
+ QDateTime m_lastPlayed;
+ int64_t m_randomSeed = 0;
+ bool is_valid = false;
+};
diff --git a/api/logic/minecraft/WorldList.cpp b/api/logic/minecraft/WorldList.cpp
new file mode 100644
index 00000000..42c8a3e6
--- /dev/null
+++ b/api/logic/minecraft/WorldList.cpp
@@ -0,0 +1,355 @@
+/* Copyright 2015 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 "WorldList.h"
+#include <FileSystem.h>
+#include <QMimeData>
+#include <QUrl>
+#include <QUuid>
+#include <QString>
+#include <QFileSystemWatcher>
+#include <QDebug>
+
+WorldList::WorldList(const QString &dir)
+ : QAbstractListModel(), m_dir(dir)
+{
+ FS::ensureFolderPathExists(m_dir.absolutePath());
+ m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs |
+ QDir::NoSymLinks);
+ m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
+ m_watcher = new QFileSystemWatcher(this);
+ is_watching = false;
+ connect(m_watcher, SIGNAL(directoryChanged(QString)), this,
+ SLOT(directoryChanged(QString)));
+}
+
+void WorldList::startWatching()
+{
+ update();
+ is_watching = m_watcher->addPath(m_dir.absolutePath());
+ if (is_watching)
+ {
+ qDebug() << "Started watching " << m_dir.absolutePath();
+ }
+ else
+ {
+ qDebug() << "Failed to start watching " << m_dir.absolutePath();
+ }
+}
+
+void WorldList::stopWatching()
+{
+ is_watching = !m_watcher->removePath(m_dir.absolutePath());
+ if (!is_watching)
+ {
+ qDebug() << "Stopped watching " << m_dir.absolutePath();
+ }
+ else
+ {
+ qDebug() << "Failed to stop watching " << m_dir.absolutePath();
+ }
+}
+
+bool WorldList::update()
+{
+ if (!isValid())
+ return false;
+
+ QList<World> newWorlds;
+ m_dir.refresh();
+ auto folderContents = m_dir.entryInfoList();
+ // if there are any untracked files...
+ for (QFileInfo entry : folderContents)
+ {
+ if(!entry.isDir())
+ continue;
+
+ World w(entry);
+ if(w.isValid())
+ {
+ newWorlds.append(w);
+ }
+ }
+ beginResetModel();
+ worlds.swap(newWorlds);
+ endResetModel();
+ return true;
+}
+
+void WorldList::directoryChanged(QString path)
+{
+ update();
+}
+
+bool WorldList::isValid()
+{
+ return m_dir.exists() && m_dir.isReadable();
+}
+
+bool WorldList::deleteWorld(int index)
+{
+ if (index >= worlds.size() || index < 0)
+ return false;
+ World &m = worlds[index];
+ if (m.destroy())
+ {
+ beginRemoveRows(QModelIndex(), index, index);
+ worlds.removeAt(index);
+ endRemoveRows();
+ emit changed();
+ return true;
+ }
+ return false;
+}
+
+bool WorldList::deleteWorlds(int first, int last)
+{
+ for (int i = first; i <= last; i++)
+ {
+ World &m = worlds[i];
+ m.destroy();
+ }
+ beginRemoveRows(QModelIndex(), first, last);
+ worlds.erase(worlds.begin() + first, worlds.begin() + last + 1);
+ endRemoveRows();
+ emit changed();
+ return true;
+}
+
+int WorldList::columnCount(const QModelIndex &parent) const
+{
+ return 2;
+}
+
+QVariant WorldList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+
+ if (row < 0 || row >= worlds.size())
+ return QVariant();
+
+ auto & world = worlds[row];
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (column)
+ {
+ case NameColumn:
+ return world.name();
+
+ case LastPlayedColumn:
+ return world.lastPlayed();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ {
+ return world.folderName();
+ }
+ case ObjectRole:
+ {
+ return QVariant::fromValue<void *>((void *)&world);
+ }
+ case FolderRole:
+ {
+ return QDir::toNativeSeparators(dir().absoluteFilePath(world.folderName()));
+ }
+ case SeedRole:
+ {
+ return qVariantFromValue<qlonglong>(world.seed());
+ }
+ case NameRole:
+ {
+ return world.name();
+ }
+ case LastPlayedRole:
+ {
+ return world.lastPlayed();
+ }
+ default:
+ return QVariant();
+ }
+}
+
+QVariant WorldList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case NameColumn:
+ return tr("Name");
+ case LastPlayedColumn:
+ return tr("Last Played");
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case NameColumn:
+ return tr("The name of the world.");
+ case LastPlayedColumn:
+ return tr("Date and time the world was last played.");
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ return QVariant();
+}
+
+QStringList WorldList::mimeTypes() const
+{
+ QStringList types;
+ types << "text/uri-list";
+ return types;
+}
+
+class WorldMimeData : public QMimeData
+{
+Q_OBJECT
+
+public:
+ WorldMimeData(QList<World> worlds)
+ {
+ m_worlds = worlds;
+
+ }
+ QStringList formats() const
+ {
+ return QMimeData::formats() << "text/uri-list";
+ }
+
+protected:
+ QVariant retrieveData(const QString &mimetype, QVariant::Type type) const
+ {
+ QList<QUrl> urls;
+ for(auto &world: m_worlds)
+ {
+ if(!world.isValid() || !world.isOnFS())
+ continue;
+ QString worldPath = world.container().absoluteFilePath();
+ qDebug() << worldPath;
+ urls.append(QUrl::fromLocalFile(worldPath));
+ }
+ const_cast<WorldMimeData*>(this)->setUrls(urls);
+ return QMimeData::retrieveData(mimetype, type);
+ }
+private:
+ QList<World> m_worlds;
+};
+
+QMimeData *WorldList::mimeData(const QModelIndexList &indexes) const
+{
+ if (indexes.size() == 0)
+ return new QMimeData();
+
+ QList<World> worlds;
+ for(auto idx : indexes)
+ {
+ if(idx.column() != 0)
+ continue;
+ int row = idx.row();
+ if (row < 0 || row >= this->worlds.size())
+ continue;
+ worlds.append(this->worlds[row]);
+ }
+ if(!worlds.size())
+ {
+ return new QMimeData();
+ }
+ return new WorldMimeData(worlds);
+}
+
+Qt::ItemFlags WorldList::flags(const QModelIndex &index) const
+{
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ if (index.isValid())
+ return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled |
+ defaultFlags;
+ else
+ return Qt::ItemIsDropEnabled | defaultFlags;
+}
+
+Qt::DropActions WorldList::supportedDragActions() const
+{
+ // move to other mod lists or VOID
+ return Qt::MoveAction;
+}
+
+Qt::DropActions WorldList::supportedDropActions() const
+{
+ // copy from outside, move from within and other mod lists
+ return Qt::CopyAction | Qt::MoveAction;
+}
+
+void WorldList::installWorld(QFileInfo filename)
+{
+ qDebug() << "installing: " << filename.absoluteFilePath();
+ World w(filename);
+ if(!w.isValid())
+ {
+ return;
+ }
+ w.install(m_dir.absolutePath());
+}
+
+bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
+ const QModelIndex &parent)
+{
+ if (action == Qt::IgnoreAction)
+ return true;
+ // check if the action is supported
+ if (!data || !(action & supportedDropActions()))
+ return false;
+ // files dropped from outside?
+ if (data->hasUrls())
+ {
+ bool was_watching = is_watching;
+ if (was_watching)
+ stopWatching();
+ auto urls = data->urls();
+ for (auto url : urls)
+ {
+ // only local files may be dropped...
+ if (!url.isLocalFile())
+ continue;
+ QString filename = url.toLocalFile();
+
+ QFileInfo worldInfo(filename);
+
+ if(!m_dir.entryInfoList().contains(worldInfo))
+ {
+ installWorld(worldInfo);
+ }
+ }
+ if (was_watching)
+ startWatching();
+ return true;
+ }
+ return false;
+}
+
+#include "WorldList.moc"
diff --git a/api/logic/minecraft/WorldList.h b/api/logic/minecraft/WorldList.h
new file mode 100644
index 00000000..34b30e9c
--- /dev/null
+++ b/api/logic/minecraft/WorldList.h
@@ -0,0 +1,125 @@
+/* Copyright 2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QList>
+#include <QString>
+#include <QDir>
+#include <QAbstractListModel>
+#include <QMimeData>
+#include "minecraft/World.h"
+
+#include "multimc_logic_export.h"
+
+class QFileSystemWatcher;
+
+class MULTIMC_LOGIC_EXPORT WorldList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Columns
+ {
+ NameColumn,
+ LastPlayedColumn
+ };
+
+ enum Roles
+ {
+ ObjectRole = Qt::UserRole + 1,
+ FolderRole,
+ SeedRole,
+ NameRole,
+ LastPlayedRole
+ };
+
+ WorldList(const QString &dir);
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
+
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const
+ {
+ return size();
+ };
+ virtual QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const;
+ virtual int columnCount(const QModelIndex &parent) const;
+
+ size_t size() const
+ {
+ return worlds.size();
+ };
+ bool empty() const
+ {
+ return size() == 0;
+ }
+ World &operator[](size_t index)
+ {
+ return worlds[index];
+ }
+
+ /// Reloads the mod list and returns true if the list changed.
+ virtual bool update();
+
+ /// Install a world from location
+ void installWorld(QFileInfo filename);
+
+ /// Deletes the mod at the given index.
+ virtual bool deleteWorld(int index);
+
+ /// Deletes all the selected mods
+ virtual bool deleteWorlds(int first, int last);
+
+ /// flags, mostly to support drag&drop
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const;
+ /// get data for drag action
+ virtual QMimeData *mimeData(const QModelIndexList &indexes) const;
+ /// get the supported mime types
+ virtual QStringList mimeTypes() const;
+ /// process data from drop action
+ virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
+ /// what drag actions do we support?
+ virtual Qt::DropActions supportedDragActions() const;
+
+ /// what drop actions do we support?
+ virtual Qt::DropActions supportedDropActions() const;
+
+ void startWatching();
+ void stopWatching();
+
+ virtual bool isValid();
+
+ QDir dir() const
+ {
+ return m_dir;
+ }
+
+ const QList<World> &allWorlds() const
+ {
+ return worlds;
+ }
+
+private slots:
+ void directoryChanged(QString path);
+
+signals:
+ void changed();
+
+protected:
+ QFileSystemWatcher *m_watcher;
+ bool is_watching;
+ QDir m_dir;
+ QList<World> worlds;
+};
diff --git a/api/logic/minecraft/auth/AuthSession.cpp b/api/logic/minecraft/auth/AuthSession.cpp
new file mode 100644
index 00000000..8758bfbd
--- /dev/null
+++ b/api/logic/minecraft/auth/AuthSession.cpp
@@ -0,0 +1,30 @@
+#include "AuthSession.h"
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QStringList>
+
+QString AuthSession::serializeUserProperties()
+{
+ QJsonObject userAttrs;
+ for (auto key : u.properties.keys())
+ {
+ auto array = QJsonArray::fromStringList(u.properties.values(key));
+ userAttrs.insert(key, array);
+ }
+ QJsonDocument value(userAttrs);
+ return value.toJson(QJsonDocument::Compact);
+
+}
+
+bool AuthSession::MakeOffline(QString offline_playername)
+{
+ if (status != PlayableOffline && status != PlayableOnline)
+ {
+ return false;
+ }
+ session = "-";
+ player_name = offline_playername;
+ status = PlayableOffline;
+ return true;
+}
diff --git a/api/logic/minecraft/auth/AuthSession.h b/api/logic/minecraft/auth/AuthSession.h
new file mode 100644
index 00000000..dede90a9
--- /dev/null
+++ b/api/logic/minecraft/auth/AuthSession.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include <QString>
+#include <QMultiMap>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+struct User
+{
+ QString id;
+ QMultiMap<QString, QString> properties;
+};
+
+struct MULTIMC_LOGIC_EXPORT AuthSession
+{
+ bool MakeOffline(QString offline_playername);
+
+ QString serializeUserProperties();
+
+ enum Status
+ {
+ Undetermined,
+ RequiresPassword,
+ PlayableOffline,
+ PlayableOnline
+ } status = Undetermined;
+
+ User u;
+
+ // client token
+ QString client_token;
+ // account user name
+ QString username;
+ // combined session ID
+ QString session;
+ // volatile auth token
+ QString access_token;
+ // profile name
+ QString player_name;
+ // profile ID
+ QString uuid;
+ // 'legacy' or 'mojang', depending on account type
+ QString user_type;
+ // Did the auth server reply?
+ bool auth_server_online = false;
+ // Did the user request online mode?
+ bool wants_online = true;
+};
+
+typedef std::shared_ptr<AuthSession> AuthSessionPtr;
diff --git a/api/logic/minecraft/auth/MojangAccount.cpp b/api/logic/minecraft/auth/MojangAccount.cpp
new file mode 100644
index 00000000..69a24c09
--- /dev/null
+++ b/api/logic/minecraft/auth/MojangAccount.cpp
@@ -0,0 +1,278 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Authors: Orochimarufan <orochimarufan.x3@gmail.com>
+ *
+ * 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 "MojangAccount.h"
+#include "flows/RefreshTask.h"
+#include "flows/AuthenticateTask.h"
+
+#include <QUuid>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QRegExp>
+#include <QStringList>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
+{
+ // The JSON object must at least have a username for it to be valid.
+ if (!object.value("username").isString())
+ {
+ qCritical() << "Can't load Mojang account info from JSON object. Username field is "
+ "missing or of the wrong type.";
+ return nullptr;
+ }
+
+ QString username = object.value("username").toString("");
+ QString clientToken = object.value("clientToken").toString("");
+ QString accessToken = object.value("accessToken").toString("");
+
+ QJsonArray profileArray = object.value("profiles").toArray();
+ if (profileArray.size() < 1)
+ {
+ qCritical() << "Can't load Mojang account with username \"" << username
+ << "\". No profiles found.";
+ return nullptr;
+ }
+
+ 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())
+ {
+ qWarning() << "Unable to load a profile because it was missing an ID or a name.";
+ continue;
+ }
+ profiles.append({id, name, legacy});
+ }
+
+ 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->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() const
+{
+ QJsonObject json;
+ 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("legacy", profile.legacy);
+ profileArray.append(profileObj);
+ }
+ json.insert("profiles", profileArray);
+
+ 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;
+}
+
+bool MojangAccount::setCurrentProfile(const QString &profileId)
+{
+ for (int i = 0; i < m_profiles.length(); i++)
+ {
+ if (m_profiles[i].id == profileId)
+ {
+ m_currentProfile = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+const AccountProfile *MojangAccount::currentProfile() const
+{
+ if (m_currentProfile == -1)
+ return nullptr;
+ return &m_profiles[m_currentProfile];
+}
+
+AccountStatus MojangAccount::accountStatus() const
+{
+ if (m_accessToken.isEmpty())
+ return NotVerified;
+ else
+ return Verified;
+}
+
+std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session,
+ QString password)
+{
+ Q_ASSERT(m_currentTask.get() == nullptr);
+
+ // take care of the true offline status
+ if (accountStatus() == NotVerified && password.isEmpty())
+ {
+ if (session)
+ {
+ session->status = AuthSession::RequiresPassword;
+ fillSession(session);
+ }
+ return nullptr;
+ }
+
+ if (password.isEmpty())
+ {
+ m_currentTask.reset(new RefreshTask(this));
+ }
+ else
+ {
+ m_currentTask.reset(new AuthenticateTask(this, password));
+ }
+ m_currentTask->assignSession(session);
+
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
+ return m_currentTask;
+}
+
+void MojangAccount::authSucceeded()
+{
+ auto session = m_currentTask->getAssignedSession();
+ if (session)
+ {
+ session->status =
+ session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
+ fillSession(session);
+ session->auth_server_online = true;
+ }
+ m_currentTask.reset();
+ emit changed();
+}
+
+void MojangAccount::authFailed(QString reason)
+{
+ auto session = m_currentTask->getAssignedSession();
+ // This is emitted when the yggdrasil tasks time out or are cancelled.
+ // -> we treat the error as no-op
+ if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT)
+ {
+ if (session)
+ {
+ session->status = accountStatus() == Verified ? AuthSession::PlayableOffline
+ : AuthSession::RequiresPassword;
+ session->auth_server_online = false;
+ fillSession(session);
+ }
+ }
+ else
+ {
+ m_accessToken = QString();
+ emit changed();
+ if (session)
+ {
+ session->status = AuthSession::RequiresPassword;
+ session->auth_server_online = true;
+ fillSession(session);
+ }
+ }
+ m_currentTask.reset();
+}
+
+void MojangAccount::fillSession(AuthSessionPtr session)
+{
+ // the user name. you have to have an user name
+ session->username = m_username;
+ // volatile auth token
+ session->access_token = m_accessToken;
+ // the semi-permanent client token
+ session->client_token = m_clientToken;
+ if (currentProfile())
+ {
+ // profile name
+ session->player_name = currentProfile()->name;
+ // profile ID
+ session->uuid = currentProfile()->id;
+ // 'legacy' or 'mojang', depending on account type
+ session->user_type = currentProfile()->legacy ? "legacy" : "mojang";
+ if (!session->access_token.isEmpty())
+ {
+ session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id;
+ }
+ else
+ {
+ session->session = "-";
+ }
+ }
+ else
+ {
+ session->player_name = "Player";
+ session->session = "-";
+ }
+ session->u = user();
+}
diff --git a/api/logic/minecraft/auth/MojangAccount.h b/api/logic/minecraft/auth/MojangAccount.h
new file mode 100644
index 00000000..2de0c19c
--- /dev/null
+++ b/api/logic/minecraft/auth/MojangAccount.h
@@ -0,0 +1,173 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QString>
+#include <QList>
+#include <QJsonObject>
+#include <QPair>
+#include <QMap>
+
+#include <memory>
+#include "AuthSession.h"
+
+#include "multimc_logic_export.h"
+
+class Task;
+class YggdrasilTask;
+class MojangAccount;
+
+typedef std::shared_ptr<MojangAccount> MojangAccountPtr;
+Q_DECLARE_METATYPE(MojangAccountPtr)
+
+/**
+ * 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.
+ */
+struct AccountProfile
+{
+ QString id;
+ QString name;
+ bool legacy;
+};
+
+enum AccountStatus
+{
+ NotVerified,
+ Verified
+};
+
+/**
+ * Object that stores information about a certain Mojang account.
+ *
+ * Said information may include things such as that account's username, client token, and access
+ * token if the user chose to stay logged in.
+ */
+class MULTIMC_LOGIC_EXPORT MojangAccount : public QObject
+{
+ Q_OBJECT
+public: /* construction */
+ //! Do not copy accounts. ever.
+ explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete;
+
+ //! Default constructor
+ explicit MojangAccount(QObject *parent = 0) : 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.
+ static MojangAccountPtr loadFromJson(const QJsonObject &json);
+
+ //! Saves a MojangAccount to a JSON object and returns it.
+ QJsonObject saveToJson() const;
+
+public: /* manipulation */
+ /**
+ * 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<YggdrasilTask> login(AuthSessionPtr session,
+ QString password = QString());
+
+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;
+ }
+
+ //! Returns the currently selected profile (if none, returns nullptr)
+ const AccountProfile *currentProfile() const;
+
+ //! Returns whether the account is NotVerified, Verified or Online
+ AccountStatus accountStatus() const;
+
+signals:
+ /**
+ * This signal is emitted when the account changes
+ */
+ void changed();
+
+ // TODO: better signalling for the various possible state changes - especially errors
+
+protected: /* variables */
+ QString m_username;
+
+ // 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;
+
+ // Blank if not logged in.
+ QString m_accessToken;
+
+ // Index of the selected profile within the list of available
+ // profiles. -1 if nothing is selected.
+ int m_currentProfile = -1;
+
+ // List of available profiles.
+ QList<AccountProfile> m_profiles;
+
+ // the user structure, whatever it is.
+ User m_user;
+
+ // current task we are executing here
+ std::shared_ptr<YggdrasilTask> m_currentTask;
+
+private
+slots:
+ void authSucceeded();
+ void authFailed(QString reason);
+
+private:
+ void fillSession(AuthSessionPtr session);
+
+public:
+ friend class YggdrasilTask;
+ friend class AuthenticateTask;
+ friend class ValidateTask;
+ friend class RefreshTask;
+};
diff --git a/api/logic/minecraft/auth/MojangAccountList.cpp b/api/logic/minecraft/auth/MojangAccountList.cpp
new file mode 100644
index 00000000..26cbc81a
--- /dev/null
+++ b/api/logic/minecraft/auth/MojangAccountList.cpp
@@ -0,0 +1,427 @@
+/* Copyright 2013-2015 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 "MojangAccountList.h"
+#include "MojangAccount.h"
+
+#include <QIODevice>
+#include <QFile>
+#include <QTextStream>
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <QDir>
+
+#include <QDebug>
+
+#include <FileSystem.h>
+
+#define ACCOUNT_LIST_FORMAT_VERSION 2
+
+MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+MojangAccountPtr MojangAccountList::findAccount(const QString &username) const
+{
+ for (int i = 0; i < count(); i++)
+ {
+ MojangAccountPtr account = at(i);
+ if (account->username() == username)
+ return account;
+ }
+ return nullptr;
+}
+
+const MojangAccountPtr MojangAccountList::at(int i) const
+{
+ return MojangAccountPtr(m_accounts.at(i));
+}
+
+void MojangAccountList::addAccount(const MojangAccountPtr account)
+{
+ beginResetModel();
+ connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
+ m_accounts.append(account);
+ endResetModel();
+ onListChanged();
+}
+
+void MojangAccountList::removeAccount(const QString &username)
+{
+ beginResetModel();
+ for (auto account : m_accounts)
+ {
+ if (account->username() == username)
+ {
+ m_accounts.removeOne(account);
+ return;
+ }
+ }
+ endResetModel();
+ onListChanged();
+}
+
+void MojangAccountList::removeAccount(QModelIndex index)
+{
+ beginResetModel();
+ m_accounts.removeAt(index.row());
+ endResetModel();
+ onListChanged();
+}
+
+MojangAccountPtr MojangAccountList::activeAccount() const
+{
+ return m_activeAccount;
+}
+
+void MojangAccountList::setActiveAccount(const QString &username)
+{
+ beginResetModel();
+ if (username.isEmpty())
+ {
+ m_activeAccount = nullptr;
+ }
+ else
+ {
+ for (MojangAccountPtr account : m_accounts)
+ {
+ if (account->username() == username)
+ m_activeAccount = account;
+ }
+ }
+ endResetModel();
+ onActiveChanged();
+}
+
+void MojangAccountList::accountChanged()
+{
+ // the list changed. there is no doubt.
+ onListChanged();
+}
+
+void MojangAccountList::onListChanged()
+{
+ if (m_autosave)
+ // TODO: Alert the user if this fails.
+ saveList();
+
+ emit listChanged();
+}
+
+void MojangAccountList::onActiveChanged()
+{
+ if (m_autosave)
+ saveList();
+
+ emit activeAccountChanged();
+}
+
+int MojangAccountList::count() const
+{
+ return m_accounts.count();
+}
+
+QVariant MojangAccountList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ MojangAccountPtr account = at(index.row());
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case NameColumn:
+ return account->username();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return account->username();
+
+ case PointerRole:
+ return qVariantFromValue(account);
+
+ case Qt::CheckStateRole:
+ switch (index.column())
+ {
+ case ActiveColumn:
+ return account == m_activeAccount;
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case ActiveColumn:
+ return tr("Active?");
+
+ case NameColumn:
+ return tr("Name");
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case NameColumn:
+ return tr("The name of the version.");
+
+ default:
+ return QVariant();
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+int MojangAccountList::rowCount(const QModelIndex &parent) const
+{
+ // Return count
+ return count();
+}
+
+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();
+ m_accounts = versions;
+ endResetModel();
+}
+
+bool MojangAccountList::loadList(const QString &filePath)
+{
+ QString path = filePath;
+ if (path.isEmpty())
+ path = m_listFilePath;
+ if (path.isEmpty())
+ {
+ qCritical() << "Can't load Mojang account list. No file path given and no default set.";
+ return false;
+ }
+
+ 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))
+ {
+ qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
+ 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)
+ {
+ qCritical() << QString("Failed to parse account list file: %1 at offset %2")
+ .arg(parseError.errorString(), QString::number(parseError.offset))
+ .toUtf8();
+ return false;
+ }
+
+ // Make sure the root is an object.
+ if (!jsonDoc.isObject())
+ {
+ qCritical() << "Invalid account list JSON: Root should be an array.";
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // Make sure the format version matches.
+ if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION)
+ {
+ QString newName = "accounts-old.json";
+ qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to"
+ << newName;
+
+ // Attempt to rename the old version.
+ file.rename(newName);
+ return false;
+ }
+
+ // Now, load the accounts array.
+ beginResetModel();
+ QJsonArray accounts = root.value("accounts").toArray();
+ for (QJsonValue accountVal : accounts)
+ {
+ QJsonObject accountObj = accountVal.toObject();
+ MojangAccountPtr account = MojangAccount::loadFromJson(accountObj);
+ if (account.get() != nullptr)
+ {
+ connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
+ m_accounts.append(account);
+ }
+ else
+ {
+ qWarning() << "Failed to load an account.";
+ }
+ }
+ // Load the active account.
+ m_activeAccount = findAccount(root.value("activeAccount").toString(""));
+ endResetModel();
+ return true;
+}
+
+bool MojangAccountList::saveList(const QString &filePath)
+{
+ QString path(filePath);
+ if (path.isEmpty())
+ path = m_listFilePath;
+ if (path.isEmpty())
+ {
+ qCritical() << "Can't save Mojang account list. No file path given and no default set.";
+ return false;
+ }
+
+ // make sure the parent folder exists
+ if(!FS::ensureFilePathExists(path))
+ return false;
+
+ // make sure the file wasn't overwritten with a folder before (fixes a bug)
+ QFileInfo finfo(path);
+ if(finfo.isDir())
+ {
+ QDir badDir(path);
+ badDir.removeRecursively();
+ }
+
+ qDebug() << "Writing account list to" << path;
+
+ qDebug() << "Building JSON data structure.";
+ // Build the JSON document to write to the list file.
+ QJsonObject root;
+
+ root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION);
+
+ // Build a list of accounts.
+ qDebug() << "Building account array.";
+ QJsonArray accounts;
+ for (MojangAccountPtr account : m_accounts)
+ {
+ QJsonObject accountObj = account->saveToJson();
+ accounts.append(accountObj);
+ }
+
+ // Insert the account list into the root object.
+ root.insert("accounts", accounts);
+
+ 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);
+
+ // Now that we're done building the JSON object, we can write it to the file.
+ qDebug() << "Writing account list to file.";
+ 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::WriteOnly))
+ {
+ qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
+ return false;
+ }
+
+ // Write the JSON to the file.
+ file.write(doc.toJson());
+ file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser);
+ file.close();
+
+ qDebug() << "Saved account list to" << path;
+
+ return true;
+}
+
+void MojangAccountList::setListFilePath(QString path, bool autosave)
+{
+ m_listFilePath = path;
+ m_autosave = autosave;
+}
+
+bool MojangAccountList::anyAccountIsValid()
+{
+ for(auto account:m_accounts)
+ {
+ if(account->accountStatus() != NotVerified)
+ return true;
+ }
+ return false;
+}
diff --git a/api/logic/minecraft/auth/MojangAccountList.h b/api/logic/minecraft/auth/MojangAccountList.h
new file mode 100644
index 00000000..c40fa6a3
--- /dev/null
+++ b/api/logic/minecraft/auth/MojangAccountList.h
@@ -0,0 +1,201 @@
+/* Copyright 2013-2015 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 "MojangAccount.h"
+
+#include <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+#include <QSharedPointer>
+
+#include "multimc_logic_export.h"
+
+/*!
+ * \brief List of available Mojang accounts.
+ * This should be loaded in the background by MultiMC on startup.
+ *
+ * This class also inherits from QAbstractListModel. Methods from that
+ * class determine how this list shows up in a list view. Said methods
+ * all have a default implementation, but they can be overridden by subclasses to
+ * change the behavior of the list.
+ */
+class MULTIMC_LOGIC_EXPORT MojangAccountList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum ModelRoles
+ {
+ PointerRole = 0x34B1CB48
+ };
+
+ enum VListColumns
+ {
+ // TODO: Add icon column.
+
+ // First column - Active?
+ ActiveColumn = 0,
+
+ // Second column - Name
+ NameColumn,
+ };
+
+ explicit MojangAccountList(QObject *parent = 0);
+
+ //! Gets the account at the given index.
+ virtual const MojangAccountPtr at(int i) const;
+
+ //! Returns the number of accounts in the list.
+ virtual int count() const;
+
+ //////// List Model Functions ////////
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ 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.
+ */
+ virtual void addAccount(const MojangAccountPtr account);
+
+ /*!
+ * Removes the mojang account with the given username from the account list.
+ */
+ virtual void removeAccount(const QString &username);
+
+ /*!
+ * Removes the account at the given QModelIndex.
+ */
+ virtual void removeAccount(QModelIndex index);
+
+ /*!
+ * \brief Finds an account by its username.
+ * \param The username of the account to find.
+ * \return A const pointer to the account with the given username. NULL if
+ * one doesn't exist.
+ */
+ virtual MojangAccountPtr findAccount(const QString &username) const;
+
+ /*!
+ * Sets the default path to save the list file to.
+ * If autosave is true, this list will automatically save to the given path whenever it changes.
+ * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately
+ * after calling this function to ensure an autosaved change doesn't overwrite the list you intended
+ * to load.
+ */
+ virtual void setListFilePath(QString path, bool autosave = false);
+
+ /*!
+ * \brief Loads the account list from the given file path.
+ * If the given file is an empty string (default), will load from the default account list file.
+ * \return True if successful, otherwise false.
+ */
+ virtual bool loadList(const QString &file = "");
+
+ /*!
+ * \brief Saves the account list to the given file.
+ * If the given file is an empty string (default), will save from the default account list file.
+ * \return True if successful, otherwise false.
+ */
+ virtual bool saveList(const QString &file = "");
+
+ /*!
+ * \brief Gets a pointer to the account that the user has selected as their "active" account.
+ * Which account is active can be overridden on a per-instance basis, but this will return the one that
+ * is set as active globally.
+ * \return The currently active MojangAccount. If there isn't an active account, returns a null pointer.
+ */
+ virtual MojangAccountPtr activeAccount() const;
+
+ /*!
+ * Sets the given account as the current active account.
+ * If the username given is an empty string, sets the active account to nothing.
+ */
+ virtual void setActiveAccount(const QString &username);
+
+ /*!
+ * Returns true if any of the account is at least Validated
+ */
+ bool anyAccountIsValid();
+
+signals:
+ /*!
+ * Signal emitted to indicate that the account list has changed.
+ * This will also fire if the value of an element in the list changes (will be implemented
+ * later).
+ */
+ void listChanged();
+
+ /*!
+ * Signal emitted to indicate that the active account has changed.
+ */
+ void activeAccountChanged();
+
+public
+slots:
+ /**
+ * This is called when one of the accounts changes and the list needs to be updated
+ */
+ void accountChanged();
+
+protected:
+ /*!
+ * Called whenever the list changes.
+ * This emits the listChanged() signal and autosaves the list (if autosave is enabled).
+ */
+ void onListChanged();
+
+ /*!
+ * Called whenever the active account changes.
+ * Emits the activeAccountChanged() signal and autosaves the list if enabled.
+ */
+ void onActiveChanged();
+
+ QList<MojangAccountPtr> m_accounts;
+
+ /*!
+ * Account that is currently active.
+ */
+ MojangAccountPtr m_activeAccount;
+
+ //! Path to the account list file. Empty string if there isn't one.
+ QString m_listFilePath;
+
+ /*!
+ * If true, the account list will automatically save to the account list path when it changes.
+ * Ignored if m_listFilePath is blank.
+ */
+ bool m_autosave = false;
+
+protected
+slots:
+ /*!
+ * Updates this list with the given list of accounts.
+ * This is done by copying each account in the given list and inserting it
+ * into this one.
+ * We need to do this so that we can set the parents of the accounts are set to this
+ * account list. This can't be done in the load task, because the accounts the load
+ * task creates are on the load task's thread and Qt won't allow their parents
+ * to be set to something created on another thread.
+ * To get around that problem, we invoke this method on the GUI thread, which
+ * then copies the accounts and sets their parents correctly.
+ * \param accounts List of accounts whose parents should be set.
+ */
+ virtual void updateListData(QList<MojangAccountPtr> versions);
+};
diff --git a/api/logic/minecraft/auth/YggdrasilTask.cpp b/api/logic/minecraft/auth/YggdrasilTask.cpp
new file mode 100644
index 00000000..c6971c9f
--- /dev/null
+++ b/api/logic/minecraft/auth/YggdrasilTask.cpp
@@ -0,0 +1,255 @@
+/* Copyright 2013-2015 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 "YggdrasilTask.h"
+#include "MojangAccount.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QByteArray>
+
+#include <Env.h>
+
+#include <net/URLConstants.h>
+
+#include <QDebug>
+
+YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent)
+ : Task(parent), m_account(account)
+{
+ changeState(STATE_CREATED);
+}
+
+void YggdrasilTask::executeTask()
+{
+ changeState(STATE_SENDING_REQUEST);
+
+ // Get the content of the request we're going to send to the server.
+ QJsonDocument doc(getRequestContent());
+
+ auto worker = ENV.qnam();
+ 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);
+ connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors);
+ 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::abortByTimeout);
+ connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat);
+}
+
+void YggdrasilTask::refreshTimers(qint64, qint64)
+{
+ timeout_keeper.stop();
+ timeout_keeper.start(timeout_max);
+ progress(count = 0, timeout_max);
+}
+void YggdrasilTask::heartbeat()
+{
+ count += time_step;
+ progress(count, timeout_max);
+}
+
+bool YggdrasilTask::abort()
+{
+ progress(timeout_max, timeout_max);
+ // TODO: actually use this in a meaningful way
+ m_aborted = YggdrasilTask::BY_USER;
+ m_netReply->abort();
+ return true;
+}
+
+void YggdrasilTask::abortByTimeout()
+{
+ progress(timeout_max, timeout_max);
+ // TODO: actually use this in a meaningful way
+ m_aborted = YggdrasilTask::BY_TIMEOUT;
+ m_netReply->abort();
+}
+
+void YggdrasilTask::sslErrors(QList<QSslError> errors)
+{
+ int i = 1;
+ for (auto error : errors)
+ {
+ qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+}
+
+void YggdrasilTask::processReply()
+{
+ changeState(STATE_PROCESSING_RESPONSE);
+
+ switch (m_netReply->error())
+ {
+ case QNetworkReply::NoError:
+ break;
+ case QNetworkReply::TimeoutError:
+ changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out."));
+ return;
+ case QNetworkReply::OperationCanceledError:
+ changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
+ return;
+ case QNetworkReply::SslHandshakeFailedError:
+ changeState(
+ STATE_FAILED_SOFT,
+ tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
+ "<ul>"
+ "<li>You use Windows XP and need to <a "
+ "href=\"http://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
+ "your root certificates</a></li>"
+ "<li>Some device on your network is interfering with SSL traffic. In that case, "
+ "you have bigger worries than Minecraft not starting.</li>"
+ "<li>Possibly something else. Check the MultiMC log file for details</li>"
+ "</ul>"));
+ return;
+ // used for invalid credentials and similar errors. Fall through.
+ case QNetworkReply::ContentOperationNotPermittedError:
+ break;
+ default:
+ changeState(STATE_FAILED_SOFT,
+ tr("Authentication operation failed due to a network error: %1 (%2)")
+ .arg(m_netReply->errorString()).arg(m_netReply->error()));
+ return;
+ }
+
+ // 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)
+ {
+ // 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)
+ {
+ processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
+ return;
+ }
+ else
+ {
+ changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response "
+ "JSON response: %1 at offset %2.")
+ .arg(jsonError.errorString())
+ .arg(jsonError.offset));
+ qCritical() << replyData;
+ }
+ 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.
+ qDebug() << "The request failed, but the server gave us an error message. "
+ "Processing error.";
+ processError(doc.object());
+ }
+ else
+ {
+ // The server didn't say anything regarding the error. Give the user an unknown
+ // error.
+ qDebug()
+ << "The request failed and the server gave no error message. Unknown error.";
+ changeState(STATE_FAILED_SOFT,
+ tr("An unknown error occurred when trying to communicate with the "
+ "authentication server: %1").arg(m_netReply->errorString()));
+ }
+}
+
+void YggdrasilTask::processError(QJsonObject responseData)
+{
+ QJsonValue errorVal = responseData.value("error");
+ QJsonValue errorMessageValue = responseData.value("errorMessage");
+ QJsonValue causeVal = responseData.value("cause");
+
+ if (errorVal.isString() && errorMessageValue.isString())
+ {
+ m_error = std::shared_ptr<Error>(new Error{
+ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")});
+ changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
+ }
+ else
+ {
+ // Error is not in standard format. Don't set m_error and return unknown error.
+ changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
+ }
+}
+
+QString YggdrasilTask::getStateMessage() const
+{
+ switch (m_state)
+ {
+ case STATE_CREATED:
+ return "Waiting...";
+ case STATE_SENDING_REQUEST:
+ return tr("Sending request to auth servers...");
+ case STATE_PROCESSING_RESPONSE:
+ return tr("Processing response from servers...");
+ case STATE_SUCCEEDED:
+ return tr("Authentication task succeeded.");
+ case STATE_FAILED_SOFT:
+ return tr("Failed to contact the authentication server.");
+ case STATE_FAILED_HARD:
+ return tr("Failed to authenticate.");
+ default:
+ return tr("...");
+ }
+}
+
+void YggdrasilTask::changeState(YggdrasilTask::State newState, QString reason)
+{
+ m_state = newState;
+ setStatus(getStateMessage());
+ if (newState == STATE_SUCCEEDED)
+ {
+ emitSucceeded();
+ }
+ else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT)
+ {
+ emitFailed(reason);
+ }
+}
+
+YggdrasilTask::State YggdrasilTask::state()
+{
+ return m_state;
+}
diff --git a/api/logic/minecraft/auth/YggdrasilTask.h b/api/logic/minecraft/auth/YggdrasilTask.h
new file mode 100644
index 00000000..c84cfc06
--- /dev/null
+++ b/api/logic/minecraft/auth/YggdrasilTask.h
@@ -0,0 +1,150 @@
+/* Copyright 2013-2015 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 <tasks/Task.h>
+
+#include <QString>
+#include <QJsonObject>
+#include <QTimer>
+#include <qsslerror.h>
+
+#include "MojangAccount.h"
+
+class QNetworkReply;
+
+/**
+ * A Yggdrasil task is a task that performs an operation on a given mojang account.
+ */
+class YggdrasilTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0);
+
+ /**
+ * assign a session to this task. the session will be filled with required infomration
+ * upon completion
+ */
+ void assignSession(AuthSessionPtr session)
+ {
+ m_session = session;
+ }
+
+ /// get the assigned session for filling with information.
+ AuthSessionPtr getAssignedSession()
+ {
+ return m_session;
+ }
+
+ /**
+ * Class describing a Yggdrasil error response.
+ */
+ struct Error
+ {
+ QString m_errorMessageShort;
+ QString m_errorMessageVerbose;
+ QString m_cause;
+ };
+
+ enum AbortedBy
+ {
+ BY_NOTHING,
+ BY_USER,
+ BY_TIMEOUT
+ } m_aborted = BY_NOTHING;
+
+ /**
+ * Enum for describing the state of the current task.
+ * Used by the getStateMessage function to determine what the status message should be.
+ */
+ enum State
+ {
+ STATE_CREATED,
+ STATE_SENDING_REQUEST,
+ STATE_PROCESSING_RESPONSE,
+ STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated
+ STATE_FAILED_HARD, //!< hard failure. auth is invalid
+ STATE_SUCCEEDED
+ } m_state = STATE_CREATED;
+
+protected:
+
+ virtual void executeTask() override;
+
+ /**
+ * Gets the JSON object that will be sent to the authentication server.
+ * Should be overridden by subclasses.
+ */
+ virtual QJsonObject getRequestContent() const = 0;
+
+ /**
+ * Gets the endpoint to POST to.
+ * No leading slash.
+ */
+ virtual QString getEndpoint() const = 0;
+
+ /**
+ * Processes the response received from the server.
+ * If an error occurred, this should emit a failed signal and return false.
+ * If Yggdrasil gave an error response, it should call setError() first, and then return false.
+ * Otherwise, it should return true.
+ * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
+ * an empty QJsonObject.
+ */
+ virtual void processResponse(QJsonObject responseData) = 0;
+
+ /**
+ * Processes an error response received from the server.
+ * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
+ * \returns a QString error message that will be passed to emitFailed.
+ */
+ virtual void processError(QJsonObject responseData);
+
+ /**
+ * Returns the state message for the given state.
+ * Used to set the status message for the task.
+ * Should be overridden by subclasses that want to change messages for a given state.
+ */
+ virtual QString getStateMessage() const;
+
+protected
+slots:
+ void processReply();
+ void refreshTimers(qint64, qint64);
+ void heartbeat();
+ void sslErrors(QList<QSslError>);
+
+ void changeState(State newState, QString reason=QString());
+public
+slots:
+ virtual bool abort() override;
+ void abortByTimeout();
+ State state();
+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 = 30000;
+ const int time_step = 50;
+
+ AuthSessionPtr m_session;
+};
diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.cpp b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp
new file mode 100644
index 00000000..8d136f0b
--- /dev/null
+++ b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp
@@ -0,0 +1,202 @@
+
+/* Copyright 2013-2015 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 "AuthenticateTask.h"
+#include "../MojangAccount.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QVariant>
+
+#include <QDebug>
+#include <QUuid>
+
+AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password,
+ QObject *parent)
+ : YggdrasilTask(account, parent), m_password(password)
+{
+}
+
+QJsonObject AuthenticateTask::getRequestContent() const
+{
+ /*
+ * {
+ * "agent": { // optional
+ * "name": "Minecraft", // So far this is the only encountered value
+ * "version": 1 // This number might be increased
+ * // by the vanilla client in the future
+ * },
+ * "username": "mojang account name", // Can be an email address or player name for
+ // unmigrated accounts
+ * "password": "mojang account password",
+ * "clientToken": "client identifier" // optional
+ * "requestUser": true/false // request the user structure
+ * }
+ */
+ QJsonObject req;
+
+ {
+ QJsonObject agent;
+ // C++ makes string literals void* for some stupid reason, so we have to tell it
+ // QString... Thanks Obama.
+ agent.insert("name", QString("Minecraft"));
+ agent.insert("version", 1);
+ req.insert("agent", agent);
+ }
+
+ 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(m_account->m_clientToken.isEmpty())
+ {
+ auto uuid = QUuid::createUuid();
+ auto uuidString = uuid.toString().remove('{').remove('-').remove('}');
+ m_account->m_clientToken = uuidString;
+ }
+ req.insert("clientToken", m_account->m_clientToken);
+
+ return req;
+}
+
+void AuthenticateTask::processResponse(QJsonObject responseData)
+{
+ // Read the response data. We need to get the client token, access token, and the selected
+ // profile.
+ qDebug() << "Processing authentication response.";
+ // qDebug() << responseData;
+ // If we already have a client token, make sure the one the server gave us matches our
+ // existing one.
+ qDebug() << "Getting client token.";
+ QString clientToken = responseData.value("clientToken").toString("");
+ if (clientToken.isEmpty())
+ {
+ // Fail if the server gave us an empty client token
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
+ return;
+ }
+ if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
+ return;
+ }
+ // Set the client token.
+ m_account->m_clientToken = clientToken;
+
+ // Now, we set the access token.
+ qDebug() << "Getting access token.";
+ QString accessToken = responseData.value("accessToken").toString("");
+ if (accessToken.isEmpty())
+ {
+ // Fail if the server didn't give us an access token.
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
+ return;
+ }
+ // Set the access token.
+ m_account->m_accessToken = accessToken;
+
+ // Now we load the list of available profiles.
+ // Mojang hasn't yet implemented the profile system,
+ // but we might as well support what's there so we
+ // don't have trouble implementing it later.
+ qDebug() << "Loading profile list.";
+ QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
+ 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())
+ {
+ // This should never happen, but we might as well
+ // warn about it if it does so we can debug it easily.
+ // You never know when Mojang might do something truly derpy.
+ qWarning() << "Found entry in available profiles list with missing ID or name "
+ "field. Ignoring it.";
+ }
+
+ // Now, add a new AccountProfile entry to the list.
+ loadedProfiles.append({id, name, legacy});
+ }
+ // Put the list of profiles we loaded into the MojangAccount object.
+ 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
+ // is actually in the available profiles list.
+ // If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know).
+ qDebug() << "Setting current profile.";
+ QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
+ QString currentProfileId = currentProfile.value("id").toString("");
+ if (currentProfileId.isEmpty())
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium."));
+ return;
+ }
+ if (!m_account->setCurrentProfile(currentProfileId))
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list."));
+ return;
+ }
+
+ // this is what the vanilla launcher passes to the userProperties launch param
+ if (responseData.contains("user"))
+ {
+ User u;
+ auto obj = responseData.value("user").toObject();
+ u.id = obj.value("id").toString();
+ auto propArray = obj.value("properties").toArray();
+ for (auto prop : propArray)
+ {
+ auto propTuple = prop.toObject();
+ auto name = propTuple.value("name").toString();
+ auto value = propTuple.value("value").toString();
+ 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.
+ qDebug() << "Finished reading authentication response.";
+ changeState(STATE_SUCCEEDED);
+}
+
+QString AuthenticateTask::getEndpoint() const
+{
+ return "authenticate";
+}
+
+QString AuthenticateTask::getStateMessage() const
+{
+ switch (m_state)
+ {
+ case STATE_SENDING_REQUEST:
+ return tr("Authenticating: Sending request...");
+ case STATE_PROCESSING_RESPONSE:
+ return tr("Authenticating: Processing response...");
+ default:
+ return YggdrasilTask::getStateMessage();
+ }
+}
diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.h b/api/logic/minecraft/auth/flows/AuthenticateTask.h
new file mode 100644
index 00000000..398fab98
--- /dev/null
+++ b/api/logic/minecraft/auth/flows/AuthenticateTask.h
@@ -0,0 +1,46 @@
+/* Copyright 2013-2015 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 "../YggdrasilTask.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+
+/**
+ * The authenticate task takes a MojangAccount with no access token and password and attempts to
+ * authenticate with Mojang's servers.
+ * If successful, it will set the MojangAccount's access token.
+ */
+class AuthenticateTask : public YggdrasilTask
+{
+ Q_OBJECT
+public:
+ AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0);
+
+protected:
+ virtual QJsonObject getRequestContent() const override;
+
+ virtual QString getEndpoint() const override;
+
+ virtual void processResponse(QJsonObject responseData) override;
+
+ virtual QString getStateMessage() const override;
+
+private:
+ QString m_password;
+};
diff --git a/api/logic/minecraft/auth/flows/RefreshTask.cpp b/api/logic/minecraft/auth/flows/RefreshTask.cpp
new file mode 100644
index 00000000..a0fb2e48
--- /dev/null
+++ b/api/logic/minecraft/auth/flows/RefreshTask.cpp
@@ -0,0 +1,144 @@
+/* Copyright 2013-2015 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 "RefreshTask.h"
+#include "../MojangAccount.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QVariant>
+
+#include <QDebug>
+
+RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account)
+{
+}
+
+QJsonObject RefreshTask::getRequestContent() const
+{
+ /*
+ * {
+ * "clientToken": "client identifier"
+ * "accessToken": "current access token to be refreshed"
+ * "selectedProfile": // specifying this causes errors
+ * {
+ * "id": "profile ID"
+ * "name": "profile name"
+ * }
+ * "requestUser": true/false // request the user structure
+ * }
+ */
+ QJsonObject req;
+ req.insert("clientToken", m_account->m_clientToken);
+ req.insert("accessToken", m_account->m_accessToken);
+ /*
+ {
+ auto currentProfile = m_account->currentProfile();
+ QJsonObject profile;
+ profile.insert("id", currentProfile->id());
+ profile.insert("name", currentProfile->name());
+ req.insert("selectedProfile", profile);
+ }
+ */
+ req.insert("requestUser", true);
+
+ return req;
+}
+
+void RefreshTask::processResponse(QJsonObject responseData)
+{
+ // Read the response data. We need to get the client token, access token, and the selected
+ // profile.
+ qDebug() << "Processing authentication response.";
+
+ // qDebug() << 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("");
+ if (clientToken.isEmpty())
+ {
+ // Fail if the server gave us an empty client token
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
+ return;
+ }
+ if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
+ return;
+ }
+
+ // Now, we set the access token.
+ qDebug() << "Getting new access token.";
+ QString accessToken = responseData.value("accessToken").toString("");
+ if (accessToken.isEmpty())
+ {
+ // Fail if the server didn't give us an access token.
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
+ return;
+ }
+
+ // we validate that the server responded right. (our current profile = returned current
+ // profile)
+ QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
+ QString currentProfileId = currentProfile.value("id").toString("");
+ if (m_account->currentProfile()->id != currentProfileId)
+ {
+ changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected."));
+ return;
+ }
+
+ // this is what the vanilla launcher passes to the userProperties launch param
+ if (responseData.contains("user"))
+ {
+ User u;
+ auto obj = responseData.value("user").toObject();
+ u.id = obj.value("id").toString();
+ auto propArray = obj.value("properties").toArray();
+ for (auto prop : propArray)
+ {
+ auto propTuple = prop.toObject();
+ auto name = propTuple.value("name").toString();
+ auto value = propTuple.value("value").toString();
+ 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.
+ qDebug() << "Finished reading refresh response.";
+ // Reset the access token.
+ m_account->m_accessToken = accessToken;
+ changeState(STATE_SUCCEEDED);
+}
+
+QString RefreshTask::getEndpoint() const
+{
+ return "refresh";
+}
+
+QString RefreshTask::getStateMessage() const
+{
+ switch (m_state)
+ {
+ case STATE_SENDING_REQUEST:
+ return tr("Refreshing login token...");
+ case STATE_PROCESSING_RESPONSE:
+ return tr("Refreshing login token: Processing response...");
+ default:
+ return YggdrasilTask::getStateMessage();
+ }
+}
diff --git a/api/logic/minecraft/auth/flows/RefreshTask.h b/api/logic/minecraft/auth/flows/RefreshTask.h
new file mode 100644
index 00000000..17714b4f
--- /dev/null
+++ b/api/logic/minecraft/auth/flows/RefreshTask.h
@@ -0,0 +1,44 @@
+/* Copyright 2013-2015 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 "../YggdrasilTask.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+
+/**
+ * The authenticate task takes a MojangAccount with a possibly timed-out access token
+ * and attempts to authenticate with Mojang's servers.
+ * If successful, it will set the new access token. The token is considered validated.
+ */
+class RefreshTask : public YggdrasilTask
+{
+ Q_OBJECT
+public:
+ RefreshTask(MojangAccount * account);
+
+protected:
+ virtual QJsonObject getRequestContent() const override;
+
+ virtual QString getEndpoint() const override;
+
+ virtual void processResponse(QJsonObject responseData) override;
+
+ virtual QString getStateMessage() const override;
+};
+
diff --git a/api/logic/minecraft/auth/flows/ValidateTask.cpp b/api/logic/minecraft/auth/flows/ValidateTask.cpp
new file mode 100644
index 00000000..4deceb6a
--- /dev/null
+++ b/api/logic/minecraft/auth/flows/ValidateTask.cpp
@@ -0,0 +1,61 @@
+
+/* Copyright 2013-2015 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 "ValidateTask.h"
+#include "../MojangAccount.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QVariant>
+
+#include <QDebug>
+
+ValidateTask::ValidateTask(MojangAccount * account, QObject *parent)
+ : YggdrasilTask(account, parent)
+{
+}
+
+QJsonObject ValidateTask::getRequestContent() const
+{
+ QJsonObject req;
+ req.insert("accessToken", m_account->m_accessToken);
+ return req;
+}
+
+void ValidateTask::processResponse(QJsonObject responseData)
+{
+ // Assume that if processError wasn't called, then the request was successful.
+ changeState(YggdrasilTask::STATE_SUCCEEDED);
+}
+
+QString ValidateTask::getEndpoint() const
+{
+ return "validate";
+}
+
+QString ValidateTask::getStateMessage() const
+{
+ switch (m_state)
+ {
+ case YggdrasilTask::STATE_SENDING_REQUEST:
+ return tr("Validating access token: Sending request...");
+ case YggdrasilTask::STATE_PROCESSING_RESPONSE:
+ return tr("Validating access token: Processing response...");
+ default:
+ return YggdrasilTask::getStateMessage();
+ }
+}
diff --git a/api/logic/minecraft/auth/flows/ValidateTask.h b/api/logic/minecraft/auth/flows/ValidateTask.h
new file mode 100644
index 00000000..77d628a0
--- /dev/null
+++ b/api/logic/minecraft/auth/flows/ValidateTask.h
@@ -0,0 +1,47 @@
+/* Copyright 2013-2015 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.
+ */
+
+/*
+ * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME:
+ */
+
+#pragma once
+
+#include "../YggdrasilTask.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+
+/**
+ * The validate task takes a MojangAccount and checks to make sure its access token is valid.
+ */
+class ValidateTask : public YggdrasilTask
+{
+ Q_OBJECT
+public:
+ ValidateTask(MojangAccount *account, QObject *parent = 0);
+
+protected:
+ virtual QJsonObject getRequestContent() const override;
+
+ virtual QString getEndpoint() const override;
+
+ virtual void processResponse(QJsonObject responseData) override;
+
+ virtual QString getStateMessage() const override;
+
+private:
+};
diff --git a/api/logic/minecraft/forge/ForgeInstaller.cpp b/api/logic/minecraft/forge/ForgeInstaller.cpp
new file mode 100644
index 00000000..353328ab
--- /dev/null
+++ b/api/logic/minecraft/forge/ForgeInstaller.cpp
@@ -0,0 +1,458 @@
+/* Copyright 2013-2015 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 "ForgeInstaller.h"
+#include "ForgeVersionList.h"
+
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/GradleSpecifier.h"
+#include "net/HttpMetaCache.h"
+#include "tasks/Task.h"
+#include "minecraft/onesix/OneSixInstance.h"
+#include <minecraft/onesix/OneSixVersionFormat.h>
+#include "minecraft/VersionFilterData.h"
+#include "minecraft/MinecraftVersion.h"
+#include "Env.h"
+#include "Exception.h"
+#include <FileSystem.h>
+
+#include <quazip.h>
+#include <quazipfile.h>
+#include <QStringList>
+#include <QRegularExpression>
+#include <QRegularExpressionMatch>
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QSaveFile>
+#include <QCryptographicHash>
+
+ForgeInstaller::ForgeInstaller() : BaseInstaller()
+{
+}
+
+void ForgeInstaller::prepare(const QString &filename, const QString &universalUrl)
+{
+ VersionFilePtr newVersion;
+ m_universal_url = universalUrl;
+
+ QuaZip zip(filename);
+ if (!zip.open(QuaZip::mdUnzip))
+ return;
+
+ QuaZipFile file(&zip);
+
+ // read the install profile
+ if (!zip.setCurrentFile("install_profile.json"))
+ return;
+
+ QJsonParseError jsonError;
+ if (!file.open(QIODevice::ReadOnly))
+ return;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(file.readAll(), &jsonError);
+ file.close();
+ if (jsonError.error != QJsonParseError::NoError)
+ return;
+
+ if (!jsonDoc.isObject())
+ return;
+
+ QJsonObject root = jsonDoc.object();
+
+ auto installVal = root.value("install");
+ auto versionInfoVal = root.value("versionInfo");
+ if (!installVal.isObject() || !versionInfoVal.isObject())
+ return;
+
+ try
+ {
+ newVersion = OneSixVersionFormat::versionFileFromJson(QJsonDocument(versionInfoVal.toObject()), QString(), false);
+ }
+ catch(Exception &err)
+ {
+ qWarning() << "Forge: Fatal error while parsing version file:" << err.what();
+ return;
+ }
+
+ for(auto problem: newVersion->getProblems())
+ {
+ qWarning() << "Forge: Problem found: " << problem.getDescription();
+ }
+ if(newVersion->getProblemSeverity() == ProblemSeverity::PROBLEM_ERROR)
+ {
+ qWarning() << "Forge: Errors found while parsing version file";
+ return;
+ }
+
+ QJsonObject installObj = installVal.toObject();
+ QString libraryName = installObj.value("path").toString();
+ internalPath = installObj.value("filePath").toString();
+ m_forgeVersionString = installObj.value("version").toString().remove("Forge", Qt::CaseInsensitive).trimmed();
+
+ // where do we put the library? decode the mojang path
+ GradleSpecifier lib(libraryName);
+
+ auto cacheentry = ENV.metacache()->resolveEntry("libraries", lib.toPath());
+ finalPath = "libraries/" + lib.toPath();
+ if (!FS::ensureFilePathExists(finalPath))
+ return;
+
+ if (!zip.setCurrentFile(internalPath))
+ return;
+ if (!file.open(QIODevice::ReadOnly))
+ return;
+ {
+ QByteArray data = file.readAll();
+ // extract file
+ QSaveFile extraction(finalPath);
+ if (!extraction.open(QIODevice::WriteOnly))
+ return;
+ if (extraction.write(data) != data.size())
+ return;
+ if (!extraction.commit())
+ return;
+ QCryptographicHash md5sum(QCryptographicHash::Md5);
+ md5sum.addData(data);
+
+ cacheentry->setStale(false);
+ cacheentry->setMD5Sum(md5sum.result().toHex().constData());
+ ENV.metacache()->updateEntry(cacheentry);
+ }
+ file.close();
+
+ m_forge_json = newVersion;
+}
+
+bool ForgeInstaller::add(OneSixInstance *to)
+{
+ if (!BaseInstaller::add(to))
+ {
+ return false;
+ }
+
+ if (!m_forge_json)
+ {
+ return false;
+ }
+
+ // A blacklist
+ QSet<QString> blacklist{"authlib", "realms"};
+ QList<QString> xzlist{"org.scala-lang", "com.typesafe"};
+
+ // get the minecraft version from the instance
+ VersionFilePtr minecraft;
+ auto minecraftPatch = to->getMinecraftProfile()->versionPatch("net.minecraft");
+ if(minecraftPatch)
+ {
+ minecraft = std::dynamic_pointer_cast<VersionFile>(minecraftPatch);
+ if(!minecraft)
+ {
+ auto mcWrap = std::dynamic_pointer_cast<MinecraftVersion>(minecraftPatch);
+ if(mcWrap)
+ {
+ minecraft = mcWrap->getVersionFile();
+ }
+ }
+ }
+
+ // for each library in the version we are adding (except for the blacklisted)
+ QMutableListIterator<LibraryPtr> iter(m_forge_json->libraries);
+ while (iter.hasNext())
+ {
+ auto library = iter.next();
+ QString libName = library->artifactId();
+ QString libVersion = library->version();
+ QString rawName = library->rawName();
+
+ // ignore lwjgl libraries.
+ if (g_VersionFilterData.lwjglWhitelist.contains(library->artifactPrefix()))
+ {
+ iter.remove();
+ continue;
+ }
+ // ignore other blacklisted (realms, authlib)
+ if (blacklist.contains(libName))
+ {
+ iter.remove();
+ continue;
+ }
+ // if minecraft version was found, ignore everything that is already in the minecraft version
+ if(minecraft)
+ {
+ bool found = false;
+ for (auto & lib: minecraft->libraries)
+ {
+ if(library->artifactPrefix() == lib->artifactPrefix() && library->version() == lib->version())
+ {
+ found = true;
+ break;
+ }
+ }
+ if (found)
+ continue;
+ }
+
+ // if this is the actual forge lib, set an absolute url for the download
+ if (m_forge_version->type == ForgeVersion::Gradle)
+ {
+ if (libName == "forge")
+ {
+ library->setClassifier("universal");
+ }
+ else if (libName == "minecraftforge")
+ {
+ QString forgeCoord("net.minecraftforge:forge:%1:universal");
+ // using insane form of the MC version...
+ QString longVersion = m_forge_version->mcver + "-" + m_forge_version->jobbuildver;
+ GradleSpecifier spec(forgeCoord.arg(longVersion));
+ library->setRawName(spec);
+ }
+ }
+ else
+ {
+ if (libName.contains("minecraftforge"))
+ {
+ library->setAbsoluteUrl(m_universal_url);
+ }
+ }
+
+ // mark bad libraries based on the xzlist above
+ for (auto entry : xzlist)
+ {
+ qDebug() << "Testing " << rawName << " : " << entry;
+ if (rawName.startsWith(entry))
+ {
+ library->setHint("forge-pack-xz");
+ break;
+ }
+ }
+ }
+ QString &args = m_forge_json->minecraftArguments;
+ QStringList tweakers;
+ {
+ QRegularExpression expression("--tweakClass ([a-zA-Z0-9\\.]*)");
+ QRegularExpressionMatch match = expression.match(args);
+ while (match.hasMatch())
+ {
+ tweakers.append(match.captured(1));
+ args.remove(match.capturedStart(), match.capturedLength());
+ match = expression.match(args);
+ }
+ if(tweakers.size())
+ {
+ args.operator=(args.trimmed());
+ m_forge_json->addTweakers = tweakers;
+ }
+ }
+ if(minecraft && args == minecraft->minecraftArguments)
+ {
+ args.clear();
+ }
+
+ m_forge_json->name = "Forge";
+ m_forge_json->fileId = id();
+ m_forge_json->version = m_forgeVersionString;
+ m_forge_json->dependsOnMinecraftVersion = to->intendedVersionId();
+ m_forge_json->order = 5;
+
+ // reset some things we do not want to be passed along.
+ m_forge_json->m_releaseTime = QDateTime();
+ m_forge_json->m_updateTime = QDateTime();
+ m_forge_json->minimumLauncherVersion = -1;
+ m_forge_json->type.clear();
+ m_forge_json->minecraftArguments.clear();
+ m_forge_json->minecraftVersion.clear();
+
+ QSaveFile file(filename(to->instanceRoot()));
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(m_forge_json, true).toJson());
+ file.commit();
+
+ return true;
+}
+
+bool ForgeInstaller::addLegacy(OneSixInstance *to)
+{
+ if (!BaseInstaller::add(to))
+ {
+ return false;
+ }
+ auto entry = ENV.metacache()->resolveEntry("minecraftforge", m_forge_version->filename());
+ finalPath = FS::PathCombine(to->jarModsDir(), m_forge_version->filename());
+ if (!FS::ensureFilePathExists(finalPath))
+ {
+ return false;
+ }
+ if (!QFile::copy(entry->getFullPath(), finalPath))
+ {
+ return false;
+ }
+ QJsonObject obj;
+ obj.insert("order", 5);
+ {
+ QJsonArray jarmodsPlus;
+ {
+ QJsonObject libObj;
+ libObj.insert("name", m_forge_version->universal_filename);
+ jarmodsPlus.append(libObj);
+ }
+ obj.insert("+jarMods", jarmodsPlus);
+ }
+
+ obj.insert("name", QString("Forge"));
+ obj.insert("fileId", id());
+ obj.insert("version", m_forge_version->jobbuildver);
+ obj.insert("mcVersion", to->intendedVersionId());
+ if (g_VersionFilterData.fmlLibsMapping.contains(m_forge_version->mcver))
+ {
+ QJsonArray traitsPlus;
+ traitsPlus.append(QString("legacyFML"));
+ obj.insert("+traits", traitsPlus);
+ }
+ auto fullversion = to->getMinecraftProfile();
+ fullversion->remove("net.minecraftforge");
+
+ QFile file(filename(to->instanceRoot()));
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(QJsonDocument(obj).toJson());
+ file.close();
+ return true;
+}
+
+class ForgeInstallTask : public Task
+{
+ Q_OBJECT
+public:
+ ForgeInstallTask(ForgeInstaller *installer, OneSixInstance *instance,
+ BaseVersionPtr version, QObject *parent = 0)
+ : Task(parent), m_installer(installer), m_instance(instance), m_version(version)
+ {
+ }
+
+protected:
+ void executeTask() override
+ {
+ setStatus(tr("Installing Forge..."));
+ ForgeVersionPtr forgeVersion = std::dynamic_pointer_cast<ForgeVersion>(m_version);
+ if (!forgeVersion)
+ {
+ emitFailed(tr("Unknown error occured"));
+ return;
+ }
+ prepare(forgeVersion);
+ }
+ void prepare(ForgeVersionPtr forgeVersion)
+ {
+ auto entry = ENV.metacache()->resolveEntry("minecraftforge", forgeVersion->filename());
+ auto installFunction = [this, entry, forgeVersion]()
+ {
+ if (!install(entry, forgeVersion))
+ {
+ qCritical() << "Failure installing Forge";
+ emitFailed(tr("Failure to install Forge"));
+ }
+ else
+ {
+ reload();
+ }
+ };
+
+ /*
+ * HACK IF the local non-stale file is too small, mark is as stale
+ *
+ * This fixes some problems with bad files acquired because of unhandled HTTP redirects
+ * in old versions of MultiMC.
+ */
+ if (!entry->isStale())
+ {
+ QFileInfo localFile(entry->getFullPath());
+ if (localFile.size() <= 0x4000)
+ {
+ entry->setStale(true);
+ }
+ }
+
+ if (entry->isStale())
+ {
+ NetJob *fjob = new NetJob("Forge download");
+ fjob->addNetAction(CacheDownload::make(forgeVersion->url(), entry));
+ connect(fjob, &NetJob::progress, this, &Task::setProgress);
+ connect(fjob, &NetJob::status, this, &Task::setStatus);
+ connect(fjob, &NetJob::failed, [this](QString reason)
+ { emitFailed(tr("Failure to download Forge:\n%1").arg(reason)); });
+ connect(fjob, &NetJob::succeeded, installFunction);
+ fjob->start();
+ }
+ else
+ {
+ installFunction();
+ }
+ }
+ bool install(const std::shared_ptr<MetaEntry> &entry, const ForgeVersionPtr &forgeVersion)
+ {
+ if (forgeVersion->usesInstaller())
+ {
+ QString forgePath = entry->getFullPath();
+ m_installer->prepare(forgePath, forgeVersion->universal_url);
+ return m_installer->add(m_instance);
+ }
+ else
+ return m_installer->addLegacy(m_instance);
+ }
+ void reload()
+ {
+ try
+ {
+ m_instance->reloadProfile();
+ emitSucceeded();
+ }
+ catch (Exception &e)
+ {
+ emitFailed(e.cause());
+ }
+ catch (...)
+ {
+ emitFailed(tr("Failed to load the version description file for reasons unknown."));
+ }
+ }
+
+private:
+ ForgeInstaller *m_installer;
+ OneSixInstance *m_instance;
+ BaseVersionPtr m_version;
+};
+
+Task *ForgeInstaller::createInstallTask(OneSixInstance *instance,
+ BaseVersionPtr version, QObject *parent)
+{
+ if (!version)
+ {
+ return nullptr;
+ }
+ m_forge_version = std::dynamic_pointer_cast<ForgeVersion>(version);
+ return new ForgeInstallTask(this, instance, version, parent);
+}
+
+#include "ForgeInstaller.moc"
diff --git a/api/logic/minecraft/forge/ForgeInstaller.h b/api/logic/minecraft/forge/ForgeInstaller.h
new file mode 100644
index 00000000..499a6fb3
--- /dev/null
+++ b/api/logic/minecraft/forge/ForgeInstaller.h
@@ -0,0 +1,52 @@
+/* Copyright 2013-2015 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 "BaseInstaller.h"
+
+#include <QString>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class VersionFile;
+class ForgeInstallTask;
+struct ForgeVersion;
+
+class MULTIMC_LOGIC_EXPORT ForgeInstaller : public BaseInstaller
+{
+ friend class ForgeInstallTask;
+public:
+ ForgeInstaller();
+ virtual ~ForgeInstaller(){}
+ virtual Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override;
+ virtual QString id() const override { return "net.minecraftforge"; }
+
+protected:
+ void prepare(const QString &filename, const QString &universalUrl);
+ bool add(OneSixInstance *to) override;
+ bool addLegacy(OneSixInstance *to);
+
+private:
+ // the parsed version json, read from the installer
+ std::shared_ptr<VersionFile> m_forge_json;
+ // the actual forge version
+ std::shared_ptr<ForgeVersion> m_forge_version;
+ QString internalPath;
+ QString finalPath;
+ QString m_forgeVersionString;
+ QString m_universal_url;
+};
diff --git a/api/logic/minecraft/forge/ForgeVersion.cpp b/api/logic/minecraft/forge/ForgeVersion.cpp
new file mode 100644
index 00000000..b859a28c
--- /dev/null
+++ b/api/logic/minecraft/forge/ForgeVersion.cpp
@@ -0,0 +1,55 @@
+#include "ForgeVersion.h"
+#include "minecraft/VersionFilterData.h"
+#include <QObject>
+
+QString ForgeVersion::name()
+{
+ return "Forge " + jobbuildver;
+}
+
+QString ForgeVersion::descriptor()
+{
+ return universal_filename;
+}
+
+QString ForgeVersion::typeString() const
+{
+ if (is_recommended)
+ return QObject::tr("Recommended");
+ return QString();
+}
+
+bool ForgeVersion::operator<(BaseVersion &a)
+{
+ ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a);
+ if (!pa)
+ return true;
+ return m_buildnr < pa->m_buildnr;
+}
+
+bool ForgeVersion::operator>(BaseVersion &a)
+{
+ ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a);
+ if (!pa)
+ return false;
+ return m_buildnr > pa->m_buildnr;
+}
+
+bool ForgeVersion::usesInstaller()
+{
+ if(installer_url.isEmpty())
+ return false;
+ if(g_VersionFilterData.forgeInstallerBlacklist.contains(mcver))
+ return false;
+ return true;
+}
+
+QString ForgeVersion::filename()
+{
+ return usesInstaller() ? installer_filename : universal_filename;
+}
+
+QString ForgeVersion::url()
+{
+ return usesInstaller() ? installer_url : universal_url;
+}
diff --git a/api/logic/minecraft/forge/ForgeVersion.h b/api/logic/minecraft/forge/ForgeVersion.h
new file mode 100644
index 00000000..e77d32f1
--- /dev/null
+++ b/api/logic/minecraft/forge/ForgeVersion.h
@@ -0,0 +1,42 @@
+#pragma once
+#include <QString>
+#include <memory>
+#include "BaseVersion.h"
+
+struct ForgeVersion;
+typedef std::shared_ptr<ForgeVersion> ForgeVersionPtr;
+
+struct ForgeVersion : public BaseVersion
+{
+ virtual QString descriptor() override;
+ virtual QString name() override;
+ virtual QString typeString() const override;
+ virtual bool operator<(BaseVersion &a) override;
+ virtual bool operator>(BaseVersion &a) override;
+
+ QString filename();
+ QString url();
+
+ enum
+ {
+ Invalid,
+ Legacy,
+ Gradle
+ } type = Invalid;
+
+ bool usesInstaller();
+
+ int m_buildnr = 0;
+ QString branch;
+ QString universal_url;
+ QString changelog_url;
+ QString installer_url;
+ QString jobbuildver;
+ QString mcver;
+ QString mcver_sane;
+ QString universal_filename;
+ QString installer_filename;
+ bool is_recommended = false;
+};
+
+Q_DECLARE_METATYPE(ForgeVersionPtr)
diff --git a/api/logic/minecraft/forge/ForgeVersionList.cpp b/api/logic/minecraft/forge/ForgeVersionList.cpp
new file mode 100644
index 00000000..de185e5f
--- /dev/null
+++ b/api/logic/minecraft/forge/ForgeVersionList.cpp
@@ -0,0 +1,450 @@
+/* Copyright 2013-2015 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 "ForgeVersionList.h"
+#include "ForgeVersion.h"
+
+#include "net/NetJob.h"
+#include "net/URLConstants.h"
+#include "Env.h"
+
+#include <QtNetwork>
+#include <QtXml>
+#include <QRegExp>
+
+#include <QDebug>
+
+ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent)
+{
+}
+
+Task *ForgeVersionList::getLoadTask()
+{
+ return new ForgeListLoadTask(this);
+}
+
+bool ForgeVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+const BaseVersionPtr ForgeVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+int ForgeVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+int ForgeVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ForgeVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<ForgeVersion>(m_vlist[index.row()]);
+ switch (role)
+ {
+ case VersionPointerRole:
+ return qVariantFromValue(m_vlist[index.row()]);
+
+ case VersionRole:
+ return version->name();
+
+ case VersionIdRole:
+ return version->descriptor();
+
+ case ParentGameVersionRole:
+ return version->mcver_sane;
+
+ case RecommendedRole:
+ return version->is_recommended;
+
+ case BranchRole:
+ return version->branch;
+
+ default:
+ return QVariant();
+ }
+}
+
+BaseVersionList::RoleList ForgeVersionList::providesRoles() const
+{
+ return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole, BranchRole};
+}
+
+BaseVersionPtr ForgeVersionList::getLatestStable() const
+{
+ return BaseVersionPtr();
+}
+
+void ForgeVersionList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ m_loaded = true;
+ endResetModel();
+ // NOW SORT!!
+ // sort();
+}
+
+void ForgeVersionList::sortVersions()
+{
+ // NO-OP for now
+}
+
+ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task()
+{
+ m_list = vlist;
+}
+
+void ForgeListLoadTask::executeTask()
+{
+ setStatus(tr("Fetching Forge version lists..."));
+ auto job = new NetJob("Version index");
+ // we do not care if the version is stale or not.
+ auto forgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "list.json");
+ auto gradleForgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "json");
+
+ // verify by poking the server.
+ forgeListEntry->setStale(true);
+ gradleForgeListEntry->setStale(true);
+
+ job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL),
+ forgeListEntry));
+ job->addNetAction(gradleListDownload = CacheDownload::make(
+ QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry));
+
+ connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed()));
+ connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed()));
+
+ listJob.reset(job);
+ connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded()));
+ connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ listJob->start();
+}
+
+bool ForgeListLoadTask::abort()
+{
+ return listJob->abort();
+}
+
+bool ForgeListLoadTask::parseForgeList(QList<BaseVersionPtr> &out)
+{
+ QByteArray data;
+ {
+ auto dlJob = listDownload;
+ auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
+ QFile listFile(filename);
+ if (!listFile.open(QIODevice::ReadOnly))
+ {
+ return false;
+ }
+ data = listFile.readAll();
+ dlJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed("Error parsing version list JSON:" + jsonError.errorString());
+ return false;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed("Error parsing version list JSON: JSON root is not an object");
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // Now, get the array of versions.
+ if (!root.value("builds").isArray())
+ {
+ emitFailed(
+ "Error parsing version list JSON: version list object is missing 'builds' array");
+ return false;
+ }
+ QJsonArray builds = root.value("builds").toArray();
+
+ for (int i = 0; i < builds.count(); i++)
+ {
+ // Load the version info.
+ if (!builds[i].isObject())
+ {
+ // FIXME: log this somewhere
+ continue;
+ }
+ QJsonObject obj = builds[i].toObject();
+ int build_nr = obj.value("build").toDouble(0);
+ if (!build_nr)
+ continue;
+ QJsonArray files = obj.value("files").toArray();
+ QString url, jobbuildver, mcver, buildtype, universal_filename;
+ QString changelog_url, installer_url;
+ QString installer_filename;
+ bool valid = false;
+ for (int j = 0; j < files.count(); j++)
+ {
+ if (!files[j].isObject())
+ {
+ continue;
+ }
+ QJsonObject file = files[j].toObject();
+ buildtype = file.value("buildtype").toString();
+ if ((buildtype == "client" || buildtype == "universal") && !valid)
+ {
+ mcver = file.value("mcver").toString();
+ url = file.value("url").toString();
+ jobbuildver = file.value("jobbuildver").toString();
+ int lastSlash = url.lastIndexOf('/');
+ universal_filename = url.mid(lastSlash + 1);
+ valid = true;
+ }
+ else if (buildtype == "changelog")
+ {
+ QString ext = file.value("ext").toString();
+ if (ext.isEmpty())
+ {
+ continue;
+ }
+ changelog_url = file.value("url").toString();
+ }
+ else if (buildtype == "installer")
+ {
+ installer_url = file.value("url").toString();
+ int lastSlash = installer_url.lastIndexOf('/');
+ installer_filename = installer_url.mid(lastSlash + 1);
+ }
+ }
+ if (valid)
+ {
+ // Now, we construct the version object and add it to the list.
+ std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
+ fVersion->universal_url = url;
+ fVersion->changelog_url = changelog_url;
+ fVersion->installer_url = installer_url;
+ fVersion->jobbuildver = jobbuildver;
+ fVersion->mcver = fVersion->mcver_sane = mcver;
+ fVersion->installer_filename = installer_filename;
+ fVersion->universal_filename = universal_filename;
+ fVersion->m_buildnr = build_nr;
+ fVersion->type = ForgeVersion::Legacy;
+ out.append(fVersion);
+ }
+ }
+
+ return true;
+}
+
+bool ForgeListLoadTask::parseForgeGradleList(QList<BaseVersionPtr> &out)
+{
+ QMap<int, std::shared_ptr<ForgeVersion>> lookup;
+ QByteArray data;
+ {
+ auto dlJob = gradleListDownload;
+ auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
+ QFile listFile(filename);
+ if (!listFile.open(QIODevice::ReadOnly))
+ {
+ return false;
+ }
+ data = listFile.readAll();
+ dlJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString());
+ return false;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed("Error parsing gradle version list JSON: JSON root is not an object");
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ // we probably could hard code these, but it might still be worth doing it this way
+ const QString webpath = root.value("webpath").toString();
+ const QString artifact = root.value("artifact").toString();
+
+ QJsonObject numbers = root.value("number").toObject();
+ for (auto it = numbers.begin(); it != numbers.end(); ++it)
+ {
+ QJsonObject number = it.value().toObject();
+ std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
+ fVersion->m_buildnr = number.value("build").toDouble();
+ if(fVersion->m_buildnr >= 953 && fVersion->m_buildnr <= 965)
+ {
+ qDebug() << fVersion->m_buildnr;
+ }
+ fVersion->jobbuildver = number.value("version").toString();
+ fVersion->branch = number.value("branch").toString("");
+ fVersion->mcver = number.value("mcversion").toString();
+ fVersion->universal_filename = "";
+ fVersion->installer_filename = "";
+ // HACK: here, we fix the minecraft version used by forge.
+ // HACK: this will inevitably break (later)
+ // FIXME: replace with a dictionary
+ fVersion->mcver_sane = fVersion->mcver;
+ fVersion->mcver_sane.replace("_pre", "-pre");
+
+ QString universal_filename, installer_filename;
+ QJsonArray files = number.value("files").toArray();
+ for (auto fIt = files.begin(); fIt != files.end(); ++fIt)
+ {
+ // TODO with gradle we also get checksums, use them
+ QJsonArray file = (*fIt).toArray();
+ if (file.size() < 3)
+ {
+ continue;
+ }
+
+ QString extension = file.at(0).toString();
+ QString part = file.at(1).toString();
+ QString checksum = file.at(2).toString();
+
+ // insane form of mcver is used here
+ QString longVersion = fVersion->mcver + "-" + fVersion->jobbuildver;
+ if (!fVersion->branch.isEmpty())
+ {
+ longVersion = longVersion + "-" + fVersion->branch;
+ }
+ QString filename = artifact + "-" + longVersion + "-" + part + "." + extension;
+
+ QString url = QString("%1/%2/%3")
+ .arg(webpath)
+ .arg(longVersion)
+ .arg(filename);
+
+ if (part == "installer")
+ {
+ fVersion->installer_url = url;
+ installer_filename = filename;
+ }
+ else if (part == "universal")
+ {
+ fVersion->universal_url = url;
+ universal_filename = filename;
+ }
+ else if (part == "changelog")
+ {
+ fVersion->changelog_url = url;
+ }
+ }
+ if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty())
+ {
+ continue;
+ }
+ fVersion->universal_filename = universal_filename;
+ fVersion->installer_filename = installer_filename;
+ fVersion->type = ForgeVersion::Gradle;
+ out.append(fVersion);
+ lookup[fVersion->m_buildnr] = fVersion;
+ }
+ QJsonObject promos = root.value("promos").toObject();
+ for (auto it = promos.begin(); it != promos.end(); ++it)
+ {
+ QString key = it.key();
+ int build = it.value().toInt();
+ QRegularExpression regexp("^(?<mcversion>[0-9]+(.[0-9]+)*)-(?<label>[a-z]+)$");
+ auto match = regexp.match(key);
+ if(!match.hasMatch())
+ {
+ qDebug() << key << "doesn't match." << "build" << build;
+ continue;
+ }
+
+ QString label = match.captured("label");
+ if(label != "recommended")
+ {
+ continue;
+ }
+ QString mcversion = match.captured("mcversion");
+ qDebug() << "Forge build" << build << "is the" << label << "for Minecraft" << mcversion << QString("<%1>").arg(key);
+ lookup[build]->is_recommended = true;
+ }
+ return true;
+}
+
+void ForgeListLoadTask::listDownloaded()
+{
+ QList<BaseVersionPtr> list;
+ bool ret = true;
+ if (!parseForgeList(list))
+ {
+ ret = false;
+ }
+ if (!parseForgeGradleList(list))
+ {
+ ret = false;
+ }
+
+ if (!ret)
+ {
+ return;
+ }
+ std::sort(list.begin(), list.end(), [](const BaseVersionPtr & l, const BaseVersionPtr & r)
+ { return (*l > *r); });
+
+ m_list->updateListData(list);
+
+ emitSucceeded();
+ return;
+}
+
+void ForgeListLoadTask::listFailed()
+{
+ auto &reply = listDownload->m_reply;
+ if (reply)
+ {
+ qCritical() << "Getting forge version list failed: " << reply->errorString();
+ }
+ else
+ {
+ qCritical() << "Getting forge version list failed for reasons unknown.";
+ }
+}
+
+void ForgeListLoadTask::gradleListFailed()
+{
+ auto &reply = gradleListDownload->m_reply;
+ if (reply)
+ {
+ qCritical() << "Getting forge version list failed: " << reply->errorString();
+ }
+ else
+ {
+ qCritical() << "Getting forge version list failed for reasons unknown.";
+ }
+}
diff --git a/api/logic/minecraft/forge/ForgeVersionList.h b/api/logic/minecraft/forge/ForgeVersionList.h
new file mode 100644
index 00000000..62c08b2a
--- /dev/null
+++ b/api/logic/minecraft/forge/ForgeVersionList.h
@@ -0,0 +1,90 @@
+/* Copyright 2013-2015 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 "ForgeVersion.h"
+
+#include <QObject>
+#include <QAbstractListModel>
+#include <QUrl>
+#include <QNetworkReply>
+
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT ForgeVersionList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ friend class ForgeListLoadTask;
+
+ explicit ForgeVersionList(QObject *parent = 0);
+
+ virtual Task *getLoadTask() override;
+ virtual bool isLoaded() override;
+ virtual const BaseVersionPtr at(int i) const override;
+ virtual int count() const override;
+ virtual void sortVersions() override;
+
+ virtual BaseVersionPtr getLatestStable() const override;
+
+ ForgeVersionPtr findVersionByVersionNr(QString version);
+
+ virtual QVariant data(const QModelIndex &index, int role) const override;
+ virtual RoleList providesRoles() const override;
+
+ virtual int columnCount(const QModelIndex &parent) const override;
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+
+ bool m_loaded = false;
+
+protected
+slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions) override;
+};
+
+class ForgeListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit ForgeListLoadTask(ForgeVersionList *vlist);
+
+ virtual void executeTask();
+ virtual bool abort();
+
+protected
+slots:
+ void listDownloaded();
+ void listFailed();
+ void gradleListFailed();
+
+protected:
+ NetJobPtr listJob;
+ ForgeVersionList *m_list;
+
+ CacheDownloadPtr listDownload;
+ CacheDownloadPtr gradleListDownload;
+
+private:
+ bool parseForgeList(QList<BaseVersionPtr> &out);
+ bool parseForgeGradleList(QList<BaseVersionPtr> &out);
+};
diff --git a/api/logic/minecraft/forge/ForgeXzDownload.cpp b/api/logic/minecraft/forge/ForgeXzDownload.cpp
new file mode 100644
index 00000000..adf96552
--- /dev/null
+++ b/api/logic/minecraft/forge/ForgeXzDownload.cpp
@@ -0,0 +1,358 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Env.h"
+#include "ForgeXzDownload.h"
+#include <FileSystem.h>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QDateTime>
+#include <QDir>
+#include <QDebug>
+
+ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction()
+{
+ m_entry = entry;
+ m_target_path = entry->getFullPath();
+ m_pack200_xz_file.setFileTemplate("./dl_temp.XXXXXX");
+ m_status = Job_NotStarted;
+ m_url_path = relative_path;
+ m_url = "http://files.minecraftforge.net/maven/" + m_url_path + ".pack.xz";
+}
+
+void ForgeXzDownload::start()
+{
+ m_status = Job_InProgress;
+ if (!m_entry->isStale())
+ {
+ m_status = Job_Finished;
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ // can we actually create the real, final file?
+ if (!FS::ensureFilePathExists(m_target_path))
+ {
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+
+ qDebug() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+ request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1());
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)");
+
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply.reset(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ // TODO: log the reason why
+ m_status = Job_Failed;
+}
+
+void ForgeXzDownload::failAndTryNextMirror()
+{
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+}
+
+void ForgeXzDownload::downloadFinished()
+{
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ if (m_pack200_xz_file.isOpen())
+ {
+ // we actually downloaded something! process and isntall it
+ decompressAndInstall();
+ return;
+ }
+ else
+ {
+ // something bad happened -- on the local machine!
+ m_status = Job_Failed;
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ // else the download failed
+ else
+ {
+ m_status = Job_Failed;
+ m_pack200_xz_file.close();
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ failAndTryNextMirror();
+ return;
+ }
+}
+
+void ForgeXzDownload::downloadReadyRead()
+{
+
+ if (!m_pack200_xz_file.isOpen())
+ {
+ if (!m_pack200_xz_file.open())
+ {
+ /*
+ * Can't open the file... the job failed
+ */
+ m_reply->abort();
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ m_pack200_xz_file.write(m_reply->readAll());
+}
+
+#include "xz.h"
+#include "unpack200.h"
+#include <stdexcept>
+#include <unistd.h>
+
+const size_t buffer_size = 8196;
+
+void ForgeXzDownload::decompressAndInstall()
+{
+ // rewind the downloaded temp file
+ m_pack200_xz_file.seek(0);
+ // de-xz'd file
+ QTemporaryFile pack200_file("./dl_temp.XXXXXX");
+ pack200_file.open();
+
+ bool xz_success = false;
+ // first, de-xz
+ {
+ uint8_t in[buffer_size];
+ uint8_t out[buffer_size];
+ struct xz_buf b;
+ struct xz_dec *s;
+ enum xz_ret ret;
+ xz_crc32_init();
+ xz_crc64_init();
+ s = xz_dec_init(XZ_DYNALLOC, 1 << 26);
+ if (s == nullptr)
+ {
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+ }
+ b.in = in;
+ b.in_pos = 0;
+ b.in_size = 0;
+ b.out = out;
+ b.out_pos = 0;
+ b.out_size = buffer_size;
+ while (!xz_success)
+ {
+ if (b.in_pos == b.in_size)
+ {
+ b.in_size = m_pack200_xz_file.read((char *)in, sizeof(in));
+ b.in_pos = 0;
+ }
+
+ ret = xz_dec_run(s, &b);
+
+ if (b.out_pos == sizeof(out))
+ {
+ if (pack200_file.write((char *)out, b.out_pos) != b.out_pos)
+ {
+ // msg = "Write error\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+ }
+
+ b.out_pos = 0;
+ }
+
+ if (ret == XZ_OK)
+ continue;
+
+ if (ret == XZ_UNSUPPORTED_CHECK)
+ {
+ // unsupported check. this is OK, but we should log this
+ continue;
+ }
+
+ if (pack200_file.write((char *)out, b.out_pos) != b.out_pos)
+ {
+ // write error
+ pack200_file.close();
+ xz_dec_end(s);
+ return;
+ }
+
+ switch (ret)
+ {
+ case XZ_STREAM_END:
+ xz_dec_end(s);
+ xz_success = true;
+ break;
+
+ case XZ_MEM_ERROR:
+ qCritical() << "Memory allocation failed\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_MEMLIMIT_ERROR:
+ qCritical() << "Memory usage limit reached\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_FORMAT_ERROR:
+ qCritical() << "Not a .xz file\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_OPTIONS_ERROR:
+ qCritical() << "Unsupported options in the .xz headers\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_DATA_ERROR:
+ case XZ_BUF_ERROR:
+ qCritical() << "File is corrupt\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ default:
+ qCritical() << "Bug!\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+ }
+ }
+ }
+ m_pack200_xz_file.remove();
+
+ // revert pack200
+ pack200_file.seek(0);
+ int handle_in = pack200_file.handle();
+ // FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects.
+ if(handle_in == -1)
+ {
+ qCritical() << "Error reopening " << pack200_file.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ int handle_in_dup = dup (handle_in);
+ if(handle_in_dup == -1)
+ {
+ qCritical() << "Error reopening " << pack200_file.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ FILE *file_in = fdopen (handle_in_dup, "rb");
+ if(!file_in)
+ {
+ qCritical() << "Error reopening " << pack200_file.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ QFile qfile_out(m_target_path);
+ if(!qfile_out.open(QIODevice::WriteOnly))
+ {
+ qCritical() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ int handle_out = qfile_out.handle();
+ if(handle_out == -1)
+ {
+ qCritical() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ int handle_out_dup = dup (handle_out);
+ if(handle_out_dup == -1)
+ {
+ qCritical() << "Error reopening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ FILE *file_out = fdopen (handle_out_dup, "wb");
+ if(!file_out)
+ {
+ qCritical() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ try
+ {
+ // NOTE: this takes ownership of both FILE pointers. That's why we duplicate them above.
+ unpack_200(file_in, file_out);
+ }
+ catch (std::runtime_error &err)
+ {
+ m_status = Job_Failed;
+ qCritical() << "Error unpacking " << pack200_file.fileName() << " : " << err.what();
+ QFile f(m_target_path);
+ if (f.exists())
+ f.remove();
+ failAndTryNextMirror();
+ return;
+ }
+ pack200_file.remove();
+
+ QFile jar_file(m_target_path);
+
+ if (!jar_file.open(QIODevice::ReadOnly))
+ {
+ jar_file.remove();
+ failAndTryNextMirror();
+ return;
+ }
+ auto hash = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5);
+ m_entry->setMD5Sum(hash.toHex().constData());
+ jar_file.close();
+
+ QFileInfo output_file_info(m_target_path);
+ m_entry->setETag(m_reply->rawHeader("ETag").constData());
+ m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch());
+ m_entry->setStale(false);
+ ENV.metacache()->updateEntry(m_entry);
+
+ m_reply.reset();
+ emit succeeded(m_index_within_job);
+}
diff --git a/api/logic/minecraft/forge/ForgeXzDownload.h b/api/logic/minecraft/forge/ForgeXzDownload.h
new file mode 100644
index 00000000..67524405
--- /dev/null
+++ b/api/logic/minecraft/forge/ForgeXzDownload.h
@@ -0,0 +1,59 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "net/NetAction.h"
+#include "net/HttpMetaCache.h"
+#include <QFile>
+#include <QTemporaryFile>
+
+typedef std::shared_ptr<class ForgeXzDownload> ForgeXzDownloadPtr;
+
+class ForgeXzDownload : public NetAction
+{
+ Q_OBJECT
+public:
+ MetaEntryPtr m_entry;
+ /// if saving to file, use the one specified in this string
+ QString m_target_path;
+ /// this is the output file, if any
+ QTemporaryFile m_pack200_xz_file;
+ /// path relative to the mirror base
+ QString m_url_path;
+
+public:
+ explicit ForgeXzDownload(QString relative_path, MetaEntryPtr entry);
+ static ForgeXzDownloadPtr make(QString relative_path, MetaEntryPtr entry)
+ {
+ return ForgeXzDownloadPtr(new ForgeXzDownload(relative_path, entry));
+ }
+ virtual ~ForgeXzDownload(){};
+
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+public
+slots:
+ virtual void start();
+
+private:
+ void decompressAndInstall();
+ void failAndTryNextMirror();
+};
diff --git a/api/logic/minecraft/forge/LegacyForge.cpp b/api/logic/minecraft/forge/LegacyForge.cpp
new file mode 100644
index 00000000..aa2c8063
--- /dev/null
+++ b/api/logic/minecraft/forge/LegacyForge.cpp
@@ -0,0 +1,56 @@
+/* Copyright 2013-2015 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 "LegacyForge.h"
+
+MinecraftForge::MinecraftForge(const QString &file) : Mod(file)
+{
+}
+
+bool MinecraftForge::FixVersionIfNeeded(QString newVersion)
+{/*
+ wxString reportedVersion = GetModVersion();
+ if(reportedVersion == "..." || reportedVersion.empty())
+ {
+ std::auto_ptr<wxFFileInputStream> in(new wxFFileInputStream("forge.zip"));
+ wxTempFileOutputStream out("forge.zip");
+ wxTextOutputStream textout(out);
+ wxZipInputStream inzip(*in);
+ wxZipOutputStream outzip(out);
+ std::auto_ptr<wxZipEntry> entry;
+ // preserve metadata
+ outzip.CopyArchiveMetaData(inzip);
+ // copy all entries
+ while (entry.reset(inzip.GetNextEntry()), entry.get() != NULL)
+ if (!outzip.CopyEntry(entry.release(), inzip))
+ return false;
+ // release last entry
+ in.reset();
+ outzip.PutNextEntry("forgeversion.properties");
+
+ wxStringTokenizer tokenizer(newVersion,".");
+ wxString verFile;
+ verFile << wxString("forge.major.number=") << tokenizer.GetNextToken() << "\n";
+ verFile << wxString("forge.minor.number=") << tokenizer.GetNextToken() << "\n";
+ verFile << wxString("forge.revision.number=") << tokenizer.GetNextToken() << "\n";
+ verFile << wxString("forge.build.number=") << tokenizer.GetNextToken() << "\n";
+ auto buf = verFile.ToUTF8();
+ outzip.Write(buf.data(), buf.length());
+ // check if we succeeded
+ return inzip.Eof() && outzip.Close() && out.Commit();
+ }
+ */
+ return true;
+}
diff --git a/api/logic/minecraft/forge/LegacyForge.h b/api/logic/minecraft/forge/LegacyForge.h
new file mode 100644
index 00000000..f51d5e85
--- /dev/null
+++ b/api/logic/minecraft/forge/LegacyForge.h
@@ -0,0 +1,25 @@
+/* Copyright 2013-2015 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 "minecraft/Mod.h"
+
+class MinecraftForge : public Mod
+{
+public:
+ MinecraftForge(const QString &file);
+ bool FixVersionIfNeeded(QString newVersion);
+};
diff --git a/api/logic/minecraft/ftb/FTBPlugin.cpp b/api/logic/minecraft/ftb/FTBPlugin.cpp
new file mode 100644
index 00000000..a142c106
--- /dev/null
+++ b/api/logic/minecraft/ftb/FTBPlugin.cpp
@@ -0,0 +1,395 @@
+#include "FTBPlugin.h"
+#include <Env.h>
+#include "FTBVersion.h"
+#include "LegacyFTBInstance.h"
+#include "OneSixFTBInstance.h"
+#include <BaseInstance.h>
+#include <InstanceList.h>
+#include <minecraft/MinecraftVersionList.h>
+#include <settings/INISettingsObject.h>
+#include <FileSystem.h>
+#include "QDebug"
+#include <QXmlStreamReader>
+#include <QRegularExpression>
+
+struct FTBRecord
+{
+ QString dirName;
+ QString name;
+ QString logo;
+ QString iconKey;
+ QString mcVersion;
+ QString description;
+ QString instanceDir;
+ QString templateDir;
+ bool operator==(const FTBRecord other) const
+ {
+ return instanceDir == other.instanceDir;
+ }
+};
+
+inline uint qHash(FTBRecord record)
+{
+ return qHash(record.instanceDir);
+}
+
+QSet<FTBRecord> discoverFTBInstances(SettingsObjectPtr globalSettings)
+{
+ QSet<FTBRecord> records;
+ QDir dir = QDir(globalSettings->get("FTBLauncherLocal").toString());
+ QDir dataDir = QDir(globalSettings->get("FTBRoot").toString());
+ if (!dataDir.exists())
+ {
+ qDebug() << "The FTB directory specified does not exist. Please check your settings";
+ return records;
+ }
+ else if (!dir.exists())
+ {
+ qDebug() << "The FTB launcher data directory specified does not exist. Please check "
+ "your settings";
+ return records;
+ }
+ dir.cd("ModPacks");
+ auto allFiles = dir.entryList(QDir::Readable | QDir::Files, QDir::Name);
+ for (auto filename : allFiles)
+ {
+ if (!filename.endsWith(".xml"))
+ continue;
+ auto fpath = dir.absoluteFilePath(filename);
+ QFile f(fpath);
+ qDebug() << "Discovering FTB instances -- " << fpath;
+ if (!f.open(QFile::ReadOnly))
+ continue;
+
+ // read the FTB packs XML.
+ QXmlStreamReader reader(&f);
+ while (!reader.atEnd())
+ {
+ switch (reader.readNext())
+ {
+ case QXmlStreamReader::StartElement:
+ {
+ if (reader.name() == "modpack")
+ {
+ QXmlStreamAttributes attrs = reader.attributes();
+ FTBRecord record;
+ record.dirName = attrs.value("dir").toString();
+ record.instanceDir = dataDir.absoluteFilePath(record.dirName);
+ record.templateDir = dir.absoluteFilePath(record.dirName);
+ QDir test(record.instanceDir);
+ qDebug() << dataDir.absolutePath() << record.instanceDir << record.dirName;
+ if (!test.exists())
+ continue;
+ record.name = attrs.value("name").toString();
+ record.logo = attrs.value("logo").toString();
+ QString logo = record.logo;
+ record.iconKey = logo.remove(QRegularExpression("\\..*"));
+ auto customVersions = attrs.value("customMCVersions");
+ if (!customVersions.isNull())
+ {
+ QMap<QString, QString> versionMatcher;
+ QString customVersionsStr = customVersions.toString();
+ QStringList list = customVersionsStr.split(';');
+ for (auto item : list)
+ {
+ auto segment = item.split('^');
+ if (segment.size() != 2)
+ {
+ qCritical() << "FTB: Segment of size < 2 in "
+ << customVersionsStr;
+ continue;
+ }
+ versionMatcher[segment[0]] = segment[1];
+ }
+ auto actualVersion = attrs.value("version").toString();
+ if (versionMatcher.contains(actualVersion))
+ {
+ record.mcVersion = versionMatcher[actualVersion];
+ }
+ else
+ {
+ record.mcVersion = attrs.value("mcVersion").toString();
+ }
+ }
+ else
+ {
+ record.mcVersion = attrs.value("mcVersion").toString();
+ }
+ record.description = attrs.value("description").toString();
+ records.insert(record);
+ }
+ break;
+ }
+ case QXmlStreamReader::EndElement:
+ break;
+ case QXmlStreamReader::Characters:
+ break;
+ default:
+ break;
+ }
+ }
+ f.close();
+ }
+ return records;
+}
+
+InstancePtr loadInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record)
+{
+ InstancePtr inst;
+
+ auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg"));
+ m_settings->registerSetting("InstanceType", "Legacy");
+
+ qDebug() << "Loading existing " << record.name;
+
+ QString inst_type = m_settings->get("InstanceType").toString();
+ if (inst_type == "LegacyFTB")
+ {
+ inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir));
+ }
+ else if (inst_type == "OneSixFTB")
+ {
+ inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir));
+ }
+ else
+ {
+ return nullptr;
+ }
+ qDebug() << "Construction " << record.instanceDir;
+
+ SettingsObject::Lock lock(inst->settings());
+ inst->init();
+ qDebug() << "Init " << record.instanceDir;
+ inst->setGroupInitial("FTB");
+ /**
+ * FIXME: this does not respect the user's preferences. BUT, it would work nicely with the planned pack support
+ * -> instead of changing the user values, change pack values (defaults you can look at and revert to)
+ */
+ /*
+ inst->setName(record.name);
+ inst->setIconKey(record.iconKey);
+ inst->setNotes(record.description);
+ */
+ if (inst->intendedVersionId() != record.mcVersion)
+ {
+ inst->setIntendedVersionId(record.mcVersion);
+ }
+ qDebug() << "Post-Process " << record.instanceDir;
+ if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap))
+ {
+ return nullptr;
+ }
+ qDebug() << "Final " << record.instanceDir;
+ return inst;
+}
+
+InstancePtr createInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record)
+{
+ QDir rootDir(record.instanceDir);
+
+ InstancePtr inst;
+
+ qDebug() << "Converting " << record.name << " as new.";
+
+ auto mcVersion = std::dynamic_pointer_cast<MinecraftVersion>(ENV.getVersion("net.minecraft", record.mcVersion));
+ if (!mcVersion)
+ {
+ qCritical() << "Can't load instance " << record.instanceDir
+ << " because minecraft version " << record.mcVersion
+ << " can't be resolved.";
+ return nullptr;
+ }
+
+ if (!rootDir.exists() && !rootDir.mkpath("."))
+ {
+ qCritical() << "Can't create instance folder" << record.instanceDir;
+ return nullptr;
+ }
+
+ auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg"));
+ m_settings->registerSetting("InstanceType", "Legacy");
+
+ if (mcVersion->usesLegacyLauncher())
+ {
+ m_settings->set("InstanceType", "LegacyFTB");
+ inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir));
+ }
+ else
+ {
+ m_settings->set("InstanceType", "OneSixFTB");
+ inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir));
+ }
+ // initialize
+ {
+ SettingsObject::Lock lock(inst->settings());
+ inst->setIntendedVersionId(mcVersion->descriptor());
+ inst->init();
+ inst->setGroupInitial("FTB");
+ inst->setName(record.name);
+ inst->setIconKey(record.iconKey);
+ inst->setNotes(record.description);
+ qDebug() << "Post-Process " << record.instanceDir;
+ if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap))
+ {
+ return nullptr;
+ }
+ }
+ return inst;
+}
+
+void FTBPlugin::loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList)
+{
+ // nothing to load when we don't have
+ if (globalSettings->get("TrackFTBInstances").toBool() != true)
+ {
+ return;
+ }
+
+ auto records = discoverFTBInstances(globalSettings);
+ if (!records.size())
+ {
+ qDebug() << "No FTB instances to load.";
+ return;
+ }
+ qDebug() << "Loading FTB instances! -- got " << records.size();
+ // process the records we acquired.
+ for (auto record : records)
+ {
+ qDebug() << "Loading FTB instance from " << record.instanceDir;
+ QString iconKey = record.iconKey;
+ // MMC->icons()->addIcon(iconKey, iconKey, FS::PathCombine(record.templateDir, record.logo), MMCIcon::Transient);
+ auto settingsFilePath = FS::PathCombine(record.instanceDir, "instance.cfg");
+ qDebug() << "ICON get!";
+
+ if (QFileInfo(settingsFilePath).exists())
+ {
+ auto instPtr = loadInstance(globalSettings, groupMap, record);
+ if (!instPtr)
+ {
+ qWarning() << "Couldn't load instance config:" << settingsFilePath;
+ if(!QFile::remove(settingsFilePath))
+ {
+ qWarning() << "Couldn't remove broken instance config!";
+ continue;
+ }
+ // failed to load, but removed the poisonous file
+ }
+ else
+ {
+ tempList.append(InstancePtr(instPtr));
+ continue;
+ }
+ }
+ auto instPtr = createInstance(globalSettings, groupMap, record);
+ if (!instPtr)
+ {
+ qWarning() << "Couldn't create FTB instance!";
+ continue;
+ }
+ tempList.append(InstancePtr(instPtr));
+ }
+}
+
+#ifdef Q_OS_WIN32
+#include <windows.h>
+static const int APPDATA_BUFFER_SIZE = 1024;
+#endif
+
+static QString getLocalCacheStorageLocation()
+{
+ QString ftbDefault;
+#ifdef Q_OS_WIN32
+ wchar_t buf[APPDATA_BUFFER_SIZE];
+ if (GetEnvironmentVariableW(L"LOCALAPPDATA", buf, APPDATA_BUFFER_SIZE)) // local
+ {
+ ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
+ }
+ else if (GetEnvironmentVariableW(L"APPDATA", buf, APPDATA_BUFFER_SIZE)) // roaming
+ {
+ ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
+ }
+ else
+ {
+ qCritical() << "Your LOCALAPPDATA and APPDATA folders are missing!"
+ " If you are on windows, this means your system is broken.";
+ }
+#elif defined(Q_OS_MAC)
+ ftbDefault = FS::PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher");
+#else
+ ftbDefault = QDir::home().absoluteFilePath(".ftblauncher");
+#endif
+ return ftbDefault;
+}
+
+
+static QString getRoamingStorageLocation()
+{
+ QString ftbDefault;
+#ifdef Q_OS_WIN32
+ wchar_t buf[APPDATA_BUFFER_SIZE];
+ QString cacheStorage;
+ if (GetEnvironmentVariableW(L"APPDATA", buf, APPDATA_BUFFER_SIZE))
+ {
+ ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
+ }
+ else
+ {
+ qCritical() << "Your APPDATA folder is missing! If you are on windows, this means your system is broken.";
+ }
+#elif defined(Q_OS_MAC)
+ ftbDefault = FS::PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher");
+#else
+ ftbDefault = QDir::home().absoluteFilePath(".ftblauncher");
+#endif
+ return ftbDefault;
+}
+
+void FTBPlugin::initialize(SettingsObjectPtr globalSettings)
+{
+ // FTB
+ globalSettings->registerSetting("TrackFTBInstances", false);
+ QString ftbRoaming = getRoamingStorageLocation();
+ QString ftbLocal = getLocalCacheStorageLocation();
+
+ globalSettings->registerSetting("FTBLauncherRoaming", ftbRoaming);
+ globalSettings->registerSetting("FTBLauncherLocal", ftbLocal);
+ qDebug() << "FTB Launcher paths:" << globalSettings->get("FTBLauncherRoaming").toString()
+ << "and" << globalSettings->get("FTBLauncherLocal").toString();
+
+ globalSettings->registerSetting("FTBRoot");
+ if (globalSettings->get("FTBRoot").isNull())
+ {
+ QString ftbRoot;
+ QFile f(QDir(globalSettings->get("FTBLauncherRoaming").toString()).absoluteFilePath("ftblaunch.cfg"));
+ qDebug() << "Attempting to read" << f.fileName();
+ if (f.open(QFile::ReadOnly))
+ {
+ const QString data = QString::fromLatin1(f.readAll());
+ QRegularExpression exp("installPath=(.*)");
+ ftbRoot = QDir::cleanPath(exp.match(data).captured(1));
+#ifdef Q_OS_WIN32
+ if (!ftbRoot.isEmpty())
+ {
+ if (ftbRoot.at(0).isLetter() && ftbRoot.size() > 1 && ftbRoot.at(1) == '/')
+ {
+ ftbRoot.remove(1, 1);
+ }
+ }
+#endif
+ if (ftbRoot.isEmpty())
+ {
+ qDebug() << "Failed to get FTB root path";
+ }
+ else
+ {
+ qDebug() << "FTB is installed at" << ftbRoot;
+ globalSettings->set("FTBRoot", ftbRoot);
+ }
+ }
+ else
+ {
+ qWarning() << "Couldn't open" << f.fileName() << ":" << f.errorString();
+ qWarning() << "This is perfectly normal if you don't have FTB installed";
+ }
+ }
+}
diff --git a/api/logic/minecraft/ftb/FTBPlugin.h b/api/logic/minecraft/ftb/FTBPlugin.h
new file mode 100644
index 00000000..6851d8a5
--- /dev/null
+++ b/api/logic/minecraft/ftb/FTBPlugin.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <BaseInstance.h>
+
+#include "multimc_logic_export.h"
+
+// Pseudo-plugin for FTB related things. Super derpy!
+class MULTIMC_LOGIC_EXPORT FTBPlugin
+{
+public:
+ static void initialize(SettingsObjectPtr globalSettings);
+ static void loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList);
+};
diff --git a/api/logic/minecraft/ftb/FTBProfileStrategy.cpp b/api/logic/minecraft/ftb/FTBProfileStrategy.cpp
new file mode 100644
index 00000000..f5faacae
--- /dev/null
+++ b/api/logic/minecraft/ftb/FTBProfileStrategy.cpp
@@ -0,0 +1,128 @@
+#include "FTBProfileStrategy.h"
+#include "OneSixFTBInstance.h"
+
+#include "minecraft/VersionBuildError.h"
+#include "minecraft/MinecraftVersionList.h"
+#include <FileSystem.h>
+
+#include <QDir>
+#include <QUuid>
+#include <QJsonDocument>
+#include <QJsonArray>
+
+FTBProfileStrategy::FTBProfileStrategy(OneSixFTBInstance* instance) : OneSixProfileStrategy(instance)
+{
+}
+
+void FTBProfileStrategy::loadDefaultBuiltinPatches()
+{
+ // FIXME: this should be here, but it needs us to be able to deal with multiple libraries paths
+ // OneSixProfileStrategy::loadDefaultBuiltinPatches();
+ auto mcVersion = m_instance->intendedVersionId();
+ auto nativeInstance = dynamic_cast<OneSixFTBInstance *>(m_instance);
+
+ ProfilePatchPtr minecraftPatch;
+ {
+ auto mcJson = m_instance->versionsPath().absoluteFilePath(mcVersion + "/" + mcVersion + ".json");
+ // load up the base minecraft patch
+ if(QFile::exists(mcJson))
+ {
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false);
+ file->fileId = "net.minecraft";
+ file->name = QObject::tr("Minecraft (tracked)");
+ file->setVanilla(true);
+ if(file->version.isEmpty())
+ {
+ file->version = mcVersion;
+ }
+ for(auto addLib: file->libraries)
+ {
+ addLib->setHint("local");
+ addLib->setStoragePrefix(nativeInstance->librariesPath().absolutePath());
+ }
+ minecraftPatch = std::dynamic_pointer_cast<ProfilePatch>(file);
+ }
+ else
+ {
+ throw VersionIncomplete("net.minecraft");
+ }
+ minecraftPatch->setOrder(-2);
+ }
+ profile->appendPatch(minecraftPatch);
+
+ ProfilePatchPtr packPatch;
+ {
+ auto mcJson = m_instance->minecraftRoot() + "/pack.json";
+ // load up the base minecraft patch
+ if(QFile::exists(mcJson))
+ {
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false);
+
+ // adapt the loaded file - the FTB patch file format is different than ours.
+ file->minecraftVersion.clear();
+ for(auto addLib: file->libraries)
+ {
+ addLib->setHint("local");
+ addLib->setStoragePrefix(nativeInstance->librariesPath().absolutePath());
+ }
+ file->fileId = "org.multimc.ftb.pack";
+ file->setVanilla(true);
+ file->name = QObject::tr("%1 (FTB pack)").arg(m_instance->name());
+ if(file->version.isEmpty())
+ {
+ file->version = QObject::tr("Unknown");
+ QFile versionFile (FS::PathCombine(m_instance->instanceRoot(), "version"));
+ if(versionFile.exists())
+ {
+ if(versionFile.open(QIODevice::ReadOnly))
+ {
+ // FIXME: just guessing the encoding/charset here.
+ auto version = QString::fromUtf8(versionFile.readAll());
+ file->version = version;
+ }
+ }
+ }
+ packPatch = std::dynamic_pointer_cast<ProfilePatch>(file);
+ }
+ else
+ {
+ throw VersionIncomplete("org.multimc.ftb.pack");
+ }
+ packPatch->setOrder(1);
+ }
+ profile->appendPatch(packPatch);
+
+}
+
+void FTBProfileStrategy::load()
+{
+ profile->clearPatches();
+
+ loadDefaultBuiltinPatches();
+ loadUserPatches();
+}
+
+bool FTBProfileStrategy::saveOrder(ProfileUtils::PatchOrder order)
+{
+ return false;
+}
+
+bool FTBProfileStrategy::resetOrder()
+{
+ return false;
+}
+
+bool FTBProfileStrategy::installJarMods(QStringList filepaths)
+{
+ return false;
+}
+
+bool FTBProfileStrategy::customizePatch(ProfilePatchPtr patch)
+{
+ return false;
+}
+
+bool FTBProfileStrategy::revertPatch(ProfilePatchPtr patch)
+{
+ return false;
+}
diff --git a/api/logic/minecraft/ftb/FTBProfileStrategy.h b/api/logic/minecraft/ftb/FTBProfileStrategy.h
new file mode 100644
index 00000000..522af098
--- /dev/null
+++ b/api/logic/minecraft/ftb/FTBProfileStrategy.h
@@ -0,0 +1,21 @@
+#pragma once
+#include "minecraft/ProfileStrategy.h"
+#include "minecraft/onesix/OneSixProfileStrategy.h"
+
+class OneSixFTBInstance;
+
+class FTBProfileStrategy : public OneSixProfileStrategy
+{
+public:
+ FTBProfileStrategy(OneSixFTBInstance * instance);
+ virtual ~FTBProfileStrategy() {};
+ virtual void load() override;
+ virtual bool resetOrder() override;
+ virtual bool saveOrder(ProfileUtils::PatchOrder order) override;
+ virtual bool installJarMods(QStringList filepaths) override;
+ virtual bool customizePatch (ProfilePatchPtr patch) override;
+ virtual bool revertPatch (ProfilePatchPtr patch) override;
+
+protected:
+ virtual void loadDefaultBuiltinPatches() override;
+};
diff --git a/api/logic/minecraft/ftb/FTBVersion.h b/api/logic/minecraft/ftb/FTBVersion.h
new file mode 100644
index 00000000..805319b4
--- /dev/null
+++ b/api/logic/minecraft/ftb/FTBVersion.h
@@ -0,0 +1,32 @@
+#pragma once
+#include <minecraft/MinecraftVersion.h>
+
+class FTBVersion : public BaseVersion
+{
+public:
+ FTBVersion(MinecraftVersionPtr parent) : m_version(parent){};
+
+public:
+ virtual QString descriptor() override
+ {
+ return m_version->descriptor();
+ }
+
+ virtual QString name() override
+ {
+ return m_version->name();
+ }
+
+ virtual QString typeString() const override
+ {
+ return m_version->typeString();
+ }
+
+ MinecraftVersionPtr getMinecraftVersion()
+ {
+ return m_version;
+ }
+
+private:
+ MinecraftVersionPtr m_version;
+};
diff --git a/api/logic/minecraft/ftb/LegacyFTBInstance.cpp b/api/logic/minecraft/ftb/LegacyFTBInstance.cpp
new file mode 100644
index 00000000..a7091f1d
--- /dev/null
+++ b/api/logic/minecraft/ftb/LegacyFTBInstance.cpp
@@ -0,0 +1,27 @@
+#include "LegacyFTBInstance.h"
+#include <settings/INISettingsObject.h>
+#include <QDir>
+
+LegacyFTBInstance::LegacyFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) :
+ LegacyInstance(globalSettings, settings, rootDir)
+{
+}
+
+QString LegacyFTBInstance::id() const
+{
+ return "FTB/" + BaseInstance::id();
+}
+
+void LegacyFTBInstance::copy(const QDir &newDir)
+{
+ // set the target instance to be plain Legacy
+ INISettingsObject settings_obj(newDir.absoluteFilePath("instance.cfg"));
+ settings_obj.registerSetting("InstanceType", "Legacy");
+ QString inst_type = settings_obj.get("InstanceType").toString();
+ settings_obj.set("InstanceType", "Legacy");
+}
+
+QString LegacyFTBInstance::typeName() const
+{
+ return tr("Legacy FTB");
+}
diff --git a/api/logic/minecraft/ftb/LegacyFTBInstance.h b/api/logic/minecraft/ftb/LegacyFTBInstance.h
new file mode 100644
index 00000000..7178bca4
--- /dev/null
+++ b/api/logic/minecraft/ftb/LegacyFTBInstance.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "minecraft/legacy/LegacyInstance.h"
+
+class LegacyFTBInstance : public LegacyInstance
+{
+ Q_OBJECT
+public:
+ explicit LegacyFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+ virtual QString id() const;
+ virtual void copy(const QDir &newDir);
+ virtual QString typeName() const;
+ bool canExport() const override
+ {
+ return false;
+ }
+};
diff --git a/api/logic/minecraft/ftb/OneSixFTBInstance.cpp b/api/logic/minecraft/ftb/OneSixFTBInstance.cpp
new file mode 100644
index 00000000..81e939a1
--- /dev/null
+++ b/api/logic/minecraft/ftb/OneSixFTBInstance.cpp
@@ -0,0 +1,138 @@
+#include "OneSixFTBInstance.h"
+#include "FTBProfileStrategy.h"
+
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/GradleSpecifier.h"
+#include "tasks/SequentialTask.h"
+#include <settings/INISettingsObject.h>
+#include <FileSystem.h>
+
+#include <QJsonArray>
+
+OneSixFTBInstance::OneSixFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) :
+ OneSixInstance(globalSettings, settings, rootDir)
+{
+ m_globalSettings = globalSettings;
+}
+
+void OneSixFTBInstance::copy(const QDir &newDir)
+{
+ QStringList libraryNames;
+ // create patch file
+ {
+ qDebug()<< "Creating patch file for FTB instance...";
+ QFile f(minecraftRoot() + "/pack.json");
+ if (!f.open(QFile::ReadOnly))
+ {
+ qCritical() << "Couldn't open" << f.fileName() << ":" << f.errorString();
+ return;
+ }
+ QJsonObject root = QJsonDocument::fromJson(f.readAll()).object();
+ QJsonArray libs = root.value("libraries").toArray();
+ QJsonArray outLibs;
+ for (auto lib : libs)
+ {
+ QJsonObject libObj = lib.toObject();
+ libObj.insert("MMC-hint", QString("local"));
+ libObj.insert("insert", QString("prepend"));
+ libraryNames.append(libObj.value("name").toString());
+ outLibs.append(libObj);
+ }
+ root.remove("libraries");
+ root.remove("id");
+
+ // HACK HACK HACK HACK
+ // A workaround for a problem in MultiMC, triggered by a historical problem in FTB,
+ // triggered by Mojang getting their library versions wrong in 1.7.10
+ if(intendedVersionId() == "1.7.10")
+ {
+ auto insert = [&outLibs, &libraryNames](QString name)
+ {
+ QJsonObject libObj;
+ libObj.insert("insert", QString("replace"));
+ libObj.insert("name", name);
+ libraryNames.push_back(name);
+ outLibs.prepend(libObj);
+ };
+ insert("com.google.guava:guava:16.0");
+ insert("org.apache.commons:commons-lang3:3.2.1");
+ }
+ root.insert("+libraries", outLibs);
+ root.insert("order", 1);
+ root.insert("fileId", QString("org.multimc.ftb.pack.json"));
+ root.insert("name", name());
+ root.insert("mcVersion", intendedVersionId());
+ root.insert("version", intendedVersionId());
+ FS::ensureFilePathExists(newDir.absoluteFilePath("patches/ftb.json"));
+ QFile out(newDir.absoluteFilePath("patches/ftb.json"));
+ if (!out.open(QFile::WriteOnly | QFile::Truncate))
+ {
+ qCritical() << "Couldn't open" << out.fileName() << ":" << out.errorString();
+ return;
+ }
+ out.write(QJsonDocument(root).toJson());
+ }
+ // copy libraries
+ {
+ qDebug() << "Copying FTB libraries";
+ for (auto library : libraryNames)
+ {
+ GradleSpecifier lib(library);
+ const QString out = QDir::current().absoluteFilePath("libraries/" + lib.toPath());
+ if (QFile::exists(out))
+ {
+ continue;
+ }
+ if (!FS::ensureFilePathExists(out))
+ {
+ qCritical() << "Couldn't create folder structure for" << out;
+ }
+ if (!QFile::copy(librariesPath().absoluteFilePath(lib.toPath()), out))
+ {
+ qCritical() << "Couldn't copy" << QString(lib);
+ }
+ }
+ }
+ // now set the target instance to be plain OneSix
+ INISettingsObject settings_obj(newDir.absoluteFilePath("instance.cfg"));
+ settings_obj.registerSetting("InstanceType", "Legacy");
+ QString inst_type = settings_obj.get("InstanceType").toString();
+ settings_obj.set("InstanceType", "OneSix");
+}
+
+QString OneSixFTBInstance::id() const
+{
+ return "FTB/" + BaseInstance::id();
+}
+
+QDir OneSixFTBInstance::librariesPath() const
+{
+ return QDir(m_globalSettings->get("FTBRoot").toString() + "/libraries");
+}
+
+QDir OneSixFTBInstance::versionsPath() const
+{
+ return QDir(m_globalSettings->get("FTBRoot").toString() + "/versions");
+}
+
+bool OneSixFTBInstance::providesVersionFile() const
+{
+ return true;
+}
+
+void OneSixFTBInstance::createProfile()
+{
+ m_profile.reset(new MinecraftProfile(new FTBProfileStrategy(this)));
+}
+
+std::shared_ptr<Task> OneSixFTBInstance::createUpdateTask()
+{
+ return OneSixInstance::createUpdateTask();
+}
+
+QString OneSixFTBInstance::typeName() const
+{
+ return tr("OneSix FTB");
+}
+
+#include "OneSixFTBInstance.moc"
diff --git a/api/logic/minecraft/ftb/OneSixFTBInstance.h b/api/logic/minecraft/ftb/OneSixFTBInstance.h
new file mode 100644
index 00000000..e7f8f485
--- /dev/null
+++ b/api/logic/minecraft/ftb/OneSixFTBInstance.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "minecraft/onesix/OneSixInstance.h"
+
+class OneSixFTBInstance : public OneSixInstance
+{
+ Q_OBJECT
+public:
+ explicit OneSixFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+ virtual ~OneSixFTBInstance(){};
+
+ void copy(const QDir &newDir) override;
+
+ virtual void createProfile() override;
+
+ virtual std::shared_ptr<Task> createUpdateTask() override;
+
+ virtual QString id() const override;
+
+ QDir librariesPath() const override;
+ QDir versionsPath() const override;
+ bool providesVersionFile() const override;
+ virtual QString typeName() const override;
+ bool canExport() const override
+ {
+ return false;
+ }
+private:
+ SettingsObjectPtr m_globalSettings;
+};
diff --git a/api/logic/minecraft/legacy/LegacyInstance.cpp b/api/logic/minecraft/legacy/LegacyInstance.cpp
new file mode 100644
index 00000000..f8264f20
--- /dev/null
+++ b/api/logic/minecraft/legacy/LegacyInstance.cpp
@@ -0,0 +1,453 @@
+/* Copyright 2013-2015 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 <QFileInfo>
+#include <QDir>
+#include <settings/Setting.h>
+
+#include "LegacyInstance.h"
+
+#include "minecraft/legacy/LegacyUpdate.h"
+#include "launch/LaunchTask.h"
+#include <launch/steps/LaunchMinecraft.h>
+#include <launch/steps/PostLaunchCommand.h>
+#include <launch/steps/ModMinecraftJar.h>
+#include <launch/steps/Update.h>
+#include <launch/steps/PreLaunchCommand.h>
+#include <launch/steps/TextPrint.h>
+#include <launch/steps/CheckJava.h>
+#include "minecraft/ModList.h"
+#include "minecraft/WorldList.h"
+#include <MMCZip.h>
+#include <FileSystem.h>
+
+LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
+ : MinecraftInstance(globalSettings, settings, rootDir)
+{
+ m_lwjglFolderSetting = globalSettings->getSetting("LWJGLDir");
+ settings->registerSetting("NeedsRebuild", true);
+ settings->registerSetting("ShouldUpdate", false);
+ settings->registerSetting("JarVersion", "Unknown");
+ settings->registerSetting("LwjglVersion", "2.9.0");
+ settings->registerSetting("IntendedJarVersion", "");
+ /*
+ * custom base jar has no default. it is determined in code... see the accessor methods for
+ *it
+ *
+ * for instances that DO NOT have the CustomBaseJar setting (legacy instances),
+ * [.]minecraft/bin/mcbackup.jar is the default base jar
+ */
+ settings->registerSetting("UseCustomBaseJar", true);
+ settings->registerSetting("CustomBaseJar", "");
+}
+
+QString LegacyInstance::baseJar() const
+{
+ bool customJar = m_settings->get("UseCustomBaseJar").toBool();
+ if (customJar)
+ {
+ return customBaseJar();
+ }
+ else
+ return defaultBaseJar();
+}
+
+QString LegacyInstance::customBaseJar() const
+{
+ QString value = m_settings->get("CustomBaseJar").toString();
+ if (value.isNull() || value.isEmpty())
+ {
+ return defaultCustomBaseJar();
+ }
+ return value;
+}
+
+void LegacyInstance::setCustomBaseJar(QString val)
+{
+ if (val.isNull() || val.isEmpty() || val == defaultCustomBaseJar())
+ m_settings->reset("CustomBaseJar");
+ else
+ m_settings->set("CustomBaseJar", val);
+}
+
+void LegacyInstance::setShouldUseCustomBaseJar(bool val)
+{
+ m_settings->set("UseCustomBaseJar", val);
+}
+
+bool LegacyInstance::shouldUseCustomBaseJar() const
+{
+ return m_settings->get("UseCustomBaseJar").toBool();
+}
+
+
+std::shared_ptr<Task> LegacyInstance::createUpdateTask()
+{
+ // make sure the jar mods list is initialized by asking for it.
+ auto list = jarModList();
+ // create an update task
+ return std::shared_ptr<Task>(new LegacyUpdate(this, this));
+}
+
+std::shared_ptr<LaunchTask> LegacyInstance::createLaunchTask(AuthSessionPtr session)
+{
+ auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(getSharedPtr()));
+ auto pptr = process.get();
+
+ // print a header
+ {
+ process->appendStep(std::make_shared<TextPrint>(pptr, "Minecraft folder is:\n" + minecraftRoot() + "\n\n", MessageLevel::MultiMC));
+ }
+ {
+ auto step = std::make_shared<CheckJava>(pptr);
+ process->appendStep(step);
+ }
+ // run pre-launch command if that's needed
+ if(getPreLaunchCommand().size())
+ {
+ auto step = std::make_shared<PreLaunchCommand>(pptr);
+ step->setWorkingDirectory(minecraftRoot());
+ process->appendStep(step);
+ }
+ // if we aren't in offline mode,.
+ if(session->status != AuthSession::PlayableOffline)
+ {
+ process->appendStep(std::make_shared<Update>(pptr));
+ }
+ // if there are any jar mods
+ if(getJarMods().size())
+ {
+ auto step = std::make_shared<ModMinecraftJar>(pptr);
+ process->appendStep(step);
+ }
+ // actually launch the game
+ {
+ auto step = std::make_shared<LaunchMinecraft>(pptr);
+ step->setWorkingDirectory(minecraftRoot());
+ step->setAuthSession(session);
+ process->appendStep(step);
+ }
+ // run post-exit command if that's needed
+ if(getPostExitCommand().size())
+ {
+ auto step = std::make_shared<PostLaunchCommand>(pptr);
+ step->setWorkingDirectory(minecraftRoot());
+ process->appendStep(step);
+ }
+ if (session)
+ {
+ process->setCensorFilter(createCensorFilterFromSession(session));
+ }
+ return process;
+}
+
+std::shared_ptr<Task> LegacyInstance::createJarModdingTask()
+{
+ class JarModTask : public Task
+ {
+ public:
+ explicit JarModTask(std::shared_ptr<LegacyInstance> inst) : Task(nullptr), m_inst(inst)
+ {
+ }
+ virtual void executeTask()
+ {
+ if (!m_inst->shouldRebuild())
+ {
+ emitSucceeded();
+ return;
+ }
+
+ // Get the mod list
+ auto modList = m_inst->getJarMods();
+
+ QFileInfo runnableJar(m_inst->runnableJar());
+ QFileInfo baseJar(m_inst->baseJar());
+ bool base_is_custom = m_inst->shouldUseCustomBaseJar();
+
+ // Nothing to do if there are no jar mods to install, no backup and just the mc jar
+ if (base_is_custom)
+ {
+ // yes, this can happen if the instance only has the runnable jar and not the base jar
+ // it *could* be assumed that such an instance is vanilla, but that wouldn't be safe
+ // because that's not something mmc4 guarantees
+ if (runnableJar.isFile() && !baseJar.exists() && modList.empty())
+ {
+ m_inst->setShouldRebuild(false);
+ emitSucceeded();
+ return;
+ }
+
+ setStatus(tr("Installing mods: Backing up minecraft.jar ..."));
+ if (!baseJar.exists() && !QFile::copy(runnableJar.filePath(), baseJar.filePath()))
+ {
+ emitFailed("It seems both the active and base jar are gone. A fresh base jar will "
+ "be used on next run.");
+ m_inst->setShouldRebuild(true);
+ m_inst->setShouldUpdate(true);
+ m_inst->setShouldUseCustomBaseJar(false);
+ return;
+ }
+ }
+
+ if (!baseJar.exists())
+ {
+ emitFailed("The base jar " + baseJar.filePath() + " does not exist");
+ return;
+ }
+
+ if (runnableJar.exists() && !QFile::remove(runnableJar.filePath()))
+ {
+ emitFailed("Failed to delete old minecraft.jar");
+ return;
+ }
+
+ setStatus(tr("Installing mods: Opening minecraft.jar ..."));
+
+ QString outputJarPath = runnableJar.filePath();
+ QString inputJarPath = baseJar.filePath();
+
+ if(!MMCZip::createModdedJar(inputJarPath, outputJarPath, modList))
+ {
+ emitFailed(tr("Failed to create the custom Minecraft jar file."));
+ return;
+ }
+ m_inst->setShouldRebuild(false);
+ // inst->UpdateVersion(true);
+ emitSucceeded();
+ return;
+
+ }
+ std::shared_ptr<LegacyInstance> m_inst;
+ };
+ return std::make_shared<JarModTask>(std::dynamic_pointer_cast<LegacyInstance>(shared_from_this()));
+}
+
+QString LegacyInstance::createLaunchScript(AuthSessionPtr session)
+{
+ QString launchScript;
+
+ // window size
+ QString windowParams;
+ if (settings()->get("LaunchMaximized").toBool())
+ {
+ windowParams = "max";
+ }
+ else
+ {
+ windowParams = QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt());
+ }
+
+ QString lwjgl = QDir(m_lwjglFolderSetting->get().toString() + "/" + lwjglVersion()).absolutePath();
+ launchScript += "userName " + session->player_name + "\n";
+ launchScript += "sessionId " + session->session + "\n";
+ launchScript += "windowTitle " + windowTitle() + "\n";
+ launchScript += "windowParams " + windowParams + "\n";
+ launchScript += "lwjgl " + lwjgl + "\n";
+ launchScript += "launcher legacy\n";
+ return launchScript;
+}
+
+void LegacyInstance::cleanupAfterRun()
+{
+ // FIXME: delete the launcher and icons and whatnot.
+}
+
+std::shared_ptr<ModList> LegacyInstance::coreModList() const
+{
+ if (!core_mod_list)
+ {
+ core_mod_list.reset(new ModList(coreModsDir()));
+ }
+ core_mod_list->update();
+ return core_mod_list;
+}
+
+std::shared_ptr<ModList> LegacyInstance::jarModList() const
+{
+ if (!jar_mod_list)
+ {
+ auto list = new ModList(jarModsDir(), modListFile());
+ connect(list, SIGNAL(changed()), SLOT(jarModsChanged()));
+ jar_mod_list.reset(list);
+ }
+ jar_mod_list->update();
+ return jar_mod_list;
+}
+
+QList<Mod> LegacyInstance::getJarMods() const
+{
+ return jarModList()->allMods();
+}
+
+void LegacyInstance::jarModsChanged()
+{
+ qDebug() << "Jar mods of instance " << name() << " have changed. Jar will be rebuilt.";
+ setShouldRebuild(true);
+}
+
+std::shared_ptr<ModList> LegacyInstance::loaderModList() const
+{
+ if (!loader_mod_list)
+ {
+ loader_mod_list.reset(new ModList(loaderModsDir()));
+ }
+ loader_mod_list->update();
+ return loader_mod_list;
+}
+
+std::shared_ptr<ModList> LegacyInstance::texturePackList() const
+{
+ if (!texture_pack_list)
+ {
+ texture_pack_list.reset(new ModList(texturePacksDir()));
+ }
+ texture_pack_list->update();
+ return texture_pack_list;
+}
+
+std::shared_ptr<WorldList> LegacyInstance::worldList() const
+{
+ if (!m_world_list)
+ {
+ m_world_list.reset(new WorldList(savesDir()));
+ }
+ return m_world_list;
+}
+
+QString LegacyInstance::jarModsDir() const
+{
+ return FS::PathCombine(instanceRoot(), "instMods");
+}
+
+QString LegacyInstance::binDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "bin");
+}
+
+QString LegacyInstance::libDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "lib");
+}
+
+QString LegacyInstance::savesDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "saves");
+}
+
+QString LegacyInstance::loaderModsDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "mods");
+}
+
+QString LegacyInstance::coreModsDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "coremods");
+}
+
+QString LegacyInstance::resourceDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "resources");
+}
+QString LegacyInstance::texturePacksDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "texturepacks");
+}
+
+QString LegacyInstance::runnableJar() const
+{
+ return FS::PathCombine(binDir(), "minecraft.jar");
+}
+
+QString LegacyInstance::modListFile() const
+{
+ return FS::PathCombine(instanceRoot(), "modlist");
+}
+
+QString LegacyInstance::instanceConfigFolder() const
+{
+ return FS::PathCombine(minecraftRoot(), "config");
+}
+
+bool LegacyInstance::shouldRebuild() const
+{
+ return m_settings->get("NeedsRebuild").toBool();
+}
+
+void LegacyInstance::setShouldRebuild(bool val)
+{
+ m_settings->set("NeedsRebuild", val);
+}
+
+QString LegacyInstance::currentVersionId() const
+{
+ return m_settings->get("JarVersion").toString();
+}
+
+QString LegacyInstance::lwjglVersion() const
+{
+ return m_settings->get("LwjglVersion").toString();
+}
+
+void LegacyInstance::setLWJGLVersion(QString val)
+{
+ m_settings->set("LwjglVersion", val);
+}
+
+QString LegacyInstance::intendedVersionId() const
+{
+ return m_settings->get("IntendedJarVersion").toString();
+}
+
+bool LegacyInstance::setIntendedVersionId(QString version)
+{
+ settings()->set("IntendedJarVersion", version);
+ setShouldUpdate(true);
+ return true;
+}
+
+bool LegacyInstance::shouldUpdate() const
+{
+ QVariant var = settings()->get("ShouldUpdate");
+ if (!var.isValid() || var.toBool() == false)
+ {
+ return intendedVersionId() != currentVersionId();
+ }
+ return true;
+}
+
+void LegacyInstance::setShouldUpdate(bool val)
+{
+ settings()->set("ShouldUpdate", val);
+}
+
+QString LegacyInstance::defaultBaseJar() const
+{
+ return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar";
+}
+
+QString LegacyInstance::defaultCustomBaseJar() const
+{
+ return FS::PathCombine(binDir(), "mcbackup.jar");
+}
+
+QString LegacyInstance::lwjglFolder() const
+{
+ return m_lwjglFolderSetting->get().toString();
+}
+
+QString LegacyInstance::typeName() const
+{
+ return tr("Legacy");
+}
diff --git a/api/logic/minecraft/legacy/LegacyInstance.h b/api/logic/minecraft/legacy/LegacyInstance.h
new file mode 100644
index 00000000..3bef240d
--- /dev/null
+++ b/api/logic/minecraft/legacy/LegacyInstance.h
@@ -0,0 +1,142 @@
+/* Copyright 2013-2015 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 "minecraft/MinecraftInstance.h"
+
+#include "multimc_logic_export.h"
+
+class ModList;
+class Task;
+
+class MULTIMC_LOGIC_EXPORT LegacyInstance : public MinecraftInstance
+{
+ Q_OBJECT
+public:
+
+ explicit LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+
+ virtual void init() override {};
+
+ /// Path to the instance's minecraft.jar
+ QString runnableJar() const;
+
+ //! Path to the instance's modlist file.
+ QString modListFile() const;
+
+ /*
+ ////// Edit Instance Dialog stuff //////
+ virtual QList<BasePage *> getPages();
+ virtual QString dialogTitle();
+ */
+
+ ////// Mod Lists //////
+ std::shared_ptr<ModList> jarModList() const ;
+ virtual QList< Mod > getJarMods() const override;
+ std::shared_ptr<ModList> coreModList() const;
+ std::shared_ptr<ModList> loaderModList() const;
+ std::shared_ptr<ModList> texturePackList() const override;
+ std::shared_ptr<WorldList> worldList() const override;
+
+ ////// Directories //////
+ QString libDir() const;
+ QString savesDir() const;
+ QString texturePacksDir() const;
+ QString jarModsDir() const;
+ QString binDir() const;
+ QString loaderModsDir() const;
+ QString coreModsDir() const;
+ QString resourceDir() const;
+ virtual QString instanceConfigFolder() const override;
+
+ /// Get the curent base jar of this instance. By default, it's the
+ /// versions/$version/$version.jar
+ QString baseJar() const;
+
+ /// the default base jar of this instance
+ QString defaultBaseJar() const;
+ /// the default custom base jar of this instance
+ QString defaultCustomBaseJar() const;
+
+ /*!
+ * Whether or not custom base jar is used
+ */
+ bool shouldUseCustomBaseJar() const;
+ void setShouldUseCustomBaseJar(bool val);
+
+ /*!
+ * The value of the custom base jar
+ */
+ QString customBaseJar() const;
+ void setCustomBaseJar(QString val);
+
+ /*!
+ * Whether or not the instance's minecraft.jar needs to be rebuilt.
+ * If this is true, when the instance launches, its jar mods will be
+ * re-added to a fresh minecraft.jar file.
+ */
+ bool shouldRebuild() const;
+ void setShouldRebuild(bool val);
+
+ virtual QString currentVersionId() const override;
+
+ //! The version of LWJGL that this instance uses.
+ QString lwjglVersion() const;
+
+ //! Where the lwjgl versions foor this instance can be found... HACK HACK HACK
+ QString lwjglFolder() const;
+
+ /// st the version of LWJGL libs this instance will use
+ void setLWJGLVersion(QString val);
+
+ virtual QString intendedVersionId() const override;
+ virtual bool setIntendedVersionId(QString version) override;
+
+ virtual QSet<QString> traits() override
+ {
+ return {"legacy-instance", "texturepacks"};
+ };
+
+ virtual bool shouldUpdate() const override;
+ virtual void setShouldUpdate(bool val) override;
+ virtual std::shared_ptr<Task> createUpdateTask() override;
+
+ virtual std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override;
+
+ virtual std::shared_ptr<Task> createJarModdingTask() override;
+
+ virtual QString createLaunchScript(AuthSessionPtr session) override;
+
+ virtual void cleanupAfterRun() override;
+
+ virtual QString typeName() const override;
+
+ bool canExport() const override
+ {
+ return true;
+ }
+
+protected:
+ mutable std::shared_ptr<ModList> jar_mod_list;
+ mutable std::shared_ptr<ModList> core_mod_list;
+ mutable std::shared_ptr<ModList> loader_mod_list;
+ mutable std::shared_ptr<ModList> texture_pack_list;
+ mutable std::shared_ptr<WorldList> m_world_list;
+ std::shared_ptr<Setting> m_lwjglFolderSetting;
+protected
+slots:
+ virtual void jarModsChanged();
+};
diff --git a/api/logic/minecraft/legacy/LegacyUpdate.cpp b/api/logic/minecraft/legacy/LegacyUpdate.cpp
new file mode 100644
index 00000000..2d7e8dd2
--- /dev/null
+++ b/api/logic/minecraft/legacy/LegacyUpdate.cpp
@@ -0,0 +1,393 @@
+/* Copyright 2013-2015 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 <QStringList>
+#include <quazip.h>
+#include <quazipfile.h>
+#include <QDebug>
+
+#include "Env.h"
+#include "BaseInstance.h"
+#include "net/URLConstants.h"
+#include "MMCZip.h"
+
+#include "LegacyUpdate.h"
+
+#include "LwjglVersionList.h"
+#include "minecraft/MinecraftVersionList.h"
+#include "minecraft/ModList.h"
+#include "LegacyInstance.h"
+#include <FileSystem.h>
+
+LegacyUpdate::LegacyUpdate(BaseInstance *inst, QObject *parent) : Task(parent), m_inst(inst)
+{
+}
+
+void LegacyUpdate::executeTask()
+{
+ fmllibsStart();
+}
+
+void LegacyUpdate::fmllibsStart()
+{
+ // Get the mod list
+ LegacyInstance *inst = (LegacyInstance *)m_inst;
+ auto modList = inst->jarModList();
+
+ bool forge_present = false;
+
+ QString version = inst->intendedVersionId();
+ auto & fmlLibsMapping = g_VersionFilterData.fmlLibsMapping;
+ if (!fmlLibsMapping.contains(version))
+ {
+ lwjglStart();
+ return;
+ }
+
+ auto &libList = fmlLibsMapping[version];
+
+ // determine if we need some libs for FML or forge
+ setStatus(tr("Checking for FML libraries..."));
+ for (unsigned i = 0; i < modList->size(); i++)
+ {
+ auto &mod = modList->operator[](i);
+
+ // do not use disabled mods.
+ if (!mod.enabled())
+ continue;
+
+ if (mod.type() != Mod::MOD_ZIPFILE)
+ continue;
+
+ if (mod.mmc_id().contains("forge", Qt::CaseInsensitive))
+ {
+ forge_present = true;
+ break;
+ }
+ if (mod.mmc_id().contains("fml", Qt::CaseInsensitive))
+ {
+ forge_present = true;
+ break;
+ }
+ }
+ // we don't...
+ if (!forge_present)
+ {
+ lwjglStart();
+ return;
+ }
+
+ // now check the lib folder inside the instance for files.
+ for (auto &lib : libList)
+ {
+ QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename));
+ if (libInfo.exists())
+ continue;
+ fmlLibsToProcess.append(lib);
+ }
+
+ // if everything is in place, there's nothing to do here...
+ if (fmlLibsToProcess.isEmpty())
+ {
+ lwjglStart();
+ return;
+ }
+
+ // download missing libs to our place
+ setStatus(tr("Dowloading FML libraries..."));
+ auto dljob = new NetJob("FML libraries");
+ auto metacache = ENV.metacache();
+ for (auto &lib : fmlLibsToProcess)
+ {
+ auto entry = metacache->resolveEntry("fmllibs", lib.filename);
+ QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename
+ : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename;
+ dljob->addNetAction(CacheDownload::make(QUrl(urlString), entry));
+ }
+
+ connect(dljob, &NetJob::succeeded, this, &LegacyUpdate::fmllibsFinished);
+ connect(dljob, &NetJob::failed, this, &LegacyUpdate::fmllibsFailed);
+ connect(dljob, &NetJob::progress, this, &LegacyUpdate::progress);
+ legacyDownloadJob.reset(dljob);
+ legacyDownloadJob->start();
+}
+
+void LegacyUpdate::fmllibsFinished()
+{
+ legacyDownloadJob.reset();
+ if(!fmlLibsToProcess.isEmpty())
+ {
+ setStatus(tr("Copying FML libraries into the instance..."));
+ LegacyInstance *inst = (LegacyInstance *)m_inst;
+ auto metacache = ENV.metacache();
+ int index = 0;
+ for (auto &lib : fmlLibsToProcess)
+ {
+ progress(index, fmlLibsToProcess.size());
+ auto entry = metacache->resolveEntry("fmllibs", lib.filename);
+ auto path = FS::PathCombine(inst->libDir(), lib.filename);
+ if(!FS::ensureFilePathExists(path))
+ {
+ emitFailed(tr("Failed creating FML library folder inside the instance."));
+ return;
+ }
+ if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename)))
+ {
+ emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename));
+ return;
+ }
+ index++;
+ }
+ progress(index, fmlLibsToProcess.size());
+ }
+ lwjglStart();
+}
+
+void LegacyUpdate::fmllibsFailed(QString reason)
+{
+ emitFailed(tr("Game update failed: it was impossible to fetch the required FML libraries. Reason: %1").arg(reason));
+ return;
+}
+
+void LegacyUpdate::lwjglStart()
+{
+ LegacyInstance *inst = (LegacyInstance *)m_inst;
+
+ lwjglVersion = inst->lwjglVersion();
+ lwjglTargetPath = FS::PathCombine(inst->lwjglFolder(), lwjglVersion);
+ lwjglNativesPath = FS::PathCombine(lwjglTargetPath, "natives");
+
+ // if the 'done' file exists, we don't have to download this again
+ QFileInfo doneFile(FS::PathCombine(lwjglTargetPath, "done"));
+ if (doneFile.exists())
+ {
+ jarStart();
+ return;
+ }
+
+ auto list = std::dynamic_pointer_cast<LWJGLVersionList>(ENV.getVersionList("org.lwjgl.legacy"));
+ if (!list->isLoaded())
+ {
+ emitFailed("Too soon! Let the LWJGL list load :)");
+ return;
+ }
+
+ setStatus(tr("Downloading new LWJGL..."));
+ auto version = std::dynamic_pointer_cast<LWJGLVersion>(list->findVersion(lwjglVersion));
+ if (!version)
+ {
+ emitFailed("Game update failed: the selected LWJGL version is invalid.");
+ return;
+ }
+
+ QString url = version->url();
+ QUrl realUrl(url);
+ QString hostname = realUrl.host();
+ auto worker = ENV.qnam();
+ QNetworkRequest req(realUrl);
+ req.setRawHeader("Host", hostname.toLatin1());
+ req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)");
+ QNetworkReply *rep = worker->get(req);
+
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+ connect(rep, &QNetworkReply::downloadProgress, this, &LegacyUpdate::progress);
+ connect(worker.get(), &QNetworkAccessManager::finished, this, &LegacyUpdate::lwjglFinished);
+}
+
+void LegacyUpdate::lwjglFinished(QNetworkReply *reply)
+{
+ if (m_reply.get() != reply)
+ {
+ return;
+ }
+ if (reply->error() != QNetworkReply::NoError)
+ {
+ emitFailed("Failed to download: " + reply->errorString() +
+ "\nSometimes you have to wait a bit if you download many LWJGL versions in "
+ "a row. YMMV");
+ return;
+ }
+ auto worker = ENV.qnam();
+ // Here i check if there is a cookie for me in the reply and extract it
+ QList<QNetworkCookie> cookies =
+ qvariant_cast<QList<QNetworkCookie>>(reply->header(QNetworkRequest::SetCookieHeader));
+ if (cookies.count() != 0)
+ {
+ // you must tell which cookie goes with which url
+ worker->cookieJar()->setCookiesFromUrl(cookies, QUrl("sourceforge.net"));
+ }
+
+ // here you can check for the 302 or whatever other header i need
+ QVariant newLoc = reply->header(QNetworkRequest::LocationHeader);
+ if (newLoc.isValid())
+ {
+ QString redirectedTo = reply->header(QNetworkRequest::LocationHeader).toString();
+ QUrl realUrl(redirectedTo);
+ QString hostname = realUrl.host();
+ QNetworkRequest req(redirectedTo);
+ req.setRawHeader("Host", hostname.toLatin1());
+ req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)");
+ QNetworkReply *rep = worker->get(req);
+ connect(rep, &QNetworkReply::downloadProgress, this, &LegacyUpdate::progress);
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+ return;
+ }
+ QFile saveMe("lwjgl.zip");
+ saveMe.open(QIODevice::WriteOnly);
+ saveMe.write(m_reply->readAll());
+ saveMe.close();
+ setStatus(tr("Installing new LWJGL..."));
+ extractLwjgl();
+ jarStart();
+}
+void LegacyUpdate::extractLwjgl()
+{
+ // make sure the directories are there
+
+ bool success = FS::ensureFolderPathExists(lwjglNativesPath);
+
+ if (!success)
+ {
+ emitFailed("Failed to extract the lwjgl libs - error when creating required folders.");
+ return;
+ }
+
+ QuaZip zip("lwjgl.zip");
+ if (!zip.open(QuaZip::mdUnzip))
+ {
+ emitFailed("Failed to extract the lwjgl libs - not a valid archive.");
+ return;
+ }
+
+ // and now we are going to access files inside it
+ QuaZipFile file(&zip);
+ const QString jarNames[] = {"jinput.jar", "lwjgl_util.jar", "lwjgl.jar"};
+ for (bool more = zip.goToFirstFile(); more; more = zip.goToNextFile())
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ emitFailed("Failed to extract the lwjgl libs - error while reading archive.");
+ return;
+ }
+ QuaZipFileInfo info;
+ QString name = file.getActualFileName();
+ if (name.endsWith('/'))
+ {
+ file.close();
+ continue;
+ }
+ QString destFileName;
+ // Look for the jars
+ for (int i = 0; i < 3; i++)
+ {
+ if (name.endsWith(jarNames[i]))
+ {
+ destFileName = FS::PathCombine(lwjglTargetPath, jarNames[i]);
+ }
+ }
+ // Not found? look for the natives
+ if (destFileName.isEmpty())
+ {
+#ifdef Q_OS_WIN32
+ QString nativesDir = "windows";
+#else
+#ifdef Q_OS_MAC
+ QString nativesDir = "macosx";
+#else
+ QString nativesDir = "linux";
+#endif
+#endif
+ if (name.contains(nativesDir))
+ {
+ int lastSlash = name.lastIndexOf('/');
+ int lastBackSlash = name.lastIndexOf('\\');
+ if (lastSlash != -1)
+ name = name.mid(lastSlash + 1);
+ else if (lastBackSlash != -1)
+ name = name.mid(lastBackSlash + 1);
+ destFileName = FS::PathCombine(lwjglNativesPath, name);
+ }
+ }
+ // Now if destFileName is still empty, go to the next file.
+ if (!destFileName.isEmpty())
+ {
+ setStatus(tr("Installing new LWJGL - extracting ") + name + "...");
+ QFile output(destFileName);
+ output.open(QIODevice::WriteOnly);
+ output.write(file.readAll());
+ output.close();
+ }
+ file.close(); // do not forget to close!
+ }
+ zip.close();
+ m_reply.reset();
+ QFile doneFile(FS::PathCombine(lwjglTargetPath, "done"));
+ doneFile.open(QIODevice::WriteOnly);
+ doneFile.write("done.");
+ doneFile.close();
+}
+
+void LegacyUpdate::lwjglFailed(QString reason)
+{
+ emitFailed(tr("Bad stuff happened while trying to get the lwjgl libs: %1").arg(reason));
+}
+
+void LegacyUpdate::jarStart()
+{
+ LegacyInstance *inst = (LegacyInstance *)m_inst;
+ if (!inst->shouldUpdate() || inst->shouldUseCustomBaseJar())
+ {
+ emitSucceeded();
+ return;
+ }
+
+ setStatus(tr("Checking for jar updates..."));
+ // Make directories
+ QDir binDir(inst->binDir());
+ if (!binDir.exists() && !binDir.mkpath("."))
+ {
+ emitFailed("Failed to create bin folder.");
+ return;
+ }
+
+ // Build a list of URLs that will need to be downloaded.
+ setStatus(tr("Downloading new minecraft.jar ..."));
+
+ QString version_id = inst->intendedVersionId();
+
+ auto dljob = new NetJob("Minecraft.jar for version " + version_id);
+
+ auto metacache = ENV.metacache();
+ auto entry = metacache->resolveEntry("versions", URLConstants::getJarPath(version_id));
+ dljob->addNetAction(CacheDownload::make(QUrl(URLConstants::getLegacyJarUrl(version_id)), entry));
+ connect(dljob, SIGNAL(succeeded()), SLOT(jarFinished()));
+ connect(dljob, SIGNAL(failed(QString)), SLOT(jarFailed(QString)));
+ connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ legacyDownloadJob.reset(dljob);
+ legacyDownloadJob->start();
+}
+
+void LegacyUpdate::jarFinished()
+{
+ // process the jar
+ emitSucceeded();
+}
+
+void LegacyUpdate::jarFailed(QString reason)
+{
+ // bad, bad
+ emitFailed(tr("Failed to download the minecraft jar: %1.").arg(reason));
+}
diff --git a/api/logic/minecraft/legacy/LegacyUpdate.h b/api/logic/minecraft/legacy/LegacyUpdate.h
new file mode 100644
index 00000000..c52bf934
--- /dev/null
+++ b/api/logic/minecraft/legacy/LegacyUpdate.h
@@ -0,0 +1,70 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QList>
+#include <QUrl>
+
+#include "net/NetJob.h"
+#include "tasks/Task.h"
+#include "minecraft/VersionFilterData.h"
+
+class MinecraftVersion;
+class BaseInstance;
+class QuaZip;
+class Mod;
+
+class LegacyUpdate : public Task
+{
+ Q_OBJECT
+public:
+ explicit LegacyUpdate(BaseInstance *inst, QObject *parent = 0);
+ virtual void executeTask();
+
+private
+slots:
+ void lwjglStart();
+ void lwjglFinished(QNetworkReply *);
+ void lwjglFailed(QString reason);
+
+ void jarStart();
+ void jarFinished();
+ void jarFailed(QString reason);
+
+ void fmllibsStart();
+ void fmllibsFinished();
+ void fmllibsFailed(QString reason);
+
+ void extractLwjgl();
+
+private:
+
+ std::shared_ptr<QNetworkReply> m_reply;
+
+ // target version, determined during this task
+ // MinecraftVersion *targetVersion;
+ QString lwjglURL;
+ QString lwjglVersion;
+
+ QString lwjglTargetPath;
+ QString lwjglNativesPath;
+
+private:
+ NetJobPtr legacyDownloadJob;
+ BaseInstance *m_inst = nullptr;
+ QList<FMLlib> fmlLibsToProcess;
+};
diff --git a/api/logic/minecraft/legacy/LwjglVersionList.cpp b/api/logic/minecraft/legacy/LwjglVersionList.cpp
new file mode 100644
index 00000000..bb017368
--- /dev/null
+++ b/api/logic/minecraft/legacy/LwjglVersionList.cpp
@@ -0,0 +1,189 @@
+/* Copyright 2013-2015 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 "LwjglVersionList.h"
+#include "Env.h"
+
+#include <QtNetwork>
+#include <QtXml>
+#include <QRegExp>
+
+#include <QDebug>
+
+#define RSS_URL "http://sourceforge.net/projects/java-game-lib/rss"
+
+LWJGLVersionList::LWJGLVersionList(QObject *parent) : BaseVersionList(parent)
+{
+ setLoading(false);
+}
+
+QVariant LWJGLVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ const PtrLWJGLVersion version = m_vlist.at(index.row());
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return version->name();
+
+ case Qt::ToolTipRole:
+ return version->url();
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant LWJGLVersionList::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return tr("Version");
+
+ case Qt::ToolTipRole:
+ return tr("LWJGL version name.");
+
+ default:
+ return QVariant();
+ }
+}
+
+int LWJGLVersionList::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+bool LWJGLVersionList::isLoading() const
+{
+ return m_loading;
+}
+
+void LWJGLVersionList::loadList()
+{
+ Q_ASSERT_X(!m_loading, "loadList", "list is already loading (m_loading is true)");
+
+ setLoading(true);
+ auto worker = ENV.qnam();
+ QNetworkRequest req(QUrl(RSS_URL));
+ req.setRawHeader("Accept", "application/rss+xml, text/xml, */*");
+ req.setRawHeader("User-Agent", "MultiMC/5.0 (Uncached)");
+ reply = worker->get(req);
+ connect(reply, SIGNAL(finished()), SLOT(netRequestComplete()));
+}
+
+inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname)
+{
+ QDomNodeList elementList = parent.elementsByTagName(tagname);
+ if (elementList.count())
+ return elementList.at(0).toElement();
+ else
+ return QDomElement();
+}
+
+void LWJGLVersionList::netRequestComplete()
+{
+ if (reply->error() == QNetworkReply::NoError)
+ {
+ QRegExp lwjglRegex("lwjgl-(([0-9]\\.?)+)\\.zip");
+ Q_ASSERT_X(lwjglRegex.isValid(), "load LWJGL list", "LWJGL regex is invalid");
+
+ QDomDocument doc;
+
+ QString xmlErrorMsg;
+ int errorLine;
+ auto rawData = reply->readAll();
+ if (!doc.setContent(rawData, false, &xmlErrorMsg, &errorLine))
+ {
+ failed("Failed to load LWJGL list. XML error: " + xmlErrorMsg + " at line " +
+ QString::number(errorLine));
+ setLoading(false);
+ return;
+ }
+
+ QDomNodeList items = doc.elementsByTagName("item");
+
+ QList<PtrLWJGLVersion> tempList;
+
+ for (int i = 0; i < items.length(); i++)
+ {
+ Q_ASSERT_X(items.at(i).isElement(), "load LWJGL list",
+ "XML element isn't an element... wat?");
+
+ QDomElement linkElement = getDomElementByTagName(items.at(i).toElement(), "link");
+ if (linkElement.isNull())
+ {
+ qDebug() << "Link element" << i << "in RSS feed doesn't exist! Skipping.";
+ continue;
+ }
+
+ QString link = linkElement.text();
+
+ // Make sure it's a download link.
+ if (link.endsWith("/download") && link.contains(lwjglRegex))
+ {
+ QString name = link.mid(lwjglRegex.indexIn(link) + 6);
+ // Subtract 4 here to remove the .zip file extension.
+ name = name.left(lwjglRegex.matchedLength() - 10);
+
+ QUrl url(link);
+ if (!url.isValid())
+ {
+ qWarning() << "LWJGL version URL isn't valid:" << link << "Skipping.";
+ continue;
+ }
+ qDebug() << "Discovered LWGL version" << name << "at" << link;
+ tempList.append(std::make_shared<LWJGLVersion>(name, link));
+ }
+ }
+
+ beginResetModel();
+ m_vlist.swap(tempList);
+ endResetModel();
+
+ qDebug() << "Loaded LWJGL list.";
+ finished();
+ }
+ else
+ {
+ failed("Failed to load LWJGL list. Network error: " + reply->errorString());
+ }
+
+ setLoading(false);
+ reply->deleteLater();
+}
+
+void LWJGLVersionList::failed(QString msg)
+{
+ qCritical() << msg;
+ emit loadListFailed(msg);
+}
+
+void LWJGLVersionList::finished()
+{
+ emit loadListFinished();
+}
+
+void LWJGLVersionList::setLoading(bool loading)
+{
+ m_loading = loading;
+ emit loadingStateUpdated(m_loading);
+}
diff --git a/api/logic/minecraft/legacy/LwjglVersionList.h b/api/logic/minecraft/legacy/LwjglVersionList.h
new file mode 100644
index 00000000..f043f6e2
--- /dev/null
+++ b/api/logic/minecraft/legacy/LwjglVersionList.h
@@ -0,0 +1,156 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QAbstractListModel>
+#include <QUrl>
+#include <QNetworkReply>
+#include <memory>
+
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+
+#include "multimc_logic_export.h"
+
+class LWJGLVersion;
+typedef std::shared_ptr<LWJGLVersion> PtrLWJGLVersion;
+
+class MULTIMC_LOGIC_EXPORT LWJGLVersion : public BaseVersion
+{
+public:
+ LWJGLVersion(const QString &name, const QString &url)
+ : m_name(name), m_url(url)
+ {
+ }
+
+ virtual QString descriptor()
+ {
+ return m_name;
+ }
+
+ virtual QString name()
+ {
+ return m_name;
+ }
+
+ virtual QString typeString() const
+ {
+ return QObject::tr("Upstream");
+ }
+
+ QString url() const
+ {
+ return m_url;
+ }
+
+protected:
+ QString m_name;
+ QString m_url;
+};
+
+class MULTIMC_LOGIC_EXPORT LWJGLVersionList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ explicit LWJGLVersionList(QObject *parent = 0);
+
+ bool isLoaded() override
+ {
+ return m_vlist.length() > 0;
+ }
+ virtual const BaseVersionPtr at(int i) const override
+ {
+ return m_vlist[i];
+ }
+
+ virtual Task* getLoadTask() override
+ {
+ return nullptr;
+ }
+
+ virtual void sortVersions() override {};
+
+ virtual void updateListData(QList< BaseVersionPtr > versions) override {};
+
+ int count() const override
+ {
+ return m_vlist.length();
+ }
+
+ virtual QVariant data(const QModelIndex &index, int role) const override;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ virtual int rowCount(const QModelIndex &parent) const override
+ {
+ return count();
+ }
+ virtual int columnCount(const QModelIndex &parent) const override;
+
+ virtual bool isLoading() const;
+ virtual bool errored() const
+ {
+ return m_errored;
+ }
+
+ virtual QString lastErrorMsg() const
+ {
+ return m_lastErrorMsg;
+ }
+
+public
+slots:
+ /*!
+ * Loads the version list.
+ * This is done asynchronously. On success, the loadListFinished() signal will
+ * be emitted. The list model will be reset as well, resulting in the modelReset()
+ * signal being emitted. Note that the model will be reset before loadListFinished() is
+ * emitted.
+ * If loading the list failed, the loadListFailed(QString msg),
+ * signal will be emitted.
+ */
+ virtual void loadList();
+
+signals:
+ /*!
+ * Emitted when the list either starts or finishes loading.
+ * \param loading Whether or not the list is loading.
+ */
+ void loadingStateUpdated(bool loading);
+
+ void loadListFinished();
+
+ void loadListFailed(QString msg);
+
+private:
+ QList<PtrLWJGLVersion> m_vlist;
+
+ QNetworkReply *m_netReply;
+ QNetworkReply *reply;
+
+ bool m_loading;
+ bool m_errored;
+ QString m_lastErrorMsg;
+
+ void failed(QString msg);
+
+ void finished();
+
+ void setLoading(bool loading);
+
+private
+slots:
+ virtual void netRequestComplete();
+};
diff --git a/api/logic/minecraft/liteloader/LiteLoaderInstaller.cpp b/api/logic/minecraft/liteloader/LiteLoaderInstaller.cpp
new file mode 100644
index 00000000..25297fa4
--- /dev/null
+++ b/api/logic/minecraft/liteloader/LiteLoaderInstaller.cpp
@@ -0,0 +1,142 @@
+/* Copyright 2013-2015 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 "LiteLoaderInstaller.h"
+
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/Library.h"
+#include "minecraft/onesix/OneSixInstance.h"
+#include <minecraft/onesix/OneSixVersionFormat.h>
+#include "minecraft/liteloader/LiteLoaderVersionList.h"
+#include "Exception.h"
+
+LiteLoaderInstaller::LiteLoaderInstaller() : BaseInstaller()
+{
+}
+
+void LiteLoaderInstaller::prepare(LiteLoaderVersionPtr version)
+{
+ m_version = version;
+}
+bool LiteLoaderInstaller::add(OneSixInstance *to)
+{
+ if (!BaseInstaller::add(to))
+ {
+ return false;
+ }
+
+ QJsonObject obj;
+
+ obj.insert("mainClass", QString("net.minecraft.launchwrapper.Launch"));
+ obj.insert("+tweakers", QJsonArray::fromStringList(QStringList() << m_version->tweakClass));
+ obj.insert("order", 10);
+
+ QJsonArray libraries;
+
+ for (auto Library : m_version->libraries)
+ {
+ libraries.append(OneSixVersionFormat::libraryToJson(Library.get()));
+ }
+
+ // liteloader
+ {
+ Library liteloaderLib("com.mumfrey:liteloader:" + m_version->version);
+ liteloaderLib.setAbsoluteUrl(QString("http://dl.liteloader.com/versions/com/mumfrey/liteloader/%1/%2").arg(m_version->mcVersion, m_version->file));
+ QJsonObject llLibObj = OneSixVersionFormat::libraryToJson(&liteloaderLib);
+ libraries.append(llLibObj);
+ }
+
+ obj.insert("+libraries", libraries);
+ obj.insert("name", QString("LiteLoader"));
+ obj.insert("fileId", id());
+ obj.insert("version", m_version->version);
+ obj.insert("mcVersion", to->intendedVersionId());
+
+ QFile file(filename(to->instanceRoot()));
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(QJsonDocument(obj).toJson());
+ file.close();
+
+ return true;
+}
+
+class LiteLoaderInstallTask : public Task
+{
+ Q_OBJECT
+public:
+ LiteLoaderInstallTask(LiteLoaderInstaller *installer, OneSixInstance *instance,
+ BaseVersionPtr version, QObject *parent)
+ : Task(parent), m_installer(installer), m_instance(instance), m_version(version)
+ {
+ }
+
+protected:
+ void executeTask() override
+ {
+ LiteLoaderVersionPtr liteloaderVersion =
+ std::dynamic_pointer_cast<LiteLoaderVersion>(m_version);
+ if (!liteloaderVersion)
+ {
+ return;
+ }
+ m_installer->prepare(liteloaderVersion);
+ if (!m_installer->add(m_instance))
+ {
+ emitFailed(tr("For reasons unknown, the LiteLoader installation failed. Check your "
+ "MultiMC log files for details."));
+ }
+ else
+ {
+ try
+ {
+ m_instance->reloadProfile();
+ emitSucceeded();
+ }
+ catch (Exception &e)
+ {
+ emitFailed(e.cause());
+ }
+ catch (...)
+ {
+ emitFailed(
+ tr("Failed to load the version description file for reasons unknown."));
+ }
+ }
+ }
+
+private:
+ LiteLoaderInstaller *m_installer;
+ OneSixInstance *m_instance;
+ BaseVersionPtr m_version;
+};
+
+Task *LiteLoaderInstaller::createInstallTask(OneSixInstance *instance,
+ BaseVersionPtr version,
+ QObject *parent)
+{
+ return new LiteLoaderInstallTask(this, instance, version, parent);
+}
+
+#include "LiteLoaderInstaller.moc"
diff --git a/api/logic/minecraft/liteloader/LiteLoaderInstaller.h b/api/logic/minecraft/liteloader/LiteLoaderInstaller.h
new file mode 100644
index 00000000..fe0aee3d
--- /dev/null
+++ b/api/logic/minecraft/liteloader/LiteLoaderInstaller.h
@@ -0,0 +1,39 @@
+/* Copyright 2013-2015 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>
+#include <QMap>
+
+#include "BaseInstaller.h"
+#include "LiteLoaderVersionList.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT LiteLoaderInstaller : public BaseInstaller
+{
+public:
+ LiteLoaderInstaller();
+
+ void prepare(LiteLoaderVersionPtr version);
+ bool add(OneSixInstance *to) override;
+ virtual QString id() const override { return "com.mumfrey.liteloader"; }
+
+ Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override;
+
+private:
+ LiteLoaderVersionPtr m_version;
+};
diff --git a/api/logic/minecraft/liteloader/LiteLoaderVersionList.cpp b/api/logic/minecraft/liteloader/LiteLoaderVersionList.cpp
new file mode 100644
index 00000000..b0c9736a
--- /dev/null
+++ b/api/logic/minecraft/liteloader/LiteLoaderVersionList.cpp
@@ -0,0 +1,276 @@
+/* Copyright 2013-2015 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 "LiteLoaderVersionList.h"
+#include <minecraft/onesix/OneSixVersionFormat.h>
+#include "Env.h"
+#include "net/URLConstants.h"
+#include "Exception.h"
+
+#include <QtXml>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QJsonParseError>
+
+#include <QtAlgorithms>
+
+#include <QtNetwork>
+
+LiteLoaderVersionList::LiteLoaderVersionList(QObject *parent) : BaseVersionList(parent)
+{
+}
+
+Task *LiteLoaderVersionList::getLoadTask()
+{
+ return new LLListLoadTask(this);
+}
+
+bool LiteLoaderVersionList::isLoaded()
+{
+ return m_loaded;
+}
+
+const BaseVersionPtr LiteLoaderVersionList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+int LiteLoaderVersionList::count() const
+{
+ return m_vlist.count();
+}
+
+static bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second)
+{
+ auto left = std::dynamic_pointer_cast<LiteLoaderVersion>(first);
+ auto right = std::dynamic_pointer_cast<LiteLoaderVersion>(second);
+ return left->timestamp > right->timestamp;
+}
+
+void LiteLoaderVersionList::sortVersions()
+{
+ beginResetModel();
+ std::sort(m_vlist.begin(), m_vlist.end(), cmpVersions);
+ endResetModel();
+}
+
+QVariant LiteLoaderVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.row() > count())
+ return QVariant();
+
+ auto version = std::dynamic_pointer_cast<LiteLoaderVersion>(m_vlist[index.row()]);
+ switch (role)
+ {
+ case VersionPointerRole:
+ return qVariantFromValue(m_vlist[index.row()]);
+
+ case VersionRole:
+ return version->name();
+
+ case VersionIdRole:
+ return version->descriptor();
+
+ case ParentGameVersionRole:
+ return version->mcVersion;
+
+ case RecommendedRole:
+ return version->isLatest;
+
+ default:
+ return QVariant();
+ }
+}
+
+QList<BaseVersionList::ModelRoles> LiteLoaderVersionList::providesRoles()
+{
+ return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole};
+}
+
+BaseVersionPtr LiteLoaderVersionList::getLatestStable() const
+{
+ for (int i = 0; i < m_vlist.length(); i++)
+ {
+ auto ver = std::dynamic_pointer_cast<LiteLoaderVersion>(m_vlist.at(i));
+ if (ver->isLatest)
+ {
+ return m_vlist.at(i);
+ }
+ }
+ return BaseVersionPtr();
+}
+
+void LiteLoaderVersionList::updateListData(QList<BaseVersionPtr> versions)
+{
+ beginResetModel();
+ m_vlist = versions;
+ m_loaded = true;
+ std::sort(m_vlist.begin(), m_vlist.end(), cmpVersions);
+ endResetModel();
+}
+
+LLListLoadTask::LLListLoadTask(LiteLoaderVersionList *vlist)
+{
+ m_list = vlist;
+}
+
+LLListLoadTask::~LLListLoadTask()
+{
+}
+
+void LLListLoadTask::executeTask()
+{
+ setStatus(tr("Loading LiteLoader version list..."));
+ auto job = new NetJob("Version index");
+ // we do not care if the version is stale or not.
+ auto liteloaderEntry = ENV.metacache()->resolveEntry("liteloader", "versions.json");
+
+ // verify by poking the server.
+ liteloaderEntry->setStale(true);
+
+ job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::LITELOADER_URL),
+ liteloaderEntry));
+
+ connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed()));
+
+ listJob.reset(job);
+ connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded()));
+ connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ listJob->start();
+}
+
+void LLListLoadTask::listFailed()
+{
+ emitFailed("Failed to load LiteLoader version list.");
+ return;
+}
+
+void LLListLoadTask::listDownloaded()
+{
+ QByteArray data;
+ {
+ auto dlJob = listDownload;
+ auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
+ QFile listFile(filename);
+ if (!listFile.open(QIODevice::ReadOnly))
+ {
+ emitFailed("Failed to open the LiteLoader version list.");
+ return;
+ }
+ data = listFile.readAll();
+ listFile.close();
+ dlJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed("Error parsing version list JSON:" + jsonError.errorString());
+ return;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed("Error parsing version list JSON: jsonDoc is not an object");
+ return;
+ }
+
+ const QJsonObject root = jsonDoc.object();
+
+ // Now, get the array of versions.
+ if (!root.value("versions").isObject())
+ {
+ emitFailed("Error parsing version list JSON: missing 'versions' object");
+ return;
+ }
+
+ auto meta = root.value("meta").toObject();
+ QString description = meta.value("description").toString(tr("This is a lightweight loader for mods that don't change game mechanics."));
+ QString defaultUrl = meta.value("url").toString("http://dl.liteloader.com");
+ QString authors = meta.value("authors").toString("Mumfrey");
+ auto versions = root.value("versions").toObject();
+
+ QList<BaseVersionPtr> tempList;
+ for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt)
+ {
+ const QString mcVersion = vIt.key();
+ QString latest;
+ const QJsonObject artefacts = vIt.value()
+ .toObject()
+ .value("artefacts")
+ .toObject()
+ .value("com.mumfrey:liteloader")
+ .toObject();
+ QList<BaseVersionPtr> perMcVersionList;
+ for (auto aIt = artefacts.begin(); aIt != artefacts.end(); ++aIt)
+ {
+ const QString identifier = aIt.key();
+ const QJsonObject artefact = aIt.value().toObject();
+ if (identifier == "latest")
+ {
+ latest = artefact.value("version").toString();
+ continue;
+ }
+ LiteLoaderVersionPtr version(new LiteLoaderVersion());
+ version->version = artefact.value("version").toString();
+ version->file = artefact.value("file").toString();
+ version->mcVersion = mcVersion;
+ version->md5 = artefact.value("md5").toString();
+ version->timestamp = artefact.value("timestamp").toString().toInt();
+ version->tweakClass = artefact.value("tweakClass").toString();
+ version->authors = authors;
+ version->description = description;
+ version->defaultUrl = defaultUrl;
+ const QJsonArray libs = artefact.value("libraries").toArray();
+ for (auto lIt = libs.begin(); lIt != libs.end(); ++lIt)
+ {
+ auto libobject = (*lIt).toObject();
+ try
+ {
+ auto lib = OneSixVersionFormat::libraryFromJson(libobject, "versions.json");
+ // hack to make liteloader 1.7.10_00 work
+ if(lib->rawName() == GradleSpecifier("org.ow2.asm:asm-all:5.0.3"))
+ {
+ lib->setRepositoryURL("http://repo.maven.apache.org/maven2/");
+ }
+ version->libraries.append(lib);
+ }
+ catch (Exception &e)
+ {
+ qCritical() << "Couldn't read JSON object:";
+ continue;
+ }
+ }
+ perMcVersionList.append(version);
+ }
+ for (auto version : perMcVersionList)
+ {
+ auto v = std::dynamic_pointer_cast<LiteLoaderVersion>(version);
+ v->isLatest = v->version == latest;
+ }
+ tempList.append(perMcVersionList);
+ }
+ m_list->updateListData(tempList);
+
+ emitSucceeded();
+}
diff --git a/api/logic/minecraft/liteloader/LiteLoaderVersionList.h b/api/logic/minecraft/liteloader/LiteLoaderVersionList.h
new file mode 100644
index 00000000..1dba4b6a
--- /dev/null
+++ b/api/logic/minecraft/liteloader/LiteLoaderVersionList.h
@@ -0,0 +1,119 @@
+/* Copyright 2013-2015 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 <QObject>
+
+#include <QString>
+#include <QStringList>
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include <minecraft/Library.h>
+
+#include "multimc_logic_export.h"
+
+class LLListLoadTask;
+class QNetworkReply;
+
+class LiteLoaderVersion : public BaseVersion
+{
+public:
+ QString descriptor() override
+ {
+ if (isLatest)
+ {
+ return QObject::tr("Latest");
+ }
+ return QString();
+ }
+ QString typeString() const override
+ {
+ return mcVersion;
+ }
+ QString name() override
+ {
+ return version;
+ }
+
+ // important info
+ QString version;
+ QString file;
+ QString mcVersion;
+ QString md5;
+ int timestamp;
+ bool isLatest;
+ QString tweakClass;
+ QList<LibraryPtr> libraries;
+
+ // meta
+ QString defaultUrl;
+ QString description;
+ QString authors;
+};
+typedef std::shared_ptr<LiteLoaderVersion> LiteLoaderVersionPtr;
+
+class MULTIMC_LOGIC_EXPORT LiteLoaderVersionList : public BaseVersionList
+{
+ Q_OBJECT
+public:
+ friend class LLListLoadTask;
+
+ explicit LiteLoaderVersionList(QObject *parent = 0);
+
+ virtual Task *getLoadTask();
+ virtual bool isLoaded();
+ virtual const BaseVersionPtr at(int i) const;
+ virtual int count() const;
+ virtual void sortVersions();
+ virtual QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const;
+ virtual QList< ModelRoles > providesRoles();
+
+ virtual BaseVersionPtr getLatestStable() const;
+
+protected:
+ QList<BaseVersionPtr> m_vlist;
+
+ bool m_loaded = false;
+
+protected
+slots:
+ virtual void updateListData(QList<BaseVersionPtr> versions);
+};
+
+class LLListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit LLListLoadTask(LiteLoaderVersionList *vlist);
+ ~LLListLoadTask();
+
+ virtual void executeTask();
+
+protected
+slots:
+ void listDownloaded();
+ void listFailed();
+
+protected:
+ NetJobPtr listJob;
+ CacheDownloadPtr listDownload;
+ LiteLoaderVersionList *m_list;
+};
+
+Q_DECLARE_METATYPE(LiteLoaderVersionPtr)
diff --git a/api/logic/minecraft/onesix/OneSixInstance.cpp b/api/logic/minecraft/onesix/OneSixInstance.cpp
new file mode 100644
index 00000000..258e26c5
--- /dev/null
+++ b/api/logic/minecraft/onesix/OneSixInstance.cpp
@@ -0,0 +1,597 @@
+/* Copyright 2013-2015 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 <QDebug>
+#include <Env.h>
+
+#include "OneSixInstance.h"
+#include "OneSixUpdate.h"
+#include "OneSixProfileStrategy.h"
+
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/VersionBuildError.h"
+#include "launch/LaunchTask.h"
+#include "launch/steps/PreLaunchCommand.h"
+#include "launch/steps/Update.h"
+#include "launch/steps/LaunchMinecraft.h"
+#include "launch/steps/PostLaunchCommand.h"
+#include "launch/steps/TextPrint.h"
+#include "launch/steps/ModMinecraftJar.h"
+#include "launch/steps/CheckJava.h"
+#include "MMCZip.h"
+
+#include "minecraft/AssetsUtils.h"
+#include "minecraft/WorldList.h"
+#include <FileSystem.h>
+
+OneSixInstance::OneSixInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
+ : MinecraftInstance(globalSettings, settings, rootDir)
+{
+ m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, "");
+}
+
+void OneSixInstance::init()
+{
+ createProfile();
+}
+
+void OneSixInstance::createProfile()
+{
+ m_profile.reset(new MinecraftProfile(new OneSixProfileStrategy(this)));
+}
+
+QSet<QString> OneSixInstance::traits()
+{
+ auto version = getMinecraftProfile();
+ if (!version)
+ {
+ return {"version-incomplete"};
+ }
+ else
+ {
+ return version->getTraits();
+ }
+}
+
+std::shared_ptr<Task> OneSixInstance::createUpdateTask()
+{
+ return std::shared_ptr<Task>(new OneSixUpdate(this));
+}
+
+QString replaceTokensIn(QString text, QMap<QString, QString> with)
+{
+ QString result;
+ QRegExp token_regexp("\\$\\{(.+)\\}");
+ token_regexp.setMinimal(true);
+ QStringList list;
+ int tail = 0;
+ int head = 0;
+ while ((head = token_regexp.indexIn(text, head)) != -1)
+ {
+ result.append(text.mid(tail, head - tail));
+ QString key = token_regexp.cap(1);
+ auto iter = with.find(key);
+ if (iter != with.end())
+ {
+ result.append(*iter);
+ }
+ head += token_regexp.matchedLength();
+ tail = head;
+ }
+ result.append(text.mid(tail));
+ return result;
+}
+
+QStringList OneSixInstance::processMinecraftArgs(AuthSessionPtr session)
+{
+ QString args_pattern = m_profile->getMinecraftArguments();
+ for (auto tweaker : m_profile->getTweakers())
+ {
+ args_pattern += " --tweakClass " + tweaker;
+ }
+
+ QMap<QString, QString> token_mapping;
+ // yggdrasil!
+ token_mapping["auth_username"] = session->username;
+ token_mapping["auth_session"] = session->session;
+ token_mapping["auth_access_token"] = session->access_token;
+ token_mapping["auth_player_name"] = session->player_name;
+ token_mapping["auth_uuid"] = session->uuid;
+
+ // blatant self-promotion.
+ token_mapping["profile_name"] = token_mapping["version_name"] = "MultiMC5";
+ if(m_profile->isVanilla())
+ {
+ token_mapping["version_type"] = m_profile->getMinecraftVersionType();
+ }
+ else
+ {
+ token_mapping["version_type"] = "custom";
+ }
+
+ QString absRootDir = QDir(minecraftRoot()).absolutePath();
+ token_mapping["game_directory"] = absRootDir;
+ QString absAssetsDir = QDir("assets/").absolutePath();
+ auto assets = m_profile->getMinecraftAssets();
+ token_mapping["game_assets"] = AssetsUtils::reconstructAssets(assets->id).absolutePath();
+
+ token_mapping["user_properties"] = session->serializeUserProperties();
+ token_mapping["user_type"] = session->user_type;
+
+ // 1.7.3+ assets tokens
+ token_mapping["assets_root"] = absAssetsDir;
+ token_mapping["assets_index_name"] = assets->id;
+
+ QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts);
+ for (int i = 0; i < parts.length(); i++)
+ {
+ parts[i] = replaceTokensIn(parts[i], token_mapping);
+ }
+ return parts;
+}
+
+QString OneSixInstance::createLaunchScript(AuthSessionPtr session)
+{
+ QString launchScript;
+
+ if (!m_profile)
+ return nullptr;
+
+ for(auto & mod: loaderModList()->allMods())
+ {
+ if(!mod.enabled())
+ continue;
+ if(mod.type() == Mod::MOD_FOLDER)
+ continue;
+ // TODO: proper implementation would need to descend into folders.
+
+ launchScript += "mod " + mod.filename().completeBaseName() + "\n";;
+ }
+
+ for(auto & coremod: coreModList()->allMods())
+ {
+ if(!coremod.enabled())
+ continue;
+ if(coremod.type() == Mod::MOD_FOLDER)
+ continue;
+ // TODO: proper implementation would need to descend into folders.
+
+ launchScript += "coremod " + coremod.filename().completeBaseName() + "\n";;
+ }
+
+ for(auto & jarmod: m_profile->getJarMods())
+ {
+ launchScript += "jarmod " + jarmod->originalName + " (" + jarmod->name + ")\n";
+ }
+
+ auto mainClass = m_profile->getMainClass();
+ if (!mainClass.isEmpty())
+ {
+ launchScript += "mainClass " + mainClass + "\n";
+ }
+ auto appletClass = m_profile->getAppletClass();
+ if (!appletClass.isEmpty())
+ {
+ launchScript += "appletClass " + appletClass + "\n";
+ }
+
+ // generic minecraft params
+ for (auto param : processMinecraftArgs(session))
+ {
+ launchScript += "param " + param + "\n";
+ }
+
+ // window size, title and state, legacy
+ {
+ QString windowParams;
+ if (settings()->get("LaunchMaximized").toBool())
+ windowParams = "max";
+ else
+ windowParams = QString("%1x%2")
+ .arg(settings()->get("MinecraftWinWidth").toInt())
+ .arg(settings()->get("MinecraftWinHeight").toInt());
+ launchScript += "windowTitle " + windowTitle() + "\n";
+ launchScript += "windowParams " + windowParams + "\n";
+ }
+
+ // legacy auth
+ {
+ launchScript += "userName " + session->player_name + "\n";
+ launchScript += "sessionId " + session->session + "\n";
+ }
+
+ // libraries and class path.
+ {
+ auto libs = m_profile->getLibraries();
+
+ QStringList jar, native, native32, native64;
+ for (auto lib : libs)
+ {
+ lib->getApplicableFiles(currentSystem, jar, native, native32, native64);
+ }
+ for(auto file: jar)
+ {
+ launchScript += "cp " + file + "\n";
+ }
+ for(auto file: native)
+ {
+ launchScript += "ext " + file + "\n";
+ }
+ for(auto file: native32)
+ {
+ launchScript += "ext32 " + file + "\n";
+ }
+ for(auto file: native64)
+ {
+ launchScript += "ext64 " + file + "\n";
+ }
+ QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/"));
+ launchScript += "natives " + natives_dir.absolutePath() + "\n";
+ auto jarMods = getJarMods();
+ if (!jarMods.isEmpty())
+ {
+ launchScript += "cp " + QDir(instanceRoot()).absoluteFilePath("minecraft.jar") + "\n";
+ }
+ else
+ {
+ QString relpath = m_profile->getMinecraftVersion() + "/" + m_profile->getMinecraftVersion() + ".jar";
+ launchScript += "cp " + versionsPath().absoluteFilePath(relpath) + "\n";
+ }
+ }
+
+ // traits. including legacyLaunch and others ;)
+ for (auto trait : m_profile->getTraits())
+ {
+ launchScript += "traits " + trait + "\n";
+ }
+ launchScript += "launcher onesix\n";
+ return launchScript;
+}
+
+std::shared_ptr<LaunchTask> OneSixInstance::createLaunchTask(AuthSessionPtr session)
+{
+ auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(getSharedPtr()));
+ auto pptr = process.get();
+
+ // print a header
+ {
+ process->appendStep(std::make_shared<TextPrint>(pptr, "Minecraft folder is:\n" + minecraftRoot() + "\n\n", MessageLevel::MultiMC));
+ }
+ {
+ auto step = std::make_shared<CheckJava>(pptr);
+ process->appendStep(step);
+ }
+ // run pre-launch command if that's needed
+ if(getPreLaunchCommand().size())
+ {
+ auto step = std::make_shared<PreLaunchCommand>(pptr);
+ step->setWorkingDirectory(minecraftRoot());
+ process->appendStep(step);
+ }
+ // if we aren't in offline mode,.
+ if(session->status != AuthSession::PlayableOffline)
+ {
+ process->appendStep(std::make_shared<Update>(pptr));
+ }
+ // if there are any jar mods
+ if(getJarMods().size())
+ {
+ auto step = std::make_shared<ModMinecraftJar>(pptr);
+ process->appendStep(step);
+ }
+ // actually launch the game
+ {
+ auto step = std::make_shared<LaunchMinecraft>(pptr);
+ step->setWorkingDirectory(minecraftRoot());
+ step->setAuthSession(session);
+ process->appendStep(step);
+ }
+ // run post-exit command if that's needed
+ if(getPostExitCommand().size())
+ {
+ auto step = std::make_shared<PostLaunchCommand>(pptr);
+ step->setWorkingDirectory(minecraftRoot());
+ process->appendStep(step);
+ }
+ if (session)
+ {
+ process->setCensorFilter(createCensorFilterFromSession(session));
+ }
+ return process;
+}
+
+std::shared_ptr<Task> OneSixInstance::createJarModdingTask()
+{
+ class JarModTask : public Task
+ {
+ public:
+ explicit JarModTask(std::shared_ptr<OneSixInstance> inst) : Task(nullptr), m_inst(inst)
+ {
+ }
+ virtual void executeTask()
+ {
+ auto profile = m_inst->getMinecraftProfile();
+ // nuke obsolete stripped jar(s) if needed
+ QString version_id = profile->getMinecraftVersion();
+ QString strippedPath = version_id + "/" + version_id + "-stripped.jar";
+ QFile strippedJar(strippedPath);
+ if(strippedJar.exists())
+ {
+ strippedJar.remove();
+ }
+ auto tempJarPath = QDir(m_inst->instanceRoot()).absoluteFilePath("temp.jar");
+ QFile tempJar(tempJarPath);
+ if(tempJar.exists())
+ {
+ tempJar.remove();
+ }
+ auto finalJarPath = QDir(m_inst->instanceRoot()).absoluteFilePath("minecraft.jar");
+ QFile finalJar(finalJarPath);
+ if(finalJar.exists())
+ {
+ if(!finalJar.remove())
+ {
+ emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath));
+ return;
+ }
+ }
+
+ // create temporary modded jar, if needed
+ auto jarMods = m_inst->getJarMods();
+ if(jarMods.size())
+ {
+ auto sourceJarPath = m_inst->versionsPath().absoluteFilePath(version_id + "/" + version_id + ".jar");
+ QString localPath = version_id + "/" + version_id + ".jar";
+ auto metacache = ENV.metacache();
+ auto entry = metacache->resolveEntry("versions", localPath);
+ QString fullJarPath = entry->getFullPath();
+ if(!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods))
+ {
+ emitFailed(tr("Failed to create the custom Minecraft jar file."));
+ return;
+ }
+ }
+ emitSucceeded();
+ }
+ std::shared_ptr<OneSixInstance> m_inst;
+ };
+ return std::make_shared<JarModTask>(std::dynamic_pointer_cast<OneSixInstance>(shared_from_this()));
+}
+
+void OneSixInstance::cleanupAfterRun()
+{
+ QString target_dir = FS::PathCombine(instanceRoot(), "natives/");
+ QDir dir(target_dir);
+ dir.removeRecursively();
+}
+
+std::shared_ptr<ModList> OneSixInstance::loaderModList() const
+{
+ if (!m_loader_mod_list)
+ {
+ m_loader_mod_list.reset(new ModList(loaderModsDir()));
+ }
+ m_loader_mod_list->update();
+ return m_loader_mod_list;
+}
+
+std::shared_ptr<ModList> OneSixInstance::coreModList() const
+{
+ if (!m_core_mod_list)
+ {
+ m_core_mod_list.reset(new ModList(coreModsDir()));
+ }
+ m_core_mod_list->update();
+ return m_core_mod_list;
+}
+
+std::shared_ptr<ModList> OneSixInstance::resourcePackList() const
+{
+ if (!m_resource_pack_list)
+ {
+ m_resource_pack_list.reset(new ModList(resourcePacksDir()));
+ }
+ m_resource_pack_list->update();
+ return m_resource_pack_list;
+}
+
+std::shared_ptr<ModList> OneSixInstance::texturePackList() const
+{
+ if (!m_texture_pack_list)
+ {
+ m_texture_pack_list.reset(new ModList(texturePacksDir()));
+ }
+ m_texture_pack_list->update();
+ return m_texture_pack_list;
+}
+
+std::shared_ptr<WorldList> OneSixInstance::worldList() const
+{
+ if (!m_world_list)
+ {
+ m_world_list.reset(new WorldList(worldDir()));
+ }
+ return m_world_list;
+}
+
+bool OneSixInstance::setIntendedVersionId(QString version)
+{
+ settings()->set("IntendedVersion", version);
+ if(getMinecraftProfile())
+ {
+ clearProfile();
+ }
+ emit propertiesChanged(this);
+ return true;
+}
+
+QList< Mod > OneSixInstance::getJarMods() const
+{
+ QList<Mod> mods;
+ for (auto jarmod : m_profile->getJarMods())
+ {
+ QString filePath = jarmodsPath().absoluteFilePath(jarmod->name);
+ mods.push_back(Mod(QFileInfo(filePath)));
+ }
+ return mods;
+}
+
+
+QString OneSixInstance::intendedVersionId() const
+{
+ return settings()->get("IntendedVersion").toString();
+}
+
+void OneSixInstance::setShouldUpdate(bool)
+{
+}
+
+bool OneSixInstance::shouldUpdate() const
+{
+ return true;
+}
+
+QString OneSixInstance::currentVersionId() const
+{
+ return intendedVersionId();
+}
+
+void OneSixInstance::reloadProfile()
+{
+ m_profile->reload();
+ auto severity = m_profile->getProblemSeverity();
+ if(severity == ProblemSeverity::PROBLEM_ERROR)
+ {
+ setFlag(VersionBrokenFlag);
+ }
+ else
+ {
+ unsetFlag(VersionBrokenFlag);
+ }
+ emit versionReloaded();
+}
+
+void OneSixInstance::clearProfile()
+{
+ m_profile->clear();
+ emit versionReloaded();
+}
+
+std::shared_ptr<MinecraftProfile> OneSixInstance::getMinecraftProfile() const
+{
+ return m_profile;
+}
+
+QDir OneSixInstance::librariesPath() const
+{
+ return QDir::current().absoluteFilePath("libraries");
+}
+
+QDir OneSixInstance::jarmodsPath() const
+{
+ return QDir(jarModsDir());
+}
+
+QDir OneSixInstance::versionsPath() const
+{
+ return QDir::current().absoluteFilePath("versions");
+}
+
+bool OneSixInstance::providesVersionFile() const
+{
+ return false;
+}
+
+bool OneSixInstance::reload()
+{
+ if (BaseInstance::reload())
+ {
+ try
+ {
+ reloadProfile();
+ return true;
+ }
+ catch (...)
+ {
+ return false;
+ }
+ }
+ return false;
+}
+
+QString OneSixInstance::loaderModsDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "mods");
+}
+
+QString OneSixInstance::coreModsDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "coremods");
+}
+
+QString OneSixInstance::resourcePacksDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "resourcepacks");
+}
+
+QString OneSixInstance::texturePacksDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "texturepacks");
+}
+
+QString OneSixInstance::instanceConfigFolder() const
+{
+ return FS::PathCombine(minecraftRoot(), "config");
+}
+
+QString OneSixInstance::jarModsDir() const
+{
+ return FS::PathCombine(instanceRoot(), "jarmods");
+}
+
+QString OneSixInstance::libDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "lib");
+}
+
+QString OneSixInstance::worldDir() const
+{
+ return FS::PathCombine(minecraftRoot(), "saves");
+}
+
+QStringList OneSixInstance::extraArguments() const
+{
+ auto list = BaseInstance::extraArguments();
+ auto version = getMinecraftProfile();
+ if (!version)
+ return list;
+ auto jarMods = getJarMods();
+ if (!jarMods.isEmpty())
+ {
+ list.append({"-Dfml.ignoreInvalidMinecraftCertificates=true",
+ "-Dfml.ignorePatchDiscrepancies=true"});
+ }
+ return list;
+}
+
+std::shared_ptr<OneSixInstance> OneSixInstance::getSharedPtr()
+{
+ return std::dynamic_pointer_cast<OneSixInstance>(BaseInstance::getSharedPtr());
+}
+
+QString OneSixInstance::typeName() const
+{
+ return tr("OneSix");
+}
diff --git a/api/logic/minecraft/onesix/OneSixInstance.h b/api/logic/minecraft/onesix/OneSixInstance.h
new file mode 100644
index 00000000..2dfab48c
--- /dev/null
+++ b/api/logic/minecraft/onesix/OneSixInstance.h
@@ -0,0 +1,117 @@
+/* Copyright 2013-2015 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 "minecraft/MinecraftInstance.h"
+
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/ModList.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT OneSixInstance : public MinecraftInstance
+{
+ Q_OBJECT
+public:
+ explicit OneSixInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+ virtual ~OneSixInstance(){};
+
+ virtual void init() override;
+
+ ////// Mod Lists //////
+ std::shared_ptr<ModList> loaderModList() const;
+ std::shared_ptr<ModList> coreModList() const;
+ std::shared_ptr<ModList> resourcePackList() const override;
+ std::shared_ptr<ModList> texturePackList() const override;
+ std::shared_ptr<WorldList> worldList() const override;
+ virtual QList<Mod> getJarMods() const override;
+ virtual void createProfile();
+
+ virtual QSet<QString> traits() override;
+
+ ////// Directories and files //////
+ QString jarModsDir() const;
+ QString resourcePacksDir() const;
+ QString texturePacksDir() const;
+ QString loaderModsDir() const;
+ QString coreModsDir() const;
+ QString libDir() const;
+ QString worldDir() const;
+ virtual QString instanceConfigFolder() const override;
+
+ virtual std::shared_ptr<Task> createUpdateTask() override;
+ virtual std::shared_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account) override;
+ virtual std::shared_ptr<Task> createJarModdingTask() override;
+
+ virtual QString createLaunchScript(AuthSessionPtr session) override;
+
+ virtual void cleanupAfterRun() override;
+
+ virtual QString intendedVersionId() const override;
+ virtual bool setIntendedVersionId(QString version) override;
+
+ virtual QString currentVersionId() const override;
+
+ virtual bool shouldUpdate() const override;
+ virtual void setShouldUpdate(bool val) override;
+
+ /**
+ * reload the profile, including version json files.
+ *
+ * throws various exceptions :3
+ */
+ void reloadProfile();
+
+ /// clears all version information in preparation for an update
+ void clearProfile();
+
+ /// get the current full version info
+ std::shared_ptr<MinecraftProfile> getMinecraftProfile() const;
+
+ virtual QDir jarmodsPath() const;
+ virtual QDir librariesPath() const;
+ virtual QDir versionsPath() const;
+ virtual bool providesVersionFile() const;
+
+ bool reload() override;
+
+ virtual QStringList extraArguments() const override;
+
+ std::shared_ptr<OneSixInstance> getSharedPtr();
+
+ virtual QString typeName() const override;
+
+ bool canExport() const override
+ {
+ return true;
+ }
+
+signals:
+ void versionReloaded();
+
+private:
+ QStringList processMinecraftArgs(AuthSessionPtr account);
+
+protected:
+ std::shared_ptr<MinecraftProfile> m_profile;
+ mutable std::shared_ptr<ModList> m_loader_mod_list;
+ mutable std::shared_ptr<ModList> m_core_mod_list;
+ mutable std::shared_ptr<ModList> m_resource_pack_list;
+ mutable std::shared_ptr<ModList> m_texture_pack_list;
+ mutable std::shared_ptr<WorldList> m_world_list;
+};
+
+Q_DECLARE_METATYPE(std::shared_ptr<OneSixInstance>)
diff --git a/api/logic/minecraft/onesix/OneSixProfileStrategy.cpp b/api/logic/minecraft/onesix/OneSixProfileStrategy.cpp
new file mode 100644
index 00000000..af42286d
--- /dev/null
+++ b/api/logic/minecraft/onesix/OneSixProfileStrategy.cpp
@@ -0,0 +1,418 @@
+#include "OneSixProfileStrategy.h"
+#include "OneSixInstance.h"
+#include "OneSixVersionFormat.h"
+
+#include "minecraft/VersionBuildError.h"
+#include "minecraft/MinecraftVersionList.h"
+#include "Env.h"
+#include <FileSystem.h>
+
+#include <QDir>
+#include <QUuid>
+#include <QJsonDocument>
+#include <QJsonArray>
+
+OneSixProfileStrategy::OneSixProfileStrategy(OneSixInstance* instance)
+{
+ m_instance = instance;
+}
+
+void OneSixProfileStrategy::upgradeDeprecatedFiles()
+{
+ auto versionJsonPath = FS::PathCombine(m_instance->instanceRoot(), "version.json");
+ auto customJsonPath = FS::PathCombine(m_instance->instanceRoot(), "custom.json");
+ auto mcJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "net.minecraft.json");
+
+ QString sourceFile;
+ QString renameFile;
+
+ // convert old crap.
+ if(QFile::exists(customJsonPath))
+ {
+ sourceFile = customJsonPath;
+ renameFile = versionJsonPath;
+ }
+ else if(QFile::exists(versionJsonPath))
+ {
+ sourceFile = versionJsonPath;
+ }
+ if(!sourceFile.isEmpty() && !QFile::exists(mcJson))
+ {
+ if(!FS::ensureFilePathExists(mcJson))
+ {
+ qWarning() << "Couldn't create patches folder for" << m_instance->name();
+ return;
+ }
+ if(!renameFile.isEmpty() && QFile::exists(renameFile))
+ {
+ if(!QFile::rename(renameFile, renameFile + ".old"))
+ {
+ qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << m_instance->name();
+ return;
+ }
+ }
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false);
+ ProfileUtils::removeLwjglFromPatch(file);
+ file->fileId = "net.minecraft";
+ file->version = file->minecraftVersion;
+ file->name = "Minecraft";
+ auto data = OneSixVersionFormat::versionFileToJson(file, false).toJson();
+ QSaveFile newPatchFile(mcJson);
+ if(!newPatchFile.open(QIODevice::WriteOnly))
+ {
+ newPatchFile.cancelWriting();
+ qWarning() << "Couldn't open main patch for writing in" << m_instance->name();
+ return;
+ }
+ newPatchFile.write(data);
+ if(!newPatchFile.commit())
+ {
+ qWarning() << "Couldn't save main patch in" << m_instance->name();
+ return;
+ }
+ if(!QFile::rename(sourceFile, sourceFile + ".old"))
+ {
+ qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << m_instance->name();
+ return;
+ }
+ }
+}
+
+void OneSixProfileStrategy::loadDefaultBuiltinPatches()
+{
+ {
+ auto mcJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "net.minecraft.json");
+ // load up the base minecraft patch
+ ProfilePatchPtr minecraftPatch;
+ if(QFile::exists(mcJson))
+ {
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false);
+ if(file->version.isEmpty())
+ {
+ file->version = m_instance->intendedVersionId();
+ }
+ file->setVanilla(false);
+ file->setRevertible(true);
+ minecraftPatch = std::dynamic_pointer_cast<ProfilePatch>(file);
+ }
+ else
+ {
+ auto mcversion = ENV.getVersion("net.minecraft", m_instance->intendedVersionId());
+ minecraftPatch = std::dynamic_pointer_cast<ProfilePatch>(mcversion);
+ }
+ if (!minecraftPatch)
+ {
+ throw VersionIncomplete("net.minecraft");
+ }
+ minecraftPatch->setOrder(-2);
+ profile->appendPatch(minecraftPatch);
+ }
+
+ {
+ auto lwjglJson = FS::PathCombine(m_instance->instanceRoot(), "patches" , "org.lwjgl.json");
+ ProfilePatchPtr lwjglPatch;
+ if(QFile::exists(lwjglJson))
+ {
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(lwjglJson), false);
+ file->setVanilla(false);
+ file->setRevertible(true);
+ lwjglPatch = std::dynamic_pointer_cast<ProfilePatch>(file);
+ }
+ else
+ {
+ // NOTE: this is obviously fake, is fixed in unstable.
+ QResource LWJGL(":/versions/LWJGL/2.9.1.json");
+ auto lwjgl = ProfileUtils::parseJsonFile(LWJGL.absoluteFilePath(), false);
+ lwjgl->setVanilla(true);
+ lwjgl->setCustomizable(true);
+ lwjglPatch = std::dynamic_pointer_cast<ProfilePatch>(lwjgl);
+ }
+ if (!lwjglPatch)
+ {
+ throw VersionIncomplete("org.lwjgl");
+ }
+ lwjglPatch->setOrder(-1);
+ profile->appendPatch(lwjglPatch);
+ }
+}
+
+void OneSixProfileStrategy::loadUserPatches()
+{
+ // load all patches, put into map for ordering, apply in the right order
+ ProfileUtils::PatchOrder userOrder;
+ ProfileUtils::readOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), userOrder);
+ QDir patches(FS::PathCombine(m_instance->instanceRoot(),"patches"));
+ QSet<QString> seen_extra;
+
+ // first, load things by sort order.
+ for (auto id : userOrder)
+ {
+ // ignore builtins
+ if (id == "net.minecraft")
+ continue;
+ if (id == "org.lwjgl")
+ continue;
+ // parse the file
+ QString filename = patches.absoluteFilePath(id + ".json");
+ QFileInfo finfo(filename);
+ if(!finfo.exists())
+ {
+ qDebug() << "Patch file " << filename << " was deleted by external means...";
+ continue;
+ }
+ qDebug() << "Reading" << filename << "by user order";
+ VersionFilePtr file = ProfileUtils::parseJsonFile(finfo, false);
+ // sanity check. prevent tampering with files.
+ if (file->fileId != id)
+ {
+ file->addProblem(PROBLEM_WARNING, QObject::tr("load id %1 does not match internal id %2").arg(id, file->fileId));
+ seen_extra.insert(file->fileId);
+ }
+ file->setRemovable(true);
+ file->setMovable(true);
+ profile->appendPatch(file);
+ }
+ // now load the rest by internal preference.
+ QMultiMap<int, VersionFilePtr> files;
+ for (auto info : patches.entryInfoList(QStringList() << "*.json", QDir::Files))
+ {
+ // parse the file
+ qDebug() << "Reading" << info.fileName();
+ auto file = ProfileUtils::parseJsonFile(info, true);
+ // ignore builtins
+ if (file->fileId == "net.minecraft")
+ continue;
+ if (file->fileId == "org.lwjgl")
+ continue;
+ // do not load versions with broken IDs twice
+ if(seen_extra.contains(file->fileId))
+ continue;
+ // do not load what we already loaded in the first pass
+ if (userOrder.contains(file->fileId))
+ continue;
+ file->setRemovable(true);
+ file->setMovable(true);
+ files.insert(file->order, file);
+ }
+ QSet<int> seen;
+ for (auto order : files.keys())
+ {
+ if(seen.contains(order))
+ continue;
+ seen.insert(order);
+ const auto &values = files.values(order);
+ if(values.size() == 1)
+ {
+ profile->appendPatch(values[0]);
+ continue;
+ }
+ for(auto &file: values)
+ {
+ QStringList list;
+ for(auto &file2: values)
+ {
+ if(file != file2)
+ list.append(file2->name);
+ }
+ file->addProblem(PROBLEM_WARNING, QObject::tr("%1 has the same order as the following components:\n%2").arg(file->name, list.join(", ")));
+ profile->appendPatch(file);
+ }
+ }
+}
+
+
+void OneSixProfileStrategy::load()
+{
+ profile->clearPatches();
+
+ upgradeDeprecatedFiles();
+ loadDefaultBuiltinPatches();
+ loadUserPatches();
+}
+
+bool OneSixProfileStrategy::saveOrder(ProfileUtils::PatchOrder order)
+{
+ return ProfileUtils::writeOverrideOrders(FS::PathCombine(m_instance->instanceRoot(), "order.json"), order);
+}
+
+bool OneSixProfileStrategy::resetOrder()
+{
+ return QDir(m_instance->instanceRoot()).remove("order.json");
+}
+
+bool OneSixProfileStrategy::removePatch(ProfilePatchPtr patch)
+{
+ bool ok = true;
+ // first, remove the patch file. this ensures it's not used anymore
+ auto fileName = patch->getFilename();
+ if(fileName.size())
+ {
+ QFile patchFile(fileName);
+ if(patchFile.exists() && !patchFile.remove())
+ {
+ qCritical() << "File" << fileName << "could not be removed because:" << patchFile.errorString();
+ return false;
+ }
+ }
+
+
+ auto preRemoveJarMod = [&](JarmodPtr jarMod) -> bool
+ {
+ QString fullpath = FS::PathCombine(m_instance->jarModsDir(), jarMod->name);
+ QFileInfo finfo (fullpath);
+ if(finfo.exists())
+ {
+ QFile jarModFile(fullpath);
+ if(!jarModFile.remove())
+ {
+ qCritical() << "File" << fullpath << "could not be removed because:" << jarModFile.errorString();
+ return false;
+ }
+ return true;
+ }
+ return true;
+ };
+
+ for(auto &jarmod: patch->getJarMods())
+ {
+ ok &= preRemoveJarMod(jarmod);
+ }
+ return ok;
+}
+
+bool OneSixProfileStrategy::customizePatch(ProfilePatchPtr patch)
+{
+ if(patch->isCustom())
+ {
+ return false;
+ }
+
+ auto filename = FS::PathCombine(m_instance->instanceRoot(), "patches" , patch->getID() + ".json");
+ if(!FS::ensureFilePathExists(filename))
+ {
+ return false;
+ }
+ try
+ {
+ QSaveFile jsonFile(filename);
+ if(!jsonFile.open(QIODevice::WriteOnly))
+ {
+ return false;
+ }
+ auto vfile = patch->getVersionFile();
+ if(!vfile)
+ {
+ return false;
+ }
+ auto document = OneSixVersionFormat::versionFileToJson(vfile, true);
+ jsonFile.write(document.toJson());
+ if(!jsonFile.commit())
+ {
+ return false;
+ }
+ load();
+ }
+ catch (VersionIncomplete &error)
+ {
+ qDebug() << "Version was incomplete:" << error.cause();
+ }
+ catch (Exception &error)
+ {
+ qWarning() << "Version could not be loaded:" << error.cause();
+ }
+ return true;
+}
+
+bool OneSixProfileStrategy::revertPatch(ProfilePatchPtr patch)
+{
+ if(!patch->isCustom())
+ {
+ // already not custom
+ return true;
+ }
+ auto filename = patch->getFilename();
+ if(!QFile::exists(filename))
+ {
+ // already gone / not custom
+ return true;
+ }
+ // just kill the file and reload
+ bool result = QFile::remove(filename);
+ try
+ {
+ load();
+ }
+ catch (VersionIncomplete &error)
+ {
+ qDebug() << "Version was incomplete:" << error.cause();
+ }
+ catch (Exception &error)
+ {
+ qWarning() << "Version could not be loaded:" << error.cause();
+ }
+ return result;
+}
+
+bool OneSixProfileStrategy::installJarMods(QStringList filepaths)
+{
+ QString patchDir = FS::PathCombine(m_instance->instanceRoot(), "patches");
+ if(!FS::ensureFolderPathExists(patchDir))
+ {
+ return false;
+ }
+
+ if (!FS::ensureFolderPathExists(m_instance->jarModsDir()))
+ {
+ return false;
+ }
+
+ for(auto filepath:filepaths)
+ {
+ QFileInfo sourceInfo(filepath);
+ auto uuid = QUuid::createUuid();
+ QString id = uuid.toString().remove('{').remove('}');
+ QString target_filename = id + ".jar";
+ QString target_id = "org.multimc.jarmod." + id;
+ QString target_name = sourceInfo.completeBaseName() + " (jar mod)";
+ QString finalPath = FS::PathCombine(m_instance->jarModsDir(), target_filename);
+
+ QFileInfo targetInfo(finalPath);
+ if(targetInfo.exists())
+ {
+ return false;
+ }
+
+ if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath()))
+ {
+ return false;
+ }
+
+ auto f = std::make_shared<VersionFile>();
+ auto jarMod = std::make_shared<Jarmod>();
+ jarMod->name = target_filename;
+ jarMod->originalName = sourceInfo.completeBaseName();
+ f->jarMods.append(jarMod);
+ f->name = target_name;
+ f->fileId = target_id;
+ f->order = profile->getFreeOrderNumber();
+ QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+ f->filename = patchFileName;
+ f->setMovable(true);
+ f->setRemovable(true);
+
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f, true).toJson());
+ file.close();
+ profile->appendPatch(f);
+ }
+ profile->saveCurrentOrder();
+ profile->reapplyPatches();
+ return true;
+}
+
diff --git a/api/logic/minecraft/onesix/OneSixProfileStrategy.h b/api/logic/minecraft/onesix/OneSixProfileStrategy.h
new file mode 100644
index 00000000..96c1ba7b
--- /dev/null
+++ b/api/logic/minecraft/onesix/OneSixProfileStrategy.h
@@ -0,0 +1,26 @@
+#pragma once
+#include "minecraft/ProfileStrategy.h"
+
+class OneSixInstance;
+
+class OneSixProfileStrategy : public ProfileStrategy
+{
+public:
+ OneSixProfileStrategy(OneSixInstance * instance);
+ virtual ~OneSixProfileStrategy() {};
+ virtual void load() override;
+ virtual bool resetOrder() override;
+ virtual bool saveOrder(ProfileUtils::PatchOrder order) override;
+ virtual bool installJarMods(QStringList filepaths) override;
+ virtual bool removePatch(ProfilePatchPtr patch) override;
+ virtual bool customizePatch(ProfilePatchPtr patch) override;
+ virtual bool revertPatch(ProfilePatchPtr patch) override;
+
+protected:
+ virtual void loadDefaultBuiltinPatches();
+ virtual void loadUserPatches();
+ void upgradeDeprecatedFiles();
+
+protected:
+ OneSixInstance *m_instance;
+};
diff --git a/api/logic/minecraft/onesix/OneSixUpdate.cpp b/api/logic/minecraft/onesix/OneSixUpdate.cpp
new file mode 100644
index 00000000..1c2cd196
--- /dev/null
+++ b/api/logic/minecraft/onesix/OneSixUpdate.cpp
@@ -0,0 +1,342 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Env.h"
+#include <minecraft/forge/ForgeXzDownload.h>
+#include "OneSixUpdate.h"
+#include "OneSixInstance.h"
+
+#include <QtNetwork>
+
+#include <QFile>
+#include <QFileInfo>
+#include <QTextStream>
+#include <QDataStream>
+#include <JlCompress.h>
+
+#include "BaseInstance.h"
+#include "minecraft/MinecraftVersionList.h"
+#include "minecraft/MinecraftProfile.h"
+#include "minecraft/Library.h"
+#include "net/URLConstants.h"
+#include "minecraft/AssetsUtils.h"
+#include "Exception.h"
+#include "MMCZip.h"
+#include <FileSystem.h>
+
+OneSixUpdate::OneSixUpdate(OneSixInstance *inst, QObject *parent) : Task(parent), m_inst(inst)
+{
+}
+
+void OneSixUpdate::executeTask()
+{
+ // Make directories
+ QDir mcDir(m_inst->minecraftRoot());
+ if (!mcDir.exists() && !mcDir.mkpath("."))
+ {
+ emitFailed(tr("Failed to create folder for minecraft binaries."));
+ return;
+ }
+
+ // Get a pointer to the version object that corresponds to the instance's version.
+ targetVersion = std::dynamic_pointer_cast<MinecraftVersion>(ENV.getVersion("net.minecraft", m_inst->intendedVersionId()));
+ if (targetVersion == nullptr)
+ {
+ // don't do anything if it was invalid
+ emitFailed(tr("The specified Minecraft version is invalid. Choose a different one."));
+ return;
+ }
+ if (m_inst->providesVersionFile() || !targetVersion->needsUpdate())
+ {
+ qDebug() << "Instance either provides a version file or doesn't need an update.";
+ jarlibStart();
+ return;
+ }
+ versionUpdateTask = std::dynamic_pointer_cast<MinecraftVersionList>(ENV.getVersionList("net.minecraft"))->createUpdateTask(m_inst->intendedVersionId());
+ if (!versionUpdateTask)
+ {
+ qDebug() << "Didn't spawn an update task.";
+ jarlibStart();
+ return;
+ }
+ connect(versionUpdateTask.get(), SIGNAL(succeeded()), SLOT(jarlibStart()));
+ connect(versionUpdateTask.get(), &NetJob::failed, this, &OneSixUpdate::versionUpdateFailed);
+ connect(versionUpdateTask.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ setStatus(tr("Getting the version files from Mojang..."));
+ versionUpdateTask->start();
+}
+
+void OneSixUpdate::versionUpdateFailed(QString reason)
+{
+ emitFailed(reason);
+}
+
+void OneSixUpdate::assetIndexStart()
+{
+ setStatus(tr("Updating assets index..."));
+ OneSixInstance *inst = (OneSixInstance *)m_inst;
+ auto profile = inst->getMinecraftProfile();
+ auto assets = profile->getMinecraftAssets();
+ QUrl indexUrl = assets->url;
+ QString localPath = assets->id + ".json";
+ auto job = new NetJob(tr("Asset index for %1").arg(inst->name()));
+
+ auto metacache = ENV.metacache();
+ auto entry = metacache->resolveEntry("asset_indexes", localPath);
+ entry->setStale(true);
+ job->addNetAction(CacheDownload::make(indexUrl, entry));
+ jarlibDownloadJob.reset(job);
+
+ connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetIndexFinished()));
+ connect(jarlibDownloadJob.get(), &NetJob::failed, this, &OneSixUpdate::assetIndexFailed);
+ connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+
+ qDebug() << m_inst->name() << ": Starting asset index download";
+ jarlibDownloadJob->start();
+}
+
+void OneSixUpdate::assetIndexFinished()
+{
+ AssetsIndex index;
+ qDebug() << m_inst->name() << ": Finished asset index download";
+
+ OneSixInstance *inst = (OneSixInstance *)m_inst;
+ auto profile = inst->getMinecraftProfile();
+ auto assets = profile->getMinecraftAssets();
+
+ QString asset_fname = "assets/indexes/" + assets->id + ".json";
+ // FIXME: this looks like a job for a generic validator based on json schema?
+ if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, &index))
+ {
+ auto metacache = ENV.metacache();
+ auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json");
+ metacache->evictEntry(entry);
+ emitFailed(tr("Failed to read the assets index!"));
+ }
+
+ auto job = index.getDownloadJob();
+ if(job)
+ {
+ setStatus(tr("Getting the assets files from Mojang..."));
+ jarlibDownloadJob = job;
+ connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetsFinished()));
+ connect(jarlibDownloadJob.get(), &NetJob::failed, this, &OneSixUpdate::assetsFailed);
+ connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ jarlibDownloadJob->start();
+ return;
+ }
+ assetsFinished();
+}
+
+void OneSixUpdate::assetIndexFailed(QString reason)
+{
+ qDebug() << m_inst->name() << ": Failed asset index download";
+ emitFailed(tr("Failed to download the assets index:\n%1").arg(reason));
+}
+
+void OneSixUpdate::assetsFinished()
+{
+ emitSucceeded();
+}
+
+void OneSixUpdate::assetsFailed(QString reason)
+{
+ emitFailed(tr("Failed to download assets:\n%1").arg(reason));
+}
+
+void OneSixUpdate::jarlibStart()
+{
+ setStatus(tr("Getting the library files from Mojang..."));
+ qDebug() << m_inst->name() << ": downloading libraries";
+ OneSixInstance *inst = (OneSixInstance *)m_inst;
+ inst->reloadProfile();
+ if(inst->flags() & BaseInstance::VersionBrokenFlag)
+ {
+ emitFailed(tr("Failed to load the version description files - check the instance for errors."));
+ return;
+ }
+
+ // Build a list of URLs that will need to be downloaded.
+ std::shared_ptr<MinecraftProfile> profile = inst->getMinecraftProfile();
+ // minecraft.jar for this version
+ {
+ QString version_id = profile->getMinecraftVersion();
+ QString localPath = version_id + "/" + version_id + ".jar";
+ QString urlstr = profile->getMainJarUrl();
+
+ auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name()));
+
+ auto metacache = ENV.metacache();
+ auto entry = metacache->resolveEntry("versions", localPath);
+ job->addNetAction(CacheDownload::make(QUrl(urlstr), entry));
+ jarlibDownloadJob.reset(job);
+ }
+
+ auto libs = profile->getLibraries();
+
+ auto metacache = ENV.metacache();
+ QList<LibraryPtr> brokenLocalLibs;
+
+ QStringList failedFiles;
+ for (auto lib : libs)
+ {
+ auto dls = lib->getDownloads(currentSystem, metacache.get(), failedFiles);
+ for(auto dl : dls)
+ {
+ jarlibDownloadJob->addNetAction(dl);
+ }
+ }
+ if (!brokenLocalLibs.empty())
+ {
+ jarlibDownloadJob.reset();
+
+ QString failed_all = failedFiles.join("\n");
+ emitFailed(tr("Some libraries marked as 'local' are missing their jar "
+ "files:\n%1\n\nYou'll have to correct this problem manually. If this is "
+ "an externally tracked instance, make sure to run it at least once "
+ "outside of MultiMC.").arg(failed_all));
+ return;
+ }
+
+ connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(jarlibFinished()));
+ connect(jarlibDownloadJob.get(), &NetJob::failed, this, &OneSixUpdate::jarlibFailed);
+ connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)),
+ SIGNAL(progress(qint64, qint64)));
+
+ jarlibDownloadJob->start();
+}
+
+void OneSixUpdate::jarlibFinished()
+{
+ OneSixInstance *inst = (OneSixInstance *)m_inst;
+ std::shared_ptr<MinecraftProfile> profile = inst->getMinecraftProfile();
+
+ if (profile->hasTrait("legacyFML"))
+ {
+ fmllibsStart();
+ }
+ else
+ {
+ assetIndexStart();
+ }
+}
+
+void OneSixUpdate::jarlibFailed(QString reason)
+{
+ QStringList failed = jarlibDownloadJob->getFailedFiles();
+ QString failed_all = failed.join("\n");
+ emitFailed(
+ tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason));
+}
+
+void OneSixUpdate::fmllibsStart()
+{
+ // Get the mod list
+ OneSixInstance *inst = (OneSixInstance *)m_inst;
+ std::shared_ptr<MinecraftProfile> profile = inst->getMinecraftProfile();
+ bool forge_present = false;
+
+ QString version = inst->intendedVersionId();
+ auto &fmlLibsMapping = g_VersionFilterData.fmlLibsMapping;
+ if (!fmlLibsMapping.contains(version))
+ {
+ assetIndexStart();
+ return;
+ }
+
+ auto &libList = fmlLibsMapping[version];
+
+ // determine if we need some libs for FML or forge
+ setStatus(tr("Checking for FML libraries..."));
+ forge_present = (profile->versionPatch("net.minecraftforge") != nullptr);
+ // we don't...
+ if (!forge_present)
+ {
+ assetIndexStart();
+ return;
+ }
+
+ // now check the lib folder inside the instance for files.
+ for (auto &lib : libList)
+ {
+ QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename));
+ if (libInfo.exists())
+ continue;
+ fmlLibsToProcess.append(lib);
+ }
+
+ // if everything is in place, there's nothing to do here...
+ if (fmlLibsToProcess.isEmpty())
+ {
+ assetIndexStart();
+ return;
+ }
+
+ // download missing libs to our place
+ setStatus(tr("Dowloading FML libraries..."));
+ auto dljob = new NetJob("FML libraries");
+ auto metacache = ENV.metacache();
+ for (auto &lib : fmlLibsToProcess)
+ {
+ auto entry = metacache->resolveEntry("fmllibs", lib.filename);
+ QString urlString = lib.ours ? URLConstants::FMLLIBS_OUR_BASE_URL + lib.filename
+ : URLConstants::FMLLIBS_FORGE_BASE_URL + lib.filename;
+ dljob->addNetAction(CacheDownload::make(QUrl(urlString), entry));
+ }
+
+ connect(dljob, SIGNAL(succeeded()), SLOT(fmllibsFinished()));
+ connect(dljob, &NetJob::failed, this, &OneSixUpdate::fmllibsFailed);
+ connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
+ legacyDownloadJob.reset(dljob);
+ legacyDownloadJob->start();
+}
+
+void OneSixUpdate::fmllibsFinished()
+{
+ legacyDownloadJob.reset();
+ if (!fmlLibsToProcess.isEmpty())
+ {
+ setStatus(tr("Copying FML libraries into the instance..."));
+ OneSixInstance *inst = (OneSixInstance *)m_inst;
+ auto metacache = ENV.metacache();
+ int index = 0;
+ for (auto &lib : fmlLibsToProcess)
+ {
+ progress(index, fmlLibsToProcess.size());
+ auto entry = metacache->resolveEntry("fmllibs", lib.filename);
+ auto path = FS::PathCombine(inst->libDir(), lib.filename);
+ if (!FS::ensureFilePathExists(path))
+ {
+ emitFailed(tr("Failed creating FML library folder inside the instance."));
+ return;
+ }
+ if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename)))
+ {
+ emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename));
+ return;
+ }
+ index++;
+ }
+ progress(index, fmlLibsToProcess.size());
+ }
+ assetIndexStart();
+}
+
+void OneSixUpdate::fmllibsFailed(QString reason)
+{
+ emitFailed(tr("Game update failed: it was impossible to fetch the required FML libraries.\nReason:\n%1").arg(reason));
+ return;
+}
+
diff --git a/api/logic/minecraft/onesix/OneSixUpdate.h b/api/logic/minecraft/onesix/OneSixUpdate.h
new file mode 100644
index 00000000..b5195364
--- /dev/null
+++ b/api/logic/minecraft/onesix/OneSixUpdate.h
@@ -0,0 +1,67 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QList>
+#include <QUrl>
+
+#include "net/NetJob.h"
+#include "tasks/Task.h"
+#include "minecraft/VersionFilterData.h"
+#include <quazip.h>
+
+class MinecraftVersion;
+class OneSixInstance;
+
+class OneSixUpdate : public Task
+{
+ Q_OBJECT
+public:
+ explicit OneSixUpdate(OneSixInstance *inst, QObject *parent = 0);
+ virtual void executeTask();
+
+private
+slots:
+ void versionUpdateFailed(QString reason);
+
+ void jarlibStart();
+ void jarlibFinished();
+ void jarlibFailed(QString reason);
+
+ void fmllibsStart();
+ void fmllibsFinished();
+ void fmllibsFailed(QString reason);
+
+ void assetIndexStart();
+ void assetIndexFinished();
+ void assetIndexFailed(QString reason);
+
+ void assetsFinished();
+ void assetsFailed(QString reason);
+
+private:
+ NetJobPtr jarlibDownloadJob;
+ NetJobPtr legacyDownloadJob;
+
+ /// target version, determined during this task
+ std::shared_ptr<MinecraftVersion> targetVersion;
+ /// the task that is spawned for version updates
+ std::shared_ptr<Task> versionUpdateTask;
+
+ OneSixInstance *m_inst = nullptr;
+ QList<FMLlib> fmlLibsToProcess;
+};
diff --git a/api/logic/minecraft/onesix/OneSixVersionFormat.cpp b/api/logic/minecraft/onesix/OneSixVersionFormat.cpp
new file mode 100644
index 00000000..541fb109
--- /dev/null
+++ b/api/logic/minecraft/onesix/OneSixVersionFormat.cpp
@@ -0,0 +1,225 @@
+#include "OneSixVersionFormat.h"
+#include <Json.h>
+#include "minecraft/ParseUtils.h"
+#include <minecraft/MinecraftVersion.h>
+#include <minecraft/VersionBuildError.h>
+#include <minecraft/MojangVersionFormat.h>
+
+using namespace Json;
+
+static void readString(const QJsonObject &root, const QString &key, QString &variable)
+{
+ if (root.contains(key))
+ {
+ variable = requireString(root.value(key));
+ }
+}
+
+LibraryPtr OneSixVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename)
+{
+ LibraryPtr out = MojangVersionFormat::libraryFromJson(libObj, filename);
+ readString(libObj, "MMC-hint", out->m_hint);
+ readString(libObj, "MMC-absulute_url", out->m_absoluteURL);
+ readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL);
+ return out;
+}
+
+QJsonObject OneSixVersionFormat::libraryToJson(Library *library)
+{
+ QJsonObject libRoot = MojangVersionFormat::libraryToJson(library);
+ if (library->m_absoluteURL.size())
+ libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL);
+ if (library->m_hint.size())
+ libRoot.insert("MMC-hint", library->m_hint);
+ return libRoot;
+}
+
+VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc, const QString &filename, const bool requireOrder)
+{
+ VersionFilePtr out(new VersionFile());
+ if (doc.isEmpty() || doc.isNull())
+ {
+ throw JSONValidationError(filename + " is empty or null");
+ }
+ if (!doc.isObject())
+ {
+ throw JSONValidationError(filename + " is not an object");
+ }
+
+ QJsonObject root = doc.object();
+
+ if (requireOrder)
+ {
+ if (root.contains("order"))
+ {
+ out->order = requireInteger(root.value("order"));
+ }
+ else
+ {
+ // FIXME: evaluate if we don't want to throw exceptions here instead
+ qCritical() << filename << "doesn't contain an order field";
+ }
+ }
+
+ out->name = root.value("name").toString();
+ out->fileId = root.value("fileId").toString();
+ out->version = root.value("version").toString();
+ out->dependsOnMinecraftVersion = root.value("mcVersion").toString();
+ out->filename = filename;
+
+ MojangVersionFormat::readVersionProperties(root, out.get());
+
+ // added for legacy Minecraft window embedding, TODO: remove
+ readString(root, "appletClass", out->appletClass);
+
+ if (root.contains("+tweakers"))
+ {
+ for (auto tweakerVal : requireArray(root.value("+tweakers")))
+ {
+ out->addTweakers.append(requireString(tweakerVal));
+ }
+ }
+
+ if (root.contains("+traits"))
+ {
+ for (auto tweakerVal : requireArray(root.value("+traits")))
+ {
+ out->traits.insert(requireString(tweakerVal));
+ }
+ }
+
+ if (root.contains("+jarMods"))
+ {
+ for (auto libVal : requireArray(root.value("+jarMods")))
+ {
+ QJsonObject libObj = requireObject(libVal);
+ // parse the jarmod
+ auto lib = OneSixVersionFormat::jarModFromJson(libObj, filename, out->name);
+ if(lib->originalName.isEmpty())
+ {
+ auto fixed = out->name;
+ fixed.remove(" (jar mod)");
+ lib->originalName = out->name;
+ }
+ // and add to jar mods
+ out->jarMods.append(lib);
+ }
+ }
+
+ auto readLibs = [&](const char * which)
+ {
+ for (auto libVal : requireArray(root.value(which)))
+ {
+ QJsonObject libObj = requireObject(libVal);
+ // parse the library
+ auto lib = libraryFromJson(libObj, filename);
+ out->libraries.append(lib);
+ }
+ };
+ bool hasPlusLibs = root.contains("+libraries");
+ bool hasLibs = root.contains("libraries");
+ if (hasPlusLibs && hasLibs)
+ {
+ out->addProblem(PROBLEM_WARNING, QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported."));
+ readLibs("libraries");
+ readLibs("+libraries");
+ }
+ else if (hasLibs)
+ {
+ readLibs("libraries");
+ }
+ else if(hasPlusLibs)
+ {
+ readLibs("+libraries");
+ }
+
+ /* removed features that shouldn't be used */
+ if (root.contains("tweakers"))
+ {
+ out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element 'tweakers'"));
+ }
+ if (root.contains("-libraries"))
+ {
+ out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element '-libraries'"));
+ }
+ if (root.contains("-tweakers"))
+ {
+ out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element '-tweakers'"));
+ }
+ if (root.contains("-minecraftArguments"))
+ {
+ out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element '-minecraftArguments'"));
+ }
+ if (root.contains("+minecraftArguments"))
+ {
+ out->addProblem(PROBLEM_ERROR, QObject::tr("Version file contains unsupported element '+minecraftArguments'"));
+ }
+ return out;
+}
+
+QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch, bool saveOrder)
+{
+ QJsonObject root;
+ if (saveOrder)
+ {
+ root.insert("order", patch->order);
+ }
+ writeString(root, "name", patch->name);
+ writeString(root, "fileId", patch->fileId);
+ writeString(root, "version", patch->version);
+ writeString(root, "mcVersion", patch->dependsOnMinecraftVersion);
+
+ MojangVersionFormat::writeVersionProperties(patch.get(), root);
+
+ writeString(root, "appletClass", patch->appletClass);
+ writeStringList(root, "+tweakers", patch->addTweakers);
+ writeStringList(root, "+traits", patch->traits.toList());
+ if (!patch->libraries.isEmpty())
+ {
+ QJsonArray array;
+ for (auto value: patch->libraries)
+ {
+ array.append(OneSixVersionFormat::libraryToJson(value.get()));
+ }
+ root.insert("+libraries", array);
+ }
+ if (!patch->jarMods.isEmpty())
+ {
+ QJsonArray array;
+ for (auto value: patch->jarMods)
+ {
+ array.append(OneSixVersionFormat::jarModtoJson(value.get()));
+ }
+ root.insert("+jarMods", array);
+ }
+ // write the contents to a json document.
+ {
+ QJsonDocument out;
+ out.setObject(root);
+ return out;
+ }
+}
+
+JarmodPtr OneSixVersionFormat::jarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName)
+{
+ JarmodPtr out(new Jarmod());
+ if (!libObj.contains("name"))
+ {
+ throw JSONValidationError(filename +
+ "contains a jarmod that doesn't have a 'name' field");
+ }
+ out->name = libObj.value("name").toString();
+ out->originalName = libObj.value("originalName").toString();
+ return out;
+}
+
+QJsonObject OneSixVersionFormat::jarModtoJson(Jarmod *jarmod)
+{
+ QJsonObject out;
+ writeString(out, "name", jarmod->name);
+ if(!jarmod->originalName.isEmpty())
+ {
+ writeString(out, "originalName", jarmod->originalName);
+ }
+ return out;
+}
diff --git a/api/logic/minecraft/onesix/OneSixVersionFormat.h b/api/logic/minecraft/onesix/OneSixVersionFormat.h
new file mode 100644
index 00000000..5696e79e
--- /dev/null
+++ b/api/logic/minecraft/onesix/OneSixVersionFormat.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <minecraft/VersionFile.h>
+#include <minecraft/MinecraftProfile.h>
+#include <minecraft/Library.h>
+#include <QJsonDocument>
+
+class OneSixVersionFormat
+{
+public:
+ // version files / profile patches
+ static VersionFilePtr versionFileFromJson(const QJsonDocument &doc, const QString &filename, const bool requireOrder);
+ static QJsonDocument versionFileToJson(const VersionFilePtr &patch, bool saveOrder);
+
+ // libraries
+ static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename);
+ static QJsonObject libraryToJson(Library *library);
+
+ // jar mods
+ static JarmodPtr jarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName);
+ static QJsonObject jarModtoJson(Jarmod * jarmod);
+};
diff --git a/api/logic/net/ByteArrayDownload.cpp b/api/logic/net/ByteArrayDownload.cpp
new file mode 100644
index 00000000..21990eeb
--- /dev/null
+++ b/api/logic/net/ByteArrayDownload.cpp
@@ -0,0 +1,105 @@
+/* Copyright 2013-2015 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 "ByteArrayDownload.h"
+#include "Env.h"
+#include <QDebug>
+
+ByteArrayDownload::ByteArrayDownload(QUrl url) : NetAction()
+{
+ m_url = url;
+ m_status = Job_NotStarted;
+}
+
+void ByteArrayDownload::start()
+{
+ qDebug() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply.reset(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void ByteArrayDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void ByteArrayDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "Error getting URL:" << m_url.toString().toLocal8Bit()
+ << "Network error: " << error;
+ m_status = Job_Failed;
+ m_errorString = m_reply->errorString();
+}
+
+void ByteArrayDownload::downloadFinished()
+{
+ QVariant redirect = m_reply->header(QNetworkRequest::LocationHeader);
+ QString redirectURL;
+ if(redirect.isValid())
+ {
+ redirectURL = redirect.toString();
+ }
+ // FIXME: This is a hack for https://bugreports.qt-project.org/browse/QTBUG-41061
+ else if(m_reply->hasRawHeader("Location"))
+ {
+ auto data = m_reply->rawHeader("Location");
+ if(data.size() > 2 && data[0] == '/' && data[1] == '/')
+ redirectURL = m_reply->url().scheme() + ":" + data;
+ }
+ if (!redirectURL.isEmpty())
+ {
+ m_url = QUrl(redirect.toString());
+ qDebug() << "Following redirect to " << m_url.toString();
+ start();
+ return;
+ }
+
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ m_data = m_reply->readAll();
+ m_content_type = m_reply->header(QNetworkRequest::ContentTypeHeader).toString();
+ m_reply.reset();
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ // else the download failed
+ else
+ {
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+}
+
+void ByteArrayDownload::downloadReadyRead()
+{
+ // ~_~
+}
diff --git a/api/logic/net/ByteArrayDownload.h b/api/logic/net/ByteArrayDownload.h
new file mode 100644
index 00000000..e2fc2911
--- /dev/null
+++ b/api/logic/net/ByteArrayDownload.h
@@ -0,0 +1,48 @@
+/* Copyright 2013-2015 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 "NetAction.h"
+
+#include "multimc_logic_export.h"
+
+typedef std::shared_ptr<class ByteArrayDownload> ByteArrayDownloadPtr;
+class MULTIMC_LOGIC_EXPORT ByteArrayDownload : public NetAction
+{
+ Q_OBJECT
+public:
+ ByteArrayDownload(QUrl url);
+ static ByteArrayDownloadPtr make(QUrl url)
+ {
+ return ByteArrayDownloadPtr(new ByteArrayDownload(url));
+ }
+ virtual ~ByteArrayDownload() {};
+public:
+ /// if not saving to file, downloaded data is placed here
+ QByteArray m_data;
+
+ QString m_errorString;
+
+public
+slots:
+ virtual void start();
+
+protected
+slots:
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ void downloadError(QNetworkReply::NetworkError error);
+ void downloadFinished();
+ void downloadReadyRead();
+};
diff --git a/api/logic/net/CacheDownload.cpp b/api/logic/net/CacheDownload.cpp
new file mode 100644
index 00000000..1ac55180
--- /dev/null
+++ b/api/logic/net/CacheDownload.cpp
@@ -0,0 +1,192 @@
+/* Copyright 2013-2015 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 "CacheDownload.h"
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QDateTime>
+#include <QDebug>
+#include "Env.h"
+#include <FileSystem.h>
+
+CacheDownload::CacheDownload(QUrl url, MetaEntryPtr entry)
+ : NetAction(), md5sum(QCryptographicHash::Md5)
+{
+ m_url = url;
+ m_entry = entry;
+ m_target_path = entry->getFullPath();
+ m_status = Job_NotStarted;
+}
+
+void CacheDownload::start()
+{
+ m_status = Job_InProgress;
+ if (!m_entry->isStale())
+ {
+ m_status = Job_Finished;
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ // create a new save file
+ m_output_file.reset(new QSaveFile(m_target_path));
+
+ // if there already is a file and md5 checking is in effect and it can be opened
+ if (!FS::ensureFilePathExists(m_target_path))
+ {
+ qCritical() << "Could not create folder for " + m_target_path;
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+ if (!m_output_file->open(QIODevice::WriteOnly))
+ {
+ qCritical() << "Could not open " + m_target_path + " for writing";
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+ qDebug() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+
+ // check file consistency first.
+ QFile current(m_target_path);
+ if(current.exists() && current.size() != 0)
+ {
+ if (m_entry->getRemoteChangedTimestamp().size())
+ request.setRawHeader(QString("If-Modified-Since").toLatin1(),
+ m_entry->getRemoteChangedTimestamp().toLatin1());
+ if (m_entry->getETag().size())
+ request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1());
+ }
+
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)");
+
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply.reset(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void CacheDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void CacheDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "Failed " << m_url.toString() << " with reason " << error;
+ m_status = Job_Failed;
+}
+void CacheDownload::downloadFinished()
+{
+ QVariant redirect = m_reply->header(QNetworkRequest::LocationHeader);
+ QString redirectURL;
+ if(redirect.isValid())
+ {
+ redirectURL = redirect.toString();
+ }
+ // FIXME: This is a hack for https://bugreports.qt-project.org/browse/QTBUG-41061
+ else if(m_reply->hasRawHeader("Location"))
+ {
+ auto data = m_reply->rawHeader("Location");
+ if(data.size() > 2 && data[0] == '/' && data[1] == '/')
+ redirectURL = m_reply->url().scheme() + ":" + data;
+ }
+ if (!redirectURL.isEmpty())
+ {
+ m_url = QUrl(redirect.toString());
+ qDebug() << "Following redirect to " << m_url.toString();
+ start();
+ return;
+ }
+
+ // if the download succeeded
+ if (m_status == Job_Failed)
+ {
+ m_output_file->cancelWriting();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+
+ // if we wrote any data to the save file, we try to commit the data to the real file.
+ if (wroteAnyData)
+ {
+ // nothing went wrong...
+ if (m_output_file->commit())
+ {
+ m_status = Job_Finished;
+ m_entry->setMD5Sum(md5sum.result().toHex().constData());
+ }
+ else
+ {
+ qCritical() << "Failed to commit changes to " << m_target_path;
+ m_output_file->cancelWriting();
+ m_reply.reset();
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ else
+ {
+ m_status = Job_Finished;
+ }
+
+ // then get rid of the save file
+ m_output_file.reset();
+
+ QFileInfo output_file_info(m_target_path);
+
+ m_entry->setETag(m_reply->rawHeader("ETag").constData());
+ if (m_reply->hasRawHeader("Last-Modified"))
+ {
+ m_entry->setRemoteChangedTimestamp(m_reply->rawHeader("Last-Modified").constData());
+ }
+ m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch());
+ m_entry->setStale(false);
+ ENV.metacache()->updateEntry(m_entry);
+
+ m_reply.reset();
+ emit succeeded(m_index_within_job);
+ return;
+}
+
+void CacheDownload::downloadReadyRead()
+{
+ QByteArray ba = m_reply->readAll();
+ md5sum.addData(ba);
+ if (m_output_file->write(ba) != ba.size())
+ {
+ qCritical() << "Failed writing into " + m_target_path;
+ m_status = Job_Failed;
+ m_output_file->cancelWriting();
+ m_output_file.reset();
+ emit failed(m_index_within_job);
+ wroteAnyData = false;
+ return;
+ }
+ wroteAnyData = true;
+}
diff --git a/api/logic/net/CacheDownload.h b/api/logic/net/CacheDownload.h
new file mode 100644
index 00000000..d83b2a0f
--- /dev/null
+++ b/api/logic/net/CacheDownload.h
@@ -0,0 +1,63 @@
+/* Copyright 2013-2015 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 "NetAction.h"
+#include "HttpMetaCache.h"
+#include <QCryptographicHash>
+#include <QSaveFile>
+
+#include "multimc_logic_export.h"
+
+typedef std::shared_ptr<class CacheDownload> CacheDownloadPtr;
+class MULTIMC_LOGIC_EXPORT CacheDownload : public NetAction
+{
+ Q_OBJECT
+private:
+ MetaEntryPtr m_entry;
+ /// if saving to file, use the one specified in this string
+ QString m_target_path;
+
+ /// this is the output file, if any
+ std::unique_ptr<QSaveFile> m_output_file;
+
+ /// the hash-as-you-download
+ QCryptographicHash md5sum;
+
+ bool wroteAnyData = false;
+
+public:
+ explicit CacheDownload(QUrl url, MetaEntryPtr entry);
+ static CacheDownloadPtr make(QUrl url, MetaEntryPtr entry)
+ {
+ return CacheDownloadPtr(new CacheDownload(url, entry));
+ }
+ virtual ~CacheDownload(){};
+ QString getTargetFilepath()
+ {
+ return m_target_path;
+ }
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+public
+slots:
+ virtual void start();
+};
diff --git a/api/logic/net/HttpMetaCache.cpp b/api/logic/net/HttpMetaCache.cpp
new file mode 100644
index 00000000..ea3e2834
--- /dev/null
+++ b/api/logic/net/HttpMetaCache.cpp
@@ -0,0 +1,273 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Env.h"
+#include "HttpMetaCache.h"
+#include "FileSystem.h"
+
+#include <QFileInfo>
+#include <QFile>
+#include <QDateTime>
+#include <QCryptographicHash>
+
+#include <QDebug>
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+
+QString MetaEntry::getFullPath()
+{
+ // FIXME: make local?
+ return FS::PathCombine(basePath, relativePath);
+}
+
+HttpMetaCache::HttpMetaCache(QString path) : QObject()
+{
+ m_index_file = path;
+ saveBatchingTimer.setSingleShot(true);
+ saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer);
+ connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow()));
+}
+
+HttpMetaCache::~HttpMetaCache()
+{
+ saveBatchingTimer.stop();
+ SaveNow();
+}
+
+MetaEntryPtr HttpMetaCache::getEntry(QString base, QString resource_path)
+{
+ // no base. no base path. can't store
+ if (!m_entries.contains(base))
+ {
+ // TODO: log problem
+ return MetaEntryPtr();
+ }
+ EntryMap &map = m_entries[base];
+ if (map.entry_list.contains(resource_path))
+ {
+ return map.entry_list[resource_path];
+ }
+ return MetaEntryPtr();
+}
+
+MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag)
+{
+ auto entry = getEntry(base, resource_path);
+ // it's not present? generate a default stale entry
+ if (!entry)
+ {
+ return staleEntry(base, resource_path);
+ }
+
+ auto &selected_base = m_entries[base];
+ QString real_path = FS::PathCombine(selected_base.base_path, resource_path);
+ QFileInfo finfo(real_path);
+
+ // is the file really there? if not -> stale
+ if (!finfo.isFile() || !finfo.isReadable())
+ {
+ // if the file doesn't exist, we disown the entry
+ selected_base.entry_list.remove(resource_path);
+ return staleEntry(base, resource_path);
+ }
+
+ if (!expected_etag.isEmpty() && expected_etag != entry->etag)
+ {
+ // if the etag doesn't match expected, we disown the entry
+ selected_base.entry_list.remove(resource_path);
+ return staleEntry(base, resource_path);
+ }
+
+ // if the file changed, check md5sum
+ qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch();
+ if (file_last_changed != entry->local_changed_timestamp)
+ {
+ QFile input(real_path);
+ input.open(QIODevice::ReadOnly);
+ QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5)
+ .toHex()
+ .constData();
+ if (entry->md5sum != md5sum)
+ {
+ selected_base.entry_list.remove(resource_path);
+ return staleEntry(base, resource_path);
+ }
+ // md5sums matched... keep entry and save the new state to file
+ entry->local_changed_timestamp = file_last_changed;
+ SaveEventually();
+ }
+
+ // entry passed all the checks we cared about.
+ entry->basePath = getBasePath(base);
+ return entry;
+}
+
+bool HttpMetaCache::updateEntry(MetaEntryPtr stale_entry)
+{
+ if (!m_entries.contains(stale_entry->baseId))
+ {
+ qCritical() << "Cannot add entry with unknown base: "
+ << stale_entry->baseId.toLocal8Bit();
+ return false;
+ }
+ if (stale_entry->stale)
+ {
+ qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit();
+ return false;
+ }
+ m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry;
+ SaveEventually();
+ return true;
+}
+
+bool HttpMetaCache::evictEntry(MetaEntryPtr entry)
+{
+ if(entry)
+ {
+ entry->stale = true;
+ SaveEventually();
+ return true;
+ }
+ return false;
+}
+
+MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path)
+{
+ auto foo = new MetaEntry();
+ foo->baseId = base;
+ foo->basePath = getBasePath(base);
+ foo->relativePath = resource_path;
+ foo->stale = true;
+ return MetaEntryPtr(foo);
+}
+
+void HttpMetaCache::addBase(QString base, QString base_root)
+{
+ // TODO: report error
+ if (m_entries.contains(base))
+ return;
+ // TODO: check if the base path is valid
+ EntryMap foo;
+ foo.base_path = base_root;
+ m_entries[base] = foo;
+}
+
+QString HttpMetaCache::getBasePath(QString base)
+{
+ if (m_entries.contains(base))
+ {
+ return m_entries[base].base_path;
+ }
+ return QString();
+}
+
+void HttpMetaCache::Load()
+{
+ if(m_index_file.isNull())
+ return;
+
+ QFile index(m_index_file);
+ if (!index.open(QIODevice::ReadOnly))
+ return;
+
+ QJsonDocument json = QJsonDocument::fromJson(index.readAll());
+ if (!json.isObject())
+ return;
+ auto root = json.object();
+ // check file version first
+ auto version_val = root.value("version");
+ if (!version_val.isString())
+ return;
+ if (version_val.toString() != "1")
+ return;
+
+ // read the entry array
+ auto entries_val = root.value("entries");
+ if (!entries_val.isArray())
+ return;
+ QJsonArray array = entries_val.toArray();
+ for (auto element : array)
+ {
+ if (!element.isObject())
+ return;
+ auto element_obj = element.toObject();
+ QString base = element_obj.value("base").toString();
+ if (!m_entries.contains(base))
+ continue;
+ auto &entrymap = m_entries[base];
+ auto foo = new MetaEntry();
+ foo->baseId = base;
+ QString path = foo->relativePath = element_obj.value("path").toString();
+ foo->md5sum = element_obj.value("md5sum").toString();
+ foo->etag = element_obj.value("etag").toString();
+ foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble();
+ foo->remote_changed_timestamp =
+ element_obj.value("remote_changed_timestamp").toString();
+ // presumed innocent until closer examination
+ foo->stale = false;
+ entrymap.entry_list[path] = MetaEntryPtr(foo);
+ }
+}
+
+void HttpMetaCache::SaveEventually()
+{
+ // reset the save timer
+ saveBatchingTimer.stop();
+ saveBatchingTimer.start(30000);
+}
+
+void HttpMetaCache::SaveNow()
+{
+ if(m_index_file.isNull())
+ return;
+ QJsonObject toplevel;
+ toplevel.insert("version", QJsonValue(QString("1")));
+ QJsonArray entriesArr;
+ for (auto group : m_entries)
+ {
+ for (auto entry : group.entry_list)
+ {
+ // do not save stale entries. they are dead.
+ if(entry->stale)
+ {
+ continue;
+ }
+ QJsonObject entryObj;
+ entryObj.insert("base", QJsonValue(entry->baseId));
+ entryObj.insert("path", QJsonValue(entry->relativePath));
+ entryObj.insert("md5sum", QJsonValue(entry->md5sum));
+ entryObj.insert("etag", QJsonValue(entry->etag));
+ entryObj.insert("last_changed_timestamp",
+ QJsonValue(double(entry->local_changed_timestamp)));
+ if (!entry->remote_changed_timestamp.isEmpty())
+ entryObj.insert("remote_changed_timestamp",
+ QJsonValue(entry->remote_changed_timestamp));
+ entriesArr.append(entryObj);
+ }
+ }
+ toplevel.insert("entries", entriesArr);
+
+ QJsonDocument doc(toplevel);
+ try
+ {
+ FS::write(m_index_file, doc.toJson());
+ }
+ catch (Exception & e)
+ {
+ qWarning() << e.what();
+ }
+}
diff --git a/api/logic/net/HttpMetaCache.h b/api/logic/net/HttpMetaCache.h
new file mode 100644
index 00000000..7b626c70
--- /dev/null
+++ b/api/logic/net/HttpMetaCache.h
@@ -0,0 +1,125 @@
+/* Copyright 2013-2015 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>
+#include <QMap>
+#include <qtimer.h>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class HttpMetaCache;
+
+class MULTIMC_LOGIC_EXPORT MetaEntry
+{
+friend class HttpMetaCache;
+protected:
+ MetaEntry() {}
+public:
+ bool isStale()
+ {
+ return stale;
+ }
+ void setStale(bool stale)
+ {
+ this->stale = stale;
+ }
+ QString getFullPath();
+ QString getRemoteChangedTimestamp()
+ {
+ return remote_changed_timestamp;
+ }
+ void setRemoteChangedTimestamp(QString remote_changed_timestamp)
+ {
+ this->remote_changed_timestamp = remote_changed_timestamp;
+ }
+ void setLocalChangedTimestamp(qint64 timestamp)
+ {
+ local_changed_timestamp = timestamp;
+ }
+ QString getETag()
+ {
+ return etag;
+ }
+ void setETag(QString etag)
+ {
+ this->etag = etag;
+ }
+ QString getMD5Sum()
+ {
+ return md5sum;
+ }
+ void setMD5Sum(QString md5sum)
+ {
+ this->md5sum = md5sum;
+ }
+protected:
+ QString baseId;
+ QString basePath;
+ QString relativePath;
+ QString md5sum;
+ QString etag;
+ qint64 local_changed_timestamp = 0;
+ QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time
+ bool stale = true;
+};
+
+typedef std::shared_ptr<MetaEntry> MetaEntryPtr;
+
+class MULTIMC_LOGIC_EXPORT HttpMetaCache : public QObject
+{
+ Q_OBJECT
+public:
+ // supply path to the cache index file
+ HttpMetaCache(QString path = QString());
+ ~HttpMetaCache();
+
+ // get the entry solely from the cache
+ // you probably don't want this, unless you have some specific caching needs.
+ MetaEntryPtr getEntry(QString base, QString resource_path);
+
+ // get the entry from cache and verify that it isn't stale (within reason)
+ MetaEntryPtr resolveEntry(QString base, QString resource_path,
+ QString expected_etag = QString());
+
+ // add a previously resolved stale entry
+ bool updateEntry(MetaEntryPtr stale_entry);
+
+ // evict selected entry from cache
+ bool evictEntry(MetaEntryPtr entry);
+
+ void addBase(QString base, QString base_root);
+
+ // (re)start a timer that calls SaveNow later.
+ void SaveEventually();
+ void Load();
+ QString getBasePath(QString base);
+public
+slots:
+ void SaveNow();
+
+private:
+ // create a new stale entry, given the parameters
+ MetaEntryPtr staleEntry(QString base, QString resource_path);
+ struct EntryMap
+ {
+ QString base_path;
+ QMap<QString, MetaEntryPtr> entry_list;
+ };
+ QMap<QString, EntryMap> m_entries;
+ QString m_index_file;
+ QTimer saveBatchingTimer;
+};
diff --git a/api/logic/net/MD5EtagDownload.cpp b/api/logic/net/MD5EtagDownload.cpp
new file mode 100644
index 00000000..3b4d5dcd
--- /dev/null
+++ b/api/logic/net/MD5EtagDownload.cpp
@@ -0,0 +1,155 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Env.h"
+#include "MD5EtagDownload.h"
+#include <FileSystem.h>
+#include <QCryptographicHash>
+#include <QDebug>
+
+MD5EtagDownload::MD5EtagDownload(QUrl url, QString target_path) : NetAction()
+{
+ m_url = url;
+ m_target_path = target_path;
+ m_status = Job_NotStarted;
+}
+
+void MD5EtagDownload::start()
+{
+ QString filename = m_target_path;
+ m_output_file.setFileName(filename);
+ // if there already is a file and md5 checking is in effect and it can be opened
+ if (m_output_file.exists() && m_output_file.open(QIODevice::ReadOnly))
+ {
+ // get the md5 of the local file.
+ m_local_md5 =
+ QCryptographicHash::hash(m_output_file.readAll(), QCryptographicHash::Md5)
+ .toHex()
+ .constData();
+ m_output_file.close();
+ // if we are expecting some md5sum, compare it with the local one
+ if (!m_expected_md5.isEmpty())
+ {
+ // skip if they match
+ if(m_local_md5 == m_expected_md5)
+ {
+ qDebug() << "Skipping " << m_url.toString() << ": md5 match.";
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ }
+ else
+ {
+ // no expected md5. we use the local md5sum as an ETag
+ }
+ }
+ if (!FS::ensureFilePathExists(filename))
+ {
+ emit failed(m_index_within_job);
+ return;
+ }
+
+ QNetworkRequest request(m_url);
+
+ qDebug() << "Downloading " << m_url.toString() << " local MD5: " << m_local_md5;
+
+ if(!m_local_md5.isEmpty())
+ {
+ request.setRawHeader(QString("If-None-Match").toLatin1(), m_local_md5.toLatin1());
+ }
+ if(!m_expected_md5.isEmpty())
+ qDebug() << "Expecting " << m_expected_md5;
+
+ 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(m_index_within_job);
+ return;
+ }
+
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply.reset(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void MD5EtagDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void MD5EtagDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ qCritical() << "Error" << error << ":" << m_reply->errorString() << "while downloading"
+ << m_reply->url();
+ m_status = Job_Failed;
+}
+
+void MD5EtagDownload::downloadFinished()
+{
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ m_output_file.close();
+
+ // FIXME: compare with the real written data md5sum
+ // this is just an ETag
+ qDebug() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData();
+
+ m_reply.reset();
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ // else the download failed
+ else
+ {
+ m_output_file.close();
+ m_output_file.remove();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+}
+
+void MD5EtagDownload::downloadReadyRead()
+{
+ if (!m_output_file.isOpen())
+ {
+ if (!m_output_file.open(QIODevice::WriteOnly))
+ {
+ /*
+ * Can't open the file... the job failed
+ */
+ m_reply->abort();
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ m_output_file.write(m_reply->readAll());
+}
diff --git a/api/logic/net/MD5EtagDownload.h b/api/logic/net/MD5EtagDownload.h
new file mode 100644
index 00000000..cd1cb550
--- /dev/null
+++ b/api/logic/net/MD5EtagDownload.h
@@ -0,0 +1,52 @@
+/* Copyright 2013-2015 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 "NetAction.h"
+#include <QFile>
+
+typedef std::shared_ptr<class MD5EtagDownload> Md5EtagDownloadPtr;
+class MD5EtagDownload : public NetAction
+{
+ Q_OBJECT
+public:
+ /// the expected md5 checksum. Only set from outside
+ QString m_expected_md5;
+ /// the md5 checksum of a file that already exists.
+ QString m_local_md5;
+ /// if saving to file, use the one specified in this string
+ QString m_target_path;
+ /// this is the output file, if any
+ QFile m_output_file;
+
+public:
+ explicit MD5EtagDownload(QUrl url, QString target_path);
+ static Md5EtagDownloadPtr make(QUrl url, QString target_path)
+ {
+ return Md5EtagDownloadPtr(new MD5EtagDownload(url, target_path));
+ }
+ virtual ~MD5EtagDownload(){};
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+public
+slots:
+ virtual void start();
+};
diff --git a/api/logic/net/NetAction.h b/api/logic/net/NetAction.h
new file mode 100644
index 00000000..3c395605
--- /dev/null
+++ b/api/logic/net/NetAction.h
@@ -0,0 +1,96 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QUrl>
+#include <memory>
+#include <QNetworkReply>
+#include <QObjectPtr.h>
+
+#include "multimc_logic_export.h"
+
+enum JobStatus
+{
+ Job_NotStarted,
+ Job_InProgress,
+ Job_Finished,
+ Job_Failed
+};
+
+typedef std::shared_ptr<class NetAction> NetActionPtr;
+class MULTIMC_LOGIC_EXPORT NetAction : public QObject
+{
+ Q_OBJECT
+protected:
+ explicit NetAction() : QObject(0) {};
+
+public:
+ virtual ~NetAction() {};
+
+public:
+ virtual qint64 totalProgress() const
+ {
+ return m_total_progress;
+ }
+ virtual qint64 currentProgress() const
+ {
+ return m_progress;
+ }
+ virtual qint64 numberOfFailures() const
+ {
+ return m_failures;
+ }
+
+public:
+ /// the network reply
+ unique_qobject_ptr<QNetworkReply> m_reply;
+
+ /// the content of the content-type header
+ QString m_content_type;
+
+ /// source URL
+ QUrl m_url;
+
+ /// The file's status
+ JobStatus m_status = Job_NotStarted;
+
+ /// index within the parent job
+ int m_index_within_job = 0;
+
+ qint64 m_progress = 0;
+ qint64 m_total_progress = 1;
+
+ /// number of failures up to this point
+ int m_failures = 0;
+
+signals:
+ void started(int index);
+ void netActionProgress(int index, qint64 current, qint64 total);
+ void succeeded(int index);
+ void failed(int index);
+
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0;
+ virtual void downloadError(QNetworkReply::NetworkError error) = 0;
+ virtual void downloadFinished() = 0;
+ virtual void downloadReadyRead() = 0;
+
+public
+slots:
+ virtual void start() = 0;
+};
diff --git a/api/logic/net/NetJob.cpp b/api/logic/net/NetJob.cpp
new file mode 100644
index 00000000..76c61c35
--- /dev/null
+++ b/api/logic/net/NetJob.cpp
@@ -0,0 +1,125 @@
+/* Copyright 2013-2015 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 "NetJob.h"
+#include "MD5EtagDownload.h"
+#include "ByteArrayDownload.h"
+#include "CacheDownload.h"
+
+#include <QDebug>
+
+void NetJob::partSucceeded(int index)
+{
+ // do progress. all slots are 1 in size at least
+ auto &slot = parts_progress[index];
+ partProgress(index, slot.total_progress, slot.total_progress);
+
+ m_doing.remove(index);
+ m_done.insert(index);
+ downloads[index].get()->disconnect(this);
+ startMoreParts();
+}
+
+void NetJob::partFailed(int index)
+{
+ m_doing.remove(index);
+ auto &slot = parts_progress[index];
+ if (slot.failures == 3)
+ {
+ m_failed.insert(index);
+ }
+ else
+ {
+ slot.failures++;
+ m_todo.enqueue(index);
+ }
+ downloads[index].get()->disconnect(this);
+ startMoreParts();
+}
+
+void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal)
+{
+ auto &slot = parts_progress[index];
+
+ current_progress -= slot.current_progress;
+ slot.current_progress = bytesReceived;
+ current_progress += slot.current_progress;
+
+ total_progress -= slot.total_progress;
+ slot.total_progress = bytesTotal;
+ total_progress += slot.total_progress;
+ setProgress(current_progress, total_progress);
+}
+
+void NetJob::executeTask()
+{
+ qDebug() << m_job_name.toLocal8Bit() << " started.";
+ m_running = true;
+ for (int i = 0; i < downloads.size(); i++)
+ {
+ m_todo.enqueue(i);
+ }
+ // hack that delays early failures so they can be caught easier
+ QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection);
+}
+
+void NetJob::startMoreParts()
+{
+ // check for final conditions if there's nothing in the queue
+ if(!m_todo.size())
+ {
+ if(!m_doing.size())
+ {
+ if(!m_failed.size())
+ {
+ qDebug() << m_job_name << "succeeded.";
+ emitSucceeded();
+ }
+ else
+ {
+ qCritical() << m_job_name << "failed.";
+ emitFailed(tr("Job '%1' failed to process:\n%2").arg(m_job_name).arg(getFailedFiles().join("\n")));
+ }
+ }
+ return;
+ }
+ // otherwise try to start more parts
+ while (m_doing.size() < 6)
+ {
+ if(!m_todo.size())
+ return;
+ int doThis = m_todo.dequeue();
+ m_doing.insert(doThis);
+ auto part = downloads[doThis];
+ // connect signals :D
+ connect(part.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int)));
+ connect(part.get(), SIGNAL(failed(int)), SLOT(partFailed(int)));
+ connect(part.get(), SIGNAL(netActionProgress(int, qint64, qint64)),
+ SLOT(partProgress(int, qint64, qint64)));
+ part->start();
+ }
+}
+
+
+QStringList NetJob::getFailedFiles()
+{
+ QStringList failed;
+ for (auto index: m_failed)
+ {
+ failed.push_back(downloads[index]->m_url.toString());
+ }
+ failed.sort();
+ return failed;
+}
diff --git a/api/logic/net/NetJob.h b/api/logic/net/NetJob.h
new file mode 100644
index 00000000..167fe176
--- /dev/null
+++ b/api/logic/net/NetJob.h
@@ -0,0 +1,117 @@
+/* Copyright 2013-2015 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 "NetAction.h"
+#include "ByteArrayDownload.h"
+#include "MD5EtagDownload.h"
+#include "CacheDownload.h"
+#include "HttpMetaCache.h"
+#include "tasks/Task.h"
+#include "QObjectPtr.h"
+
+#include "multimc_logic_export.h"
+
+class NetJob;
+typedef shared_qobject_ptr<NetJob> NetJobPtr;
+
+class MULTIMC_LOGIC_EXPORT NetJob : public Task
+{
+ Q_OBJECT
+public:
+ explicit NetJob(QString job_name) : Task(), m_job_name(job_name) {}
+ virtual ~NetJob() {}
+ bool addNetAction(NetActionPtr action)
+ {
+ action->m_index_within_job = downloads.size();
+ downloads.append(action);
+ part_info pi;
+ {
+ pi.current_progress = action->currentProgress();
+ pi.total_progress = action->totalProgress();
+ pi.failures = action->numberOfFailures();
+ }
+ parts_progress.append(pi);
+ total_progress += pi.total_progress;
+ // if this is already running, the action needs to be started right away!
+ if (isRunning())
+ {
+ setProgress(current_progress, total_progress);
+ connect(action.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int)));
+ connect(action.get(), SIGNAL(failed(int)), SLOT(partFailed(int)));
+ connect(action.get(), SIGNAL(netActionProgress(int, qint64, qint64)),
+ SLOT(partProgress(int, qint64, qint64)));
+ action->start();
+ }
+ return true;
+ }
+
+ NetActionPtr operator[](int index)
+ {
+ return downloads[index];
+ }
+ const NetActionPtr at(const int index)
+ {
+ return downloads.at(index);
+ }
+ NetActionPtr first()
+ {
+ if (downloads.size())
+ return downloads[0];
+ return NetActionPtr();
+ }
+ int size() const
+ {
+ return downloads.size();
+ }
+ virtual bool isRunning() const
+ {
+ return m_running;
+ }
+ QStringList getFailedFiles();
+
+private slots:
+ void startMoreParts();
+
+public slots:
+ virtual void executeTask();
+ // FIXME: implement
+ virtual bool abort() {return false;};
+
+private slots:
+ void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal);
+ void partSucceeded(int index);
+ void partFailed(int index);
+
+private:
+ struct part_info
+ {
+ qint64 current_progress = 0;
+ qint64 total_progress = 1;
+ int failures = 0;
+ bool connected = false;
+ };
+ QString m_job_name;
+ QList<NetActionPtr> downloads;
+ QList<part_info> parts_progress;
+ QQueue<int> m_todo;
+ QSet<int> m_doing;
+ QSet<int> m_done;
+ QSet<int> m_failed;
+ qint64 current_progress = 0;
+ qint64 total_progress = 0;
+ bool m_running = false;
+};
diff --git a/api/logic/net/PasteUpload.cpp b/api/logic/net/PasteUpload.cpp
new file mode 100644
index 00000000..4b671d6f
--- /dev/null
+++ b/api/logic/net/PasteUpload.cpp
@@ -0,0 +1,99 @@
+#include "PasteUpload.h"
+#include "Env.h"
+#include <QDebug>
+#include <QJsonObject>
+#include <QJsonDocument>
+
+PasteUpload::PasteUpload(QWidget *window, QString text, QString key) : m_window(window)
+{
+ m_key = key;
+ QByteArray temp;
+ temp = text.toUtf8();
+ temp.replace('\n', "\r\n");
+ m_textSize = temp.size();
+ m_text = "key=" + m_key.toLatin1() + "&description=MultiMC5+Log+File&language=plain&format=json&expire=2592000&paste=" + temp.toPercentEncoding();
+ buf = new QBuffer(&m_text);
+}
+
+PasteUpload::~PasteUpload()
+{
+ if(buf)
+ {
+ delete buf;
+ }
+}
+
+bool PasteUpload::validateText()
+{
+ return m_textSize <= maxSize();
+}
+
+void PasteUpload::executeTask()
+{
+ QNetworkRequest request(QUrl("http://paste.ee/api"));
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+
+ request.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
+ request.setRawHeader("Content-Length", QByteArray::number(m_text.size()));
+
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->post(request, buf);
+
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+ setStatus(tr("Uploading to paste.ee"));
+ connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void PasteUpload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "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;
+ }
+ if (!parseResult(doc))
+ {
+ emitFailed(tr("paste.ee returned an error. Please consult the logs for more information"));
+ 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)
+{
+ auto object = doc.object();
+ auto status = object.value("status").toString("error");
+ if (status == "error")
+ {
+ qCritical() << "paste.ee reported error:" << QString(object.value("error").toString());
+ return false;
+ }
+ m_pasteLink = object.value("paste").toObject().value("link").toString();
+ m_pasteID = object.value("paste").toObject().value("id").toString();
+ return true;
+}
+
diff --git a/api/logic/net/PasteUpload.h b/api/logic/net/PasteUpload.h
new file mode 100644
index 00000000..06e3f955
--- /dev/null
+++ b/api/logic/net/PasteUpload.h
@@ -0,0 +1,50 @@
+#pragma once
+#include "tasks/Task.h"
+#include <QNetworkReply>
+#include <QBuffer>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT PasteUpload : public Task
+{
+ Q_OBJECT
+public:
+ PasteUpload(QWidget *window, QString text, QString key = "public");
+ virtual ~PasteUpload();
+ QString pasteLink()
+ {
+ return m_pasteLink;
+ }
+ QString pasteID()
+ {
+ return m_pasteID;
+ }
+ uint32_t maxSize()
+ {
+ // 2MB for paste.ee - public
+ if(m_key == "public")
+ return 1024*1024*2;
+ // 12MB for paste.ee - with actual key
+ return 1024*1024*12;
+ }
+ bool validateText();
+protected:
+ virtual void executeTask();
+
+private:
+ bool parseResult(QJsonDocument doc);
+ QByteArray m_text;
+ QString m_error;
+ QWidget *m_window;
+ QString m_pasteID;
+ QString m_pasteLink;
+ QString m_key;
+ int m_textSize = 0;
+ QBuffer * buf = nullptr;
+ std::shared_ptr<QNetworkReply> m_reply;
+public
+slots:
+ void downloadError(QNetworkReply::NetworkError);
+ void downloadFinished();
+};
diff --git a/api/logic/net/URLConstants.cpp b/api/logic/net/URLConstants.cpp
new file mode 100644
index 00000000..bd476b2c
--- /dev/null
+++ b/api/logic/net/URLConstants.cpp
@@ -0,0 +1,16 @@
+#include "URLConstants.h"
+
+namespace URLConstants {
+
+QString getLegacyJarUrl(QString version)
+{
+ return "http://" + AWS_DOWNLOAD_VERSIONS + getJarPath(version);
+}
+
+QString getJarPath(QString version)
+{
+ return version + "/" + version + ".jar";
+}
+
+
+}
diff --git a/api/logic/net/URLConstants.h b/api/logic/net/URLConstants.h
new file mode 100644
index 00000000..8923ef54
--- /dev/null
+++ b/api/logic/net/URLConstants.h
@@ -0,0 +1,40 @@
+/* Copyright 2013-2015 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_VERSIONS("s3.amazonaws.com/Minecraft.Download/versions/");
+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 SKINS_BASE("crafatar.com/skins/");
+const QString AUTH_BASE("authserver.mojang.com/");
+const QString FORGE_LEGACY_URL("http://files.minecraftforge.net/minecraftforge/json");
+const QString FORGE_GRADLE_URL("http://files.minecraftforge.net/maven/net/minecraftforge/forge/json");
+const QString MOJANG_STATUS_URL("http://status.mojang.com/check");
+const QString MOJANG_STATUS_NEWS_URL("http://status.mojang.com/news");
+const QString LITELOADER_URL("http://dl.liteloader.com/versions/versions.json");
+const QString IMGUR_BASE_URL("https://api.imgur.com/3/");
+const QString FMLLIBS_OUR_BASE_URL("http://files.multimc.org/fmllibs/");
+const QString FMLLIBS_FORGE_BASE_URL("http://files.minecraftforge.net/fmllibs/");
+const QString TRANSLATIONS_BASE_URL("http://files.multimc.org/translations/");
+
+QString getJarPath(QString version);
+QString getLegacyJarUrl(QString version);
+}
diff --git a/api/logic/news/NewsChecker.cpp b/api/logic/news/NewsChecker.cpp
new file mode 100644
index 00000000..be4aa1d1
--- /dev/null
+++ b/api/logic/news/NewsChecker.cpp
@@ -0,0 +1,135 @@
+/* Copyright 2013-2015 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 "NewsChecker.h"
+
+#include <QByteArray>
+#include <QDomDocument>
+
+#include <QDebug>
+
+NewsChecker::NewsChecker(const QString& feedUrl)
+{
+ m_feedUrl = feedUrl;
+}
+
+void NewsChecker::reloadNews()
+{
+ // Start a netjob to download the RSS feed and call rssDownloadFinished() when it's done.
+ if (isLoadingNews())
+ {
+ qDebug() << "Ignored request to reload news. Currently reloading already.";
+ return;
+ }
+
+ qDebug() << "Reloading news.";
+
+ NetJob* job = new NetJob("News RSS Feed");
+ job->addNetAction(ByteArrayDownload::make(m_feedUrl));
+ QObject::connect(job, &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished);
+ QObject::connect(job, &NetJob::failed, this, &NewsChecker::rssDownloadFailed);
+ m_newsNetJob.reset(job);
+ job->start();
+}
+
+void NewsChecker::rssDownloadFinished()
+{
+ // Parse the XML file and process the RSS feed entries.
+ qDebug() << "Finished loading RSS feed.";
+
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(m_newsNetJob->first());
+ data = dl->m_data;
+ m_newsNetJob.reset();
+ }
+
+ QDomDocument doc;
+ {
+ // Stuff to store error info in.
+ QString errorMsg = "Unknown error.";
+ int errorLine = -1;
+ int errorCol = -1;
+
+ // Parse the XML.
+ if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol))
+ {
+ QString fullErrorMsg = QString("Error parsing RSS feed XML. %s at %d:%d.").arg(errorMsg, errorLine, errorCol);
+ fail(fullErrorMsg);
+ return;
+ }
+ }
+
+ // If the parsing succeeded, read it.
+ QDomNodeList items = doc.elementsByTagName("item");
+ m_newsEntries.clear();
+ for (int i = 0; i < items.length(); i++)
+ {
+ QDomElement element = items.at(i).toElement();
+ NewsEntryPtr entry;
+ entry.reset(new NewsEntry());
+ QString errorMsg = "An unknown error occurred.";
+ if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg))
+ {
+ qDebug() << "Loaded news entry" << entry->title;
+ m_newsEntries.append(entry);
+ }
+ else
+ {
+ qWarning() << "Failed to load news entry at index" << i << ":" << errorMsg;
+ }
+ }
+
+ succeed();
+}
+
+void NewsChecker::rssDownloadFailed(QString reason)
+{
+ // Set an error message and fail.
+ fail(tr("Failed to load news RSS feed:\n%1").arg(reason));
+}
+
+
+QList<NewsEntryPtr> NewsChecker::getNewsEntries() const
+{
+ return m_newsEntries;
+}
+
+bool NewsChecker::isLoadingNews() const
+{
+ return m_newsNetJob.get() != nullptr;
+}
+
+QString NewsChecker::getLastLoadErrorMsg() const
+{
+ return m_lastLoadError;
+}
+
+void NewsChecker::succeed()
+{
+ m_lastLoadError = "";
+ qDebug() << "News loading succeeded.";
+ m_newsNetJob.reset();
+ emit newsLoaded();
+}
+
+void NewsChecker::fail(const QString& errorMsg)
+{
+ m_lastLoadError = errorMsg;
+ qDebug() << "Failed to load news:" << errorMsg;
+ m_newsNetJob.reset();
+ emit newsLoadingFailed(errorMsg);
+}
+
diff --git a/api/logic/news/NewsChecker.h b/api/logic/news/NewsChecker.h
new file mode 100644
index 00000000..b8b90728
--- /dev/null
+++ b/api/logic/news/NewsChecker.h
@@ -0,0 +1,107 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QString>
+#include <QList>
+
+#include <net/NetJob.h>
+
+#include "NewsEntry.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT NewsChecker : public QObject
+{
+ Q_OBJECT
+public:
+ /*!
+ * Constructs a news reader to read from the given RSS feed URL.
+ */
+ NewsChecker(const QString& feedUrl);
+
+ /*!
+ * Returns the error message for the last time the news was loaded.
+ * Empty string if the last load was successful.
+ */
+ QString getLastLoadErrorMsg() const;
+
+ /*!
+ * Returns true if the news has been loaded successfully.
+ */
+ bool isNewsLoaded() const;
+
+ //! True if the news is currently loading. If true, reloadNews() will do nothing.
+ bool isLoadingNews() const;
+
+ /*!
+ * Returns a list of news entries.
+ */
+ QList<NewsEntryPtr> getNewsEntries() const;
+
+ /*!
+ * Reloads the news from the website's RSS feed.
+ * If the news is already loading, this does nothing.
+ */
+ void Q_SLOT reloadNews();
+
+signals:
+ /*!
+ * Signal fired after the news has finished loading.
+ */
+ void newsLoaded();
+
+ /*!
+ * Signal fired after the news fails to load.
+ */
+ void newsLoadingFailed(QString errorMsg);
+
+protected slots:
+ void rssDownloadFinished();
+ void rssDownloadFailed(QString reason);
+
+protected:
+ //! The URL for the RSS feed to fetch.
+ QString m_feedUrl;
+
+ //! List of news entries.
+ QList<NewsEntryPtr> m_newsEntries;
+
+ //! The network job to use to load the news.
+ NetJobPtr m_newsNetJob;
+
+ //! True if news has been loaded.
+ bool m_loadedNews;
+
+ /*!
+ * Gets the error message that was given last time the news was loaded.
+ * If the last news load succeeded, this will be an empty string.
+ */
+ QString m_lastLoadError;
+
+
+ /*!
+ * Emits newsLoaded() and sets m_lastLoadError to empty string.
+ */
+ void Q_SLOT succeed();
+
+ /*!
+ * Emits newsLoadingFailed() and sets m_lastLoadError to the given message.
+ */
+ void Q_SLOT fail(const QString& errorMsg);
+};
+
diff --git a/api/logic/news/NewsEntry.cpp b/api/logic/news/NewsEntry.cpp
new file mode 100644
index 00000000..79abbaa3
--- /dev/null
+++ b/api/logic/news/NewsEntry.cpp
@@ -0,0 +1,77 @@
+/* Copyright 2013-2015 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 "NewsEntry.h"
+
+#include <QDomNodeList>
+#include <QVariant>
+
+NewsEntry::NewsEntry(QObject* parent) :
+ QObject(parent)
+{
+ this->title = tr("Untitled");
+ this->content = tr("No content.");
+ this->link = "";
+ this->author = tr("Unknown Author");
+ this->pubDate = QDateTime::currentDateTime();
+}
+
+NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent) :
+ QObject(parent)
+{
+ this->title = title;
+ this->content = content;
+ this->link = link;
+ this->author = author;
+ this->pubDate = pubDate;
+}
+
+/*!
+ * Gets the text content of the given child element as a QVariant.
+ */
+inline QString childValue(const QDomElement& element, const QString& childName, QString defaultVal="")
+{
+ QDomNodeList nodes = element.elementsByTagName(childName);
+ if (nodes.count() > 0)
+ {
+ QDomElement element = nodes.at(0).toElement();
+ return element.text();
+ }
+ else
+ {
+ return defaultVal;
+ }
+}
+
+bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg)
+{
+ QString title = childValue(element, "title", tr("Untitled"));
+ QString content = childValue(element, "description", tr("No content."));
+ QString link = childValue(element, "link");
+ QString author = childValue(element, "dc:creator", tr("Unknown Author"));
+ QString pubDateStr = childValue(element, "pubDate");
+
+ // FIXME: For now, we're just ignoring timezones. We assume that all time zones in the RSS feed are the same.
+ QString dateFormat("ddd, dd MMM yyyy hh:mm:ss");
+ QDateTime pubDate = QDateTime::fromString(pubDateStr, dateFormat);
+
+ entry->title = title;
+ entry->content = content;
+ entry->link = link;
+ entry->author = author;
+ entry->pubDate = pubDate;
+ return true;
+}
+
diff --git a/api/logic/news/NewsEntry.h b/api/logic/news/NewsEntry.h
new file mode 100644
index 00000000..adb79e8f
--- /dev/null
+++ b/api/logic/news/NewsEntry.h
@@ -0,0 +1,65 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QString>
+#include <QDomElement>
+#include <QDateTime>
+
+#include <memory>
+
+class NewsEntry : public QObject
+{
+ Q_OBJECT
+
+public:
+ /*!
+ * Constructs an empty news entry.
+ */
+ explicit NewsEntry(QObject* parent=0);
+
+ /*!
+ * Constructs a new news entry.
+ * Note that content may contain HTML.
+ */
+ NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent=0);
+
+ /*!
+ * Attempts to load information from the given XML element into the given news entry pointer.
+ * If this fails, the function will return false and store an error message in the errorMsg pointer.
+ */
+ static bool fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg=0);
+
+
+ //! The post title.
+ QString title;
+
+ //! The post's content. May contain HTML.
+ QString content;
+
+ //! URL to the post.
+ QString link;
+
+ //! The post's author.
+ QString author;
+
+ //! The date and time that this post was published.
+ QDateTime pubDate;
+};
+
+typedef std::shared_ptr<NewsEntry> NewsEntryPtr;
+
diff --git a/api/logic/notifications/NotificationChecker.cpp b/api/logic/notifications/NotificationChecker.cpp
new file mode 100644
index 00000000..ab2570b7
--- /dev/null
+++ b/api/logic/notifications/NotificationChecker.cpp
@@ -0,0 +1,130 @@
+#include "NotificationChecker.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QDebug>
+
+#include "Env.h"
+#include "net/CacheDownload.h"
+
+
+NotificationChecker::NotificationChecker(QObject *parent)
+ : QObject(parent)
+{
+}
+
+void NotificationChecker::setNotificationsUrl(const QUrl &notificationsUrl)
+{
+ m_notificationsUrl = notificationsUrl;
+}
+
+void NotificationChecker::setApplicationChannel(QString channel)
+{
+ m_appVersionChannel = channel;
+}
+
+void NotificationChecker::setApplicationFullVersion(QString version)
+{
+ m_appFullVersion = version;
+}
+
+void NotificationChecker::setApplicationPlatform(QString platform)
+{
+ m_appPlatform = platform;
+}
+
+QList<NotificationChecker::NotificationEntry> NotificationChecker::notificationEntries() const
+{
+ return m_entries;
+}
+
+void NotificationChecker::checkForNotifications()
+{
+ if (!m_notificationsUrl.isValid())
+ {
+ qCritical() << "Failed to check for notifications. No notifications URL set."
+ << "If you'd like to use MultiMC's notification system, please pass the "
+ "URL to CMake at compile time.";
+ return;
+ }
+ if (m_checkJob)
+ {
+ return;
+ }
+ m_checkJob.reset(new NetJob("Checking for notifications"));
+ auto entry = ENV.metacache()->resolveEntry("root", "notifications.json");
+ entry->setStale(true);
+ m_checkJob->addNetAction(m_download = CacheDownload::make(m_notificationsUrl, entry));
+ connect(m_download.get(), &CacheDownload::succeeded, this,
+ &NotificationChecker::downloadSucceeded);
+ m_checkJob->start();
+}
+
+void NotificationChecker::downloadSucceeded(int)
+{
+ m_entries.clear();
+
+ QFile file(m_download->getTargetFilepath());
+ if (file.open(QFile::ReadOnly))
+ {
+ QJsonArray root = QJsonDocument::fromJson(file.readAll()).array();
+ for (auto it = root.begin(); it != root.end(); ++it)
+ {
+ QJsonObject obj = (*it).toObject();
+ NotificationEntry entry;
+ entry.id = obj.value("id").toDouble();
+ entry.message = obj.value("message").toString();
+ entry.channel = obj.value("channel").toString();
+ entry.platform = obj.value("platform").toString();
+ entry.from = obj.value("from").toString();
+ entry.to = obj.value("to").toString();
+ const QString type = obj.value("type").toString("critical");
+ if (type == "critical")
+ {
+ entry.type = NotificationEntry::Critical;
+ }
+ else if (type == "warning")
+ {
+ entry.type = NotificationEntry::Warning;
+ }
+ else if (type == "information")
+ {
+ entry.type = NotificationEntry::Information;
+ }
+ if(entryApplies(entry))
+ m_entries.append(entry);
+ }
+ }
+
+ m_checkJob.reset();
+
+ emit notificationCheckFinished();
+}
+
+bool versionLessThan(const QString &v1, const QString &v2)
+{
+ QStringList l1 = v1.split('.');
+ QStringList l2 = v2.split('.');
+ while (!l1.isEmpty() && !l2.isEmpty())
+ {
+ int one = l1.isEmpty() ? 0 : l1.takeFirst().toInt();
+ int two = l2.isEmpty() ? 0 : l2.takeFirst().toInt();
+ if (one != two)
+ {
+ return one < two;
+ }
+ }
+ return false;
+}
+
+bool NotificationChecker::entryApplies(const NotificationChecker::NotificationEntry& entry) const
+{
+ bool channelApplies = entry.channel.isEmpty() || entry.channel == m_appVersionChannel;
+ bool platformApplies = entry.platform.isEmpty() || entry.platform == m_appPlatform;
+ bool fromApplies =
+ entry.from.isEmpty() || entry.from == m_appFullVersion || !versionLessThan(m_appFullVersion, entry.from);
+ bool toApplies =
+ entry.to.isEmpty() || entry.to == m_appFullVersion || !versionLessThan(entry.to, m_appFullVersion);
+ return channelApplies && platformApplies && fromApplies && toApplies;
+}
diff --git a/api/logic/notifications/NotificationChecker.h b/api/logic/notifications/NotificationChecker.h
new file mode 100644
index 00000000..a2d92ab9
--- /dev/null
+++ b/api/logic/notifications/NotificationChecker.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <QObject>
+
+#include "net/NetJob.h"
+#include "net/CacheDownload.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT NotificationChecker : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit NotificationChecker(QObject *parent = 0);
+
+ void setNotificationsUrl(const QUrl &notificationsUrl);
+ void setApplicationPlatform(QString platform);
+ void setApplicationChannel(QString channel);
+ void setApplicationFullVersion(QString version);
+
+ struct NotificationEntry
+ {
+ int id;
+ QString message;
+ enum
+ {
+ Critical,
+ Warning,
+ Information
+ } type;
+ QString channel;
+ QString platform;
+ QString from;
+ QString to;
+ };
+
+ QList<NotificationEntry> notificationEntries() const;
+
+public
+slots:
+ void checkForNotifications();
+
+private
+slots:
+ void downloadSucceeded(int);
+
+signals:
+ void notificationCheckFinished();
+
+private:
+ bool entryApplies(const NotificationEntry &entry) const;
+
+private:
+ QList<NotificationEntry> m_entries;
+ QUrl m_notificationsUrl;
+ NetJobPtr m_checkJob;
+ CacheDownloadPtr m_download;
+
+ QString m_appVersionChannel;
+ QString m_appPlatform;
+ QString m_appFullVersion;
+};
diff --git a/api/logic/pathmatcher/FSTreeMatcher.h b/api/logic/pathmatcher/FSTreeMatcher.h
new file mode 100644
index 00000000..a5bed57c
--- /dev/null
+++ b/api/logic/pathmatcher/FSTreeMatcher.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "IPathMatcher.h"
+#include <SeparatorPrefixTree.h>
+#include <QRegularExpression>
+
+class FSTreeMatcher : public IPathMatcher
+{
+public:
+ virtual ~FSTreeMatcher() {};
+ FSTreeMatcher(SeparatorPrefixTree<'/'> & tree) : m_fsTree(tree)
+ {
+ }
+
+ virtual bool matches(const QString &string) const override
+ {
+ return m_fsTree.covers(string);
+ }
+
+ SeparatorPrefixTree<'/'> & m_fsTree;
+};
diff --git a/api/logic/pathmatcher/IPathMatcher.h b/api/logic/pathmatcher/IPathMatcher.h
new file mode 100644
index 00000000..1d410947
--- /dev/null
+++ b/api/logic/pathmatcher/IPathMatcher.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <memory>
+
+class IPathMatcher
+{
+public:
+ typedef std::shared_ptr<IPathMatcher> Ptr;
+
+public:
+ virtual ~IPathMatcher(){};
+ virtual bool matches(const QString &string) const = 0;
+};
diff --git a/api/logic/pathmatcher/MultiMatcher.h b/api/logic/pathmatcher/MultiMatcher.h
new file mode 100644
index 00000000..91f70aa4
--- /dev/null
+++ b/api/logic/pathmatcher/MultiMatcher.h
@@ -0,0 +1,31 @@
+#include "IPathMatcher.h"
+#include <SeparatorPrefixTree.h>
+#include <QRegularExpression>
+
+class MultiMatcher : public IPathMatcher
+{
+public:
+ virtual ~MultiMatcher() {};
+ MultiMatcher()
+ {
+ }
+ MultiMatcher &add(Ptr add)
+ {
+ m_matchers.append(add);
+ return *this;
+ }
+
+ virtual bool matches(const QString &string) const override
+ {
+ for(auto iter: m_matchers)
+ {
+ if(iter->matches(string))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ QList<Ptr> m_matchers;
+};
diff --git a/api/logic/pathmatcher/RegexpMatcher.h b/api/logic/pathmatcher/RegexpMatcher.h
new file mode 100644
index 00000000..da552123
--- /dev/null
+++ b/api/logic/pathmatcher/RegexpMatcher.h
@@ -0,0 +1,42 @@
+#include "IPathMatcher.h"
+#include <QRegularExpression>
+
+class RegexpMatcher : public IPathMatcher
+{
+public:
+ virtual ~RegexpMatcher() {};
+ RegexpMatcher(const QString &regexp)
+ {
+ m_regexp.setPattern(regexp);
+ m_onlyFilenamePart = !regexp.contains('/');
+ }
+
+ RegexpMatcher &caseSensitive(bool cs = true)
+ {
+ if(cs)
+ {
+ m_regexp.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
+ }
+ else
+ {
+ m_regexp.setPatternOptions(QRegularExpression::NoPatternOption);
+ }
+ return *this;
+ }
+
+ virtual bool matches(const QString &string) const override
+ {
+ if(m_onlyFilenamePart)
+ {
+ auto slash = string.lastIndexOf('/');
+ if(slash != -1)
+ {
+ auto part = string.mid(slash + 1);
+ return m_regexp.match(part).hasMatch();
+ }
+ }
+ return m_regexp.match(string).hasMatch();
+ }
+ QRegularExpression m_regexp;
+ bool m_onlyFilenamePart = false;
+};
diff --git a/api/logic/resources/Resource.cpp b/api/logic/resources/Resource.cpp
new file mode 100644
index 00000000..e95675d7
--- /dev/null
+++ b/api/logic/resources/Resource.cpp
@@ -0,0 +1,155 @@
+#include "Resource.h"
+
+#include <QDebug>
+
+#include "ResourceObserver.h"
+#include "ResourceHandler.h"
+
+// definition of static members of Resource
+QMap<QString, std::function<std::shared_ptr<ResourceHandler>(const QString &)>> Resource::m_handlers;
+QMap<QPair<int, int>, std::function<QVariant(QVariant)>> Resource::m_transfomers;
+QMap<QString, std::weak_ptr<Resource>> Resource::m_resources;
+
+struct NullResourceResult {};
+Q_DECLARE_METATYPE(NullResourceResult)
+class NullResourceHandler : public ResourceHandler
+{
+public:
+ explicit NullResourceHandler()
+ {
+ setResult(QVariant::fromValue<NullResourceResult>(NullResourceResult()));
+ }
+};
+
+Resource::Resource(const QString &resource)
+ : m_resource(resource)
+{
+ if (!resource.isEmpty())
+ {
+ // a valid resource identifier has the format <id>:<data>
+ Q_ASSERT(resource.contains(':'));
+ // "parse" the resource identifier into id and data
+ const QString resourceId = resource.left(resource.indexOf(':'));
+ const QString resourceData = resource.mid(resource.indexOf(':') + 1);
+
+ // create and set up the handler
+ Q_ASSERT(m_handlers.contains(resourceId));
+ m_handler = m_handlers.value(resourceId)(resourceData);
+ }
+ else
+ {
+ m_handler = std::make_shared<NullResourceHandler>();
+ }
+
+ Q_ASSERT(m_handler);
+ m_handler->init(m_handler);
+ m_handler->setResource(this);
+}
+Resource::~Resource()
+{
+ qDeleteAll(m_observers);
+}
+
+Resource::Ptr Resource::create(const QString &resource, Ptr placeholder)
+{
+ const QString storageId = storageIdentifier(resource, placeholder);
+
+ // do we already have a resource? even if m_resources contains it it might not be valid any longer (weak_ptr)
+ Resource::Ptr ptr = m_resources.contains(storageId)
+ ? m_resources.value(storageId).lock()
+ : nullptr;
+ // did we have one? and is it still valid?
+ if (!ptr)
+ {
+ /* We don't want Resource to have a public constructor, but std::make_shared needs it,
+ * so we create a subclass of Resource here that exposes the constructor as public.
+ * The alternative would be making the allocator for std::make_shared a friend, but it
+ * differs between different STL implementations, so that would be a pain.
+ */
+ struct ConstructableResource : public Resource
+ {
+ explicit ConstructableResource(const QString &resource)
+ : Resource(resource) {}
+ };
+ ptr = std::make_shared<ConstructableResource>(resource);
+ ptr->m_placeholder = placeholder;
+ m_resources.insert(storageId, ptr);
+ }
+ return ptr;
+}
+
+Resource::Ptr Resource::applyTo(ResourceObserver *observer)
+{
+ m_observers.append(observer);
+ observer->setSource(shared_from_this()); // give the observer a shared_ptr for us so we don't get deleted
+ observer->resourceUpdated(); // ask the observer to poll us immediently, we might already have data
+ return shared_from_this(); // allow chaining
+}
+Resource::Ptr Resource::applyTo(QObject *target, const char *property)
+{
+ // the cast to ResourceObserver* is required to ensure the right overload gets choosen,
+ // since QObjectResourceObserver also inherits from QObject
+ return applyTo(static_cast<ResourceObserver *>(new QObjectResourceObserver(target, property)));
+}
+
+QVariant Resource::getResourceInternal(const int typeId) const
+{
+ // no result (yet), but a placeholder? delegate to the placeholder.
+ if (m_handler->result().isNull() && m_placeholder)
+ {
+ return m_placeholder->getResourceInternal(typeId);
+ }
+ const QVariant variant = m_handler->result();
+ const auto typePair = qMakePair(int(variant.type()), typeId);
+
+ // do we have an explicit transformer? use it.
+ if (m_transfomers.contains(typePair))
+ {
+ return m_transfomers.value(typePair)(variant);
+ }
+ else
+ {
+ // we do not have an explicit transformer, so we just pass the QVariant, which will automatically
+ // transform some types for us (different numbers to each other etc.)
+ return variant;
+ }
+}
+
+void Resource::reportResult()
+{
+ for (ResourceObserver *observer : m_observers)
+ {
+ observer->resourceUpdated();
+ }
+}
+void Resource::reportFailure(const QString &reason)
+{
+ for (ResourceObserver *observer : m_observers)
+ {
+ observer->setFailure(reason);
+ }
+}
+void Resource::reportProgress(const int progress)
+{
+ for (ResourceObserver *observer : m_observers)
+ {
+ observer->setProgress(progress);
+ }
+}
+
+void Resource::notifyObserverDeleted(ResourceObserver *observer)
+{
+ m_observers.removeAll(observer);
+}
+
+QString Resource::storageIdentifier(const QString &id, Resource::Ptr placeholder)
+{
+ if (placeholder)
+ {
+ return id + '#' + storageIdentifier(placeholder->m_resource, placeholder->m_placeholder);
+ }
+ else
+ {
+ return id;
+ }
+}
diff --git a/api/logic/resources/Resource.h b/api/logic/resources/Resource.h
new file mode 100644
index 00000000..63e97b88
--- /dev/null
+++ b/api/logic/resources/Resource.h
@@ -0,0 +1,132 @@
+#pragma once
+
+#include <QString>
+#include <QMap>
+#include <QVariant>
+#include <functional>
+#include <memory>
+
+#include "ResourceObserver.h"
+#include "TypeMagic.h"
+
+#include "multimc_logic_export.h"
+
+class ResourceHandler;
+
+/** Frontend class for resources
+ *
+ * Usage:
+ * Resource::create("icon:noaccount")->applyTo(accountsAction);
+ * Resource::create("web:http://asdf.com/image.png")->applyTo(imageLbl)->placeholder(Resource::create("icon:loading"));
+ *
+ * Memory management:
+ * Resource caches ResourcePtrs using weak pointers, so while a resource is still existing
+ * when a new resource is created the resources will be the same (including the same handler).
+ *
+ * ResourceObservers keep a shared pointer to the resource, as does the Resource itself to it's
+ * placeholder (if present). This means a resource stays valid while it's still used ("applied to" etc.)
+ * by something. When nothing uses it anymore it gets deleted.
+ *
+ * @note Always pass resource around using Resource::Ptr! Copy and move constructors are disabled for a reason.
+ */
+class MULTIMC_LOGIC_EXPORT Resource : public std::enable_shared_from_this<Resource>
+{
+ // only allow creation from Resource::create and disallow passing around non-pointers
+ explicit Resource(const QString &resource);
+ Resource(const Resource &) = delete;
+ Resource(Resource &&) = delete;
+public:
+ using Ptr = std::shared_ptr<Resource>;
+
+ ~Resource();
+
+ /// The returned pointer needs to be stored until either Resource::applyTo or Resource::then is called, or it is passed as
+ /// a placeholder to Resource::create itself.
+ static Ptr create(const QString &resource, Ptr placeholder = nullptr);
+
+ /// Use these functions to specify what should happen when e.g. the resource changes
+ Ptr applyTo(ResourceObserver *observer);
+ Ptr applyTo(QObject *target, const char *property = nullptr);
+ template<typename Func>
+ Ptr then(Func &&func)
+ {
+ // Arg will be the functions argument with references and cv-qualifiers (const, volatile) removed
+ using Arg = TypeMagic::CleanType<typename TypeMagic::Function<Func>::Argument>;
+ // Ret will be the functions return type
+ using Ret = typename TypeMagic::Function<Func>::ReturnType;
+
+ // FunctionResourceObserver<ReturnType, ArgumentType, FunctionSignature>
+ return applyTo(new FunctionResourceObserver<Ret, Arg, Func>(std::forward<Func>(func)));
+ }
+
+ /// Retrieve the currently active resource. If it's type is different from T a conversion will be attempted.
+ template<typename T>
+ T getResource() const { return getResourceInternal(qMetaTypeId<T>()).template value<T>(); }
+
+ /// @internal Used by ResourceObserver and ResourceProxyModel
+ QVariant getResourceInternal(const int typeId) const;
+
+ /** Register a new ResourceHandler. T needs to inherit from ResourceHandler
+ * Usage: Resource::registerHandler<MyResourceHandler>("myid");
+ */
+ template<typename T>
+ static void registerHandler(const QString &id)
+ {
+ m_handlers.insert(id, [](const QString &res) { return std::make_shared<T>(res); });
+ }
+ /** Register a new resource transformer
+ * Resource transformers are functions that are responsible for converting between different types,
+ * for example converting from a QByteArray to a QPixmap. They are registered "externally" because not
+ * all types might be available in this library, for example gui types like QPixmap.
+ *
+ * Usage: Resource::registerTransformer([](const InputType &type) { return OutputType(type); });
+ * This assumes that OutputType has a constructor that takes InputType as an argument. More
+ * complicated transformers can of course also be registered.
+ *
+ * When a ResourceObserver requests a type that's different from the actual resource type, a matching
+ * transformer will be looked up from the list of transformers.
+ * @note Only one-stage transforms will be performed (you can't registerTransformers for QString => int
+ * and int => float and expect QString to automatically be transformed into a float.
+ */
+ template<typename Func>
+ static void registerTransformer(Func &&func)
+ {
+ using Out = typename TypeMagic::Function<Func>::ReturnType;
+ using In = TypeMagic::CleanType<typename TypeMagic::Function<Func>::Argument>;
+ static_assert(!std::is_same<Out, In>::value, "It does not make sense to transform a value to itself");
+ m_transfomers.insert(qMakePair(qMetaTypeId<In>(), qMetaTypeId<Out>()), [func](const QVariant &in)
+ {
+ return QVariant::fromValue<Out>(func(in.value<In>()));
+ });
+ }
+
+private: // half private, implementation details
+ friend class ResourceHandler;
+ // the following three functions are called by ResourceHandlers
+ /** Notifies the observers. They will call Resource::getResourceInternal which will call ResourceHandler::result
+ * or delegate to it's placeholder.
+ */
+ void reportResult();
+ void reportFailure(const QString &reason);
+ void reportProgress(const int progress);
+
+ friend class ResourceObserver;
+ /// Removes observer from the list of observers so that we don't attempt to notify something that doesn't exist
+ void notifyObserverDeleted(ResourceObserver *observer);
+
+private: // truly private
+ QList<ResourceObserver *> m_observers;
+ std::shared_ptr<ResourceHandler> m_handler = nullptr;
+ Ptr m_placeholder = nullptr;
+ const QString m_resource;
+
+ static QString storageIdentifier(const QString &id, Ptr placeholder = nullptr);
+ QString storageIdentifier() const;
+
+ // a list of resource handler factories, registered using registerHandler
+ static QMap<QString, std::function<std::shared_ptr<ResourceHandler>(const QString &)>> m_handlers;
+ // a list of resource transformers, registered using registerTransformer
+ static QMap<QPair<int, int>, std::function<QVariant(QVariant)>> m_transfomers;
+ // a list of resources so that we can reuse them
+ static QMap<QString, std::weak_ptr<Resource>> m_resources;
+};
diff --git a/api/logic/resources/ResourceHandler.cpp b/api/logic/resources/ResourceHandler.cpp
new file mode 100644
index 00000000..46a4422c
--- /dev/null
+++ b/api/logic/resources/ResourceHandler.cpp
@@ -0,0 +1,28 @@
+#include "ResourceHandler.h"
+
+#include "Resource.h"
+
+void ResourceHandler::setResult(const QVariant &result)
+{
+ m_result = result;
+ if (m_resource)
+ {
+ m_resource->reportResult();
+ }
+}
+
+void ResourceHandler::setFailure(const QString &reason)
+{
+ if (m_resource)
+ {
+ m_resource->reportFailure(reason);
+ }
+}
+
+void ResourceHandler::setProgress(const int progress)
+{
+ if (m_resource)
+ {
+ m_resource->reportProgress(progress);
+ }
+}
diff --git a/api/logic/resources/ResourceHandler.h b/api/logic/resources/ResourceHandler.h
new file mode 100644
index 00000000..f09d8904
--- /dev/null
+++ b/api/logic/resources/ResourceHandler.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <QVariant>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class Resource;
+
+/** Base class for things that can retrieve a resource.
+ *
+ * Subclass, provide a constructor that takes a single QString as argument, and
+ * call Resource::registerHandler<MyResourceHandler>("<id>"), where <id> is the
+ * prefix of the resource ("web", "icon", etc.)
+ */
+class MULTIMC_LOGIC_EXPORT ResourceHandler
+{
+public:
+ virtual ~ResourceHandler() {}
+
+ void setResource(Resource *resource) { m_resource = resource; }
+ /// reimplement this if you need to do something after you have been put in a shared pointer
+ // we do this instead of inheriting from std::enable_shared_from_this
+ virtual void init(std::shared_ptr<ResourceHandler>&) {}
+
+ QVariant result() const { return m_result; }
+
+protected: // use these methods to notify the resource of changes
+ void setResult(const QVariant &result);
+ void setFailure(const QString &reason);
+ void setProgress(const int progress);
+
+private:
+ QVariant m_result;
+ Resource *m_resource = nullptr;
+};
diff --git a/api/logic/resources/ResourceObserver.cpp b/api/logic/resources/ResourceObserver.cpp
new file mode 100644
index 00000000..4f168fd2
--- /dev/null
+++ b/api/logic/resources/ResourceObserver.cpp
@@ -0,0 +1,55 @@
+#include "ResourceObserver.h"
+
+#include <QDebug>
+
+#include "Resource.h"
+
+static const char *defaultPropertyForTarget(QObject *target)
+{
+ if (target->inherits("QLabel"))
+ {
+ return "pixmap";
+ }
+ else if (target->inherits("QAction") ||
+ target->inherits("QMenu") ||
+ target->inherits("QAbstractButton"))
+ {
+ return "icon";
+ }
+ // for unit tests
+ else if (target->inherits("DummyObserverObject"))
+ {
+ return "property";
+ }
+ else
+ {
+ Q_ASSERT_X(false, "ResourceObserver.cpp: defaultPropertyForTarget", "Unrecognized QObject subclass");
+ return nullptr;
+ }
+}
+
+QObjectResourceObserver::QObjectResourceObserver(QObject *target, const char *property)
+ : QObject(target), m_target(target)
+{
+ const QMetaObject *mo = m_target->metaObject();
+ m_property = mo->property(mo->indexOfProperty(
+ property ?
+ property
+ : defaultPropertyForTarget(target)));
+}
+void QObjectResourceObserver::resourceUpdated()
+{
+ m_property.write(m_target, getInternal(m_property.type()));
+}
+
+
+ResourceObserver::~ResourceObserver()
+{
+ m_resource->notifyObserverDeleted(this);
+}
+
+QVariant ResourceObserver::getInternal(const int typeId) const
+{
+ Q_ASSERT(m_resource);
+ return m_resource->getResourceInternal(typeId);
+}
diff --git a/api/logic/resources/ResourceObserver.h b/api/logic/resources/ResourceObserver.h
new file mode 100644
index 00000000..c42e41ba
--- /dev/null
+++ b/api/logic/resources/ResourceObserver.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include <memory>
+#include <functional>
+
+#include <QObject>
+#include <QMetaProperty>
+#include "multimc_logic_export.h"
+
+class QVariant;
+class Resource;
+
+/// Base class for things that can use a resource
+class MULTIMC_LOGIC_EXPORT ResourceObserver
+{
+public:
+ virtual ~ResourceObserver();
+
+protected: // these methods are called by the Resource when something changes
+ virtual void resourceUpdated() = 0;
+ virtual void setFailure(const QString &) {}
+ virtual void setProgress(const int) {}
+
+private:
+ friend class Resource;
+ void setSource(std::shared_ptr<Resource> resource) { m_resource = resource; }
+
+protected:
+ template<typename T>
+ T get() const { return getInternal(qMetaTypeId<T>()).template value<T>(); }
+ QVariant getInternal(const int typeId) const;
+
+private:
+ std::shared_ptr<Resource> m_resource;
+};
+
+/** Observer for QObject properties
+ *
+ * Give it a target and the name of a property, and that property will be set when the resource changes.
+ *
+ * If no name is given an attempt to find a default property for some common classes is done.
+ */
+class MULTIMC_LOGIC_EXPORT QObjectResourceObserver : public QObject, public ResourceObserver
+{
+public:
+ explicit QObjectResourceObserver(QObject *target, const char *property = nullptr);
+
+ void resourceUpdated() override;
+
+private:
+ QObject *m_target;
+ QMetaProperty m_property;
+};
+
+/** Observer for functions, lambdas etc.
+ * Template arguments:
+ * * We need Ret and Arg in order to create the std::function
+ * * We need Func in order to std::forward the function
+ */
+template <typename Ret, typename Arg, typename Func>
+class FunctionResourceObserver : public ResourceObserver
+{
+ std::function<Ret(Arg)> m_function;
+public:
+ template <typename T>
+ explicit FunctionResourceObserver(T &&func)
+ : m_function(std::forward<Func>(func)) {}
+
+ void resourceUpdated() override
+ {
+ m_function(get<Arg>());
+ }
+};
diff --git a/api/logic/resources/ResourceProxyModel.cpp b/api/logic/resources/ResourceProxyModel.cpp
new file mode 100644
index 00000000..f026d9a9
--- /dev/null
+++ b/api/logic/resources/ResourceProxyModel.cpp
@@ -0,0 +1,89 @@
+#include "ResourceProxyModel.h"
+
+#include <QItemSelectionRange>
+
+#include "Resource.h"
+#include "ResourceObserver.h"
+
+class ModelResourceObserver : public ResourceObserver
+{
+public:
+ explicit ModelResourceObserver(const QModelIndex &index, const int role)
+ : m_index(index), m_role(role)
+ {
+ qRegisterMetaType<QVector<int>>("QVector<int>");
+ }
+
+ void resourceUpdated() override
+ {
+ if (m_index.isValid())
+ {
+ // the resource changed, pretend to be the model and notify the views of the update. they will re-poll the model which will return the new resource value
+ QMetaObject::invokeMethod(const_cast<QAbstractItemModel *>(m_index.model()),
+ "dataChanged", Qt::QueuedConnection,
+ Q_ARG(QModelIndex, m_index), Q_ARG(QModelIndex, m_index), Q_ARG(QVector<int>, QVector<int>() << m_role));
+ }
+ }
+
+private:
+ QPersistentModelIndex m_index;
+ int m_role;
+};
+
+ResourceProxyModel::ResourceProxyModel(const int resultTypeId, QObject *parent)
+ : QIdentityProxyModel(parent), m_resultTypeId(resultTypeId)
+{
+}
+
+QVariant ResourceProxyModel::data(const QModelIndex &proxyIndex, int role) const
+{
+ const QModelIndex mapped = mapToSource(proxyIndex);
+ // valid cell that's a Qt::DecorationRole and that contains a non-empty string
+ if (mapped.isValid() && role == Qt::DecorationRole && !mapToSource(proxyIndex).data(role).toString().isEmpty())
+ {
+ // do we already have a resource for this index?
+ if (!m_resources.contains(mapped))
+ {
+ Resource::Ptr placeholder;
+ const QVariant placeholderIdentifier = mapped.data(PlaceholderRole);
+ if (!placeholderIdentifier.isNull() && placeholderIdentifier.type() == QVariant::String)
+ {
+ placeholder = Resource::create(placeholderIdentifier.toString());
+ }
+
+ // create the Resource and apply the observer for models
+ Resource::Ptr res = Resource::create(mapToSource(proxyIndex).data(role).toString(), placeholder)
+ ->applyTo(new ModelResourceObserver(proxyIndex, role));
+
+ m_resources.insert(mapped, res);
+ }
+
+ return m_resources.value(mapped)->getResourceInternal(m_resultTypeId);
+ }
+ // otherwise fall back to the source model
+ return mapped.data(role);
+}
+
+void ResourceProxyModel::setSourceModel(QAbstractItemModel *model)
+{
+ if (sourceModel())
+ {
+ disconnect(sourceModel(), 0, this, 0);
+ }
+ if (model)
+ {
+ connect(model, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &tl, const QModelIndex &br, const QVector<int> &roles)
+ {
+ // invalidate resources so that they will be re-created
+ if (roles.contains(Qt::DecorationRole) || roles.contains(PlaceholderRole) || roles.isEmpty())
+ {
+ const QItemSelectionRange range(tl, br);
+ for (const QModelIndex &index : range.indexes())
+ {
+ m_resources.remove(index);
+ }
+ }
+ });
+ }
+ QIdentityProxyModel::setSourceModel(model);
+}
diff --git a/api/logic/resources/ResourceProxyModel.h b/api/logic/resources/ResourceProxyModel.h
new file mode 100644
index 00000000..98a3dbd1
--- /dev/null
+++ b/api/logic/resources/ResourceProxyModel.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <QIdentityProxyModel>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+/// Convenience proxy model that transforms resource identifiers (strings) for Qt::DecorationRole into other types.
+class MULTIMC_LOGIC_EXPORT ResourceProxyModel : public QIdentityProxyModel
+{
+ Q_OBJECT
+public:
+ // resultTypeId is found using qMetaTypeId<T>()
+ explicit ResourceProxyModel(const int resultTypeId, QObject *parent = nullptr);
+
+ enum
+ {
+ // provide this role from your model if you want to show a placeholder
+ PlaceholderRole = Qt::UserRole + 0xabc // some random offset to not collide with other stuff
+ };
+
+ QVariant data(const QModelIndex &proxyIndex, int role) const override;
+ void setSourceModel(QAbstractItemModel *model) override;
+
+ /// Helper function, usage: m_view->setModel(ResourceProxyModel::mixin<QIcon>(m_model));
+ template <typename T>
+ static QAbstractItemModel *mixin(QAbstractItemModel *model)
+ {
+ ResourceProxyModel *proxy = new ResourceProxyModel(qMetaTypeId<T>(), model);
+ proxy->setSourceModel(model);
+ return proxy;
+ }
+
+private:
+ // mutable because it needs to be available from the const data()
+ mutable QMap<QPersistentModelIndex, std::shared_ptr<class Resource>> m_resources;
+
+ const int m_resultTypeId;
+};
diff --git a/api/logic/screenshots/ImgurAlbumCreation.cpp b/api/logic/screenshots/ImgurAlbumCreation.cpp
new file mode 100644
index 00000000..e009ef4d
--- /dev/null
+++ b/api/logic/screenshots/ImgurAlbumCreation.cpp
@@ -0,0 +1,90 @@
+#include "ImgurAlbumCreation.h"
+
+#include <QNetworkRequest>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QUrl>
+#include <QStringList>
+
+#include "net/URLConstants.h"
+#include "Env.h"
+#include <QDebug>
+
+ImgurAlbumCreation::ImgurAlbumCreation(QList<ScreenshotPtr> screenshots) : NetAction(), m_screenshots(screenshots)
+{
+ m_url = URLConstants::IMGUR_BASE_URL + "album.json";
+ m_status = Job_NotStarted;
+}
+
+void ImgurAlbumCreation::start()
+{
+ m_status = Job_InProgress;
+ QNetworkRequest request(m_url);
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ request.setRawHeader("Authorization", "Client-ID 5b97b0713fba4a3");
+ request.setRawHeader("Accept", "application/json");
+
+ QStringList ids;
+ for (auto shot : m_screenshots)
+ {
+ ids.append(shot->m_imgurId);
+ }
+
+ const QByteArray data = "ids=" + ids.join(',').toUtf8() + "&title=Minecraft%20Screenshots&privacy=hidden";
+
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->post(request, data);
+
+ m_reply.reset(rep);
+ connect(rep, &QNetworkReply::uploadProgress, this, &ImgurAlbumCreation::downloadProgress);
+ connect(rep, &QNetworkReply::finished, this, &ImgurAlbumCreation::downloadFinished);
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+}
+void ImgurAlbumCreation::downloadError(QNetworkReply::NetworkError error)
+{
+ qDebug() << m_reply->errorString();
+ m_status = Job_Failed;
+}
+void ImgurAlbumCreation::downloadFinished()
+{
+ if (m_status != Job_Failed)
+ {
+ QByteArray data = m_reply->readAll();
+ m_reply.reset();
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ qDebug() << jsonError.errorString();
+ emit failed(m_index_within_job);
+ return;
+ }
+ auto object = doc.object();
+ if (!object.value("success").toBool())
+ {
+ qDebug() << doc.toJson();
+ emit failed(m_index_within_job);
+ return;
+ }
+ m_deleteHash = object.value("data").toObject().value("deletehash").toString();
+ m_id = object.value("data").toObject().value("id").toString();
+ m_status = Job_Finished;
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ else
+ {
+ qDebug() << m_reply->readAll();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+}
+void ImgurAlbumCreation::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
diff --git a/api/logic/screenshots/ImgurAlbumCreation.h b/api/logic/screenshots/ImgurAlbumCreation.h
new file mode 100644
index 00000000..469174e4
--- /dev/null
+++ b/api/logic/screenshots/ImgurAlbumCreation.h
@@ -0,0 +1,44 @@
+#pragma once
+#include "net/NetAction.h"
+#include "Screenshot.h"
+
+#include "multimc_logic_export.h"
+
+typedef std::shared_ptr<class ImgurAlbumCreation> ImgurAlbumCreationPtr;
+class MULTIMC_LOGIC_EXPORT ImgurAlbumCreation : public NetAction
+{
+public:
+ explicit ImgurAlbumCreation(QList<ScreenshotPtr> screenshots);
+ static ImgurAlbumCreationPtr make(QList<ScreenshotPtr> screenshots)
+ {
+ return ImgurAlbumCreationPtr(new ImgurAlbumCreation(screenshots));
+ }
+
+ QString deleteHash() const
+ {
+ return m_deleteHash;
+ }
+ QString id() const
+ {
+ return m_id;
+ }
+
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead()
+ {
+ }
+
+public
+slots:
+ virtual void start();
+
+private:
+ QList<ScreenshotPtr> m_screenshots;
+
+ QString m_deleteHash;
+ QString m_id;
+};
diff --git a/api/logic/screenshots/ImgurUpload.cpp b/api/logic/screenshots/ImgurUpload.cpp
new file mode 100644
index 00000000..48e0ec18
--- /dev/null
+++ b/api/logic/screenshots/ImgurUpload.cpp
@@ -0,0 +1,114 @@
+#include "ImgurUpload.h"
+
+#include <QNetworkRequest>
+#include <QHttpMultiPart>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QHttpPart>
+#include <QFile>
+#include <QUrl>
+
+#include "net/URLConstants.h"
+#include "Env.h"
+#include <QDebug>
+
+ImgurUpload::ImgurUpload(ScreenshotPtr shot) : NetAction(), m_shot(shot)
+{
+ m_url = URLConstants::IMGUR_BASE_URL + "upload.json";
+ m_status = Job_NotStarted;
+}
+
+void ImgurUpload::start()
+{
+ finished = false;
+ m_status = Job_InProgress;
+ QNetworkRequest request(m_url);
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+ request.setRawHeader("Authorization", "Client-ID 5b97b0713fba4a3");
+ request.setRawHeader("Accept", "application/json");
+
+ QFile f(m_shot->m_file.absoluteFilePath());
+ if (!f.open(QFile::ReadOnly))
+ {
+ emit failed(m_index_within_job);
+ return;
+ }
+
+ QHttpMultiPart *multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
+ QHttpPart filePart;
+ filePart.setBody(f.readAll().toBase64());
+ filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
+ filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\"");
+ multipart->append(filePart);
+ QHttpPart typePart;
+ typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\"");
+ typePart.setBody("base64");
+ multipart->append(typePart);
+ QHttpPart namePart;
+ namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"name\"");
+ namePart.setBody(m_shot->m_file.baseName().toUtf8());
+ multipart->append(namePart);
+
+ auto worker = ENV.qnam();
+ QNetworkReply *rep = worker->post(request, multipart);
+
+ m_reply.reset(rep);
+ connect(rep, &QNetworkReply::uploadProgress, this, &ImgurUpload::downloadProgress);
+ connect(rep, &QNetworkReply::finished, this, &ImgurUpload::downloadFinished);
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+}
+void ImgurUpload::downloadError(QNetworkReply::NetworkError error)
+{
+ qCritical() << "ImgurUpload failed with error" << m_reply->errorString() << "Server reply:\n" << m_reply->readAll();
+ if(finished)
+ {
+ qCritical() << "Double finished ImgurUpload!";
+ return;
+ }
+ m_status = Job_Failed;
+ finished = true;
+ m_reply.reset();
+ emit failed(m_index_within_job);
+}
+void ImgurUpload::downloadFinished()
+{
+ if(finished)
+ {
+ qCritical() << "Double finished ImgurUpload!";
+ return;
+ }
+ QByteArray data = m_reply->readAll();
+ m_reply.reset();
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ qDebug() << "imgur server did not reply with JSON" << jsonError.errorString();
+ finished = true;
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+ auto object = doc.object();
+ if (!object.value("success").toBool())
+ {
+ qDebug() << "Screenshot upload not successful:" << doc.toJson();
+ finished = true;
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+ m_shot->m_imgurId = object.value("data").toObject().value("id").toString();
+ m_shot->m_url = object.value("data").toObject().value("link").toString();
+ m_status = Job_Finished;
+ finished = true;
+ emit succeeded(m_index_within_job);
+ return;
+}
+void ImgurUpload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
diff --git a/api/logic/screenshots/ImgurUpload.h b/api/logic/screenshots/ImgurUpload.h
new file mode 100644
index 00000000..0a766b8f
--- /dev/null
+++ b/api/logic/screenshots/ImgurUpload.h
@@ -0,0 +1,33 @@
+#pragma once
+#include "net/NetAction.h"
+#include "Screenshot.h"
+
+#include "multimc_logic_export.h"
+
+typedef std::shared_ptr<class ImgurUpload> ImgurUploadPtr;
+class MULTIMC_LOGIC_EXPORT ImgurUpload : public NetAction
+{
+public:
+ explicit ImgurUpload(ScreenshotPtr shot);
+ static ImgurUploadPtr make(ScreenshotPtr shot)
+ {
+ return ImgurUploadPtr(new ImgurUpload(shot));
+ }
+
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead()
+ {
+ }
+
+public
+slots:
+ virtual void start();
+
+private:
+ ScreenshotPtr m_shot;
+ bool finished = true;
+};
diff --git a/api/logic/screenshots/Screenshot.h b/api/logic/screenshots/Screenshot.h
new file mode 100644
index 00000000..b48cbe99
--- /dev/null
+++ b/api/logic/screenshots/Screenshot.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <QDateTime>
+#include <QString>
+#include <QFileInfo>
+#include <memory>
+
+struct ScreenShot
+{
+ ScreenShot(QFileInfo file)
+ {
+ m_file = file;
+ }
+ QFileInfo m_file;
+ QString m_url;
+ QString m_imgurId;
+};
+
+typedef std::shared_ptr<ScreenShot> ScreenshotPtr;
diff --git a/api/logic/settings/INIFile.cpp b/api/logic/settings/INIFile.cpp
new file mode 100644
index 00000000..69a6b87e
--- /dev/null
+++ b/api/logic/settings/INIFile.cpp
@@ -0,0 +1,151 @@
+/* Copyright 2013-2015 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 "settings/INIFile.h"
+#include <FileSystem.h>
+
+#include <QFile>
+#include <QTextStream>
+#include <QStringList>
+#include <QSaveFile>
+#include <QDebug>
+
+INIFile::INIFile()
+{
+}
+
+QString INIFile::unescape(QString orig)
+{
+ QString out;
+ QChar prev = 0;
+ for(auto c: orig)
+ {
+ if(prev == '\\')
+ {
+ if(c == 'n')
+ out += '\n';
+ else if (c == 't')
+ out += '\t';
+ else
+ out += c;
+ prev = 0;
+ }
+ else
+ {
+ if(c == '\\')
+ {
+ prev = c;
+ continue;
+ }
+ out += c;
+ prev = 0;
+ }
+ }
+ return out;
+}
+
+QString INIFile::escape(QString orig)
+{
+ QString out;
+ for(auto c: orig)
+ {
+ if(c == '\n')
+ out += "\\n";
+ else if (c == '\t')
+ out += "\\t";
+ else if(c == '\\')
+ out += "\\\\";
+ else
+ out += c;
+ }
+ return out;
+}
+
+bool INIFile::saveFile(QString fileName)
+{
+ QByteArray outArray;
+ for (Iterator iter = begin(); iter != end(); iter++)
+ {
+ QString value = iter.value().toString();
+ value = escape(value);
+ outArray.append(iter.key().toUtf8());
+ outArray.append('=');
+ outArray.append(value.toUtf8());
+ outArray.append('\n');
+ }
+
+ try
+ {
+ FS::write(fileName, outArray);
+ }
+ catch (Exception & e)
+ {
+ qCritical() << e.what();
+ return false;
+ }
+
+ return true;
+}
+
+
+bool INIFile::loadFile(QString fileName)
+{
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly))
+ return false;
+ bool success = loadFile(file.readAll());
+ file.close();
+ return success;
+}
+
+bool INIFile::loadFile(QByteArray file)
+{
+ QTextStream in(file);
+ in.setCodec("UTF-8");
+
+ QStringList lines = in.readAll().split('\n');
+ for (int i = 0; i < lines.count(); i++)
+ {
+ QString &lineRaw = lines[i];
+ // Ignore comments.
+ QString line = lineRaw.left(lineRaw.indexOf('#')).trimmed();
+
+ int eqPos = line.indexOf('=');
+ if (eqPos == -1)
+ continue;
+ QString key = line.left(eqPos).trimmed();
+ QString valueStr = line.right(line.length() - eqPos - 1).trimmed();
+
+ valueStr = unescape(valueStr);
+
+ QVariant value(valueStr);
+ this->operator[](key) = value;
+ }
+
+ return true;
+}
+
+QVariant INIFile::get(QString key, QVariant def) const
+{
+ if (!this->contains(key))
+ return def;
+ else
+ return this->operator[](key);
+}
+
+void INIFile::set(QString key, QVariant val)
+{
+ this->operator[](key) = val;
+}
diff --git a/api/logic/settings/INIFile.h b/api/logic/settings/INIFile.h
new file mode 100644
index 00000000..5013eb2d
--- /dev/null
+++ b/api/logic/settings/INIFile.h
@@ -0,0 +1,38 @@
+/* Copyright 2013-2015 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>
+#include <QVariant>
+#include <QIODevice>
+
+#include "multimc_logic_export.h"
+
+// Sectionless INI parser (for instance config files)
+class MULTIMC_LOGIC_EXPORT INIFile : public QMap<QString, QVariant>
+{
+public:
+ explicit INIFile();
+
+ bool loadFile(QByteArray file);
+ bool loadFile(QString fileName);
+ bool saveFile(QString fileName);
+
+ QVariant get(QString key, QVariant def) const;
+ void set(QString key, QVariant val);
+ static QString unescape(QString orig);
+ static QString escape(QString orig);
+};
diff --git a/api/logic/settings/INISettingsObject.cpp b/api/logic/settings/INISettingsObject.cpp
new file mode 100644
index 00000000..5ccc7446
--- /dev/null
+++ b/api/logic/settings/INISettingsObject.cpp
@@ -0,0 +1,107 @@
+/* Copyright 2013-2015 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 "INISettingsObject.h"
+#include "Setting.h"
+
+INISettingsObject::INISettingsObject(const QString &path, QObject *parent)
+ : SettingsObject(parent)
+{
+ m_filePath = path;
+ m_ini.loadFile(path);
+}
+
+void INISettingsObject::setFilePath(const QString &filePath)
+{
+ m_filePath = filePath;
+}
+
+bool INISettingsObject::reload()
+{
+ return m_ini.loadFile(m_filePath) && SettingsObject::reload();
+}
+
+void INISettingsObject::suspendSave()
+{
+ m_suspendSave = true;
+}
+
+void INISettingsObject::resumeSave()
+{
+ m_suspendSave = false;
+ if(m_doSave)
+ {
+ m_ini.saveFile(m_filePath);
+ }
+}
+
+void INISettingsObject::changeSetting(const Setting &setting, QVariant value)
+{
+ if (contains(setting.id()))
+ {
+ // valid value -> set the main config, remove all the sysnonyms
+ if (value.isValid())
+ {
+ auto list = setting.configKeys();
+ m_ini.set(list.takeFirst(), value);
+ for(auto iter: list)
+ m_ini.remove(iter);
+ }
+ // invalid -> remove all (just like resetSetting)
+ else
+ {
+ for(auto iter: setting.configKeys())
+ m_ini.remove(iter);
+ }
+ doSave();
+ }
+}
+
+void INISettingsObject::doSave()
+{
+ if(m_suspendSave)
+ {
+ m_doSave = true;
+ }
+ else
+ {
+ m_ini.saveFile(m_filePath);
+ }
+}
+
+void INISettingsObject::resetSetting(const Setting &setting)
+{
+ // if we have the setting, remove all the synonyms. ALL OF THEM
+ if (contains(setting.id()))
+ {
+ for(auto iter: setting.configKeys())
+ m_ini.remove(iter);
+ doSave();
+ }
+}
+
+QVariant INISettingsObject::retrieveValue(const Setting &setting)
+{
+ // if we have the setting, return value of the first matching synonym
+ if (contains(setting.id()))
+ {
+ for(auto iter: setting.configKeys())
+ {
+ if(m_ini.contains(iter))
+ return m_ini[iter];
+ }
+ }
+ return QVariant();
+}
diff --git a/api/logic/settings/INISettingsObject.h b/api/logic/settings/INISettingsObject.h
new file mode 100644
index 00000000..4afa2a2c
--- /dev/null
+++ b/api/logic/settings/INISettingsObject.h
@@ -0,0 +1,66 @@
+/* Copyright 2013-2015 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 <QObject>
+
+#include "settings/INIFile.h"
+
+#include "settings/SettingsObject.h"
+
+#include "multimc_logic_export.h"
+
+/*!
+ * \brief A settings object that stores its settings in an INIFile.
+ */
+class MULTIMC_LOGIC_EXPORT INISettingsObject : public SettingsObject
+{
+ Q_OBJECT
+public:
+ explicit INISettingsObject(const QString &path, QObject *parent = 0);
+
+ /*!
+ * \brief Gets the path to the INI file.
+ * \return The path to the INI file.
+ */
+ virtual QString filePath() const
+ {
+ return m_filePath;
+ }
+
+ /*!
+ * \brief Sets the path to the INI file and reloads it.
+ * \param filePath The INI file's new path.
+ */
+ virtual void setFilePath(const QString &filePath);
+
+ bool reload() override;
+
+ void suspendSave() override;
+ void resumeSave() override;
+
+protected slots:
+ virtual void changeSetting(const Setting &setting, QVariant value) override;
+ virtual void resetSetting(const Setting &setting) override;
+
+protected:
+ virtual QVariant retrieveValue(const Setting &setting) override;
+ void doSave();
+
+protected:
+ INIFile m_ini;
+ QString m_filePath;
+};
diff --git a/api/logic/settings/OverrideSetting.cpp b/api/logic/settings/OverrideSetting.cpp
new file mode 100644
index 00000000..25162dff
--- /dev/null
+++ b/api/logic/settings/OverrideSetting.cpp
@@ -0,0 +1,54 @@
+/* Copyright 2013-2015 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 "OverrideSetting.h"
+
+OverrideSetting::OverrideSetting(std::shared_ptr<Setting> other, std::shared_ptr<Setting> gate)
+ : Setting(other->configKeys(), QVariant())
+{
+ Q_ASSERT(other);
+ Q_ASSERT(gate);
+ m_other = other;
+ m_gate = gate;
+}
+
+bool OverrideSetting::isOverriding() const
+{
+ return m_gate->get().toBool();
+}
+
+QVariant OverrideSetting::defValue() const
+{
+ return m_other->get();
+}
+
+QVariant OverrideSetting::get() const
+{
+ if(isOverriding())
+ {
+ return Setting::get();
+ }
+ return m_other->get();
+}
+
+void OverrideSetting::reset()
+{
+ Setting::reset();
+}
+
+void OverrideSetting::set(QVariant value)
+{
+ Setting::set(value);
+}
diff --git a/api/logic/settings/OverrideSetting.h b/api/logic/settings/OverrideSetting.h
new file mode 100644
index 00000000..68595cde
--- /dev/null
+++ b/api/logic/settings/OverrideSetting.h
@@ -0,0 +1,46 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <memory>
+
+#include "Setting.h"
+
+/*!
+ * \brief A setting that 'overrides another.'
+ * This means that the setting's default value will be the value of another setting.
+ * The other setting can be (and usually is) a part of a different SettingsObject
+ * than this one.
+ */
+class OverrideSetting : public Setting
+{
+ Q_OBJECT
+public:
+ explicit OverrideSetting(std::shared_ptr<Setting> overriden, std::shared_ptr<Setting> gate);
+
+ virtual QVariant defValue() const;
+ virtual QVariant get() const;
+ virtual void set (QVariant value);
+ virtual void reset();
+
+private:
+ bool isOverriding() const;
+
+protected:
+ std::shared_ptr<Setting> m_other;
+ std::shared_ptr<Setting> m_gate;
+};
diff --git a/api/logic/settings/PassthroughSetting.cpp b/api/logic/settings/PassthroughSetting.cpp
new file mode 100644
index 00000000..45a560de
--- /dev/null
+++ b/api/logic/settings/PassthroughSetting.cpp
@@ -0,0 +1,66 @@
+/* Copyright 2013-2015 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 "PassthroughSetting.h"
+
+PassthroughSetting::PassthroughSetting(std::shared_ptr<Setting> other, std::shared_ptr<Setting> gate)
+ : Setting(other->configKeys(), QVariant())
+{
+ Q_ASSERT(other);
+ Q_ASSERT(gate);
+ m_other = other;
+ m_gate = gate;
+}
+
+bool PassthroughSetting::isOverriding() const
+{
+ return m_gate->get().toBool();
+}
+
+QVariant PassthroughSetting::defValue() const
+{
+ if(isOverriding())
+ {
+ return m_other->get();
+ }
+ return m_other->defValue();
+}
+
+QVariant PassthroughSetting::get() const
+{
+ if(isOverriding())
+ {
+ return Setting::get();
+ }
+ return m_other->get();
+}
+
+void PassthroughSetting::reset()
+{
+ if(isOverriding())
+ {
+ Setting::reset();
+ }
+ m_other->reset();
+}
+
+void PassthroughSetting::set(QVariant value)
+{
+ if(isOverriding())
+ {
+ Setting::set(value);
+ }
+ m_other->set(value);
+}
diff --git a/api/logic/settings/PassthroughSetting.h b/api/logic/settings/PassthroughSetting.h
new file mode 100644
index 00000000..c4dc646c
--- /dev/null
+++ b/api/logic/settings/PassthroughSetting.h
@@ -0,0 +1,45 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <memory>
+
+#include "Setting.h"
+
+/*!
+ * \brief A setting that 'overrides another.' based on the value of a 'gate' setting
+ * If 'gate' evaluates to true, the override stores and returns data
+ * If 'gate' evaluates to false, the original does,
+ */
+class PassthroughSetting : public Setting
+{
+ Q_OBJECT
+public:
+ explicit PassthroughSetting(std::shared_ptr<Setting> overriden, std::shared_ptr<Setting> gate);
+
+ virtual QVariant defValue() const;
+ virtual QVariant get() const;
+ virtual void set (QVariant value);
+ virtual void reset();
+
+private:
+ bool isOverriding() const;
+
+protected:
+ std::shared_ptr<Setting> m_other;
+ std::shared_ptr<Setting> m_gate;
+};
diff --git a/api/logic/settings/Setting.cpp b/api/logic/settings/Setting.cpp
new file mode 100644
index 00000000..b17101a2
--- /dev/null
+++ b/api/logic/settings/Setting.cpp
@@ -0,0 +1,53 @@
+/* Copyright 2013-2015 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 "Setting.h"
+#include "settings/SettingsObject.h"
+
+Setting::Setting(QStringList synonyms, QVariant defVal)
+ : QObject(), m_synonyms(synonyms), m_defVal(defVal)
+{
+}
+
+QVariant Setting::get() const
+{
+ SettingsObject *sbase = m_storage;
+ if (!sbase)
+ {
+ return defValue();
+ }
+ else
+ {
+ QVariant test = sbase->retrieveValue(*this);
+ if (!test.isValid())
+ return defValue();
+ return test;
+ }
+}
+
+QVariant Setting::defValue() const
+{
+ return m_defVal;
+}
+
+void Setting::set(QVariant value)
+{
+ emit SettingChanged(*this, value);
+}
+
+void Setting::reset()
+{
+ emit settingReset(*this);
+}
diff --git a/api/logic/settings/Setting.h b/api/logic/settings/Setting.h
new file mode 100644
index 00000000..6d53ac6d
--- /dev/null
+++ b/api/logic/settings/Setting.h
@@ -0,0 +1,119 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QVariant>
+#include <QStringList>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class SettingsObject;
+
+/*!
+ *
+ */
+class MULTIMC_LOGIC_EXPORT Setting : public QObject
+{
+ Q_OBJECT
+public:
+ /**
+ * Construct a Setting
+ *
+ * Synonyms are all the possible names used in the settings object, in order of preference.
+ * First synonym is the ID, which identifies the setting in MultiMC.
+ *
+ * defVal is the default value that will be returned when the settings object
+ * doesn't have any value for this setting.
+ */
+ explicit Setting(QStringList synonyms, QVariant defVal = QVariant());
+
+ /*!
+ * \brief Gets this setting's ID.
+ * This is used to refer to the setting within the application.
+ * \warning Changing the ID while the setting is registered with a SettingsObject results in
+ * undefined behavior.
+ * \return The ID of the setting.
+ */
+ virtual QString id() const
+ {
+ return m_synonyms.first();
+ }
+
+ /*!
+ * \brief Gets this setting's config file key.
+ * This is used to store the setting's value in the config file. It is usually
+ * the same as the setting's ID, but it can be different.
+ * \return The setting's config file key.
+ */
+ virtual QStringList configKeys() const
+ {
+ return m_synonyms;
+ }
+
+ /*!
+ * \brief Gets this setting's value as a QVariant.
+ * This is done by calling the SettingsObject's retrieveValue() function.
+ * If this Setting doesn't have a SettingsObject, this returns an invalid QVariant.
+ * \return QVariant containing this setting's value.
+ * \sa value()
+ */
+ virtual QVariant get() const;
+
+ /*!
+ * \brief Gets this setting's default value.
+ * \return The default value of this setting.
+ */
+ virtual QVariant defValue() const;
+
+signals:
+ /*!
+ * \brief Signal emitted when this Setting object's value changes.
+ * \param setting A reference to the Setting that changed.
+ * \param value This Setting object's new value.
+ */
+ void SettingChanged(const Setting &setting, QVariant value);
+
+ /*!
+ * \brief Signal emitted when this Setting object's value resets to default.
+ * \param setting A reference to the Setting that changed.
+ */
+ void settingReset(const Setting &setting);
+
+public
+slots:
+ /*!
+ * \brief Changes the setting's value.
+ * This is done by emitting the SettingChanged() signal which will then be
+ * handled by the SettingsObject object and cause the setting to change.
+ * \param value The new value.
+ */
+ virtual void set(QVariant value);
+
+ /*!
+ * \brief Reset the setting to default
+ * This is done by emitting the settingReset() signal which will then be
+ * handled by the SettingsObject object and cause the setting to change.
+ */
+ virtual void reset();
+
+protected:
+ friend class SettingsObject;
+ SettingsObject * m_storage;
+ QStringList m_synonyms;
+ QVariant m_defVal;
+};
diff --git a/api/logic/settings/SettingsObject.cpp b/api/logic/settings/SettingsObject.cpp
new file mode 100644
index 00000000..f2ffdf9b
--- /dev/null
+++ b/api/logic/settings/SettingsObject.cpp
@@ -0,0 +1,142 @@
+/* Copyright 2013-2015 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 "settings/SettingsObject.h"
+#include "settings/Setting.h"
+#include "settings/OverrideSetting.h"
+#include "PassthroughSetting.h"
+#include <QDebug>
+
+#include <QVariant>
+
+SettingsObject::SettingsObject(QObject *parent) : QObject(parent)
+{
+}
+
+SettingsObject::~SettingsObject()
+{
+ m_settings.clear();
+}
+
+std::shared_ptr<Setting> SettingsObject::registerOverride(std::shared_ptr<Setting> original,
+ std::shared_ptr<Setting> gate)
+{
+ if (contains(original->id()))
+ {
+ qCritical() << QString("Failed to register setting %1. ID already exists.")
+ .arg(original->id());
+ return nullptr; // Fail
+ }
+ auto override = std::make_shared<OverrideSetting>(original, gate);
+ override->m_storage = this;
+ connectSignals(*override);
+ m_settings.insert(override->id(), override);
+ return override;
+}
+
+std::shared_ptr<Setting> SettingsObject::registerPassthrough(std::shared_ptr<Setting> original,
+ std::shared_ptr<Setting> gate)
+{
+ if (contains(original->id()))
+ {
+ qCritical() << QString("Failed to register setting %1. ID already exists.")
+ .arg(original->id());
+ return nullptr; // Fail
+ }
+ auto passthrough = std::make_shared<PassthroughSetting>(original, gate);
+ passthrough->m_storage = this;
+ connectSignals(*passthrough);
+ m_settings.insert(passthrough->id(), passthrough);
+ return passthrough;
+}
+
+std::shared_ptr<Setting> SettingsObject::registerSetting(QStringList synonyms, QVariant defVal)
+{
+ if (synonyms.empty())
+ return nullptr;
+ if (contains(synonyms.first()))
+ {
+ qCritical() << QString("Failed to register setting %1. ID already exists.")
+ .arg(synonyms.first());
+ return nullptr; // Fail
+ }
+ auto setting = std::make_shared<Setting>(synonyms, defVal);
+ setting->m_storage = this;
+ connectSignals(*setting);
+ m_settings.insert(setting->id(), setting);
+ return setting;
+}
+
+std::shared_ptr<Setting> SettingsObject::getSetting(const QString &id) const
+{
+ // Make sure there is a setting with the given ID.
+ if (!m_settings.contains(id))
+ return NULL;
+
+ return m_settings[id];
+}
+
+QVariant SettingsObject::get(const QString &id) const
+{
+ auto setting = getSetting(id);
+ return (setting ? setting->get() : QVariant());
+}
+
+bool SettingsObject::set(const QString &id, QVariant value)
+{
+ auto setting = getSetting(id);
+ if (!setting)
+ {
+ qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id);
+ return false;
+ }
+ else
+ {
+ setting->set(value);
+ return true;
+ }
+}
+
+void SettingsObject::reset(const QString &id) const
+{
+ auto setting = getSetting(id);
+ if (setting)
+ setting->reset();
+}
+
+bool SettingsObject::contains(const QString &id)
+{
+ return m_settings.contains(id);
+}
+
+bool SettingsObject::reload()
+{
+ for (auto setting : m_settings.values())
+ {
+ setting->set(setting->get());
+ }
+ return true;
+}
+
+void SettingsObject::connectSignals(const Setting &setting)
+{
+ connect(&setting, SIGNAL(SettingChanged(const Setting &, QVariant)),
+ SLOT(changeSetting(const Setting &, QVariant)));
+ connect(&setting, SIGNAL(SettingChanged(const Setting &, QVariant)),
+ SIGNAL(SettingChanged(const Setting &, QVariant)));
+
+ connect(&setting, SIGNAL(settingReset(Setting)), SLOT(resetSetting(const Setting &)));
+ connect(&setting, SIGNAL(settingReset(Setting)), SIGNAL(settingReset(const Setting &)));
+}
diff --git a/api/logic/settings/SettingsObject.h b/api/logic/settings/SettingsObject.h
new file mode 100644
index 00000000..82193903
--- /dev/null
+++ b/api/logic/settings/SettingsObject.h
@@ -0,0 +1,214 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QMap>
+#include <QStringList>
+#include <QVariant>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class Setting;
+class SettingsObject;
+
+typedef std::shared_ptr<SettingsObject> SettingsObjectPtr;
+
+/*!
+ * \brief The SettingsObject handles communicating settings between the application and a
+ *settings file.
+ * The class keeps a list of Setting objects. Each Setting object represents one
+ * of the application's settings. These Setting objects are registered with
+ * a SettingsObject and can be managed similarly to the way a list works.
+ *
+ * \author Andrew Okin
+ * \date 2/22/2013
+ *
+ * \sa Setting
+ */
+class MULTIMC_LOGIC_EXPORT SettingsObject : public QObject
+{
+ Q_OBJECT
+public:
+ class Lock
+ {
+ public:
+ Lock(SettingsObjectPtr locked)
+ :m_locked(locked)
+ {
+ m_locked->suspendSave();
+ }
+ ~Lock()
+ {
+ m_locked->resumeSave();
+ }
+ private:
+ SettingsObjectPtr m_locked;
+ };
+public:
+ explicit SettingsObject(QObject *parent = 0);
+ virtual ~SettingsObject();
+ /*!
+ * Registers an override setting for the given original setting in this settings object
+ * gate decides if the passthrough (true) or the original (false) is used for value
+ *
+ * This will fail if there is already a setting with the same ID as
+ * the one that is being registered.
+ * \return A valid Setting shared pointer if successful.
+ */
+ std::shared_ptr<Setting> registerOverride(std::shared_ptr<Setting> original, std::shared_ptr<Setting> gate);
+
+ /*!
+ * Registers a passthorugh setting for the given original setting in this settings object
+ * gate decides if the passthrough (true) or the original (false) is used for value
+ *
+ * This will fail if there is already a setting with the same ID as
+ * the one that is being registered.
+ * \return A valid Setting shared pointer if successful.
+ */
+ std::shared_ptr<Setting> registerPassthrough(std::shared_ptr<Setting> original, std::shared_ptr<Setting> gate);
+
+ /*!
+ * Registers the given setting with this SettingsObject and connects the necessary signals.
+ *
+ * This will fail if there is already a setting with the same ID as
+ * the one that is being registered.
+ * \return A valid Setting shared pointer if successful.
+ */
+ std::shared_ptr<Setting> registerSetting(QStringList synonyms,
+ QVariant defVal = QVariant());
+
+ /*!
+ * Registers the given setting with this SettingsObject and connects the necessary signals.
+ *
+ * This will fail if there is already a setting with the same ID as
+ * the one that is being registered.
+ * \return A valid Setting shared pointer if successful.
+ */
+ std::shared_ptr<Setting> registerSetting(QString id, QVariant defVal = QVariant())
+ {
+ return registerSetting(QStringList(id), defVal);
+ }
+
+ /*!
+ * \brief Gets the setting with the given ID.
+ * \param id The ID of the setting to get.
+ * \return A pointer to the setting with the given ID.
+ * Returns null if there is no setting with the given ID.
+ * \sa operator []()
+ */
+ std::shared_ptr<Setting> getSetting(const QString &id) const;
+
+ /*!
+ * \brief Gets the value of the setting with the given ID.
+ * \param id The ID of the setting to get.
+ * \return The setting's value as a QVariant.
+ * If no setting with the given ID exists, returns an invalid QVariant.
+ */
+ QVariant get(const QString &id) const;
+
+ /*!
+ * \brief Sets the value of the setting with the given ID.
+ * If no setting with the given ID exists, returns false
+ * \param id The ID of the setting to change.
+ * \param value The new value of the setting.
+ * \return True if successful, false if it failed.
+ */
+ bool set(const QString &id, QVariant value);
+
+ /*!
+ * \brief Reverts the setting with the given ID to default.
+ * \param id The ID of the setting to reset.
+ */
+ void reset(const QString &id) const;
+
+ /*!
+ * \brief Checks if this SettingsObject contains a setting with the given ID.
+ * \param id The ID to check for.
+ * \return True if the SettingsObject has a setting with the given ID.
+ */
+ bool contains(const QString &id);
+
+ /*!
+ * \brief Reloads the settings and emit signals for changed settings
+ * \return True if reloading was successful
+ */
+ virtual bool reload();
+
+ virtual void suspendSave() = 0;
+ virtual void resumeSave() = 0;
+signals:
+ /*!
+ * \brief Signal emitted when one of this SettingsObject object's settings changes.
+ * This is usually just connected directly to each Setting object's
+ * SettingChanged() signals.
+ * \param setting A reference to the Setting object that changed.
+ * \param value The Setting object's new value.
+ */
+ void SettingChanged(const Setting &setting, QVariant value);
+
+ /*!
+ * \brief Signal emitted when one of this SettingsObject object's settings resets.
+ * This is usually just connected directly to each Setting object's
+ * settingReset() signals.
+ * \param setting A reference to the Setting object that changed.
+ */
+ void settingReset(const Setting &setting);
+
+protected
+slots:
+ /*!
+ * \brief Changes a setting.
+ * This slot is usually connected to each Setting object's
+ * SettingChanged() signal. The signal is emitted, causing this slot
+ * to update the setting's value in the config file.
+ * \param setting A reference to the Setting object that changed.
+ * \param value The setting's new value.
+ */
+ virtual void changeSetting(const Setting &setting, QVariant value) = 0;
+
+ /*!
+ * \brief Resets a setting.
+ * This slot is usually connected to each Setting object's
+ * settingReset() signal. The signal is emitted, causing this slot
+ * to update the setting's value in the config file.
+ * \param setting A reference to the Setting object that changed.
+ */
+ virtual void resetSetting(const Setting &setting) = 0;
+
+protected:
+ /*!
+ * \brief Connects the necessary signals to the given Setting.
+ * \param setting The setting to connect.
+ */
+ void connectSignals(const Setting &setting);
+
+ /*!
+ * \brief Function used by Setting objects to get their values from the SettingsObject.
+ * \param setting The
+ * \return
+ */
+ virtual QVariant retrieveValue(const Setting &setting) = 0;
+
+ friend class Setting;
+
+private:
+ QMap<QString, std::shared_ptr<Setting>> m_settings;
+protected:
+ bool m_suspendSave = false;
+ bool m_doSave = false;
+};
diff --git a/api/logic/status/StatusChecker.cpp b/api/logic/status/StatusChecker.cpp
new file mode 100644
index 00000000..13cac037
--- /dev/null
+++ b/api/logic/status/StatusChecker.cpp
@@ -0,0 +1,153 @@
+/* Copyright 2013-2015 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 "StatusChecker.h"
+
+#include <net/URLConstants.h>
+
+#include <QByteArray>
+
+#include <QDebug>
+
+StatusChecker::StatusChecker()
+{
+
+}
+
+void StatusChecker::timerEvent(QTimerEvent *e)
+{
+ QObject::timerEvent(e);
+ reloadStatus();
+}
+
+void StatusChecker::reloadStatus()
+{
+ if (isLoadingStatus())
+ {
+ // qDebug() << "Ignored request to reload status. Currently reloading already.";
+ return;
+ }
+
+ // qDebug() << "Reloading status.";
+
+ NetJob* job = new NetJob("Status JSON");
+ job->addNetAction(ByteArrayDownload::make(URLConstants::MOJANG_STATUS_URL));
+ QObject::connect(job, &NetJob::succeeded, this, &StatusChecker::statusDownloadFinished);
+ QObject::connect(job, &NetJob::failed, this, &StatusChecker::statusDownloadFailed);
+ m_statusNetJob.reset(job);
+ emit statusLoading(true);
+ job->start();
+}
+
+void StatusChecker::statusDownloadFinished()
+{
+ qDebug() << "Finished loading status JSON.";
+ m_statusEntries.clear();
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(m_statusNetJob->first());
+ data = dl->m_data;
+ m_statusNetJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ fail("Error parsing status JSON:" + jsonError.errorString());
+ return;
+ }
+
+ if (!jsonDoc.isArray())
+ {
+ fail("Error parsing status JSON: JSON root is not an array");
+ return;
+ }
+
+ QJsonArray root = jsonDoc.array();
+
+ for(auto status = root.begin(); status != root.end(); ++status)
+ {
+ QVariantMap map = (*status).toObject().toVariantMap();
+
+ for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter)
+ {
+ QString key = iter.key();
+ QVariant value = iter.value();
+
+ if(value.type() == QVariant::Type::String)
+ {
+ m_statusEntries.insert(key, value.toString());
+ //qDebug() << "Status JSON object: " << key << m_statusEntries[key];
+ }
+ else
+ {
+ fail("Malformed status JSON: expected status type to be a string.");
+ return;
+ }
+ }
+ }
+
+ succeed();
+}
+
+void StatusChecker::statusDownloadFailed(QString reason)
+{
+ fail(tr("Failed to load status JSON:\n%1").arg(reason));
+}
+
+
+QMap<QString, QString> StatusChecker::getStatusEntries() const
+{
+ return m_statusEntries;
+}
+
+bool StatusChecker::isLoadingStatus() const
+{
+ return m_statusNetJob.get() != nullptr;
+}
+
+QString StatusChecker::getLastLoadErrorMsg() const
+{
+ return m_lastLoadError;
+}
+
+void StatusChecker::succeed()
+{
+ if(m_prevEntries != m_statusEntries)
+ {
+ emit statusChanged(m_statusEntries);
+ m_prevEntries = m_statusEntries;
+ }
+ m_lastLoadError = "";
+ qDebug() << "Status loading succeeded.";
+ m_statusNetJob.reset();
+ emit statusLoading(false);
+}
+
+void StatusChecker::fail(const QString& errorMsg)
+{
+ if(m_prevEntries != m_statusEntries)
+ {
+ emit statusChanged(m_statusEntries);
+ m_prevEntries = m_statusEntries;
+ }
+ m_lastLoadError = errorMsg;
+ qDebug() << "Failed to load status:" << errorMsg;
+ m_statusNetJob.reset();
+ emit statusLoading(false);
+}
+
diff --git a/api/logic/status/StatusChecker.h b/api/logic/status/StatusChecker.h
new file mode 100644
index 00000000..c1a54dba
--- /dev/null
+++ b/api/logic/status/StatusChecker.h
@@ -0,0 +1,60 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QString>
+#include <QList>
+
+#include <net/NetJob.h>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT StatusChecker : public QObject
+{
+ Q_OBJECT
+public:
+ StatusChecker();
+
+ QString getLastLoadErrorMsg() const;
+
+ bool isLoadingStatus() const;
+
+ QMap<QString, QString> getStatusEntries() const;
+
+ void Q_SLOT reloadStatus();
+
+protected:
+ virtual void timerEvent(QTimerEvent *);
+
+signals:
+ void statusLoading(bool loading);
+ void statusChanged(QMap<QString, QString> newStatus);
+
+protected slots:
+ void statusDownloadFinished();
+ void statusDownloadFailed(QString reason);
+
+protected:
+ QMap<QString, QString> m_prevEntries;
+ QMap<QString, QString> m_statusEntries;
+ NetJobPtr m_statusNetJob;
+ QString m_lastLoadError;
+
+ void Q_SLOT succeed();
+ void Q_SLOT fail(const QString& errorMsg);
+};
+
diff --git a/api/logic/tasks/SequentialTask.cpp b/api/logic/tasks/SequentialTask.cpp
new file mode 100644
index 00000000..ac0e7820
--- /dev/null
+++ b/api/logic/tasks/SequentialTask.cpp
@@ -0,0 +1,55 @@
+#include "SequentialTask.h"
+
+SequentialTask::SequentialTask(QObject *parent) : Task(parent), m_currentIndex(-1)
+{
+}
+
+void SequentialTask::addTask(std::shared_ptr<Task> task)
+{
+ m_queue.append(task);
+}
+
+void SequentialTask::executeTask()
+{
+ m_currentIndex = -1;
+ startNext();
+}
+
+void SequentialTask::startNext()
+{
+ if (m_currentIndex != -1)
+ {
+ std::shared_ptr<Task> previous = m_queue[m_currentIndex];
+ disconnect(previous.get(), 0, this, 0);
+ }
+ m_currentIndex++;
+ if (m_queue.isEmpty() || m_currentIndex >= m_queue.size())
+ {
+ emitSucceeded();
+ return;
+ }
+ std::shared_ptr<Task> next = m_queue[m_currentIndex];
+ connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString)));
+ connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString)));
+ connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64)));
+ connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext()));
+ next->start();
+}
+
+void SequentialTask::subTaskFailed(const QString &msg)
+{
+ emitFailed(msg);
+}
+void SequentialTask::subTaskStatus(const QString &msg)
+{
+ setStatus(msg);
+}
+void SequentialTask::subTaskProgress(qint64 current, qint64 total)
+{
+ if(total == 0)
+ {
+ setProgress(0, 100);
+ return;
+ }
+ setProgress(current, total);
+}
diff --git a/api/logic/tasks/SequentialTask.h b/api/logic/tasks/SequentialTask.h
new file mode 100644
index 00000000..69031095
--- /dev/null
+++ b/api/logic/tasks/SequentialTask.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "Task.h"
+
+#include <QQueue>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT SequentialTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit SequentialTask(QObject *parent = 0);
+
+ void addTask(std::shared_ptr<Task> task);
+
+protected:
+ void executeTask();
+
+private
+slots:
+ void startNext();
+ void subTaskFailed(const QString &msg);
+ void subTaskStatus(const QString &msg);
+ void subTaskProgress(qint64 current, qint64 total);
+
+private:
+ QQueue<std::shared_ptr<Task> > m_queue;
+ int m_currentIndex;
+};
diff --git a/api/logic/tasks/Task.cpp b/api/logic/tasks/Task.cpp
new file mode 100644
index 00000000..3c4e3188
--- /dev/null
+++ b/api/logic/tasks/Task.cpp
@@ -0,0 +1,88 @@
+/* Copyright 2013-2015 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 "Task.h"
+
+#include <QDebug>
+
+Task::Task(QObject *parent) : QObject(parent)
+{
+}
+
+void Task::setStatus(const QString &new_status)
+{
+ if(m_status != new_status)
+ {
+ m_status = new_status;
+ emit status(m_status);
+ }
+}
+
+void Task::setProgress(qint64 current, qint64 total)
+{
+ m_progress = current;
+ m_progressTotal = total;
+ emit progress(m_progress, m_progressTotal);
+}
+
+void Task::start()
+{
+ m_running = true;
+ emit started();
+ executeTask();
+}
+
+void Task::emitFailed(QString reason)
+{
+ m_running = false;
+ m_finished = true;
+ m_succeeded = false;
+ m_failReason = reason;
+ qCritical() << "Task failed: " << reason;
+ emit failed(reason);
+ emit finished();
+}
+
+void Task::emitSucceeded()
+{
+ if (!m_running) { return; } // Don't succeed twice.
+ m_running = false;
+ m_finished = true;
+ m_succeeded = true;
+ qDebug() << "Task succeeded";
+ emit succeeded();
+ emit finished();
+}
+
+bool Task::isRunning() const
+{
+ return m_running;
+}
+
+bool Task::isFinished() const
+{
+ return m_finished;
+}
+
+bool Task::successful() const
+{
+ return m_succeeded;
+}
+
+QString Task::failReason() const
+{
+ return m_failReason;
+}
+
diff --git a/api/logic/tasks/Task.h b/api/logic/tasks/Task.h
new file mode 100644
index 00000000..2b0ccbcd
--- /dev/null
+++ b/api/logic/tasks/Task.h
@@ -0,0 +1,96 @@
+/* Copyright 2013-2015 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 <QObject>
+#include <QString>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT Task : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Task(QObject *parent = 0);
+ virtual ~Task() {};
+
+ virtual bool isRunning() const;
+
+ virtual bool isFinished() const;
+
+ /*!
+ * True if this task was successful.
+ * If the task failed or is still running, returns false.
+ */
+ virtual bool successful() const;
+
+ /*!
+ * Returns the string that was passed to emitFailed as the error message when the task failed.
+ * If the task hasn't failed, returns an empty string.
+ */
+ virtual QString failReason() const;
+
+ virtual bool canAbort() const { return false; }
+
+ QString getStatus()
+ {
+ return m_status;
+ }
+
+ qint64 getProgress()
+ {
+ return m_progress;
+ }
+
+ qint64 getTotalProgress()
+ {
+ return m_progressTotal;
+ }
+
+signals:
+ void started();
+ void progress(qint64 current, qint64 total);
+ void finished();
+ void succeeded();
+ void failed(QString reason);
+ void status(QString status);
+
+public
+slots:
+ virtual void start();
+ virtual bool abort() { return false; };
+
+protected:
+ virtual void executeTask() = 0;
+
+protected slots:
+ virtual void emitSucceeded();
+ virtual void emitFailed(QString reason);
+
+public slots:
+ void setStatus(const QString &status);
+ void setProgress(qint64 current, qint64 total);
+
+protected:
+ bool m_running = false;
+ bool m_finished = false;
+ bool m_succeeded = false;
+ QString m_failReason = "";
+ QString m_status;
+ int m_progress = 0;
+ int m_progressTotal = 100;
+};
+
diff --git a/api/logic/tasks/ThreadTask.cpp b/api/logic/tasks/ThreadTask.cpp
new file mode 100644
index 00000000..ddd1dee5
--- /dev/null
+++ b/api/logic/tasks/ThreadTask.cpp
@@ -0,0 +1,41 @@
+#include "ThreadTask.h"
+#include <QtConcurrentRun>
+ThreadTask::ThreadTask(Task * internal, QObject *parent) : Task(parent), m_internal(internal)
+{
+}
+
+void ThreadTask::start()
+{
+ connect(m_internal, SIGNAL(failed(QString)), SLOT(iternal_failed(QString)));
+ connect(m_internal, SIGNAL(progress(qint64,qint64)), SLOT(iternal_progress(qint64,qint64)));
+ connect(m_internal, SIGNAL(started()), SLOT(iternal_started()));
+ connect(m_internal, SIGNAL(status(QString)), SLOT(iternal_status(QString)));
+ connect(m_internal, SIGNAL(succeeded()), SLOT(iternal_succeeded()));
+ m_running = true;
+ QtConcurrent::run(m_internal, &Task::start);
+}
+
+void ThreadTask::iternal_failed(QString reason)
+{
+ emitFailed(reason);
+}
+
+void ThreadTask::iternal_progress(qint64 current, qint64 total)
+{
+ progress(current, total);
+}
+
+void ThreadTask::iternal_started()
+{
+ emit started();
+}
+
+void ThreadTask::iternal_status(QString status)
+{
+ setStatus(status);
+}
+
+void ThreadTask::iternal_succeeded()
+{
+ emitSucceeded();
+}
diff --git a/api/logic/tasks/ThreadTask.h b/api/logic/tasks/ThreadTask.h
new file mode 100644
index 00000000..718dbc91
--- /dev/null
+++ b/api/logic/tasks/ThreadTask.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "Task.h"
+
+class ThreadTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit ThreadTask(Task * internal, QObject * parent = nullptr);
+
+protected:
+ void executeTask() {};
+
+public slots:
+ virtual void start();
+
+private slots:
+ void iternal_started();
+ void iternal_progress(qint64 current, qint64 total);
+ void iternal_succeeded();
+ void iternal_failed(QString reason);
+ void iternal_status(QString status);
+private:
+ Task * m_internal;
+}; \ No newline at end of file
diff --git a/api/logic/tools/BaseExternalTool.cpp b/api/logic/tools/BaseExternalTool.cpp
new file mode 100644
index 00000000..2b97c3c9
--- /dev/null
+++ b/api/logic/tools/BaseExternalTool.cpp
@@ -0,0 +1,41 @@
+#include "BaseExternalTool.h"
+
+#include <QProcess>
+#include <QDir>
+
+#ifdef Q_OS_WIN
+#include <windows.h>
+#endif
+
+#include "BaseInstance.h"
+
+BaseExternalTool::BaseExternalTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent)
+ : QObject(parent), m_instance(instance), globalSettings(settings)
+{
+}
+
+BaseExternalTool::~BaseExternalTool()
+{
+}
+
+BaseDetachedTool::BaseDetachedTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent)
+ : BaseExternalTool(settings, instance, parent)
+{
+
+}
+
+void BaseDetachedTool::run()
+{
+ runImpl();
+}
+
+
+BaseExternalToolFactory::~BaseExternalToolFactory()
+{
+}
+
+BaseDetachedTool *BaseDetachedToolFactory::createDetachedTool(InstancePtr instance,
+ QObject *parent)
+{
+ return qobject_cast<BaseDetachedTool *>(createTool(instance, parent));
+}
diff --git a/api/logic/tools/BaseExternalTool.h b/api/logic/tools/BaseExternalTool.h
new file mode 100644
index 00000000..fe1b5dc6
--- /dev/null
+++ b/api/logic/tools/BaseExternalTool.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include <QObject>
+#include <BaseInstance.h>
+
+#include "multimc_logic_export.h"
+
+class BaseInstance;
+class SettingsObject;
+class QProcess;
+
+class MULTIMC_LOGIC_EXPORT BaseExternalTool : public QObject
+{
+ Q_OBJECT
+public:
+ explicit BaseExternalTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0);
+ virtual ~BaseExternalTool();
+
+protected:
+ InstancePtr m_instance;
+ SettingsObjectPtr globalSettings;
+};
+
+class MULTIMC_LOGIC_EXPORT BaseDetachedTool : public BaseExternalTool
+{
+ Q_OBJECT
+public:
+ explicit BaseDetachedTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0);
+
+public
+slots:
+ void run();
+
+protected:
+ virtual void runImpl() = 0;
+};
+
+class MULTIMC_LOGIC_EXPORT BaseExternalToolFactory
+{
+public:
+ virtual ~BaseExternalToolFactory();
+
+ virtual QString name() const = 0;
+
+ virtual void registerSettings(SettingsObjectPtr settings) = 0;
+
+ virtual BaseExternalTool *createTool(InstancePtr instance, QObject *parent = 0) = 0;
+
+ virtual bool check(QString *error) = 0;
+ virtual bool check(const QString &path, QString *error) = 0;
+
+protected:
+ SettingsObjectPtr globalSettings;
+};
+
+class MULTIMC_LOGIC_EXPORT BaseDetachedToolFactory : public BaseExternalToolFactory
+{
+public:
+ virtual BaseDetachedTool *createDetachedTool(InstancePtr instance, QObject *parent = 0);
+};
diff --git a/api/logic/tools/BaseProfiler.cpp b/api/logic/tools/BaseProfiler.cpp
new file mode 100644
index 00000000..5ff0fa44
--- /dev/null
+++ b/api/logic/tools/BaseProfiler.cpp
@@ -0,0 +1,35 @@
+#include "BaseProfiler.h"
+
+#include <QProcess>
+
+BaseProfiler::BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject *parent)
+ : BaseExternalTool(settings, instance, parent)
+{
+}
+
+void BaseProfiler::beginProfiling(std::shared_ptr<LaunchTask> process)
+{
+ beginProfilingImpl(process);
+}
+
+void BaseProfiler::abortProfiling()
+{
+ abortProfilingImpl();
+}
+
+void BaseProfiler::abortProfilingImpl()
+{
+ if (!m_profilerProcess)
+ {
+ return;
+ }
+ m_profilerProcess->terminate();
+ m_profilerProcess->deleteLater();
+ m_profilerProcess = 0;
+ emit abortLaunch(tr("Profiler aborted"));
+}
+
+BaseProfiler *BaseProfilerFactory::createProfiler(InstancePtr instance, QObject *parent)
+{
+ return qobject_cast<BaseProfiler *>(createTool(instance, parent));
+}
diff --git a/api/logic/tools/BaseProfiler.h b/api/logic/tools/BaseProfiler.h
new file mode 100644
index 00000000..3340b7e4
--- /dev/null
+++ b/api/logic/tools/BaseProfiler.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "BaseExternalTool.h"
+
+#include "multimc_logic_export.h"
+
+class BaseInstance;
+class SettingsObject;
+class LaunchTask;
+class QProcess;
+
+class MULTIMC_LOGIC_EXPORT BaseProfiler : public BaseExternalTool
+{
+ Q_OBJECT
+public:
+ explicit BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0);
+
+public
+slots:
+ void beginProfiling(std::shared_ptr<LaunchTask> process);
+ void abortProfiling();
+
+protected:
+ QProcess *m_profilerProcess;
+
+ virtual void beginProfilingImpl(std::shared_ptr<LaunchTask> process) = 0;
+ virtual void abortProfilingImpl();
+
+signals:
+ void readyToLaunch(const QString &message);
+ void abortLaunch(const QString &message);
+};
+
+class MULTIMC_LOGIC_EXPORT BaseProfilerFactory : public BaseExternalToolFactory
+{
+public:
+ virtual BaseProfiler *createProfiler(InstancePtr instance, QObject *parent = 0);
+};
diff --git a/api/logic/tools/JProfiler.cpp b/api/logic/tools/JProfiler.cpp
new file mode 100644
index 00000000..a0e3c895
--- /dev/null
+++ b/api/logic/tools/JProfiler.cpp
@@ -0,0 +1,116 @@
+#include "JProfiler.h"
+
+#include <QDir>
+
+#include "settings/SettingsObject.h"
+#include "launch/LaunchTask.h"
+#include "BaseInstance.h"
+
+class JProfiler : public BaseProfiler
+{
+ Q_OBJECT
+public:
+ JProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0);
+
+private slots:
+ void profilerStarted();
+ void profilerFinished(int exit, QProcess::ExitStatus status);
+
+protected:
+ void beginProfilingImpl(std::shared_ptr<LaunchTask> process);
+
+private:
+ int listeningPort = 0;
+};
+
+JProfiler::JProfiler(SettingsObjectPtr settings, InstancePtr instance,
+ QObject *parent)
+ : BaseProfiler(settings, instance, parent)
+{
+}
+
+void JProfiler::profilerStarted()
+{
+ emit readyToLaunch(tr("Listening on port: %1").arg(listeningPort));
+}
+
+void JProfiler::profilerFinished(int exit, QProcess::ExitStatus status)
+{
+ if (status == QProcess::CrashExit)
+ {
+ emit abortLaunch(tr("Profiler aborted"));
+ }
+ if (m_profilerProcess)
+ {
+ m_profilerProcess->deleteLater();
+ m_profilerProcess = 0;
+ }
+}
+
+void JProfiler::beginProfilingImpl(std::shared_ptr<LaunchTask> process)
+{
+ listeningPort = globalSettings->get("JProfilerPort").toInt();
+ QProcess *profiler = new QProcess(this);
+ QStringList profilerArgs =
+ {
+ "-d", QString::number(process->pid()),
+ "--gui",
+ "-p", QString::number(listeningPort)
+ };
+ auto basePath = globalSettings->get("JProfilerPath").toString();
+
+#ifdef Q_OS_WIN
+ QString profilerProgram = QDir(basePath).absoluteFilePath("bin/jpenable.exe");
+#else
+ QString profilerProgram = QDir(basePath).absoluteFilePath("bin/jpenable");
+#endif
+
+ profiler->setArguments(profilerArgs);
+ profiler->setProgram(profilerProgram);
+
+ connect(profiler, SIGNAL(started()), SLOT(profilerStarted()));
+ connect(profiler, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(profilerFinished(int,QProcess::ExitStatus)));
+
+ m_profilerProcess = profiler;
+ profiler->start();
+}
+
+void JProfilerFactory::registerSettings(SettingsObjectPtr settings)
+{
+ settings->registerSetting("JProfilerPath");
+ settings->registerSetting("JProfilerPort", 42042);
+ globalSettings = settings;
+}
+
+BaseExternalTool *JProfilerFactory::createTool(InstancePtr instance, QObject *parent)
+{
+ return new JProfiler(globalSettings, instance, parent);
+}
+
+bool JProfilerFactory::check(QString *error)
+{
+ return check(globalSettings->get("JProfilerPath").toString(), error);
+}
+
+bool JProfilerFactory::check(const QString &path, QString *error)
+{
+ if (path.isEmpty())
+ {
+ *error = QObject::tr("Empty path");
+ return false;
+ }
+ QDir dir(path);
+ if (!dir.exists())
+ {
+ *error = QObject::tr("Path does not exist");
+ return false;
+ }
+ if (!dir.exists("bin") || !(dir.exists("bin/jprofiler") || dir.exists("bin/jprofiler.exe")) || !dir.exists("bin/agent.jar"))
+ {
+ *error = QObject::tr("Invalid JProfiler install");
+ return false;
+ }
+ return true;
+}
+
+#include "JProfiler.moc"
diff --git a/api/logic/tools/JProfiler.h b/api/logic/tools/JProfiler.h
new file mode 100644
index 00000000..d658d6c2
--- /dev/null
+++ b/api/logic/tools/JProfiler.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "BaseProfiler.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT JProfilerFactory : public BaseProfilerFactory
+{
+public:
+ QString name() const override { return "JProfiler"; }
+ void registerSettings(SettingsObjectPtr settings) override;
+ BaseExternalTool *createTool(InstancePtr instance, QObject *parent = 0) override;
+ bool check(QString *error) override;
+ bool check(const QString &path, QString *error) override;
+};
diff --git a/api/logic/tools/JVisualVM.cpp b/api/logic/tools/JVisualVM.cpp
new file mode 100644
index 00000000..169967d9
--- /dev/null
+++ b/api/logic/tools/JVisualVM.cpp
@@ -0,0 +1,103 @@
+#include "JVisualVM.h"
+
+#include <QDir>
+#include <QStandardPaths>
+
+#include "settings/SettingsObject.h"
+#include "launch/LaunchTask.h"
+#include "BaseInstance.h"
+
+class JVisualVM : public BaseProfiler
+{
+ Q_OBJECT
+public:
+ JVisualVM(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0);
+
+private slots:
+ void profilerStarted();
+ void profilerFinished(int exit, QProcess::ExitStatus status);
+
+protected:
+ void beginProfilingImpl(std::shared_ptr<LaunchTask> process);
+};
+
+
+JVisualVM::JVisualVM(SettingsObjectPtr settings, InstancePtr instance, QObject *parent)
+ : BaseProfiler(settings, instance, parent)
+{
+}
+
+void JVisualVM::profilerStarted()
+{
+ emit readyToLaunch(tr("JVisualVM started"));
+}
+
+void JVisualVM::profilerFinished(int exit, QProcess::ExitStatus status)
+{
+ if (status == QProcess::CrashExit)
+ {
+ emit abortLaunch(tr("Profiler aborted"));
+ }
+ if (m_profilerProcess)
+ {
+ m_profilerProcess->deleteLater();
+ m_profilerProcess = 0;
+ }
+}
+
+void JVisualVM::beginProfilingImpl(std::shared_ptr<LaunchTask> process)
+{
+ QProcess *profiler = new QProcess(this);
+ QStringList profilerArgs =
+ {
+ "--openpid", QString::number(process->pid())
+ };
+ auto programPath = globalSettings->get("JVisualVMPath").toString();
+
+ profiler->setArguments(profilerArgs);
+ profiler->setProgram(programPath);
+
+ connect(profiler, SIGNAL(started()), SLOT(profilerStarted()));
+ connect(profiler, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(profilerFinished(int,QProcess::ExitStatus)));
+
+ profiler->start();
+ m_profilerProcess = profiler;
+}
+
+void JVisualVMFactory::registerSettings(SettingsObjectPtr settings)
+{
+ QString defaultValue = QStandardPaths::findExecutable("jvisualvm");
+ if (defaultValue.isNull())
+ {
+ defaultValue = QStandardPaths::findExecutable("visualvm");
+ }
+ settings->registerSetting("JVisualVMPath", defaultValue);
+ globalSettings = settings;
+}
+
+BaseExternalTool *JVisualVMFactory::createTool(InstancePtr instance, QObject *parent)
+{
+ return new JVisualVM(globalSettings, instance, parent);
+}
+
+bool JVisualVMFactory::check(QString *error)
+{
+ return check(globalSettings->get("JVisualVMPath").toString(), error);
+}
+
+bool JVisualVMFactory::check(const QString &path, QString *error)
+{
+ if (path.isEmpty())
+ {
+ *error = QObject::tr("Empty path");
+ return false;
+ }
+ if (!QDir::isAbsolutePath(path) || !QFileInfo(path).isExecutable() || !path.contains("visualvm"))
+ {
+ *error = QObject::tr("Invalid path to JVisualVM");
+ return false;
+ }
+ return true;
+}
+
+#include "JVisualVM.moc"
diff --git a/api/logic/tools/JVisualVM.h b/api/logic/tools/JVisualVM.h
new file mode 100644
index 00000000..0674da13
--- /dev/null
+++ b/api/logic/tools/JVisualVM.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "BaseProfiler.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT JVisualVMFactory : public BaseProfilerFactory
+{
+public:
+ QString name() const override { return "JVisualVM"; }
+ void registerSettings(SettingsObjectPtr settings) override;
+ BaseExternalTool *createTool(InstancePtr instance, QObject *parent = 0) override;
+ bool check(QString *error) override;
+ bool check(const QString &path, QString *error) override;
+};
diff --git a/api/logic/tools/MCEditTool.cpp b/api/logic/tools/MCEditTool.cpp
new file mode 100644
index 00000000..f3d550d0
--- /dev/null
+++ b/api/logic/tools/MCEditTool.cpp
@@ -0,0 +1,124 @@
+#include "MCEditTool.h"
+
+#include <QDir>
+#include <QProcess>
+#include <QUrl>
+
+#include "settings/SettingsObject.h"
+#include "BaseInstance.h"
+#include "minecraft/MinecraftInstance.h"
+
+MCEditTool::MCEditTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent)
+ : BaseDetachedTool(settings, instance, parent)
+{
+}
+
+QString MCEditTool::getSave() const
+{
+ auto mcInstance = std::dynamic_pointer_cast<MinecraftInstance>(m_instance);
+ if(!mcInstance)
+ {
+ return QString();
+ }
+ QDir saves(mcInstance->minecraftRoot() + "/saves");
+ QStringList worlds = saves.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
+ QMutableListIterator<QString> it(worlds);
+ while (it.hasNext())
+ {
+ it.next();
+ if (!QDir(saves.absoluteFilePath(it.value())).exists("level.dat"))
+ {
+ it.remove();
+ }
+ }
+ bool ok = true;
+ // FIXME: mixing logic and UI!!!!
+ /*
+ const QString save = QInputDialog::getItem(QApplication::activeWindow(), tr("MCEdit"), tr("Choose which world to open:"),
+ worlds, 0, false, &ok);
+ if (ok)
+ {
+ return saves.absoluteFilePath(save);
+ }
+ */
+ return QString();
+}
+
+void MCEditTool::runImpl()
+{
+ const QString mceditPath = globalSettings->get("MCEditPath").toString();
+ const QString save = getSave();
+ if (save.isNull())
+ {
+ return;
+ }
+#ifdef Q_OS_OSX
+ QProcess *process = new QProcess();
+ connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), process, SLOT(deleteLater()));
+ process->setProgram(mceditPath);
+ process->setArguments(QStringList() << save);
+ process->start();
+#else
+ QDir mceditDir(mceditPath);
+ QString program;
+ #ifdef Q_OS_LINUX
+ if (mceditDir.exists("mcedit.py"))
+ {
+ program = mceditDir.absoluteFilePath("mcedit.py");
+ }
+ else if (mceditDir.exists("mcedit.sh"))
+ {
+ program = mceditDir.absoluteFilePath("mcedit.sh");
+ }
+ #elif defined(Q_OS_WIN32)
+ if (mceditDir.exists("mcedit.exe"))
+ {
+ program = mceditDir.absoluteFilePath("mcedit.exe");
+ }
+ else if (mceditDir.exists("mcedit2.exe"))
+ {
+ program = mceditDir.absoluteFilePath("mcedit2.exe");
+ }
+ #endif
+ /*
+ if(program.size())
+ {
+ DesktopServices::openFile(program, save, mceditPath);
+ }
+ */
+#endif
+}
+
+void MCEditFactory::registerSettings(SettingsObjectPtr settings)
+{
+ settings->registerSetting("MCEditPath");
+ globalSettings = settings;
+}
+BaseExternalTool *MCEditFactory::createTool(InstancePtr instance, QObject *parent)
+{
+ return new MCEditTool(globalSettings, instance, parent);
+}
+bool MCEditFactory::check(QString *error)
+{
+ return check(globalSettings->get("MCEditPath").toString(), error);
+}
+bool MCEditFactory::check(const QString &path, QString *error)
+{
+ if (path.isEmpty())
+ {
+ *error = QObject::tr("Path is empty");
+ return false;
+ }
+ const QDir dir(path);
+ if (!dir.exists())
+ {
+ *error = QObject::tr("Path does not exist");
+ return false;
+ }
+ if (!dir.exists("mcedit.sh") && !dir.exists("mcedit.py") && !dir.exists("mcedit.exe") && !dir.exists("Contents") && !dir.exists("mcedit2.exe"))
+ {
+ *error = QObject::tr("Path does not seem to be a MCEdit path");
+ return false;
+ }
+ return true;
+}
diff --git a/api/logic/tools/MCEditTool.h b/api/logic/tools/MCEditTool.h
new file mode 100644
index 00000000..c287f1ea
--- /dev/null
+++ b/api/logic/tools/MCEditTool.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "BaseExternalTool.h"
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT MCEditTool : public BaseDetachedTool
+{
+ Q_OBJECT
+public:
+ explicit MCEditTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0);
+
+protected:
+ QString getSave() const;
+ void runImpl() override;
+};
+
+class MULTIMC_LOGIC_EXPORT MCEditFactory : public BaseDetachedToolFactory
+{
+public:
+ QString name() const override { return "MCEdit"; }
+ void registerSettings(SettingsObjectPtr settings) override;
+ BaseExternalTool *createTool(InstancePtr instance, QObject *parent = 0) override;
+ bool check(QString *error) override;
+ bool check(const QString &path, QString *error) override;
+};
diff --git a/api/logic/trans/TranslationDownloader.cpp b/api/logic/trans/TranslationDownloader.cpp
new file mode 100644
index 00000000..ee5c1fd2
--- /dev/null
+++ b/api/logic/trans/TranslationDownloader.cpp
@@ -0,0 +1,53 @@
+#include "TranslationDownloader.h"
+#include "net/NetJob.h"
+#include "net/CacheDownload.h"
+#include "net/URLConstants.h"
+#include "Env.h"
+#include <QDebug>
+
+TranslationDownloader::TranslationDownloader()
+{
+}
+void TranslationDownloader::downloadTranslations()
+{
+ qDebug() << "Downloading Translations Index...";
+ m_index_job.reset(new NetJob("Translations Index"));
+ m_index_task = ByteArrayDownload::make(QUrl("http://files.multimc.org/translations/index"));
+ m_index_job->addNetAction(m_index_task);
+ connect(m_index_job.get(), &NetJob::failed, this, &TranslationDownloader::indexFailed);
+ connect(m_index_job.get(), &NetJob::succeeded, this, &TranslationDownloader::indexRecieved);
+ m_index_job->start();
+}
+void TranslationDownloader::indexRecieved()
+{
+ qDebug() << "Got translations index!";
+ m_dl_job.reset(new NetJob("Translations"));
+ QList<QByteArray> lines = m_index_task->m_data.split('\n');
+ for (const auto line : lines)
+ {
+ if (!line.isEmpty())
+ {
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "mmc_" + line);
+ entry->setStale(true);
+ CacheDownloadPtr dl = CacheDownload::make(
+ QUrl(URLConstants::TRANSLATIONS_BASE_URL + line),
+ entry);
+ m_dl_job->addNetAction(dl);
+ }
+ }
+ connect(m_dl_job.get(), &NetJob::succeeded, this, &TranslationDownloader::dlGood);
+ connect(m_dl_job.get(), &NetJob::failed, this, &TranslationDownloader::dlFailed);
+ m_dl_job->start();
+}
+void TranslationDownloader::dlFailed(QString reason)
+{
+ qCritical() << "Translations Download Failed:" << reason;
+}
+void TranslationDownloader::dlGood()
+{
+ qDebug() << "Got translations!";
+}
+void TranslationDownloader::indexFailed(QString reason)
+{
+ qCritical() << "Translations Index Download Failed:" << reason;
+}
diff --git a/api/logic/trans/TranslationDownloader.h b/api/logic/trans/TranslationDownloader.h
new file mode 100644
index 00000000..e7893805
--- /dev/null
+++ b/api/logic/trans/TranslationDownloader.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <QList>
+#include <QUrl>
+#include <memory>
+#include <QObject>
+#include <net/NetJob.h>
+#include "multimc_logic_export.h"
+
+class ByteArrayDownload;
+class NetJob;
+
+class MULTIMC_LOGIC_EXPORT TranslationDownloader : public QObject
+{
+ Q_OBJECT
+
+public:
+ TranslationDownloader();
+
+ void downloadTranslations();
+
+private slots:
+ void indexRecieved();
+ void indexFailed(QString reason);
+ void dlFailed(QString reason);
+ void dlGood();
+
+private:
+ std::shared_ptr<ByteArrayDownload> m_index_task;
+ NetJobPtr m_dl_job;
+ NetJobPtr m_index_job;
+};
diff --git a/api/logic/updater/DownloadTask.cpp b/api/logic/updater/DownloadTask.cpp
new file mode 100644
index 00000000..6947e8bf
--- /dev/null
+++ b/api/logic/updater/DownloadTask.cpp
@@ -0,0 +1,169 @@
+/* Copyright 2013-2014 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 "DownloadTask.h"
+
+#include "updater/UpdateChecker.h"
+#include "GoUpdate.h"
+#include "net/NetJob.h"
+
+#include <QFile>
+#include <QTemporaryDir>
+#include <QCryptographicHash>
+
+namespace GoUpdate
+{
+
+DownloadTask::DownloadTask(Status status, QString target, QObject *parent)
+ : Task(parent), m_updateFilesDir(target)
+{
+ m_status = status;
+
+ m_updateFilesDir.setAutoRemove(false);
+}
+
+void DownloadTask::executeTask()
+{
+ loadVersionInfo();
+}
+
+void DownloadTask::loadVersionInfo()
+{
+ setStatus(tr("Loading version information..."));
+
+ m_currentVersionFileListDownload.reset();
+ m_newVersionFileListDownload.reset();
+
+ NetJob *netJob = new NetJob("Version Info");
+
+ // Find the index URL.
+ QUrl newIndexUrl = QUrl(m_status.newRepoUrl).resolved(QString::number(m_status.newVersionId) + ".json");
+ qDebug() << m_status.newRepoUrl << " turns into " << newIndexUrl;
+
+ netJob->addNetAction(m_newVersionFileListDownload = ByteArrayDownload::make(newIndexUrl));
+
+ // If we have a current version URL, get that one too.
+ if (!m_status.currentRepoUrl.isEmpty())
+ {
+ QUrl cIndexUrl = QUrl(m_status.currentRepoUrl).resolved(QString::number(m_status.currentVersionId) + ".json");
+ netJob->addNetAction(m_currentVersionFileListDownload = ByteArrayDownload::make(cIndexUrl));
+ qDebug() << m_status.currentRepoUrl << " turns into " << cIndexUrl;
+ }
+
+ // connect signals and start the job
+ connect(netJob, &NetJob::succeeded, this, &DownloadTask::processDownloadedVersionInfo);
+ connect(netJob, &NetJob::failed, this, &DownloadTask::vinfoDownloadFailed);
+ m_vinfoNetJob.reset(netJob);
+ netJob->start();
+}
+
+void DownloadTask::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_newVersionFileListDownload->m_status != Job_Failed)
+ {
+ processDownloadedVersionInfo();
+ return;
+ }
+
+ // TODO: Give a more detailed error message.
+ qCritical() << "Failed to download version info files.";
+ emitFailed(tr("Failed to download version info files."));
+}
+
+void DownloadTask::processDownloadedVersionInfo()
+{
+ VersionFileList m_currentVersionFileList;
+ VersionFileList m_newVersionFileList;
+
+ setStatus(tr("Reading file list for new version..."));
+ qDebug() << "Reading file list for new version...";
+ QString error;
+ if (!parseVersionInfo(m_newVersionFileListDownload->m_data, m_newVersionFileList, error))
+ {
+ qCritical() << error;
+ emitFailed(error);
+ return;
+ }
+
+ // if we have the current version info, use it.
+ if (m_currentVersionFileListDownload && m_currentVersionFileListDownload->m_status != Job_Failed)
+ {
+ setStatus(tr("Reading file list for current version..."));
+ qDebug() << "Reading file list for current version...";
+ // if this fails, it's not a complete loss.
+ QString error;
+ if(!parseVersionInfo( m_currentVersionFileListDownload->m_data, m_currentVersionFileList, error))
+ {
+ qDebug() << error << "This is not a fatal error.";
+ }
+ }
+
+ // We don't need this any more.
+ m_currentVersionFileListDownload.reset();
+ m_newVersionFileListDownload.reset();
+ m_vinfoNetJob.reset();
+
+ setStatus(tr("Processing file lists - figuring out how to install the update..."));
+
+ // make a new netjob for the actual update files
+ NetJobPtr netJob (new NetJob("Update Files"));
+
+ // fill netJob and operationList
+ if (!processFileLists(m_currentVersionFileList, m_newVersionFileList, m_status.rootPath, m_updateFilesDir.path(), netJob, m_operations))
+ {
+ emitFailed(tr("Failed to process update lists..."));
+ return;
+ }
+
+ // Now start the download.
+ QObject::connect(netJob.get(), &NetJob::succeeded, this, &DownloadTask::fileDownloadFinished);
+ QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged);
+ QObject::connect(netJob.get(), &NetJob::failed, this, &DownloadTask::fileDownloadFailed);
+
+ setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
+ qDebug() << "Begin downloading update files to" << m_updateFilesDir.path();
+ m_filesNetJob = netJob;
+ m_filesNetJob->start();
+}
+
+void DownloadTask::fileDownloadFinished()
+{
+ emitSucceeded();
+}
+
+void DownloadTask::fileDownloadFailed(QString reason)
+{
+ qCritical() << "Failed to download update files:" << reason;
+ emitFailed(tr("Failed to download update files: %1").arg(reason));
+}
+
+void DownloadTask::fileDownloadProgressChanged(qint64 current, qint64 total)
+{
+ setProgress(current, total);
+}
+
+QString DownloadTask::updateFilesDir()
+{
+ return m_updateFilesDir.path();
+}
+
+OperationList DownloadTask::operations()
+{
+ return m_operations;
+}
+
+} \ No newline at end of file
diff --git a/api/logic/updater/DownloadTask.h b/api/logic/updater/DownloadTask.h
new file mode 100644
index 00000000..83b4a142
--- /dev/null
+++ b/api/logic/updater/DownloadTask.h
@@ -0,0 +1,95 @@
+/* Copyright 2013-2014 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 "tasks/Task.h"
+#include "net/NetJob.h"
+#include "GoUpdate.h"
+
+#include "multimc_logic_export.h"
+
+namespace GoUpdate
+{
+/*!
+ * The DownloadTask 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 MULTIMC_LOGIC_EXPORT DownloadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ /**
+ * Create a download task
+ *
+ * target is a template - XXXXXX at the end will be replaced with a random generated string, ensuring uniqueness
+ */
+ explicit DownloadTask(Status status, QString target, QObject* parent = 0);
+
+ /// Get the directory that will contain the update files.
+ QString updateFilesDir();
+
+ /// Get the list of operations that should be done
+ OperationList operations();
+
+ /// set updater download behavior
+ void setUseLocalUpdater(bool useLocal);
+
+protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+ /*!
+ * 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.
+ */
+ void loadVersionInfo();
+
+ NetJobPtr m_vinfoNetJob;
+ ByteArrayDownloadPtr m_currentVersionFileListDownload;
+ ByteArrayDownloadPtr m_newVersionFileListDownload;
+
+ NetJobPtr m_filesNetJob;
+
+ Status m_status;
+
+ OperationList m_operations;
+
+ /*!
+ * 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:
+ /*!
+ * This function is called when version information is finished downloading
+ * and at least the new file list download succeeded
+ */
+ void processDownloadedVersionInfo();
+ void vinfoDownloadFailed();
+
+ void fileDownloadFinished();
+ void fileDownloadFailed(QString reason);
+ void fileDownloadProgressChanged(qint64 current, qint64 total);
+};
+
+} \ No newline at end of file
diff --git a/api/logic/updater/GoUpdate.cpp b/api/logic/updater/GoUpdate.cpp
new file mode 100644
index 00000000..4e465d5c
--- /dev/null
+++ b/api/logic/updater/GoUpdate.cpp
@@ -0,0 +1,215 @@
+#include "GoUpdate.h"
+#include <QDebug>
+#include <QDomDocument>
+#include <QFile>
+#include <FileSystem.h>
+
+namespace GoUpdate
+{
+
+bool parseVersionInfo(const QByteArray &data, VersionFileList &list, QString &error)
+{
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ error = QString("Failed to parse version info JSON: %1 at %2")
+ .arg(jsonError.errorString())
+ .arg(jsonError.offset);
+ qCritical() << error;
+ return false;
+ }
+
+ QJsonObject json = jsonDoc.object();
+
+ qDebug() << data;
+ qDebug() << "Loading version info from JSON.";
+ QJsonArray filesArray = json.value("Files").toArray();
+ for (QJsonValue fileValue : filesArray)
+ {
+ QJsonObject fileObj = fileValue.toObject();
+
+ QString file_path = fileObj.value("Path").toString();
+#ifdef Q_OS_MAC
+ // On OSX, the paths for the updater need to be fixed.
+ // basically, anything that isn't in the .app folder is ignored.
+ // everything else is changed so the code that processes the files actually finds
+ // them and puts the replacements in the right spots.
+ fixPathForOSX(file_path);
+#endif
+ VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(),
+ FileSourceList(), fileObj.value("MD5").toString(), };
+ qDebug() << "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
+ {
+ qWarning() << "Unknown source type" << type << "ignored.";
+ }
+ }
+
+ qDebug() << "Loaded info for" << file.path;
+
+ list.append(file);
+ }
+
+ return true;
+}
+
+bool processFileLists
+(
+ const VersionFileList &currentVersion,
+ const VersionFileList &newVersion,
+ const QString &rootPath,
+ const QString &tempPath,
+ NetJobPtr job,
+ OperationList &ops
+)
+{
+ // 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 : currentVersion)
+ {
+ QFileInfo toDelete(FS::PathCombine(rootPath, entry.path));
+ if (!toDelete.exists())
+ {
+ qCritical() << "Expected file " << toDelete.absoluteFilePath()
+ << " doesn't exist!";
+ }
+ bool keep = false;
+
+ //
+ for (VersionFileEntry newEntry : newVersion)
+ {
+ if (newEntry.path == entry.path)
+ {
+ qDebug() << "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)
+ {
+ if (toDelete.exists())
+ ops.append(Operation::DeleteOp(entry.path));
+ }
+ }
+
+ // Next, check each file in MultiMC's folder and see if we need to update them.
+ for (VersionFileEntry entry : newVersion)
+ {
+ // 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;
+ QString realEntryPath = FS::PathCombine(rootPath, entry.path);
+ QFile entryFile(realEntryPath);
+ QFileInfo entryInfo(realEntryPath);
+
+ bool needs_upgrade = false;
+ if (!entryFile.exists())
+ {
+ needs_upgrade = true;
+ }
+ else
+ {
+ bool pass = true;
+ if (!entryInfo.isReadable())
+ {
+ qCritical() << "File " << realEntryPath << " is not readable.";
+ pass = false;
+ }
+ if (!entryInfo.isWritable())
+ {
+ qCritical() << "File " << realEntryPath << " is not writable.";
+ pass = false;
+ }
+ if (!entryFile.open(QFile::ReadOnly))
+ {
+ qCritical() << "File " << realEntryPath << " cannot be opened for reading.";
+ pass = false;
+ }
+ if (!pass)
+ {
+ ops.clear();
+ return false;
+ }
+ }
+
+ if(!needs_upgrade)
+ {
+ QCryptographicHash hash(QCryptographicHash::Md5);
+ auto foo = entryFile.readAll();
+
+ hash.addData(foo);
+ fileMD5 = hash.result().toHex();
+ if ((fileMD5 != entry.md5))
+ {
+ qDebug() << "MD5Sum does not match!";
+ qDebug() << "Expected:'" << entry.md5 << "'";
+ qDebug() << "Got: '" << fileMD5 << "'";
+ needs_upgrade = true;
+ }
+ }
+
+ // skip file. it doesn't need an upgrade.
+ if (!needs_upgrade)
+ {
+ qDebug() << "File" << realEntryPath << " does not need updating.";
+ continue;
+ }
+
+ // yep. this file actually needs an upgrade. PROCEED.
+ qDebug() << "Found file" << realEntryPath << " 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")
+ continue;
+
+ qDebug() << "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 = FS::PathCombine(tempPath, 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_expected_md5 = entry.md5;
+ job->addNetAction(download);
+ ops.append(Operation::CopyOp(dlPath, entry.path, entry.mode));
+ }
+ }
+ return true;
+}
+
+bool fixPathForOSX(QString &path)
+{
+ if (path.startsWith("MultiMC.app/"))
+ {
+ // remove the prefix and add a new, more appropriate one.
+ path.remove(0, 12);
+ return true;
+ }
+ else
+ {
+ qCritical() << "Update path not within .app: " << path;
+ return false;
+ }
+}
+} \ No newline at end of file
diff --git a/api/logic/updater/GoUpdate.h b/api/logic/updater/GoUpdate.h
new file mode 100644
index 00000000..b8a534de
--- /dev/null
+++ b/api/logic/updater/GoUpdate.h
@@ -0,0 +1,133 @@
+#pragma once
+#include <QByteArray>
+#include <net/NetJob.h>
+
+#include "multimc_logic_export.h"
+
+namespace GoUpdate
+{
+
+/**
+ * A temporary object exchanged between updated checker and the actual update task
+ */
+struct MULTIMC_LOGIC_EXPORT Status
+{
+ bool updateAvailable = false;
+
+ int newVersionId = -1;
+ QString newRepoUrl;
+
+ int currentVersionId = -1;
+ QString currentRepoUrl;
+
+ // path to the root of the application
+ QString rootPath;
+};
+
+/**
+ * Struct that describes an entry in a VersionFileEntry's `Sources` list.
+ */
+struct MULTIMC_LOGIC_EXPORT FileSource
+{
+ FileSource(QString type, QString url, QString compression="")
+ {
+ this->type = type;
+ this->url = url;
+ this->compressionType = compression;
+ }
+
+ bool operator==(const FileSource &f2) const
+ {
+ return type == f2.type && url == f2.url && compressionType == f2.compressionType;
+ }
+
+ QString type;
+ QString url;
+ QString compressionType;
+};
+typedef QList<FileSource> FileSourceList;
+
+/**
+ * Structure that describes an entry in a GoUpdate version's `Files` list.
+ */
+struct MULTIMC_LOGIC_EXPORT VersionFileEntry
+{
+ QString path;
+ int mode;
+ FileSourceList sources;
+ QString md5;
+ bool operator==(const VersionFileEntry &v2) const
+ {
+ return path == v2.path && mode == v2.mode && sources == v2.sources && md5 == v2.md5;
+ }
+};
+typedef QList<VersionFileEntry> VersionFileList;
+
+/**
+ * Structure that describes an operation to perform when installing updates.
+ */
+struct MULTIMC_LOGIC_EXPORT Operation
+{
+ static Operation CopyOp(QString fsource, QString fdest, int fmode=0644)
+ {
+ return Operation{OP_REPLACE, fsource, fdest, fmode};
+ }
+ static Operation DeleteOp(QString file)
+ {
+ return Operation{OP_DELETE, file, "", 0644};
+ }
+
+ // FIXME: for some types, some of the other fields are irrelevant!
+ bool operator==(const Operation &u2) const
+ {
+ return type == u2.type && file == u2.file && dest == u2.dest && mode == u2.mode;
+ }
+
+ //! Specifies the type of operation that this is.
+ enum Type
+ {
+ OP_REPLACE,
+ OP_DELETE,
+ } type;
+
+ //! The file to operate on.
+ QString file;
+
+ //! The destination file.
+ QString dest;
+
+ //! The mode to change the source file to.
+ int mode;
+};
+typedef QList<Operation> OperationList;
+
+/**
+ * Loads the file list from the given version info JSON object into the given list.
+ */
+bool MULTIMC_LOGIC_EXPORT parseVersionInfo(const QByteArray &data, VersionFileList& list, QString &error);
+
+/*!
+ * 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.
+ */
+bool MULTIMC_LOGIC_EXPORT processFileLists
+(
+ const VersionFileList &currentVersion,
+ const VersionFileList &newVersion,
+ const QString &rootPath,
+ const QString &tempPath,
+ NetJobPtr job,
+ OperationList &ops
+);
+
+/*!
+ * This fixes destination paths for OSX - removes 'MultiMC.app' prefix
+ * The updater runs in MultiMC.app/Contents/MacOs by default
+ * The destination paths are such as this: MultiMC.app/blah/blah
+ *
+ * @return false if the path couldn't be fixed (is invalid)
+ */
+bool MULTIMC_LOGIC_EXPORT fixPathForOSX(QString &path);
+
+}
+Q_DECLARE_METATYPE(GoUpdate::Status); \ No newline at end of file
diff --git a/api/logic/updater/UpdateChecker.cpp b/api/logic/updater/UpdateChecker.cpp
new file mode 100644
index 00000000..1cdac916
--- /dev/null
+++ b/api/logic/updater/UpdateChecker.cpp
@@ -0,0 +1,269 @@
+/* Copyright 2013-2015 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 <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QDebug>
+
+#define API_VERSION 0
+#define CHANLIST_FORMAT 0
+
+UpdateChecker::UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild)
+{
+ m_channelListUrl = channelListUrl;
+ m_currentChannel = currentChannel;
+ m_currentBuild = currentBuild;
+
+ 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(QString updateChannel, bool notifyNoUpdate)
+{
+ qDebug() << "Checking for updates.";
+
+ // If the channel list hasn't loaded yet, load it and defer checking for updates until
+ // later.
+ if (!m_chanListLoaded)
+ {
+ qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring "
+ "update check.";
+ m_checkUpdateWaiting = true;
+ m_deferredUpdateChannel = updateChannel;
+ updateChanList(notifyNoUpdate);
+ return;
+ }
+
+ if (m_updateChecking)
+ {
+ qDebug() << "Ignoring update check request. Already checking for updates.";
+ return;
+ }
+
+ m_updateChecking = true;
+
+ // Find the desired channel within the channel list and get its repo URL. If if cannot be
+ // found, error.
+ m_newRepoUrl = "";
+ for (ChannelListEntry entry : m_channels)
+ {
+ if (entry.id == updateChannel)
+ m_newRepoUrl = entry.url;
+ if (entry.id == m_currentChannel)
+ m_currentRepoUrl = entry.url;
+ }
+
+ qDebug() << "m_repoUrl = " << m_newRepoUrl;
+
+ // If we didn't find our channel, error.
+ if (m_newRepoUrl.isEmpty())
+ {
+ qCritical() << "m_repoUrl is empty!";
+ emit updateCheckFailed();
+ return;
+ }
+
+ QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json"));
+
+ auto job = new NetJob("GoUpdate Repository Index");
+ job->addNetAction(ByteArrayDownload::make(indexUrl));
+ connect(job, &NetJob::succeeded, [this, notifyNoUpdate]()
+ { updateCheckFinished(notifyNoUpdate); });
+ connect(job, &NetJob::failed, this, &UpdateChecker::updateCheckFailed);
+ indexJob.reset(job);
+ job->start();
+}
+
+void UpdateChecker::updateCheckFinished(bool notifyNoUpdate)
+{
+ qDebug() << "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())
+ {
+ qCritical() << "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)
+ {
+ qCritical() << "Failed to check for updates. API version mismatch. We're using"
+ << API_VERSION << "server has" << apiVersion;
+ return;
+ }
+
+ qDebug() << "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())
+ {
+ 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 != m_currentBuild)
+ {
+ qDebug() << "Found newer version with ID" << newBuildNumber;
+ // Update!
+ GoUpdate::Status updateStatus;
+ updateStatus.updateAvailable = true;
+ updateStatus.currentVersionId = m_currentBuild;
+ updateStatus.currentRepoUrl = m_currentRepoUrl;
+ updateStatus.newVersionId = newBuildNumber;
+ updateStatus.newRepoUrl = m_newRepoUrl;
+ emit updateAvailable(updateStatus);
+ }
+ else if (notifyNoUpdate)
+ {
+ emit noUpdateFound();
+ }
+
+ m_updateChecking = false;
+}
+
+void UpdateChecker::updateCheckFailed()
+{
+ qCritical() << "Update check failed for reasons unknown.";
+}
+
+void UpdateChecker::updateChanList(bool notifyNoUpdate)
+{
+ qDebug() << "Loading the channel list.";
+
+ if (m_channelListUrl.isEmpty())
+ {
+ qCritical() << "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)));
+ connect(job, &NetJob::succeeded, [this, notifyNoUpdate]()
+ { chanListDownloadFinished(notifyNoUpdate); });
+ QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed);
+ chanListJob.reset(job);
+ job->start();
+}
+
+void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate)
+{
+ 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.
+ qCritical() << "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)
+ {
+ qCritical()
+ << "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())
+ {
+ qCritical() << "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;
+ qDebug() << "Successfully loaded UpdateChecker channel list.";
+
+ // If we're waiting to check for updates, do that now.
+ if (m_checkUpdateWaiting)
+ checkForUpdate(m_deferredUpdateChannel, notifyNoUpdate);
+
+ emit channelListLoaded();
+}
+
+void UpdateChecker::chanListDownloadFailed(QString reason)
+{
+ m_chanListLoading = false;
+ qCritical() << QString("Failed to download channel list: %1").arg(reason);
+ emit channelListLoaded();
+}
+
diff --git a/api/logic/updater/UpdateChecker.h b/api/logic/updater/UpdateChecker.h
new file mode 100644
index 00000000..c7fad10e
--- /dev/null
+++ b/api/logic/updater/UpdateChecker.h
@@ -0,0 +1,121 @@
+/* Copyright 2013-2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "net/NetJob.h"
+#include "GoUpdate.h"
+
+#include <QUrl>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT UpdateChecker : public QObject
+{
+ Q_OBJECT
+
+public:
+ UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild);
+ void checkForUpdate(QString updateChannel, bool notifyNoUpdate);
+
+ /*!
+ * 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(bool notifyNoUpdate);
+
+ /*!
+ * 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 false 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(GoUpdate::Status status);
+
+ //! Signal emitted when the channel list finishes loading or fails to load.
+ void channelListLoaded();
+
+ void noUpdateFound();
+
+private slots:
+ void updateCheckFinished(bool notifyNoUpdate);
+ void updateCheckFailed();
+
+ void chanListDownloadFinished(bool notifyNoUpdate);
+ void chanListDownloadFailed(QString reason);
+
+private:
+ friend class UpdateCheckerTest;
+
+ NetJobPtr indexJob;
+ NetJobPtr chanListJob;
+
+ QString m_channelListUrl;
+
+ 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;
+
+ /*!
+ * if m_checkUpdateWaiting, this is the last used update channel
+ */
+ QString m_deferredUpdateChannel;
+
+ int m_currentBuild = -1;
+ QString m_currentChannel;
+ QString m_currentRepoUrl;
+
+ QString m_newRepoUrl;
+};
+
diff --git a/api/logic/wonko/BaseWonkoEntity.cpp b/api/logic/wonko/BaseWonkoEntity.cpp
new file mode 100644
index 00000000..f5c59363
--- /dev/null
+++ b/api/logic/wonko/BaseWonkoEntity.cpp
@@ -0,0 +1,39 @@
+/* Copyright 2015 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 "BaseWonkoEntity.h"
+
+#include "Json.h"
+#include "WonkoUtil.h"
+
+BaseWonkoEntity::~BaseWonkoEntity()
+{
+}
+
+void BaseWonkoEntity::store() const
+{
+ Json::write(serialized(), Wonko::localWonkoDir().absoluteFilePath(localFilename()));
+}
+
+void BaseWonkoEntity::notifyLocalLoadComplete()
+{
+ m_localLoaded = true;
+ store();
+}
+void BaseWonkoEntity::notifyRemoteLoadComplete()
+{
+ m_remoteLoaded = true;
+ store();
+}
diff --git a/api/logic/wonko/BaseWonkoEntity.h b/api/logic/wonko/BaseWonkoEntity.h
new file mode 100644
index 00000000..191b4184
--- /dev/null
+++ b/api/logic/wonko/BaseWonkoEntity.h
@@ -0,0 +1,51 @@
+/* Copyright 2015 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 <QObject>
+#include <memory>
+
+#include "multimc_logic_export.h"
+
+class Task;
+
+class MULTIMC_LOGIC_EXPORT BaseWonkoEntity
+{
+public:
+ virtual ~BaseWonkoEntity();
+
+ using Ptr = std::shared_ptr<BaseWonkoEntity>;
+
+ virtual std::unique_ptr<Task> remoteUpdateTask() = 0;
+ virtual std::unique_ptr<Task> localUpdateTask() = 0;
+ virtual void merge(const std::shared_ptr<BaseWonkoEntity> &other) = 0;
+
+ void store() const;
+ virtual QString localFilename() const = 0;
+ virtual QJsonObject serialized() const = 0;
+
+ bool isComplete() const { return m_localLoaded || m_remoteLoaded; }
+
+ bool isLocalLoaded() const { return m_localLoaded; }
+ bool isRemoteLoaded() const { return m_remoteLoaded; }
+
+ void notifyLocalLoadComplete();
+ void notifyRemoteLoadComplete();
+
+private:
+ bool m_localLoaded = false;
+ bool m_remoteLoaded = false;
+};
diff --git a/api/logic/wonko/WonkoIndex.cpp b/api/logic/wonko/WonkoIndex.cpp
new file mode 100644
index 00000000..8306af84
--- /dev/null
+++ b/api/logic/wonko/WonkoIndex.cpp
@@ -0,0 +1,147 @@
+/* Copyright 2015 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 "WonkoIndex.h"
+
+#include "WonkoVersionList.h"
+#include "tasks/BaseWonkoEntityLocalLoadTask.h"
+#include "tasks/BaseWonkoEntityRemoteLoadTask.h"
+#include "format/WonkoFormat.h"
+
+WonkoIndex::WonkoIndex(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+WonkoIndex::WonkoIndex(const QVector<WonkoVersionListPtr> &lists, QObject *parent)
+ : QAbstractListModel(parent), m_lists(lists)
+{
+ for (int i = 0; i < m_lists.size(); ++i)
+ {
+ m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i));
+ connectVersionList(i, m_lists.at(i));
+ }
+}
+
+QVariant WonkoIndex::data(const QModelIndex &index, int role) const
+{
+ if (index.parent().isValid() || index.row() < 0 || index.row() >= m_lists.size())
+ {
+ return QVariant();
+ }
+
+ WonkoVersionListPtr list = m_lists.at(index.row());
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (index.column())
+ {
+ case 0: return list->humanReadable();
+ default: break;
+ }
+ case UidRole: return list->uid();
+ case NameRole: return list->name();
+ case ListPtrRole: return QVariant::fromValue(list);
+ }
+ return QVariant();
+}
+int WonkoIndex::rowCount(const QModelIndex &parent) const
+{
+ return m_lists.size();
+}
+int WonkoIndex::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+QVariant WonkoIndex::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0)
+ {
+ return tr("Name");
+ }
+ else
+ {
+ return QVariant();
+ }
+}
+
+std::unique_ptr<Task> WonkoIndex::remoteUpdateTask()
+{
+ return std::unique_ptr<WonkoIndexRemoteLoadTask>(new WonkoIndexRemoteLoadTask(this, this));
+}
+std::unique_ptr<Task> WonkoIndex::localUpdateTask()
+{
+ return std::unique_ptr<WonkoIndexLocalLoadTask>(new WonkoIndexLocalLoadTask(this, this));
+}
+
+QJsonObject WonkoIndex::serialized() const
+{
+ return WonkoFormat::serializeIndex(this);
+}
+
+bool WonkoIndex::hasUid(const QString &uid) const
+{
+ return m_uids.contains(uid);
+}
+WonkoVersionListPtr WonkoIndex::getList(const QString &uid) const
+{
+ return m_uids.value(uid, nullptr);
+}
+WonkoVersionListPtr WonkoIndex::getListGuaranteed(const QString &uid) const
+{
+ return m_uids.value(uid, std::make_shared<WonkoVersionList>(uid));
+}
+
+void WonkoIndex::merge(const Ptr &other)
+{
+ const QVector<WonkoVersionListPtr> lists = std::dynamic_pointer_cast<WonkoIndex>(other)->m_lists;
+ // initial load, no need to merge
+ if (m_lists.isEmpty())
+ {
+ beginResetModel();
+ m_lists = lists;
+ for (int i = 0; i < lists.size(); ++i)
+ {
+ m_uids.insert(lists.at(i)->uid(), lists.at(i));
+ connectVersionList(i, lists.at(i));
+ }
+ endResetModel();
+ }
+ else
+ {
+ for (const WonkoVersionListPtr &list : lists)
+ {
+ if (m_uids.contains(list->uid()))
+ {
+ m_uids[list->uid()]->merge(list);
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), m_lists.size(), m_lists.size());
+ connectVersionList(m_lists.size(), list);
+ m_lists.append(list);
+ m_uids.insert(list->uid(), list);
+ endInsertRows();
+ }
+ }
+ }
+}
+
+void WonkoIndex::connectVersionList(const int row, const WonkoVersionListPtr &list)
+{
+ connect(list.get(), &WonkoVersionList::nameChanged, this, [this, row]()
+ {
+ emit dataChanged(index(row), index(row), QVector<int>() << Qt::DisplayRole);
+ });
+}
diff --git a/api/logic/wonko/WonkoIndex.h b/api/logic/wonko/WonkoIndex.h
new file mode 100644
index 00000000..8b149c7d
--- /dev/null
+++ b/api/logic/wonko/WonkoIndex.h
@@ -0,0 +1,68 @@
+/* Copyright 2015 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 <QAbstractListModel>
+#include <memory>
+
+#include "BaseWonkoEntity.h"
+
+#include "multimc_logic_export.h"
+
+class Task;
+using WonkoVersionListPtr = std::shared_ptr<class WonkoVersionList>;
+
+class MULTIMC_LOGIC_EXPORT WonkoIndex : public QAbstractListModel, public BaseWonkoEntity
+{
+ Q_OBJECT
+public:
+ explicit WonkoIndex(QObject *parent = nullptr);
+ explicit WonkoIndex(const QVector<WonkoVersionListPtr> &lists, QObject *parent = nullptr);
+
+ enum
+ {
+ UidRole = Qt::UserRole,
+ NameRole,
+ ListPtrRole
+ };
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ std::unique_ptr<Task> remoteUpdateTask() override;
+ std::unique_ptr<Task> localUpdateTask() override;
+
+ QString localFilename() const override { return "index.json"; }
+ QJsonObject serialized() const override;
+
+ // queries
+ bool hasUid(const QString &uid) const;
+ WonkoVersionListPtr getList(const QString &uid) const;
+ WonkoVersionListPtr getListGuaranteed(const QString &uid) const;
+
+ QVector<WonkoVersionListPtr> lists() const { return m_lists; }
+
+public: // for usage by parsers only
+ void merge(const BaseWonkoEntity::Ptr &other);
+
+private:
+ QVector<WonkoVersionListPtr> m_lists;
+ QHash<QString, WonkoVersionListPtr> m_uids;
+
+ void connectVersionList(const int row, const WonkoVersionListPtr &list);
+};
diff --git a/api/logic/wonko/WonkoReference.cpp b/api/logic/wonko/WonkoReference.cpp
new file mode 100644
index 00000000..519d59aa
--- /dev/null
+++ b/api/logic/wonko/WonkoReference.cpp
@@ -0,0 +1,44 @@
+/* Copyright 2015 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 "WonkoReference.h"
+
+WonkoReference::WonkoReference(const QString &uid)
+ : m_uid(uid)
+{
+}
+
+QString WonkoReference::uid() const
+{
+ return m_uid;
+}
+
+QString WonkoReference::version() const
+{
+ return m_version;
+}
+void WonkoReference::setVersion(const QString &version)
+{
+ m_version = version;
+}
+
+bool WonkoReference::operator==(const WonkoReference &other) const
+{
+ return m_uid == other.m_uid && m_version == other.m_version;
+}
+bool WonkoReference::operator!=(const WonkoReference &other) const
+{
+ return m_uid != other.m_uid || m_version != other.m_version;
+}
diff --git a/api/logic/wonko/WonkoReference.h b/api/logic/wonko/WonkoReference.h
new file mode 100644
index 00000000..73a85d76
--- /dev/null
+++ b/api/logic/wonko/WonkoReference.h
@@ -0,0 +1,41 @@
+/* Copyright 2015 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>
+#include <QMetaType>
+
+#include "multimc_logic_export.h"
+
+class MULTIMC_LOGIC_EXPORT WonkoReference
+{
+public:
+ WonkoReference() {}
+ explicit WonkoReference(const QString &uid);
+
+ QString uid() const;
+
+ QString version() const;
+ void setVersion(const QString &version);
+
+ bool operator==(const WonkoReference &other) const;
+ bool operator!=(const WonkoReference &other) const;
+
+private:
+ QString m_uid;
+ QString m_version;
+};
+Q_DECLARE_METATYPE(WonkoReference)
diff --git a/api/logic/wonko/WonkoUtil.cpp b/api/logic/wonko/WonkoUtil.cpp
new file mode 100644
index 00000000..94726c6b
--- /dev/null
+++ b/api/logic/wonko/WonkoUtil.cpp
@@ -0,0 +1,47 @@
+/* Copyright 2015 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 "WonkoUtil.h"
+
+#include <QUrl>
+#include <QDir>
+
+#include "Env.h"
+
+namespace Wonko
+{
+QUrl rootUrl()
+{
+ return ENV.wonkoRootUrl();
+}
+QUrl indexUrl()
+{
+ return rootUrl().resolved(QStringLiteral("index.json"));
+}
+QUrl versionListUrl(const QString &uid)
+{
+ return rootUrl().resolved(uid + ".json");
+}
+QUrl versionUrl(const QString &uid, const QString &version)
+{
+ return rootUrl().resolved(uid + "/" + version + ".json");
+}
+
+QDir localWonkoDir()
+{
+ return QDir("wonko");
+}
+
+}
diff --git a/api/logic/wonko/WonkoUtil.h b/api/logic/wonko/WonkoUtil.h
new file mode 100644
index 00000000..b618ab71
--- /dev/null
+++ b/api/logic/wonko/WonkoUtil.h
@@ -0,0 +1,31 @@
+/* Copyright 2015 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 "multimc_logic_export.h"
+
+class QUrl;
+class QString;
+class QDir;
+
+namespace Wonko
+{
+MULTIMC_LOGIC_EXPORT QUrl rootUrl();
+MULTIMC_LOGIC_EXPORT QUrl indexUrl();
+MULTIMC_LOGIC_EXPORT QUrl versionListUrl(const QString &uid);
+MULTIMC_LOGIC_EXPORT QUrl versionUrl(const QString &uid, const QString &version);
+MULTIMC_LOGIC_EXPORT QDir localWonkoDir();
+}
diff --git a/api/logic/wonko/WonkoVersion.cpp b/api/logic/wonko/WonkoVersion.cpp
new file mode 100644
index 00000000..7b7da86c
--- /dev/null
+++ b/api/logic/wonko/WonkoVersion.cpp
@@ -0,0 +1,102 @@
+/* Copyright 2015 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 "WonkoVersion.h"
+
+#include <QDateTime>
+
+#include "tasks/BaseWonkoEntityLocalLoadTask.h"
+#include "tasks/BaseWonkoEntityRemoteLoadTask.h"
+#include "format/WonkoFormat.h"
+
+WonkoVersion::WonkoVersion(const QString &uid, const QString &version)
+ : BaseVersion(), m_uid(uid), m_version(version)
+{
+}
+
+QString WonkoVersion::descriptor()
+{
+ return m_version;
+}
+QString WonkoVersion::name()
+{
+ return m_version;
+}
+QString WonkoVersion::typeString() const
+{
+ return m_type;
+}
+
+QDateTime WonkoVersion::time() const
+{
+ return QDateTime::fromMSecsSinceEpoch(m_time * 1000, Qt::UTC);
+}
+
+std::unique_ptr<Task> WonkoVersion::remoteUpdateTask()
+{
+ return std::unique_ptr<WonkoVersionRemoteLoadTask>(new WonkoVersionRemoteLoadTask(this, this));
+}
+std::unique_ptr<Task> WonkoVersion::localUpdateTask()
+{
+ return std::unique_ptr<WonkoVersionLocalLoadTask>(new WonkoVersionLocalLoadTask(this, this));
+}
+
+void WonkoVersion::merge(const std::shared_ptr<BaseWonkoEntity> &other)
+{
+ WonkoVersionPtr version = std::dynamic_pointer_cast<WonkoVersion>(other);
+ if (m_type != version->m_type)
+ {
+ setType(version->m_type);
+ }
+ if (m_time != version->m_time)
+ {
+ setTime(version->m_time);
+ }
+ if (m_requires != version->m_requires)
+ {
+ setRequires(version->m_requires);
+ }
+
+ setData(version->m_data);
+}
+
+QString WonkoVersion::localFilename() const
+{
+ return m_uid + '/' + m_version + ".json";
+}
+QJsonObject WonkoVersion::serialized() const
+{
+ return WonkoFormat::serializeVersion(this);
+}
+
+void WonkoVersion::setType(const QString &type)
+{
+ m_type = type;
+ emit typeChanged();
+}
+void WonkoVersion::setTime(const qint64 time)
+{
+ m_time = time;
+ emit timeChanged();
+}
+void WonkoVersion::setRequires(const QVector<WonkoReference> &requires)
+{
+ m_requires = requires;
+ emit requiresChanged();
+}
+void WonkoVersion::setData(const VersionFilePtr &data)
+{
+ m_data = data;
+}
diff --git a/api/logic/wonko/WonkoVersion.h b/api/logic/wonko/WonkoVersion.h
new file mode 100644
index 00000000..a1de4d9b
--- /dev/null
+++ b/api/logic/wonko/WonkoVersion.h
@@ -0,0 +1,83 @@
+/* Copyright 2015 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 "BaseVersion.h"
+#include "BaseWonkoEntity.h"
+
+#include <QVector>
+#include <QStringList>
+#include <QJsonObject>
+#include <memory>
+
+#include "minecraft/VersionFile.h"
+#include "WonkoReference.h"
+
+#include "multimc_logic_export.h"
+
+using WonkoVersionPtr = std::shared_ptr<class WonkoVersion>;
+
+class MULTIMC_LOGIC_EXPORT WonkoVersion : public QObject, public BaseVersion, public BaseWonkoEntity
+{
+ Q_OBJECT
+ Q_PROPERTY(QString uid READ uid CONSTANT)
+ Q_PROPERTY(QString version READ version CONSTANT)
+ Q_PROPERTY(QString type READ type NOTIFY typeChanged)
+ Q_PROPERTY(QDateTime time READ time NOTIFY timeChanged)
+ Q_PROPERTY(QVector<WonkoReference> requires READ requires NOTIFY requiresChanged)
+public:
+ explicit WonkoVersion(const QString &uid, const QString &version);
+
+ QString descriptor() override;
+ QString name() override;
+ QString typeString() const override;
+
+ QString uid() const { return m_uid; }
+ QString version() const { return m_version; }
+ QString type() const { return m_type; }
+ QDateTime time() const;
+ qint64 rawTime() const { return m_time; }
+ QVector<WonkoReference> requires() const { return m_requires; }
+ VersionFilePtr data() const { return m_data; }
+
+ std::unique_ptr<Task> remoteUpdateTask() override;
+ std::unique_ptr<Task> localUpdateTask() override;
+ void merge(const std::shared_ptr<BaseWonkoEntity> &other) override;
+
+ QString localFilename() const override;
+ QJsonObject serialized() const override;
+
+public: // for usage by format parsers only
+ void setType(const QString &type);
+ void setTime(const qint64 time);
+ void setRequires(const QVector<WonkoReference> &requires);
+ void setData(const VersionFilePtr &data);
+
+signals:
+ void typeChanged();
+ void timeChanged();
+ void requiresChanged();
+
+private:
+ QString m_uid;
+ QString m_version;
+ QString m_type;
+ qint64 m_time;
+ QVector<WonkoReference> m_requires;
+ VersionFilePtr m_data;
+};
+
+Q_DECLARE_METATYPE(WonkoVersionPtr)
diff --git a/api/logic/wonko/WonkoVersionList.cpp b/api/logic/wonko/WonkoVersionList.cpp
new file mode 100644
index 00000000..e9d79327
--- /dev/null
+++ b/api/logic/wonko/WonkoVersionList.cpp
@@ -0,0 +1,283 @@
+/* Copyright 2015 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 "WonkoVersionList.h"
+
+#include <QDateTime>
+
+#include "WonkoVersion.h"
+#include "tasks/BaseWonkoEntityRemoteLoadTask.h"
+#include "tasks/BaseWonkoEntityLocalLoadTask.h"
+#include "format/WonkoFormat.h"
+#include "WonkoReference.h"
+
+class WVLLoadTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit WVLLoadTask(WonkoVersionList *list, QObject *parent = nullptr)
+ : Task(parent), m_list(list)
+ {
+ }
+
+ bool canAbort() const override
+ {
+ return !m_currentTask || m_currentTask->canAbort();
+ }
+ bool abort() override
+ {
+ return m_currentTask->abort();
+ }
+
+private:
+ void executeTask() override
+ {
+ if (!m_list->isLocalLoaded())
+ {
+ m_currentTask = m_list->localUpdateTask();
+ connect(m_currentTask.get(), &Task::succeeded, this, &WVLLoadTask::next);
+ }
+ else
+ {
+ m_currentTask = m_list->remoteUpdateTask();
+ connect(m_currentTask.get(), &Task::succeeded, this, &WVLLoadTask::emitSucceeded);
+ }
+ connect(m_currentTask.get(), &Task::status, this, &WVLLoadTask::setStatus);
+ connect(m_currentTask.get(), &Task::progress, this, &WVLLoadTask::setProgress);
+ connect(m_currentTask.get(), &Task::failed, this, &WVLLoadTask::emitFailed);
+ m_currentTask->start();
+ }
+
+ void next()
+ {
+ m_currentTask = m_list->remoteUpdateTask();
+ connect(m_currentTask.get(), &Task::status, this, &WVLLoadTask::setStatus);
+ connect(m_currentTask.get(), &Task::progress, this, &WVLLoadTask::setProgress);
+ connect(m_currentTask.get(), &Task::succeeded, this, &WVLLoadTask::emitSucceeded);
+ m_currentTask->start();
+ }
+
+ WonkoVersionList *m_list;
+ std::unique_ptr<Task> m_currentTask;
+};
+
+WonkoVersionList::WonkoVersionList(const QString &uid, QObject *parent)
+ : BaseVersionList(parent), m_uid(uid)
+{
+ setObjectName("Wonko version list: " + uid);
+}
+
+Task *WonkoVersionList::getLoadTask()
+{
+ return new WVLLoadTask(this);
+}
+
+bool WonkoVersionList::isLoaded()
+{
+ return isLocalLoaded() && isRemoteLoaded();
+}
+
+const BaseVersionPtr WonkoVersionList::at(int i) const
+{
+ return m_versions.at(i);
+}
+int WonkoVersionList::count() const
+{
+ return m_versions.size();
+}
+
+void WonkoVersionList::sortVersions()
+{
+ beginResetModel();
+ std::sort(m_versions.begin(), m_versions.end(), [](const WonkoVersionPtr &a, const WonkoVersionPtr &b)
+ {
+ return *a.get() < *b.get();
+ });
+ endResetModel();
+}
+
+QVariant WonkoVersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_versions.size() || index.parent().isValid())
+ {
+ return QVariant();
+ }
+
+ WonkoVersionPtr version = m_versions.at(index.row());
+
+ switch (role)
+ {
+ case VersionPointerRole: return QVariant::fromValue(std::dynamic_pointer_cast<BaseVersion>(version));
+ case VersionRole:
+ case VersionIdRole:
+ return version->version();
+ case ParentGameVersionRole:
+ {
+ const auto end = version->requires().end();
+ const auto it = std::find_if(version->requires().begin(), end,
+ [](const WonkoReference &ref) { return ref.uid() == "net.minecraft"; });
+ if (it != end)
+ {
+ return (*it).version();
+ }
+ return QVariant();
+ }
+ case TypeRole: return version->type();
+
+ case UidRole: return version->uid();
+ case TimeRole: return version->time();
+ case RequiresRole: return QVariant::fromValue(version->requires());
+ case SortRole: return version->rawTime();
+ case WonkoVersionPtrRole: return QVariant::fromValue(version);
+ case RecommendedRole: return version == getRecommended();
+ case LatestRole: return version == getLatestStable();
+ default: return QVariant();
+ }
+}
+
+BaseVersionList::RoleList WonkoVersionList::providesRoles() const
+{
+ return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole,
+ TypeRole, UidRole, TimeRole, RequiresRole, SortRole,
+ RecommendedRole, LatestRole, WonkoVersionPtrRole};
+}
+
+QHash<int, QByteArray> WonkoVersionList::roleNames() const
+{
+ QHash<int, QByteArray> roles = BaseVersionList::roleNames();
+ roles.insert(UidRole, "uid");
+ roles.insert(TimeRole, "time");
+ roles.insert(SortRole, "sort");
+ roles.insert(RequiresRole, "requires");
+ return roles;
+}
+
+std::unique_ptr<Task> WonkoVersionList::remoteUpdateTask()
+{
+ return std::unique_ptr<WonkoVersionListRemoteLoadTask>(new WonkoVersionListRemoteLoadTask(this, this));
+}
+std::unique_ptr<Task> WonkoVersionList::localUpdateTask()
+{
+ return std::unique_ptr<WonkoVersionListLocalLoadTask>(new WonkoVersionListLocalLoadTask(this, this));
+}
+
+QString WonkoVersionList::localFilename() const
+{
+ return m_uid + ".json";
+}
+QJsonObject WonkoVersionList::serialized() const
+{
+ return WonkoFormat::serializeVersionList(this);
+}
+
+QString WonkoVersionList::humanReadable() const
+{
+ return m_name.isEmpty() ? m_uid : m_name;
+}
+
+bool WonkoVersionList::hasVersion(const QString &version) const
+{
+ return m_lookup.contains(version);
+}
+WonkoVersionPtr WonkoVersionList::getVersion(const QString &version) const
+{
+ return m_lookup.value(version);
+}
+
+void WonkoVersionList::setName(const QString &name)
+{
+ m_name = name;
+ emit nameChanged(name);
+}
+void WonkoVersionList::setVersions(const QVector<WonkoVersionPtr> &versions)
+{
+ beginResetModel();
+ m_versions = versions;
+ std::sort(m_versions.begin(), m_versions.end(), [](const WonkoVersionPtr &a, const WonkoVersionPtr &b)
+ {
+ return a->rawTime() > b->rawTime();
+ });
+ for (int i = 0; i < m_versions.size(); ++i)
+ {
+ m_lookup.insert(m_versions.at(i)->version(), m_versions.at(i));
+ setupAddedVersion(i, m_versions.at(i));
+ }
+
+ m_latest = m_versions.isEmpty() ? nullptr : m_versions.first();
+ auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const WonkoVersionPtr &ptr) { return ptr->type() == "release"; });
+ m_recommended = recommendedIt == m_versions.constEnd() ? nullptr : *recommendedIt;
+ endResetModel();
+}
+
+void WonkoVersionList::merge(const BaseWonkoEntity::Ptr &other)
+{
+ const WonkoVersionListPtr list = std::dynamic_pointer_cast<WonkoVersionList>(other);
+ if (m_name != list->m_name)
+ {
+ setName(list->m_name);
+ }
+
+ if (m_versions.isEmpty())
+ {
+ setVersions(list->m_versions);
+ }
+ else
+ {
+ for (const WonkoVersionPtr &version : list->m_versions)
+ {
+ if (m_lookup.contains(version->version()))
+ {
+ m_lookup.value(version->version())->merge(version);
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), m_versions.size(), m_versions.size());
+ setupAddedVersion(m_versions.size(), version);
+ m_versions.append(version);
+ m_lookup.insert(version->uid(), version);
+ endInsertRows();
+
+ if (!m_latest || version->rawTime() > m_latest->rawTime())
+ {
+ m_latest = version;
+ emit dataChanged(index(0), index(m_versions.size() - 1), QVector<int>() << LatestRole);
+ }
+ if (!m_recommended || (version->type() == "release" && version->rawTime() > m_recommended->rawTime()))
+ {
+ m_recommended = version;
+ emit dataChanged(index(0), index(m_versions.size() - 1), QVector<int>() << RecommendedRole);
+ }
+ }
+ }
+ }
+}
+
+void WonkoVersionList::setupAddedVersion(const int row, const WonkoVersionPtr &version)
+{
+ connect(version.get(), &WonkoVersion::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << RequiresRole); });
+ connect(version.get(), &WonkoVersion::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TimeRole << SortRole); });
+ connect(version.get(), &WonkoVersion::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TypeRole); });
+}
+
+BaseVersionPtr WonkoVersionList::getLatestStable() const
+{
+ return m_latest;
+}
+BaseVersionPtr WonkoVersionList::getRecommended() const
+{
+ return m_recommended;
+}
+
+#include "WonkoVersionList.moc"
diff --git a/api/logic/wonko/WonkoVersionList.h b/api/logic/wonko/WonkoVersionList.h
new file mode 100644
index 00000000..8ea35be6
--- /dev/null
+++ b/api/logic/wonko/WonkoVersionList.h
@@ -0,0 +1,92 @@
+/* Copyright 2015 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 "BaseVersionList.h"
+#include "BaseWonkoEntity.h"
+#include <memory>
+
+using WonkoVersionPtr = std::shared_ptr<class WonkoVersion>;
+using WonkoVersionListPtr = std::shared_ptr<class WonkoVersionList>;
+
+class MULTIMC_LOGIC_EXPORT WonkoVersionList : public BaseVersionList, public BaseWonkoEntity
+{
+ Q_OBJECT
+ Q_PROPERTY(QString uid READ uid CONSTANT)
+ Q_PROPERTY(QString name READ name NOTIFY nameChanged)
+public:
+ explicit WonkoVersionList(const QString &uid, QObject *parent = nullptr);
+
+ enum Roles
+ {
+ UidRole = Qt::UserRole + 100,
+ TimeRole,
+ RequiresRole,
+ WonkoVersionPtrRole
+ };
+
+ Task *getLoadTask() override;
+ bool isLoaded() override;
+ const BaseVersionPtr at(int i) const override;
+ int count() const override;
+ void sortVersions() override;
+
+ BaseVersionPtr getLatestStable() const override;
+ BaseVersionPtr getRecommended() const override;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ RoleList providesRoles() const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ std::unique_ptr<Task> remoteUpdateTask() override;
+ std::unique_ptr<Task> localUpdateTask() override;
+
+ QString localFilename() const override;
+ QJsonObject serialized() const override;
+
+ QString uid() const { return m_uid; }
+ QString name() const { return m_name; }
+ QString humanReadable() const;
+
+ bool hasVersion(const QString &version) const;
+ WonkoVersionPtr getVersion(const QString &version) const;
+
+ QVector<WonkoVersionPtr> versions() const { return m_versions; }
+
+public: // for usage only by parsers
+ void setName(const QString &name);
+ void setVersions(const QVector<WonkoVersionPtr> &versions);
+ void merge(const BaseWonkoEntity::Ptr &other);
+
+signals:
+ void nameChanged(const QString &name);
+
+protected slots:
+ void updateListData(QList<BaseVersionPtr> versions) override {}
+
+private:
+ QVector<WonkoVersionPtr> m_versions;
+ QHash<QString, WonkoVersionPtr> m_lookup;
+ QString m_uid;
+ QString m_name;
+
+ WonkoVersionPtr m_recommended;
+ WonkoVersionPtr m_latest;
+
+ void setupAddedVersion(const int row, const WonkoVersionPtr &version);
+};
+
+Q_DECLARE_METATYPE(WonkoVersionListPtr)
diff --git a/api/logic/wonko/format/WonkoFormat.cpp b/api/logic/wonko/format/WonkoFormat.cpp
new file mode 100644
index 00000000..11192cbe
--- /dev/null
+++ b/api/logic/wonko/format/WonkoFormat.cpp
@@ -0,0 +1,80 @@
+/* Copyright 2015 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 "WonkoFormat.h"
+
+#include "WonkoFormatV1.h"
+
+#include "wonko/WonkoIndex.h"
+#include "wonko/WonkoVersion.h"
+#include "wonko/WonkoVersionList.h"
+
+static int formatVersion(const QJsonObject &obj)
+{
+ if (!obj.contains("formatVersion")) {
+ throw WonkoParseException(QObject::tr("Missing required field: 'formatVersion'"));
+ }
+ if (!obj.value("formatVersion").isDouble()) {
+ throw WonkoParseException(QObject::tr("Required field has invalid type: 'formatVersion'"));
+ }
+ return obj.value("formatVersion").toInt();
+}
+
+void WonkoFormat::parseIndex(const QJsonObject &obj, WonkoIndex *ptr)
+{
+ const int version = formatVersion(obj);
+ switch (version) {
+ case 1:
+ ptr->merge(WonkoFormatV1().parseIndexInternal(obj));
+ break;
+ default:
+ throw WonkoParseException(QObject::tr("Unknown formatVersion: %1").arg(version));
+ }
+}
+void WonkoFormat::parseVersion(const QJsonObject &obj, WonkoVersion *ptr)
+{
+ const int version = formatVersion(obj);
+ switch (version) {
+ case 1:
+ ptr->merge(WonkoFormatV1().parseVersionInternal(obj));
+ break;
+ default:
+ throw WonkoParseException(QObject::tr("Unknown formatVersion: %1").arg(version));
+ }
+}
+void WonkoFormat::parseVersionList(const QJsonObject &obj, WonkoVersionList *ptr)
+{
+ const int version = formatVersion(obj);
+ switch (version) {
+ case 10:
+ ptr->merge(WonkoFormatV1().parseVersionListInternal(obj));
+ break;
+ default:
+ throw WonkoParseException(QObject::tr("Unknown formatVersion: %1").arg(version));
+ }
+}
+
+QJsonObject WonkoFormat::serializeIndex(const WonkoIndex *ptr)
+{
+ return WonkoFormatV1().serializeIndexInternal(ptr);
+}
+QJsonObject WonkoFormat::serializeVersion(const WonkoVersion *ptr)
+{
+ return WonkoFormatV1().serializeVersionInternal(ptr);
+}
+QJsonObject WonkoFormat::serializeVersionList(const WonkoVersionList *ptr)
+{
+ return WonkoFormatV1().serializeVersionListInternal(ptr);
+}
diff --git a/api/logic/wonko/format/WonkoFormat.h b/api/logic/wonko/format/WonkoFormat.h
new file mode 100644
index 00000000..450d6ccc
--- /dev/null
+++ b/api/logic/wonko/format/WonkoFormat.h
@@ -0,0 +1,54 @@
+/* Copyright 2015 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 <QJsonObject>
+#include <memory>
+
+#include "Exception.h"
+#include "wonko/BaseWonkoEntity.h"
+
+class WonkoIndex;
+class WonkoVersion;
+class WonkoVersionList;
+
+class WonkoParseException : public Exception
+{
+public:
+ using Exception::Exception;
+};
+
+class WonkoFormat
+{
+public:
+ virtual ~WonkoFormat() {}
+
+ static void parseIndex(const QJsonObject &obj, WonkoIndex *ptr);
+ static void parseVersion(const QJsonObject &obj, WonkoVersion *ptr);
+ static void parseVersionList(const QJsonObject &obj, WonkoVersionList *ptr);
+
+ static QJsonObject serializeIndex(const WonkoIndex *ptr);
+ static QJsonObject serializeVersion(const WonkoVersion *ptr);
+ static QJsonObject serializeVersionList(const WonkoVersionList *ptr);
+
+protected:
+ virtual BaseWonkoEntity::Ptr parseIndexInternal(const QJsonObject &obj) const = 0;
+ virtual BaseWonkoEntity::Ptr parseVersionInternal(const QJsonObject &obj) const = 0;
+ virtual BaseWonkoEntity::Ptr parseVersionListInternal(const QJsonObject &obj) const = 0;
+ virtual QJsonObject serializeIndexInternal(const WonkoIndex *ptr) const = 0;
+ virtual QJsonObject serializeVersionInternal(const WonkoVersion *ptr) const = 0;
+ virtual QJsonObject serializeVersionListInternal(const WonkoVersionList *ptr) const = 0;
+};
diff --git a/api/logic/wonko/format/WonkoFormatV1.cpp b/api/logic/wonko/format/WonkoFormatV1.cpp
new file mode 100644
index 00000000..363eebfb
--- /dev/null
+++ b/api/logic/wonko/format/WonkoFormatV1.cpp
@@ -0,0 +1,156 @@
+/* Copyright 2015 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 "WonkoFormatV1.h"
+#include <minecraft/onesix/OneSixVersionFormat.h>
+
+#include "Json.h"
+
+#include "wonko/WonkoIndex.h"
+#include "wonko/WonkoVersion.h"
+#include "wonko/WonkoVersionList.h"
+#include "Env.h"
+
+using namespace Json;
+
+static WonkoVersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj)
+{
+ const QVector<QJsonObject> requiresRaw = obj.contains("requires") ? requireIsArrayOf<QJsonObject>(obj, "requires") : QVector<QJsonObject>();
+ QVector<WonkoReference> requires;
+ requires.reserve(requiresRaw.size());
+ std::transform(requiresRaw.begin(), requiresRaw.end(), std::back_inserter(requires), [](const QJsonObject &rObj)
+ {
+ WonkoReference ref(requireString(rObj, "uid"));
+ ref.setVersion(ensureString(rObj, "version", QString()));
+ return ref;
+ });
+
+ WonkoVersionPtr version = std::make_shared<WonkoVersion>(uid, requireString(obj, "version"));
+ if (obj.value("time").isString())
+ {
+ version->setTime(QDateTime::fromString(requireString(obj, "time"), Qt::ISODate).toMSecsSinceEpoch() / 1000);
+ }
+ else
+ {
+ version->setTime(requireInteger(obj, "time"));
+ }
+ version->setType(ensureString(obj, "type", QString()));
+ version->setRequires(requires);
+ return version;
+}
+static void serializeCommonVersion(const WonkoVersion *version, QJsonObject &obj)
+{
+ QJsonArray requires;
+ for (const WonkoReference &ref : version->requires())
+ {
+ if (ref.version().isEmpty())
+ {
+ requires.append(QJsonObject({{"uid", ref.uid()}}));
+ }
+ else
+ {
+ requires.append(QJsonObject({
+ {"uid", ref.uid()},
+ {"version", ref.version()}
+ }));
+ }
+ }
+
+ obj.insert("version", version->version());
+ obj.insert("type", version->type());
+ obj.insert("time", version->time().toString(Qt::ISODate));
+ obj.insert("requires", requires);
+}
+
+BaseWonkoEntity::Ptr WonkoFormatV1::parseIndexInternal(const QJsonObject &obj) const
+{
+ const QVector<QJsonObject> objects = requireIsArrayOf<QJsonObject>(obj, "index");
+ QVector<WonkoVersionListPtr> lists;
+ lists.reserve(objects.size());
+ std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject &obj)
+ {
+ WonkoVersionListPtr list = std::make_shared<WonkoVersionList>(requireString(obj, "uid"));
+ list->setName(ensureString(obj, "name", QString()));
+ return list;
+ });
+ return std::make_shared<WonkoIndex>(lists);
+}
+BaseWonkoEntity::Ptr WonkoFormatV1::parseVersionInternal(const QJsonObject &obj) const
+{
+ WonkoVersionPtr version = parseCommonVersion(requireString(obj, "uid"), obj);
+
+ version->setData(OneSixVersionFormat::versionFileFromJson(QJsonDocument(obj),
+ QString("%1/%2.json").arg(version->uid(), version->version()),
+ obj.contains("order")));
+ return version;
+}
+BaseWonkoEntity::Ptr WonkoFormatV1::parseVersionListInternal(const QJsonObject &obj) const
+{
+ const QString uid = requireString(obj, "uid");
+
+ const QVector<QJsonObject> versionsRaw = requireIsArrayOf<QJsonObject>(obj, "versions");
+ QVector<WonkoVersionPtr> versions;
+ versions.reserve(versionsRaw.size());
+ std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [this, uid](const QJsonObject &vObj)
+ { return parseCommonVersion(uid, vObj); });
+
+ WonkoVersionListPtr list = std::make_shared<WonkoVersionList>(uid);
+ list->setName(ensureString(obj, "name", QString()));
+ list->setVersions(versions);
+ return list;
+}
+
+QJsonObject WonkoFormatV1::serializeIndexInternal(const WonkoIndex *ptr) const
+{
+ QJsonArray index;
+ for (const WonkoVersionListPtr &list : ptr->lists())
+ {
+ index.append(QJsonObject({
+ {"uid", list->uid()},
+ {"name", list->name()}
+ }));
+ }
+ return QJsonObject({
+ {"formatVersion", 1},
+ {"index", index}
+ });
+}
+QJsonObject WonkoFormatV1::serializeVersionInternal(const WonkoVersion *ptr) const
+{
+ QJsonObject obj = OneSixVersionFormat::versionFileToJson(ptr->data(), true).object();
+ serializeCommonVersion(ptr, obj);
+ obj.insert("formatVersion", 1);
+ obj.insert("uid", ptr->uid());
+ // TODO: the name should be looked up in the UI based on the uid
+ obj.insert("name", ENV.wonkoIndex()->getListGuaranteed(ptr->uid())->name());
+
+ return obj;
+}
+QJsonObject WonkoFormatV1::serializeVersionListInternal(const WonkoVersionList *ptr) const
+{
+ QJsonArray versions;
+ for (const WonkoVersionPtr &version : ptr->versions())
+ {
+ QJsonObject obj;
+ serializeCommonVersion(version.get(), obj);
+ versions.append(obj);
+ }
+ return QJsonObject({
+ {"formatVersion", 10},
+ {"uid", ptr->uid()},
+ {"name", ptr->name().isNull() ? QJsonValue() : ptr->name()},
+ {"versions", versions}
+ });
+}
diff --git a/api/logic/wonko/format/WonkoFormatV1.h b/api/logic/wonko/format/WonkoFormatV1.h
new file mode 100644
index 00000000..92759804
--- /dev/null
+++ b/api/logic/wonko/format/WonkoFormatV1.h
@@ -0,0 +1,30 @@
+/* Copyright 2015 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 "WonkoFormat.h"
+
+class WonkoFormatV1 : public WonkoFormat
+{
+public:
+ BaseWonkoEntity::Ptr parseIndexInternal(const QJsonObject &obj) const override;
+ BaseWonkoEntity::Ptr parseVersionInternal(const QJsonObject &obj) const override;
+ BaseWonkoEntity::Ptr parseVersionListInternal(const QJsonObject &obj) const override;
+
+ QJsonObject serializeIndexInternal(const WonkoIndex *ptr) const override;
+ QJsonObject serializeVersionInternal(const WonkoVersion *ptr) const override;
+ QJsonObject serializeVersionListInternal(const WonkoVersionList *ptr) const override;
+};
diff --git a/api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp b/api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp
new file mode 100644
index 00000000..b54c592f
--- /dev/null
+++ b/api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp
@@ -0,0 +1,117 @@
+/* Copyright 2015 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 "BaseWonkoEntityLocalLoadTask.h"
+
+#include <QFile>
+
+#include "wonko/format/WonkoFormat.h"
+#include "wonko/WonkoUtil.h"
+#include "wonko/WonkoIndex.h"
+#include "wonko/WonkoVersion.h"
+#include "wonko/WonkoVersionList.h"
+#include "Env.h"
+#include "Json.h"
+
+BaseWonkoEntityLocalLoadTask::BaseWonkoEntityLocalLoadTask(BaseWonkoEntity *entity, QObject *parent)
+ : Task(parent), m_entity(entity)
+{
+}
+
+void BaseWonkoEntityLocalLoadTask::executeTask()
+{
+ const QString fname = Wonko::localWonkoDir().absoluteFilePath(filename());
+ if (!QFile::exists(fname))
+ {
+ emitFailed(tr("File doesn't exist"));
+ return;
+ }
+
+ setStatus(tr("Reading %1...").arg(name()));
+ setProgress(0, 0);
+
+ try
+ {
+ parse(Json::requireObject(Json::requireDocument(fname, name()), name()));
+ m_entity->notifyLocalLoadComplete();
+ emitSucceeded();
+ }
+ catch (Exception &e)
+ {
+ emitFailed(tr("Unable to parse file %1: %2").arg(fname, e.cause()));
+ }
+}
+
+// WONKO INDEX //
+WonkoIndexLocalLoadTask::WonkoIndexLocalLoadTask(WonkoIndex *index, QObject *parent)
+ : BaseWonkoEntityLocalLoadTask(index, parent)
+{
+}
+QString WonkoIndexLocalLoadTask::filename() const
+{
+ return "index.json";
+}
+QString WonkoIndexLocalLoadTask::name() const
+{
+ return tr("Wonko Index");
+}
+void WonkoIndexLocalLoadTask::parse(const QJsonObject &obj) const
+{
+ WonkoFormat::parseIndex(obj, dynamic_cast<WonkoIndex *>(entity()));
+}
+
+// WONKO VERSION LIST //
+WonkoVersionListLocalLoadTask::WonkoVersionListLocalLoadTask(WonkoVersionList *list, QObject *parent)
+ : BaseWonkoEntityLocalLoadTask(list, parent)
+{
+}
+QString WonkoVersionListLocalLoadTask::filename() const
+{
+ return list()->uid() + ".json";
+}
+QString WonkoVersionListLocalLoadTask::name() const
+{
+ return tr("Wonko Version List for %1").arg(list()->humanReadable());
+}
+void WonkoVersionListLocalLoadTask::parse(const QJsonObject &obj) const
+{
+ WonkoFormat::parseVersionList(obj, list());
+}
+WonkoVersionList *WonkoVersionListLocalLoadTask::list() const
+{
+ return dynamic_cast<WonkoVersionList *>(entity());
+}
+
+// WONKO VERSION //
+WonkoVersionLocalLoadTask::WonkoVersionLocalLoadTask(WonkoVersion *version, QObject *parent)
+ : BaseWonkoEntityLocalLoadTask(version, parent)
+{
+}
+QString WonkoVersionLocalLoadTask::filename() const
+{
+ return version()->uid() + "/" + version()->version() + ".json";
+}
+QString WonkoVersionLocalLoadTask::name() const
+{
+ return tr("Wonko Version for %1").arg(version()->name());
+}
+void WonkoVersionLocalLoadTask::parse(const QJsonObject &obj) const
+{
+ WonkoFormat::parseVersion(obj, version());
+}
+WonkoVersion *WonkoVersionLocalLoadTask::version() const
+{
+ return dynamic_cast<WonkoVersion *>(entity());
+}
diff --git a/api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h b/api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h
new file mode 100644
index 00000000..2affa17f
--- /dev/null
+++ b/api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h
@@ -0,0 +1,81 @@
+/* Copyright 2015 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 "tasks/Task.h"
+#include <memory>
+
+class BaseWonkoEntity;
+class WonkoIndex;
+class WonkoVersionList;
+class WonkoVersion;
+
+class BaseWonkoEntityLocalLoadTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit BaseWonkoEntityLocalLoadTask(BaseWonkoEntity *entity, QObject *parent = nullptr);
+
+protected:
+ virtual QString filename() const = 0;
+ virtual QString name() const = 0;
+ virtual void parse(const QJsonObject &obj) const = 0;
+
+ BaseWonkoEntity *entity() const { return m_entity; }
+
+private:
+ void executeTask() override;
+
+ BaseWonkoEntity *m_entity;
+};
+
+class WonkoIndexLocalLoadTask : public BaseWonkoEntityLocalLoadTask
+{
+ Q_OBJECT
+public:
+ explicit WonkoIndexLocalLoadTask(WonkoIndex *index, QObject *parent = nullptr);
+
+private:
+ QString filename() const override;
+ QString name() const override;
+ void parse(const QJsonObject &obj) const override;
+};
+class WonkoVersionListLocalLoadTask : public BaseWonkoEntityLocalLoadTask
+{
+ Q_OBJECT
+public:
+ explicit WonkoVersionListLocalLoadTask(WonkoVersionList *list, QObject *parent = nullptr);
+
+private:
+ QString filename() const override;
+ QString name() const override;
+ void parse(const QJsonObject &obj) const override;
+
+ WonkoVersionList *list() const;
+};
+class WonkoVersionLocalLoadTask : public BaseWonkoEntityLocalLoadTask
+{
+ Q_OBJECT
+public:
+ explicit WonkoVersionLocalLoadTask(WonkoVersion *version, QObject *parent = nullptr);
+
+private:
+ QString filename() const override;
+ QString name() const override;
+ void parse(const QJsonObject &obj) const override;
+
+ WonkoVersion *version() const;
+};
diff --git a/api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp b/api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp
new file mode 100644
index 00000000..727ec89d
--- /dev/null
+++ b/api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp
@@ -0,0 +1,126 @@
+/* Copyright 2015 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 "BaseWonkoEntityRemoteLoadTask.h"
+
+#include "net/CacheDownload.h"
+#include "net/HttpMetaCache.h"
+#include "net/NetJob.h"
+#include "wonko/format/WonkoFormat.h"
+#include "wonko/WonkoUtil.h"
+#include "wonko/WonkoIndex.h"
+#include "wonko/WonkoVersion.h"
+#include "wonko/WonkoVersionList.h"
+#include "Env.h"
+#include "Json.h"
+
+BaseWonkoEntityRemoteLoadTask::BaseWonkoEntityRemoteLoadTask(BaseWonkoEntity *entity, QObject *parent)
+ : Task(parent), m_entity(entity)
+{
+}
+
+void BaseWonkoEntityRemoteLoadTask::executeTask()
+{
+ NetJob *job = new NetJob(name());
+
+ auto entry = ENV.metacache()->resolveEntry("wonko", url().toString());
+ entry->setStale(true);
+ m_dl = CacheDownload::make(url(), entry);
+ job->addNetAction(m_dl);
+ connect(job, &NetJob::failed, this, &BaseWonkoEntityRemoteLoadTask::emitFailed);
+ connect(job, &NetJob::succeeded, this, &BaseWonkoEntityRemoteLoadTask::networkFinished);
+ connect(job, &NetJob::status, this, &BaseWonkoEntityRemoteLoadTask::setStatus);
+ connect(job, &NetJob::progress, this, &BaseWonkoEntityRemoteLoadTask::setProgress);
+ job->start();
+}
+
+void BaseWonkoEntityRemoteLoadTask::networkFinished()
+{
+ setStatus(tr("Parsing..."));
+ setProgress(0, 0);
+
+ try
+ {
+ parse(Json::requireObject(Json::requireDocument(m_dl->getTargetFilepath(), name()), name()));
+ m_entity->notifyRemoteLoadComplete();
+ emitSucceeded();
+ }
+ catch (Exception &e)
+ {
+ emitFailed(tr("Unable to parse response: %1").arg(e.cause()));
+ }
+}
+
+// WONKO INDEX //
+WonkoIndexRemoteLoadTask::WonkoIndexRemoteLoadTask(WonkoIndex *index, QObject *parent)
+ : BaseWonkoEntityRemoteLoadTask(index, parent)
+{
+}
+QUrl WonkoIndexRemoteLoadTask::url() const
+{
+ return Wonko::indexUrl();
+}
+QString WonkoIndexRemoteLoadTask::name() const
+{
+ return tr("Wonko Index");
+}
+void WonkoIndexRemoteLoadTask::parse(const QJsonObject &obj) const
+{
+ WonkoFormat::parseIndex(obj, dynamic_cast<WonkoIndex *>(entity()));
+}
+
+// WONKO VERSION LIST //
+WonkoVersionListRemoteLoadTask::WonkoVersionListRemoteLoadTask(WonkoVersionList *list, QObject *parent)
+ : BaseWonkoEntityRemoteLoadTask(list, parent)
+{
+}
+QUrl WonkoVersionListRemoteLoadTask::url() const
+{
+ return Wonko::versionListUrl(list()->uid());
+}
+QString WonkoVersionListRemoteLoadTask::name() const
+{
+ return tr("Wonko Version List for %1").arg(list()->humanReadable());
+}
+void WonkoVersionListRemoteLoadTask::parse(const QJsonObject &obj) const
+{
+ WonkoFormat::parseVersionList(obj, list());
+}
+WonkoVersionList *WonkoVersionListRemoteLoadTask::list() const
+{
+ return dynamic_cast<WonkoVersionList *>(entity());
+}
+
+// WONKO VERSION //
+WonkoVersionRemoteLoadTask::WonkoVersionRemoteLoadTask(WonkoVersion *version, QObject *parent)
+ : BaseWonkoEntityRemoteLoadTask(version, parent)
+{
+}
+QUrl WonkoVersionRemoteLoadTask::url() const
+{
+ return Wonko::versionUrl(version()->uid(), version()->version());
+}
+QString WonkoVersionRemoteLoadTask::name() const
+{
+ return tr("Wonko Version for %1").arg(version()->name());
+}
+void WonkoVersionRemoteLoadTask::parse(const QJsonObject &obj) const
+{
+ WonkoFormat::parseVersion(obj, version());
+}
+WonkoVersion *WonkoVersionRemoteLoadTask::version() const
+{
+ return dynamic_cast<WonkoVersion *>(entity());
+}
diff --git a/api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h b/api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h
new file mode 100644
index 00000000..91ed6af0
--- /dev/null
+++ b/api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h
@@ -0,0 +1,85 @@
+/* Copyright 2015 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 "tasks/Task.h"
+#include <memory>
+
+class BaseWonkoEntity;
+class WonkoIndex;
+class WonkoVersionList;
+class WonkoVersion;
+
+class BaseWonkoEntityRemoteLoadTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit BaseWonkoEntityRemoteLoadTask(BaseWonkoEntity *entity, QObject *parent = nullptr);
+
+protected:
+ virtual QUrl url() const = 0;
+ virtual QString name() const = 0;
+ virtual void parse(const QJsonObject &obj) const = 0;
+
+ BaseWonkoEntity *entity() const { return m_entity; }
+
+private slots:
+ void networkFinished();
+
+private:
+ void executeTask() override;
+
+ BaseWonkoEntity *m_entity;
+ std::shared_ptr<class CacheDownload> m_dl;
+};
+
+class WonkoIndexRemoteLoadTask : public BaseWonkoEntityRemoteLoadTask
+{
+ Q_OBJECT
+public:
+ explicit WonkoIndexRemoteLoadTask(WonkoIndex *index, QObject *parent = nullptr);
+
+private:
+ QUrl url() const override;
+ QString name() const override;
+ void parse(const QJsonObject &obj) const override;
+};
+class WonkoVersionListRemoteLoadTask : public BaseWonkoEntityRemoteLoadTask
+{
+ Q_OBJECT
+public:
+ explicit WonkoVersionListRemoteLoadTask(WonkoVersionList *list, QObject *parent = nullptr);
+
+private:
+ QUrl url() const override;
+ QString name() const override;
+ void parse(const QJsonObject &obj) const override;
+
+ WonkoVersionList *list() const;
+};
+class WonkoVersionRemoteLoadTask : public BaseWonkoEntityRemoteLoadTask
+{
+ Q_OBJECT
+public:
+ explicit WonkoVersionRemoteLoadTask(WonkoVersion *version, QObject *parent = nullptr);
+
+private:
+ QUrl url() const override;
+ QString name() const override;
+ void parse(const QJsonObject &obj) const override;
+
+ WonkoVersion *version() const;
+};