aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'launcher')
-rw-r--r--launcher/BaseInstaller.cpp61
-rw-r--r--launcher/BaseInstaller.h44
-rw-r--r--launcher/BaseInstance.cpp275
-rw-r--r--launcher/BaseInstance.h270
-rw-r--r--launcher/BaseVersion.h59
-rw-r--r--launcher/BaseVersionList.cpp99
-rw-r--r--launcher/BaseVersionList.h121
-rw-r--r--launcher/CMakeLists.txt1001
-rw-r--r--launcher/ColorCache.cpp35
-rw-r--r--launcher/ColorCache.h119
-rw-r--r--launcher/ColumnResizer.cpp199
-rw-r--r--launcher/ColumnResizer.h41
-rw-r--r--launcher/Commandline.cpp483
-rw-r--r--launcher/Commandline.h250
-rw-r--r--launcher/DefaultVariable.h35
-rw-r--r--launcher/DesktopServices.cpp149
-rw-r--r--launcher/DesktopServices.h36
-rw-r--r--launcher/Env.cpp211
-rw-r--r--launcher/Env.h63
-rw-r--r--launcher/Exception.h32
-rw-r--r--launcher/ExponentialSeries.h43
-rw-r--r--launcher/FileSystem.cpp457
-rw-r--r--launcher/FileSystem.h127
-rw-r--r--launcher/FileSystem_test.cpp164
-rw-r--r--launcher/Filter.cpp31
-rw-r--r--launcher/Filter.h42
-rw-r--r--launcher/GZip.cpp115
-rw-r--r--launcher/GZip.h10
-rw-r--r--launcher/GZip_test.cpp57
-rw-r--r--launcher/GuiUtil.cpp131
-rw-r--r--launcher/GuiUtil.h11
-rw-r--r--launcher/HoeDown.h76
-rw-r--r--launcher/InstanceCopyTask.cpp60
-rw-r--r--launcher/InstanceCopyTask.h31
-rw-r--r--launcher/InstanceCreationTask.cpp31
-rw-r--r--launcher/InstanceCreationTask.h22
-rw-r--r--launcher/InstanceImportTask.cpp456
-rw-r--r--launcher/InstanceImportTask.h72
-rw-r--r--launcher/InstanceList.cpp867
-rw-r--r--launcher/InstanceList.h173
-rw-r--r--launcher/InstancePageProvider.h76
-rw-r--r--launcher/InstanceProxyModel.cpp34
-rw-r--r--launcher/InstanceProxyModel.h16
-rw-r--r--launcher/InstanceTask.cpp9
-rw-r--r--launcher/InstanceTask.h52
-rw-r--r--launcher/InstanceWindow.cpp236
-rw-r--r--launcher/InstanceWindow.h73
-rw-r--r--launcher/JavaCommon.cpp104
-rw-r--r--launcher/JavaCommon.h48
-rw-r--r--launcher/Json.cpp272
-rw-r--r--launcher/Json.h249
-rw-r--r--launcher/KonamiCode.cpp44
-rw-r--r--launcher/KonamiCode.h17
-rw-r--r--launcher/LaunchController.cpp353
-rw-r--r--launcher/LaunchController.h68
-rw-r--r--launcher/LoggedProcess.cpp176
-rw-r--r--launcher/LoggedProcess.h79
-rw-r--r--launcher/MMCStrings.cpp76
-rw-r--r--launcher/MMCStrings.h8
-rw-r--r--launcher/MMCZip.cpp312
-rw-r--r--launcher/MMCZip.h92
-rw-r--r--launcher/MainWindow.cpp1952
-rw-r--r--launcher/MainWindow.h226
-rw-r--r--launcher/MessageLevel.cpp36
-rw-r--r--launcher/MessageLevel.h28
-rw-r--r--launcher/MultiMC.cpp1448
-rw-r--r--launcher/MultiMC.h235
-rw-r--r--launcher/NullInstance.h76
-rw-r--r--launcher/ProblemProvider.h47
-rw-r--r--launcher/QObjectPtr.h83
-rw-r--r--launcher/RWStorage.h66
-rw-r--r--launcher/RecursiveFileSystemWatcher.cpp111
-rw-r--r--launcher/RecursiveFileSystemWatcher.h61
-rw-r--r--launcher/SeparatorPrefixTree.h298
-rw-r--r--launcher/SkinUtils.cpp52
-rw-r--r--launcher/SkinUtils.h23
-rw-r--r--launcher/UpdateController.cpp449
-rw-r--r--launcher/UpdateController.h44
-rw-r--r--launcher/Usable.h58
-rw-r--r--launcher/Version.cpp85
-rw-r--r--launcher/Version.h105
-rw-r--r--launcher/VersionProxyModel.cpp447
-rw-r--r--launcher/VersionProxyModel.h67
-rw-r--r--launcher/Version_test.cpp85
-rw-r--r--launcher/WatchLock.h20
-rw-r--r--launcher/dialogs/AboutDialog.cpp138
-rw-r--r--launcher/dialogs/AboutDialog.h47
-rw-r--r--launcher/dialogs/AboutDialog.ui312
-rw-r--r--launcher/dialogs/CopyInstanceDialog.cpp144
-rw-r--r--launcher/dialogs/CopyInstanceDialog.h58
-rw-r--r--launcher/dialogs/CopyInstanceDialog.ui182
-rw-r--r--launcher/dialogs/CustomMessageBox.cpp35
-rw-r--r--launcher/dialogs/CustomMessageBox.h26
-rw-r--r--launcher/dialogs/EditAccountDialog.cpp61
-rw-r--r--launcher/dialogs/EditAccountDialog.h56
-rw-r--r--launcher/dialogs/EditAccountDialog.ui94
-rw-r--r--launcher/dialogs/ExportInstanceDialog.cpp482
-rw-r--r--launcher/dialogs/ExportInstanceDialog.h54
-rw-r--r--launcher/dialogs/ExportInstanceDialog.ui83
-rw-r--r--launcher/dialogs/IconPickerDialog.cpp163
-rw-r--r--launcher/dialogs/IconPickerDialog.h49
-rw-r--r--launcher/dialogs/IconPickerDialog.ui67
-rw-r--r--launcher/dialogs/LoginDialog.cpp110
-rw-r--r--launcher/dialogs/LoginDialog.h58
-rw-r--r--launcher/dialogs/LoginDialog.ui87
-rw-r--r--launcher/dialogs/NewComponentDialog.cpp106
-rw-r--r--launcher/dialogs/NewComponentDialog.h48
-rw-r--r--launcher/dialogs/NewComponentDialog.ui101
-rw-r--r--launcher/dialogs/NewInstanceDialog.cpp255
-rw-r--r--launcher/dialogs/NewInstanceDialog.h80
-rw-r--r--launcher/dialogs/NewInstanceDialog.ui87
-rw-r--r--launcher/dialogs/NotificationDialog.cpp86
-rw-r--r--launcher/dialogs/NotificationDialog.h44
-rw-r--r--launcher/dialogs/NotificationDialog.ui85
-rw-r--r--launcher/dialogs/ProfileSelectDialog.cpp116
-rw-r--r--launcher/dialogs/ProfileSelectDialog.h90
-rw-r--r--launcher/dialogs/ProfileSelectDialog.ui62
-rw-r--r--launcher/dialogs/ProgressDialog.cpp196
-rw-r--r--launcher/dialogs/ProgressDialog.h71
-rw-r--r--launcher/dialogs/ProgressDialog.ui66
-rw-r--r--launcher/dialogs/SkinUploadDialog.cpp114
-rw-r--r--launcher/dialogs/SkinUploadDialog.h29
-rw-r--r--launcher/dialogs/SkinUploadDialog.ui85
-rw-r--r--launcher/dialogs/UpdateDialog.cpp182
-rw-r--r--launcher/dialogs/UpdateDialog.h67
-rw-r--r--launcher/dialogs/UpdateDialog.ui91
-rw-r--r--launcher/dialogs/VersionSelectDialog.cpp141
-rw-r--r--launcher/dialogs/VersionSelectDialog.h78
-rw-r--r--launcher/groupview/AccessibleGroupView.cpp778
-rw-r--r--launcher/groupview/AccessibleGroupView.h6
-rw-r--r--launcher/groupview/AccessibleGroupView_p.h118
-rw-r--r--launcher/groupview/GroupView.cpp1020
-rw-r--r--launcher/groupview/GroupView.h157
-rw-r--r--launcher/groupview/GroupedProxyModel.cpp48
-rw-r--r--launcher/groupview/GroupedProxyModel.h30
-rw-r--r--launcher/groupview/InstanceDelegate.cpp428
-rw-r--r--launcher/groupview/InstanceDelegate.h39
-rw-r--r--launcher/groupview/VisualGroup.cpp317
-rw-r--r--launcher/groupview/VisualGroup.h106
-rw-r--r--launcher/icons/IIconList.cpp7
-rw-r--r--launcher/icons/IIconList.h25
-rw-r--r--launcher/icons/IconList.cpp419
-rw-r--r--launcher/icons/IconList.h86
-rw-r--r--launcher/icons/IconUtils.cpp62
-rw-r--r--launcher/icons/IconUtils.h13
-rw-r--r--launcher/icons/MMCIcon.cpp118
-rw-r--r--launcher/icons/MMCIcon.h49
-rw-r--r--launcher/install_prereqs.cmake.in27
-rw-r--r--launcher/java/JavaChecker.cpp166
-rw-r--r--launcher/java/JavaChecker.h61
-rw-r--r--launcher/java/JavaCheckerJob.cpp44
-rw-r--r--launcher/java/JavaCheckerJob.h61
-rw-r--r--launcher/java/JavaInstall.cpp28
-rw-r--r--launcher/java/JavaInstall.h38
-rw-r--r--launcher/java/JavaInstallList.cpp208
-rw-r--r--launcher/java/JavaInstallList.h81
-rw-r--r--launcher/java/JavaUtils.cpp399
-rw-r--r--launcher/java/JavaUtils.h42
-rw-r--r--launcher/java/JavaVersion.cpp121
-rw-r--r--launcher/java/JavaVersion.h49
-rw-r--r--launcher/java/JavaVersion_test.cpp116
-rw-r--r--launcher/java/launch/CheckJava.cpp139
-rw-r--r--launcher/java/launch/CheckJava.h45
-rw-r--r--launcher/launch/LaunchStep.cpp27
-rw-r--r--launcher/launch/LaunchStep.h50
-rw-r--r--launcher/launch/LaunchTask.cpp280
-rw-r--r--launcher/launch/LaunchTask.h123
-rw-r--r--launcher/launch/LogModel.cpp167
-rw-r--r--launcher/launch/LogModel.h58
-rw-r--r--launcher/launch/steps/LookupServerAddress.cpp95
-rw-r--r--launcher/launch/steps/LookupServerAddress.h49
-rw-r--r--launcher/launch/steps/PostLaunchCommand.cpp84
-rw-r--r--launcher/launch/steps/PostLaunchCommand.h41
-rw-r--r--launcher/launch/steps/PreLaunchCommand.cpp85
-rw-r--r--launcher/launch/steps/PreLaunchCommand.h41
-rw-r--r--launcher/launch/steps/TextPrint.cpp29
-rw-r--r--launcher/launch/steps/TextPrint.h41
-rw-r--r--launcher/launch/steps/Update.cpp80
-rw-r--r--launcher/launch/steps/Update.h45
-rw-r--r--launcher/main.cpp61
-rw-r--r--launcher/meta/BaseEntity.cpp168
-rw-r--r--launcher/meta/BaseEntity.h67
-rw-r--r--launcher/meta/Index.cpp148
-rw-r--r--launcher/meta/Index.h69
-rw-r--r--launcher/meta/Index_test.cpp44
-rw-r--r--launcher/meta/JsonFormat.cpp218
-rw-r--r--launcher/meta/JsonFormat.h83
-rw-r--r--launcher/meta/Version.cpp140
-rw-r--r--launcher/meta/Version.h116
-rw-r--r--launcher/meta/VersionList.cpp245
-rw-r--r--launcher/meta/VersionList.h101
-rw-r--r--launcher/minecraft/AssetsUtils.cpp333
-rw-r--r--launcher/minecraft/AssetsUtils.h53
-rw-r--r--launcher/minecraft/Component.cpp439
-rw-r--r--launcher/minecraft/Component.h110
-rw-r--r--launcher/minecraft/ComponentUpdateTask.cpp704
-rw-r--r--launcher/minecraft/ComponentUpdateTask.h37
-rw-r--r--launcher/minecraft/ComponentUpdateTask_p.h32
-rw-r--r--launcher/minecraft/GradleSpecifier.h151
-rw-r--r--launcher/minecraft/GradleSpecifier_test.cpp78
-rw-r--r--launcher/minecraft/LaunchProfile.cpp319
-rw-r--r--launcher/minecraft/LaunchProfile.h104
-rw-r--r--launcher/minecraft/Library.cpp309
-rw-r--r--launcher/minecraft/Library.h217
-rw-r--r--launcher/minecraft/Library_test.cpp272
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp1054
-rw-r--r--launcher/minecraft/MinecraftInstance.h132
-rw-r--r--launcher/minecraft/MinecraftLoadAndCheck.cpp45
-rw-r--r--launcher/minecraft/MinecraftLoadAndCheck.h48
-rw-r--r--launcher/minecraft/MinecraftUpdate.cpp182
-rw-r--r--launcher/minecraft/MinecraftUpdate.h57
-rw-r--r--launcher/minecraft/MojangDownloadInfo.h82
-rw-r--r--launcher/minecraft/MojangVersionFormat.cpp383
-rw-r--r--launcher/minecraft/MojangVersionFormat.h24
-rw-r--r--launcher/minecraft/MojangVersionFormat_test.cpp55
-rw-r--r--launcher/minecraft/OneSixVersionFormat.cpp391
-rw-r--r--launcher/minecraft/OneSixVersionFormat.h30
-rw-r--r--launcher/minecraft/OpSys.cpp42
-rw-r--r--launcher/minecraft/OpSys.h37
-rw-r--r--launcher/minecraft/PackProfile.cpp1225
-rw-r--r--launcher/minecraft/PackProfile.h151
-rw-r--r--launcher/minecraft/PackProfile_p.h42
-rw-r--r--launcher/minecraft/ParseUtils.cpp34
-rw-r--r--launcher/minecraft/ParseUtils.h9
-rw-r--r--launcher/minecraft/ParseUtils_test.cpp45
-rw-r--r--launcher/minecraft/ProfileUtils.cpp178
-rw-r--r--launcher/minecraft/ProfileUtils.h28
-rw-r--r--launcher/minecraft/Rule.cpp93
-rw-r--r--launcher/minecraft/Rule.h101
-rw-r--r--launcher/minecraft/VersionFile.cpp60
-rw-r--r--launcher/minecraft/VersionFile.h114
-rw-r--r--launcher/minecraft/VersionFilterData.cpp71
-rw-r--r--launcher/minecraft/VersionFilterData.h29
-rw-r--r--launcher/minecraft/World.cpp520
-rw-r--r--launcher/minecraft/World.h111
-rw-r--r--launcher/minecraft/WorldList.cpp387
-rw-r--r--launcher/minecraft/WorldList.h129
-rw-r--r--launcher/minecraft/auth-msa/BuildConfig.cpp.in9
-rw-r--r--launcher/minecraft/auth-msa/BuildConfig.h11
-rw-r--r--launcher/minecraft/auth-msa/CMakeLists.txt28
-rw-r--r--launcher/minecraft/auth-msa/context.cpp938
-rw-r--r--launcher/minecraft/auth-msa/context.h128
-rw-r--r--launcher/minecraft/auth-msa/main.cpp100
-rw-r--r--launcher/minecraft/auth-msa/mainwindow.cpp97
-rw-r--r--launcher/minecraft/auth-msa/mainwindow.h34
-rw-r--r--launcher/minecraft/auth-msa/mainwindow.ui72
-rw-r--r--launcher/minecraft/auth/AuthSession.cpp30
-rw-r--r--launcher/minecraft/auth/AuthSession.h52
-rw-r--r--launcher/minecraft/auth/MojangAccount.cpp315
-rw-r--r--launcher/minecraft/auth/MojangAccount.h180
-rw-r--r--launcher/minecraft/auth/MojangAccountList.cpp468
-rw-r--r--launcher/minecraft/auth/MojangAccountList.h199
-rw-r--r--launcher/minecraft/auth/YggdrasilTask.cpp255
-rw-r--r--launcher/minecraft/auth/YggdrasilTask.h151
-rw-r--r--launcher/minecraft/auth/flows/AuthenticateTask.cpp202
-rw-r--r--launcher/minecraft/auth/flows/AuthenticateTask.h46
-rw-r--r--launcher/minecraft/auth/flows/RefreshTask.cpp144
-rw-r--r--launcher/minecraft/auth/flows/RefreshTask.h44
-rw-r--r--launcher/minecraft/auth/flows/ValidateTask.cpp61
-rw-r--r--launcher/minecraft/auth/flows/ValidateTask.h47
-rw-r--r--launcher/minecraft/gameoptions/GameOptions.cpp144
-rw-r--r--launcher/minecraft/gameoptions/GameOptions.h34
-rw-r--r--launcher/minecraft/launch/ClaimAccount.cpp24
-rw-r--r--launcher/minecraft/launch/ClaimAccount.h37
-rw-r--r--launcher/minecraft/launch/CreateGameFolders.cpp28
-rw-r--r--launcher/minecraft/launch/CreateGameFolders.h37
-rw-r--r--launcher/minecraft/launch/DirectJavaLaunch.cpp148
-rw-r--r--launcher/minecraft/launch/DirectJavaLaunch.h58
-rw-r--r--launcher/minecraft/launch/ExtractNatives.cpp111
-rw-r--r--launcher/minecraft/launch/ExtractNatives.h38
-rw-r--r--launcher/minecraft/launch/LauncherPartLaunch.cpp218
-rw-r--r--launcher/minecraft/launch/LauncherPartLaunch.h60
-rw-r--r--launcher/minecraft/launch/MinecraftServerTarget.cpp66
-rw-r--r--launcher/minecraft/launch/MinecraftServerTarget.h29
-rw-r--r--launcher/minecraft/launch/ModMinecraftJar.cpp82
-rw-r--r--launcher/minecraft/launch/ModMinecraftJar.h36
-rw-r--r--launcher/minecraft/launch/PrintInstanceInfo.cpp106
-rw-r--r--launcher/minecraft/launch/PrintInstanceInfo.h41
-rw-r--r--launcher/minecraft/launch/ReconstructAssets.cpp36
-rw-r--r--launcher/minecraft/launch/ReconstructAssets.h33
-rw-r--r--launcher/minecraft/launch/ScanModFolders.cpp59
-rw-r--r--launcher/minecraft/launch/ScanModFolders.h42
-rw-r--r--launcher/minecraft/launch/VerifyJavaInstall.cpp34
-rw-r--r--launcher/minecraft/launch/VerifyJavaInstall.h17
-rw-r--r--launcher/minecraft/legacy/LegacyInstance.cpp256
-rw-r--r--launcher/minecraft/legacy/LegacyInstance.h140
-rw-r--r--launcher/minecraft/legacy/LegacyModList.cpp136
-rw-r--r--launcher/minecraft/legacy/LegacyModList.h47
-rw-r--r--launcher/minecraft/legacy/LegacyUpgradeTask.cpp138
-rw-r--r--launcher/minecraft/legacy/LegacyUpgradeTask.h29
-rw-r--r--launcher/minecraft/mod/LocalModParseTask.cpp467
-rw-r--r--launcher/minecraft/mod/LocalModParseTask.h37
-rw-r--r--launcher/minecraft/mod/Mod.cpp151
-rw-r--r--launcher/minecraft/mod/Mod.h115
-rw-r--r--launcher/minecraft/mod/ModDetails.h17
-rw-r--r--launcher/minecraft/mod/ModFolderLoadTask.cpp18
-rw-r--r--launcher/minecraft/mod/ModFolderLoadTask.h29
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp554
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h148
-rw-r--r--launcher/minecraft/mod/ModFolderModel_test.cpp53
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp23
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.h13
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.cpp23
-rw-r--r--launcher/minecraft/mod/TexturePackFolderModel.h13
-rw-r--r--launcher/minecraft/services/SkinDelete.cpp42
-rw-r--r--launcher/minecraft/services/SkinDelete.h29
-rw-r--r--launcher/minecraft/services/SkinUpload.cpp66
-rw-r--r--launcher/minecraft/services/SkinUpload.h38
-rw-r--r--launcher/minecraft/testdata/1.9-simple.json198
-rw-r--r--launcher/minecraft/testdata/1.9.json529
-rw-r--r--launcher/minecraft/testdata/codecwav-20101023.jar1
-rw-r--r--launcher/minecraft/testdata/lib-native-arch.json46
-rw-r--r--launcher/minecraft/testdata/lib-native.json52
-rw-r--r--launcher/minecraft/testdata/lib-simple.json11
-rw-r--r--launcher/minecraft/testdata/testname-testversion-linux-32.jar1
-rw-r--r--launcher/minecraft/update/AssetUpdateTask.cpp107
-rw-r--r--launcher/minecraft/update/AssetUpdateTask.h28
-rw-r--r--launcher/minecraft/update/FMLLibrariesTask.cpp131
-rw-r--r--launcher/minecraft/update/FMLLibrariesTask.h31
-rw-r--r--launcher/minecraft/update/FoldersTask.cpp21
-rw-r--r--launcher/minecraft/update/FoldersTask.h17
-rw-r--r--launcher/minecraft/update/LibrariesTask.cpp90
-rw-r--r--launcher/minecraft/update/LibrariesTask.h26
-rw-r--r--launcher/modplatform/atlauncher/ATLPackIndex.cpp33
-rw-r--r--launcher/modplatform/atlauncher/ATLPackIndex.h34
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp764
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.h101
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.cpp218
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.h125
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp63
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h32
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.cpp92
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.h41
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp126
-rw-r--r--launcher/modplatform/flame/PackManifest.h62
-rw-r--r--launcher/modplatform/legacy_ftb/PackFetchTask.cpp172
-rw-r--r--launcher/modplatform/legacy_ftb/PackFetchTask.h44
-rw-r--r--launcher/modplatform/legacy_ftb/PackHelpers.h45
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.cpp214
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h55
-rw-r--r--launcher/modplatform/legacy_ftb/PrivatePackManager.cpp41
-rw-r--r--launcher/modplatform/legacy_ftb/PrivatePackManager.h43
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp209
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.h46
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.cpp156
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.h125
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.cpp141
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.h64
-rw-r--r--launcher/modplatform/technic/SolderPackInstallTask.cpp207
-rw-r--r--launcher/modplatform/technic/SolderPackInstallTask.h60
-rw-r--r--launcher/modplatform/technic/TechnicPackProcessor.cpp208
-rw-r--r--launcher/modplatform/technic/TechnicPackProcessor.h35
-rw-r--r--launcher/mojang/PackageManifest.cpp427
-rw-r--r--launcher/mojang/PackageManifest.h171
-rw-r--r--launcher/mojang/PackageManifest_test.cpp344
-rw-r--r--launcher/mojang/testdata/1.8.0_202-x64.json1
-rwxr-xr-xlauncher/mojang/testdata/inspect/a/b.txt0
l---------launcher/mojang/testdata/inspect/a/b/b.txt1
-rw-r--r--launcher/mojang/testdata/inspect_win/a/b.txt0
-rw-r--r--launcher/mojang/testdata/inspect_win/a/b/b.txt0
-rw-r--r--launcher/net/ByteArraySink.h62
-rw-r--r--launcher/net/ChecksumValidator.h55
-rw-r--r--launcher/net/Download.cpp309
-rw-r--r--launcher/net/Download.h75
-rw-r--r--launcher/net/FileSink.cpp115
-rw-r--r--launcher/net/FileSink.h28
-rw-r--r--launcher/net/HttpMetaCache.cpp273
-rw-r--r--launcher/net/HttpMetaCache.h123
-rw-r--r--launcher/net/MetaCacheSink.cpp65
-rw-r--r--launcher/net/MetaCacheSink.h22
-rw-r--r--launcher/net/Mode.h10
-rw-r--r--launcher/net/NetAction.h113
-rw-r--r--launcher/net/NetJob.cpp218
-rw-r--r--launcher/net/NetJob.h89
-rw-r--r--launcher/net/PasteUpload.cpp104
-rw-r--r--launcher/net/PasteUpload.h47
-rw-r--r--launcher/net/Sink.h70
-rw-r--r--launcher/net/Validator.h18
-rw-r--r--launcher/news/NewsChecker.cpp131
-rw-r--r--launcher/news/NewsChecker.h103
-rw-r--r--launcher/news/NewsEntry.cpp77
-rw-r--r--launcher/news/NewsEntry.h65
-rw-r--r--launcher/notifications/NotificationChecker.cpp129
-rw-r--r--launcher/notifications/NotificationChecker.h61
-rwxr-xr-xlauncher/package/linux/MultiMC93
-rwxr-xr-xlauncher/package/linux/multimc.desktop11
-rw-r--r--launcher/package/rpm/MultiMC5.spec47
-rw-r--r--launcher/package/rpm/README.md12
-rw-r--r--launcher/package/ubuntu/README.md14
-rw-r--r--launcher/package/ubuntu/multimc/DEBIAN/control12
-rwxr-xr-xlauncher/package/ubuntu/multimc/DEBIAN/postrm3
-rw-r--r--launcher/package/ubuntu/multimc/opt/multimc/icon.svg353
-rwxr-xr-xlauncher/package/ubuntu/multimc/opt/multimc/run.sh33
-rwxr-xr-xlauncher/package/ubuntu/multimc/usr/share/applications/multimc.desktop16
-rw-r--r--launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml54
-rw-r--r--launcher/pagedialog/PageDialog.cpp61
-rw-r--r--launcher/pagedialog/PageDialog.h35
-rw-r--r--launcher/pages/BasePage.h58
-rw-r--r--launcher/pages/BasePageContainer.h10
-rw-r--r--launcher/pages/BasePageProvider.h68
-rw-r--r--launcher/pages/global/AccountListPage.cpp217
-rw-r--r--launcher/pages/global/AccountListPage.h84
-rw-r--r--launcher/pages/global/AccountListPage.ui98
-rw-r--r--launcher/pages/global/CustomCommandsPage.cpp51
-rw-r--r--launcher/pages/global/CustomCommandsPage.h55
-rw-r--r--launcher/pages/global/ExternalToolsPage.cpp233
-rw-r--r--launcher/pages/global/ExternalToolsPage.h74
-rw-r--r--launcher/pages/global/ExternalToolsPage.ui194
-rw-r--r--launcher/pages/global/JavaPage.cpp153
-rw-r--r--launcher/pages/global/JavaPage.h72
-rw-r--r--launcher/pages/global/JavaPage.ui260
-rw-r--r--launcher/pages/global/LanguagePage.cpp51
-rw-r--r--launcher/pages/global/LanguagePage.h60
-rw-r--r--launcher/pages/global/MinecraftPage.cpp90
-rw-r--r--launcher/pages/global/MinecraftPage.h70
-rw-r--r--launcher/pages/global/MinecraftPage.ui189
-rw-r--r--launcher/pages/global/MultiMCPage.cpp467
-rw-r--r--launcher/pages/global/MultiMCPage.h103
-rw-r--r--launcher/pages/global/MultiMCPage.ui584
-rw-r--r--launcher/pages/global/PasteEEPage.cpp81
-rw-r--r--launcher/pages/global/PasteEEPage.h62
-rw-r--r--launcher/pages/global/PasteEEPage.ui128
-rw-r--r--launcher/pages/global/ProxyPage.cpp101
-rw-r--r--launcher/pages/global/ProxyPage.h66
-rw-r--r--launcher/pages/global/ProxyPage.ui203
-rw-r--r--launcher/pages/instance/GameOptionsPage.cpp37
-rw-r--r--launcher/pages/instance/GameOptionsPage.h63
-rw-r--r--launcher/pages/instance/GameOptionsPage.ui88
-rw-r--r--launcher/pages/instance/InstanceSettingsPage.cpp338
-rw-r--r--launcher/pages/instance/InstanceSettingsPage.h76
-rw-r--r--launcher/pages/instance/InstanceSettingsPage.ui548
-rw-r--r--launcher/pages/instance/LegacyUpgradePage.cpp50
-rw-r--r--launcher/pages/instance/LegacyUpgradePage.h64
-rw-r--r--launcher/pages/instance/LegacyUpgradePage.ui47
-rw-r--r--launcher/pages/instance/LogPage.cpp312
-rw-r--r--launcher/pages/instance/LogPage.h86
-rw-r--r--launcher/pages/instance/LogPage.ui182
-rw-r--r--launcher/pages/instance/ModFolderPage.cpp363
-rw-r--r--launcher/pages/instance/ModFolderPage.h119
-rw-r--r--launcher/pages/instance/ModFolderPage.ui164
-rw-r--r--launcher/pages/instance/NotesPage.cpp21
-rw-r--r--launcher/pages/instance/NotesPage.h60
-rw-r--r--launcher/pages/instance/NotesPage.ui49
-rw-r--r--launcher/pages/instance/OtherLogsPage.cpp313
-rw-r--r--launcher/pages/instance/OtherLogsPage.h81
-rw-r--r--launcher/pages/instance/OtherLogsPage.ui150
-rw-r--r--launcher/pages/instance/ResourcePackPage.h23
-rw-r--r--launcher/pages/instance/ScreenshotsPage.cpp422
-rw-r--r--launcher/pages/instance/ScreenshotsPage.h89
-rw-r--r--launcher/pages/instance/ScreenshotsPage.ui87
-rw-r--r--launcher/pages/instance/ServersPage.cpp768
-rw-r--r--launcher/pages/instance/ServersPage.h94
-rw-r--r--launcher/pages/instance/ServersPage.ui194
-rw-r--r--launcher/pages/instance/TexturePackPage.h22
-rw-r--r--launcher/pages/instance/VersionPage.cpp642
-rw-r--r--launcher/pages/instance/VersionPage.h104
-rw-r--r--launcher/pages/instance/VersionPage.ui285
-rw-r--r--launcher/pages/instance/WorldListPage.cpp408
-rw-r--r--launcher/pages/instance/WorldListPage.h99
-rw-r--r--launcher/pages/instance/WorldListPage.ui161
-rw-r--r--launcher/pages/modplatform/ImportPage.cpp130
-rw-r--r--launcher/pages/modplatform/ImportPage.h70
-rw-r--r--launcher/pages/modplatform/ImportPage.ui52
-rw-r--r--launcher/pages/modplatform/VanillaPage.cpp104
-rw-r--r--launcher/pages/modplatform/VanillaPage.h75
-rw-r--r--launcher/pages/modplatform/VanillaPage.ui169
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp81
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlFilterModel.h34
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlListModel.cpp194
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlListModel.h52
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp209
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h66
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui65
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlPage.cpp175
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlPage.h87
-rw-r--r--launcher/pages/modplatform/atlauncher/AtlPage.ui97
-rw-r--r--launcher/pages/modplatform/flame/FlameModel.cpp259
-rw-r--r--launcher/pages/modplatform/flame/FlameModel.h76
-rw-r--r--launcher/pages/modplatform/flame/FlamePage.cpp185
-rw-r--r--launcher/pages/modplatform/flame/FlamePage.h80
-rw-r--r--launcher/pages/modplatform/flame/FlamePage.ui90
-rw-r--r--launcher/pages/modplatform/ftb/FtbFilterModel.cpp64
-rw-r--r--launcher/pages/modplatform/ftb/FtbFilterModel.h33
-rw-r--r--launcher/pages/modplatform/ftb/FtbListModel.cpp304
-rw-r--r--launcher/pages/modplatform/ftb/FtbListModel.h69
-rw-r--r--launcher/pages/modplatform/ftb/FtbPage.cpp145
-rw-r--r--launcher/pages/modplatform/ftb/FtbPage.h80
-rw-r--r--launcher/pages/modplatform/ftb/FtbPage.ui84
-rw-r--r--launcher/pages/modplatform/legacy_ftb/ListModel.cpp260
-rw-r--r--launcher/pages/modplatform/legacy_ftb/ListModel.h78
-rw-r--r--launcher/pages/modplatform/legacy_ftb/Page.cpp369
-rw-r--r--launcher/pages/modplatform/legacy_ftb/Page.h119
-rw-r--r--launcher/pages/modplatform/legacy_ftb/Page.ui135
-rw-r--r--launcher/pages/modplatform/technic/TechnicData.h42
-rw-r--r--launcher/pages/modplatform/technic/TechnicModel.cpp238
-rw-r--r--launcher/pages/modplatform/technic/TechnicModel.h70
-rw-r--r--launcher/pages/modplatform/technic/TechnicPage.cpp198
-rw-r--r--launcher/pages/modplatform/technic/TechnicPage.h78
-rw-r--r--launcher/pages/modplatform/technic/TechnicPage.ui95
-rw-r--r--launcher/pathmatcher/FSTreeMatcher.h21
-rw-r--r--launcher/pathmatcher/IPathMatcher.h12
-rw-r--r--launcher/pathmatcher/MultiMatcher.h31
-rw-r--r--launcher/pathmatcher/RegexpMatcher.h42
-rw-r--r--launcher/resources/MultiMC.icnsbin0 -> 782703 bytes
-rw-r--r--launcher/resources/MultiMC.icobin0 -> 55224 bytes
-rw-r--r--launcher/resources/MultiMC.manifest31
-rw-r--r--launcher/resources/OSX/OSX.qrc38
-rw-r--r--launcher/resources/OSX/index.theme11
-rw-r--r--launcher/resources/OSX/scalable/about.svg20
-rw-r--r--launcher/resources/OSX/scalable/accounts.svg16
-rw-r--r--launcher/resources/OSX/scalable/bug.svg25
-rw-r--r--launcher/resources/OSX/scalable/centralmods.svg16
-rw-r--r--launcher/resources/OSX/scalable/checkupdate.svg22
-rw-r--r--launcher/resources/OSX/scalable/copy.svg18
-rw-r--r--launcher/resources/OSX/scalable/coremods.svg21
-rw-r--r--launcher/resources/OSX/scalable/externaltools.svg14
-rw-r--r--launcher/resources/OSX/scalable/help.svg51
-rw-r--r--launcher/resources/OSX/scalable/instance-settings.svg25
-rw-r--r--launcher/resources/OSX/scalable/jarmods.svg30
-rw-r--r--launcher/resources/OSX/scalable/java.svg33
-rw-r--r--launcher/resources/OSX/scalable/language.svg40
-rw-r--r--launcher/resources/OSX/scalable/loadermods.svg14
-rw-r--r--launcher/resources/OSX/scalable/log.svg19
-rw-r--r--launcher/resources/OSX/scalable/minecraft.svg12
-rw-r--r--launcher/resources/OSX/scalable/multimc.svg18
-rw-r--r--launcher/resources/OSX/scalable/new.svg19
-rw-r--r--launcher/resources/OSX/scalable/news.svg16
-rw-r--r--launcher/resources/OSX/scalable/notes.svg21
-rw-r--r--launcher/resources/OSX/scalable/patreon.svg15
-rw-r--r--launcher/resources/OSX/scalable/proxy.svg16
-rw-r--r--launcher/resources/OSX/scalable/quickmods.svg18
-rw-r--r--launcher/resources/OSX/scalable/refresh.svg16
-rw-r--r--launcher/resources/OSX/scalable/resourcepacks.svg17
-rw-r--r--launcher/resources/OSX/scalable/screenshots.svg19
-rw-r--r--launcher/resources/OSX/scalable/settings.svg25
-rw-r--r--launcher/resources/OSX/scalable/status-bad.svg11
-rw-r--r--launcher/resources/OSX/scalable/status-good.svg19
-rw-r--r--launcher/resources/OSX/scalable/status-yellow.svg16
-rw-r--r--launcher/resources/OSX/scalable/viewfolder.svg16
-rw-r--r--launcher/resources/OSX/scalable/worlds.svg58
-rw-r--r--launcher/resources/assets/underconstruction.pngbin0 -> 14490 bytes
-rw-r--r--launcher/resources/backgrounds/backgrounds.qrc7
-rw-r--r--launcher/resources/backgrounds/catbgrnd2.pngbin0 -> 62973 bytes
-rw-r--r--launcher/resources/backgrounds/catmas.pngbin0 -> 72818 bytes
-rw-r--r--launcher/resources/documents/documents.qrc7
-rw-r--r--launcher/resources/flat/flat.qrc45
-rw-r--r--launcher/resources/flat/index.theme11
-rw-r--r--launcher/resources/flat/scalable/about.svg3
-rw-r--r--launcher/resources/flat/scalable/accounts.svg3
-rw-r--r--launcher/resources/flat/scalable/bug.svg3
-rw-r--r--launcher/resources/flat/scalable/cat.svg3
-rw-r--r--launcher/resources/flat/scalable/centralmods.svg3
-rw-r--r--launcher/resources/flat/scalable/checkupdate.svg3
-rw-r--r--launcher/resources/flat/scalable/copy.svg3
-rw-r--r--launcher/resources/flat/scalable/coremods.svg3
-rw-r--r--launcher/resources/flat/scalable/discord.svg4
-rw-r--r--launcher/resources/flat/scalable/externaltools.svg3
-rw-r--r--launcher/resources/flat/scalable/help.svg17
-rw-r--r--launcher/resources/flat/scalable/instance-settings.svg3
-rw-r--r--launcher/resources/flat/scalable/jarmods.svg3
-rw-r--r--launcher/resources/flat/scalable/java.svg3
-rw-r--r--launcher/resources/flat/scalable/language.svg103
-rw-r--r--launcher/resources/flat/scalable/loadermods.svg3
-rw-r--r--launcher/resources/flat/scalable/log.svg3
-rw-r--r--launcher/resources/flat/scalable/minecraft.svg3
-rw-r--r--launcher/resources/flat/scalable/multimc.svg3
-rw-r--r--launcher/resources/flat/scalable/new.svg3
-rw-r--r--launcher/resources/flat/scalable/news.svg3
-rw-r--r--launcher/resources/flat/scalable/notes.svg3
-rw-r--r--launcher/resources/flat/scalable/packages.svg3
-rw-r--r--launcher/resources/flat/scalable/patreon.svg3
-rw-r--r--launcher/resources/flat/scalable/proxy.svg3
-rw-r--r--launcher/resources/flat/scalable/quickmods.svg3
-rw-r--r--launcher/resources/flat/scalable/reddit-alien.svg3
-rw-r--r--launcher/resources/flat/scalable/refresh.svg3
-rw-r--r--launcher/resources/flat/scalable/resourcepacks.svg3
-rw-r--r--launcher/resources/flat/scalable/screenshot-placeholder.svg3
-rw-r--r--launcher/resources/flat/scalable/screenshots.svg3
-rw-r--r--launcher/resources/flat/scalable/settings.svg3
-rw-r--r--launcher/resources/flat/scalable/star.svg3
-rw-r--r--launcher/resources/flat/scalable/status-bad.svg3
-rw-r--r--launcher/resources/flat/scalable/status-good.svg3
-rw-r--r--launcher/resources/flat/scalable/status-running.svg3
-rw-r--r--launcher/resources/flat/scalable/status-yellow.svg3
-rw-r--r--launcher/resources/flat/scalable/viewfolder.svg3
-rw-r--r--launcher/resources/flat/scalable/worlds.svg3
-rw-r--r--launcher/resources/iOS/iOS.qrc38
-rw-r--r--launcher/resources/iOS/index.theme11
-rw-r--r--launcher/resources/iOS/scalable/about.svg16
-rw-r--r--launcher/resources/iOS/scalable/accounts.svg14
-rw-r--r--launcher/resources/iOS/scalable/bug.svg22
-rw-r--r--launcher/resources/iOS/scalable/centralmods.svg13
-rw-r--r--launcher/resources/iOS/scalable/checkupdate.svg16
-rw-r--r--launcher/resources/iOS/scalable/copy.svg13
-rw-r--r--launcher/resources/iOS/scalable/coremods.svg18
-rw-r--r--launcher/resources/iOS/scalable/externaltools.svg13
-rw-r--r--launcher/resources/iOS/scalable/help.svg38
-rw-r--r--launcher/resources/iOS/scalable/instance-settings.svg19
-rw-r--r--launcher/resources/iOS/scalable/jarmods.svg31
-rw-r--r--launcher/resources/iOS/scalable/java.svg33
-rw-r--r--launcher/resources/iOS/scalable/language.svg32
-rw-r--r--launcher/resources/iOS/scalable/loadermods.svg14
-rw-r--r--launcher/resources/iOS/scalable/log.svg13
-rw-r--r--launcher/resources/iOS/scalable/minecraft.svg8
-rw-r--r--launcher/resources/iOS/scalable/multimc.svg13
-rw-r--r--launcher/resources/iOS/scalable/new.svg13
-rw-r--r--launcher/resources/iOS/scalable/news.svg14
-rw-r--r--launcher/resources/iOS/scalable/notes.svg15
-rw-r--r--launcher/resources/iOS/scalable/patreon.svg12
-rw-r--r--launcher/resources/iOS/scalable/proxy.svg11
-rw-r--r--launcher/resources/iOS/scalable/quickmods.svg14
-rw-r--r--launcher/resources/iOS/scalable/refresh.svg13
-rw-r--r--launcher/resources/iOS/scalable/resourcepacks.svg15
-rw-r--r--launcher/resources/iOS/scalable/screenshots.svg14
-rw-r--r--launcher/resources/iOS/scalable/settings.svg19
-rw-r--r--launcher/resources/iOS/scalable/status-bad.svg10
-rw-r--r--launcher/resources/iOS/scalable/status-good.svg18
-rw-r--r--launcher/resources/iOS/scalable/status-yellow.svg56
-rw-r--r--launcher/resources/iOS/scalable/viewfolder.svg12
-rw-r--r--launcher/resources/iOS/scalable/worlds.svg44
-rw-r--r--launcher/resources/multimc.rc29
-rw-r--r--launcher/resources/multimc/128x128/instances/chicken.pngbin0 -> 6369 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/creeper.pngbin0 -> 9046 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/enderpearl.pngbin0 -> 21425 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/flame.pngbin0 -> 3375 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/ftb_glow.pngbin0 -> 12708 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/ftb_logo.pngbin0 -> 7883 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/gear.pngbin0 -> 18321 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/herobrine.pngbin0 -> 4937 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/infinity.pngbin0 -> 15151 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/magitech.pngbin0 -> 23097 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/meat.pngbin0 -> 10583 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/netherstar.pngbin0 -> 14062 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/skeleton.pngbin0 -> 3673 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/squarecreeper.pngbin0 -> 9136 bytes
-rw-r--r--launcher/resources/multimc/128x128/instances/steve.pngbin0 -> 4312 bytes
-rw-r--r--launcher/resources/multimc/128x128/unknown_server.pngbin0 -> 11085 bytes
-rw-r--r--launcher/resources/multimc/16x16/about.pngbin0 -> 1270 bytes
-rw-r--r--launcher/resources/multimc/16x16/bug.pngbin0 -> 734 bytes
-rw-r--r--launcher/resources/multimc/16x16/cat.pngbin0 -> 736 bytes
-rw-r--r--launcher/resources/multimc/16x16/centralmods.pngbin0 -> 1145 bytes
-rw-r--r--launcher/resources/multimc/16x16/checkupdate.pngbin0 -> 1212 bytes
-rw-r--r--launcher/resources/multimc/16x16/copy.pngbin0 -> 957 bytes
-rw-r--r--launcher/resources/multimc/16x16/coremods.pngbin0 -> 702 bytes
-rw-r--r--launcher/resources/multimc/16x16/help.pngbin0 -> 1297 bytes
-rw-r--r--launcher/resources/multimc/16x16/instance-settings.pngbin0 -> 1410 bytes
-rw-r--r--launcher/resources/multimc/16x16/jarmods.pngbin0 -> 693 bytes
-rw-r--r--launcher/resources/multimc/16x16/loadermods.pngbin0 -> 731 bytes
-rw-r--r--launcher/resources/multimc/16x16/log.pngbin0 -> 630 bytes
-rw-r--r--launcher/resources/multimc/16x16/minecraft.pngbin0 -> 782 bytes
-rw-r--r--launcher/resources/multimc/16x16/new.pngbin0 -> 1175 bytes
-rw-r--r--launcher/resources/multimc/16x16/news.pngbin0 -> 727 bytes
-rw-r--r--launcher/resources/multimc/16x16/noaccount.pngbin0 -> 334 bytes
-rw-r--r--launcher/resources/multimc/16x16/patreon.pngbin0 -> 840 bytes
-rw-r--r--launcher/resources/multimc/16x16/refresh.pngbin0 -> 931 bytes
-rw-r--r--launcher/resources/multimc/16x16/resourcepacks.pngbin0 -> 1207 bytes
-rw-r--r--launcher/resources/multimc/16x16/screenshots.pngbin0 -> 976 bytes
-rw-r--r--launcher/resources/multimc/16x16/settings.pngbin0 -> 1410 bytes
-rw-r--r--launcher/resources/multimc/16x16/star.pngbin0 -> 729 bytes
-rw-r--r--launcher/resources/multimc/16x16/status-bad.pngbin0 -> 643 bytes
-rw-r--r--launcher/resources/multimc/16x16/status-good.pngbin0 -> 714 bytes
-rw-r--r--launcher/resources/multimc/16x16/status-running.pngbin0 -> 675 bytes
-rw-r--r--launcher/resources/multimc/16x16/status-yellow.pngbin0 -> 590 bytes
-rw-r--r--launcher/resources/multimc/16x16/viewfolder.pngbin0 -> 852 bytes
-rw-r--r--launcher/resources/multimc/16x16/worlds.pngbin0 -> 870 bytes
-rw-r--r--launcher/resources/multimc/22x22/about.pngbin0 -> 1693 bytes
-rw-r--r--launcher/resources/multimc/22x22/bug.pngbin0 -> 1180 bytes
-rw-r--r--launcher/resources/multimc/22x22/cat.pngbin0 -> 1034 bytes
-rw-r--r--launcher/resources/multimc/22x22/centralmods.pngbin0 -> 1561 bytes
-rw-r--r--launcher/resources/multimc/22x22/checkupdate.pngbin0 -> 1635 bytes
-rw-r--r--launcher/resources/multimc/22x22/copy.pngbin0 -> 1004 bytes
-rw-r--r--launcher/resources/multimc/22x22/help.pngbin0 -> 1735 bytes
-rw-r--r--launcher/resources/multimc/22x22/instance-settings.pngbin0 -> 1964 bytes
-rw-r--r--launcher/resources/multimc/22x22/new.pngbin0 -> 1440 bytes
-rw-r--r--launcher/resources/multimc/22x22/news.pngbin0 -> 1173 bytes
-rw-r--r--launcher/resources/multimc/22x22/patreon.pngbin0 -> 939 bytes
-rw-r--r--launcher/resources/multimc/22x22/refresh.pngbin0 -> 1283 bytes
-rw-r--r--launcher/resources/multimc/22x22/screenshots.pngbin0 -> 1320 bytes
-rw-r--r--launcher/resources/multimc/22x22/settings.pngbin0 -> 1964 bytes
-rw-r--r--launcher/resources/multimc/22x22/status-bad.pngbin0 -> 968 bytes
-rw-r--r--launcher/resources/multimc/22x22/status-good.pngbin0 -> 994 bytes
-rw-r--r--launcher/resources/multimc/22x22/status-running.pngbin0 -> 957 bytes
-rw-r--r--launcher/resources/multimc/22x22/status-yellow.pngbin0 -> 803 bytes
-rw-r--r--launcher/resources/multimc/22x22/viewfolder.pngbin0 -> 1006 bytes
-rw-r--r--launcher/resources/multimc/22x22/worlds.pngbin0 -> 1397 bytes
-rw-r--r--launcher/resources/multimc/24x24/cat.pngbin0 -> 1252 bytes
-rw-r--r--launcher/resources/multimc/24x24/coremods.pngbin0 -> 1281 bytes
-rw-r--r--launcher/resources/multimc/24x24/jarmods.pngbin0 -> 1170 bytes
-rw-r--r--launcher/resources/multimc/24x24/loadermods.pngbin0 -> 1240 bytes
-rw-r--r--launcher/resources/multimc/24x24/log.pngbin0 -> 1117 bytes
-rw-r--r--launcher/resources/multimc/24x24/minecraft.pngbin0 -> 1500 bytes
-rw-r--r--launcher/resources/multimc/24x24/noaccount.pngbin0 -> 344 bytes
-rw-r--r--launcher/resources/multimc/24x24/patreon.pngbin0 -> 977 bytes
-rw-r--r--launcher/resources/multimc/24x24/resourcepacks.pngbin0 -> 2000 bytes
-rw-r--r--launcher/resources/multimc/24x24/star.pngbin0 -> 1217 bytes
-rw-r--r--launcher/resources/multimc/24x24/status-bad.pngbin0 -> 1102 bytes
-rw-r--r--launcher/resources/multimc/24x24/status-good.pngbin0 -> 1066 bytes
-rw-r--r--launcher/resources/multimc/24x24/status-running.pngbin0 -> 1059 bytes
-rw-r--r--launcher/resources/multimc/24x24/status-yellow.pngbin0 -> 872 bytes
-rw-r--r--launcher/resources/multimc/256x256/minecraft.pngbin0 -> 49869 bytes
-rw-r--r--launcher/resources/multimc/32x32/about.pngbin0 -> 2658 bytes
-rw-r--r--launcher/resources/multimc/32x32/bug.pngbin0 -> 1772 bytes
-rw-r--r--launcher/resources/multimc/32x32/cat.pngbin0 -> 1678 bytes
-rw-r--r--launcher/resources/multimc/32x32/centralmods.pngbin0 -> 2119 bytes
-rw-r--r--launcher/resources/multimc/32x32/checkupdate.pngbin0 -> 2480 bytes
-rw-r--r--launcher/resources/multimc/32x32/copy.pngbin0 -> 1401 bytes
-rw-r--r--launcher/resources/multimc/32x32/coremods.pngbin0 -> 1758 bytes
-rw-r--r--launcher/resources/multimc/32x32/help.pngbin0 -> 2720 bytes
-rw-r--r--launcher/resources/multimc/32x32/instance-settings.pngbin0 -> 2983 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/brick.pngbin0 -> 2388 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/chicken.pngbin0 -> 1181 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/creeper.pngbin0 -> 1524 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/diamond.pngbin0 -> 2444 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/dirt.pngbin0 -> 482 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/enderpearl.pngbin0 -> 2120 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/flame.pngbin0 -> 849 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/ftb_glow.pngbin0 -> 1747 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/ftb_logo.pngbin0 -> 1607 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/gear.pngbin0 -> 2414 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/gold.pngbin0 -> 2366 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/grass.pngbin0 -> 618 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/herobrine.pngbin0 -> 1034 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/infinity.pngbin0 -> 1714 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/iron.pngbin0 -> 1772 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/magitech.pngbin0 -> 2646 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/meat.pngbin0 -> 1514 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/netherstar.pngbin0 -> 1942 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/planks.pngbin0 -> 2299 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/skeleton.pngbin0 -> 696 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/squarecreeper.pngbin0 -> 1623 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/steve.pngbin0 -> 969 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/stone.pngbin0 -> 1866 bytes
-rw-r--r--launcher/resources/multimc/32x32/instances/tnt.pngbin0 -> 378 bytes
-rw-r--r--launcher/resources/multimc/32x32/jarmods.pngbin0 -> 1566 bytes
-rw-r--r--launcher/resources/multimc/32x32/loadermods.pngbin0 -> 1708 bytes
-rw-r--r--launcher/resources/multimc/32x32/log.pngbin0 -> 1460 bytes
-rw-r--r--launcher/resources/multimc/32x32/minecraft.pngbin0 -> 2495 bytes
-rw-r--r--launcher/resources/multimc/32x32/new.pngbin0 -> 1769 bytes
-rw-r--r--launcher/resources/multimc/32x32/news.pngbin0 -> 1752 bytes
-rw-r--r--launcher/resources/multimc/32x32/noaccount.pngbin0 -> 363 bytes
-rw-r--r--launcher/resources/multimc/32x32/patreon.pngbin0 -> 1086 bytes
-rw-r--r--launcher/resources/multimc/32x32/refresh.pngbin0 -> 2182 bytes
-rw-r--r--launcher/resources/multimc/32x32/resourcepacks.pngbin0 -> 2818 bytes
-rw-r--r--launcher/resources/multimc/32x32/screenshots.pngbin0 -> 1892 bytes
-rw-r--r--launcher/resources/multimc/32x32/settings.pngbin0 -> 2983 bytes
-rw-r--r--launcher/resources/multimc/32x32/star.pngbin0 -> 1698 bytes
-rw-r--r--launcher/resources/multimc/32x32/status-bad.pngbin0 -> 1422 bytes
-rw-r--r--launcher/resources/multimc/32x32/status-good.pngbin0 -> 1400 bytes
-rw-r--r--launcher/resources/multimc/32x32/status-running.pngbin0 -> 1425 bytes
-rw-r--r--launcher/resources/multimc/32x32/status-yellow.pngbin0 -> 1158 bytes
-rw-r--r--launcher/resources/multimc/32x32/viewfolder.pngbin0 -> 1518 bytes
-rw-r--r--launcher/resources/multimc/32x32/worlds.pngbin0 -> 2336 bytes
-rw-r--r--launcher/resources/multimc/48x48/about.pngbin0 -> 3995 bytes
-rw-r--r--launcher/resources/multimc/48x48/bug.pngbin0 -> 3124 bytes
-rw-r--r--launcher/resources/multimc/48x48/cat.pngbin0 -> 2733 bytes
-rw-r--r--launcher/resources/multimc/48x48/centralmods.pngbin0 -> 3201 bytes
-rw-r--r--launcher/resources/multimc/48x48/checkupdate.pngbin0 -> 4196 bytes
-rw-r--r--launcher/resources/multimc/48x48/copy.pngbin0 -> 1952 bytes
-rw-r--r--launcher/resources/multimc/48x48/help.pngbin0 -> 4170 bytes
-rw-r--r--launcher/resources/multimc/48x48/instance-settings.pngbin0 -> 4797 bytes
-rw-r--r--launcher/resources/multimc/48x48/log.pngbin0 -> 2825 bytes
-rw-r--r--launcher/resources/multimc/48x48/minecraft.pngbin0 -> 5077 bytes
-rw-r--r--launcher/resources/multimc/48x48/new.pngbin0 -> 2870 bytes
-rw-r--r--launcher/resources/multimc/48x48/news.pngbin0 -> 3333 bytes
-rw-r--r--launcher/resources/multimc/48x48/noaccount.pngbin0 -> 387 bytes
-rw-r--r--launcher/resources/multimc/48x48/patreon.pngbin0 -> 1390 bytes
-rw-r--r--launcher/resources/multimc/48x48/refresh.pngbin0 -> 3743 bytes
-rw-r--r--launcher/resources/multimc/48x48/screenshots.pngbin0 -> 3010 bytes
-rw-r--r--launcher/resources/multimc/48x48/settings.pngbin0 -> 4797 bytes
-rw-r--r--launcher/resources/multimc/48x48/star.pngbin0 -> 3227 bytes
-rw-r--r--launcher/resources/multimc/48x48/status-bad.pngbin0 -> 2389 bytes
-rw-r--r--launcher/resources/multimc/48x48/status-good.pngbin0 -> 2248 bytes
-rw-r--r--launcher/resources/multimc/48x48/status-running.pngbin0 -> 2288 bytes
-rw-r--r--launcher/resources/multimc/48x48/status-yellow.pngbin0 -> 1773 bytes
-rw-r--r--launcher/resources/multimc/48x48/viewfolder.pngbin0 -> 1945 bytes
-rw-r--r--launcher/resources/multimc/48x48/worlds.pngbin0 -> 4043 bytes
-rw-r--r--launcher/resources/multimc/50x50/instances/enderman.pngbin0 -> 2429 bytes
-rw-r--r--launcher/resources/multimc/64x64/about.pngbin0 -> 5513 bytes
-rw-r--r--launcher/resources/multimc/64x64/bug.pngbin0 -> 4263 bytes
-rw-r--r--launcher/resources/multimc/64x64/cat.pngbin0 -> 4033 bytes
-rw-r--r--launcher/resources/multimc/64x64/centralmods.pngbin0 -> 4408 bytes
-rw-r--r--launcher/resources/multimc/64x64/checkupdate.pngbin0 -> 5858 bytes
-rw-r--r--launcher/resources/multimc/64x64/copy.pngbin0 -> 2884 bytes
-rw-r--r--launcher/resources/multimc/64x64/coremods.pngbin0 -> 5036 bytes
-rw-r--r--launcher/resources/multimc/64x64/help.pngbin0 -> 5402 bytes
-rw-r--r--launcher/resources/multimc/64x64/instance-settings.pngbin0 -> 7125 bytes
-rw-r--r--launcher/resources/multimc/64x64/jarmods.pngbin0 -> 4003 bytes
-rw-r--r--launcher/resources/multimc/64x64/loadermods.pngbin0 -> 4406 bytes
-rw-r--r--launcher/resources/multimc/64x64/log.pngbin0 -> 3686 bytes
-rw-r--r--launcher/resources/multimc/64x64/new.pngbin0 -> 3949 bytes
-rw-r--r--launcher/resources/multimc/64x64/news.pngbin0 -> 4968 bytes
-rw-r--r--launcher/resources/multimc/64x64/patreon.pngbin0 -> 1667 bytes
-rw-r--r--launcher/resources/multimc/64x64/refresh.pngbin0 -> 5745 bytes
-rw-r--r--launcher/resources/multimc/64x64/resourcepacks.pngbin0 -> 6805 bytes
-rw-r--r--launcher/resources/multimc/64x64/screenshots.pngbin0 -> 4518 bytes
-rw-r--r--launcher/resources/multimc/64x64/settings.pngbin0 -> 7125 bytes
-rw-r--r--launcher/resources/multimc/64x64/star.pngbin0 -> 4554 bytes
-rw-r--r--launcher/resources/multimc/64x64/status-bad.pngbin0 -> 2827 bytes
-rw-r--r--launcher/resources/multimc/64x64/status-good.pngbin0 -> 2954 bytes
-rw-r--r--launcher/resources/multimc/64x64/status-running.pngbin0 -> 3178 bytes
-rw-r--r--launcher/resources/multimc/64x64/status-yellow.pngbin0 -> 2358 bytes
-rw-r--r--launcher/resources/multimc/64x64/viewfolder.pngbin0 -> 2134 bytes
-rw-r--r--launcher/resources/multimc/64x64/worlds.pngbin0 -> 7303 bytes
-rw-r--r--launcher/resources/multimc/8x8/noaccount.pngbin0 -> 284 bytes
-rw-r--r--launcher/resources/multimc/index.theme58
-rw-r--r--launcher/resources/multimc/multimc.qrc320
-rw-r--r--launcher/resources/multimc/scalable/atlauncher-placeholder.pngbin0 -> 13542 bytes
-rw-r--r--launcher/resources/multimc/scalable/atlauncher.svg15
-rw-r--r--launcher/resources/multimc/scalable/bug.svg387
-rw-r--r--launcher/resources/multimc/scalable/centralmods.svg346
-rw-r--r--launcher/resources/multimc/scalable/checkupdate.svg167
-rw-r--r--launcher/resources/multimc/scalable/custom-commands.svg338
-rw-r--r--launcher/resources/multimc/scalable/discord.svg108
-rw-r--r--launcher/resources/multimc/scalable/instances/bee.svg159
-rw-r--r--launcher/resources/multimc/scalable/instances/fox.svg290
-rw-r--r--launcher/resources/multimc/scalable/java.svg773
-rw-r--r--launcher/resources/multimc/scalable/language.svg109
-rw-r--r--launcher/resources/multimc/scalable/logo.svg353
-rw-r--r--launcher/resources/multimc/scalable/multimc.svg353
-rw-r--r--launcher/resources/multimc/scalable/new.svg127
-rw-r--r--launcher/resources/multimc/scalable/news.svg296
-rw-r--r--launcher/resources/multimc/scalable/proxy.svg260
-rw-r--r--launcher/resources/multimc/scalable/reddit-alien.svg189
-rw-r--r--launcher/resources/multimc/scalable/screenshot-placeholder.svg86
-rw-r--r--launcher/resources/multimc/scalable/screenshots.svg1231
-rw-r--r--launcher/resources/multimc/scalable/status-bad.svg142
-rw-r--r--launcher/resources/multimc/scalable/status-good.svg201
-rw-r--r--launcher/resources/multimc/scalable/status-running.svg187
-rw-r--r--launcher/resources/multimc/scalable/status-yellow.svg155
-rw-r--r--launcher/resources/multimc/scalable/technic.svg13
-rw-r--r--launcher/resources/multimc/scalable/viewfolder.svg122
-rw-r--r--launcher/resources/pe_blue/index.theme11
-rw-r--r--launcher/resources/pe_blue/pe_blue.qrc38
-rw-r--r--launcher/resources/pe_blue/scalable/about.svg16
-rw-r--r--launcher/resources/pe_blue/scalable/accounts.svg46
-rw-r--r--launcher/resources/pe_blue/scalable/bug.svg47
-rw-r--r--launcher/resources/pe_blue/scalable/centralmods.svg43
-rw-r--r--launcher/resources/pe_blue/scalable/checkupdate.svg43
-rw-r--r--launcher/resources/pe_blue/scalable/copy.svg41
-rw-r--r--launcher/resources/pe_blue/scalable/coremods.svg41
-rw-r--r--launcher/resources/pe_blue/scalable/externaltools.svg41
-rw-r--r--launcher/resources/pe_blue/scalable/help.svg40
-rw-r--r--launcher/resources/pe_blue/scalable/instance-settings.svg46
-rw-r--r--launcher/resources/pe_blue/scalable/jarmods.svg22
-rw-r--r--launcher/resources/pe_blue/scalable/java.svg47
-rw-r--r--launcher/resources/pe_blue/scalable/language.svg46
-rw-r--r--launcher/resources/pe_blue/scalable/loadermods.svg42
-rw-r--r--launcher/resources/pe_blue/scalable/log.svg41
-rw-r--r--launcher/resources/pe_blue/scalable/minecraft.svg44
-rw-r--r--launcher/resources/pe_blue/scalable/multimc.svg61
-rw-r--r--launcher/resources/pe_blue/scalable/new.svg44
-rw-r--r--launcher/resources/pe_blue/scalable/news.svg13
-rw-r--r--launcher/resources/pe_blue/scalable/notes.svg46
-rw-r--r--launcher/resources/pe_blue/scalable/patreon.svg41
-rw-r--r--launcher/resources/pe_blue/scalable/proxy.svg45
-rw-r--r--launcher/resources/pe_blue/scalable/quickmods.svg43
-rw-r--r--launcher/resources/pe_blue/scalable/refresh.svg41
-rw-r--r--launcher/resources/pe_blue/scalable/resourcepacks.svg13
-rw-r--r--launcher/resources/pe_blue/scalable/screenshots.svg44
-rw-r--r--launcher/resources/pe_blue/scalable/settings.svg46
-rw-r--r--launcher/resources/pe_blue/scalable/status-bad.svg10
-rw-r--r--launcher/resources/pe_blue/scalable/status-good.svg15
-rw-r--r--launcher/resources/pe_blue/scalable/status-yellow.svg16
-rw-r--r--launcher/resources/pe_blue/scalable/viewfolder.svg42
-rw-r--r--launcher/resources/pe_blue/scalable/worlds.svg63
-rw-r--r--launcher/resources/pe_colored/index.theme11
-rw-r--r--launcher/resources/pe_colored/pe_colored.qrc38
-rw-r--r--launcher/resources/pe_colored/scalable/about.svg19
-rw-r--r--launcher/resources/pe_colored/scalable/accounts.svg20
-rw-r--r--launcher/resources/pe_colored/scalable/bug.svg17
-rw-r--r--launcher/resources/pe_colored/scalable/centralmods.svg18
-rw-r--r--launcher/resources/pe_colored/scalable/checkupdate.svg13
-rw-r--r--launcher/resources/pe_colored/scalable/copy.svg16
-rw-r--r--launcher/resources/pe_colored/scalable/coremods.svg16
-rw-r--r--launcher/resources/pe_colored/scalable/externaltools.svg13
-rw-r--r--launcher/resources/pe_colored/scalable/help.svg46
-rw-r--r--launcher/resources/pe_colored/scalable/instance-settings.svg18
-rw-r--r--launcher/resources/pe_colored/scalable/jarmods.svg22
-rw-r--r--launcher/resources/pe_colored/scalable/java.svg49
-rw-r--r--launcher/resources/pe_colored/scalable/language.svg44
-rw-r--r--launcher/resources/pe_colored/scalable/loadermods.svg15
-rw-r--r--launcher/resources/pe_colored/scalable/log.svg16
-rw-r--r--launcher/resources/pe_colored/scalable/minecraft.svg14
-rw-r--r--launcher/resources/pe_colored/scalable/multimc.svg32
-rw-r--r--launcher/resources/pe_colored/scalable/new.svg16
-rw-r--r--launcher/resources/pe_colored/scalable/news.svg13
-rw-r--r--launcher/resources/pe_colored/scalable/notes.svg21
-rw-r--r--launcher/resources/pe_colored/scalable/patreon.svg12
-rw-r--r--launcher/resources/pe_colored/scalable/proxy.svg15
-rw-r--r--launcher/resources/pe_colored/scalable/quickmods.svg14
-rw-r--r--launcher/resources/pe_colored/scalable/refresh.svg11
-rw-r--r--launcher/resources/pe_colored/scalable/resourcepacks.svg15
-rw-r--r--launcher/resources/pe_colored/scalable/screenshots.svg16
-rw-r--r--launcher/resources/pe_colored/scalable/settings.svg18
-rw-r--r--launcher/resources/pe_colored/scalable/status-bad.svg10
-rw-r--r--launcher/resources/pe_colored/scalable/status-good.svg15
-rw-r--r--launcher/resources/pe_colored/scalable/status-yellow.svg16
-rw-r--r--launcher/resources/pe_colored/scalable/viewfolder.svg17
-rw-r--r--launcher/resources/pe_colored/scalable/worlds.svg50
-rw-r--r--launcher/resources/pe_dark/index.theme11
-rw-r--r--launcher/resources/pe_dark/pe_dark.qrc38
-rw-r--r--launcher/resources/pe_dark/scalable/about.svg15
-rw-r--r--launcher/resources/pe_dark/scalable/accounts.svg46
-rw-r--r--launcher/resources/pe_dark/scalable/bug.svg47
-rw-r--r--launcher/resources/pe_dark/scalable/centralmods.svg40
-rw-r--r--launcher/resources/pe_dark/scalable/checkupdate.svg12
-rw-r--r--launcher/resources/pe_dark/scalable/copy.svg40
-rw-r--r--launcher/resources/pe_dark/scalable/coremods.svg41
-rw-r--r--launcher/resources/pe_dark/scalable/externaltools.svg41
-rw-r--r--launcher/resources/pe_dark/scalable/help.svg34
-rw-r--r--launcher/resources/pe_dark/scalable/instance-settings.svg43
-rw-r--r--launcher/resources/pe_dark/scalable/jarmods.svg41
-rw-r--r--launcher/resources/pe_dark/scalable/java.svg48
-rw-r--r--launcher/resources/pe_dark/scalable/language.svg45
-rw-r--r--launcher/resources/pe_dark/scalable/loadermods.svg42
-rw-r--r--launcher/resources/pe_dark/scalable/log.svg41
-rw-r--r--launcher/resources/pe_dark/scalable/minecraft.svg44
-rw-r--r--launcher/resources/pe_dark/scalable/multimc.svg61
-rw-r--r--launcher/resources/pe_dark/scalable/new.svg41
-rw-r--r--launcher/resources/pe_dark/scalable/news.svg13
-rw-r--r--launcher/resources/pe_dark/scalable/notes.svg46
-rw-r--r--launcher/resources/pe_dark/scalable/patreon.svg41
-rw-r--r--launcher/resources/pe_dark/scalable/proxy.svg43
-rw-r--r--launcher/resources/pe_dark/scalable/quickmods.svg43
-rw-r--r--launcher/resources/pe_dark/scalable/refresh.svg11
-rw-r--r--launcher/resources/pe_dark/scalable/resourcepacks.svg13
-rw-r--r--launcher/resources/pe_dark/scalable/screenshots.svg44
-rw-r--r--launcher/resources/pe_dark/scalable/settings.svg43
-rw-r--r--launcher/resources/pe_dark/scalable/status-bad.svg10
-rw-r--r--launcher/resources/pe_dark/scalable/status-good.svg12
-rw-r--r--launcher/resources/pe_dark/scalable/status-yellow.svg16
-rw-r--r--launcher/resources/pe_dark/scalable/viewfolder.svg39
-rw-r--r--launcher/resources/pe_dark/scalable/worlds.svg63
-rw-r--r--launcher/resources/pe_light/index.theme11
-rw-r--r--launcher/resources/pe_light/pe_light.qrc39
-rw-r--r--launcher/resources/pe_light/scalable/about.svg15
-rw-r--r--launcher/resources/pe_light/scalable/accounts.svg46
-rw-r--r--launcher/resources/pe_light/scalable/bug.svg47
-rw-r--r--launcher/resources/pe_light/scalable/centralmods.svg41
-rw-r--r--launcher/resources/pe_light/scalable/checkupdate.svg13
-rw-r--r--launcher/resources/pe_light/scalable/copy.svg40
-rw-r--r--launcher/resources/pe_light/scalable/coremods.svg41
-rw-r--r--launcher/resources/pe_light/scalable/externaltools.svg41
-rw-r--r--launcher/resources/pe_light/scalable/help.svg36
-rw-r--r--launcher/resources/pe_light/scalable/instance-settings.svg43
-rw-r--r--launcher/resources/pe_light/scalable/jarmods.svg41
-rw-r--r--launcher/resources/pe_light/scalable/java.svg49
-rw-r--r--launcher/resources/pe_light/scalable/language.svg80
-rw-r--r--launcher/resources/pe_light/scalable/loadermods.svg42
-rw-r--r--launcher/resources/pe_light/scalable/log.svg41
-rw-r--r--launcher/resources/pe_light/scalable/minecraft.svg44
-rw-r--r--launcher/resources/pe_light/scalable/multimc.svg61
-rw-r--r--launcher/resources/pe_light/scalable/new.svg41
-rw-r--r--launcher/resources/pe_light/scalable/news.svg12
-rw-r--r--launcher/resources/pe_light/scalable/notes.svg46
-rw-r--r--launcher/resources/pe_light/scalable/patreon.svg40
-rw-r--r--launcher/resources/pe_light/scalable/proxy.svg45
-rw-r--r--launcher/resources/pe_light/scalable/quickmods.svg43
-rw-r--r--launcher/resources/pe_light/scalable/refresh.svg11
-rw-r--r--launcher/resources/pe_light/scalable/resourcepacks.svg13
-rw-r--r--launcher/resources/pe_light/scalable/screenshots.svg44
-rw-r--r--launcher/resources/pe_light/scalable/settings.svg43
-rw-r--r--launcher/resources/pe_light/scalable/status-bad.svg10
-rw-r--r--launcher/resources/pe_light/scalable/status-good.svg12
-rw-r--r--launcher/resources/pe_light/scalable/status-yellow.svg16
-rw-r--r--launcher/resources/pe_light/scalable/viewfolder.svg40
-rw-r--r--launcher/resources/pe_light/scalable/worlds.svg64
-rw-r--r--launcher/resources/sources/clucker.svg404
-rw-r--r--launcher/resources/sources/creeper.svg775
-rw-r--r--launcher/resources/sources/enderpearl.svg271
-rw-r--r--launcher/resources/sources/flame.svg51
-rw-r--r--launcher/resources/sources/ftb-glow.svg606
-rw-r--r--launcher/resources/sources/ftb-logo.svg257
-rw-r--r--launcher/resources/sources/gear.svg434
-rw-r--r--launcher/resources/sources/herobrine.svg583
-rw-r--r--launcher/resources/sources/magitech.svg886
-rw-r--r--launcher/resources/sources/meat.svg371
-rw-r--r--launcher/resources/sources/multimc-discord.svg265
-rw-r--r--launcher/resources/sources/netherstar.svg342
-rw-r--r--launcher/resources/sources/pskeleton.svg581
-rw-r--r--launcher/resources/sources/skeleton.svg610
-rw-r--r--launcher/resources/sources/squarecreeper.svg828
-rw-r--r--launcher/resources/sources/steve.svg534
-rw-r--r--launcher/screenshots/ImgurAlbumCreation.cpp88
-rw-r--r--launcher/screenshots/ImgurAlbumCreation.h42
-rw-r--r--launcher/screenshots/ImgurUpload.cpp114
-rw-r--r--launcher/screenshots/ImgurUpload.h31
-rw-r--r--launcher/screenshots/Screenshot.h20
-rw-r--r--launcher/settings/INIFile.cpp163
-rw-r--r--launcher/settings/INIFile.h36
-rw-r--r--launcher/settings/INIFile_test.cpp63
-rw-r--r--launcher/settings/INISettingsObject.cpp107
-rw-r--r--launcher/settings/INISettingsObject.h64
-rw-r--r--launcher/settings/OverrideSetting.cpp54
-rw-r--r--launcher/settings/OverrideSetting.h46
-rw-r--r--launcher/settings/PassthroughSetting.cpp69
-rw-r--r--launcher/settings/PassthroughSetting.h45
-rw-r--r--launcher/settings/Setting.cpp53
-rw-r--r--launcher/settings/Setting.h117
-rw-r--r--launcher/settings/SettingsObject.cpp142
-rw-r--r--launcher/settings/SettingsObject.h212
-rw-r--r--launcher/setupwizard/AnalyticsWizardPage.cpp63
-rw-r--r--launcher/setupwizard/AnalyticsWizardPage.h25
-rw-r--r--launcher/setupwizard/BaseWizardPage.h33
-rw-r--r--launcher/setupwizard/JavaWizardPage.cpp96
-rw-r--r--launcher/setupwizard/JavaWizardPage.h29
-rw-r--r--launcher/setupwizard/LanguageWizardPage.cpp48
-rw-r--r--launcher/setupwizard/LanguageWizardPage.h26
-rw-r--r--launcher/setupwizard/SetupWizard.cpp88
-rw-r--r--launcher/setupwizard/SetupWizard.h45
-rw-r--r--launcher/status/StatusChecker.cpp148
-rw-r--r--launcher/status/StatusChecker.h58
-rw-r--r--launcher/tasks/SequentialTask.cpp55
-rw-r--r--launcher/tasks/SequentialTask.h30
-rw-r--r--launcher/tasks/Task.cpp168
-rw-r--r--launcher/tasks/Task.h106
-rwxr-xr-xlauncher/testdata/FileSystem-test_createShortcut-unix6
-rw-r--r--launcher/testdata/test_folder/assets/minecraft/textures/blah.txt1
-rw-r--r--launcher/testdata/test_folder/pack.mcmeta6
-rw-r--r--launcher/testdata/test_folder/pack.nfo1
-rw-r--r--launcher/themes/BrightTheme.cpp56
-rw-r--r--launcher/themes/BrightTheme.h19
-rw-r--r--launcher/themes/CustomTheme.cpp244
-rw-r--r--launcher/themes/CustomTheme.h31
-rw-r--r--launcher/themes/DarkTheme.cpp55
-rw-r--r--launcher/themes/DarkTheme.h18
-rw-r--r--launcher/themes/FusionTheme.cpp6
-rw-r--r--launcher/themes/FusionTheme.h11
-rw-r--r--launcher/themes/ITheme.cpp47
-rw-r--r--launcher/themes/ITheme.h27
-rw-r--r--launcher/themes/SystemTheme.cpp83
-rw-r--r--launcher/themes/SystemTheme.h24
-rw-r--r--launcher/tools/BaseExternalTool.cpp41
-rw-r--r--launcher/tools/BaseExternalTool.h58
-rw-r--r--launcher/tools/BaseProfiler.cpp36
-rw-r--r--launcher/tools/BaseProfiler.h37
-rw-r--r--launcher/tools/JProfiler.cpp116
-rw-r--r--launcher/tools/JProfiler.h13
-rw-r--r--launcher/tools/JVisualVM.cpp104
-rw-r--r--launcher/tools/JVisualVM.h13
-rw-r--r--launcher/tools/MCEditTool.cpp77
-rw-r--r--launcher/tools/MCEditTool.h16
-rw-r--r--launcher/translations/POTranslator.cpp373
-rw-r--r--launcher/translations/POTranslator.h16
-rw-r--r--launcher/translations/TranslationsModel.cpp653
-rw-r--r--launcher/translations/TranslationsModel.h64
-rw-r--r--launcher/updater/DownloadTask.cpp173
-rw-r--r--launcher/updater/DownloadTask.h96
-rw-r--r--launcher/updater/DownloadTask_test.cpp195
-rw-r--r--launcher/updater/GoUpdate.cpp198
-rw-r--r--launcher/updater/GoUpdate.h125
-rw-r--r--launcher/updater/UpdateChecker.cpp260
-rw-r--r--launcher/updater/UpdateChecker.h119
-rw-r--r--launcher/updater/UpdateChecker_test.cpp147
-rw-r--r--launcher/updater/testdata/1.json43
-rw-r--r--launcher/updater/testdata/2.json31
-rw-r--r--launcher/updater/testdata/channels.json23
-rw-r--r--launcher/updater/testdata/errorChannels.json23
-rw-r--r--launcher/updater/testdata/fileOneA1
-rw-r--r--launcher/updater/testdata/fileOneB3
-rw-r--r--launcher/updater/testdata/fileThree1
-rw-r--r--launcher/updater/testdata/fileTwo1
-rw-r--r--launcher/updater/testdata/garbageChannels.json22
-rw-r--r--launcher/updater/testdata/index.json9
-rw-r--r--launcher/updater/testdata/noChannels.json5
-rw-r--r--launcher/updater/testdata/oneChannel.json11
-rw-r--r--launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml17
-rw-r--r--launcher/widgets/Common.cpp27
-rw-r--r--launcher/widgets/Common.h6
-rw-r--r--launcher/widgets/CustomCommands.cpp49
-rw-r--r--launcher/widgets/CustomCommands.h43
-rw-r--r--launcher/widgets/CustomCommands.ui107
-rw-r--r--launcher/widgets/DropLabel.cpp41
-rw-r--r--launcher/widgets/DropLabel.h20
-rw-r--r--launcher/widgets/FocusLineEdit.cpp25
-rw-r--r--launcher/widgets/FocusLineEdit.h17
-rw-r--r--launcher/widgets/IconLabel.cpp43
-rw-r--r--launcher/widgets/IconLabel.h26
-rw-r--r--launcher/widgets/InstanceCardWidget.ui58
-rw-r--r--launcher/widgets/JavaSettingsWidget.cpp428
-rw-r--r--launcher/widgets/JavaSettingsWidget.h102
-rw-r--r--launcher/widgets/LabeledToolButton.cpp115
-rw-r--r--launcher/widgets/LabeledToolButton.h40
-rw-r--r--launcher/widgets/LanguageSelectionWidget.cpp66
-rw-r--r--launcher/widgets/LanguageSelectionWidget.h41
-rw-r--r--launcher/widgets/LineSeparator.cpp37
-rw-r--r--launcher/widgets/LineSeparator.h18
-rw-r--r--launcher/widgets/LogView.cpp144
-rw-r--r--launcher/widgets/LogView.h36
-rw-r--r--launcher/widgets/MCModInfoFrame.cpp167
-rw-r--r--launcher/widgets/MCModInfoFrame.h52
-rw-r--r--launcher/widgets/MCModInfoFrame.ui92
-rw-r--r--launcher/widgets/ModListView.cpp66
-rw-r--r--launcher/widgets/ModListView.h25
-rw-r--r--launcher/widgets/PageContainer.cpp239
-rw-r--r--launcher/widgets/PageContainer.h89
-rw-r--r--launcher/widgets/PageContainer_p.h123
-rw-r--r--launcher/widgets/ProgressWidget.cpp73
-rw-r--r--launcher/widgets/ProgressWidget.h32
-rw-r--r--launcher/widgets/ServerStatus.cpp179
-rw-r--r--launcher/widgets/ServerStatus.h40
-rw-r--r--launcher/widgets/VersionListView.cpp163
-rw-r--r--launcher/widgets/VersionListView.h56
-rw-r--r--launcher/widgets/VersionSelectWidget.cpp202
-rw-r--r--launcher/widgets/VersionSelectWidget.h81
-rw-r--r--launcher/widgets/WideBar.cpp116
-rw-r--r--launcher/widgets/WideBar.h26
1107 files changed, 99235 insertions, 0 deletions
diff --git a/launcher/BaseInstaller.cpp b/launcher/BaseInstaller.cpp
new file mode 100644
index 00000000..d61c3fe9
--- /dev/null
+++ b/launcher/BaseInstaller.cpp
@@ -0,0 +1,61 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QFile>
+
+#include "BaseInstaller.h"
+#include "minecraft/MinecraftInstance.h"
+
+BaseInstaller::BaseInstaller()
+{
+
+}
+
+bool BaseInstaller::isApplied(MinecraftInstance *on)
+{
+ return QFile::exists(filename(on->instanceRoot()));
+}
+
+bool BaseInstaller::add(MinecraftInstance *to)
+{
+ if (!patchesDir(to->instanceRoot()).exists())
+ {
+ QDir(to->instanceRoot()).mkdir("patches");
+ }
+
+ if (isApplied(to))
+ {
+ if (!remove(to))
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool BaseInstaller::remove(MinecraftInstance *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/launcher/BaseInstaller.h b/launcher/BaseInstaller.h
new file mode 100644
index 00000000..b2e6a14d
--- /dev/null
+++ b/launcher/BaseInstaller.h
@@ -0,0 +1,44 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+
+class MinecraftInstance;
+class QDir;
+class QString;
+class QObject;
+class Task;
+class BaseVersion;
+typedef std::shared_ptr<BaseVersion> BaseVersionPtr;
+
+class BaseInstaller
+{
+public:
+ BaseInstaller();
+ virtual ~BaseInstaller(){};
+ bool isApplied(MinecraftInstance *on);
+
+ virtual bool add(MinecraftInstance *to);
+ virtual bool remove(MinecraftInstance *from);
+
+ virtual Task *createInstallTask(MinecraftInstance *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/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp
new file mode 100644
index 00000000..46b45827
--- /dev/null
+++ b/launcher/BaseInstance.cpp
@@ -0,0 +1,275 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "BaseInstance.h"
+
+#include <QFileInfo>
+#include <QDir>
+#include <QDebug>
+
+#include "settings/INISettingsObject.h"
+#include "settings/Setting.h"
+#include "settings/OverrideSetting.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);
+ m_settings->registerSetting("lastTimePlayed", 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("ShowConsoleOnError"), consoleSetting);
+ m_settings->registerOverride(globalSettings->getSetting("LogPrePostOutput"), consoleSetting);
+
+ m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr);
+ m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr);
+}
+
+QString BaseInstance::getPreLaunchCommand()
+{
+ return settings()->get("PreLaunchCommand").toString();
+}
+
+QString BaseInstance::getWrapperCommand()
+{
+ return settings()->get("WrapperCommand").toString();
+}
+
+QString BaseInstance::getPostExitCommand()
+{
+ return settings()->get("PostExitCommand").toString();
+}
+
+int BaseInstance::getConsoleMaxLines() const
+{
+ auto lineSetting = settings()->getSetting("ConsoleMaxLines");
+ bool conversionOk = false;
+ int maxLines = lineSetting->get().toInt(&conversionOk);
+ if(!conversionOk)
+ {
+ maxLines = lineSetting->defValue().toInt();
+ qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines;
+ }
+ return maxLines;
+}
+
+bool BaseInstance::shouldStopOnConsoleOverflow() const
+{
+ return settings()->get("ConsoleOverflowStop").toBool();
+}
+
+void BaseInstance::iconUpdated(QString key)
+{
+ if(iconKey() == key)
+ {
+ emit propertiesChanged(this);
+ }
+}
+
+void BaseInstance::invalidate()
+{
+ changeStatus(Status::Gone);
+ qDebug() << "Instance" << id() << "has been invalidated.";
+}
+
+void BaseInstance::changeStatus(BaseInstance::Status newStatus)
+{
+ Status status = currentStatus();
+ if(status != newStatus)
+ {
+ m_status = newStatus;
+ emit statusChanged(status, newStatus);
+ }
+}
+
+BaseInstance::Status BaseInstance::currentStatus() const
+{
+ return m_status;
+}
+
+QString BaseInstance::id() const
+{
+ return QFileInfo(instanceRoot()).fileName();
+}
+
+bool BaseInstance::isRunning() const
+{
+ return m_isRunning;
+}
+
+void BaseInstance::setRunning(bool running)
+{
+ if(running == m_isRunning)
+ return;
+
+ m_isRunning = running;
+
+ if(!m_settings->get("RecordGameTime").toBool())
+ {
+ emit runningStatusChanged(running);
+ return;
+ }
+
+ if(running)
+ {
+ m_timeStarted = QDateTime::currentDateTime();
+ }
+ else
+ {
+ QDateTime timeEnded = QDateTime::currentDateTime();
+
+ qint64 current = settings()->get("totalTimePlayed").toLongLong();
+ settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded));
+ settings()->set("lastTimePlayed", m_timeStarted.secsTo(timeEnded));
+
+ emit propertiesChanged(this);
+ }
+
+ emit runningStatusChanged(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;
+}
+
+int64_t BaseInstance::lastTimePlayed() const
+{
+ if(m_isRunning)
+ {
+ QDateTime timeNow = QDateTime::currentDateTime();
+ return m_timeStarted.secsTo(timeNow);
+ }
+ return settings()->get("lastTimePlayed").toLongLong();
+}
+
+void BaseInstance::resetTimePlayed()
+{
+ settings()->reset("totalTimePlayed");
+ settings()->reset("lastTimePlayed");
+}
+
+QString BaseInstance::instanceType() const
+{
+ return m_settings->get("InstanceType").toString();
+}
+
+QString BaseInstance::instanceRoot() const
+{
+ return m_rootDir;
+}
+
+SettingsObjectPtr BaseInstance::settings() const
+{
+ return m_settings;
+}
+
+bool BaseInstance::canLaunch() const
+{
+ return (!hasVersionBroken() && !isRunning());
+}
+
+bool BaseInstance::reloadSettings()
+{
+ 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::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().replace(QRegExp("[ \n\r\t]+"), " ");
+}
+
+// FIXME: why is this here? move it to MinecraftInstance!!!
+QStringList BaseInstance::extraArguments() const
+{
+ return Commandline::splitArgs(settings()->get("JvmArgs").toString());
+}
+
+shared_qobject_ptr<LaunchTask> BaseInstance::getLaunchTask()
+{
+ return m_launchProcess;
+}
diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h
new file mode 100644
index 00000000..833646c0
--- /dev/null
+++ b/launcher/BaseInstance.h
@@ -0,0 +1,270 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <cassert>
+
+#include <QObject>
+#include "QObjectPtr.h"
+#include <QDateTime>
+#include <QSet>
+#include <QProcess>
+
+#include "settings/SettingsObject.h"
+
+#include "settings/INIFile.h"
+#include "BaseVersionList.h"
+#include "minecraft/auth/MojangAccount.h"
+#include "MessageLevel.h"
+#include "pathmatcher/IPathMatcher.h"
+
+#include "net/Mode.h"
+
+#include "minecraft/launch/MinecraftServerTarget.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 BaseInstance : public QObject, public std::enable_shared_from_this<BaseInstance>
+{
+ Q_OBJECT
+protected:
+ /// no-touchy!
+ BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+
+public: /* types */
+ enum class Status
+ {
+ Present,
+ Gone // either nuked or invalidated
+ };
+
+public:
+ /// virtual destructor to make sure the destruction is COMPLETE
+ virtual ~BaseInstance() {};
+
+ virtual void saveNow() = 0;
+
+ /***
+ * the instance has been invalidated - it is no longer tracked by MultiMC for some reason,
+ * but it has not necessarily been deleted.
+ *
+ * Happens when the instance folder changes to some other location, or the instance is removed by external means.
+ */
+ void invalidate();
+
+ /// 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;
+ int64_t lastTimePlayed() const;
+ void resetTimePlayed();
+
+ /// get the type of this instance
+ QString instanceType() const;
+
+ /// Path to the instance's root directory.
+ QString instanceRoot() const;
+
+ /// Path to the instance's game root directory.
+ virtual QString gameRoot() const
+ {
+ return instanceRoot();
+ }
+
+ 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 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;
+
+ /// Traits. Normally inside the version, depends on instance implementation.
+ virtual QSet <QString> traits() const = 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());
+
+ /*!
+ * \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 shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) = 0;
+
+ /// returns a valid launcher (task container)
+ virtual shared_qobject_ptr<LaunchTask> createLaunchTask(
+ AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) = 0;
+
+ /// returns the current launch task (if any)
+ shared_qobject_ptr<LaunchTask> getLaunchTask();
+
+ /*!
+ * 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;
+
+ 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;
+
+ bool hasVersionBroken() const
+ {
+ return m_hasBrokenVersion;
+ }
+ void setVersionBroken(bool value)
+ {
+ if(m_hasBrokenVersion != value)
+ {
+ m_hasBrokenVersion = value;
+ emit propertiesChanged(this);
+ }
+ }
+
+ bool hasUpdateAvailable() const
+ {
+ return m_hasUpdate;
+ }
+ void setUpdateAvailable(bool value)
+ {
+ if(m_hasUpdate != value)
+ {
+ m_hasUpdate = value;
+ emit propertiesChanged(this);
+ }
+ }
+
+ bool hasCrashed() const
+ {
+ return m_crashed;
+ }
+ void setCrashed(bool value)
+ {
+ if(m_crashed != value)
+ {
+ m_crashed = value;
+ emit propertiesChanged(this);
+ }
+ }
+
+ virtual bool canLaunch() const;
+ virtual bool canEdit() const = 0;
+ virtual bool canExport() const = 0;
+
+ bool reloadSettings();
+
+ /**
+ * 'print' a verbose description of the instance into a QStringList
+ */
+ virtual QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) = 0;
+
+ Status currentStatus() const;
+
+ int getConsoleMaxLines() const;
+ bool shouldStopOnConsoleOverflow() const;
+
+protected:
+ void changeStatus(Status newStatus);
+
+signals:
+ /*!
+ * \brief Signal emitted when properties relevant to the instance view change
+ */
+ void propertiesChanged(BaseInstance *inst);
+
+ void launchTaskChanged(shared_qobject_ptr<LaunchTask>);
+
+ void runningStatusChanged(bool running);
+
+ void statusChanged(Status from, Status to);
+
+protected slots:
+ void iconUpdated(QString key);
+
+protected: /* data */
+ QString m_rootDir;
+ SettingsObjectPtr m_settings;
+ // InstanceFlags m_flags;
+ bool m_isRunning = false;
+ shared_qobject_ptr<LaunchTask> m_launchProcess;
+ QDateTime m_timeStarted;
+
+private: /* data */
+ Status m_status = Status::Present;
+ bool m_crashed = false;
+ bool m_hasUpdate = false;
+ bool m_hasBrokenVersion = false;
+};
+
+Q_DECLARE_METATYPE(shared_qobject_ptr<BaseInstance>)
+//Q_DECLARE_METATYPE(BaseInstance::InstanceFlag)
+//Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags)
diff --git a/launcher/BaseVersion.h b/launcher/BaseVersion.h
new file mode 100644
index 00000000..b88105fb
--- /dev/null
+++ b/launcher/BaseVersion.h
@@ -0,0 +1,59 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#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/launcher/BaseVersionList.cpp b/launcher/BaseVersionList.cpp
new file mode 100644
index 00000000..aa9cb6cf
--- /dev/null
+++ b/launcher/BaseVersionList.cpp
@@ -0,0 +1,99 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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::getRecommended() const
+{
+ if (count() <= 0)
+ return BaseVersionPtr();
+ else
+ return at(0);
+}
+
+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(ParentVersionRole, "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/launcher/BaseVersionList.h b/launcher/BaseVersionList.h
new file mode 100644
index 00000000..ce7abce1
--- /dev/null
+++ b/launcher/BaseVersionList.h
@@ -0,0 +1,121 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+
+#include "BaseVersion.h"
+#include "tasks/Task.h"
+#include "QObjectPtr.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 BaseVersionList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum ModelRoles
+ {
+ VersionPointerRole = Qt::UserRole,
+ VersionRole,
+ VersionIdRole,
+ ParentVersionRole,
+ 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 shared_qobject_ptr<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 ////////
+ QVariant data(const QModelIndex &index, int role) const override;
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ 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 descriptor 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 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/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
new file mode 100644
index 00000000..e3ef5039
--- /dev/null
+++ b/launcher/CMakeLists.txt
@@ -0,0 +1,1001 @@
+project(application)
+
+################################ FILES ################################
+
+######## Sources and headers ########
+
+include (UnitTest)
+
+set(CORE_SOURCES
+ # LOGIC - Base classes and infrastructure
+ BaseInstaller.h
+ BaseInstaller.cpp
+ BaseVersionList.h
+ BaseVersionList.cpp
+ InstanceList.h
+ InstanceList.cpp
+ InstanceTask.h
+ InstanceTask.cpp
+ LoggedProcess.h
+ LoggedProcess.cpp
+ MessageLevel.cpp
+ MessageLevel.h
+ BaseVersion.h
+ BaseInstance.h
+ BaseInstance.cpp
+ NullInstance.h
+ MMCZip.h
+ MMCZip.cpp
+ MMCStrings.h
+ MMCStrings.cpp
+
+ # Basic instance manipulation tasks (derived from InstanceTask)
+ InstanceCreationTask.h
+ InstanceCreationTask.cpp
+ InstanceCopyTask.h
+ InstanceCopyTask.cpp
+ InstanceImportTask.h
+ InstanceImportTask.cpp
+
+ # Use tracking separate from memory management
+ Usable.h
+
+ # Prefix tree where node names are strings between separators
+ SeparatorPrefixTree.h
+
+ # WARNING: globals live here
+ Env.h
+ Env.cpp
+
+ # String filters
+ Filter.h
+ Filter.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
+
+ # Compression support
+ GZip.h
+ GZip.cpp
+
+ # Command line parameter parsing
+ Commandline.h
+ Commandline.cpp
+
+ # Version number string support
+ Version.h
+ Version.cpp
+
+ # A Recursive file system watcher
+ RecursiveFileSystemWatcher.h
+ RecursiveFileSystemWatcher.cpp
+)
+
+add_unit_test(FileSystem
+ SOURCES FileSystem_test.cpp
+ LIBS MultiMC_logic
+ DATA testdata
+ )
+
+add_unit_test(GZip
+ SOURCES GZip_test.cpp
+ LIBS MultiMC_logic
+ )
+
+set(PATHMATCHER_SOURCES
+ # Path matchers
+ pathmatcher/FSTreeMatcher.h
+ pathmatcher/IPathMatcher.h
+ pathmatcher/MultiMatcher.h
+ pathmatcher/RegexpMatcher.h
+)
+
+set(NET_SOURCES
+ # network stuffs
+ net/ByteArraySink.h
+ net/ChecksumValidator.h
+ net/Download.cpp
+ net/Download.h
+ net/FileSink.cpp
+ net/FileSink.h
+ net/HttpMetaCache.cpp
+ net/HttpMetaCache.h
+ net/MetaCacheSink.cpp
+ net/MetaCacheSink.h
+ net/NetAction.h
+ net/NetJob.cpp
+ net/NetJob.h
+ net/PasteUpload.cpp
+ net/PasteUpload.h
+ net/Sink.h
+ net/Validator.h
+)
+
+# Game launch logic
+set(LAUNCH_SOURCES
+ launch/steps/LookupServerAddress.cpp
+ launch/steps/LookupServerAddress.h
+ launch/steps/PostLaunchCommand.cpp
+ launch/steps/PostLaunchCommand.h
+ launch/steps/PreLaunchCommand.cpp
+ 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/LogModel.cpp
+ launch/LogModel.h
+)
+
+# Old update system
+set(UPDATE_SOURCES
+ updater/GoUpdate.h
+ updater/GoUpdate.cpp
+ updater/UpdateChecker.h
+ updater/UpdateChecker.cpp
+ updater/DownloadTask.h
+ updater/DownloadTask.cpp
+)
+
+add_unit_test(UpdateChecker
+ SOURCES updater/UpdateChecker_test.cpp
+ LIBS MultiMC_logic
+ DATA updater/testdata
+ )
+
+add_unit_test(DownloadTask
+ SOURCES updater/DownloadTask_test.cpp
+ LIBS MultiMC_logic
+ DATA updater/testdata
+ )
+
+# Rarely used notifications
+set(NOTIFICATIONS_SOURCES
+ # Notifications - short warning messages
+ notifications/NotificationChecker.h
+ notifications/NotificationChecker.cpp
+)
+
+# Backend for the news bar... there's usually no news.
+set(NEWS_SOURCES
+ # News System
+ news/NewsChecker.h
+ news/NewsChecker.cpp
+ news/NewsEntry.h
+ news/NewsEntry.cpp
+)
+
+# Icon interface
+set(ICONS_SOURCES
+ # Icons System and related code
+ icons/IIconList.h
+ icons/IIconList.cpp
+ icons/IconUtils.h
+ icons/IconUtils.cpp
+)
+
+# Minecraft services status checker
+set(STATUS_SOURCES
+ # Status system
+ status/StatusChecker.h
+ status/StatusChecker.cpp
+)
+
+# Support for Minecraft instances and launch
+set(MINECRAFT_SOURCES
+ # Minecraft support
+ 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
+
+ minecraft/gameoptions/GameOptions.h
+ minecraft/gameoptions/GameOptions.cpp
+
+ minecraft/update/AssetUpdateTask.h
+ minecraft/update/AssetUpdateTask.cpp
+ minecraft/update/FMLLibrariesTask.cpp
+ minecraft/update/FMLLibrariesTask.h
+ minecraft/update/FoldersTask.cpp
+ minecraft/update/FoldersTask.h
+ minecraft/update/LibrariesTask.cpp
+ minecraft/update/LibrariesTask.h
+
+ minecraft/launch/ClaimAccount.cpp
+ minecraft/launch/ClaimAccount.h
+ minecraft/launch/CreateGameFolders.cpp
+ minecraft/launch/CreateGameFolders.h
+ minecraft/launch/ModMinecraftJar.cpp
+ minecraft/launch/ModMinecraftJar.h
+ minecraft/launch/DirectJavaLaunch.cpp
+ minecraft/launch/DirectJavaLaunch.h
+ minecraft/launch/ExtractNatives.cpp
+ minecraft/launch/ExtractNatives.h
+ minecraft/launch/LauncherPartLaunch.cpp
+ minecraft/launch/LauncherPartLaunch.h
+ minecraft/launch/MinecraftServerTarget.cpp
+ minecraft/launch/MinecraftServerTarget.h
+ minecraft/launch/PrintInstanceInfo.cpp
+ minecraft/launch/PrintInstanceInfo.h
+ minecraft/launch/ReconstructAssets.cpp
+ minecraft/launch/ReconstructAssets.h
+ minecraft/launch/ScanModFolders.cpp
+ minecraft/launch/ScanModFolders.h
+ minecraft/launch/VerifyJavaInstall.cpp
+ minecraft/launch/VerifyJavaInstall.h
+
+ minecraft/legacy/LegacyModList.h
+ minecraft/legacy/LegacyModList.cpp
+ minecraft/legacy/LegacyInstance.h
+ minecraft/legacy/LegacyInstance.cpp
+ minecraft/legacy/LegacyUpgradeTask.h
+ minecraft/legacy/LegacyUpgradeTask.cpp
+
+ minecraft/GradleSpecifier.h
+ minecraft/MinecraftInstance.cpp
+ minecraft/MinecraftInstance.h
+ minecraft/LaunchProfile.cpp
+ minecraft/LaunchProfile.h
+ minecraft/Component.cpp
+ minecraft/Component.h
+ minecraft/PackProfile.cpp
+ minecraft/PackProfile.h
+ minecraft/ComponentUpdateTask.cpp
+ minecraft/ComponentUpdateTask.h
+ minecraft/MinecraftLoadAndCheck.h
+ minecraft/MinecraftLoadAndCheck.cpp
+ minecraft/MinecraftUpdate.h
+ minecraft/MinecraftUpdate.cpp
+ minecraft/MojangVersionFormat.cpp
+ minecraft/MojangVersionFormat.h
+ minecraft/Rule.cpp
+ minecraft/Rule.h
+ minecraft/OneSixVersionFormat.cpp
+ minecraft/OneSixVersionFormat.h
+ minecraft/OpSys.cpp
+ minecraft/OpSys.h
+ minecraft/ParseUtils.cpp
+ minecraft/ParseUtils.h
+ minecraft/ProfileUtils.cpp
+ minecraft/ProfileUtils.h
+ minecraft/Library.cpp
+ minecraft/Library.h
+ minecraft/MojangDownloadInfo.h
+ minecraft/VersionFile.cpp
+ minecraft/VersionFile.h
+ minecraft/VersionFilterData.h
+ minecraft/VersionFilterData.cpp
+ minecraft/World.h
+ minecraft/World.cpp
+ minecraft/WorldList.h
+ minecraft/WorldList.cpp
+
+ minecraft/mod/Mod.h
+ minecraft/mod/Mod.cpp
+ minecraft/mod/ModDetails.h
+ minecraft/mod/ModFolderModel.h
+ minecraft/mod/ModFolderModel.cpp
+ minecraft/mod/ModFolderLoadTask.h
+ minecraft/mod/ModFolderLoadTask.cpp
+ minecraft/mod/LocalModParseTask.h
+ minecraft/mod/LocalModParseTask.cpp
+ minecraft/mod/ResourcePackFolderModel.h
+ minecraft/mod/ResourcePackFolderModel.cpp
+ minecraft/mod/TexturePackFolderModel.h
+ minecraft/mod/TexturePackFolderModel.cpp
+
+ # Assets
+ minecraft/AssetsUtils.h
+ minecraft/AssetsUtils.cpp
+
+ # Minecraft services
+ minecraft/services/SkinUpload.cpp
+ minecraft/services/SkinUpload.h
+ minecraft/services/SkinDelete.cpp
+ minecraft/services/SkinDelete.h
+
+ mojang/PackageManifest.h
+ mojang/PackageManifest.cpp
+ )
+
+add_unit_test(GradleSpecifier
+ SOURCES minecraft/GradleSpecifier_test.cpp
+ LIBS MultiMC_logic
+ )
+
+add_executable(PackageManifest
+ mojang/PackageManifest_test.cpp
+)
+target_link_libraries(PackageManifest
+ MultiMC_logic
+ Qt5::Test
+)
+target_include_directories(PackageManifest
+ PRIVATE ../cmake/UnitTest/
+)
+add_test(
+ NAME PackageManifest
+ COMMAND PackageManifest
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+add_unit_test(MojangVersionFormat
+ SOURCES minecraft/MojangVersionFormat_test.cpp
+ LIBS MultiMC_logic
+ DATA minecraft/testdata
+ )
+
+add_unit_test(Library
+ SOURCES minecraft/Library_test.cpp
+ LIBS MultiMC_logic
+ )
+
+# FIXME: shares data with FileSystem test
+add_unit_test(ModFolderModel
+ SOURCES minecraft/mod/ModFolderModel_test.cpp
+ DATA testdata
+ LIBS MultiMC_logic
+ )
+
+add_unit_test(ParseUtils
+ SOURCES minecraft/ParseUtils_test.cpp
+ LIBS MultiMC_logic
+ )
+
+# the screenshots feature
+set(SCREENSHOTS_SOURCES
+ screenshots/Screenshot.h
+ screenshots/ImgurUpload.h
+ screenshots/ImgurUpload.cpp
+ screenshots/ImgurAlbumCreation.h
+ screenshots/ImgurAlbumCreation.cpp
+)
+
+set(TASKS_SOURCES
+ # Tasks
+ tasks/Task.h
+ tasks/Task.cpp
+ tasks/SequentialTask.h
+ tasks/SequentialTask.cpp
+)
+
+set(SETTINGS_SOURCES
+ # 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
+)
+
+add_unit_test(INIFile
+ SOURCES settings/INIFile_test.cpp
+ LIBS MultiMC_logic
+ )
+
+set(JAVA_SOURCES
+ # Java related code
+ java/launch/CheckJava.cpp
+ java/launch/CheckJava.h
+ 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
+)
+
+add_unit_test(JavaVersion
+ SOURCES java/JavaVersion_test.cpp
+ LIBS MultiMC_logic
+ )
+
+set(TRANSLATIONS_SOURCES
+ translations/TranslationsModel.h
+ translations/TranslationsModel.cpp
+ translations/POTranslator.h
+ translations/POTranslator.cpp
+)
+
+set(TOOLS_SOURCES
+ # 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
+)
+
+set(META_SOURCES
+ # Metadata sources
+ meta/JsonFormat.cpp
+ meta/JsonFormat.h
+ meta/BaseEntity.cpp
+ meta/BaseEntity.h
+ meta/VersionList.cpp
+ meta/VersionList.h
+ meta/Version.cpp
+ meta/Version.h
+ meta/Index.cpp
+ meta/Index.h
+)
+
+set(FTB_SOURCES
+ modplatform/legacy_ftb/PackFetchTask.h
+ modplatform/legacy_ftb/PackFetchTask.cpp
+ modplatform/legacy_ftb/PackInstallTask.h
+ modplatform/legacy_ftb/PackInstallTask.cpp
+ modplatform/legacy_ftb/PrivatePackManager.h
+ modplatform/legacy_ftb/PrivatePackManager.cpp
+
+ modplatform/legacy_ftb/PackHelpers.h
+)
+
+set(FLAME_SOURCES
+ # Flame
+ modplatform/flame/FlamePackIndex.cpp
+ modplatform/flame/FlamePackIndex.h
+ modplatform/flame/PackManifest.h
+ modplatform/flame/PackManifest.cpp
+ modplatform/flame/FileResolvingTask.h
+ modplatform/flame/FileResolvingTask.cpp
+)
+
+set(MODPACKSCH_SOURCES
+ modplatform/modpacksch/FTBPackInstallTask.h
+ modplatform/modpacksch/FTBPackInstallTask.cpp
+ modplatform/modpacksch/FTBPackManifest.h
+ modplatform/modpacksch/FTBPackManifest.cpp
+)
+
+set(TECHNIC_SOURCES
+ modplatform/technic/SingleZipPackInstallTask.h
+ modplatform/technic/SingleZipPackInstallTask.cpp
+ modplatform/technic/SolderPackInstallTask.h
+ modplatform/technic/SolderPackInstallTask.cpp
+ modplatform/technic/TechnicPackProcessor.h
+ modplatform/technic/TechnicPackProcessor.cpp
+)
+
+set(ATLAUNCHER_SOURCES
+ modplatform/atlauncher/ATLPackIndex.cpp
+ modplatform/atlauncher/ATLPackIndex.h
+ modplatform/atlauncher/ATLPackInstallTask.cpp
+ modplatform/atlauncher/ATLPackInstallTask.h
+ modplatform/atlauncher/ATLPackManifest.cpp
+ modplatform/atlauncher/ATLPackManifest.h
+)
+
+add_unit_test(Index
+ SOURCES meta/Index_test.cpp
+ LIBS MultiMC_logic
+ )
+
+################################ COMPILE ################################
+
+# we need zlib
+find_package(ZLIB REQUIRED)
+
+set(LOGIC_SOURCES
+ ${CORE_SOURCES}
+ ${PATHMATCHER_SOURCES}
+ ${NET_SOURCES}
+ ${LAUNCH_SOURCES}
+ ${UPDATE_SOURCES}
+ ${NOTIFICATIONS_SOURCES}
+ ${NEWS_SOURCES}
+ ${STATUS_SOURCES}
+ ${MINECRAFT_SOURCES}
+ ${SCREENSHOTS_SOURCES}
+ ${TASKS_SOURCES}
+ ${SETTINGS_SOURCES}
+ ${JAVA_SOURCES}
+ ${TRANSLATIONS_SOURCES}
+ ${TOOLS_SOURCES}
+ ${META_SOURCES}
+ ${ICONS_SOURCES}
+ ${FTB_SOURCES}
+ ${FLAME_SOURCES}
+ ${MODPACKSCH_SOURCES}
+ ${TECHNIC_SOURCES}
+ ${ATLAUNCHER_SOURCES}
+)
+
+SET(MULTIMC_SOURCES
+ # Application base
+ MultiMC.h
+ MultiMC.cpp
+ UpdateController.cpp
+ UpdateController.h
+
+ # GUI - general utilities
+ DesktopServices.h
+ DesktopServices.cpp
+ GuiUtil.h
+ GuiUtil.cpp
+ ColumnResizer.h
+ ColumnResizer.cpp
+ InstanceProxyModel.h
+ InstanceProxyModel.cpp
+ VersionProxyModel.h
+ VersionProxyModel.cpp
+ ColorCache.h
+ ColorCache.cpp
+ HoeDown.h
+
+ # Super secret!
+ KonamiCode.h
+ KonamiCode.cpp
+
+ # Icons
+ icons/MMCIcon.h
+ icons/MMCIcon.cpp
+ icons/IconList.h
+ icons/IconList.cpp
+
+ # GUI - windows
+ MainWindow.h
+ MainWindow.cpp
+ InstanceWindow.h
+ InstanceWindow.cpp
+
+ # FIXME: maybe find a better home for this.
+ SkinUtils.cpp
+ SkinUtils.h
+
+ # GUI - setup wizard
+ setupwizard/SetupWizard.h
+ setupwizard/SetupWizard.cpp
+ setupwizard/AnalyticsWizardPage.cpp
+ setupwizard/AnalyticsWizardPage.h
+ setupwizard/BaseWizardPage.h
+ setupwizard/JavaWizardPage.cpp
+ setupwizard/JavaWizardPage.h
+ setupwizard/LanguageWizardPage.cpp
+ setupwizard/LanguageWizardPage.h
+
+ # GUI - themes
+ themes/FusionTheme.cpp
+ themes/FusionTheme.h
+ themes/BrightTheme.cpp
+ themes/BrightTheme.h
+ themes/CustomTheme.cpp
+ themes/CustomTheme.h
+ themes/DarkTheme.cpp
+ themes/DarkTheme.h
+ themes/ITheme.cpp
+ themes/ITheme.h
+ themes/SystemTheme.cpp
+ themes/SystemTheme.h
+
+ # Processes
+ LaunchController.h
+ LaunchController.cpp
+
+ # page provider for instances
+ InstancePageProvider.h
+
+ # Common java checking UI
+ JavaCommon.h
+ JavaCommon.cpp
+
+ # GUI - paged dialog base
+ pages/BasePage.h
+ pages/BasePageContainer.h
+ pages/BasePageProvider.h
+
+ # GUI - instance pages
+ pages/instance/GameOptionsPage.cpp
+ pages/instance/GameOptionsPage.h
+ pages/instance/VersionPage.cpp
+ pages/instance/VersionPage.h
+ pages/instance/TexturePackPage.h
+ pages/instance/ResourcePackPage.h
+ pages/instance/ModFolderPage.cpp
+ pages/instance/ModFolderPage.h
+ pages/instance/NotesPage.cpp
+ pages/instance/NotesPage.h
+ pages/instance/LogPage.cpp
+ pages/instance/LogPage.h
+ pages/instance/InstanceSettingsPage.cpp
+ pages/instance/InstanceSettingsPage.h
+ pages/instance/ScreenshotsPage.cpp
+ pages/instance/ScreenshotsPage.h
+ pages/instance/OtherLogsPage.cpp
+ pages/instance/OtherLogsPage.h
+ pages/instance/ServersPage.cpp
+ pages/instance/ServersPage.h
+ pages/instance/LegacyUpgradePage.cpp
+ pages/instance/LegacyUpgradePage.h
+ pages/instance/WorldListPage.cpp
+ pages/instance/WorldListPage.h
+
+ # GUI - global settings pages
+ pages/global/AccountListPage.cpp
+ pages/global/AccountListPage.h
+ pages/global/CustomCommandsPage.cpp
+ pages/global/CustomCommandsPage.h
+ pages/global/ExternalToolsPage.cpp
+ pages/global/ExternalToolsPage.h
+ pages/global/JavaPage.cpp
+ pages/global/JavaPage.h
+ pages/global/LanguagePage.cpp
+ pages/global/LanguagePage.h
+ pages/global/MinecraftPage.cpp
+ pages/global/MinecraftPage.h
+ pages/global/MultiMCPage.cpp
+ pages/global/MultiMCPage.h
+ pages/global/ProxyPage.cpp
+ pages/global/ProxyPage.h
+ pages/global/PasteEEPage.cpp
+ pages/global/PasteEEPage.h
+
+ # GUI - platform pages
+ pages/modplatform/VanillaPage.cpp
+ pages/modplatform/VanillaPage.h
+
+ pages/modplatform/atlauncher/AtlFilterModel.cpp
+ pages/modplatform/atlauncher/AtlFilterModel.h
+ pages/modplatform/atlauncher/AtlListModel.cpp
+ pages/modplatform/atlauncher/AtlListModel.h
+ pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
+ pages/modplatform/atlauncher/AtlOptionalModDialog.h
+ pages/modplatform/atlauncher/AtlPage.cpp
+ pages/modplatform/atlauncher/AtlPage.h
+
+ pages/modplatform/ftb/FtbFilterModel.cpp
+ pages/modplatform/ftb/FtbFilterModel.h
+ pages/modplatform/ftb/FtbListModel.cpp
+ pages/modplatform/ftb/FtbListModel.h
+ pages/modplatform/ftb/FtbPage.cpp
+ pages/modplatform/ftb/FtbPage.h
+
+ pages/modplatform/legacy_ftb/Page.cpp
+ pages/modplatform/legacy_ftb/Page.h
+ pages/modplatform/legacy_ftb/ListModel.h
+ pages/modplatform/legacy_ftb/ListModel.cpp
+
+ pages/modplatform/flame/FlameModel.cpp
+ pages/modplatform/flame/FlameModel.h
+ pages/modplatform/flame/FlamePage.cpp
+ pages/modplatform/flame/FlamePage.h
+
+ pages/modplatform/technic/TechnicModel.cpp
+ pages/modplatform/technic/TechnicModel.h
+ pages/modplatform/technic/TechnicPage.cpp
+ pages/modplatform/technic/TechnicPage.h
+
+ pages/modplatform/ImportPage.cpp
+ pages/modplatform/ImportPage.h
+
+ # GUI - dialogs
+ dialogs/AboutDialog.cpp
+ dialogs/AboutDialog.h
+ dialogs/ProfileSelectDialog.cpp
+ dialogs/ProfileSelectDialog.h
+ dialogs/CopyInstanceDialog.cpp
+ dialogs/CopyInstanceDialog.h
+ dialogs/CustomMessageBox.cpp
+ dialogs/CustomMessageBox.h
+ dialogs/EditAccountDialog.cpp
+ dialogs/EditAccountDialog.h
+ dialogs/ExportInstanceDialog.cpp
+ dialogs/ExportInstanceDialog.h
+ dialogs/IconPickerDialog.cpp
+ dialogs/IconPickerDialog.h
+ dialogs/LoginDialog.cpp
+ dialogs/LoginDialog.h
+ dialogs/NewComponentDialog.cpp
+ dialogs/NewComponentDialog.h
+ dialogs/NewInstanceDialog.cpp
+ dialogs/NewInstanceDialog.h
+ dialogs/NotificationDialog.cpp
+ dialogs/NotificationDialog.h
+ pagedialog/PageDialog.cpp
+ pagedialog/PageDialog.h
+ dialogs/ProgressDialog.cpp
+ dialogs/ProgressDialog.h
+ dialogs/UpdateDialog.cpp
+ dialogs/UpdateDialog.h
+ dialogs/VersionSelectDialog.cpp
+ dialogs/VersionSelectDialog.h
+ dialogs/SkinUploadDialog.cpp
+ dialogs/SkinUploadDialog.h
+
+
+ # GUI - widgets
+ widgets/Common.cpp
+ widgets/Common.h
+ widgets/CustomCommands.cpp
+ widgets/CustomCommands.h
+ widgets/DropLabel.cpp
+ widgets/DropLabel.h
+ widgets/FocusLineEdit.cpp
+ widgets/FocusLineEdit.h
+ widgets/IconLabel.cpp
+ widgets/IconLabel.h
+ widgets/JavaSettingsWidget.cpp
+ widgets/JavaSettingsWidget.h
+ widgets/LabeledToolButton.cpp
+ widgets/LabeledToolButton.h
+ widgets/LanguageSelectionWidget.cpp
+ widgets/LanguageSelectionWidget.h
+ widgets/LineSeparator.cpp
+ widgets/LineSeparator.h
+ widgets/LogView.cpp
+ widgets/LogView.h
+ widgets/MCModInfoFrame.cpp
+ widgets/MCModInfoFrame.h
+ widgets/ModListView.cpp
+ widgets/ModListView.h
+ widgets/PageContainer.cpp
+ widgets/PageContainer.h
+ widgets/PageContainer_p.h
+ widgets/ServerStatus.cpp
+ widgets/ServerStatus.h
+ widgets/VersionListView.cpp
+ widgets/VersionListView.h
+ widgets/VersionSelectWidget.cpp
+ widgets/VersionSelectWidget.h
+ widgets/ProgressWidget.h
+ widgets/ProgressWidget.cpp
+ widgets/WideBar.h
+ widgets/WideBar.cpp
+
+ # GUI - instance group view
+ groupview/GroupedProxyModel.cpp
+ groupview/GroupedProxyModel.h
+ groupview/AccessibleGroupView.cpp
+ groupview/AccessibleGroupView.h
+ groupview/AccessibleGroupView_p.h
+ groupview/GroupView.cpp
+ groupview/GroupView.h
+ groupview/InstanceDelegate.cpp
+ groupview/InstanceDelegate.h
+ groupview/VisualGroup.cpp
+ groupview/VisualGroup.h
+ )
+
+######## UIs ########
+SET(MULTIMC_UIS
+ # Instance pages
+ pages/instance/GameOptionsPage.ui
+ pages/instance/VersionPage.ui
+ pages/instance/ModFolderPage.ui
+ pages/instance/LogPage.ui
+ pages/instance/InstanceSettingsPage.ui
+ pages/instance/NotesPage.ui
+ pages/instance/ScreenshotsPage.ui
+ pages/instance/OtherLogsPage.ui
+ pages/instance/LegacyUpgradePage.ui
+ pages/instance/ServersPage.ui
+ pages/instance/WorldListPage.ui
+
+ # Global settings pages
+ pages/global/AccountListPage.ui
+ pages/global/ExternalToolsPage.ui
+ pages/global/JavaPage.ui
+ pages/global/MinecraftPage.ui
+ pages/global/MultiMCPage.ui
+ pages/global/ProxyPage.ui
+ pages/global/PasteEEPage.ui
+
+ # Platform pages
+ pages/modplatform/VanillaPage.ui
+ pages/modplatform/atlauncher/AtlPage.ui
+ pages/modplatform/ftb/FtbPage.ui
+ pages/modplatform/legacy_ftb/Page.ui
+ pages/modplatform/flame/FlamePage.ui
+ pages/modplatform/technic/TechnicPage.ui
+ pages/modplatform/ImportPage.ui
+
+ # Platform Dialogs
+ pages/modplatform/atlauncher/AtlOptionalModDialog.ui
+
+ # Dialogs
+ dialogs/CopyInstanceDialog.ui
+ dialogs/NewComponentDialog.ui
+ dialogs/NewInstanceDialog.ui
+ dialogs/AboutDialog.ui
+ dialogs/ProgressDialog.ui
+ dialogs/IconPickerDialog.ui
+ dialogs/ProfileSelectDialog.ui
+ dialogs/EditAccountDialog.ui
+ dialogs/ExportInstanceDialog.ui
+ dialogs/LoginDialog.ui
+ dialogs/UpdateDialog.ui
+ dialogs/NotificationDialog.ui
+ dialogs/SkinUploadDialog.ui
+
+ # Widgets/other
+ widgets/CustomCommands.ui
+ widgets/MCModInfoFrame.ui
+)
+
+set(MULTIMC_QRCS
+ resources/backgrounds/backgrounds.qrc
+ resources/multimc/multimc.qrc
+ resources/pe_dark/pe_dark.qrc
+ resources/pe_light/pe_light.qrc
+ resources/pe_colored/pe_colored.qrc
+ resources/pe_blue/pe_blue.qrc
+ resources/OSX/OSX.qrc
+ resources/iOS/iOS.qrc
+ resources/flat/flat.qrc
+ resources/documents/documents.qrc
+)
+
+######## Windows resource files ########
+if(WIN32)
+ set(MULTIMC_RCS resources/multimc.rc)
+endif()
+
+# Qt 5 stuff
+qt5_wrap_ui(MULTIMC_UI ${MULTIMC_UIS})
+qt5_add_resources(MULTIMC_RESOURCES ${MULTIMC_QRCS})
+
+# Add executable
+add_library(MultiMC_logic STATIC ${LOGIC_SOURCES} ${MULTIMC_SOURCES} ${MULTIMC_UI} ${MULTIMC_RESOURCES})
+target_link_libraries(MultiMC_logic
+ systeminfo
+ MultiMC_quazip
+ MultiMC_classparser
+ ${NBT_NAME}
+ ${ZLIB_LIBRARIES}
+ optional-bare
+ tomlc99
+ BuildConfig
+)
+target_link_libraries(MultiMC_logic
+ Qt5::Core
+ Qt5::Xml
+ Qt5::Network
+ Qt5::Concurrent
+ Qt5::Gui
+)
+target_link_libraries(MultiMC_logic
+ MultiMC_iconfix
+ ${QUAZIP_LIBRARIES}
+ hoedown
+ MultiMC_rainbow
+ LocalPeer
+ ganalytics
+)
+
+add_executable(MultiMC MACOSX_BUNDLE WIN32 main.cpp ${MULTIMC_RCS})
+target_link_libraries(MultiMC MultiMC_logic)
+
+if(DEFINED MultiMC_APP_BINARY_NAME)
+ set_target_properties(MultiMC PROPERTIES OUTPUT_NAME "${MultiMC_APP_BINARY_NAME}")
+endif()
+if(DEFINED MultiMC_BINARY_RPATH)
+ SET_TARGET_PROPERTIES(MultiMC PROPERTIES INSTALL_RPATH "${MultiMC_BINARY_RPATH}")
+endif()
+if(DEFINED MultiMC_APP_BINARY_DEFS)
+ target_compile_definitions(MultiMC PRIVATE ${MultiMC_APP_BINARY_DEFS})
+endif()
+
+install(TARGETS MultiMC
+ BUNDLE DESTINATION ${BUNDLE_DEST_DIR} COMPONENT Runtime
+ LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime
+ RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime
+)
+
+#### The MultiMC bundle mess! ####
+# Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system.
+# NOTE: it seems that this absolutely has to be here, and nowhere else.
+if(INSTALL_BUNDLE STREQUAL "full")
+ # Add qt.conf - this makes Qt stop looking for things outside the bundle
+ install(
+ CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")"
+ COMPONENT Runtime
+ )
+ # Bundle plugins
+ if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
+ # Image formats
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "tga|tiff|mng|webp" EXCLUDE
+ )
+ # Icon engines
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "fontawesome" EXCLUDE
+ )
+ # Platform plugins
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/platforms"
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "minimal|linuxfb|offscreen" EXCLUDE
+ )
+ else()
+ # Image formats
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/imageformats"
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "tga|tiff|mng|webp" EXCLUDE
+ REGEX "d\\." EXCLUDE
+ REGEX "_debug\\." EXCLUDE
+ REGEX "\\.dSYM" EXCLUDE
+ )
+ # Icon engines
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/iconengines"
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "fontawesome" EXCLUDE
+ REGEX "d\\." EXCLUDE
+ REGEX "_debug\\." EXCLUDE
+ REGEX "\\.dSYM" EXCLUDE
+ )
+ # Platform plugins
+ install(
+ DIRECTORY "${QT_PLUGINS_DIR}/platforms"
+ DESTINATION ${PLUGIN_DEST_DIR}
+ COMPONENT Runtime
+ REGEX "minimal|linuxfb|offscreen" EXCLUDE
+ REGEX "d\\." EXCLUDE
+ REGEX "_debug\\." EXCLUDE
+ REGEX "\\.dSYM" EXCLUDE
+ )
+ endif()
+ configure_file(
+ "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake"
+ @ONLY
+ )
+ install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" COMPONENT Runtime)
+endif()
diff --git a/launcher/ColorCache.cpp b/launcher/ColorCache.cpp
new file mode 100644
index 00000000..ef268dd2
--- /dev/null
+++ b/launcher/ColorCache.cpp
@@ -0,0 +1,35 @@
+#include "ColorCache.h"
+
+
+/**
+ * Blend the color with the front color, adapting to the back color
+ */
+QColor ColorCache::blend(QColor color)
+{
+ if (Rainbow::luma(m_front) > Rainbow::luma(m_back))
+ {
+ // for dark color schemes, produce a fitting color first
+ color = Rainbow::tint(m_front, color, 0.5);
+ }
+ // adapt contrast
+ return Rainbow::mix(m_front, color, m_bias);
+}
+
+/**
+ * Blend the color with the back color
+ */
+QColor ColorCache::blendBackground(QColor color)
+{
+ // adapt contrast
+ return Rainbow::mix(m_back, color, m_bias);
+}
+
+void ColorCache::recolorAll()
+{
+ auto iter = m_colors.begin();
+ while(iter != m_colors.end())
+ {
+ iter->front = blend(iter->original);
+ iter->back = blendBackground(iter->original);
+ }
+}
diff --git a/launcher/ColorCache.h b/launcher/ColorCache.h
new file mode 100644
index 00000000..6ae633b9
--- /dev/null
+++ b/launcher/ColorCache.h
@@ -0,0 +1,119 @@
+#pragma once
+#include <QtGui/QColor>
+#include <rainbow.h>
+#include <MessageLevel.h>
+#include <QMap>
+
+class ColorCache
+{
+public:
+ ColorCache(QColor front, QColor back, qreal bias)
+ {
+ m_front = front;
+ m_back = back;
+ m_bias = bias;
+ };
+
+ void addColor(int key, QColor color)
+ {
+ m_colors[key] = {color, blend(color), blendBackground(color)};
+ }
+
+ void setForeground(QColor front)
+ {
+ if(m_front != front)
+ {
+ m_front = front;
+ recolorAll();
+ }
+ }
+
+ void setBackground(QColor back)
+ {
+ if(m_back != back)
+ {
+ m_back = back;
+ recolorAll();
+ }
+ }
+
+ QColor getFront(int key)
+ {
+ auto iter = m_colors.find(key);
+ if(iter == m_colors.end())
+ {
+ return QColor();
+ }
+ return (*iter).front;
+ }
+
+ QColor getBack(int key)
+ {
+ auto iter = m_colors.find(key);
+ if(iter == m_colors.end())
+ {
+ return QColor();
+ }
+ return (*iter).back;
+ }
+
+ /**
+ * Blend the color with the front color, adapting to the back color
+ */
+ QColor blend(QColor color);
+
+ /**
+ * Blend the color with the back color
+ */
+ QColor blendBackground(QColor color);
+
+protected:
+ void recolorAll();
+
+protected:
+ struct ColorEntry
+ {
+ QColor original;
+ QColor front;
+ QColor back;
+ };
+
+protected:
+ qreal m_bias;
+ QColor m_front;
+ QColor m_back;
+ QMap<int, ColorEntry> m_colors;
+};
+
+class LogColorCache : public ColorCache
+{
+public:
+ LogColorCache(QColor front, QColor back)
+ : ColorCache(front, back, 1.0)
+ {
+ addColor((int)MessageLevel::MultiMC, QColor("purple"));
+ addColor((int)MessageLevel::Debug, QColor("green"));
+ addColor((int)MessageLevel::Warning, QColor("orange"));
+ addColor((int)MessageLevel::Error, QColor("red"));
+ addColor((int)MessageLevel::Fatal, QColor("red"));
+ addColor((int)MessageLevel::Message, front);
+ }
+
+ QColor getFront(MessageLevel::Enum level)
+ {
+ if(!m_colors.contains((int) level))
+ {
+ return ColorCache::getFront((int)MessageLevel::Message);
+ }
+ return ColorCache::getFront((int)level);
+ }
+
+ QColor getBack(MessageLevel::Enum level)
+ {
+ if(level == MessageLevel::Fatal)
+ {
+ return QColor(Qt::black);
+ }
+ return QColor(Qt::transparent);
+ }
+};
diff --git a/launcher/ColumnResizer.cpp b/launcher/ColumnResizer.cpp
new file mode 100644
index 00000000..fe415067
--- /dev/null
+++ b/launcher/ColumnResizer.cpp
@@ -0,0 +1,199 @@
+/*
+* Copyright 2011 Aurélien Gâteau <agateau@kde.org>
+* License: BSD-3-Clause
+*/
+#include <ColumnResizer.h>
+
+#include <QDebug>
+#include <QEvent>
+#include <QFormLayout>
+#include <QGridLayout>
+#include <QTimer>
+#include <QWidget>
+
+class FormLayoutWidgetItem : public QWidgetItem
+{
+public:
+ FormLayoutWidgetItem(QWidget* widget, QFormLayout* formLayout, QFormLayout::ItemRole itemRole)
+ : QWidgetItem(widget)
+ , m_width(-1)
+ , m_formLayout(formLayout)
+ , m_itemRole(itemRole)
+ {}
+
+ QSize sizeHint() const
+ {
+ QSize size = QWidgetItem::sizeHint();
+ if (m_width != -1) {
+ size.setWidth(m_width);
+ }
+ return size;
+ }
+
+ QSize minimumSize() const
+ {
+ QSize size = QWidgetItem::minimumSize();
+ if (m_width != -1) {
+ size.setWidth(m_width);
+ }
+ return size;
+ }
+
+ QSize maximumSize() const
+ {
+ QSize size = QWidgetItem::maximumSize();
+ if (m_width != -1) {
+ size.setWidth(m_width);
+ }
+ return size;
+ }
+
+ void setWidth(int width)
+ {
+ if (width != m_width) {
+ m_width = width;
+ invalidate();
+ }
+ }
+
+ void setGeometry(const QRect& _rect)
+ {
+ QRect rect = _rect;
+ int width = widget()->sizeHint().width();
+ if (m_itemRole == QFormLayout::LabelRole && m_formLayout->labelAlignment() & Qt::AlignRight) {
+ rect.setLeft(rect.right() - width);
+ }
+ QWidgetItem::setGeometry(rect);
+ }
+
+ QFormLayout* formLayout() const
+ {
+ return m_formLayout;
+ }
+
+private:
+ int m_width;
+ QFormLayout* m_formLayout;
+ QFormLayout::ItemRole m_itemRole;
+};
+
+typedef QPair<QGridLayout*, int> GridColumnInfo;
+
+class ColumnResizerPrivate
+{
+public:
+ ColumnResizerPrivate(ColumnResizer* q_ptr)
+ : q(q_ptr)
+ , m_updateTimer(new QTimer(q))
+ {
+ m_updateTimer->setSingleShot(true);
+ m_updateTimer->setInterval(0);
+ QObject::connect(m_updateTimer, SIGNAL(timeout()), q, SLOT(updateWidth()));
+ }
+
+ void scheduleWidthUpdate()
+ {
+ m_updateTimer->start();
+ }
+
+ ColumnResizer* q;
+ QTimer* m_updateTimer;
+ QList<QWidget*> m_widgets;
+ QList<FormLayoutWidgetItem*> m_wrWidgetItemList;
+ QList<GridColumnInfo> m_gridColumnInfoList;
+};
+
+ColumnResizer::ColumnResizer(QObject* parent)
+: QObject(parent)
+, d(new ColumnResizerPrivate(this))
+{}
+
+ColumnResizer::~ColumnResizer()
+{
+ delete d;
+}
+
+void ColumnResizer::addWidget(QWidget* widget)
+{
+ d->m_widgets.append(widget);
+ widget->installEventFilter(this);
+ d->scheduleWidthUpdate();
+}
+
+void ColumnResizer::updateWidth()
+{
+ int width = 0;
+ Q_FOREACH(QWidget* widget, d->m_widgets) {
+ width = qMax(widget->sizeHint().width(), width);
+ }
+ Q_FOREACH(FormLayoutWidgetItem* item, d->m_wrWidgetItemList) {
+ item->setWidth(width);
+ item->formLayout()->update();
+ }
+ Q_FOREACH(GridColumnInfo info, d->m_gridColumnInfoList) {
+ info.first->setColumnMinimumWidth(info.second, width);
+ }
+}
+
+bool ColumnResizer::eventFilter(QObject*, QEvent* event)
+{
+ if (event->type() == QEvent::Resize) {
+ d->scheduleWidthUpdate();
+ }
+ return false;
+}
+
+void ColumnResizer::addWidgetsFromLayout(QLayout* layout, int column)
+{
+ Q_ASSERT(column >= 0);
+ QGridLayout* gridLayout = qobject_cast<QGridLayout*>(layout);
+ QFormLayout* formLayout = qobject_cast<QFormLayout*>(layout);
+ if (gridLayout) {
+ addWidgetsFromGridLayout(gridLayout, column);
+ } else if (formLayout) {
+ if (column > QFormLayout::SpanningRole) {
+ qCritical() << "column should not be more than" << QFormLayout::SpanningRole << "for QFormLayout";
+ return;
+ }
+ QFormLayout::ItemRole role = static_cast<QFormLayout::ItemRole>(column);
+ addWidgetsFromFormLayout(formLayout, role);
+ } else {
+ qCritical() << "Don't know how to handle layout" << layout;
+ }
+}
+
+void ColumnResizer::addWidgetsFromGridLayout(QGridLayout* layout, int column)
+{
+ for (int row = 0; row < layout->rowCount(); ++row) {
+ QLayoutItem* item = layout->itemAtPosition(row, column);
+ if (!item) {
+ continue;
+ }
+ QWidget* widget = item->widget();
+ if (!widget) {
+ continue;
+ }
+ addWidget(widget);
+ }
+ d->m_gridColumnInfoList << GridColumnInfo(layout, column);
+}
+
+void ColumnResizer::addWidgetsFromFormLayout(QFormLayout* layout, QFormLayout::ItemRole role)
+{
+ for (int row = 0; row < layout->rowCount(); ++row) {
+ QLayoutItem* item = layout->itemAt(row, role);
+ if (!item) {
+ continue;
+ }
+ QWidget* widget = item->widget();
+ if (!widget) {
+ continue;
+ }
+ layout->removeItem(item);
+ delete item;
+ FormLayoutWidgetItem* newItem = new FormLayoutWidgetItem(widget, layout, role);
+ layout->setItem(row, role, newItem);
+ addWidget(widget);
+ d->m_wrWidgetItemList << newItem;
+ }
+}
diff --git a/launcher/ColumnResizer.h b/launcher/ColumnResizer.h
new file mode 100644
index 00000000..8c920f01
--- /dev/null
+++ b/launcher/ColumnResizer.h
@@ -0,0 +1,41 @@
+/*
+* Copyright 2011 Aurélien Gâteau <agateau@kde.org>
+* License: BSD-3-Clause
+*/
+#ifndef COLUMNRESIZER_H
+#define COLUMNRESIZER_H
+
+#include <QFormLayout>
+
+#include <QtCore/QObject>
+#include <QtCore/QList>
+
+class QEvent;
+class QGridLayout;
+class QLayout;
+class QWidget;
+
+class ColumnResizerPrivate;
+class ColumnResizer : public QObject
+{
+ Q_OBJECT
+public:
+ ColumnResizer(QObject* parent = 0);
+ ~ColumnResizer();
+
+ void addWidget(QWidget* widget);
+ void addWidgetsFromLayout(QLayout*, int column);
+ void addWidgetsFromGridLayout(QGridLayout*, int column);
+ void addWidgetsFromFormLayout(QFormLayout*, QFormLayout::ItemRole role);
+
+private Q_SLOTS:
+ void updateWidth();
+
+protected:
+ bool eventFilter(QObject*, QEvent* event);
+
+private:
+ ColumnResizerPrivate* const d;
+};
+
+#endif /* COLUMNRESIZER_H */
diff --git a/launcher/Commandline.cpp b/launcher/Commandline.cpp
new file mode 100644
index 00000000..2c0fde64
--- /dev/null
+++ b/launcher/Commandline.cpp
@@ -0,0 +1,483 @@
+/* Copyright 2013-2021 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 == '\\')
+ escape = true;
+ else if (cchar == inquotes)
+ inquotes = 0;
+ else
+ current += cchar;
+ // otherwise
+ }
+ else
+ {
+ if (cchar == ' ')
+ {
+ if (!current.isEmpty())
+ {
+ argv << current;
+ current.clear();
+ }
+ }
+ else if (cchar == '"' || cchar == '\'')
+ 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/launcher/Commandline.h b/launcher/Commandline.h
new file mode 100644
index 00000000..a4e7aa61
--- /dev/null
+++ b/launcher/Commandline.h
@@ -0,0 +1,250 @@
+/* Copyright 2013-2021 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>
+
+/**
+ * @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
+ */
+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 ParsingError : public std::runtime_error
+{
+public:
+ ParsingError(const QString &what);
+};
+
+/**
+ * @brief The Parser class
+ */
+class 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/launcher/DefaultVariable.h b/launcher/DefaultVariable.h
new file mode 100644
index 00000000..5c069bd3
--- /dev/null
+++ b/launcher/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/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp
new file mode 100644
index 00000000..5368ddc8
--- /dev/null
+++ b/launcher/DesktopServices.cpp
@@ -0,0 +1,149 @@
+#include "DesktopServices.h"
+#include <QDir>
+#include <QDesktopServices>
+#include <QProcess>
+#include <QDebug>
+
+/**
+ * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing.
+ */
+#if defined(Q_OS_LINUX)
+
+#include <unistd.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+template <typename T>
+bool IndirectOpen(T callable, qint64 *pid_forked = nullptr)
+{
+ auto pid = fork();
+ if(pid_forked)
+ {
+ if(pid > 0)
+ *pid_forked = pid;
+ else
+ *pid_forked = 0;
+ }
+ if(pid == -1)
+ {
+ qWarning() << "IndirectOpen failed to fork: " << errno;
+ return false;
+ }
+ // child - do the stuff
+ if(pid == 0)
+ {
+ // unset all this garbage so it doesn't get passed to the child process
+ qunsetenv("LD_PRELOAD");
+ qunsetenv("LD_LIBRARY_PATH");
+ qunsetenv("LD_DEBUG");
+ qunsetenv("QT_PLUGIN_PATH");
+ qunsetenv("QT_FONTPATH");
+
+ // open the URL
+ auto status = callable();
+
+ // detach from the parent process group.
+ setsid();
+
+ // die. now. do not clean up anything, it would just hang forever.
+ _exit(status ? 0 : 1);
+ }
+ else
+ {
+ //parent - assume it worked.
+ int status;
+ while (waitpid(pid, &status, 0))
+ {
+ if(WIFEXITED(status))
+ {
+ return WEXITSTATUS(status) == 0;
+ }
+ if(WIFSIGNALED(status))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+#endif
+
+namespace DesktopServices {
+bool openDirectory(const QString &path, bool ensureExists)
+{
+ qDebug() << "Opening directory" << path;
+ QDir parentPath;
+ QDir dir(path);
+ if (!dir.exists())
+ {
+ parentPath.mkpath(dir.absolutePath());
+ }
+ auto f = [&]()
+ {
+ return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath()));
+ };
+#if defined(Q_OS_LINUX)
+ return IndirectOpen(f);
+#else
+ return f();
+#endif
+}
+
+bool openFile(const QString &path)
+{
+ qDebug() << "Opening file" << path;
+ auto f = [&]()
+ {
+ return QDesktopServices::openUrl(QUrl::fromLocalFile(path));
+ };
+#if defined(Q_OS_LINUX)
+ return IndirectOpen(f);
+#else
+ return f();
+#endif
+}
+
+bool openFile(const QString &application, const QString &path, const QString &workingDirectory, qint64 *pid)
+{
+ qDebug() << "Opening file" << path << "using" << application;
+#if defined(Q_OS_LINUX)
+ // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
+ return IndirectOpen([&]()
+ {
+ return QProcess::startDetached(application, QStringList() << path, workingDirectory);
+ }, pid);
+#else
+ return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid);
+#endif
+}
+
+bool run(const QString &application, const QStringList &args, const QString &workingDirectory, qint64 *pid)
+{
+ qDebug() << "Running" << application << "with args" << args.join(' ');
+#if defined(Q_OS_LINUX)
+ // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave
+ return IndirectOpen([&]()
+ {
+ return QProcess::startDetached(application, args, workingDirectory);
+ }, pid);
+#else
+ return QProcess::startDetached(application, args, workingDirectory, pid);
+#endif
+}
+
+bool openUrl(const QUrl &url)
+{
+ qDebug() << "Opening URL" << url.toString();
+ auto f = [&]()
+ {
+ return QDesktopServices::openUrl(url);
+ };
+#if defined(Q_OS_LINUX)
+ return IndirectOpen(f);
+#else
+ return f();
+#endif
+}
+
+}
diff --git a/launcher/DesktopServices.h b/launcher/DesktopServices.h
new file mode 100644
index 00000000..1c081da4
--- /dev/null
+++ b/launcher/DesktopServices.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <QUrl>
+#include <QString>
+
+/**
+ * This wraps around QDesktopServices and adds workarounds where needed
+ * Use this instead of QDesktopServices!
+ */
+namespace DesktopServices
+{
+ /**
+ * Open a file in whatever application is applicable
+ */
+ bool openFile(const QString &path);
+
+ /**
+ * Open a file in the specified application
+ */
+ bool openFile(const QString &application, const QString &path, const QString & workingDirectory = QString(), qint64 *pid = 0);
+
+ /**
+ * Run an application
+ */
+ bool run(const QString &application,const QStringList &args, const QString & workingDirectory = QString(), qint64 *pid = 0);
+
+ /**
+ * Open a directory
+ */
+ bool openDirectory(const QString &path, bool ensureExists = false);
+
+ /**
+ * Open the URL, most likely in a browser. Maybe.
+ */
+ bool openUrl(const QUrl &url);
+}
diff --git a/launcher/Env.cpp b/launcher/Env.cpp
new file mode 100644
index 00000000..71b49d95
--- /dev/null
+++ b/launcher/Env.cpp
@@ -0,0 +1,211 @@
+#include "Env.h"
+#include "net/HttpMetaCache.h"
+#include "BaseVersion.h"
+#include "BaseVersionList.h"
+#include <QDir>
+#include <QCoreApplication>
+#include <QNetworkProxy>
+#include <QNetworkAccessManager>
+#include <QDebug>
+#include "tasks/Task.h"
+#include "meta/Index.h"
+#include "FileSystem.h"
+#include <QDebug>
+
+
+struct Env::Private
+{
+ QNetworkAccessManager m_qnam;
+ shared_qobject_ptr<HttpMetaCache> m_metacache;
+ std::shared_ptr<IIconList> m_iconlist;
+ shared_qobject_ptr<Meta::Index> m_metadataIndex;
+ QString m_jarsPath;
+ QSet<QString> m_features;
+};
+
+static Env * instance;
+
+/*
+ * The *NEW* global rat nest of an object. Handle with care.
+ */
+
+Env::Env()
+{
+ d = new Private();
+}
+
+Env::~Env()
+{
+ delete d;
+}
+
+Env& Env::Env::getInstance()
+{
+ if(!instance)
+ {
+ instance = new Env();
+ }
+ return *instance;
+}
+
+void Env::dispose()
+{
+ delete instance;
+ instance = nullptr;
+}
+
+shared_qobject_ptr< HttpMetaCache > Env::metacache()
+{
+ return d->m_metacache;
+}
+
+QNetworkAccessManager& Env::qnam() const
+{
+ return d->m_qnam;
+}
+
+std::shared_ptr<IIconList> Env::icons()
+{
+ return d->m_iconlist;
+}
+
+void Env::registerIconList(std::shared_ptr<IIconList> iconlist)
+{
+ d->m_iconlist = iconlist;
+}
+
+shared_qobject_ptr<Meta::Index> Env::metadataIndex()
+{
+ if (!d->m_metadataIndex)
+ {
+ d->m_metadataIndex.reset(new Meta::Index());
+ }
+ return d->m_metadataIndex;
+}
+
+
+void Env::initHttpMetaCache()
+{
+ auto &m_metacache = d->m_metacache;
+ 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("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath());
+ m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
+ m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
+ m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
+ m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").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("meta", QDir("meta").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();
+ d->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("%1:%2")
+ .arg(proxy.hostName())
+ .arg(proxy.port());
+ qDebug() << proxyDesc;
+}
+
+QString Env::getJarsPath()
+{
+ if(d->m_jarsPath.isEmpty())
+ {
+ return FS::PathCombine(QCoreApplication::applicationDirPath(), "jars");
+ }
+ return d->m_jarsPath;
+}
+
+void Env::setJarsPath(const QString& path)
+{
+ d->m_jarsPath = path;
+}
+
+void Env::enableFeature(const QString& featureName, bool state)
+{
+ if(state)
+ {
+ d->m_features.insert(featureName);
+ }
+ else
+ {
+ d->m_features.remove(featureName);
+ }
+}
+
+bool Env::isFeatureEnabled(const QString& featureName) const
+{
+ return d->m_features.contains(featureName);
+}
+
+void Env::getEnabledFeatures(QSet<QString>& features) const
+{
+ features = d->m_features;
+}
+
+void Env::setEnabledFeatures(const QSet<QString>& features) const
+{
+ d->m_features = features;
+}
diff --git a/launcher/Env.h b/launcher/Env.h
new file mode 100644
index 00000000..7d1a8bc9
--- /dev/null
+++ b/launcher/Env.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <memory>
+#include "icons/IIconList.h"
+#include <QString>
+#include <QMap>
+
+#include "QObjectPtr.h"
+
+class QNetworkAccessManager;
+class HttpMetaCache;
+class BaseVersionList;
+class BaseVersion;
+
+namespace Meta
+{
+class Index;
+}
+
+#if defined(ENV)
+ #undef ENV
+#endif
+#define ENV (Env::getInstance())
+
+
+class Env
+{
+ friend class MultiMC;
+private:
+ struct Private;
+ Env();
+ ~Env();
+ static void dispose();
+public:
+ static Env& getInstance();
+
+ QNetworkAccessManager &qnam() const;
+
+ shared_qobject_ptr<HttpMetaCache> metacache();
+
+ std::shared_ptr<IIconList> icons();
+
+ /// 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);
+
+ void registerIconList(std::shared_ptr<IIconList> iconlist);
+
+ shared_qobject_ptr<Meta::Index> metadataIndex();
+
+ QString getJarsPath();
+ void setJarsPath(const QString & path);
+
+ bool isFeatureEnabled(const QString & featureName) const;
+ void enableFeature(const QString & featureName, bool state = true);
+ void getEnabledFeatures(QSet<QString> & features) const;
+ void setEnabledFeatures(const QSet<QString> & features) const;
+
+protected:
+ Private * d;
+};
diff --git a/launcher/Exception.h b/launcher/Exception.h
new file mode 100644
index 00000000..fe0b86b5
--- /dev/null
+++ b/launcher/Exception.h
@@ -0,0 +1,32 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QString>
+#include <QDebug>
+#include <exception>
+
+class 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/launcher/ExponentialSeries.h b/launcher/ExponentialSeries.h
new file mode 100644
index 00000000..a9487f0a
--- /dev/null
+++ b/launcher/ExponentialSeries.h
@@ -0,0 +1,43 @@
+
+#pragma once
+
+template <typename T>
+inline void clamp(T& current, T min, T max)
+{
+ if (current < min)
+ {
+ current = min;
+ }
+ else if(current > max)
+ {
+ current = max;
+ }
+}
+
+// List of numbers from min to max. Next is exponent times bigger than previous.
+
+class ExponentialSeries
+{
+public:
+ ExponentialSeries(unsigned min, unsigned max, unsigned exponent = 2)
+ {
+ m_current = m_min = min;
+ m_max = max;
+ m_exponent = exponent;
+ }
+ void reset()
+ {
+ m_current = m_min;
+ }
+ unsigned operator()()
+ {
+ unsigned retval = m_current;
+ m_current *= m_exponent;
+ clamp(m_current, m_min, m_max);
+ return retval;
+ }
+ unsigned m_current;
+ unsigned m_min;
+ unsigned m_max;
+ unsigned m_exponent;
+};
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
new file mode 100644
index 00000000..13f05b86
--- /dev/null
+++ b/launcher/FileSystem.cpp
@@ -0,0 +1,457 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "FileSystem.h"
+
+#include <QDir>
+#include <QFile>
+#include <QSaveFile>
+#include <QFileInfo>
+#include <QDebug>
+#include <QUrl>
+#include <QStandardPaths>
+#include <QTextStream>
+
+#if defined Q_OS_WIN32
+ #include <windows.h>
+ #include <string>
+ #include <sys/utime.h>
+ #include <winnls.h>
+ #include <shobjidl.h>
+ #include <objbase.h>
+ #include <objidl.h>
+ #include <shlguid.h>
+ #include <shlobj.h>
+#else
+ #include <utime.h>
+#endif
+
+namespace FS {
+
+void ensureExists(const QDir &dir)
+{
+ if (!QDir().mkpath(dir.absolutePath()))
+ {
+ throw FileSystemException("Unable to create folder " + 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 updateTimestamp(const QString& filename)
+{
+#ifdef Q_OS_WIN32
+ std::wstring filename_utf_16 = filename.toStdWString();
+ return (_wutime64(filename_utf_16.c_str(), nullptr) == 0);
+#else
+ QByteArray filenameBA = QFile::encodeName(filename);
+ return (utime(filenameBA.data(), nullptr) == 0);
+#endif
+}
+
+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))
+ {
+ qWarning() << "Failed to copy" << inner_offset;
+ return false;
+ }
+ }
+ }
+ else
+ {
+ qCritical() << "Copy ERROR: Unknown filesystem object:" << src;
+ return false;
+ }
+ return true;
+}
+
+bool deletePath(QString path)
+{
+ bool OK = true;
+ QFileInfo finfo(path);
+ if(finfo.isFile()) {
+ return QFile::remove(path);
+ }
+
+ 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(const QString & path1, const QString & path2)
+{
+ if(!path1.size())
+ return path2;
+ if(!path2.size())
+ return path1;
+ return QDir::cleanPath(path1 + QDir::separator() + path2);
+}
+
+QString PathCombine(const QString & path1, const QString & path2, const QString & path3)
+{
+ return PathCombine(PathCombine(path1, path2), path3);
+}
+
+QString PathCombine(const QString & path1, const QString & path2, const QString & path3, const QString & path4)
+{
+ return PathCombine(PathCombine(path1, path2, path3), path4);
+}
+
+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 folder 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 = "\"\\/?<>:;*|!+\r\n";
+
+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 folder 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);
+}
+
+// Win32 crap
+#if defined Q_OS_WIN
+
+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%08lX", 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/launcher/FileSystem.h b/launcher/FileSystem.h
new file mode 100644
index 00000000..8f6e8b48
--- /dev/null
+++ b/launcher/FileSystem.h
@@ -0,0 +1,127 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include "Exception.h"
+#include "pathmatcher/IPathMatcher.h"
+
+#include <QDir>
+#include <QFlags>
+
+namespace FS
+{
+
+class FileSystemException : public ::Exception
+{
+public:
+ FileSystemException(const QString &message) : Exception(message) {}
+};
+
+/**
+ * write data to a file safely
+ */
+void write(const QString &filename, const QByteArray &data);
+
+/**
+ * read data from a file safely\
+ */
+QByteArray read(const QString &filename);
+
+/**
+ * Update the last changed timestamp of an existing file
+ */
+bool updateTimestamp(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!
+ */
+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!
+ */
+bool ensureFolderPathExists(QString filenamepath);
+
+class copy
+{
+public:
+ 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
+ */
+bool deletePath(QString path);
+
+QString PathCombine(const QString &path1, const QString &path2);
+QString PathCombine(const QString &path1, const QString &path2, const QString &path3);
+QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4);
+
+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
+ */
+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)
+ */
+QString NormalizePath(QString path);
+
+QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-');
+
+QString DirNameFromString(QString string, QString inDir = ".");
+
+/// Checks if the a given Path contains "!"
+bool checkProblemticPathJava(QDir folder);
+
+// Get the Directory representing the User's Desktop
+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
+bool createShortCut(QString location, QString dest, QStringList args, QString name, QString iconLocation);
+}
diff --git a/launcher/FileSystem_test.cpp b/launcher/FileSystem_test.cpp
new file mode 100644
index 00000000..df653ea1
--- /dev/null
+++ b/launcher/FileSystem_test.cpp
@@ -0,0 +1,164 @@
+#include <QTest>
+#include <QTemporaryDir>
+#include <QStandardPaths>
+#include "TestUtil.h"
+
+#include "FileSystem.h"
+
+class FileSystemTest : public QObject
+{
+ Q_OBJECT
+
+ const QString bothSlash = "/foo/";
+ const QString trailingSlash = "foo/";
+ const QString leadingSlash = "/foo";
+
+private
+slots:
+ void test_pathCombine()
+ {
+ QCOMPARE(QString("/foo/foo"), FS::PathCombine(bothSlash, bothSlash));
+ QCOMPARE(QString("foo/foo"), FS::PathCombine(trailingSlash, trailingSlash));
+ QCOMPARE(QString("/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash));
+
+ QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(bothSlash, bothSlash, bothSlash));
+ QCOMPARE(QString("foo/foo/foo"), FS::PathCombine(trailingSlash, trailingSlash, trailingSlash));
+ QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash, leadingSlash));
+ }
+
+ void test_PathCombine1_data()
+ {
+ QTest::addColumn<QString>("result");
+ QTest::addColumn<QString>("path1");
+ QTest::addColumn<QString>("path2");
+
+ QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl";
+ QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl";
+#if defined(Q_OS_WIN)
+ QTest::newRow("win native, from C:") << "C:/abc" << "C:" << "abc";
+ QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def" << "ghi\\jkl";
+ QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def\\" << "ghi\\jkl";
+#endif
+ }
+
+ void test_PathCombine1()
+ {
+ QFETCH(QString, result);
+ QFETCH(QString, path1);
+ QFETCH(QString, path2);
+
+ QCOMPARE(FS::PathCombine(path1, path2), result);
+ }
+
+ void test_PathCombine2_data()
+ {
+ QTest::addColumn<QString>("result");
+ QTest::addColumn<QString>("path1");
+ QTest::addColumn<QString>("path2");
+ QTest::addColumn<QString>("path3");
+
+ QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl";
+ QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl";
+ QTest::newRow("qt 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl";
+ QTest::newRow("qt 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl";
+#if defined(Q_OS_WIN)
+ QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def" << "ghi\\jkl";
+ QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl";
+ QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def\\" << "ghi\\jkl";
+ QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl";
+#endif
+ }
+
+ void test_PathCombine2()
+ {
+ QFETCH(QString, result);
+ QFETCH(QString, path1);
+ QFETCH(QString, path2);
+ QFETCH(QString, path3);
+
+ QCOMPARE(FS::PathCombine(path1, path2, path3), result);
+ }
+
+ void test_copy()
+ {
+ QString folder = QFINDTESTDATA("data/test_folder");
+ auto f = [&folder]()
+ {
+ QTemporaryDir tempDir;
+ tempDir.setAutoRemove(true);
+ qDebug() << "From:" << folder << "To:" << tempDir.path();
+
+ QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder"));
+ qDebug() << tempDir.path();
+ qDebug() << target_dir.path();
+ FS::copy c(folder, target_dir.path());
+ c();
+
+ for(auto entry: target_dir.entryList())
+ {
+ qDebug() << entry;
+ }
+ QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
+ QVERIFY(target_dir.entryList().contains("assets"));
+ };
+
+ // first try variant without trailing /
+ QVERIFY(!folder.endsWith('/'));
+ f();
+
+ // then variant with trailing /
+ folder.append('/');
+ QVERIFY(folder.endsWith('/'));
+ f();
+ }
+
+ void test_getDesktop()
+ {
+ QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation));
+ }
+
+// this is only valid on linux
+// FIXME: implement on windows, OSX, then test.
+#if defined(Q_OS_LINUX)
+ void test_createShortcut_data()
+ {
+ QTest::addColumn<QString>("location");
+ QTest::addColumn<QString>("dest");
+ QTest::addColumn<QStringList>("args");
+ QTest::addColumn<QString>("name");
+ QTest::addColumn<QString>("iconLocation");
+ QTest::addColumn<QByteArray>("result");
+
+ QTest::newRow("unix") << QDir::currentPath()
+ << "asdfDest"
+ << (QStringList() << "arg1" << "arg2")
+ << "asdf"
+ << QString()
+ #if defined(Q_OS_LINUX)
+ << MULTIMC_GET_TEST_FILE("data/FileSystem-test_createShortcut-unix")
+ #elif defined(Q_OS_WIN)
+ << QByteArray()
+ #endif
+ ;
+ }
+
+ void test_createShortcut()
+ {
+ QFETCH(QString, location);
+ QFETCH(QString, dest);
+ QFETCH(QStringList, args);
+ QFETCH(QString, name);
+ QFETCH(QString, iconLocation);
+ QFETCH(QByteArray, result);
+
+ QVERIFY(FS::createShortCut(location, dest, args, name, iconLocation));
+ QCOMPARE(QString::fromLocal8Bit(TestsInternal::readFile(location + QDir::separator() + name + ".desktop")), QString::fromLocal8Bit(result));
+
+ //QDir().remove(location);
+ }
+#endif
+};
+
+QTEST_GUILESS_MAIN(FileSystemTest)
+
+#include "FileSystem_test.moc"
diff --git a/launcher/Filter.cpp b/launcher/Filter.cpp
new file mode 100644
index 00000000..c65ca0ce
--- /dev/null
+++ b/launcher/Filter.cpp
@@ -0,0 +1,31 @@
+#include "Filter.h"
+
+Filter::~Filter(){}
+
+ContainsFilter::ContainsFilter(const QString& pattern) : pattern(pattern){}
+ContainsFilter::~ContainsFilter(){}
+bool ContainsFilter::accepts(const QString& value)
+{
+ return value.contains(pattern);
+}
+
+ExactFilter::ExactFilter(const QString& pattern) : pattern(pattern){}
+ExactFilter::~ExactFilter(){}
+bool ExactFilter::accepts(const QString& value)
+{
+ return value == pattern;
+}
+
+RegexpFilter::RegexpFilter(const QString& regexp, bool invert)
+ :invert(invert)
+{
+ pattern.setPattern(regexp);
+ pattern.optimize();
+}
+RegexpFilter::~RegexpFilter(){}
+bool RegexpFilter::accepts(const QString& value)
+{
+ auto match = pattern.match(value);
+ bool matched = match.hasMatch();
+ return invert ? (!matched) : (matched);
+}
diff --git a/launcher/Filter.h b/launcher/Filter.h
new file mode 100644
index 00000000..b55067ac
--- /dev/null
+++ b/launcher/Filter.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <QString>
+#include <QRegularExpression>
+
+class Filter
+{
+public:
+ virtual ~Filter();
+ virtual bool accepts(const QString & value) = 0;
+};
+
+class ContainsFilter: public Filter
+{
+public:
+ ContainsFilter(const QString &pattern);
+ virtual ~ContainsFilter();
+ bool accepts(const QString & value) override;
+private:
+ QString pattern;
+};
+
+class ExactFilter: public Filter
+{
+public:
+ ExactFilter(const QString &pattern);
+ virtual ~ExactFilter();
+ bool accepts(const QString & value) override;
+private:
+ QString pattern;
+};
+
+class RegexpFilter: public Filter
+{
+public:
+ RegexpFilter(const QString &regexp, bool invert);
+ virtual ~RegexpFilter();
+ bool accepts(const QString & value) override;
+private:
+ QRegularExpression pattern;
+ bool invert = false;
+};
diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp
new file mode 100644
index 00000000..0368c32d
--- /dev/null
+++ b/launcher/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/launcher/GZip.h b/launcher/GZip.h
new file mode 100644
index 00000000..7d4b1c33
--- /dev/null
+++ b/launcher/GZip.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <QByteArray>
+
+class GZip
+{
+public:
+ static bool unzip(const QByteArray &compressedBytes, QByteArray &uncompressedBytes);
+ static bool zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes);
+};
+
diff --git a/launcher/GZip_test.cpp b/launcher/GZip_test.cpp
new file mode 100644
index 00000000..3f4d181c
--- /dev/null
+++ b/launcher/GZip_test.cpp
@@ -0,0 +1,57 @@
+#include <QTest>
+#include "TestUtil.h"
+
+#include "GZip.h"
+#include <random>
+
+void fib(int &prev, int &cur)
+{
+ auto ret = prev + cur;
+ prev = cur;
+ cur = ret;
+}
+
+class GZipTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+
+ void test_Through()
+ {
+ // test up to 10 MB
+ static const int size = 10 * 1024 * 1024;
+ QByteArray random;
+ QByteArray compressed;
+ QByteArray decompressed;
+ std::default_random_engine eng((std::random_device())());
+ std::uniform_int_distribution<uint8_t> idis(0, std::numeric_limits<uint8_t>::max());
+
+ // initialize random buffer
+ for(int i = 0; i < size; i++)
+ {
+ random.append((char)idis(eng));
+ }
+
+ // initialize fibonacci
+ int prev = 1;
+ int cur = 1;
+
+ // test if fibonacci long random buffers pass through GZip
+ do
+ {
+ QByteArray copy = random;
+ copy.resize(cur);
+ compressed.clear();
+ decompressed.clear();
+ QVERIFY(GZip::zip(copy, compressed));
+ QVERIFY(GZip::unzip(compressed, decompressed));
+ QCOMPARE(decompressed, copy);
+ fib(prev, cur);
+ } while (cur < size);
+ }
+};
+
+QTEST_GUILESS_MAIN(GZipTest)
+
+#include "GZip_test.moc"
diff --git a/launcher/GuiUtil.cpp b/launcher/GuiUtil.cpp
new file mode 100644
index 00000000..302206f5
--- /dev/null
+++ b/launcher/GuiUtil.cpp
@@ -0,0 +1,131 @@
+#include "GuiUtil.h"
+
+#include <QClipboard>
+#include <QApplication>
+#include <QFileDialog>
+
+#include "dialogs/ProgressDialog.h"
+#include "net/PasteUpload.h"
+#include "dialogs/CustomMessageBox.h"
+
+#include "MultiMC.h"
+#include <settings/SettingsObject.h>
+#include <DesktopServices.h>
+#include <BuildConfig.h>
+
+QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget)
+{
+ ProgressDialog dialog(parentWidget);
+ auto APIKeySetting = MMC->settings()->get("PasteEEAPIKey").toString();
+ if(APIKeySetting == "multimc")
+ {
+ APIKeySetting = BuildConfig.PASTE_EE_KEY;
+ }
+ std::unique_ptr<PasteUpload> paste(new PasteUpload(parentWidget, text, APIKeySetting));
+
+ if (!paste->validateText())
+ {
+ CustomMessageBox::selectable(
+ parentWidget, QObject::tr("Upload failed"),
+ QObject::tr("The log file is too big. You'll have to upload it manually."),
+ QMessageBox::Warning)->exec();
+ return QString();
+ }
+
+ dialog.execWithTask(paste.get());
+ if (!paste->wasSuccessful())
+ {
+ CustomMessageBox::selectable(parentWidget, QObject::tr("Upload failed"),
+ paste->failReason(), QMessageBox::Critical)->exec();
+ return QString();
+ }
+ else
+ {
+ const QString link = paste->pasteLink();
+ setClipboardText(link);
+ CustomMessageBox::selectable(
+ parentWidget, QObject::tr("Upload finished"),
+ QObject::tr("The <a href=\"%1\">link to the uploaded log</a> has been placed in your clipboard.").arg(link),
+ QMessageBox::Information)->exec();
+ return link;
+ }
+}
+
+void GuiUtil::setClipboardText(const QString &text)
+{
+ QApplication::clipboard()->setText(text);
+}
+
+static QStringList BrowseForFileInternal(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget, bool single)
+{
+ static QMap<QString, QString> savedPaths;
+
+ QFileDialog w(parentWidget, caption);
+ QSet<QString> locations;
+ auto f = [&](QStandardPaths::StandardLocation l)
+ {
+ QString location = QStandardPaths::writableLocation(l);
+ QFileInfo finfo(location);
+ if (!finfo.exists())
+ return;
+ locations.insert(location);
+ };
+ f(QStandardPaths::DesktopLocation);
+ f(QStandardPaths::DocumentsLocation);
+ f(QStandardPaths::DownloadLocation);
+ f(QStandardPaths::HomeLocation);
+ QList<QUrl> urls;
+ for (auto location : locations)
+ {
+ urls.append(QUrl::fromLocalFile(location));
+ }
+ urls.append(QUrl::fromLocalFile(defaultPath));
+
+ w.setFileMode(single ? QFileDialog::ExistingFile : QFileDialog::ExistingFiles);
+ w.setAcceptMode(QFileDialog::AcceptOpen);
+ w.setNameFilter(filter);
+
+ QString pathToOpen;
+ if(savedPaths.contains(context))
+ {
+ pathToOpen = savedPaths[context];
+ }
+ else
+ {
+ pathToOpen = defaultPath;
+ }
+ if(!pathToOpen.isEmpty())
+ {
+ QFileInfo finfo(pathToOpen);
+ if(finfo.exists() && finfo.isDir())
+ {
+ w.setDirectory(finfo.absoluteFilePath());
+ }
+ }
+
+ w.setSidebarUrls(urls);
+
+ if (w.exec())
+ {
+ savedPaths[context] = w.directory().absolutePath();
+ return w.selectedFiles();
+ }
+ savedPaths[context] = w.directory().absolutePath();
+ return {};
+}
+
+QString GuiUtil::BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget)
+{
+ auto resultList = BrowseForFileInternal(context, caption, filter, defaultPath, parentWidget, true);
+ if(resultList.size())
+ {
+ return resultList[0];
+ }
+ return QString();
+}
+
+
+QStringList GuiUtil::BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget)
+{
+ return BrowseForFileInternal(context, caption, filter, defaultPath, parentWidget, false);
+}
diff --git a/launcher/GuiUtil.h b/launcher/GuiUtil.h
new file mode 100644
index 00000000..5e109383
--- /dev/null
+++ b/launcher/GuiUtil.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <QWidget>
+
+namespace GuiUtil
+{
+QString uploadPaste(const QString &text, QWidget *parentWidget);
+void setClipboardText(const QString &text);
+QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget);
+QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget);
+}
diff --git a/launcher/HoeDown.h b/launcher/HoeDown.h
new file mode 100644
index 00000000..b9e06ffb
--- /dev/null
+++ b/launcher/HoeDown.h
@@ -0,0 +1,76 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <hoedown/html.h>
+#include <hoedown/document.h>
+#include <QString>
+#include <QByteArray>
+
+/**
+ * hoedown wrapper, because dealing with resource lifetime in C is stupid
+ */
+class HoeDown
+{
+public:
+ class buffer
+ {
+ public:
+ buffer(size_t unit = 4096)
+ {
+ buf = hoedown_buffer_new(unit);
+ }
+ ~buffer()
+ {
+ hoedown_buffer_free(buf);
+ }
+ const char * cstr()
+ {
+ return hoedown_buffer_cstr(buf);
+ }
+ void put(QByteArray input)
+ {
+ hoedown_buffer_put(buf, (uint8_t *) input.data(), input.size());
+ }
+ const uint8_t * data() const
+ {
+ return buf->data;
+ }
+ size_t size() const
+ {
+ return buf->size;
+ }
+ hoedown_buffer * buf;
+ } ib, ob;
+ HoeDown()
+ {
+ renderer = hoedown_html_renderer_new((hoedown_html_flags) 0,0);
+ document = hoedown_document_new(renderer, (hoedown_extensions) 0, 8);
+ }
+ ~HoeDown()
+ {
+ hoedown_document_free(document);
+ hoedown_html_renderer_free(renderer);
+ }
+ QString process(QByteArray input)
+ {
+ ib.put(input);
+ hoedown_document_render(document, ob.buf, ib.data(), ib.size());
+ return ob.cstr();
+ }
+private:
+ hoedown_document * document;
+ hoedown_renderer * renderer;
+};
diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp
new file mode 100644
index 00000000..35adeaf9
--- /dev/null
+++ b/launcher/InstanceCopyTask.cpp
@@ -0,0 +1,60 @@
+#include "InstanceCopyTask.h"
+#include "settings/INISettingsObject.h"
+#include "FileSystem.h"
+#include "NullInstance.h"
+#include "pathmatcher/RegexpMatcher.h"
+#include <QtConcurrentRun>
+
+InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime)
+{
+ m_origInstance = origInstance;
+ m_keepPlaytime = keepPlaytime;
+
+ if(!copySaves)
+ {
+ // FIXME: get this from the original instance type...
+ auto matcherReal = new RegexpMatcher("[.]?minecraft/saves");
+ matcherReal->caseSensitive(false);
+ m_matcher.reset(matcherReal);
+ }
+}
+
+void InstanceCopyTask::executeTask()
+{
+ setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
+
+ FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
+ folderCopy.followSymlinks(false).blacklist(m_matcher.get());
+
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &InstanceCopyTask::copyFinished);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &InstanceCopyTask::copyAborted);
+ m_copyFutureWatcher.setFuture(m_copyFuture);
+}
+
+void InstanceCopyTask::copyFinished()
+{
+ auto successful = m_copyFuture.result();
+ if(!successful)
+ {
+ emitFailed(tr("Instance folder copy failed."));
+ return;
+ }
+ // FIXME: shouldn't this be able to report errors?
+ auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+
+ InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath));
+ inst->setName(m_instName);
+ inst->setIconKey(m_instIcon);
+ if(!m_keepPlaytime) {
+ inst->resetTimePlayed();
+ }
+ emitSucceeded();
+}
+
+void InstanceCopyTask::copyAborted()
+{
+ emitFailed(tr("Instance folder copy has been aborted."));
+ return;
+}
diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h
new file mode 100644
index 00000000..82901732
--- /dev/null
+++ b/launcher/InstanceCopyTask.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include <QUrl>
+#include <QFuture>
+#include <QFutureWatcher>
+#include "settings/SettingsObject.h"
+#include "BaseVersion.h"
+#include "BaseInstance.h"
+#include "InstanceTask.h"
+
+class InstanceCopyTask : public InstanceTask
+{
+ Q_OBJECT
+public:
+ explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime);
+
+protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+ void copyFinished();
+ void copyAborted();
+
+private: /* data */
+ InstancePtr m_origInstance;
+ QFuture<bool> m_copyFuture;
+ QFutureWatcher<bool> m_copyFutureWatcher;
+ std::unique_ptr<IPathMatcher> m_matcher;
+ bool m_keepPlaytime;
+};
diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp
new file mode 100644
index 00000000..eafc5126
--- /dev/null
+++ b/launcher/InstanceCreationTask.cpp
@@ -0,0 +1,31 @@
+#include "InstanceCreationTask.h"
+#include "settings/INISettingsObject.h"
+#include "FileSystem.h"
+
+//FIXME: remove this
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version)
+{
+ m_version = version;
+}
+
+void InstanceCreationTask::executeTask()
+{
+ setStatus(tr("Creating instance from version %1").arg(m_version->name()));
+ {
+ auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath);
+ auto components = inst.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", m_version->descriptor(), true);
+ inst.setName(m_instName);
+ inst.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+ }
+ emitSucceeded();
+}
diff --git a/launcher/InstanceCreationTask.h b/launcher/InstanceCreationTask.h
new file mode 100644
index 00000000..54997116
--- /dev/null
+++ b/launcher/InstanceCreationTask.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include <QUrl>
+#include "settings/SettingsObject.h"
+#include "BaseVersion.h"
+#include "InstanceTask.h"
+
+class InstanceCreationTask : public InstanceTask
+{
+ Q_OBJECT
+public:
+ explicit InstanceCreationTask(BaseVersionPtr version);
+
+protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+private: /* data */
+ BaseVersionPtr m_version;
+};
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
new file mode 100644
index 00000000..3eac4d57
--- /dev/null
+++ b/launcher/InstanceImportTask.cpp
@@ -0,0 +1,456 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "InstanceImportTask.h"
+#include "BaseInstance.h"
+#include "FileSystem.h"
+#include "Env.h"
+#include "MMCZip.h"
+#include "NullInstance.h"
+#include "settings/INISettingsObject.h"
+#include "icons/IIconList.h"
+#include "icons/IconUtils.h"
+#include <QtConcurrentRun>
+
+// FIXME: this does not belong here, it's Minecraft/Flame specific
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "modplatform/flame/FileResolvingTask.h"
+#include "modplatform/flame/PackManifest.h"
+#include "Json.h"
+#include <quazipdir.h>
+#include "modplatform/technic/TechnicPackProcessor.h"
+
+InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
+{
+ m_sourceUrl = sourceUrl;
+}
+
+void InstanceImportTask::executeTask()
+{
+ if (m_sourceUrl.isLocalFile())
+ {
+ m_archivePath = m_sourceUrl.toLocalFile();
+ processZipPack();
+ }
+ else
+ {
+ setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
+ m_downloadRequired = true;
+
+ const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
+ auto entry = ENV.metacache()->resolveEntry("general", path);
+ entry->setStale(true);
+ m_filesNetJob.reset(new NetJob(tr("Modpack download")));
+ m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
+ m_archivePath = entry->getFullPath();
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
+ connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
+ connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed);
+ m_filesNetJob->start();
+ }
+}
+
+void InstanceImportTask::downloadSucceeded()
+{
+ processZipPack();
+ m_filesNetJob.reset();
+}
+
+void InstanceImportTask::downloadFailed(QString reason)
+{
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total)
+{
+ setProgress(current / 2, total);
+}
+
+void InstanceImportTask::processZipPack()
+{
+ setStatus(tr("Extracting modpack"));
+ QDir extractDir(m_stagingPath);
+ qDebug() << "Attempting to create instance from" << m_archivePath;
+
+ // open the zip and find relevant files in it
+ m_packZip.reset(new QuaZip(m_archivePath));
+ if (!m_packZip->open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Unable to open supplied modpack zip file."));
+ return;
+ }
+
+ QStringList blacklist = {"instance.cfg", "manifest.json"};
+ QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
+ bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json");
+ QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
+ QString root;
+ if(!mmcFound.isNull())
+ {
+ // process as MultiMC instance/pack
+ qDebug() << "MultiMC:" << mmcFound;
+ root = mmcFound;
+ m_modpackType = ModpackType::MultiMC;
+ }
+ else if (technicFound)
+ {
+ // process as Technic pack
+ qDebug() << "Technic:" << technicFound;
+ extractDir.mkpath(".minecraft");
+ extractDir.cd(".minecraft");
+ m_modpackType = ModpackType::Technic;
+ }
+ else if(!flameFound.isNull())
+ {
+ // process as Flame pack
+ qDebug() << "Flame:" << flameFound;
+ root = flameFound;
+ m_modpackType = ModpackType::Flame;
+ }
+ if(m_modpackType == ModpackType::Unknown)
+ {
+ emitFailed(tr("Archive does not contain a recognized modpack type."));
+ return;
+ }
+
+ // make sure we extract just the pack
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath());
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &InstanceImportTask::extractFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &InstanceImportTask::extractAborted);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void InstanceImportTask::extractFinished()
+{
+ m_packZip.reset();
+ if (!m_extractFuture.result())
+ {
+ emitFailed(tr("Failed to extract modpack"));
+ return;
+ }
+ QDir extractDir(m_stagingPath);
+
+ qDebug() << "Fixing permissions for extracted pack files...";
+ QDirIterator it(extractDir, QDirIterator::Subdirectories);
+ while (it.hasNext())
+ {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ auto origPermissions = permissions;
+ if(file.isDir())
+ {
+ // Folder +rwx for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
+ }
+ else
+ {
+ // File +rw for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
+ }
+ if(origPermissions != permissions)
+ {
+ if(!QFile::setPermissions(filepath, permissions))
+ {
+ logWarning(tr("Could not fix permissions for %1").arg(filepath));
+ }
+ else
+ {
+ qDebug() << "Fixed" << filepath;
+ }
+ }
+ }
+
+ switch(m_modpackType)
+ {
+ case ModpackType::Flame:
+ processFlame();
+ return;
+ case ModpackType::MultiMC:
+ processMultiMC();
+ return;
+ case ModpackType::Technic:
+ processTechnic();
+ return;
+ case ModpackType::Unknown:
+ emitFailed(tr("Archive does not contain a recognized modpack type."));
+ return;
+ }
+}
+
+void InstanceImportTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+ return;
+}
+
+void InstanceImportTask::processFlame()
+{
+ const static QMap<QString,QString> forgemap = {
+ {"1.2.5", "3.4.9.171"},
+ {"1.4.2", "6.0.1.355"},
+ {"1.4.7", "6.6.2.534"},
+ {"1.5.2", "7.8.1.737"}
+ };
+ Flame::Manifest pack;
+ try
+ {
+ QString configPath = FS::PathCombine(m_stagingPath, "manifest.json");
+ Flame::loadManifest(pack, configPath);
+ QFile::remove(configPath);
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ return;
+ }
+ if(!pack.overrides.isEmpty())
+ {
+ QString overridePath = FS::PathCombine(m_stagingPath, pack.overrides);
+ if (QFile::exists(overridePath))
+ {
+ QString mcPath = FS::PathCombine(m_stagingPath, "minecraft");
+ if (!QFile::rename(overridePath, mcPath))
+ {
+ emitFailed(tr("Could not rename the overrides folder:\n") + pack.overrides);
+ return;
+ }
+ }
+ else
+ {
+ logWarning(tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?").arg(pack.overrides));
+ }
+ }
+
+ QString forgeVersion;
+ QString fabricVersion;
+ for(auto &loader: pack.minecraft.modLoaders)
+ {
+ auto id = loader.id;
+ if(id.startsWith("forge-"))
+ {
+ id.remove("forge-");
+ forgeVersion = id;
+ continue;
+ }
+ if(id.startsWith("fabric-"))
+ {
+ id.remove("fabric-");
+ fabricVersion = id;
+ continue;
+ }
+ logWarning(tr("Unknown mod loader in manifest: %1").arg(id));
+ }
+
+ QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+ auto mcVersion = pack.minecraft.version;
+ // Hack to correct some 'special sauce'...
+ if(mcVersion.endsWith('.'))
+ {
+ mcVersion.remove(QRegExp("[.]+$"));
+ logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack."));
+ }
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", mcVersion, true);
+ if(!forgeVersion.isEmpty())
+ {
+ // FIXME: dirty, nasty, hack. Proper solution requires dependency resolution and knowledge of the metadata.
+ if(forgeVersion == "recommended")
+ {
+ if(forgemap.contains(mcVersion))
+ {
+ forgeVersion = forgemap[mcVersion];
+ }
+ else
+ {
+ logWarning(tr("Could not map recommended forge version for Minecraft %1").arg(mcVersion));
+ }
+ }
+ components->setComponentVersion("net.minecraftforge", forgeVersion);
+ }
+ if(!fabricVersion.isEmpty())
+ {
+ components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion);
+ }
+ if (m_instIcon != "default")
+ {
+ instance.setIconKey(m_instIcon);
+ }
+ else
+ {
+ if(pack.name.contains("Direwolf20"))
+ {
+ instance.setIconKey("steve");
+ }
+ else if(pack.name.contains("FTB") || pack.name.contains("Feed The Beast"))
+ {
+ instance.setIconKey("ftb_logo");
+ }
+ else
+ {
+ // default to something other than the MultiMC default to distinguish these
+ instance.setIconKey("flame");
+ }
+ }
+ QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods");
+ QFileInfo jarmodsInfo(jarmodsPath);
+ if(jarmodsInfo.isDir())
+ {
+ // install all the jar mods
+ qDebug() << "Found jarmods:";
+ QDir jarmodsDir(jarmodsPath);
+ QStringList jarMods;
+ for (auto info: jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
+ {
+ qDebug() << info.fileName();
+ jarMods.push_back(info.absoluteFilePath());
+ }
+ auto profile = instance.getPackProfile();
+ profile->installJarMods(jarMods);
+ // nuke the original files
+ FS::deletePath(jarmodsPath);
+ }
+ instance.setName(m_instName);
+ m_modIdResolver.reset(new Flame::FileResolvingTask(pack));
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, [&]()
+ {
+ auto results = m_modIdResolver->getResults();
+ m_filesNetJob.reset(new NetJob(tr("Mod download")));
+ for(auto result: results.files)
+ {
+ QString filename = result.fileName;
+ if(!result.required)
+ {
+ filename += ".disabled";
+ }
+
+ auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename);
+ auto path = FS::PathCombine(m_stagingPath , relpath);
+
+ switch(result.type)
+ {
+ case Flame::File::Type::Folder:
+ {
+ logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath));
+ // fall-through intentional, we treat these as plain old mods and dump them wherever.
+ }
+ case Flame::File::Type::SingleFile:
+ case Flame::File::Type::Mod:
+ {
+ qDebug() << "Will download" << result.url << "to" << path;
+ auto dl = Net::Download::makeFile(result.url, path);
+ m_filesNetJob->addNetAction(dl);
+ break;
+ }
+ case Flame::File::Type::Modpack:
+ logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath));
+ break;
+ case Flame::File::Type::Cmod2:
+ case Flame::File::Type::Ctoc:
+ case Flame::File::Type::Unknown:
+ logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath));
+ break;
+ }
+ }
+ m_modIdResolver.reset();
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
+ {
+ m_filesNetJob.reset();
+ emitSucceeded();
+ }
+ );
+ connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason)
+ {
+ m_filesNetJob.reset();
+ emitFailed(reason);
+ });
+ connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total)
+ {
+ setProgress(current, total);
+ });
+ setStatus(tr("Downloading mods..."));
+ m_filesNetJob->start();
+ }
+ );
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason)
+ {
+ m_modIdResolver.reset();
+ emitFailed(tr("Unable to resolve mod IDs:\n") + reason);
+ });
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, [&](qint64 current, qint64 total)
+ {
+ setProgress(current, total);
+ });
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, [&](QString status)
+ {
+ setStatus(status);
+ });
+ m_modIdResolver->start();
+}
+
+void InstanceImportTask::processTechnic()
+{
+ shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded);
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed);
+ packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath);
+}
+
+void InstanceImportTask::processMultiMC()
+{
+ // FIXME: copy from FolderInstanceProvider!!! FIX IT!!!
+ QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+
+ NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+
+ // reset time played on import... because packs.
+ instance.resetTimePlayed();
+
+ // set a new nice name
+ instance.setName(m_instName);
+
+ // if the icon was specified by user, use that. otherwise pull icon from the pack
+ if (m_instIcon != "default")
+ {
+ instance.setIconKey(m_instIcon);
+ }
+ else
+ {
+ m_instIcon = instance.iconKey();
+
+ auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon);
+ if (!importIconPath.isNull() && QFile::exists(importIconPath))
+ {
+ // import icon
+ auto iconList = ENV.icons();
+ if (iconList->iconFileExists(m_instIcon))
+ {
+ iconList->deleteIcon(m_instIcon);
+ }
+ iconList->installIcons({importIconPath});
+ }
+ }
+ emitSucceeded();
+}
diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h
new file mode 100644
index 00000000..72ae6851
--- /dev/null
+++ b/launcher/InstanceImportTask.h
@@ -0,0 +1,72 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+#include <QUrl>
+#include <QFuture>
+#include <QFutureWatcher>
+#include "settings/SettingsObject.h"
+#include "QObjectPtr.h"
+
+#include <nonstd/optional>
+
+class QuaZip;
+namespace Flame
+{
+ class FileResolvingTask;
+}
+
+class InstanceImportTask : public InstanceTask
+{
+ Q_OBJECT
+public:
+ explicit InstanceImportTask(const QUrl sourceUrl);
+
+protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+private:
+ void processZipPack();
+ void processMultiMC();
+ void processFlame();
+ void processTechnic();
+
+private slots:
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void extractFinished();
+ void extractAborted();
+
+private: /* data */
+ NetJobPtr m_filesNetJob;
+ shared_qobject_ptr<Flame::FileResolvingTask> m_modIdResolver;
+ QUrl m_sourceUrl;
+ QString m_archivePath;
+ bool m_downloadRequired = false;
+ std::unique_ptr<QuaZip> m_packZip;
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ enum class ModpackType{
+ Unknown,
+ MultiMC,
+ Flame,
+ Technic
+ } m_modpackType = ModpackType::Unknown;
+};
diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp
new file mode 100644
index 00000000..cb38853b
--- /dev/null
+++ b/launcher/InstanceList.cpp
@@ -0,0 +1,867 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QDir>
+#include <QDirIterator>
+#include <QSet>
+#include <QFile>
+#include <QThread>
+#include <QTextStream>
+#include <QXmlStreamReader>
+#include <QTimer>
+#include <QDebug>
+#include <QFileSystemWatcher>
+#include <QUuid>
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include "InstanceList.h"
+#include "BaseInstance.h"
+#include "InstanceTask.h"
+#include "settings/INISettingsObject.h"
+#include "minecraft/legacy/LegacyInstance.h"
+#include "NullInstance.h"
+#include "minecraft/MinecraftInstance.h"
+#include "FileSystem.h"
+#include "ExponentialSeries.h"
+#include "WatchLock.h"
+
+const static int GROUP_FILE_FORMAT_VERSION = 1;
+
+InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent)
+ : QAbstractListModel(parent), m_globalSettings(settings)
+{
+ resumeWatch();
+ // Create aand normalize path
+ if (!QDir::current().exists(instDir))
+ {
+ QDir::current().mkpath(instDir);
+ }
+
+ connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated);
+
+ // NOTE: canonicalPath requires the path to exist. Do not move this above the creation block!
+ m_instDir = QDir(instDir).canonicalPath();
+ m_watcher = new QFileSystemWatcher(this);
+ connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged);
+ m_watcher->addPath(m_instDir);
+}
+
+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::EditRole:
+ case Qt::DisplayRole:
+ {
+ return pdata->name();
+ }
+ case Qt::AccessibleTextRole:
+ {
+ return tr("%1 Instance").arg(pdata->name());
+ }
+ case Qt::ToolTipRole:
+ {
+ return pdata->instanceRoot();
+ }
+ case Qt::DecorationRole:
+ {
+ return pdata->iconKey();
+ }
+ // HACK: see GroupView.h in gui!
+ case GroupRole:
+ {
+ return getInstanceGroup(pdata->id());
+ }
+ default:
+ break;
+ }
+ return QVariant();
+}
+
+bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ if (!index.isValid())
+ {
+ return false;
+ }
+ if(role != Qt::EditRole)
+ {
+ return false;
+ }
+ BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
+ auto newName = value.toString();
+ if(pdata->name() == newName)
+ {
+ return true;
+ }
+ pdata->setName(newName);
+ return true;
+}
+
+Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
+{
+ Qt::ItemFlags f;
+ if (index.isValid())
+ {
+ f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
+ }
+ return f;
+}
+
+GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
+{
+ auto inst = getInstanceById(id);
+ if(!inst)
+ {
+ return GroupId();
+ }
+ auto iter = m_instanceGroupIndex.find(inst->id());
+ if(iter != m_instanceGroupIndex.end())
+ {
+ return *iter;
+ }
+ return GroupId();
+}
+
+void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
+{
+ auto inst = getInstanceById(id);
+ if(!inst)
+ {
+ qDebug() << "Attempt to set a null instance's group";
+ return;
+ }
+
+ bool changed = false;
+ auto iter = m_instanceGroupIndex.find(inst->id());
+ if(iter != m_instanceGroupIndex.end())
+ {
+ if(*iter != name)
+ {
+ *iter = name;
+ changed = true;
+ }
+ }
+ else
+ {
+ changed = true;
+ m_instanceGroupIndex[id] = name;
+ }
+
+ if(changed)
+ {
+ m_groupNameCache.insert(name);
+ auto idx = getInstIndex(inst.get());
+ emit dataChanged(index(idx), index(idx), {GroupRole});
+ saveGroupList();
+ }
+}
+
+QStringList InstanceList::getGroups()
+{
+ return m_groupNameCache.toList();
+}
+
+void InstanceList::deleteGroup(const QString& name)
+{
+ bool removed = false;
+ qDebug() << "Delete group" << name;
+ for(auto & instance: m_instances)
+ {
+ const auto & instID = instance->id();
+ auto instGroupName = getInstanceGroup(instID);
+ if(instGroupName == name)
+ {
+ m_instanceGroupIndex.remove(instID);
+ qDebug() << "Remove" << instID << "from group" << name;
+ removed = true;
+ auto idx = getInstIndex(instance.get());
+ if(idx > 0)
+ {
+ emit dataChanged(index(idx), index(idx), {GroupRole});
+ }
+ }
+ }
+ if(removed)
+ {
+ saveGroupList();
+ }
+}
+
+bool InstanceList::isGroupCollapsed(const QString& group)
+{
+ return m_collapsedGroups.contains(group);
+}
+
+void InstanceList::deleteInstance(const InstanceId& id)
+{
+ auto inst = getInstanceById(id);
+ if(!inst)
+ {
+ qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?).";
+ return;
+ }
+
+ if(m_instanceGroupIndex.remove(id))
+ {
+ saveGroupList();
+ }
+
+ qDebug() << "Will delete instance" << id;
+ if(!FS::deletePath(inst->instanceRoot()))
+ {
+ qWarning() << "Deletion of instance" << id << "has not been completely successful ...";
+ return;
+ }
+
+ qDebug() << "Instance" << id << "has been deleted by MultiMC.";
+}
+
+static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &list)
+{
+ QMap<InstanceId, InstanceLocator> out;
+ int i = 0;
+ for(auto & item: list)
+ {
+ auto id = item->id();
+ if(out.contains(id))
+ {
+ qWarning() << "Duplicate ID" << id << "in instance list";
+ }
+ out[id] = std::make_pair(item, i);
+ i++;
+ }
+ return out;
+}
+
+QList< InstanceId > InstanceList::discoverInstances()
+{
+ qDebug() << "Discovering instances in" << m_instDir;
+ QList<InstanceId> out;
+ QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks);
+ while (iter.hasNext())
+ {
+ QString subDir = iter.next();
+ QFileInfo dirInfo(subDir);
+ if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists())
+ continue;
+ // if it is a symlink, ignore it if it goes to the instance folder
+ if(dirInfo.isSymLink())
+ {
+ QFileInfo targetInfo(dirInfo.symLinkTarget());
+ QFileInfo instDirInfo(m_instDir);
+ if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath())
+ {
+ qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder";
+ continue;
+ }
+ }
+ auto id = dirInfo.fileName();
+ out.append(id);
+ qDebug() << "Found instance ID" << id;
+ }
+ instanceSet = out.toSet();
+ m_instancesProbed = true;
+ return out;
+}
+
+InstanceList::InstListError InstanceList::loadList()
+{
+ auto existingIds = getIdMapping(m_instances);
+
+ QList<InstancePtr> newList;
+
+ for(auto & id: discoverInstances())
+ {
+ if(existingIds.contains(id))
+ {
+ auto instPair = existingIds[id];
+ existingIds.remove(id);
+ qDebug() << "Should keep and soft-reload" << id;
+ }
+ else
+ {
+ InstancePtr instPtr = loadInstance(id);
+ if(instPtr)
+ {
+ newList.append(instPtr);
+ }
+ }
+ }
+
+ // TODO: looks like a general algorithm with a few specifics inserted. Do something about it.
+ if(!existingIds.isEmpty())
+ {
+ // get the list of removed instances and sort it by their original index, from last to first
+ auto deadList = existingIds.values();
+ auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool
+ {
+ return a.second > b.second;
+ };
+ std::sort(deadList.begin(), deadList.end(), orderSortPredicate);
+ // remove the contiguous ranges of rows
+ int front_bookmark = -1;
+ int back_bookmark = -1;
+ int currentItem = -1;
+ auto removeNow = [&]()
+ {
+ beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark);
+ m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1);
+ endRemoveRows();
+ front_bookmark = -1;
+ back_bookmark = currentItem;
+ };
+ for(auto & removedItem: deadList)
+ {
+ auto instPtr = removedItem.first;
+ instPtr->invalidate();
+ currentItem = removedItem.second;
+ if(back_bookmark == -1)
+ {
+ // no bookmark yet
+ back_bookmark = currentItem;
+ }
+ else if(currentItem == front_bookmark - 1)
+ {
+ // part of contiguous sequence, continue
+ }
+ else
+ {
+ // seam between previous and current item
+ removeNow();
+ }
+ front_bookmark = currentItem;
+ }
+ if(back_bookmark != -1)
+ {
+ removeNow();
+ }
+ }
+ if(newList.size())
+ {
+ add(newList);
+ }
+ m_dirty = false;
+ updateTotalPlayTime();
+ return NoError;
+}
+
+void InstanceList::updateTotalPlayTime()
+{
+ totalPlayTime = 0;
+ for(auto const& itr : m_instances)
+ {
+ totalPlayTime += itr.get()->totalTimePlayed();
+ }
+}
+
+void InstanceList::saveNow()
+{
+ for(auto & item: m_instances)
+ {
+ item->saveNow();
+ }
+}
+
+void InstanceList::add(const QList<InstancePtr> &t)
+{
+ beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1);
+ m_instances.append(t);
+ for(auto & ptr : t)
+ {
+ connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged);
+ }
+ endInsertRows();
+}
+
+void InstanceList::resumeWatch()
+{
+ if(m_watchLevel > 0)
+ {
+ qWarning() << "Bad suspend level resume in instance list";
+ return;
+ }
+ m_watchLevel++;
+ if(m_watchLevel > 0 && m_dirty)
+ {
+ loadList();
+ }
+}
+
+void InstanceList::suspendWatch()
+{
+ m_watchLevel --;
+}
+
+void InstanceList::providerUpdated()
+{
+ m_dirty = true;
+ if(m_watchLevel == 1)
+ {
+ loadList();
+ }
+}
+
+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;
+}
+
+void InstanceList::propertiesChanged(BaseInstance *inst)
+{
+ int i = getInstIndex(inst);
+ if (i != -1)
+ {
+ emit dataChanged(index(i), index(i));
+ updateTotalPlayTime();
+ }
+}
+
+InstancePtr InstanceList::loadInstance(const InstanceId& id)
+{
+ if(!m_groupsLoaded)
+ {
+ loadGroupList();
+ }
+
+ auto instanceRoot = FS::PathCombine(m_instDir, id);
+ auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instanceRoot, "instance.cfg"));
+ InstancePtr inst;
+
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+
+ QString inst_type = instanceSettings->get("InstanceType").toString();
+
+ if (inst_type == "OneSix" || inst_type == "Nostalgia")
+ {
+ inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot));
+ }
+ else if (inst_type == "Legacy")
+ {
+ inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instanceRoot));
+ }
+ else
+ {
+ inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot));
+ }
+ qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot();
+ return inst;
+}
+
+void InstanceList::saveGroupList()
+{
+ qDebug() << "Will save group list now.";
+ if(!m_instancesProbed)
+ {
+ qDebug() << "Group saving prevented because we don't know the full list of instances yet.";
+ return;
+ }
+ WatchLock foo(m_watcher, m_instDir);
+ QString groupFileName = m_instDir + "/instgroups.json";
+ QMap<QString, QSet<QString>> reverseGroupMap;
+ for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++)
+ {
+ QString id = iter.key();
+ QString group = iter.value();
+ if (group.isEmpty())
+ continue;
+ if(!instanceSet.contains(id))
+ {
+ qDebug() << "Skipping saving missing instance" << id << "to groups list.";
+ continue;
+ }
+
+ if (!reverseGroupMap.count(group))
+ {
+ QSet<QString> set;
+ set.insert(id);
+ reverseGroupMap[group] = set;
+ }
+ else
+ {
+ QSet<QString> &set = reverseGroupMap[group];
+ set.insert(id);
+ }
+ }
+ QJsonObject toplevel;
+ toplevel.insert("formatVersion", QJsonValue(QString("1")));
+ QJsonObject groupsArr;
+ for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++)
+ {
+ auto list = iter.value();
+ auto name = iter.key();
+ QJsonObject groupObj;
+ QJsonArray instanceArr;
+ groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name)));
+ 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());
+ qDebug() << "Group list saved.";
+ }
+ catch (const FS::FileSystemException &e)
+ {
+ qCritical() << "Failed to write instance group file :" << e.cause();
+ }
+}
+
+void InstanceList::loadGroupList()
+{
+ qDebug() << "Will load group list now.";
+
+ QString groupFileName = m_instDir + "/instgroups.json";
+
+ // if there's no group file, fail
+ if (!QFileInfo(groupFileName).exists())
+ return;
+
+ QByteArray jsonData;
+ try
+ {
+ jsonData = FS::read(groupFileName);
+ }
+ catch (const 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;
+ }
+
+ QSet<QString> groupSet;
+ m_instanceGroupIndex.clear();
+
+ // 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
+ groupSet.insert(groupName);
+
+ auto hidden = groupObj.value("hidden").toBool(false);
+ if(hidden) {
+ m_collapsedGroups.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++)
+ {
+ m_instanceGroupIndex[(*iter2).toString()] = groupName;
+ }
+ }
+ m_groupsLoaded = true;
+ m_groupNameCache.unite(groupSet);
+ qDebug() << "Group list loaded.";
+}
+
+void InstanceList::instanceDirContentsChanged(const QString& path)
+{
+ Q_UNUSED(path);
+ emit instancesChanged();
+}
+
+void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
+{
+ QString newInstDir = QDir(value.toString()).canonicalPath();
+ if(newInstDir != m_instDir)
+ {
+ if(m_groupsLoaded)
+ {
+ saveGroupList();
+ }
+ m_instDir = newInstDir;
+ m_groupsLoaded = false;
+ emit instancesChanged();
+ }
+}
+
+void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
+{
+ qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded");
+ if(collapsed) {
+ m_collapsedGroups.insert(group);
+ } else {
+ m_collapsedGroups.remove(group);
+ }
+ saveGroupList();
+}
+
+class InstanceStaging : public Task
+{
+Q_OBJECT
+ const unsigned minBackoff = 1;
+ const unsigned maxBackoff = 16;
+public:
+ InstanceStaging (
+ InstanceList * parent,
+ Task * child,
+ const QString & stagingPath,
+ const QString& instanceName,
+ const QString& groupName )
+ : backoff(minBackoff, maxBackoff)
+ {
+ m_parent = parent;
+ m_child.reset(child);
+ connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded);
+ connect(child, &Task::failed, this, &InstanceStaging::childFailed);
+ connect(child, &Task::status, this, &InstanceStaging::setStatus);
+ connect(child, &Task::progress, this, &InstanceStaging::setProgress);
+ m_instanceName = instanceName;
+ m_groupName = groupName;
+ m_stagingPath = stagingPath;
+ m_backoffTimer.setSingleShot(true);
+ connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
+ }
+
+ virtual ~InstanceStaging() {};
+
+
+ // FIXME/TODO: add ability to abort during instance commit retries
+ bool abort() override
+ {
+ if(m_child && m_child->canAbort())
+ {
+ return m_child->abort();
+ }
+ return false;
+ }
+ bool canAbort() const override
+ {
+ if(m_child && m_child->canAbort())
+ {
+ return true;
+ }
+ return false;
+ }
+
+protected:
+ virtual void executeTask() override
+ {
+ m_child->start();
+ }
+ QStringList warnings() const override
+ {
+ return m_child->warnings();
+ }
+
+private slots:
+ void childSucceded()
+ {
+ unsigned sleepTime = backoff();
+ if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName))
+ {
+ emitSucceeded();
+ return;
+ }
+ // we actually failed, retry?
+ if(sleepTime == maxBackoff)
+ {
+ emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
+ return;
+ }
+ qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime;
+ m_backoffTimer.start(sleepTime * 500);
+ }
+ void childFailed(const QString & reason)
+ {
+ m_parent->destroyStagingPath(m_stagingPath);
+ emitFailed(reason);
+ }
+
+private:
+ /*
+ * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows.
+ * Basically, it starts messing things up while MultiMC is extracting/creating instances
+ * and causes that horrible failure that is NTFS to lock files in place because they are open.
+ */
+ ExponentialSeries backoff;
+ QString m_stagingPath;
+ InstanceList * m_parent;
+ unique_qobject_ptr<Task> m_child;
+ QString m_instanceName;
+ QString m_groupName;
+ QTimer m_backoffTimer;
+};
+
+Task * InstanceList::wrapInstanceTask(InstanceTask * task)
+{
+ auto stagingPath = getStagedInstancePath();
+ task->setStagingPath(stagingPath);
+ task->setParentSettings(m_globalSettings);
+ return new InstanceStaging(this, task, stagingPath, task->name(), task->group());
+}
+
+QString InstanceList::getStagedInstancePath()
+{
+ QString key = QUuid::createUuid().toString();
+ QString relPath = FS::PathCombine("_MMC_TEMP/" , key);
+ QDir rootPath(m_instDir);
+ auto path = FS::PathCombine(m_instDir, relPath);
+ if(!rootPath.mkpath(relPath))
+ {
+ return QString();
+ }
+ return path;
+}
+
+bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName)
+{
+ QDir dir;
+ QString instID = FS::DirNameFromString(instanceName, m_instDir);
+ {
+ WatchLock lock(m_watcher, m_instDir);
+ QString destination = FS::PathCombine(m_instDir, instID);
+ if(!dir.rename(path, destination))
+ {
+ qWarning() << "Failed to move" << path << "to" << destination;
+ return false;
+ }
+ m_instanceGroupIndex[instID] = groupName;
+ instanceSet.insert(instID);
+ m_groupNameCache.insert(groupName);
+ emit instancesChanged();
+ emit instanceSelectRequest(instID);
+ }
+ saveGroupList();
+ return true;
+}
+
+bool InstanceList::destroyStagingPath(const QString& keyPath)
+{
+ return FS::deletePath(keyPath);
+}
+
+int InstanceList::getTotalPlayTime() {
+ updateTotalPlayTime();
+ return totalPlayTime;
+}
+
+#include "InstanceList.moc"
diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h
new file mode 100644
index 00000000..4d2dc1f6
--- /dev/null
+++ b/launcher/InstanceList.h
@@ -0,0 +1,173 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QAbstractListModel>
+#include <QSet>
+#include <QList>
+
+#include "BaseInstance.h"
+
+#include "QObjectPtr.h"
+
+class QFileSystemWatcher;
+class InstanceTask;
+using InstanceId = QString;
+using GroupId = QString;
+using InstanceLocator = std::pair<InstancePtr, int>;
+
+enum class InstCreateError
+{
+ NoCreateError = 0,
+ NoSuchVersion,
+ UnknownCreateError,
+ InstExists,
+ CantCreateDir
+};
+
+enum class GroupsState
+{
+ NotLoaded,
+ Steady,
+ Dirty
+};
+
+
+class InstanceList : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ explicit InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent = 0);
+ virtual ~InstanceList();
+
+public:
+ QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ bool setData(const QModelIndex & index, const QVariant & value, int role) override;
+
+ 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
+ };
+
+ InstancePtr at(int i) const
+ {
+ return m_instances.at(i);
+ }
+
+ int count() const
+ {
+ return m_instances.count();
+ }
+
+ InstListError loadList();
+ void saveNow();
+
+
+ InstancePtr getInstanceById(QString id) const;
+ QModelIndex getInstanceIndexById(const QString &id) const;
+ QStringList getGroups();
+ bool isGroupCollapsed(const QString &groupName);
+
+ GroupId getInstanceGroup(const InstanceId & id) const;
+ void setInstanceGroup(const InstanceId & id, const GroupId& name);
+
+ void deleteGroup(const GroupId & name);
+ void deleteInstance(const InstanceId & id);
+
+ // Wrap an instance creation task in some more task machinery and make it ready to be used
+ Task * wrapInstanceTask(InstanceTask * task);
+
+ /**
+ * Create a new empty staging area for instance creation and @return a path/key top commit it later.
+ * Used by instance manipulation tasks.
+ */
+ QString getStagedInstancePath();
+
+ /**
+ * Commit the staging area given by @keyPath to the provider - used when creation succeeds.
+ * Used by instance manipulation tasks.
+ */
+ bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName);
+
+ /**
+ * Destroy a previously created staging area given by @keyPath - used when creation fails.
+ * Used by instance manipulation tasks.
+ */
+ bool destroyStagingPath(const QString & keyPath);
+
+ int getTotalPlayTime();
+
+signals:
+ void dataIsInvalid();
+ void instancesChanged();
+ void instanceSelectRequest(QString instanceId);
+ void groupsChanged(QSet<QString> groups);
+
+public slots:
+ void on_InstFolderChanged(const Setting &setting, QVariant value);
+ void on_GroupStateChanged(const QString &group, bool collapsed);
+
+private slots:
+ void propertiesChanged(BaseInstance *inst);
+ void providerUpdated();
+ void instanceDirContentsChanged(const QString &path);
+
+private:
+ int getInstIndex(BaseInstance *inst) const;
+ void updateTotalPlayTime();
+ void suspendWatch();
+ void resumeWatch();
+ void add(const QList<InstancePtr> &list);
+ void loadGroupList();
+ void saveGroupList();
+ QList<InstanceId> discoverInstances();
+ InstancePtr loadInstance(const InstanceId& id);
+
+private:
+ int m_watchLevel = 0;
+ int totalPlayTime = 0;
+ bool m_dirty = false;
+ QList<InstancePtr> m_instances;
+ QSet<QString> m_groupNameCache;
+
+ SettingsObjectPtr m_globalSettings;
+ QString m_instDir;
+ QFileSystemWatcher * m_watcher;
+ // FIXME: this is so inefficient that looking at it is almost painful.
+ QSet<QString> m_collapsedGroups;
+ QMap<InstanceId, GroupId> m_instanceGroupIndex;
+ QSet<InstanceId> instanceSet;
+ bool m_groupsLoaded = false;
+ bool m_instancesProbed = false;
+};
diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h
new file mode 100644
index 00000000..3cb723c4
--- /dev/null
+++ b/launcher/InstancePageProvider.h
@@ -0,0 +1,76 @@
+#pragma once
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/legacy/LegacyInstance.h"
+#include <FileSystem.h>
+#include "pages/BasePage.h"
+#include "pages/BasePageProvider.h"
+#include "pages/instance/LogPage.h"
+#include "pages/instance/VersionPage.h"
+#include "pages/instance/ModFolderPage.h"
+#include "pages/instance/ResourcePackPage.h"
+#include "pages/instance/TexturePackPage.h"
+#include "pages/instance/NotesPage.h"
+#include "pages/instance/ScreenshotsPage.h"
+#include "pages/instance/InstanceSettingsPage.h"
+#include "pages/instance/OtherLogsPage.h"
+#include "pages/instance/LegacyUpgradePage.h"
+#include "pages/instance/WorldListPage.h"
+#include "pages/instance/ServersPage.h"
+#include "pages/instance/GameOptionsPage.h"
+
+#include "Env.h"
+
+class InstancePageProvider : public QObject, public BasePageProvider
+{
+ Q_OBJECT
+public:
+ explicit InstancePageProvider(InstancePtr parent)
+ {
+ inst = parent;
+ }
+
+ virtual ~InstancePageProvider() {};
+ virtual QList<BasePage *> getPages() override
+ {
+ QList<BasePage *> values;
+ values.append(new LogPage(inst));
+ std::shared_ptr<MinecraftInstance> onesix = std::dynamic_pointer_cast<MinecraftInstance>(inst);
+ if(onesix)
+ {
+ values.append(new VersionPage(onesix.get()));
+ auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList(), "mods", "loadermods", tr("Loader mods"), "Loader-mods");
+ modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
+ values.append(modsPage);
+ values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList(), "coremods", "coremods", tr("Core mods"), "Core-mods"));
+ values.append(new ResourcePackPage(onesix.get()));
+ values.append(new TexturePackPage(onesix.get()));
+ values.append(new NotesPage(onesix.get()));
+ values.append(new WorldListPage(onesix.get(), onesix->worldList()));
+ values.append(new ServersPage(onesix));
+ // values.append(new GameOptionsPage(onesix.get()));
+ values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots")));
+ values.append(new InstanceSettingsPage(onesix.get()));
+ }
+ std::shared_ptr<LegacyInstance> legacy = std::dynamic_pointer_cast<LegacyInstance>(inst);
+ if(legacy)
+ {
+ values.append(new LegacyUpgradePage(legacy));
+ values.append(new NotesPage(legacy.get()));
+ values.append(new WorldListPage(legacy.get(), legacy->worldList()));
+ values.append(new ScreenshotsPage(FS::PathCombine(legacy->gameRoot(), "screenshots")));
+ }
+ auto logMatcher = inst->getLogFileMatcher();
+ if(logMatcher)
+ {
+ values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher));
+ }
+ return values;
+ }
+
+ virtual QString dialogTitle() override
+ {
+ return tr("Edit Instance (%1)").arg(inst->name());
+ }
+protected:
+ InstancePtr inst;
+};
diff --git a/launcher/InstanceProxyModel.cpp b/launcher/InstanceProxyModel.cpp
new file mode 100644
index 00000000..5317f60c
--- /dev/null
+++ b/launcher/InstanceProxyModel.cpp
@@ -0,0 +1,34 @@
+#include "InstanceProxyModel.h"
+#include "MultiMC.h"
+#include <BaseInstance.h>
+#include <icons/IconList.h>
+
+InstanceProxyModel::InstanceProxyModel(QObject *parent) : GroupedProxyModel(parent)
+{
+}
+
+QVariant InstanceProxyModel::data(const QModelIndex & index, int role) const
+{
+ QVariant data = QSortFilterProxyModel::data(index, role);
+ if(role == Qt::DecorationRole)
+ {
+ return QVariant(MMC->icons()->getIcon(data.toString()));
+ }
+ return data;
+}
+
+bool InstanceProxyModel::subSortLessThan(const QModelIndex &left,
+ const QModelIndex &right) const
+{
+ BaseInstance *pdataLeft = static_cast<BaseInstance *>(left.internalPointer());
+ BaseInstance *pdataRight = static_cast<BaseInstance *>(right.internalPointer());
+ QString sortMode = MMC->settings()->get("InstSortMode").toString();
+ if (sortMode == "LastLaunch")
+ {
+ return pdataLeft->lastLaunch() > pdataRight->lastLaunch();
+ }
+ else
+ {
+ return QString::localeAwareCompare(pdataLeft->name(), pdataRight->name()) < 0;
+ }
+}
diff --git a/launcher/InstanceProxyModel.h b/launcher/InstanceProxyModel.h
new file mode 100644
index 00000000..fab6f834
--- /dev/null
+++ b/launcher/InstanceProxyModel.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "groupview/GroupedProxyModel.h"
+
+/**
+ * A proxy model that is responsible for sorting instances into groups
+ */
+class InstanceProxyModel : public GroupedProxyModel
+{
+public:
+ explicit InstanceProxyModel(QObject *parent = 0);
+ QVariant data(const QModelIndex & index, int role) const override;
+
+protected:
+ virtual bool subSortLessThan(const QModelIndex &left, const QModelIndex &right) const override;
+};
diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp
new file mode 100644
index 00000000..dd132877
--- /dev/null
+++ b/launcher/InstanceTask.cpp
@@ -0,0 +1,9 @@
+#include "InstanceTask.h"
+
+InstanceTask::InstanceTask()
+{
+}
+
+InstanceTask::~InstanceTask()
+{
+}
diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h
new file mode 100644
index 00000000..82e23f11
--- /dev/null
+++ b/launcher/InstanceTask.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "tasks/Task.h"
+#include "settings/SettingsObject.h"
+
+class InstanceTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit InstanceTask();
+ virtual ~InstanceTask();
+
+ void setParentSettings(SettingsObjectPtr settings)
+ {
+ m_globalSettings = settings;
+ }
+
+ void setStagingPath(const QString &stagingPath)
+ {
+ m_stagingPath = stagingPath;
+ }
+
+ void setName(const QString &name)
+ {
+ m_instName = name;
+ }
+ QString name() const
+ {
+ return m_instName;
+ }
+
+ void setIcon(const QString &icon)
+ {
+ m_instIcon = icon;
+ }
+
+ void setGroup(const QString &group)
+ {
+ m_instGroup = group;
+ }
+ QString group() const
+ {
+ return m_instGroup;
+ }
+
+protected: /* data */
+ SettingsObjectPtr m_globalSettings;
+ QString m_instName;
+ QString m_instIcon;
+ QString m_instGroup;
+ QString m_stagingPath;
+};
diff --git a/launcher/InstanceWindow.cpp b/launcher/InstanceWindow.cpp
new file mode 100644
index 00000000..015ffe1c
--- /dev/null
+++ b/launcher/InstanceWindow.cpp
@@ -0,0 +1,236 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "InstanceWindow.h"
+#include "MultiMC.h"
+
+#include <QScrollBar>
+#include <QMessageBox>
+#include <QHBoxLayout>
+#include <QPushButton>
+#include <qlayoutitem.h>
+#include <QCloseEvent>
+
+#include <dialogs/CustomMessageBox.h>
+#include <dialogs/ProgressDialog.h>
+#include "widgets/PageContainer.h"
+#include "InstancePageProvider.h"
+
+#include "icons/IconList.h"
+
+InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent)
+ : QMainWindow(parent), m_instance(instance)
+{
+ setAttribute(Qt::WA_DeleteOnClose);
+
+ auto icon = MMC->icons()->getIcon(m_instance->iconKey());
+ QString windowTitle = tr("Console window for ") + m_instance->name();
+
+ // Set window properties
+ {
+ setWindowIcon(icon);
+ setWindowTitle(windowTitle);
+ }
+
+ // Add page container
+ {
+ auto provider = std::make_shared<InstancePageProvider>(m_instance);
+ m_container = new PageContainer(provider.get(), "console", this);
+ m_container->setParentContainer(this);
+ setCentralWidget(m_container);
+ setContentsMargins(0, 0, 0, 0);
+ }
+
+ // Add custom buttons to the page container layout.
+ {
+ auto horizontalLayout = new QHBoxLayout();
+ horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+ horizontalLayout->setContentsMargins(6, -1, 6, -1);
+
+ auto btnHelp = new QPushButton();
+ btnHelp->setText(tr("Help"));
+ horizontalLayout->addWidget(btnHelp);
+ connect(btnHelp, SIGNAL(clicked(bool)), m_container, SLOT(help()));
+
+ auto spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
+ horizontalLayout->addSpacerItem(spacer);
+
+ m_killButton = new QPushButton();
+ horizontalLayout->addWidget(m_killButton);
+ connect(m_killButton, SIGNAL(clicked(bool)), SLOT(on_btnKillMinecraft_clicked()));
+
+ m_launchOfflineButton = new QPushButton();
+ horizontalLayout->addWidget(m_launchOfflineButton);
+ m_launchOfflineButton->setText(tr("Launch Offline"));
+ updateLaunchButtons();
+ connect(m_launchOfflineButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftOffline_clicked()));
+
+ m_closeButton = new QPushButton();
+ m_closeButton->setText(tr("Close"));
+ horizontalLayout->addWidget(m_closeButton);
+ connect(m_closeButton, SIGNAL(clicked(bool)), SLOT(on_closeButton_clicked()));
+
+ m_container->addButtons(horizontalLayout);
+ }
+
+ // restore window state
+ {
+ auto base64State = MMC->settings()->get("ConsoleWindowState").toByteArray();
+ restoreState(QByteArray::fromBase64(base64State));
+ auto base64Geometry = MMC->settings()->get("ConsoleWindowGeometry").toByteArray();
+ restoreGeometry(QByteArray::fromBase64(base64Geometry));
+ }
+
+ // set up instance and launch process recognition
+ {
+ auto launchTask = m_instance->getLaunchTask();
+ on_InstanceLaunchTask_changed(launchTask);
+ connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &InstanceWindow::on_InstanceLaunchTask_changed);
+ connect(m_instance.get(), &BaseInstance::runningStatusChanged, this, &InstanceWindow::on_RunningState_changed);
+ }
+
+ // set up instance destruction detection
+ {
+ connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged);
+ }
+ show();
+}
+
+void InstanceWindow::on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus)
+{
+ if(newStatus == BaseInstance::Status::Gone)
+ {
+ m_doNotSave = true;
+ close();
+ }
+}
+
+void InstanceWindow::updateLaunchButtons()
+{
+ if(m_instance->isRunning())
+ {
+ m_launchOfflineButton->setEnabled(false);
+ m_killButton->setText(tr("Kill"));
+ m_killButton->setObjectName("killButton");
+ m_killButton->setToolTip(tr("Kill the running instance"));
+ }
+ else if(!m_instance->canLaunch())
+ {
+ m_launchOfflineButton->setEnabled(false);
+ m_killButton->setText(tr("Launch"));
+ m_killButton->setObjectName("launchButton");
+ m_killButton->setToolTip(tr("Launch the instance"));
+ m_killButton->setEnabled(false);
+ }
+ else
+ {
+ m_launchOfflineButton->setEnabled(true);
+ m_killButton->setText(tr("Launch"));
+ m_killButton->setObjectName("launchButton");
+ m_killButton->setToolTip(tr("Launch the instance"));
+ }
+ // NOTE: this is a hack to force the button to recalculate its style
+ m_killButton->setStyleSheet("/* */");
+ m_killButton->setStyleSheet(QString());
+}
+
+void InstanceWindow::on_btnLaunchMinecraftOffline_clicked()
+{
+ MMC->launch(m_instance, false, nullptr);
+}
+
+void InstanceWindow::on_InstanceLaunchTask_changed(shared_qobject_ptr<LaunchTask> proc)
+{
+ m_proc = proc;
+}
+
+void InstanceWindow::on_RunningState_changed(bool running)
+{
+ updateLaunchButtons();
+ m_container->refreshContainer();
+ if(running) {
+ selectPage("log");
+ }
+}
+
+void InstanceWindow::on_closeButton_clicked()
+{
+ close();
+}
+
+void InstanceWindow::closeEvent(QCloseEvent *event)
+{
+ bool proceed = true;
+ if(!m_doNotSave)
+ {
+ proceed &= m_container->prepareToClose();
+ }
+
+ if(!proceed)
+ {
+ return;
+ }
+
+ MMC->settings()->set("ConsoleWindowState", saveState().toBase64());
+ MMC->settings()->set("ConsoleWindowGeometry", saveGeometry().toBase64());
+ emit isClosing();
+ event->accept();
+}
+
+bool InstanceWindow::saveAll()
+{
+ return m_container->saveAll();
+}
+
+void InstanceWindow::on_btnKillMinecraft_clicked()
+{
+ if(m_instance->isRunning())
+ {
+ MMC->kill(m_instance);
+ }
+ else
+ {
+ MMC->launch(m_instance, true, nullptr);
+ }
+}
+
+QString InstanceWindow::instanceId()
+{
+ return m_instance->id();
+}
+
+bool InstanceWindow::selectPage(QString pageId)
+{
+ return m_container->selectPage(pageId);
+}
+
+void InstanceWindow::refreshContainer()
+{
+ m_container->refreshContainer();
+}
+
+InstanceWindow::~InstanceWindow()
+{
+}
+
+bool InstanceWindow::requestClose()
+{
+ if(m_container->prepareToClose())
+ {
+ close();
+ return true;
+ }
+ return false;
+}
diff --git a/launcher/InstanceWindow.h b/launcher/InstanceWindow.h
new file mode 100644
index 00000000..cd7d2494
--- /dev/null
+++ b/launcher/InstanceWindow.h
@@ -0,0 +1,73 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMainWindow>
+#include "LaunchController.h"
+#include <QObjectPtr.h>
+#include <QSystemTrayIcon>
+#include "launch/LaunchTask.h"
+#include "pages/BasePageContainer.h"
+
+class QPushButton;
+class PageContainer;
+class InstanceWindow : public QMainWindow, public BasePageContainer
+{
+ Q_OBJECT
+
+public:
+ explicit InstanceWindow(InstancePtr proc, QWidget *parent = 0);
+ virtual ~InstanceWindow();
+
+ bool selectPage(QString pageId) override;
+ void refreshContainer() override;
+
+ QString instanceId();
+
+ // save all settings and changes (prepare for launch)
+ bool saveAll();
+
+ // request closing the window (from a page)
+ bool requestClose() override;
+
+signals:
+ void isClosing();
+
+private
+slots:
+ void on_closeButton_clicked();
+ void on_btnKillMinecraft_clicked();
+ void on_btnLaunchMinecraftOffline_clicked();
+
+ void on_InstanceLaunchTask_changed(shared_qobject_ptr<LaunchTask> proc);
+ void on_RunningState_changed(bool running);
+ void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus);
+
+protected:
+ void closeEvent(QCloseEvent *) override;
+
+private:
+ void updateLaunchButtons();
+
+private:
+ shared_qobject_ptr<LaunchTask> m_proc;
+ InstancePtr m_instance;
+ bool m_doNotSave = false;
+ PageContainer *m_container = nullptr;
+ QPushButton *m_closeButton = nullptr;
+ QPushButton *m_killButton = nullptr;
+ QPushButton *m_launchOfflineButton = nullptr;
+};
diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp
new file mode 100644
index 00000000..92a058f0
--- /dev/null
+++ b/launcher/JavaCommon.cpp
@@ -0,0 +1,104 @@
+#include "JavaCommon.h"
+#include "dialogs/CustomMessageBox.h"
+#include <MMCStrings.h>
+
+bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent)
+{
+ if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegExp("-Xm[sx]"))
+ || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize"))
+ {
+ auto warnStr = QObject::tr(
+ "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" or \"-Xms\").\n"
+ "There are dedicated boxes for these in the settings (Java tab, in the Memory group at the top).\n"
+ "This message will be displayed until you remove them from the JVM arguments.");
+ CustomMessageBox::selectable(
+ parent, QObject::tr("JVM arguments warning"),
+ warnStr,
+ QMessageBox::Warning)->exec();
+ return false;
+ }
+ return true;
+}
+
+void JavaCommon::javaWasOk(QWidget *parent, JavaCheckResult result)
+{
+ QString text;
+ text += QObject::tr("Java test succeeded!<br />Platform reported: %1<br />Java version "
+ "reported: %2<br />Java vendor "
+ "reported: %3<br />").arg(result.realPlatform, result.javaVersion.toString(), result.javaVendor);
+ if (result.errorLog.size())
+ {
+ auto htmlError = result.errorLog;
+ htmlError.replace('\n', "<br />");
+ text += QObject::tr("<br />Warnings:<br /><font color=\"orange\">%1</font>").arg(htmlError);
+ }
+ CustomMessageBox::selectable(parent, QObject::tr("Java test success"), text, QMessageBox::Information)->show();
+}
+
+void JavaCommon::javaArgsWereBad(QWidget *parent, JavaCheckResult result)
+{
+ auto htmlError = result.errorLog;
+ QString text;
+ htmlError.replace('\n', "<br />");
+ text += QObject::tr("The specified java binary didn't work with the arguments you provided:<br />");
+ text += QString("<font color=\"red\">%1</font>").arg(htmlError);
+ CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show();
+}
+
+void JavaCommon::javaBinaryWasBad(QWidget *parent, JavaCheckResult result)
+{
+ QString text;
+ text += QObject::tr(
+ "The specified java binary didn't work.<br />You should use the auto-detect feature, "
+ "or set the path to the java executable.<br />");
+ CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show();
+}
+
+void JavaCommon::TestCheck::run()
+{
+ if (!JavaCommon::checkJVMArgs(m_args, m_parent))
+ {
+ emit finished();
+ return;
+ }
+ checker.reset(new JavaChecker());
+ connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
+ SLOT(checkFinished(JavaCheckResult)));
+ checker->m_path = m_path;
+ checker->performCheck();
+}
+
+void JavaCommon::TestCheck::checkFinished(JavaCheckResult result)
+{
+ if (result.validity != JavaCheckResult::Validity::Valid)
+ {
+ javaBinaryWasBad(m_parent, result);
+ emit finished();
+ return;
+ }
+ checker.reset(new JavaChecker());
+ connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this,
+ SLOT(checkFinishedWithArgs(JavaCheckResult)));
+ checker->m_path = m_path;
+ checker->m_args = m_args;
+ checker->m_minMem = m_minMem;
+ checker->m_maxMem = m_maxMem;
+ if (result.javaVersion.requiresPermGen())
+ {
+ checker->m_permGen = m_permGen;
+ }
+ checker->performCheck();
+}
+
+void JavaCommon::TestCheck::checkFinishedWithArgs(JavaCheckResult result)
+{
+ if (result.validity == JavaCheckResult::Validity::Valid)
+ {
+ javaWasOk(m_parent, result);
+ emit finished();
+ return;
+ }
+ javaArgsWereBad(m_parent, result);
+ emit finished();
+}
+
diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h
new file mode 100644
index 00000000..ca98145c
--- /dev/null
+++ b/launcher/JavaCommon.h
@@ -0,0 +1,48 @@
+#pragma once
+#include <java/JavaChecker.h>
+
+class QWidget;
+
+/**
+ * Common UI bits for the java pages to use.
+ */
+namespace JavaCommon
+{
+ bool checkJVMArgs(QString args, QWidget *parent);
+
+ // Show a dialog saying that the Java binary was not usable
+ void javaBinaryWasBad(QWidget *parent, JavaCheckResult result);
+ // Show a dialog saying that the Java binary was not usable because of bad options
+ void javaArgsWereBad(QWidget *parent, JavaCheckResult result);
+ // Show a dialog saying that the Java binary was usable
+ void javaWasOk(QWidget *parent, JavaCheckResult result);
+
+ class TestCheck : public QObject
+ {
+ Q_OBJECT
+ public:
+ TestCheck(QWidget *parent, QString path, QString args, int minMem, int maxMem, int permGen)
+ :m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen)
+ {
+ }
+ virtual ~TestCheck() {};
+
+ void run();
+
+ signals:
+ void finished();
+
+ private slots:
+ void checkFinished(JavaCheckResult result);
+ void checkFinishedWithArgs(JavaCheckResult result);
+
+ private:
+ std::shared_ptr<JavaChecker> checker;
+ QWidget *m_parent = nullptr;
+ QString m_path;
+ QString m_args;
+ int m_minMem = 0;
+ int m_maxMem = 0;
+ int m_permGen = 64;
+ };
+}
diff --git a/launcher/Json.cpp b/launcher/Json.cpp
new file mode 100644
index 00000000..37ada1aa
--- /dev/null
+++ b/launcher/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/launcher/Json.h b/launcher/Json.h
new file mode 100644
index 00000000..f2e68f0c
--- /dev/null
+++ b/launcher/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 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
+QJsonDocument requireDocument(const QByteArray &data, const QString &what = "Document");
+/// @throw JsonException
+QJsonDocument requireDocument(const QString &filename, const QString &what = "Document");
+/// @throw JsonException
+QJsonObject requireObject(const QJsonDocument &doc, const QString &what = "Document");
+/// @throw JsonException
+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<> double requireIsType<double>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> bool requireIsType<bool>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> int requireIsType<int>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QJsonObject requireIsType<QJsonObject>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QJsonArray requireIsType<QJsonArray>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QJsonValue requireIsType<QJsonValue>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QByteArray requireIsType<QByteArray>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QDateTime requireIsType<QDateTime>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QVariant requireIsType<QVariant>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QString requireIsType<QString>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QUuid requireIsType<QUuid>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> QDir requireIsType<QDir>(const QJsonValue &value, const QString &what);
+/// @throw JsonException
+template<> 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 (const 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/launcher/KonamiCode.cpp b/launcher/KonamiCode.cpp
new file mode 100644
index 00000000..46a2a0b2
--- /dev/null
+++ b/launcher/KonamiCode.cpp
@@ -0,0 +1,44 @@
+#include "KonamiCode.h"
+
+#include <array>
+#include <QDebug>
+
+namespace {
+const std::array<Qt::Key, 10> konamiCode =
+{
+ {
+ Qt::Key_Up, Qt::Key_Up,
+ Qt::Key_Down, Qt::Key_Down,
+ Qt::Key_Left, Qt::Key_Right,
+ Qt::Key_Left, Qt::Key_Right,
+ Qt::Key_B, Qt::Key_A
+ }
+};
+}
+
+KonamiCode::KonamiCode(QObject* parent) : QObject(parent)
+{
+}
+
+
+void KonamiCode::input(QEvent* event)
+{
+ if( event->type() == QEvent::KeyPress )
+ {
+ QKeyEvent *keyEvent = static_cast<QKeyEvent*>( event );
+ auto key = Qt::Key(keyEvent->key());
+ if(key == konamiCode[m_progress])
+ {
+ m_progress ++;
+ }
+ else
+ {
+ m_progress = 0;
+ }
+ if(m_progress == static_cast<int>(konamiCode.size()))
+ {
+ m_progress = 0;
+ emit triggered();
+ }
+ }
+}
diff --git a/launcher/KonamiCode.h b/launcher/KonamiCode.h
new file mode 100644
index 00000000..3d320ae7
--- /dev/null
+++ b/launcher/KonamiCode.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <QKeyEvent>
+
+class KonamiCode : public QObject
+{
+ Q_OBJECT
+public:
+ KonamiCode(QObject *parent = 0);
+ void input(QEvent *event);
+
+signals:
+ void triggered();
+
+private:
+ int m_progress = 0;
+};
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
new file mode 100644
index 00000000..ee764082
--- /dev/null
+++ b/launcher/LaunchController.cpp
@@ -0,0 +1,353 @@
+#include "LaunchController.h"
+#include "MainWindow.h"
+#include <minecraft/auth/MojangAccountList.h>
+#include "MultiMC.h"
+#include "dialogs/CustomMessageBox.h"
+#include "dialogs/ProfileSelectDialog.h"
+#include "dialogs/ProgressDialog.h"
+#include "dialogs/EditAccountDialog.h"
+#include "InstanceWindow.h"
+#include "BuildConfig.h"
+#include "JavaCommon.h"
+#include <QLineEdit>
+#include <QInputDialog>
+#include <tasks/Task.h>
+#include <minecraft/auth/YggdrasilTask.h>
+#include <launch/steps/TextPrint.h>
+#include <QStringList>
+#include <QHostInfo>
+#include <QList>
+#include <QHostAddress>
+
+LaunchController::LaunchController(QObject *parent) : Task(parent)
+{
+}
+
+void LaunchController::executeTask()
+{
+ if (!m_instance)
+ {
+ emitFailed(tr("No instance specified!"));
+ return;
+ }
+
+ login();
+}
+
+// FIXME: minecraft specific
+void LaunchController::login()
+{
+ JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget);
+
+ // Find an account to use.
+ std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
+ MojangAccountPtr account = accounts->activeAccount();
+ if (accounts->count() <= 0)
+ {
+ // Tell the user they need to log in at least one account in order to play.
+ auto reply = CustomMessageBox::selectable(
+ m_parentWidget, tr("No Accounts"),
+ tr("In order to play Minecraft, you must have at least one Mojang or Minecraft "
+ "account logged in to MultiMC."
+ "Would you like to open the account manager to add an account now?"),
+ QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec();
+
+ if (reply == QMessageBox::Yes)
+ {
+ // Open the account manager.
+ MMC->ShowGlobalSettings(m_parentWidget, "accounts");
+ }
+ }
+ else if (account.get() == nullptr)
+ {
+ // If no default account is set, ask the user which one to use.
+ ProfileSelectDialog selectDialog(tr("Which profile would you like to use?"),
+ ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget);
+
+ selectDialog.exec();
+
+ // Launch the instance with the selected account.
+ account = selectDialog.selectedAccount();
+
+ // If the user said to use the account as default, do that.
+ if (selectDialog.useAsGlobalDefault() && account.get() != nullptr)
+ accounts->setActiveAccount(account->username());
+ }
+
+ // if no account is selected, we bail
+ if (!account.get())
+ {
+ emitFailed(tr("No account selected for launch."));
+ return;
+ }
+
+ // we try empty password first :)
+ QString password;
+ // we loop until the user succeeds in logging in or gives up
+ bool tryagain = true;
+ // the failure. the default failure.
+ const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again. <br /> <br /> This could be caused by a password change.");
+ QString failReason = needLoginAgain;
+
+ while (tryagain)
+ {
+ m_session = std::make_shared<AuthSession>();
+ m_session->wants_online = m_online;
+ auto task = account->login(m_session, password);
+ if (task)
+ {
+ // We'll need to validate the access token to make sure the account
+ // is still logged in.
+ ProgressDialog progDialog(m_parentWidget);
+ if (m_online)
+ {
+ progDialog.setSkipButton(true, tr("Play Offline"));
+ }
+ progDialog.execWithTask(task.get());
+ if (!task->wasSuccessful())
+ {
+ auto failReasonNew = task->failReason();
+ if(failReasonNew == "Invalid token.")
+ {
+ account->invalidateClientToken();
+ failReason = needLoginAgain;
+ }
+ else failReason = failReasonNew;
+ }
+ }
+ switch (m_session->status)
+ {
+ case AuthSession::Undetermined:
+ {
+ qCritical() << "Received undetermined session status during login. Bye.";
+ tryagain = false;
+ emitFailed(tr("Received undetermined session status during login."));
+ break;
+ }
+ case AuthSession::RequiresPassword:
+ {
+ EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField);
+ auto username = m_session->username;
+ auto chopN = [](QString toChop, int N) -> QString
+ {
+ if(toChop.size() > N)
+ {
+ auto left = toChop.left(N);
+ left += QString("\u25CF").repeated(toChop.size() - N);
+ return left;
+ }
+ return toChop;
+ };
+
+ if(username.contains('@'))
+ {
+ auto parts = username.split('@');
+ auto mailbox = chopN(parts[0],3);
+ QString domain = chopN(parts[1], 3);
+ username = mailbox + '@' + domain;
+ }
+ passDialog.setUsername(username);
+ if (passDialog.exec() == QDialog::Accepted)
+ {
+ password = passDialog.password();
+ }
+ else
+ {
+ tryagain = false;
+ }
+ break;
+ }
+ case AuthSession::PlayableOffline:
+ {
+ // we ask the user for a player name
+ bool ok = false;
+ QString usedname = m_session->player_name;
+ QString name = QInputDialog::getText(m_parentWidget, tr("Player name"),
+ tr("Choose your offline mode player name."),
+ QLineEdit::Normal, m_session->player_name, &ok);
+ if (!ok)
+ {
+ tryagain = false;
+ break;
+ }
+ if (name.length())
+ {
+ usedname = name;
+ }
+ m_session->MakeOffline(usedname);
+ // offline flavored game from here :3
+ }
+ case AuthSession::PlayableOnline:
+ {
+ launchInstance();
+ tryagain = false;
+ return;
+ }
+ }
+ }
+ emitFailed(tr("Failed to launch."));
+}
+
+void LaunchController::launchInstance()
+{
+ Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL");
+ Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL");
+
+ if(!m_instance->reloadSettings())
+ {
+ QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile."));
+ emitFailed(tr("Couldn't load the instance profile."));
+ return;
+ }
+
+ m_launcher = m_instance->createLaunchTask(m_session, m_serverToJoin);
+ if (!m_launcher)
+ {
+ emitFailed(tr("Couldn't instantiate a launcher."));
+ return;
+ }
+
+ auto console = qobject_cast<InstanceWindow *>(m_parentWidget);
+ auto showConsole = m_instance->settings()->get("ShowConsole").toBool();
+ if(!console && showConsole)
+ {
+ MMC->showInstanceWindow(m_instance);
+ }
+ connect(m_launcher.get(), &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch);
+ connect(m_launcher.get(), &LaunchTask::succeeded, this, &LaunchController::onSucceeded);
+ connect(m_launcher.get(), &LaunchTask::failed, this, &LaunchController::onFailed);
+ connect(m_launcher.get(), &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested);
+
+ // Prepend Online and Auth Status
+ QString online_mode;
+ if(m_session->wants_online) {
+ online_mode = "online";
+
+ // Prepend Server Status
+ QStringList servers = {"authserver.mojang.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com"};
+ QString resolved_servers = "";
+ QHostInfo host_info;
+
+ for(QString server : servers) {
+ host_info = QHostInfo::fromName(server);
+ resolved_servers = resolved_servers + server + " resolves to:\n [";
+ if(!host_info.addresses().isEmpty()) {
+ for(QHostAddress address : host_info.addresses()) {
+ resolved_servers = resolved_servers + address.toString();
+ if(!host_info.addresses().endsWith(address)) {
+ resolved_servers = resolved_servers + ", ";
+ }
+ }
+ } else {
+ resolved_servers = resolved_servers + "N/A";
+ }
+ resolved_servers = resolved_servers + "]\n\n";
+ }
+ m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::MultiMC));
+ } else {
+ online_mode = "offline";
+ }
+
+ QString auth_server_status;
+ if(m_session->auth_server_online) {
+ auth_server_status = "online";
+ } else {
+ auth_server_status = "offline";
+ }
+
+ m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\nAuthentication server is " + auth_server_status + "\n", MessageLevel::MultiMC));
+
+ // Prepend Version
+ m_launcher->prependStep(new TextPrint(m_launcher.get(), "MultiMC version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::MultiMC));
+ m_launcher->start();
+}
+
+void LaunchController::readyForLaunch()
+{
+ if (!m_profiler)
+ {
+ m_launcher->proceed();
+ return;
+ }
+
+ QString error;
+ if (!m_profiler->check(&error))
+ {
+ m_launcher->abort();
+ QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't start profiler: %1").arg(error));
+ emitFailed("Profiler startup failed!");
+ return;
+ }
+ BaseProfiler *profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this);
+
+ connect(profilerInstance, &BaseProfiler::readyToLaunch, [this](const QString & message)
+ {
+ QMessageBox msg;
+ msg.setText(tr("The game launch is delayed until you press the "
+ "button. This is the right time to setup the profiler, as the "
+ "profiler server is running now.\n\n%1").arg(message));
+ msg.setWindowTitle(tr("Waiting."));
+ msg.setIcon(QMessageBox::Information);
+ msg.addButton(tr("Launch"), QMessageBox::AcceptRole);
+ msg.setModal(true);
+ msg.exec();
+ m_launcher->proceed();
+ });
+ connect(profilerInstance, &BaseProfiler::abortLaunch, [this](const QString & message)
+ {
+ QMessageBox msg;
+ msg.setText(tr("Couldn't start the profiler: %1").arg(message));
+ msg.setWindowTitle(tr("Error"));
+ msg.setIcon(QMessageBox::Critical);
+ msg.addButton(QMessageBox::Ok);
+ msg.setModal(true);
+ msg.exec();
+ m_launcher->abort();
+ emitFailed("Profiler startup failed!");
+ });
+ profilerInstance->beginProfiling(m_launcher);
+}
+
+void LaunchController::onSucceeded()
+{
+ emitSucceeded();
+}
+
+void LaunchController::onFailed(QString reason)
+{
+ if(m_instance->settings()->get("ShowConsoleOnError").toBool())
+ {
+ MMC->showInstanceWindow(m_instance, "console");
+ }
+ emitFailed(reason);
+}
+
+void LaunchController::onProgressRequested(Task* task)
+{
+ ProgressDialog progDialog(m_parentWidget);
+ progDialog.setSkipButton(true, tr("Abort"));
+ m_launcher->proceed();
+ progDialog.execWithTask(task);
+}
+
+bool LaunchController::abort()
+{
+ if(!m_launcher)
+ {
+ return true;
+ }
+ if(!m_launcher->canAbort())
+ {
+ return false;
+ }
+ auto response = CustomMessageBox::selectable(
+ m_parentWidget, tr("Kill Minecraft?"),
+ tr("This can cause the instance to get corrupted and should only be used if Minecraft "
+ "is frozen for some reason"),
+ QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)->exec();
+ if (response == QMessageBox::Yes)
+ {
+ return m_launcher->abort();
+ }
+ return false;
+}
diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h
new file mode 100644
index 00000000..5f177e00
--- /dev/null
+++ b/launcher/LaunchController.h
@@ -0,0 +1,68 @@
+#pragma once
+#include <QObject>
+#include <BaseInstance.h>
+#include <tools/BaseProfiler.h>
+
+#include "minecraft/launch/MinecraftServerTarget.h"
+
+class InstanceWindow;
+class LaunchController: public Task
+{
+ Q_OBJECT
+public:
+ void executeTask() override;
+
+ LaunchController(QObject * parent = nullptr);
+ virtual ~LaunchController(){};
+
+ void setInstance(InstancePtr instance)
+ {
+ m_instance = instance;
+ }
+ InstancePtr instance()
+ {
+ return m_instance;
+ }
+ void setOnline(bool online)
+ {
+ m_online = online;
+ }
+ void setProfiler(BaseProfilerFactory *profiler)
+ {
+ m_profiler = profiler;
+ }
+ void setParentWidget(QWidget * widget)
+ {
+ m_parentWidget = widget;
+ }
+ void setServerToJoin(MinecraftServerTargetPtr serverToJoin)
+ {
+ m_serverToJoin = std::move(serverToJoin);
+ }
+ QString id()
+ {
+ return m_instance->id();
+ }
+ bool abort() override;
+
+private:
+ void login();
+ void launchInstance();
+
+private slots:
+ void readyForLaunch();
+
+ void onSucceeded();
+ void onFailed(QString reason);
+ void onProgressRequested(Task *task);
+
+private:
+ BaseProfilerFactory *m_profiler = nullptr;
+ bool m_online = true;
+ InstancePtr m_instance;
+ QWidget * m_parentWidget = nullptr;
+ InstanceWindow *m_console = nullptr;
+ AuthSessionPtr m_session;
+ shared_qobject_ptr<LaunchTask> m_launcher;
+ MinecraftServerTargetPtr m_serverToJoin;
+};
diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp
new file mode 100644
index 00000000..822c0f04
--- /dev/null
+++ b/launcher/LoggedProcess.cpp
@@ -0,0 +1,176 @@
+#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);
+}
+
+LoggedProcess::~LoggedProcess()
+{
+ if(m_is_detachable)
+ {
+ setProcessState(QProcess::NotRunning);
+ }
+}
+
+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
+}
+
+void LoggedProcess::setDetachable(bool detachable)
+{
+ m_is_detachable = detachable;
+}
diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h
new file mode 100644
index 00000000..e52b8a7b
--- /dev/null
+++ b/launcher/LoggedProcess.h
@@ -0,0 +1,79 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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;
+
+ void setDetachable(bool detachable);
+
+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;
+ bool m_is_detachable = false;
+};
diff --git a/launcher/MMCStrings.cpp b/launcher/MMCStrings.cpp
new file mode 100644
index 00000000..dc91c8d6
--- /dev/null
+++ b/launcher/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/launcher/MMCStrings.h b/launcher/MMCStrings.h
new file mode 100644
index 00000000..48052a00
--- /dev/null
+++ b/launcher/MMCStrings.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <QString>
+
+namespace Strings
+{
+ int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs);
+}
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
new file mode 100644
index 00000000..b25c61e7
--- /dev/null
+++ b/launcher/MMCZip.cpp
@@ -0,0 +1,312 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <quazip.h>
+#include <quazipdir.h>
+#include <quazipfile.h>
+#include <JlCompress.h>
+#include "MMCZip.h"
+#include "FileSystem.h"
+
+#include <QDebug>
+
+// ours
+bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const JlCompress::FilterFunction 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 && !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 (!JlCompress::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;
+}
+
+// ours
+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))
+ {
+ zipOut.close();
+ QFile::remove(targetJarPath);
+ qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar.";
+ return false;
+ }
+ }
+ else if (mod.type() == Mod::MOD_SINGLEFILE)
+ {
+ // FIXME: buggy - does not work with addedFiles
+ auto filename = mod.filename();
+ if (!JlCompress::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)
+ {
+ // FIXME: buggy - does not work with addedFiles
+ auto filename = mod.filename();
+ QString what_to_zip = filename.absoluteFilePath();
+ QDir dir(what_to_zip);
+ dir.cdUp();
+ QString parent_dir = dir.absolutePath();
+ if (!JlCompress::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();
+ }
+ else
+ {
+ // Make sure we do not continue launching when something is missing or undefined...
+ zipOut.close();
+ QFile::remove(targetJarPath);
+ qCritical() << "Failed to add unknown mod type" << mod.filename().fileName() << "to the jar.";
+ return false;
+ }
+ }
+
+ if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key){return !key.contains("META-INF");}))
+ {
+ 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;
+}
+
+// ours
+QString MMCZip::findFolderOfFileInZip(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 = findFolderOfFileInZip(zip, what, root + fileName);
+ if(!result.isEmpty())
+ {
+ return result;
+ }
+ }
+ return QString();
+}
+
+// ours
+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();
+}
+
+
+// ours
+nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
+{
+ QDir directory(target);
+ QStringList extracted;
+
+ qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target;
+ auto numEntries = zip->getEntriesCount();
+ if(numEntries < 0) {
+ qWarning() << "Failed to enumerate files in archive";
+ return nonstd::nullopt;
+ }
+ else if(numEntries == 0) {
+ qDebug() << "Extracting empty archives seems odd...";
+ return extracted;
+ }
+ else if (!zip->goToFirstFile())
+ {
+ qWarning() << "Failed to seek to first file in zip";
+ return nonstd::nullopt;
+ }
+
+ 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 (!JlCompress::extractFile(zip, "", absFilePath))
+ {
+ qWarning() << "Failed to extract file" << name << "to" << absFilePath;
+ JlCompress::removeFile(extracted);
+ return nonstd::nullopt;
+ }
+ extracted.append(absFilePath);
+ qDebug() << "Extracted file" << name;
+ } while (zip->goToNextFile());
+ return extracted;
+}
+
+// ours
+bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target)
+{
+ return JlCompress::extractFile(zip, file, target);
+}
+
+// ours
+nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
+{
+ QuaZip zip(fileCompressed);
+ if (!zip.open(QuaZip::mdUnzip))
+ {
+ // check if this is a minimum size empty zip file...
+ QFileInfo fileInfo(fileCompressed);
+ if(fileInfo.size() == 22) {
+ return QStringList();
+ }
+ qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
+ return nonstd::nullopt;
+ }
+ return MMCZip::extractSubDir(&zip, "", dir);
+}
+
+// ours
+nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
+{
+ QuaZip zip(fileCompressed);
+ if (!zip.open(QuaZip::mdUnzip))
+ {
+ // check if this is a minimum size empty zip file...
+ QFileInfo fileInfo(fileCompressed);
+ if(fileInfo.size() == 22) {
+ return QStringList();
+ }
+ qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
+ return nonstd::nullopt;
+ }
+ return MMCZip::extractSubDir(&zip, subdir, dir);
+}
+
+// ours
+bool MMCZip::extractFile(QString fileCompressed, QString file, QString target)
+{
+ QuaZip zip(fileCompressed);
+ if (!zip.open(QuaZip::mdUnzip))
+ {
+ // check if this is a minimum size empty zip file...
+ QFileInfo fileInfo(fileCompressed);
+ if(fileInfo.size() == 22) {
+ return true;
+ }
+ qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
+ return false;
+ }
+ return MMCZip::extractRelFile(&zip, file, target);
+}
diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h
new file mode 100644
index 00000000..9c47fa11
--- /dev/null
+++ b/launcher/MMCZip.h
@@ -0,0 +1,92 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QFileInfo>
+#include <QSet>
+#include "minecraft/mod/Mod.h"
+#include <functional>
+
+#include <JlCompress.h>
+#include <nonstd/optional>
+
+namespace MMCZip
+{
+
+ /**
+ * Merge two zip files, using a filter function
+ */
+ bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained,
+ const JlCompress::FilterFunction filter = nullptr);
+
+ /**
+ * take a source jar, add mods to it, resulting in target jar
+ */
+ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods);
+
+ /**
+ * Find a single file in archive by file name (not path)
+ *
+ * \return the path prefix where the file is
+ */
+ QString findFolderOfFileInZip(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 findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString());
+
+ /**
+ * Extract a subdirectory from an archive
+ */
+ nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
+
+ bool extractRelFile(QuaZip *zip, const QString & file, const QString &target);
+
+ /**
+ * 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.
+ */
+ nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir);
+
+ /**
+ * Extract a subdirectory from an archive
+ *
+ * \param fileCompressed The name of the archive.
+ * \param subdir The directory within the archive to extract
+ * \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.
+ */
+ nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
+
+ /**
+ * Extract a single file from an archive into a directory
+ *
+ * \param fileCompressed The name of the archive.
+ * \param file The file within the archive to extract
+ * \param dir The directory to extract to, the current directory if left empty.
+ * \return true for success or false for failure
+ */
+ bool extractFile(QString fileCompressed, QString file, QString dir);
+
+}
diff --git a/launcher/MainWindow.cpp b/launcher/MainWindow.cpp
new file mode 100644
index 00000000..9225193e
--- /dev/null
+++ b/launcher/MainWindow.cpp
@@ -0,0 +1,1952 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Authors: Andrew Okin
+ * Peterix
+ * 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 "MultiMC.h"
+#include "BuildConfig.h"
+
+#include "MainWindow.h"
+
+#include <QtCore/QVariant>
+#include <QtCore/QUrl>
+#include <QtCore/QDir>
+#include <QtCore/QFileInfo>
+
+#include <QtGui/QKeyEvent>
+
+#include <QtWidgets/QAction>
+#include <QtWidgets/QApplication>
+#include <QtWidgets/QButtonGroup>
+#include <QtWidgets/QHBoxLayout>
+#include <QtWidgets/QHeaderView>
+#include <QtWidgets/QMainWindow>
+#include <QtWidgets/QStatusBar>
+#include <QtWidgets/QToolBar>
+#include <QtWidgets/QWidget>
+#include <QtWidgets/QMenu>
+#include <QtWidgets/QMessageBox>
+#include <QtWidgets/QInputDialog>
+#include <QtWidgets/QLabel>
+#include <QtWidgets/QToolButton>
+#include <QtWidgets/QWidgetAction>
+#include <QtWidgets/QProgressDialog>
+#include <QtWidgets/QShortcut>
+
+#include <BaseInstance.h>
+#include <Env.h>
+#include <InstanceList.h>
+#include <MMCZip.h>
+#include <icons/IconList.h>
+#include <java/JavaUtils.h>
+#include <java/JavaInstallList.h>
+#include <launch/LaunchTask.h>
+#include <minecraft/auth/MojangAccountList.h>
+#include <SkinUtils.h>
+#include <BuildConfig.h>
+#include <net/NetJob.h>
+#include <net/Download.h>
+#include <news/NewsChecker.h>
+#include <notifications/NotificationChecker.h>
+#include <tools/BaseProfiler.h>
+#include <updater/DownloadTask.h>
+#include <updater/UpdateChecker.h>
+#include <DesktopServices.h>
+#include "InstanceWindow.h"
+#include "InstancePageProvider.h"
+#include "InstanceProxyModel.h"
+#include "JavaCommon.h"
+#include "LaunchController.h"
+#include "groupview/GroupView.h"
+#include "groupview/InstanceDelegate.h"
+#include "widgets/LabeledToolButton.h"
+#include "widgets/ServerStatus.h"
+#include "dialogs/NewInstanceDialog.h"
+#include "dialogs/ProgressDialog.h"
+#include "dialogs/AboutDialog.h"
+#include "dialogs/VersionSelectDialog.h"
+#include "dialogs/CustomMessageBox.h"
+#include "dialogs/IconPickerDialog.h"
+#include "dialogs/CopyInstanceDialog.h"
+#include "dialogs/UpdateDialog.h"
+#include "dialogs/EditAccountDialog.h"
+#include "dialogs/NotificationDialog.h"
+#include "dialogs/ExportInstanceDialog.h"
+#include <InstanceImportTask.h>
+#include "UpdateController.h"
+#include "KonamiCode.h"
+#include <InstanceCopyTask.h>
+
+// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code
+template <typename T>
+class Translated
+{
+public:
+ Translated(){}
+ Translated(QWidget *parent)
+ {
+ m_contained = new T(parent);
+ }
+ void setTooltipId(const char * tooltip)
+ {
+ m_tooltip = tooltip;
+ }
+ void setTextId(const char * text)
+ {
+ m_text = text;
+ }
+ operator T*()
+ {
+ return m_contained;
+ }
+ T * operator->()
+ {
+ return m_contained;
+ }
+ void retranslate()
+ {
+ if(m_text)
+ {
+ m_contained->setText(QApplication::translate("MainWindow", m_text));
+ }
+ if(m_tooltip)
+ {
+ m_contained->setToolTip(QApplication::translate("MainWindow", m_tooltip));
+ }
+ }
+private:
+ T * m_contained = nullptr;
+ const char * m_text = nullptr;
+ const char * m_tooltip = nullptr;
+};
+using TranslatedAction = Translated<QAction>;
+using TranslatedToolButton = Translated<QToolButton>;
+
+class TranslatedToolbar
+{
+public:
+ TranslatedToolbar(){}
+ TranslatedToolbar(QWidget *parent)
+ {
+ m_contained = new QToolBar(parent);
+ }
+ void setWindowTitleId(const char * title)
+ {
+ m_title = title;
+ }
+ operator QToolBar*()
+ {
+ return m_contained;
+ }
+ QToolBar * operator->()
+ {
+ return m_contained;
+ }
+ void retranslate()
+ {
+ if(m_title)
+ {
+ m_contained->setWindowTitle(QApplication::translate("MainWindow", m_title));
+ }
+ }
+private:
+ QToolBar * m_contained = nullptr;
+ const char * m_title = nullptr;
+};
+
+class MainWindow::Ui
+{
+public:
+ TranslatedAction actionAddInstance;
+ //TranslatedAction actionRefresh;
+ TranslatedAction actionCheckUpdate;
+ TranslatedAction actionSettings;
+ TranslatedAction actionPatreon;
+ TranslatedAction actionMoreNews;
+ TranslatedAction actionManageAccounts;
+ TranslatedAction actionLaunchInstance;
+ TranslatedAction actionRenameInstance;
+ TranslatedAction actionChangeInstGroup;
+ TranslatedAction actionChangeInstIcon;
+ TranslatedAction actionEditInstNotes;
+ TranslatedAction actionEditInstance;
+ TranslatedAction actionWorlds;
+ TranslatedAction actionViewSelectedInstFolder;
+ TranslatedAction actionViewSelectedMCFolder;
+ TranslatedAction actionDeleteInstance;
+ TranslatedAction actionConfig_Folder;
+ TranslatedAction actionCAT;
+ TranslatedAction actionCopyInstance;
+ TranslatedAction actionLaunchInstanceOffline;
+ TranslatedAction actionScreenshots;
+ TranslatedAction actionExportInstance;
+ QVector<TranslatedAction *> all_actions;
+
+ LabeledToolButton *renameButton = nullptr;
+ LabeledToolButton *changeIconButton = nullptr;
+
+ QMenu * foldersMenu = nullptr;
+ TranslatedToolButton foldersMenuButton;
+ TranslatedAction actionViewInstanceFolder;
+ TranslatedAction actionViewCentralModsFolder;
+
+ QMenu * helpMenu = nullptr;
+ TranslatedToolButton helpMenuButton;
+ TranslatedAction actionReportBug;
+ TranslatedAction actionDISCORD;
+ TranslatedAction actionREDDIT;
+ TranslatedAction actionAbout;
+
+ QVector<TranslatedToolButton *> all_toolbuttons;
+
+ QWidget *centralWidget = nullptr;
+ QHBoxLayout *horizontalLayout = nullptr;
+ QStatusBar *statusBar = nullptr;
+
+ TranslatedToolbar mainToolBar;
+ TranslatedToolbar instanceToolBar;
+ TranslatedToolbar newsToolBar;
+ QVector<TranslatedToolbar *> all_toolbars;
+ bool m_kill = false;
+
+ void updateLaunchAction()
+ {
+ if(m_kill)
+ {
+ actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Kill"));
+ actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance"));
+ }
+ else
+ {
+ actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch"));
+ actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance."));
+ }
+ actionLaunchInstance.retranslate();
+ }
+ void setLaunchAction(bool kill)
+ {
+ m_kill = kill;
+ updateLaunchAction();
+ }
+
+ void createMainToolbar(QMainWindow *MainWindow)
+ {
+ mainToolBar = TranslatedToolbar(MainWindow);
+ mainToolBar->setObjectName(QStringLiteral("mainToolBar"));
+ mainToolBar->setMovable(false);
+ mainToolBar->setAllowedAreas(Qt::TopToolBarArea);
+ mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ mainToolBar->setFloatable(false);
+ mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar"));
+
+ actionAddInstance = TranslatedAction(MainWindow);
+ actionAddInstance->setObjectName(QStringLiteral("actionAddInstance"));
+ actionAddInstance->setIcon(MMC->getThemedIcon("new"));
+ actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instance"));
+ actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance."));
+ all_actions.append(&actionAddInstance);
+ mainToolBar->addAction(actionAddInstance);
+
+ mainToolBar->addSeparator();
+
+ foldersMenu = new QMenu(MainWindow);
+ foldersMenu->setToolTipsVisible(true);
+
+ actionViewInstanceFolder = TranslatedAction(MainWindow);
+ actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder"));
+ actionViewInstanceFolder->setIcon(MMC->getThemedIcon("viewfolder"));
+ actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Instance Folder"));
+ actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser."));
+ all_actions.append(&actionViewInstanceFolder);
+ foldersMenu->addAction(actionViewInstanceFolder);
+
+ actionViewCentralModsFolder = TranslatedAction(MainWindow);
+ actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder"));
+ actionViewCentralModsFolder->setIcon(MMC->getThemedIcon("centralmods"));
+ actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Central Mods Folder"));
+ actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser."));
+ all_actions.append(&actionViewCentralModsFolder);
+ foldersMenu->addAction(actionViewCentralModsFolder);
+
+ foldersMenuButton = TranslatedToolButton(MainWindow);
+ foldersMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Folders"));
+ foldersMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open one of the folders shared between instances."));
+ foldersMenuButton->setMenu(foldersMenu);
+ foldersMenuButton->setPopupMode(QToolButton::InstantPopup);
+ foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ foldersMenuButton->setIcon(MMC->getThemedIcon("viewfolder"));
+ foldersMenuButton->setFocusPolicy(Qt::NoFocus);
+ all_toolbuttons.append(&foldersMenuButton);
+ QWidgetAction* foldersButtonAction = new QWidgetAction(MainWindow);
+ foldersButtonAction->setDefaultWidget(foldersMenuButton);
+ mainToolBar->addAction(foldersButtonAction);
+
+ actionSettings = TranslatedAction(MainWindow);
+ actionSettings->setObjectName(QStringLiteral("actionSettings"));
+ actionSettings->setIcon(MMC->getThemedIcon("settings"));
+ actionSettings->setMenuRole(QAction::PreferencesRole);
+ actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Settings"));
+ actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings."));
+ all_actions.append(&actionSettings);
+ mainToolBar->addAction(actionSettings);
+
+ helpMenu = new QMenu(MainWindow);
+ helpMenu->setToolTipsVisible(true);
+
+ if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) {
+ actionReportBug = TranslatedAction(MainWindow);
+ actionReportBug->setObjectName(QStringLiteral("actionReportBug"));
+ actionReportBug->setIcon(MMC->getThemedIcon("bug"));
+ actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a Bug"));
+ actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with MultiMC."));
+ all_actions.append(&actionReportBug);
+ helpMenu->addAction(actionReportBug);
+ }
+
+ if (!BuildConfig.DISCORD_URL.isEmpty()) {
+ actionDISCORD = TranslatedAction(MainWindow);
+ actionDISCORD->setObjectName(QStringLiteral("actionDISCORD"));
+ actionDISCORD->setIcon(MMC->getThemedIcon("discord"));
+ actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord"));
+ actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open MultiMC discord voice chat."));
+ all_actions.append(&actionDISCORD);
+ helpMenu->addAction(actionDISCORD);
+ }
+
+ if (!BuildConfig.SUBREDDIT_URL.isEmpty()) {
+ actionREDDIT = TranslatedAction(MainWindow);
+ actionREDDIT->setObjectName(QStringLiteral("actionREDDIT"));
+ actionREDDIT->setIcon(MMC->getThemedIcon("reddit-alien"));
+ actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Reddit"));
+ actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open MultiMC subreddit."));
+ all_actions.append(&actionREDDIT);
+ helpMenu->addAction(actionREDDIT);
+ }
+
+ actionAbout = TranslatedAction(MainWindow);
+ actionAbout->setObjectName(QStringLiteral("actionAbout"));
+ actionAbout->setIcon(MMC->getThemedIcon("about"));
+ actionAbout->setMenuRole(QAction::AboutRole);
+ actionAbout.setTextId(QT_TRANSLATE_NOOP("MainWindow", "About MultiMC"));
+ actionAbout.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View information about MultiMC."));
+ all_actions.append(&actionAbout);
+ helpMenu->addAction(actionAbout);
+
+ helpMenuButton = TranslatedToolButton(MainWindow);
+ helpMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Help"));
+ helpMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Get help with MultiMC or Minecraft."));
+ helpMenuButton->setMenu(helpMenu);
+ helpMenuButton->setPopupMode(QToolButton::InstantPopup);
+ helpMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ helpMenuButton->setIcon(MMC->getThemedIcon("help"));
+ helpMenuButton->setFocusPolicy(Qt::NoFocus);
+ all_toolbuttons.append(&helpMenuButton);
+ QWidgetAction* helpButtonAction = new QWidgetAction(MainWindow);
+ helpButtonAction->setDefaultWidget(helpMenuButton);
+ mainToolBar->addAction(helpButtonAction);
+
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ actionCheckUpdate = TranslatedAction(MainWindow);
+ actionCheckUpdate->setObjectName(QStringLiteral("actionCheckUpdate"));
+ actionCheckUpdate->setIcon(MMC->getThemedIcon("checkupdate"));
+ actionCheckUpdate.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Update"));
+ actionCheckUpdate.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Check for new updates for MultiMC."));
+ all_actions.append(&actionCheckUpdate);
+ mainToolBar->addAction(actionCheckUpdate);
+ }
+
+ mainToolBar->addSeparator();
+
+ actionPatreon = TranslatedAction(MainWindow);
+ actionPatreon->setObjectName(QStringLiteral("actionPatreon"));
+ actionPatreon->setIcon(MMC->getThemedIcon("patreon"));
+ actionPatreon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Support MultiMC"));
+ actionPatreon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the MultiMC Patreon page."));
+ all_actions.append(&actionPatreon);
+ mainToolBar->addAction(actionPatreon);
+
+ actionCAT = TranslatedAction(MainWindow);
+ actionCAT->setObjectName(QStringLiteral("actionCAT"));
+ actionCAT->setCheckable(true);
+ actionCAT->setIcon(MMC->getThemedIcon("cat"));
+ actionCAT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Meow"));
+ actionCAT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "It's a fluffy kitty :3"));
+ actionCAT->setPriority(QAction::LowPriority);
+ all_actions.append(&actionCAT);
+ mainToolBar->addAction(actionCAT);
+
+ // profile menu and its actions
+ actionManageAccounts = TranslatedAction(MainWindow);
+ actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts"));
+ actionManageAccounts.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage Accounts"));
+ // FIXME: no tooltip!
+ actionManageAccounts->setCheckable(false);
+ actionManageAccounts->setIcon(MMC->getThemedIcon("accounts"));
+ all_actions.append(&actionManageAccounts);
+
+ all_toolbars.append(&mainToolBar);
+ MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar);
+ }
+
+ void createStatusBar(QMainWindow *MainWindow)
+ {
+ statusBar = new QStatusBar(MainWindow);
+ statusBar->setObjectName(QStringLiteral("statusBar"));
+ MainWindow->setStatusBar(statusBar);
+ }
+
+ void createNewsToolbar(QMainWindow *MainWindow)
+ {
+ newsToolBar = TranslatedToolbar(MainWindow);
+ newsToolBar->setObjectName(QStringLiteral("newsToolBar"));
+ newsToolBar->setMovable(false);
+ newsToolBar->setAllowedAreas(Qt::BottomToolBarArea);
+ newsToolBar->setIconSize(QSize(16, 16));
+ newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ newsToolBar->setFloatable(false);
+ newsToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "News Toolbar"));
+
+ actionMoreNews = TranslatedAction(MainWindow);
+ actionMoreNews->setObjectName(QStringLiteral("actionMoreNews"));
+ actionMoreNews->setIcon(MMC->getThemedIcon("news"));
+ actionMoreNews.setTextId(QT_TRANSLATE_NOOP("MainWindow", "More news..."));
+ actionMoreNews.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the MultiMC development blog to read more news about MultiMC."));
+ all_actions.append(&actionMoreNews);
+ newsToolBar->addAction(actionMoreNews);
+
+ all_toolbars.append(&newsToolBar);
+ MainWindow->addToolBar(Qt::BottomToolBarArea, newsToolBar);
+ }
+
+ void createInstanceToolbar(QMainWindow *MainWindow)
+ {
+ instanceToolBar = TranslatedToolbar(MainWindow);
+ instanceToolBar->setObjectName(QStringLiteral("instanceToolBar"));
+ // disabled until we have an instance selected
+ instanceToolBar->setEnabled(false);
+ instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea);
+ instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly);
+ instanceToolBar->setFloatable(false);
+ instanceToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "Instance Toolbar"));
+
+ // NOTE: not added to toolbar, but used for instance context menu (right click)
+ actionChangeInstIcon = TranslatedAction(MainWindow);
+ actionChangeInstIcon->setObjectName(QStringLiteral("actionChangeInstIcon"));
+ actionChangeInstIcon->setIcon(QIcon(":/icons/instances/infinity"));
+ actionChangeInstIcon->setIconVisibleInMenu(true);
+ actionChangeInstIcon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Icon"));
+ actionChangeInstIcon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's icon."));
+ all_actions.append(&actionChangeInstIcon);
+
+ changeIconButton = new LabeledToolButton(MainWindow);
+ changeIconButton->setObjectName(QStringLiteral("changeIconButton"));
+ changeIconButton->setIcon(MMC->getThemedIcon("news"));
+ changeIconButton->setToolTip(actionChangeInstIcon->toolTip());
+ changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ instanceToolBar->addWidget(changeIconButton);
+
+ // NOTE: not added to toolbar, but used for instance context menu (right click)
+ actionRenameInstance = TranslatedAction(MainWindow);
+ actionRenameInstance->setObjectName(QStringLiteral("actionRenameInstance"));
+ actionRenameInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Rename"));
+ actionRenameInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Rename the selected instance."));
+ all_actions.append(&actionRenameInstance);
+
+ // the rename label is inside the rename tool button
+ renameButton = new LabeledToolButton(MainWindow);
+ renameButton->setObjectName(QStringLiteral("renameButton"));
+ renameButton->setToolTip(actionRenameInstance->toolTip());
+ renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ instanceToolBar->addWidget(renameButton);
+
+ instanceToolBar->addSeparator();
+
+ actionLaunchInstance = TranslatedAction(MainWindow);
+ actionLaunchInstance->setObjectName(QStringLiteral("actionLaunchInstance"));
+ all_actions.append(&actionLaunchInstance);
+ instanceToolBar->addAction(actionLaunchInstance);
+
+ actionLaunchInstanceOffline = TranslatedAction(MainWindow);
+ actionLaunchInstanceOffline->setObjectName(QStringLiteral("actionLaunchInstanceOffline"));
+ actionLaunchInstanceOffline.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch Offline"));
+ actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode."));
+ all_actions.append(&actionLaunchInstanceOffline);
+ instanceToolBar->addAction(actionLaunchInstanceOffline);
+
+ instanceToolBar->addSeparator();
+
+ actionEditInstance = TranslatedAction(MainWindow);
+ actionEditInstance->setObjectName(QStringLiteral("actionEditInstance"));
+ actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Instance"));
+ actionEditInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the instance settings, mods and versions."));
+ all_actions.append(&actionEditInstance);
+ instanceToolBar->addAction(actionEditInstance);
+
+ actionEditInstNotes = TranslatedAction(MainWindow);
+ actionEditInstNotes->setObjectName(QStringLiteral("actionEditInstNotes"));
+ actionEditInstNotes.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Notes"));
+ actionEditInstNotes.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Edit the notes for the selected instance."));
+ all_actions.append(&actionEditInstNotes);
+ instanceToolBar->addAction(actionEditInstNotes);
+
+ actionWorlds = TranslatedAction(MainWindow);
+ actionWorlds->setObjectName(QStringLiteral("actionWorlds"));
+ actionWorlds.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Worlds"));
+ actionWorlds.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View the worlds of this instance."));
+ all_actions.append(&actionWorlds);
+ instanceToolBar->addAction(actionWorlds);
+
+ actionScreenshots = TranslatedAction(MainWindow);
+ actionScreenshots->setObjectName(QStringLiteral("actionScreenshots"));
+ actionScreenshots.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage Screenshots"));
+ actionScreenshots.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View and upload screenshots for this instance."));
+ all_actions.append(&actionScreenshots);
+ instanceToolBar->addAction(actionScreenshots);
+
+ actionChangeInstGroup = TranslatedAction(MainWindow);
+ actionChangeInstGroup->setObjectName(QStringLiteral("actionChangeInstGroup"));
+ actionChangeInstGroup.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Group"));
+ actionChangeInstGroup.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's group."));
+ all_actions.append(&actionChangeInstGroup);
+ instanceToolBar->addAction(actionChangeInstGroup);
+
+ instanceToolBar->addSeparator();
+
+ actionViewSelectedMCFolder = TranslatedAction(MainWindow);
+ actionViewSelectedMCFolder->setObjectName(QStringLiteral("actionViewSelectedMCFolder"));
+ actionViewSelectedMCFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Minecraft Folder"));
+ actionViewSelectedMCFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's minecraft folder in a file browser."));
+ all_actions.append(&actionViewSelectedMCFolder);
+ instanceToolBar->addAction(actionViewSelectedMCFolder);
+
+ actionConfig_Folder = TranslatedAction(MainWindow);
+ actionConfig_Folder->setObjectName(QStringLiteral("actionConfig_Folder"));
+ actionConfig_Folder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Config Folder"));
+ actionConfig_Folder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance's config folder."));
+ all_actions.append(&actionConfig_Folder);
+ instanceToolBar->addAction(actionConfig_Folder);
+
+ actionViewSelectedInstFolder = TranslatedAction(MainWindow);
+ actionViewSelectedInstFolder->setObjectName(QStringLiteral("actionViewSelectedInstFolder"));
+ actionViewSelectedInstFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Instance Folder"));
+ actionViewSelectedInstFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's root folder in a file browser."));
+ all_actions.append(&actionViewSelectedInstFolder);
+ instanceToolBar->addAction(actionViewSelectedInstFolder);
+
+ instanceToolBar->addSeparator();
+
+ actionExportInstance = TranslatedAction(MainWindow);
+ actionExportInstance->setObjectName(QStringLiteral("actionExportInstance"));
+ actionExportInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Export Instance"));
+ // FIXME: missing tooltip
+ all_actions.append(&actionExportInstance);
+ instanceToolBar->addAction(actionExportInstance);
+
+ actionDeleteInstance = TranslatedAction(MainWindow);
+ actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance"));
+ actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Delete"));
+ actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance."));
+ all_actions.append(&actionDeleteInstance);
+ instanceToolBar->addAction(actionDeleteInstance);
+
+ actionCopyInstance = TranslatedAction(MainWindow);
+ actionCopyInstance->setObjectName(QStringLiteral("actionCopyInstance"));
+ actionCopyInstance->setIcon(MMC->getThemedIcon("copy"));
+ actionCopyInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Copy Instance"));
+ actionCopyInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Copy the selected instance."));
+ all_actions.append(&actionCopyInstance);
+ instanceToolBar->addAction(actionCopyInstance);
+
+ all_toolbars.append(&instanceToolBar);
+ MainWindow->addToolBar(Qt::RightToolBarArea, instanceToolBar);
+ }
+
+ void setupUi(QMainWindow *MainWindow)
+ {
+ if (MainWindow->objectName().isEmpty())
+ {
+ MainWindow->setObjectName(QStringLiteral("MainWindow"));
+ }
+ MainWindow->resize(800, 600);
+ MainWindow->setWindowIcon(MMC->getThemedIcon("logo"));
+ MainWindow->setWindowTitle("MultiMC 5");
+#ifndef QT_NO_ACCESSIBILITY
+ MainWindow->setAccessibleName("MultiMC");
+#endif
+
+ createMainToolbar(MainWindow);
+
+ centralWidget = new QWidget(MainWindow);
+ centralWidget->setObjectName(QStringLiteral("centralWidget"));
+ horizontalLayout = new QHBoxLayout(centralWidget);
+ horizontalLayout->setSpacing(0);
+ horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+ horizontalLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
+ horizontalLayout->setContentsMargins(0, 0, 0, 0);
+ MainWindow->setCentralWidget(centralWidget);
+
+ createStatusBar(MainWindow);
+ createNewsToolbar(MainWindow);
+ createInstanceToolbar(MainWindow);
+
+ retranslateUi(MainWindow);
+
+ QMetaObject::connectSlotsByName(MainWindow);
+ } // setupUi
+
+ void retranslateUi(QMainWindow *MainWindow)
+ {
+ QString winTitle = tr("MultiMC 5 - Version %1").arg(BuildConfig.printableVersionString());
+ if (!BuildConfig.BUILD_PLATFORM.isEmpty())
+ {
+ winTitle += tr(" on %1", "on platform, as in operating system").arg(BuildConfig.BUILD_PLATFORM);
+ }
+ MainWindow->setWindowTitle(winTitle);
+ // all the actions
+ for(auto * item: all_actions)
+ {
+ item->retranslate();
+ }
+ for(auto * item: all_toolbars)
+ {
+ item->retranslate();
+ }
+ for(auto * item: all_toolbuttons)
+ {
+ item->retranslate();
+ }
+ // submenu buttons
+ foldersMenuButton->setText(tr("Folders"));
+ helpMenuButton->setText(tr("Help"));
+ } // retranslateUi
+};
+
+MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow::Ui)
+{
+ ui->setupUi(this);
+
+ // OSX magic.
+ setUnifiedTitleAndToolBarOnMac(true);
+
+ // Global shortcuts
+ {
+ // FIXME: This is kinda weird. and bad. We need some kind of managed shutdown.
+ auto q = new QShortcut(QKeySequence::Quit, this);
+ connect(q, SIGNAL(activated()), qApp, SLOT(quit()));
+ }
+
+ // Konami Code
+ {
+ secretEventFilter = new KonamiCode(this);
+ connect(secretEventFilter, &KonamiCode::triggered, this, &MainWindow::konamiTriggered);
+ }
+
+ // Add the news label to the news toolbar.
+ {
+ m_newsChecker.reset(new NewsChecker(BuildConfig.NEWS_RSS_URL));
+ newsLabel = new QToolButton();
+ newsLabel->setIcon(MMC->getThemedIcon("news"));
+ newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ newsLabel->setFocusPolicy(Qt::NoFocus);
+ ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel);
+ QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked);
+ QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel);
+ updateNewsLabel();
+ }
+
+ // Create the instance list widget
+ {
+ view = new GroupView(ui->centralWidget);
+
+ view->setSelectionMode(QAbstractItemView::SingleSelection);
+ // FIXME: leaks ListViewDelegate
+ view->setItemDelegate(new ListViewDelegate(this));
+ view->setFrameShape(QFrame::NoFrame);
+ // do not show ugly blue border on the mac
+ view->setAttribute(Qt::WA_MacShowFocusRect, false);
+
+ view->installEventFilter(this);
+ view->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(view, &QWidget::customContextMenuRequested, this, &MainWindow::showInstanceContextMenu);
+ connect(view, &GroupView::droppedURLs, this, &MainWindow::droppedURLs, Qt::QueuedConnection);
+
+ proxymodel = new InstanceProxyModel(this);
+ proxymodel->setSourceModel(MMC->instances().get());
+ proxymodel->sort(0);
+ connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged);
+
+ view->setModel(proxymodel);
+ view->setSourceOfGroupCollapseStatus([](const QString & groupName)->bool {
+ return MMC->instances()->isGroupCollapsed(groupName);
+ });
+ connect(view, &GroupView::groupStateChanged, MMC->instances().get(), &InstanceList::on_GroupStateChanged);
+ ui->horizontalLayout->addWidget(view);
+ }
+ // The cat background
+ {
+ bool cat_enable = MMC->settings()->get("TheCat").toBool();
+ ui->actionCAT->setChecked(cat_enable);
+ // NOTE: calling the operator like that is an ugly hack to appease ancient gcc...
+ connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool)));
+ setCatBackground(cat_enable);
+ }
+ // start instance when double-clicked
+ connect(view, &GroupView::activated, this, &MainWindow::instanceActivated);
+
+ // track the selection -- update the instance toolbar
+ connect(view->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::instanceChanged);
+
+ // track icon changes and update the toolbar!
+ connect(MMC->icons().get(), &IconList::iconUpdated, this, &MainWindow::iconUpdated);
+
+ // model reset -> selection is invalid. All the instance pointers are wrong.
+ connect(MMC->instances().get(), &InstanceList::dataIsInvalid, this, &MainWindow::selectionBad);
+
+ // handle newly added instances
+ connect(MMC->instances().get(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest);
+
+ // When the global settings page closes, we want to know about it and update our state
+ connect(MMC, &MultiMC::globalSettingsClosed, this, &MainWindow::globalSettingsClosed);
+
+ m_statusLeft = new QLabel(tr("No instance selected"), this);
+ m_statusCenter = new QLabel(tr("Total playtime: 0s."), this);
+ m_statusRight = new ServerStatus(this);
+ statusBar()->addPermanentWidget(m_statusLeft, 1);
+ statusBar()->addPermanentWidget(m_statusCenter, 1);
+ statusBar()->addPermanentWidget(m_statusRight, 0);
+
+ // Add "manage accounts" button, right align
+ QWidget *spacer = new QWidget();
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ ui->mainToolBar->addWidget(spacer);
+
+ accountMenu = new QMenu(this);
+
+ repopulateAccountsMenu();
+
+ accountMenuButton = new QToolButton(this);
+ accountMenuButton->setMenu(accountMenu);
+ accountMenuButton->setPopupMode(QToolButton::InstantPopup);
+ accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ accountMenuButton->setIcon(MMC->getThemedIcon("noaccount"));
+
+ QWidgetAction *accountMenuButtonAction = new QWidgetAction(this);
+ accountMenuButtonAction->setDefaultWidget(accountMenuButton);
+
+ ui->mainToolBar->addAction(accountMenuButtonAction);
+
+ // Update the menu when the active account changes.
+ // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit.
+ // Template hell sucks...
+ connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this]
+ {
+ activeAccountChanged();
+ });
+ connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this]
+ {
+ repopulateAccountsMenu();
+ });
+
+ // Show initial account
+ activeAccountChanged();
+
+ auto accounts = MMC->accounts();
+
+ QList<Net::Download::Ptr> skin_dls;
+ for (int i = 0; i < accounts->count(); i++)
+ {
+ auto account = accounts->at(i);
+ if (!account)
+ {
+ qWarning() << "Null account at index" << i;
+ continue;
+ }
+ for (auto profile : account->profiles())
+ {
+ auto meta = Env::getInstance().metacache()->resolveEntry("skins", profile.id + ".png");
+ auto action = Net::Download::makeCached(QUrl(BuildConfig.SKINS_BASE + profile.id + ".png"), meta);
+ skin_dls.append(action);
+ meta->setStale(true);
+ }
+ }
+ if (!skin_dls.isEmpty())
+ {
+ auto job = new NetJob("Startup player skins download");
+ connect(job, &NetJob::succeeded, this, &MainWindow::skinJobFinished);
+ connect(job, &NetJob::failed, this, &MainWindow::skinJobFinished);
+ for (auto action : skin_dls)
+ {
+ job->addNetAction(action);
+ }
+ skin_download_job.reset(job);
+ job->start();
+ }
+
+ // load the news
+ {
+ m_newsChecker->reloadNews();
+ updateNewsLabel();
+ }
+
+
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ bool updatesAllowed = MMC->updatesAreAllowed();
+ updatesAllowedChanged(updatesAllowed);
+
+ // NOTE: calling the operator like that is an ugly hack to appease ancient gcc...
+ connect(ui->actionCheckUpdate.operator->(), &QAction::triggered, this, &MainWindow::checkForUpdates);
+
+ // set up the updater object.
+ auto updater = MMC->updateChecker();
+ connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable);
+ connect(updater.get(), &UpdateChecker::noUpdateFound, this, &MainWindow::updateNotAvailable);
+ // if automatic update checks are allowed, start one.
+ if (MMC->settings()->get("AutoUpdate").toBool() && updatesAllowed)
+ {
+ updater->checkForUpdate(MMC->settings()->get("UpdateChannel").toString(), false);
+ }
+ }
+
+ {
+ auto checker = new NotificationChecker();
+ checker->setNotificationsUrl(QUrl(BuildConfig.NOTIFICATION_URL));
+ checker->setApplicationChannel(BuildConfig.VERSION_CHANNEL);
+ checker->setApplicationPlatform(BuildConfig.BUILD_PLATFORM);
+ checker->setApplicationFullVersion(BuildConfig.FULL_VERSION_STR);
+ m_notificationChecker.reset(checker);
+ connect(m_notificationChecker.get(), &NotificationChecker::notificationCheckFinished, this, &MainWindow::notificationsChanged);
+ checker->checkForNotifications();
+ }
+
+ setSelectedInstanceById(MMC->settings()->get("SelectedInstance").toString());
+
+ // removing this looks stupid
+ view->setFocus();
+
+ retranslateUi();
+}
+
+void MainWindow::retranslateUi()
+{
+ accountMenuButton->setText(tr("Profiles"));
+
+ if (m_selectedInstance) {
+ m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
+ } else {
+ m_statusLeft->setText(tr("No instance selected"));
+ }
+
+ ui->retranslateUi(this);
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+QMenu * MainWindow::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+void MainWindow::konamiTriggered()
+{
+ // ENV.enableFeature("NewModsPage");
+ qDebug() << "Super Secret Mode ACTIVATED!";
+}
+
+void MainWindow::skinJobFinished()
+{
+ activeAccountChanged();
+ skin_download_job.reset();
+}
+
+void MainWindow::showInstanceContextMenu(const QPoint &pos)
+{
+ QList<QAction *> actions;
+
+ QAction *actionSep = new QAction("", this);
+ actionSep->setSeparator(true);
+
+ bool onInstance = view->indexAt(pos).isValid();
+ if (onInstance)
+ {
+ actions = ui->instanceToolBar->actions();
+
+ // replace the change icon widget with an actual action
+ actions.replace(0, ui->actionChangeInstIcon);
+
+ // replace the rename widget with an actual action
+ actions.replace(1, ui->actionRenameInstance);
+
+ // add header
+ actions.prepend(actionSep);
+ QAction *actionVoid = new QAction(m_selectedInstance->name(), this);
+ actionVoid->setEnabled(false);
+ actions.prepend(actionVoid);
+ }
+ else
+ {
+ auto group = view->groupNameAt(pos);
+
+ QAction *actionVoid = new QAction("MultiMC", this);
+ actionVoid->setEnabled(false);
+
+ QAction *actionCreateInstance = new QAction(tr("Create instance"), this);
+ actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip());
+ if(!group.isNull())
+ {
+ QVariantMap data;
+ data["group"] = group;
+ actionCreateInstance->setData(data);
+ }
+
+ connect(actionCreateInstance, SIGNAL(triggered(bool)), SLOT(on_actionAddInstance_triggered()));
+
+ actions.prepend(actionSep);
+ actions.prepend(actionVoid);
+ actions.append(actionCreateInstance);
+ if(!group.isNull())
+ {
+ QAction *actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this);
+ QVariantMap data;
+ data["group"] = group;
+ actionDeleteGroup->setData(data);
+ connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup()));
+ actions.append(actionDeleteGroup);
+ }
+ }
+ QMenu myMenu;
+ myMenu.addActions(actions);
+ /*
+ if (onInstance)
+ myMenu.setEnabled(m_selectedInstance->canLaunch());
+ */
+ myMenu.exec(view->mapToGlobal(pos));
+}
+
+void MainWindow::updateToolsMenu()
+{
+ QToolButton *launchButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance));
+ QToolButton *launchOfflineButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstanceOffline));
+
+ if(!m_selectedInstance || m_selectedInstance->isRunning())
+ {
+ ui->actionLaunchInstance->setMenu(nullptr);
+ ui->actionLaunchInstanceOffline->setMenu(nullptr);
+ launchButton->setPopupMode(QToolButton::InstantPopup);
+ launchOfflineButton->setPopupMode(QToolButton::InstantPopup);
+ return;
+ }
+
+ QMenu *launchMenu = ui->actionLaunchInstance->menu();
+ QMenu *launchOfflineMenu = ui->actionLaunchInstanceOffline->menu();
+ launchButton->setPopupMode(QToolButton::MenuButtonPopup);
+ launchOfflineButton->setPopupMode(QToolButton::MenuButtonPopup);
+ if (launchMenu)
+ {
+ launchMenu->clear();
+ }
+ else
+ {
+ launchMenu = new QMenu(this);
+ }
+ if (launchOfflineMenu) {
+ launchOfflineMenu->clear();
+ }
+ else
+ {
+ launchOfflineMenu = new QMenu(this);
+ }
+
+ QAction *normalLaunch = launchMenu->addAction(tr("Launch"));
+ QAction *normalLaunchOffline = launchOfflineMenu->addAction(tr("Launch Offline"));
+ connect(normalLaunch, &QAction::triggered, [this]()
+ {
+ MMC->launch(m_selectedInstance, true);
+ });
+ connect(normalLaunchOffline, &QAction::triggered, [this]()
+ {
+ MMC->launch(m_selectedInstance, false);
+ });
+ QString profilersTitle = tr("Profilers");
+ launchMenu->addSeparator()->setText(profilersTitle);
+ launchOfflineMenu->addSeparator()->setText(profilersTitle);
+ for (auto profiler : MMC->profilers().values())
+ {
+ QAction *profilerAction = launchMenu->addAction(profiler->name());
+ QAction *profilerOfflineAction = launchOfflineMenu->addAction(profiler->name());
+ QString error;
+ if (!profiler->check(&error))
+ {
+ profilerAction->setDisabled(true);
+ profilerOfflineAction->setDisabled(true);
+ QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\".");
+ profilerAction->setToolTip(profilerToolTip);
+ profilerOfflineAction->setToolTip(profilerToolTip);
+ }
+ else
+ {
+ connect(profilerAction, &QAction::triggered, [this, profiler]()
+ {
+ MMC->launch(m_selectedInstance, true, profiler.get());
+ });
+ connect(profilerOfflineAction, &QAction::triggered, [this, profiler]()
+ {
+ MMC->launch(m_selectedInstance, false, profiler.get());
+ });
+ }
+ }
+ ui->actionLaunchInstance->setMenu(launchMenu);
+ ui->actionLaunchInstanceOffline->setMenu(launchOfflineMenu);
+}
+
+QString profileInUseFilter(const QString & profile, bool used)
+{
+ if(used)
+ {
+ return profile + QObject::tr(" (in use)");
+ }
+ else
+ {
+ return profile;
+ }
+}
+
+void MainWindow::repopulateAccountsMenu()
+{
+ accountMenu->clear();
+
+ std::shared_ptr<MojangAccountList> accounts = MMC->accounts();
+ MojangAccountPtr active_account = accounts->activeAccount();
+
+ QString active_username = "";
+ if (active_account != nullptr)
+ {
+ active_username = active_account->username();
+ const AccountProfile *profile = active_account->currentProfile();
+ // this can be called before accountMenuButton exists
+ if (profile != nullptr && accountMenuButton)
+ {
+ auto profileLabel = profileInUseFilter(profile->name, active_account->isInUse());
+ accountMenuButton->setText(profileLabel);
+ }
+ }
+
+ if (accounts->count() <= 0)
+ {
+ QAction *action = new QAction(tr("No accounts added!"), this);
+ action->setEnabled(false);
+ accountMenu->addAction(action);
+ }
+ else
+ {
+ // TODO: Nicer way to iterate?
+ for (int i = 0; i < accounts->count(); i++)
+ {
+ MojangAccountPtr account = accounts->at(i);
+ for (auto profile : account->profiles())
+ {
+ auto profileLabel = profileInUseFilter(profile.name, account->isInUse());
+ QAction *action = new QAction(profileLabel, this);
+ action->setData(account->username());
+ action->setCheckable(true);
+ if (active_username == account->username())
+ {
+ action->setChecked(true);
+ }
+
+ action->setIcon(SkinUtils::getFaceFromCache(profile.id));
+ accountMenu->addAction(action);
+ connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
+ }
+ }
+ }
+
+ accountMenu->addSeparator();
+
+ QAction *action = new QAction(tr("No Default Account"), this);
+ action->setCheckable(true);
+ action->setIcon(MMC->getThemedIcon("noaccount"));
+ action->setData("");
+ if (active_username.isEmpty())
+ {
+ action->setChecked(true);
+ }
+
+ accountMenu->addAction(action);
+ connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount()));
+
+ accountMenu->addSeparator();
+ accountMenu->addAction(ui->actionManageAccounts);
+}
+
+void MainWindow::updatesAllowedChanged(bool allowed)
+{
+ if(!BuildConfig.UPDATER_ENABLED)
+ {
+ return;
+ }
+ ui->actionCheckUpdate->setEnabled(allowed);
+}
+
+/*
+ * Assumes the sender is a QAction
+ */
+void MainWindow::changeActiveAccount()
+{
+ QAction *sAction = (QAction *)sender();
+ // Profile's associated Mojang username
+ // Will need to change when profiles are properly implemented
+ if (sAction->data().type() != QVariant::Type::String)
+ return;
+
+ QVariant data = sAction->data();
+ QString id = "";
+ if (!data.isNull())
+ {
+ id = data.toString();
+ }
+
+ MMC->accounts()->setActiveAccount(id);
+
+ activeAccountChanged();
+}
+
+void MainWindow::activeAccountChanged()
+{
+ repopulateAccountsMenu();
+
+ MojangAccountPtr account = MMC->accounts()->activeAccount();
+
+ if (account != nullptr && account->username() != "")
+ {
+ const AccountProfile *profile = account->currentProfile();
+ if (profile != nullptr)
+ {
+ auto profileLabel = profileInUseFilter(profile->name, account->isInUse());
+ accountMenuButton->setIcon(SkinUtils::getFaceFromCache(profile->id));
+ accountMenuButton->setText(profileLabel);
+ return;
+ }
+ }
+
+ // Set the icon to the "no account" icon.
+ accountMenuButton->setIcon(MMC->getThemedIcon("noaccount"));
+ accountMenuButton->setText(tr("Profiles"));
+}
+
+bool MainWindow::eventFilter(QObject *obj, QEvent *ev)
+{
+ if (obj == view)
+ {
+ if (ev->type() == QEvent::KeyPress)
+ {
+ secretEventFilter->input(ev);
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
+ switch (keyEvent->key())
+ {
+ /*
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ activateInstance(m_selectedInstance);
+ return true;
+ */
+ case Qt::Key_Delete:
+ on_actionDeleteInstance_triggered();
+ return true;
+ case Qt::Key_F5:
+ refreshInstances();
+ return true;
+ case Qt::Key_F2:
+ on_actionRenameInstance_triggered();
+ return true;
+ default:
+ break;
+ }
+ }
+ }
+ return QMainWindow::eventFilter(obj, ev);
+}
+
+void MainWindow::updateNewsLabel()
+{
+ if (m_newsChecker->isLoadingNews())
+ {
+ newsLabel->setText(tr("Loading news..."));
+ newsLabel->setEnabled(false);
+ }
+ else
+ {
+ QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries();
+ if (entries.length() > 0)
+ {
+ newsLabel->setText(entries[0]->title);
+ newsLabel->setEnabled(true);
+ }
+ else
+ {
+ newsLabel->setText(tr("No news available."));
+ newsLabel->setEnabled(false);
+ }
+ }
+}
+
+void MainWindow::updateAvailable(GoUpdate::Status status)
+{
+ if(!MMC->updatesAreAllowed())
+ {
+ updateNotAvailable();
+ return;
+ }
+ UpdateDialog dlg(true, this);
+ UpdateAction action = (UpdateAction)dlg.exec();
+ switch (action)
+ {
+ case UPDATE_LATER:
+ qDebug() << "Update will be installed later.";
+ break;
+ case UPDATE_NOW:
+ downloadUpdates(status);
+ break;
+ }
+}
+
+void MainWindow::updateNotAvailable()
+{
+ UpdateDialog dlg(false, this);
+ dlg.exec();
+}
+
+QList<int> stringToIntList(const QString &string)
+{
+ QStringList split = string.split(',', QString::SkipEmptyParts);
+ QList<int> out;
+ for (int i = 0; i < split.size(); ++i)
+ {
+ out.append(split.at(i).toInt());
+ }
+ return out;
+}
+QString intListToString(const QList<int> &list)
+{
+ QStringList slist;
+ for (int i = 0; i < list.size(); ++i)
+ {
+ slist.append(QString::number(list.at(i)));
+ }
+ return slist.join(',');
+}
+void MainWindow::notificationsChanged()
+{
+ QList<NotificationChecker::NotificationEntry> entries = m_notificationChecker->notificationEntries();
+ QList<int> shownNotifications = stringToIntList(MMC->settings()->get("ShownNotifications").toString());
+ for (auto it = entries.begin(); it != entries.end(); ++it)
+ {
+ NotificationChecker::NotificationEntry entry = *it;
+ if (!shownNotifications.contains(entry.id))
+ {
+ NotificationDialog dialog(entry, this);
+ if (dialog.exec() == NotificationDialog::DontShowAgain)
+ {
+ shownNotifications.append(entry.id);
+ }
+ }
+ }
+ MMC->settings()->set("ShownNotifications", intListToString(shownNotifications));
+}
+
+void MainWindow::downloadUpdates(GoUpdate::Status status)
+{
+ if(!MMC->updatesAreAllowed())
+ {
+ return;
+ }
+ qDebug() << "Downloading updates.";
+ ProgressDialog updateDlg(this);
+ status.rootPath = MMC->root();
+
+ auto dlPath = FS::PathCombine(MMC->root(), "update", "XXXXXX");
+ if (!FS::ensureFilePathExists(dlPath))
+ {
+ CustomMessageBox::selectable(this, tr("Error"), tr("Couldn't create folder for update downloads:\n%1").arg(dlPath), QMessageBox::Warning)->show();
+ }
+ GoUpdate::DownloadTask updateTask(status, dlPath, &updateDlg);
+ // If the task succeeds, install the updates.
+ if (updateDlg.execWithTask(&updateTask))
+ {
+ /**
+ * NOTE: This disables launching instances until the update either succeeds (and this process exits)
+ * or the update fails (and the control leaves this scope).
+ */
+ MMC->updateIsRunning(true);
+ UpdateController update(this, MMC->root(), updateTask.updateFilesDir(), updateTask.operations());
+ update.installUpdates();
+ MMC->updateIsRunning(false);
+ }
+ else
+ {
+ CustomMessageBox::selectable(this, tr("Error"), updateTask.failReason(), QMessageBox::Warning)->show();
+ }
+}
+
+void MainWindow::onCatToggled(bool state)
+{
+ setCatBackground(state);
+ MMC->settings()->set("TheCat", state);
+}
+
+namespace {
+template <typename T>
+T non_stupid_abs(T in)
+{
+ if (in < 0)
+ return -in;
+ return in;
+}
+}
+
+void MainWindow::setCatBackground(bool enabled)
+{
+ if (enabled)
+ {
+ QDateTime now = QDateTime::currentDateTime();
+ QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0));
+ QString cat = (non_stupid_abs(now.daysTo(xmas)) <= 4) ? "catmas" : "kitteh";
+ view->setStyleSheet(QString(R"(
+GroupView
+{
+ background-image: url(:/backgrounds/%1);
+ background-attachment: fixed;
+ background-clip: padding;
+ background-position: top right;
+ background-repeat: none;
+ background-color:palette(base);
+})").arg(cat));
+ }
+ else
+ {
+ view->setStyleSheet(QString());
+ }
+}
+
+void MainWindow::runModalTask(Task *task)
+{
+ connect(task, &Task::failed, [this](QString reason)
+ {
+ CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
+ });
+ connect(task, &Task::succeeded, [this, task]()
+ {
+ QStringList warnings = task->warnings();
+ if(warnings.count())
+ {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
+ }
+ });
+ ProgressDialog loadDialog(this);
+ loadDialog.setSkipButton(true, tr("Abort"));
+ loadDialog.execWithTask(task);
+}
+
+void MainWindow::instanceFromInstanceTask(InstanceTask *rawTask)
+{
+ unique_qobject_ptr<Task> task(MMC->instances()->wrapInstanceTask(rawTask));
+ runModalTask(task.get());
+}
+
+void MainWindow::on_actionCopyInstance_triggered()
+{
+ if (!m_selectedInstance)
+ return;
+
+ CopyInstanceDialog copyInstDlg(m_selectedInstance, this);
+ if (!copyInstDlg.exec())
+ return;
+
+ auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.shouldCopySaves(), copyInstDlg.shouldKeepPlaytime());
+ copyTask->setName(copyInstDlg.instName());
+ copyTask->setGroup(copyInstDlg.instGroup());
+ copyTask->setIcon(copyInstDlg.iconKey());
+ unique_qobject_ptr<Task> task(MMC->instances()->wrapInstanceTask(copyTask));
+ runModalTask(task.get());
+}
+
+void MainWindow::finalizeInstance(InstancePtr inst)
+{
+ view->updateGeometries();
+ setSelectedInstanceById(inst->id());
+ if (MMC->accounts()->anyAccountIsValid())
+ {
+ ProgressDialog loadDialog(this);
+ auto update = inst->createUpdateTask(Net::Mode::Online);
+ connect(update.get(), &Task::failed, [this](QString reason)
+ {
+ QString error = QString("Instance load failed: %1").arg(reason);
+ CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
+ });
+ if(update)
+ {
+ loadDialog.setSkipButton(true, tr("Abort"));
+ loadDialog.execWithTask(update.get());
+ }
+ }
+ else
+ {
+ CustomMessageBox::selectable(this, tr("Error"), tr("MultiMC cannot download Minecraft or update instances unless you have at least "
+ "one account added.\nPlease add your Mojang or Minecraft account."),
+ QMessageBox::Warning)
+ ->show();
+ }
+}
+
+void MainWindow::addInstance(QString url)
+{
+ QString groupName;
+ do
+ {
+ QObject* obj = sender();
+ if(!obj)
+ break;
+ QAction *action = qobject_cast<QAction *>(obj);
+ if(!action)
+ break;
+ auto map = action->data().toMap();
+ if(!map.contains("group"))
+ break;
+ groupName = map["group"].toString();
+ } while(0);
+
+ if(groupName.isEmpty())
+ {
+ groupName = MMC->settings()->get("LastUsedGroupForNewInstance").toString();
+ }
+
+ NewInstanceDialog newInstDlg(groupName, url, this);
+ if (!newInstDlg.exec())
+ return;
+
+ MMC->settings()->set("LastUsedGroupForNewInstance", newInstDlg.instGroup());
+
+ InstanceTask * creationTask = newInstDlg.extractTask();
+ if(creationTask)
+ {
+ instanceFromInstanceTask(creationTask);
+ }
+}
+
+void MainWindow::on_actionAddInstance_triggered()
+{
+ addInstance();
+}
+
+void MainWindow::droppedURLs(QList<QUrl> urls)
+{
+ for(auto & url:urls)
+ {
+ if(url.isLocalFile())
+ {
+ addInstance(url.toLocalFile());
+ }
+ else
+ {
+ addInstance(url.toString());
+ }
+ // Only process one dropped file...
+ break;
+ }
+}
+
+void MainWindow::on_actionREDDIT_triggered()
+{
+ DesktopServices::openUrl(QUrl(BuildConfig.SUBREDDIT_URL));
+}
+
+void MainWindow::on_actionDISCORD_triggered()
+{
+ DesktopServices::openUrl(QUrl(BuildConfig.DISCORD_URL));
+}
+
+void MainWindow::on_actionChangeInstIcon_triggered()
+{
+ if (!m_selectedInstance)
+ return;
+
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(m_selectedInstance->iconKey());
+ if (dlg.result() == QDialog::Accepted)
+ {
+ m_selectedInstance->setIconKey(dlg.selectedIconKey);
+ auto icon = MMC->icons()->getIcon(dlg.selectedIconKey);
+ ui->actionChangeInstIcon->setIcon(icon);
+ ui->changeIconButton->setIcon(icon);
+ }
+}
+
+void MainWindow::iconUpdated(QString icon)
+{
+ if (icon == m_currentInstIcon)
+ {
+ auto icon = MMC->icons()->getIcon(m_currentInstIcon);
+ ui->actionChangeInstIcon->setIcon(icon);
+ ui->changeIconButton->setIcon(icon);
+ }
+}
+
+void MainWindow::updateInstanceToolIcon(QString new_icon)
+{
+ m_currentInstIcon = new_icon;
+ auto icon = MMC->icons()->getIcon(m_currentInstIcon);
+ ui->actionChangeInstIcon->setIcon(icon);
+ ui->changeIconButton->setIcon(icon);
+}
+
+void MainWindow::setSelectedInstanceById(const QString &id)
+{
+ if (id.isNull())
+ return;
+ const QModelIndex index = MMC->instances()->getInstanceIndexById(id);
+ if (index.isValid())
+ {
+ QModelIndex selectionIndex = proxymodel->mapFromSource(index);
+ view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect);
+ updateStatusCenter();
+ }
+}
+
+void MainWindow::on_actionChangeInstGroup_triggered()
+{
+ if (!m_selectedInstance)
+ return;
+
+ bool ok = false;
+ InstanceId instId = m_selectedInstance->id();
+ QString name(MMC->instances()->getInstanceGroup(instId));
+ auto groups = MMC->instances()->getGroups();
+ groups.insert(0, "");
+ groups.sort(Qt::CaseInsensitive);
+ int foo = groups.indexOf(name);
+
+ name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok);
+ name = name.simplified();
+ if (ok)
+ {
+ MMC->instances()->setInstanceGroup(instId, name);
+ }
+}
+
+void MainWindow::deleteGroup()
+{
+ QObject* obj = sender();
+ if(!obj)
+ return;
+ QAction *action = qobject_cast<QAction *>(obj);
+ if(!action)
+ return;
+ auto map = action->data().toMap();
+ if(!map.contains("group"))
+ return;
+ QString groupName = map["group"].toString();
+ if(!groupName.isEmpty())
+ {
+ auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1")
+ .arg(groupName), QMessageBox::Yes | QMessageBox::No);
+ if(reply == QMessageBox::Yes)
+ {
+ MMC->instances()->deleteGroup(groupName);
+ }
+ }
+}
+
+void MainWindow::on_actionViewInstanceFolder_triggered()
+{
+ QString str = MMC->settings()->get("InstanceDir").toString();
+ DesktopServices::openDirectory(str);
+}
+
+void MainWindow::refreshInstances()
+{
+ MMC->instances()->loadList();
+}
+
+void MainWindow::on_actionViewCentralModsFolder_triggered()
+{
+ DesktopServices::openDirectory(MMC->settings()->get("CentralModsDir").toString(), true);
+}
+
+void MainWindow::on_actionConfig_Folder_triggered()
+{
+ if (m_selectedInstance)
+ {
+ QString str = m_selectedInstance->instanceConfigFolder();
+ DesktopServices::openDirectory(QDir(str).absolutePath());
+ }
+}
+
+void MainWindow::checkForUpdates()
+{
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ auto updater = MMC->updateChecker();
+ updater->checkForUpdate(MMC->settings()->get("UpdateChannel").toString(), true);
+ }
+ else
+ {
+ qWarning() << "Updater not set up. Cannot check for updates.";
+ }
+}
+
+void MainWindow::on_actionSettings_triggered()
+{
+ MMC->ShowGlobalSettings(this, "global-settings");
+}
+
+void MainWindow::globalSettingsClosed()
+{
+ // FIXME: quick HACK to make this work. improve, optimize.
+ MMC->instances()->loadList();
+ proxymodel->invalidate();
+ proxymodel->sort(0);
+ updateToolsMenu();
+ update();
+}
+
+void MainWindow::on_actionInstanceSettings_triggered()
+{
+ MMC->showInstanceWindow(m_selectedInstance, "settings");
+}
+
+void MainWindow::on_actionEditInstNotes_triggered()
+{
+ MMC->showInstanceWindow(m_selectedInstance, "notes");
+}
+
+void MainWindow::on_actionWorlds_triggered()
+{
+ MMC->showInstanceWindow(m_selectedInstance, "worlds");
+}
+
+void MainWindow::on_actionEditInstance_triggered()
+{
+ MMC->showInstanceWindow(m_selectedInstance);
+}
+
+void MainWindow::on_actionScreenshots_triggered()
+{
+ MMC->showInstanceWindow(m_selectedInstance, "screenshots");
+}
+
+void MainWindow::on_actionManageAccounts_triggered()
+{
+ MMC->ShowGlobalSettings(this, "accounts");
+}
+
+void MainWindow::on_actionReportBug_triggered()
+{
+ DesktopServices::openUrl(QUrl(BuildConfig.BUG_TRACKER_URL));
+}
+
+void MainWindow::on_actionPatreon_triggered()
+{
+ DesktopServices::openUrl(QUrl("https://www.patreon.com/multimc"));
+}
+
+void MainWindow::on_actionMoreNews_triggered()
+{
+ DesktopServices::openUrl(QUrl("https://multimc.org/posts.html"));
+}
+
+void MainWindow::newsButtonClicked()
+{
+ QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries();
+ if (entries.count() > 0)
+ {
+ DesktopServices::openUrl(QUrl(entries[0]->link));
+ }
+ else
+ {
+ DesktopServices::openUrl(QUrl("https://multimc.org/posts.html"));
+ }
+}
+
+void MainWindow::on_actionAbout_triggered()
+{
+ AboutDialog dialog(this);
+ dialog.exec();
+}
+
+void MainWindow::on_actionDeleteInstance_triggered()
+{
+ if (!m_selectedInstance)
+ {
+ return;
+ }
+ auto id = m_selectedInstance->id();
+ auto response = CustomMessageBox::selectable(
+ this,
+ tr("CAREFUL!"),
+ tr("About to delete: %1\nThis is permanent and will completely delete the instance.\n\nAre you sure?").arg(m_selectedInstance->name()),
+ QMessageBox::Warning,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No
+ )->exec();
+ if (response == QMessageBox::Yes)
+ {
+ MMC->instances()->deleteInstance(id);
+ }
+}
+
+void MainWindow::on_actionExportInstance_triggered()
+{
+ if (m_selectedInstance)
+ {
+ ExportInstanceDialog dlg(m_selectedInstance, this);
+ dlg.exec();
+ }
+}
+
+void MainWindow::on_actionRenameInstance_triggered()
+{
+ if (m_selectedInstance)
+ {
+ view->edit(view->currentIndex());
+ }
+}
+
+void MainWindow::on_actionViewSelectedInstFolder_triggered()
+{
+ if (m_selectedInstance)
+ {
+ QString str = m_selectedInstance->instanceRoot();
+ DesktopServices::openDirectory(QDir(str).absolutePath());
+ }
+}
+
+void MainWindow::on_actionViewSelectedMCFolder_triggered()
+{
+ if (m_selectedInstance)
+ {
+ QString str = m_selectedInstance->gameRoot();
+ if (!FS::ensureFilePathExists(str))
+ {
+ // TODO: report error
+ return;
+ }
+ DesktopServices::openDirectory(QDir(str).absolutePath());
+ }
+}
+
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+ // Save the window state and geometry.
+ MMC->settings()->set("MainWindowState", saveState().toBase64());
+ MMC->settings()->set("MainWindowGeometry", saveGeometry().toBase64());
+ event->accept();
+ emit isClosing();
+}
+
+void MainWindow::changeEvent(QEvent* event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslateUi();
+ }
+ QMainWindow::changeEvent(event);
+}
+
+void MainWindow::instanceActivated(QModelIndex index)
+{
+ if (!index.isValid())
+ return;
+ QString id = index.data(InstanceList::InstanceIDRole).toString();
+ InstancePtr inst = MMC->instances()->getInstanceById(id);
+ if (!inst)
+ return;
+
+ activateInstance(inst);
+}
+
+void MainWindow::on_actionLaunchInstance_triggered()
+{
+ if (!m_selectedInstance)
+ {
+ return;
+ }
+ if(m_selectedInstance->isRunning())
+ {
+ MMC->kill(m_selectedInstance);
+ }
+ else
+ {
+ MMC->launch(m_selectedInstance);
+ }
+}
+
+void MainWindow::activateInstance(InstancePtr instance)
+{
+ MMC->launch(instance);
+}
+
+void MainWindow::on_actionLaunchInstanceOffline_triggered()
+{
+ if (m_selectedInstance)
+ {
+ MMC->launch(m_selectedInstance, false);
+ }
+}
+
+void MainWindow::taskEnd()
+{
+ QObject *sender = QObject::sender();
+ if (sender == m_versionLoadTask)
+ m_versionLoadTask = NULL;
+
+ sender->deleteLater();
+}
+
+void MainWindow::startTask(Task *task)
+{
+ connect(task, SIGNAL(succeeded()), SLOT(taskEnd()));
+ connect(task, SIGNAL(failed(QString)), SLOT(taskEnd()));
+ task->start();
+}
+
+void MainWindow::instanceChanged(const QModelIndex &current, const QModelIndex &previous)
+{
+ if (!current.isValid())
+ {
+ MMC->settings()->set("SelectedInstance", QString());
+ selectionBad();
+ return;
+ }
+ QString id = current.data(InstanceList::InstanceIDRole).toString();
+ m_selectedInstance = MMC->instances()->getInstanceById(id);
+ if (m_selectedInstance)
+ {
+ ui->instanceToolBar->setEnabled(true);
+ if(m_selectedInstance->isRunning())
+ {
+ ui->actionLaunchInstance->setEnabled(true);
+ ui->setLaunchAction(true);
+ }
+ else
+ {
+ ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch());
+ ui->setLaunchAction(false);
+ }
+ ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch());
+ ui->actionExportInstance->setEnabled(m_selectedInstance->canExport());
+ ui->renameButton->setText(m_selectedInstance->name());
+ m_statusLeft->setText(m_selectedInstance->getStatusbarDescription());
+ updateStatusCenter();
+ updateInstanceToolIcon(m_selectedInstance->iconKey());
+
+ updateToolsMenu();
+
+ MMC->settings()->set("SelectedInstance", m_selectedInstance->id());
+ }
+ else
+ {
+ ui->instanceToolBar->setEnabled(false);
+ MMC->settings()->set("SelectedInstance", QString());
+ selectionBad();
+ return;
+ }
+}
+
+void MainWindow::instanceSelectRequest(QString id)
+{
+ setSelectedInstanceById(id);
+}
+
+void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
+{
+ auto current = view->selectionModel()->currentIndex();
+ QItemSelection test(topLeft, bottomRight);
+ if (test.contains(current))
+ {
+ instanceChanged(current, current);
+ }
+}
+
+void MainWindow::selectionBad()
+{
+ // start by reseting everything...
+ m_selectedInstance = nullptr;
+
+ statusBar()->clearMessage();
+ ui->instanceToolBar->setEnabled(false);
+ ui->renameButton->setText(tr("Rename Instance"));
+ updateInstanceToolIcon("infinity");
+
+ // ...and then see if we can enable the previously selected instance
+ setSelectedInstanceById(MMC->settings()->get("SelectedInstance").toString());
+}
+
+void MainWindow::checkInstancePathForProblems()
+{
+ QString instanceFolder = MMC->settings()->get("InstanceDir").toString();
+ if (FS::checkProblemticPathJava(QDir(instanceFolder)))
+ {
+ QMessageBox warning(this);
+ warning.setText(tr("Your instance folder contains \'!\' and this is known to cause Java problems!"));
+ warning.setInformativeText(tr("You have now two options: <br/>"
+ " - change the instance folder in the settings <br/>"
+ " - move this installation of MultiMC5 to a different folder"));
+ warning.setDefaultButton(QMessageBox::Ok);
+ warning.exec();
+ }
+ auto tempFolderText = tr("This is a problem: <br/>"
+ " - MultiMC will likely be deleted without warning by the operating system <br/>"
+ " - close MultiMC now and extract it to a real location, not a temporary folder");
+ QString pathfoldername = QDir(instanceFolder).absolutePath();
+ if (pathfoldername.contains("Rar$", Qt::CaseInsensitive))
+ {
+ QMessageBox warning(this);
+ warning.setText(tr("Your instance folder contains \'Rar$\' - that means you haven't extracted the MultiMC zip!"));
+ warning.setInformativeText(tempFolderText);
+ warning.setDefaultButton(QMessageBox::Ok);
+ warning.exec();
+ }
+ else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/"))
+ {
+ QMessageBox warning(this);
+ warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath()));
+ warning.setInformativeText(tempFolderText);
+ warning.setDefaultButton(QMessageBox::Ok);
+ warning.exec();
+ }
+}
+
+void MainWindow::updateStatusCenter()
+{
+ int timeplayed = MMC->instances()->getTotalPlayTime();
+ int minutesTotal = timeplayed / 60;
+ int seconds = timeplayed % 60;
+ int minutes = minutesTotal % 60;
+ int hours = minutesTotal / 60;
+ if(hours != 0)
+ m_statusCenter->setText(tr("Total playtime: %1h %2m %3s").arg(hours).arg(minutes).arg(seconds));
+ else if(minutes != 0)
+ m_statusCenter->setText(tr("Total playtime: %1m %2s").arg(minutes).arg(seconds));
+ else if(seconds != 0)
+ m_statusCenter->setText(tr("Total playtime: %1s").arg(seconds));
+}
diff --git a/launcher/MainWindow.h b/launcher/MainWindow.h
new file mode 100644
index 00000000..c992ab94
--- /dev/null
+++ b/launcher/MainWindow.h
@@ -0,0 +1,226 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <QMainWindow>
+#include <QProcess>
+#include <QTimer>
+
+#include "BaseInstance.h"
+#include "minecraft/auth/MojangAccount.h"
+#include "net/NetJob.h"
+#include "updater/GoUpdate.h"
+
+class LaunchController;
+class NewsChecker;
+class NotificationChecker;
+class QToolButton;
+class InstanceProxyModel;
+class LabeledToolButton;
+class QLabel;
+class MinecraftLauncher;
+class BaseProfilerFactory;
+class GroupView;
+class ServerStatus;
+class KonamiCode;
+class InstanceTask;
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+ class Ui;
+
+public:
+ explicit MainWindow(QWidget *parent = 0);
+ ~MainWindow();
+
+ bool eventFilter(QObject *obj, QEvent *ev) override;
+ void closeEvent(QCloseEvent *event) override;
+ void changeEvent(QEvent * event) override;
+
+ void checkInstancePathForProblems();
+
+ void updatesAllowedChanged(bool allowed);
+
+ void droppedURLs(QList<QUrl> urls);
+signals:
+ void isClosing();
+
+protected:
+ QMenu * createPopupMenu() override;
+
+private slots:
+ void onCatToggled(bool);
+
+ void on_actionAbout_triggered();
+
+ void on_actionAddInstance_triggered();
+
+ void on_actionREDDIT_triggered();
+
+ void on_actionDISCORD_triggered();
+
+ void on_actionCopyInstance_triggered();
+
+ void on_actionChangeInstGroup_triggered();
+
+ void on_actionChangeInstIcon_triggered();
+ void on_changeIconButton_clicked(bool)
+ {
+ on_actionChangeInstIcon_triggered();
+ }
+
+ void on_actionViewInstanceFolder_triggered();
+
+ void on_actionConfig_Folder_triggered();
+
+ void on_actionViewSelectedInstFolder_triggered();
+
+ void on_actionViewSelectedMCFolder_triggered();
+
+ void refreshInstances();
+
+ void on_actionViewCentralModsFolder_triggered();
+
+ void checkForUpdates();
+
+ void on_actionSettings_triggered();
+
+ void on_actionInstanceSettings_triggered();
+
+ void on_actionManageAccounts_triggered();
+
+ void on_actionReportBug_triggered();
+
+ void on_actionPatreon_triggered();
+
+ void on_actionMoreNews_triggered();
+
+ void newsButtonClicked();
+
+ void on_actionLaunchInstance_triggered();
+
+ void on_actionLaunchInstanceOffline_triggered();
+
+ void on_actionDeleteInstance_triggered();
+
+ void deleteGroup();
+
+ void on_actionExportInstance_triggered();
+
+ void on_actionRenameInstance_triggered();
+ void on_renameButton_clicked(bool)
+ {
+ on_actionRenameInstance_triggered();
+ }
+
+ void on_actionEditInstance_triggered();
+
+ void on_actionEditInstNotes_triggered();
+
+ void on_actionWorlds_triggered();
+
+ void on_actionScreenshots_triggered();
+
+ void taskEnd();
+
+ /**
+ * called when an icon is changed in the icon model.
+ */
+ void iconUpdated(QString);
+
+ void showInstanceContextMenu(const QPoint &);
+
+ void updateToolsMenu();
+
+ void skinJobFinished();
+
+ void instanceActivated(QModelIndex);
+
+ void instanceChanged(const QModelIndex &current, const QModelIndex &previous);
+
+ void instanceSelectRequest(QString id);
+
+ void instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
+
+ void selectionBad();
+
+ void startTask(Task *task);
+
+ void updateAvailable(GoUpdate::Status status);
+
+ void updateNotAvailable();
+
+ void notificationsChanged();
+
+ void activeAccountChanged();
+
+ void changeActiveAccount();
+
+ void repopulateAccountsMenu();
+
+ void updateNewsLabel();
+
+ /*!
+ * Runs the DownloadTask and installs updates.
+ */
+ void downloadUpdates(GoUpdate::Status status);
+
+ void konamiTriggered();
+
+ void globalSettingsClosed();
+
+private:
+ void retranslateUi();
+
+ void addInstance(QString url = QString());
+ void activateInstance(InstancePtr instance);
+ void setCatBackground(bool enabled);
+ void updateInstanceToolIcon(QString new_icon);
+ void setSelectedInstanceById(const QString &id);
+ void updateStatusCenter();
+
+ void runModalTask(Task *task);
+ void instanceFromInstanceTask(InstanceTask *task);
+ void finalizeInstance(InstancePtr inst);
+
+private:
+ std::unique_ptr<Ui> ui;
+
+ // these are managed by Qt's memory management model!
+ GroupView *view = nullptr;
+ InstanceProxyModel *proxymodel = nullptr;
+ QToolButton *newsLabel = nullptr;
+ QLabel *m_statusLeft = nullptr;
+ QLabel *m_statusCenter = nullptr;
+ ServerStatus *m_statusRight = nullptr;
+ QMenu *accountMenu = nullptr;
+ QToolButton *accountMenuButton = nullptr;
+ KonamiCode * secretEventFilter = nullptr;
+
+ unique_qobject_ptr<NetJob> skin_download_job;
+ unique_qobject_ptr<NewsChecker> m_newsChecker;
+ unique_qobject_ptr<NotificationChecker> m_notificationChecker;
+
+ InstancePtr m_selectedInstance;
+ QString m_currentInstIcon;
+
+ // managed by the application object
+ Task *m_versionLoadTask = nullptr;
+};
diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp
new file mode 100644
index 00000000..0a2cd23d
--- /dev/null
+++ b/launcher/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/launcher/MessageLevel.h b/launcher/MessageLevel.h
new file mode 100644
index 00000000..f19048e3
--- /dev/null
+++ b/launcher/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/launcher/MultiMC.cpp b/launcher/MultiMC.cpp
new file mode 100644
index 00000000..932c7a76
--- /dev/null
+++ b/launcher/MultiMC.cpp
@@ -0,0 +1,1448 @@
+#include "MultiMC.h"
+#include "BuildConfig.h"
+#include "MainWindow.h"
+#include "InstanceWindow.h"
+
+#include "groupview/AccessibleGroupView.h"
+#include <QAccessible>
+
+#include "pages/BasePageProvider.h"
+#include "pages/global/MultiMCPage.h"
+#include "pages/global/MinecraftPage.h"
+#include "pages/global/JavaPage.h"
+#include "pages/global/LanguagePage.h"
+#include "pages/global/ProxyPage.h"
+#include "pages/global/ExternalToolsPage.h"
+#include "pages/global/AccountListPage.h"
+#include "pages/global/PasteEEPage.h"
+#include "pages/global/CustomCommandsPage.h"
+
+#include "themes/ITheme.h"
+#include "themes/SystemTheme.h"
+#include "themes/DarkTheme.h"
+#include "themes/BrightTheme.h"
+#include "themes/CustomTheme.h"
+
+#include "setupwizard/SetupWizard.h"
+#include "setupwizard/LanguageWizardPage.h"
+#include "setupwizard/JavaWizardPage.h"
+#include "setupwizard/AnalyticsWizardPage.h"
+
+#include <iostream>
+#include <QDir>
+#include <QFileInfo>
+#include <QNetworkAccessManager>
+#include <QTranslator>
+#include <QLibraryInfo>
+#include <QList>
+#include <QStringList>
+#include <QDebug>
+#include <QStyleFactory>
+
+#include "dialogs/CustomMessageBox.h"
+#include "InstanceList.h"
+
+#include <minecraft/auth/MojangAccountList.h>
+#include "icons/IconList.h"
+#include "net/HttpMetaCache.h"
+#include "Env.h"
+
+#include "java/JavaUtils.h"
+
+#include "updater/UpdateChecker.h"
+
+#include "tools/JProfiler.h"
+#include "tools/JVisualVM.h"
+#include "tools/MCEditTool.h"
+
+#include <xdgicon.h>
+#include "settings/INISettingsObject.h"
+#include "settings/Setting.h"
+
+#include "translations/TranslationsModel.h"
+
+#include <Commandline.h>
+#include <FileSystem.h>
+#include <DesktopServices.h>
+#include <LocalPeer.h>
+
+#include <ganalytics.h>
+#include <sys.h>
+
+#include "pagedialog/PageDialog.h"
+
+
+#if defined Q_OS_WIN32
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include <windows.h>
+#include <stdio.h>
+#endif
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x) STRINGIFY(x)
+
+static const QLatin1String liveCheckFile("live.check");
+
+using namespace Commandline;
+
+#define MACOS_HINT "If you are on macOS Sierra, you might have to move MultiMC.app to your /Applications or ~/Applications folder. "\
+ "This usually fixes the problem and you can move the application elsewhere afterwards.\n"\
+ "\n"
+
+static void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+{
+ const char *levels = "DWCFIS";
+ const QString format("%1 %2 %3\n");
+
+ qint64 msecstotal = MMC->timeSinceStart();
+ qint64 seconds = msecstotal / 1000;
+ qint64 msecs = msecstotal % 1000;
+ QString foo;
+ char buf[1025] = {0};
+ ::snprintf(buf, 1024, "%5lld.%03lld", seconds, msecs);
+
+ QString out = format.arg(buf).arg(levels[type]).arg(msg);
+
+ MMC->logFile->write(out.toUtf8());
+ MMC->logFile->flush();
+ QTextStream(stderr) << out.toLocal8Bit();
+ fflush(stderr);
+}
+
+MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv)
+{
+#if defined Q_OS_WIN32
+ // attach the parent console
+ if(AttachConsole(ATTACH_PARENT_PROCESS))
+ {
+ // if attach succeeds, reopen and sync all the i/o
+ if(freopen("CON", "w", stdout))
+ {
+ std::cout.sync_with_stdio();
+ }
+ if(freopen("CON", "w", stderr))
+ {
+ std::cerr.sync_with_stdio();
+ }
+ if(freopen("CON", "r", stdin))
+ {
+ std::cin.sync_with_stdio();
+ }
+ auto out = GetStdHandle (STD_OUTPUT_HANDLE);
+ DWORD written;
+ const char * endline = "\n";
+ WriteConsole(out, endline, strlen(endline), &written, NULL);
+ consoleAttached = true;
+ }
+#endif
+ setOrganizationName("MultiMC");
+ setOrganizationDomain("multimc.org");
+ setApplicationName("MultiMC5");
+ setApplicationDisplayName("MultiMC 5");
+ setApplicationVersion(BuildConfig.printableVersionString());
+
+ startTime = QDateTime::currentDateTime();
+
+#ifdef Q_OS_LINUX
+ {
+ QFile osrelease("/proc/sys/kernel/osrelease");
+ if (osrelease.open(QFile::ReadOnly | QFile::Text)) {
+ QTextStream in(&osrelease);
+ auto contents = in.readAll();
+ if(
+ contents.contains("WSL", Qt::CaseInsensitive) ||
+ contents.contains("Microsoft", Qt::CaseInsensitive)
+ ) {
+ showFatalErrorMessage(
+ "Unsupported system detected!",
+ "Linux-on-Windows distributions are not supported.\n\n"
+ "Please use the Windows MultiMC binary when playing on Windows."
+ );
+ return;
+ }
+ }
+ }
+#endif
+
+ // Don't quit on hiding the last window
+ this->setQuitOnLastWindowClosed(false);
+
+ // Commandline parsing
+ QHash<QString, QVariant> args;
+ {
+ Parser parser(FlagStyle::GNU, ArgumentStyle::SpaceAndEquals);
+
+ // --help
+ parser.addSwitch("help");
+ parser.addShortOpt("help", 'h');
+ parser.addDocumentation("help", "Display this help and exit.");
+ // --version
+ parser.addSwitch("version");
+ parser.addShortOpt("version", 'V');
+ parser.addDocumentation("version", "Display program version and exit.");
+ // --dir
+ parser.addOption("dir");
+ parser.addShortOpt("dir", 'd');
+ parser.addDocumentation("dir", "Use the supplied folder as MultiMC root instead of "
+ "the binary location (use '.' for current)");
+ // --launch
+ parser.addOption("launch");
+ parser.addShortOpt("launch", 'l');
+ parser.addDocumentation("launch", "Launch the specified instance (by instance ID)");
+ // --server
+ parser.addOption("server");
+ parser.addShortOpt("server", 's');
+ parser.addDocumentation("server", "Join the specified server on launch "
+ "(only valid in combination with --launch)");
+ // --alive
+ parser.addSwitch("alive");
+ parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after MultiMC starts");
+ // --import
+ parser.addOption("import");
+ parser.addShortOpt("import", 'I');
+ parser.addDocumentation("import", "Import instance from specified zip (local path or URL)");
+
+ // parse the arguments
+ try
+ {
+ args = parser.parse(arguments());
+ }
+ catch (const ParsingError &e)
+ {
+ std::cerr << "CommandLineError: " << e.what() << std::endl;
+ if(argc > 0)
+ std::cerr << "Try '" << argv[0] << " -h' to get help on MultiMC's command line parameters."
+ << std::endl;
+ m_status = MultiMC::Failed;
+ return;
+ }
+
+ // display help and exit
+ if (args["help"].toBool())
+ {
+ std::cout << qPrintable(parser.compileHelp(arguments()[0]));
+ m_status = MultiMC::Succeeded;
+ return;
+ }
+
+ // display version and exit
+ if (args["version"].toBool())
+ {
+ std::cout << "Version " << BuildConfig.printableVersionString().toStdString() << std::endl;
+ std::cout << "Git " << BuildConfig.GIT_COMMIT.toStdString() << std::endl;
+ m_status = MultiMC::Succeeded;
+ return;
+ }
+ }
+ m_instanceIdToLaunch = args["launch"].toString();
+ m_serverToJoin = args["server"].toString();
+ m_liveCheck = args["alive"].toBool();
+ m_zipToImport = args["import"].toUrl();
+
+ QString origcwdPath = QDir::currentPath();
+ QString binPath = applicationDirPath();
+ QString adjustedBy;
+ QString dataPath;
+ // change folder
+ QString dirParam = args["dir"].toString();
+ if (!dirParam.isEmpty())
+ {
+ // the dir param. it makes multimc data path point to whatever the user specified
+ // on command line
+ adjustedBy += "Command line " + dirParam;
+ dataPath = dirParam;
+ }
+ else
+ {
+#ifdef MULTIMC_LINUX_DATADIR
+ QString xdgDataHome = QFile::decodeName(qgetenv("XDG_DATA_HOME"));
+ if (xdgDataHome.isEmpty())
+ xdgDataHome = QDir::homePath() + QLatin1String("/.local/share");
+ dataPath = xdgDataHome + "/multimc";
+ adjustedBy += "XDG standard " + dataPath;
+#elif defined(Q_OS_MAC)
+ QDir foo(FS::PathCombine(applicationDirPath(), "../../Data"));
+ dataPath = foo.absolutePath();
+ adjustedBy += "Fallback to special Mac location " + dataPath;
+#else
+ dataPath = applicationDirPath();
+ adjustedBy += "Fallback to binary path " + dataPath;
+#endif
+ }
+
+ if (!FS::ensureFolderPathExists(dataPath))
+ {
+ showFatalErrorMessage(
+ "MultiMC data folder could not be created.",
+ "MultiMC data folder could not be created.\n"
+ "\n"
+#if defined(Q_OS_MAC)
+ MACOS_HINT
+#endif
+ "Make sure you have the right permissions to the MultiMC data folder and any folder needed to access it.\n"
+ "\n"
+ "MultiMC cannot continue until you fix this problem."
+ );
+ return;
+ }
+ if (!QDir::setCurrent(dataPath))
+ {
+ showFatalErrorMessage(
+ "MultiMC data folder could not be opened.",
+ "MultiMC data folder could not be opened.\n"
+ "\n"
+#if defined(Q_OS_MAC)
+ MACOS_HINT
+#endif
+ "Make sure you have the right permissions to the MultiMC data folder.\n"
+ "\n"
+ "MultiMC cannot continue until you fix this problem."
+ );
+ return;
+ }
+
+ if(m_instanceIdToLaunch.isEmpty() && !m_serverToJoin.isEmpty())
+ {
+ std::cerr << "--server can only be used in combination with --launch!" << std::endl;
+ m_status = MultiMC::Failed;
+ return;
+ }
+
+#if defined(Q_OS_MAC)
+ // move user data to new location if on macOS and it still exists in Contents/MacOS
+ QDir fi(applicationDirPath());
+ QString originalData = fi.absolutePath();
+ // if the config file exists in Contents/MacOS, then user data is still there and needs to moved
+ if (QFileInfo::exists(FS::PathCombine(originalData, "multimc.cfg")))
+ {
+ if (!QFileInfo::exists(FS::PathCombine(originalData, "dontmovemacdata")))
+ {
+ QMessageBox::StandardButton askMoveDialogue;
+ askMoveDialogue = QMessageBox::question(nullptr, "MultiMC 5", "Would you like to move application data to a new data location? It will improve MultiMC's performance, but if you switch to older versions it will look like instances have disappeared. If you select no, you can migrate later in settings. You should select yes unless you're commonly switching between different versions of MultiMC (eg. develop and stable).", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+ if (askMoveDialogue == QMessageBox::Yes)
+ {
+ qDebug() << "On macOS and found config file in old location, moving user data...";
+ QDir dir;
+ QStringList dataFiles {
+ "*.log", // MultiMC-@.log
+ "accounts.json",
+ "accounts",
+ "assets",
+ "cache",
+ "icons",
+ "instances",
+ "libraries",
+ "meta",
+ "metacache",
+ "mods",
+ "multimc.cfg",
+ "themes",
+ "translations"
+ };
+ QDirIterator files(originalData, dataFiles);
+ while (files.hasNext()) {
+ QString filePath(files.next());
+ QString fileName(files.fileName());
+ if (!dir.rename(filePath, FS::PathCombine(dataPath, fileName)))
+ {
+ qWarning() << "Failed to move " << fileName;
+ }
+ }
+ }
+ else
+ {
+ dataPath = originalData;
+ QDir::setCurrent(dataPath);
+ QFile file(originalData + "/dontmovemacdata");
+ file.open(QIODevice::WriteOnly);
+ }
+ }
+ else
+ {
+ dataPath = originalData;
+ QDir::setCurrent(dataPath);
+ }
+ }
+#endif
+
+ /*
+ * Establish the mechanism for communication with an already running MultiMC that uses the same data path.
+ * If there is one, tell it what the user actually wanted to do and exit.
+ * We want to initialize this before logging to avoid messing with the log of a potential already running copy.
+ */
+ auto appID = ApplicationId::fromPathAndVersion(QDir::currentPath(), BuildConfig.printableVersionString());
+ {
+ // FIXME: you can run the same binaries with multiple data dirs and they won't clash. This could cause issues for updates.
+ m_peerInstance = new LocalPeer(this, appID);
+ connect(m_peerInstance, &LocalPeer::messageReceived, this, &MultiMC::messageReceived);
+ if(m_peerInstance->isClient())
+ {
+ int timeout = 2000;
+
+ if(m_instanceIdToLaunch.isEmpty())
+ {
+ m_peerInstance->sendMessage("activate", timeout);
+
+ if(!m_zipToImport.isEmpty())
+ {
+ m_peerInstance->sendMessage("import " + m_zipToImport.toString(), timeout);
+ }
+ }
+ else
+ {
+ if(!m_serverToJoin.isEmpty())
+ {
+ m_peerInstance->sendMessage(
+ "launch-with-server " + m_instanceIdToLaunch + " " + m_serverToJoin, timeout);
+ }
+ else
+ {
+ m_peerInstance->sendMessage("launch " + m_instanceIdToLaunch, timeout);
+ }
+ }
+ m_status = MultiMC::Succeeded;
+ return;
+ }
+ }
+
+ // init the logger
+ {
+ static const QString logBase = "MultiMC-%0.log";
+ auto moveFile = [](const QString &oldName, const QString &newName)
+ {
+ QFile::remove(newName);
+ QFile::copy(oldName, newName);
+ QFile::remove(oldName);
+ };
+
+ moveFile(logBase.arg(3), logBase.arg(4));
+ moveFile(logBase.arg(2), logBase.arg(3));
+ moveFile(logBase.arg(1), logBase.arg(2));
+ moveFile(logBase.arg(0), logBase.arg(1));
+
+ logFile = std::unique_ptr<QFile>(new QFile(logBase.arg(0)));
+ if(!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
+ {
+ showFatalErrorMessage(
+ "MultiMC data folder is not writable!",
+ "MultiMC couldn't create a log file - the MultiMC data folder is not writable.\n"
+ "\n"
+ #if defined(Q_OS_MAC)
+ MACOS_HINT
+ #endif
+ "Make sure you have write permissions to the MultiMC data folder.\n"
+ "\n"
+ "MultiMC cannot continue until you fix this problem."
+ );
+ return;
+ }
+ qInstallMessageHandler(appDebugOutput);
+ qDebug() << "<> Log initialized.";
+ }
+
+ // Set up paths
+ {
+ // Root path is used for updates.
+#ifdef Q_OS_LINUX
+ QDir foo(FS::PathCombine(binPath, ".."));
+ m_rootPath = foo.absolutePath();
+#elif defined(Q_OS_WIN32)
+ m_rootPath = binPath;
+#elif defined(Q_OS_MAC)
+ QDir foo(FS::PathCombine(binPath, "../.."));
+ m_rootPath = foo.absolutePath();
+ // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues)
+ FS::updateTimestamp(m_rootPath);
+#endif
+
+#ifdef MULTIMC_JARS_LOCATION
+ ENV.setJarsPath( TOSTRING(MULTIMC_JARS_LOCATION) );
+#endif
+
+ qDebug() << "MultiMC 5, (c) 2013-2021 MultiMC Contributors";
+ qDebug() << "Version : " << BuildConfig.printableVersionString();
+ qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
+ qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
+ if (adjustedBy.size())
+ {
+ qDebug() << "Work dir before adjustment : " << origcwdPath;
+ qDebug() << "Work dir after adjustment : " << QDir::currentPath();
+ qDebug() << "Adjusted by : " << adjustedBy;
+ }
+ else
+ {
+ qDebug() << "Work dir : " << QDir::currentPath();
+ }
+ qDebug() << "Binary path : " << binPath;
+ qDebug() << "Application root path : " << m_rootPath;
+ if(!m_instanceIdToLaunch.isEmpty())
+ {
+ qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch;
+ }
+ if(!m_serverToJoin.isEmpty())
+ {
+ qDebug() << "Address of server to join :" << m_serverToJoin;
+ }
+ qDebug() << "<> Paths set.";
+ }
+
+ do // once
+ {
+ if(m_liveCheck)
+ {
+ QFile check(liveCheckFile);
+ if(!check.open(QIODevice::WriteOnly | QIODevice::Truncate))
+ {
+ qWarning() << "Could not open" << liveCheckFile << "for writing!";
+ break;
+ }
+ auto payload = appID.toString().toUtf8();
+ if(check.write(payload) != payload.size())
+ {
+ qWarning() << "Could not write into" << liveCheckFile << "!";
+ check.remove();
+ break;
+ }
+ check.close();
+ }
+ } while(false);
+
+ // Initialize application settings
+ {
+ m_settings.reset(new INISettingsObject("multimc.cfg", this));
+ // Updates
+ m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL);
+ m_settings->registerSetting("AutoUpdate", true);
+
+ // Theming
+ m_settings->registerSetting("IconTheme", QString("multimc"));
+ m_settings->registerSetting("ApplicationTheme", QString("system"));
+
+ // Notifications
+ m_settings->registerSetting("ShownNotifications", QString());
+
+ // Remembered state
+ m_settings->registerSetting("LastUsedGroupForNewInstance", QString());
+
+ QString defaultMonospace;
+ int defaultSize = 11;
+#ifdef Q_OS_WIN32
+ defaultMonospace = "Courier";
+ defaultSize = 10;
+#elif defined(Q_OS_MAC)
+ defaultMonospace = "Menlo";
+#else
+ defaultMonospace = "Monospace";
+#endif
+
+ // resolve the font so the default actually matches
+ QFont consoleFont;
+ consoleFont.setFamily(defaultMonospace);
+ consoleFont.setStyleHint(QFont::Monospace);
+ consoleFont.setFixedPitch(true);
+ QFontInfo consoleFontInfo(consoleFont);
+ QString resolvedDefaultMonospace = consoleFontInfo.family();
+ QFont resolvedFont(resolvedDefaultMonospace);
+ qDebug() << "Detected default console font:" << resolvedDefaultMonospace
+ << ", substitutions:" << resolvedFont.substitutions().join(',');
+
+ m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace);
+ m_settings->registerSetting("ConsoleFontSize", defaultSize);
+ m_settings->registerSetting("ConsoleMaxLines", 100000);
+ m_settings->registerSetting("ConsoleOverflowStop", true);
+
+ // Folders
+ m_settings->registerSetting("InstanceDir", "instances");
+ m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods");
+ m_settings->registerSetting("IconsDir", "icons");
+
+ // Editors
+ m_settings->registerSetting("JsonEditor", QString());
+
+ // Language
+ m_settings->registerSetting("Language", QString());
+
+ // Console
+ m_settings->registerSetting("ShowConsole", false);
+ m_settings->registerSetting("AutoCloseConsole", false);
+ m_settings->registerSetting("ShowConsoleOnError", true);
+ m_settings->registerSetting("LogPrePostOutput", true);
+
+ // Window Size
+ m_settings->registerSetting({"LaunchMaximized", "MCWindowMaximize"}, false);
+ m_settings->registerSetting({"MinecraftWinWidth", "MCWindowWidth"}, 854);
+ m_settings->registerSetting({"MinecraftWinHeight", "MCWindowHeight"}, 480);
+
+ // Proxy Settings
+ m_settings->registerSetting("ProxyType", "None");
+ m_settings->registerSetting({"ProxyAddr", "ProxyHostName"}, "127.0.0.1");
+ m_settings->registerSetting("ProxyPort", 8080);
+ m_settings->registerSetting({"ProxyUser", "ProxyUsername"}, "");
+ m_settings->registerSetting({"ProxyPass", "ProxyPassword"}, "");
+
+ // Memory
+ m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512);
+ m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024);
+ m_settings->registerSetting("PermGen", 128);
+
+ // Java Settings
+ m_settings->registerSetting("JavaPath", "");
+ m_settings->registerSetting("JavaTimestamp", 0);
+ m_settings->registerSetting("JavaArchitecture", "");
+ m_settings->registerSetting("JavaVersion", "");
+ m_settings->registerSetting("JavaVendor", "");
+ m_settings->registerSetting("LastHostname", "");
+ m_settings->registerSetting("JvmArgs", "");
+
+ // Native library workarounds
+ m_settings->registerSetting("UseNativeOpenAL", false);
+ m_settings->registerSetting("UseNativeGLFW", false);
+
+ // Game time
+ m_settings->registerSetting("ShowGameTime", true);
+ m_settings->registerSetting("RecordGameTime", true);
+
+ // Minecraft launch method
+ m_settings->registerSetting("MCLaunchMethod", "LauncherPart");
+
+ // Wrapper command for launch
+ m_settings->registerSetting("WrapperCommand", "");
+
+ // Custom Commands
+ m_settings->registerSetting({"PreLaunchCommand", "PreLaunchCmd"}, "");
+ m_settings->registerSetting({"PostExitCommand", "PostExitCmd"}, "");
+
+ // The cat
+ m_settings->registerSetting("TheCat", false);
+
+ m_settings->registerSetting("InstSortMode", "Name");
+ m_settings->registerSetting("SelectedInstance", QString());
+
+ // Window state and geometry
+ m_settings->registerSetting("MainWindowState", "");
+ m_settings->registerSetting("MainWindowGeometry", "");
+
+ m_settings->registerSetting("ConsoleWindowState", "");
+ m_settings->registerSetting("ConsoleWindowGeometry", "");
+
+ m_settings->registerSetting("SettingsGeometry", "");
+
+ m_settings->registerSetting("PagedGeometry", "");
+
+ m_settings->registerSetting("NewInstanceGeometry", "");
+
+ m_settings->registerSetting("UpdateDialogGeometry", "");
+
+ // paste.ee API key
+ m_settings->registerSetting("PasteEEAPIKey", "multimc");
+
+ if(!BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ // Analytics
+ m_settings->registerSetting("Analytics", true);
+ m_settings->registerSetting("AnalyticsSeen", 0);
+ m_settings->registerSetting("AnalyticsClientID", QString());
+ }
+
+ // Init page provider
+ {
+ m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));
+ m_globalSettingsProvider->addPage<MultiMCPage>();
+ m_globalSettingsProvider->addPage<MinecraftPage>();
+ m_globalSettingsProvider->addPage<JavaPage>();
+ m_globalSettingsProvider->addPage<LanguagePage>();
+ m_globalSettingsProvider->addPage<CustomCommandsPage>();
+ m_globalSettingsProvider->addPage<ProxyPage>();
+ m_globalSettingsProvider->addPage<ExternalToolsPage>();
+ m_globalSettingsProvider->addPage<AccountListPage>();
+ m_globalSettingsProvider->addPage<PasteEEPage>();
+ }
+ qDebug() << "<> Settings loaded.";
+ }
+
+#ifndef QT_NO_ACCESSIBILITY
+ QAccessible::installFactory(groupViewAccessibleFactory);
+#endif /* !QT_NO_ACCESSIBILITY */
+
+ // load translations
+ {
+ m_translations.reset(new TranslationsModel("translations"));
+ auto bcp47Name = m_settings->get("Language").toString();
+ m_translations->selectLanguage(bcp47Name);
+ qDebug() << "Your language is" << bcp47Name;
+ qDebug() << "<> Translations loaded.";
+ }
+
+ // initialize the updater
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ m_updateChecker.reset(new UpdateChecker(BuildConfig.CHANLIST_URL, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD));
+ qDebug() << "<> Updater started.";
+ }
+
+ // Instance icons
+ {
+ auto setting = MMC->settings()->getSetting("IconsDir");
+ QStringList instFolders =
+ {
+ ":/icons/multimc/32x32/instances/",
+ ":/icons/multimc/50x50/instances/",
+ ":/icons/multimc/128x128/instances/",
+ ":/icons/multimc/scalable/instances/"
+ };
+ m_icons.reset(new IconList(instFolders, setting->get().toString()));
+ connect(setting.get(), &Setting::SettingChanged,[&](const Setting &, QVariant value)
+ {
+ m_icons->directoryChanged(value.toString());
+ });
+ ENV.registerIconList(m_icons);
+ qDebug() << "<> Instance icons intialized.";
+ }
+
+ // Icon themes
+ {
+ // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies!
+ // set icon theme search path!
+ auto searchPaths = QIcon::themeSearchPaths();
+ searchPaths.append("iconthemes");
+ QIcon::setThemeSearchPaths(searchPaths);
+ qDebug() << "<> Icon themes initialized.";
+ }
+
+ // Initialize widget themes
+ {
+ auto insertTheme = [this](ITheme * theme)
+ {
+ m_themes.insert(std::make_pair(theme->id(), std::unique_ptr<ITheme>(theme)));
+ };
+ auto darkTheme = new DarkTheme();
+ insertTheme(new SystemTheme());
+ insertTheme(darkTheme);
+ insertTheme(new BrightTheme());
+ insertTheme(new CustomTheme(darkTheme, "custom"));
+ qDebug() << "<> Widget themes initialized.";
+ }
+
+ // initialize and load all instances
+ {
+ auto InstDirSetting = m_settings->getSetting("InstanceDir");
+ // instance path: check for problems with '!' in instance path and warn the user in the log
+ // and remember that we have to show him a dialog when the gui starts (if it does so)
+ QString instDir = InstDirSetting->get().toString();
+ qDebug() << "Instance path : " << instDir;
+ if (FS::checkProblemticPathJava(QDir(instDir)))
+ {
+ qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!";
+ }
+ m_instances.reset(new InstanceList(m_settings, instDir, this));
+ connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged);
+ qDebug() << "Loading Instances...";
+ m_instances->loadList();
+ qDebug() << "<> Instances loaded.";
+ }
+
+ // and accounts
+ {
+ m_accounts.reset(new MojangAccountList(this));
+ qDebug() << "Loading accounts...";
+ m_accounts->setListFilePath("accounts.json", true);
+ m_accounts->loadList();
+ qDebug() << "<> Accounts loaded.";
+ }
+
+ // init the http meta cache
+ {
+ ENV.initHttpMetaCache();
+ qDebug() << "<> Cache initialized.";
+ }
+
+ // init proxy settings
+ {
+ QString proxyTypeStr = settings()->get("ProxyType").toString();
+ QString addr = settings()->get("ProxyAddr").toString();
+ int port = settings()->get("ProxyPort").value<qint16>();
+ QString user = settings()->get("ProxyUser").toString();
+ QString pass = settings()->get("ProxyPass").toString();
+ ENV.updateProxySettings(proxyTypeStr, addr, port, user, pass);
+ qDebug() << "<> Proxy settings done.";
+ }
+
+ // now we have network, download translation updates
+ m_translations->downloadIndex();
+
+ //FIXME: what to do with these?
+ m_profilers.insert("jprofiler", std::shared_ptr<BaseProfilerFactory>(new JProfilerFactory()));
+ m_profilers.insert("jvisualvm", std::shared_ptr<BaseProfilerFactory>(new JVisualVMFactory()));
+ for (auto profiler : m_profilers.values())
+ {
+ profiler->registerSettings(m_settings);
+ }
+
+ // Create the MCEdit thing... why is this here?
+ {
+ m_mcedit.reset(new MCEditTool(m_settings));
+ }
+
+ connect(this, &MultiMC::aboutToQuit, [this](){
+ if(m_instances)
+ {
+ // save any remaining instance state
+ m_instances->saveNow();
+ }
+ if(logFile)
+ {
+ logFile->flush();
+ logFile->close();
+ }
+ });
+
+ {
+ setIconTheme(settings()->get("IconTheme").toString());
+ qDebug() << "<> Icon theme set.";
+ setApplicationTheme(settings()->get("ApplicationTheme").toString(), true);
+ qDebug() << "<> Application theme set.";
+ }
+
+ // Initialize analytics
+ [this]()
+ {
+ const int analyticsVersion = 2;
+ if(BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ return;
+ }
+
+ auto analyticsSetting = m_settings->getSetting("Analytics");
+ connect(analyticsSetting.get(), &Setting::SettingChanged, this, &MultiMC::analyticsSettingChanged);
+ QString clientID = m_settings->get("AnalyticsClientID").toString();
+ if(clientID.isEmpty())
+ {
+ clientID = QUuid::createUuid().toString();
+ clientID.remove(QLatin1Char('{'));
+ clientID.remove(QLatin1Char('}'));
+ m_settings->set("AnalyticsClientID", clientID);
+ }
+ m_analytics = new GAnalytics(BuildConfig.ANALYTICS_ID, clientID, analyticsVersion, this);
+ m_analytics->setLogLevel(GAnalytics::Debug);
+ m_analytics->setAnonymizeIPs(true);
+ m_analytics->setNetworkAccessManager(&ENV.qnam());
+
+ if(m_settings->get("AnalyticsSeen").toInt() < m_analytics->version())
+ {
+ qDebug() << "Analytics info not seen by user yet (or old version).";
+ return;
+ }
+ if(!m_settings->get("Analytics").toBool())
+ {
+ qDebug() << "Analytics disabled by user.";
+ return;
+ }
+
+ m_analytics->enable();
+ qDebug() << "<> Initialized analytics with tid" << BuildConfig.ANALYTICS_ID;
+ }();
+
+ if(createSetupWizard())
+ {
+ return;
+ }
+ performMainStartupAction();
+}
+
+bool MultiMC::createSetupWizard()
+{
+ bool javaRequired = [&]()
+ {
+ QString currentHostName = QHostInfo::localHostName();
+ QString oldHostName = settings()->get("LastHostname").toString();
+ if (currentHostName != oldHostName)
+ {
+ settings()->set("LastHostname", currentHostName);
+ return true;
+ }
+ QString currentJavaPath = settings()->get("JavaPath").toString();
+ QString actualPath = FS::ResolveExecutable(currentJavaPath);
+ if (actualPath.isNull())
+ {
+ return true;
+ }
+ return false;
+ }();
+ bool analyticsRequired = [&]()
+ {
+ if(BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ return false;
+ }
+ if (!settings()->get("Analytics").toBool())
+ {
+ return false;
+ }
+ if (settings()->get("AnalyticsSeen").toInt() < analytics()->version())
+ {
+ return true;
+ }
+ return false;
+ }();
+ bool languageRequired = [&]()
+ {
+ if (settings()->get("Language").toString().isEmpty())
+ return true;
+ return false;
+ }();
+ bool wizardRequired = javaRequired || analyticsRequired || languageRequired;
+
+ if(wizardRequired)
+ {
+ m_setupWizard = new SetupWizard(nullptr);
+ if (languageRequired)
+ {
+ m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard));
+ }
+ if (javaRequired)
+ {
+ m_setupWizard->addPage(new JavaWizardPage(m_setupWizard));
+ }
+ if(analyticsRequired)
+ {
+ m_setupWizard->addPage(new AnalyticsWizardPage(m_setupWizard));
+ }
+ connect(m_setupWizard, &QDialog::finished, this, &MultiMC::setupWizardFinished);
+ m_setupWizard->show();
+ return true;
+ }
+ return false;
+}
+
+void MultiMC::setupWizardFinished(int status)
+{
+ qDebug() << "Wizard result =" << status;
+ performMainStartupAction();
+}
+
+void MultiMC::performMainStartupAction()
+{
+ m_status = MultiMC::Initialized;
+ if(!m_instanceIdToLaunch.isEmpty())
+ {
+ auto inst = instances()->getInstanceById(m_instanceIdToLaunch);
+ if(inst)
+ {
+ MinecraftServerTargetPtr serverToJoin = nullptr;
+
+ if(!m_serverToJoin.isEmpty())
+ {
+ serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin)));
+ qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching with server" << m_serverToJoin;
+ }
+ else
+ {
+ qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching";
+ }
+
+ launch(inst, true, nullptr, serverToJoin);
+ return;
+ }
+ }
+ if(!m_mainWindow)
+ {
+ // normal main window
+ showMainWindow(false);
+ qDebug() << "<> Main window shown.";
+ }
+ if(!m_zipToImport.isEmpty())
+ {
+ qDebug() << "<> Importing instance from zip:" << m_zipToImport;
+ m_mainWindow->droppedURLs({ m_zipToImport });
+ }
+}
+
+void MultiMC::showFatalErrorMessage(const QString& title, const QString& content)
+{
+ m_status = MultiMC::Failed;
+ auto dialog = CustomMessageBox::selectable(nullptr, title, content, QMessageBox::Critical);
+ dialog->exec();
+}
+
+MultiMC::~MultiMC()
+{
+ // kill the other globals.
+ Env::dispose();
+
+ // Shut down logger by setting the logger function to nothing
+ qInstallMessageHandler(nullptr);
+
+#if defined Q_OS_WIN32
+ // Detach from Windows console
+ if(consoleAttached)
+ {
+ fclose(stdout);
+ fclose(stdin);
+ fclose(stderr);
+ FreeConsole();
+ }
+#endif
+}
+
+void MultiMC::messageReceived(const QString& message)
+{
+ if(status() != Initialized)
+ {
+ qDebug() << "Received message" << message << "while still initializing. It will be ignored.";
+ return;
+ }
+
+ QString command = message.section(' ', 0, 0);
+
+ if(command == "activate")
+ {
+ showMainWindow();
+ }
+ else if(command == "import")
+ {
+ QString arg = message.section(' ', 1);
+ if(arg.isEmpty())
+ {
+ qWarning() << "Received" << command << "message without a zip path/URL.";
+ return;
+ }
+ m_mainWindow->droppedURLs({ QUrl(arg) });
+ }
+ else if(command == "launch")
+ {
+ QString arg = message.section(' ', 1);
+ if(arg.isEmpty())
+ {
+ qWarning() << "Received" << command << "message without an instance ID.";
+ return;
+ }
+ auto inst = instances()->getInstanceById(arg);
+ if(inst)
+ {
+ launch(inst, true, nullptr);
+ }
+ }
+ else if(command == "launch-with-server")
+ {
+ QString instanceID = message.section(' ', 1, 1);
+ QString serverToJoin = message.section(' ', 2, 2);
+ if(instanceID.isEmpty())
+ {
+ qWarning() << "Received" << command << "message without an instance ID.";
+ return;
+ }
+ if(serverToJoin.isEmpty())
+ {
+ qWarning() << "Received" << command << "message without a server to join.";
+ return;
+ }
+ auto inst = instances()->getInstanceById(instanceID);
+ if(inst)
+ {
+ launch(
+ inst,
+ true,
+ nullptr,
+ std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(serverToJoin))
+ );
+ }
+ }
+ else
+ {
+ qWarning() << "Received invalid message" << message;
+ }
+}
+
+void MultiMC::analyticsSettingChanged(const Setting&, QVariant value)
+{
+ if(!m_analytics)
+ return;
+ bool enabled = value.toBool();
+ if(enabled)
+ {
+ qDebug() << "Analytics enabled by user.";
+ }
+ else
+ {
+ qDebug() << "Analytics disabled by user.";
+ }
+ m_analytics->enable(enabled);
+}
+
+std::shared_ptr<TranslationsModel> MultiMC::translations()
+{
+ return m_translations;
+}
+
+std::shared_ptr<JavaInstallList> MultiMC::javalist()
+{
+ if (!m_javalist)
+ {
+ m_javalist.reset(new JavaInstallList());
+ }
+ return m_javalist;
+}
+
+std::vector<ITheme *> MultiMC::getValidApplicationThemes()
+{
+ std::vector<ITheme *> ret;
+ auto iter = m_themes.cbegin();
+ while (iter != m_themes.cend())
+ {
+ ret.push_back((*iter).second.get());
+ iter++;
+ }
+ return ret;
+}
+
+void MultiMC::setApplicationTheme(const QString& name, bool initial)
+{
+ auto systemPalette = qApp->palette();
+ auto themeIter = m_themes.find(name);
+ if(themeIter != m_themes.end())
+ {
+ auto & theme = (*themeIter).second;
+ theme->apply(initial);
+ }
+ else
+ {
+ qWarning() << "Tried to set invalid theme:" << name;
+ }
+}
+
+void MultiMC::setIconTheme(const QString& name)
+{
+ XdgIcon::setThemeName(name);
+}
+
+QIcon MultiMC::getThemedIcon(const QString& name)
+{
+ return XdgIcon::fromTheme(name);
+}
+
+bool MultiMC::openJsonEditor(const QString &filename)
+{
+ const QString file = QDir::current().absoluteFilePath(filename);
+ if (m_settings->get("JsonEditor").toString().isEmpty())
+ {
+ return DesktopServices::openUrl(QUrl::fromLocalFile(file));
+ }
+ else
+ {
+ //return DesktopServices::openFile(m_settings->get("JsonEditor").toString(), file);
+ return DesktopServices::run(m_settings->get("JsonEditor").toString(), {file});
+ }
+}
+
+bool MultiMC::launch(
+ InstancePtr instance,
+ bool online,
+ BaseProfilerFactory *profiler,
+ MinecraftServerTargetPtr serverToJoin
+) {
+ if(m_updateRunning)
+ {
+ qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed.";
+ }
+ else if(instance->canLaunch())
+ {
+ auto & extras = m_instanceExtras[instance->id()];
+ auto & window = extras.window;
+ if(window)
+ {
+ if(!window->saveAll())
+ {
+ return false;
+ }
+ }
+ auto & controller = extras.controller;
+ controller.reset(new LaunchController());
+ controller->setInstance(instance);
+ controller->setOnline(online);
+ controller->setProfiler(profiler);
+ controller->setServerToJoin(serverToJoin);
+ if(window)
+ {
+ controller->setParentWidget(window);
+ }
+ else if(m_mainWindow)
+ {
+ controller->setParentWidget(m_mainWindow);
+ }
+ connect(controller.get(), &LaunchController::succeeded, this, &MultiMC::controllerSucceeded);
+ connect(controller.get(), &LaunchController::failed, this, &MultiMC::controllerFailed);
+ addRunningInstance();
+ controller->start();
+ return true;
+ }
+ else if (instance->isRunning())
+ {
+ showInstanceWindow(instance, "console");
+ return true;
+ }
+ else if (instance->canEdit())
+ {
+ showInstanceWindow(instance);
+ return true;
+ }
+ return false;
+}
+
+bool MultiMC::kill(InstancePtr instance)
+{
+ if (!instance->isRunning())
+ {
+ qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running.";
+ return false;
+ }
+ auto & extras = m_instanceExtras[instance->id()];
+ // NOTE: copy of the shared pointer keeps it alive
+ auto controller = extras.controller;
+ if(controller)
+ {
+ return controller->abort();
+ }
+ return true;
+}
+
+void MultiMC::addRunningInstance()
+{
+ m_runningInstances ++;
+ if(m_runningInstances == 1)
+ {
+ emit updateAllowedChanged(false);
+ }
+}
+
+void MultiMC::subRunningInstance()
+{
+ if(m_runningInstances == 0)
+ {
+ qCritical() << "Something went really wrong and we now have less than 0 running instances... WTF";
+ return;
+ }
+ m_runningInstances --;
+ if(m_runningInstances == 0)
+ {
+ emit updateAllowedChanged(true);
+ }
+}
+
+bool MultiMC::shouldExitNow() const
+{
+ return m_runningInstances == 0 && m_openWindows == 0;
+}
+
+bool MultiMC::updatesAreAllowed()
+{
+ return m_runningInstances == 0;
+}
+
+void MultiMC::updateIsRunning(bool running)
+{
+ m_updateRunning = running;
+}
+
+
+void MultiMC::controllerSucceeded()
+{
+ auto controller = qobject_cast<LaunchController *>(QObject::sender());
+ if(!controller)
+ return;
+ auto id = controller->id();
+ auto & extras = m_instanceExtras[id];
+
+ // on success, do...
+ if (controller->instance()->settings()->get("AutoCloseConsole").toBool())
+ {
+ if(extras.window)
+ {
+ extras.window->close();
+ }
+ }
+ extras.controller.reset();
+ subRunningInstance();
+
+ // quit when there are no more windows.
+ if(shouldExitNow())
+ {
+ m_status = Status::Succeeded;
+ exit(0);
+ }
+}
+
+void MultiMC::controllerFailed(const QString& error)
+{
+ Q_UNUSED(error);
+ auto controller = qobject_cast<LaunchController *>(QObject::sender());
+ if(!controller)
+ return;
+ auto id = controller->id();
+ auto & extras = m_instanceExtras[id];
+
+ // on failure, do... nothing
+ extras.controller.reset();
+ subRunningInstance();
+
+ // quit when there are no more windows.
+ if(shouldExitNow())
+ {
+ m_status = Status::Failed;
+ exit(1);
+ }
+}
+
+void MultiMC::ShowGlobalSettings(class QWidget* parent, QString open_page)
+{
+ if(!m_globalSettingsProvider) {
+ return;
+ }
+ emit globalSettingsAboutToOpen();
+ {
+ SettingsObject::Lock lock(MMC->settings());
+ PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent);
+ dlg.exec();
+ }
+ emit globalSettingsClosed();
+}
+
+MainWindow* MultiMC::showMainWindow(bool minimized)
+{
+ if(m_mainWindow)
+ {
+ m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized);
+ m_mainWindow->raise();
+ m_mainWindow->activateWindow();
+ }
+ else
+ {
+ m_mainWindow = new MainWindow();
+ m_mainWindow->restoreState(QByteArray::fromBase64(MMC->settings()->get("MainWindowState").toByteArray()));
+ m_mainWindow->restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("MainWindowGeometry").toByteArray()));
+ if(minimized)
+ {
+ m_mainWindow->showMinimized();
+ }
+ else
+ {
+ m_mainWindow->show();
+ }
+
+ m_mainWindow->checkInstancePathForProblems();
+ connect(this, &MultiMC::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged);
+ connect(m_mainWindow, &MainWindow::isClosing, this, &MultiMC::on_windowClose);
+ m_openWindows++;
+ }
+ // FIXME: move this somewhere else...
+ if(m_analytics)
+ {
+ auto windowSize = m_mainWindow->size();
+ auto sizeString = QString("%1x%2").arg(windowSize.width()).arg(windowSize.height());
+ qDebug() << "Viewport size" << sizeString;
+ m_analytics->setViewportSize(sizeString);
+ /*
+ * cm1 = java min heap [MB]
+ * cm2 = java max heap [MB]
+ * cm3 = system RAM [MB]
+ *
+ * cd1 = java version
+ * cd2 = java architecture
+ * cd3 = system architecture
+ * cd4 = CPU architecture
+ */
+ QVariantMap customValues;
+ int min = m_settings->get("MinMemAlloc").toInt();
+ int max = m_settings->get("MaxMemAlloc").toInt();
+ if(min < max)
+ {
+ customValues["cm1"] = min;
+ customValues["cm2"] = max;
+ }
+ else
+ {
+ customValues["cm1"] = max;
+ customValues["cm2"] = min;
+ }
+
+ constexpr uint64_t Mega = 1024ull * 1024ull;
+ int ramSize = int(Sys::getSystemRam() / Mega);
+ qDebug() << "RAM size is" << ramSize << "MB";
+ customValues["cm3"] = ramSize;
+
+ customValues["cd1"] = m_settings->get("JavaVersion");
+ customValues["cd2"] = m_settings->get("JavaArchitecture");
+ customValues["cd3"] = Sys::isSystem64bit() ? "64":"32";
+ customValues["cd4"] = Sys::isCPU64bit() ? "64":"32";
+ auto kernelInfo = Sys::getKernelInfo();
+ customValues["cd5"] = kernelInfo.kernelName;
+ customValues["cd6"] = kernelInfo.kernelVersion;
+ auto distInfo = Sys::getDistributionInfo();
+ if(!distInfo.distributionName.isEmpty())
+ {
+ customValues["cd7"] = distInfo.distributionName;
+ }
+ if(!distInfo.distributionVersion.isEmpty())
+ {
+ customValues["cd8"] = distInfo.distributionVersion;
+ }
+ m_analytics->sendScreenView("Main Window", customValues);
+ }
+ return m_mainWindow;
+}
+
+InstanceWindow *MultiMC::showInstanceWindow(InstancePtr instance, QString page)
+{
+ if(!instance)
+ return nullptr;
+ auto id = instance->id();
+ auto & extras = m_instanceExtras[id];
+ auto & window = extras.window;
+
+ if(window)
+ {
+ window->raise();
+ window->activateWindow();
+ }
+ else
+ {
+ window = new InstanceWindow(instance);
+ m_openWindows ++;
+ connect(window, &InstanceWindow::isClosing, this, &MultiMC::on_windowClose);
+ }
+ if(!page.isEmpty())
+ {
+ window->selectPage(page);
+ }
+ if(extras.controller)
+ {
+ extras.controller->setParentWidget(window);
+ }
+ return window;
+}
+
+void MultiMC::on_windowClose()
+{
+ m_openWindows--;
+ auto instWindow = qobject_cast<InstanceWindow *>(QObject::sender());
+ if(instWindow)
+ {
+ auto & extras = m_instanceExtras[instWindow->instanceId()];
+ extras.window = nullptr;
+ if(extras.controller)
+ {
+ extras.controller->setParentWidget(m_mainWindow);
+ }
+ }
+ auto mainWindow = qobject_cast<MainWindow *>(QObject::sender());
+ if(mainWindow)
+ {
+ m_mainWindow = nullptr;
+ }
+ // quit when there are no more windows.
+ if(shouldExitNow())
+ {
+ exit(0);
+ }
+}
diff --git a/launcher/MultiMC.h b/launcher/MultiMC.h
new file mode 100644
index 00000000..af2b41c1
--- /dev/null
+++ b/launcher/MultiMC.h
@@ -0,0 +1,235 @@
+#pragma once
+
+#include <QApplication>
+#include <memory>
+#include <QDebug>
+#include <QFlag>
+#include <QIcon>
+#include <QDateTime>
+#include <QUrl>
+#include <updater/GoUpdate.h>
+
+#include <BaseInstance.h>
+
+#include "minecraft/launch/MinecraftServerTarget.h"
+
+class LaunchController;
+class LocalPeer;
+class InstanceWindow;
+class MainWindow;
+class SetupWizard;
+class FolderInstanceProvider;
+class GenericPageProvider;
+class QFile;
+class HttpMetaCache;
+class SettingsObject;
+class InstanceList;
+class MojangAccountList;
+class IconList;
+class QNetworkAccessManager;
+class JavaInstallList;
+class UpdateChecker;
+class BaseProfilerFactory;
+class BaseDetachedToolFactory;
+class TranslationsModel;
+class ITheme;
+class MCEditTool;
+class GAnalytics;
+
+#if defined(MMC)
+#undef MMC
+#endif
+#define MMC (static_cast<MultiMC *>(QCoreApplication::instance()))
+
+class MultiMC : public QApplication
+{
+ // friends for the purpose of limiting access to deprecated stuff
+ Q_OBJECT
+public:
+ enum Status
+ {
+ StartingUp,
+ Failed,
+ Succeeded,
+ Initialized
+ };
+
+public:
+ MultiMC(int &argc, char **argv);
+ virtual ~MultiMC();
+
+ GAnalytics *analytics() const
+ {
+ return m_analytics;
+ }
+
+ std::shared_ptr<SettingsObject> settings() const
+ {
+ return m_settings;
+ }
+
+ qint64 timeSinceStart() const
+ {
+ return startTime.msecsTo(QDateTime::currentDateTime());
+ }
+
+ QIcon getThemedIcon(const QString& name);
+
+ void setIconTheme(const QString& name);
+
+ std::vector<ITheme *> getValidApplicationThemes();
+
+ void setApplicationTheme(const QString& name, bool initial);
+
+ // DownloadUpdateTask
+ std::shared_ptr<UpdateChecker> updateChecker()
+ {
+ return m_updateChecker;
+ }
+
+ std::shared_ptr<TranslationsModel> translations();
+
+ std::shared_ptr<JavaInstallList> javalist();
+
+ std::shared_ptr<InstanceList> instances() const
+ {
+ return m_instances;
+ }
+
+ FolderInstanceProvider * folderProvider() const
+ {
+ return m_instanceFolder;
+ }
+
+ std::shared_ptr<IconList> icons() const
+ {
+ return m_icons;
+ }
+
+ MCEditTool *mcedit() const
+ {
+ return m_mcedit.get();
+ }
+
+ std::shared_ptr<MojangAccountList> accounts() const
+ {
+ return m_accounts;
+ }
+
+ Status status() const
+ {
+ return m_status;
+ }
+
+ const QMap<QString, std::shared_ptr<BaseProfilerFactory>> &profilers() const
+ {
+ return m_profilers;
+ }
+
+ /// this is the root of the 'installation'. Used for automatic updates
+ const QString &root()
+ {
+ return m_rootPath;
+ }
+
+ /*!
+ * Opens a json file using either a system default editor, or, if not empty, the editor
+ * specified in the settings
+ */
+ bool openJsonEditor(const QString &filename);
+
+ InstanceWindow *showInstanceWindow(InstancePtr instance, QString page = QString());
+ MainWindow *showMainWindow(bool minimized = false);
+
+ void updateIsRunning(bool running);
+ bool updatesAreAllowed();
+
+ void ShowGlobalSettings(class QWidget * parent, QString open_page = QString());
+
+signals:
+ void updateAllowedChanged(bool status);
+ void globalSettingsAboutToOpen();
+ void globalSettingsClosed();
+
+public slots:
+ bool launch(
+ InstancePtr instance,
+ bool online = true,
+ BaseProfilerFactory *profiler = nullptr,
+ MinecraftServerTargetPtr serverToJoin = nullptr
+ );
+ bool kill(InstancePtr instance);
+
+private slots:
+ void on_windowClose();
+ void messageReceived(const QString & message);
+ void controllerSucceeded();
+ void controllerFailed(const QString & error);
+ void analyticsSettingChanged(const Setting &setting, QVariant value);
+ void setupWizardFinished(int status);
+
+private:
+ bool createSetupWizard();
+ void performMainStartupAction();
+
+ // sets the fatal error message and m_status to Failed.
+ void showFatalErrorMessage(const QString & title, const QString & content);
+
+private:
+ void addRunningInstance();
+ void subRunningInstance();
+ bool shouldExitNow() const;
+
+private:
+ QDateTime startTime;
+
+ std::shared_ptr<SettingsObject> m_settings;
+ std::shared_ptr<InstanceList> m_instances;
+ FolderInstanceProvider * m_instanceFolder = nullptr;
+ std::shared_ptr<IconList> m_icons;
+ std::shared_ptr<UpdateChecker> m_updateChecker;
+ std::shared_ptr<MojangAccountList> m_accounts;
+ std::shared_ptr<JavaInstallList> m_javalist;
+ std::shared_ptr<TranslationsModel> m_translations;
+ std::shared_ptr<GenericPageProvider> m_globalSettingsProvider;
+ std::map<QString, std::unique_ptr<ITheme>> m_themes;
+ std::unique_ptr<MCEditTool> m_mcedit;
+
+ QMap<QString, std::shared_ptr<BaseProfilerFactory>> m_profilers;
+
+ QString m_rootPath;
+ Status m_status = MultiMC::StartingUp;
+
+#if defined Q_OS_WIN32
+ // used on Windows to attach the standard IO streams
+ bool consoleAttached = false;
+#endif
+
+ // FIXME: attach to instances instead.
+ struct InstanceXtras
+ {
+ InstanceWindow * window = nullptr;
+ shared_qobject_ptr<LaunchController> controller;
+ };
+ std::map<QString, InstanceXtras> m_instanceExtras;
+
+ // main state variables
+ size_t m_openWindows = 0;
+ size_t m_runningInstances = 0;
+ bool m_updateRunning = false;
+
+ // main window, if any
+ MainWindow * m_mainWindow = nullptr;
+
+ // peer MultiMC instance connector - used to implement single instance MultiMC and signalling
+ LocalPeer * m_peerInstance = nullptr;
+
+ GAnalytics * m_analytics = nullptr;
+ SetupWizard * m_setupWizard = nullptr;
+public:
+ QString m_instanceIdToLaunch;
+ QString m_serverToJoin;
+ bool m_liveCheck = false;
+ QUrl m_zipToImport;
+ std::unique_ptr<QFile> logFile;
+};
diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h
new file mode 100644
index 00000000..94ed6c3a
--- /dev/null
+++ b/launcher/NullInstance.h
@@ -0,0 +1,76 @@
+#pragma once
+#include "BaseInstance.h"
+#include "launch/LaunchTask.h"
+
+class NullInstance: public BaseInstance
+{
+ Q_OBJECT
+public:
+ NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir)
+ :BaseInstance(globalSettings, settings, rootDir)
+ {
+ setVersionBroken(true);
+ }
+ virtual ~NullInstance() {};
+ void saveNow() override
+ {
+ }
+ QString getStatusbarDescription() override
+ {
+ return tr("Unknown instance type");
+ };
+ QSet< QString > traits() const override
+ {
+ return {};
+ };
+ QString instanceConfigFolder() const override
+ {
+ return instanceRoot();
+ };
+ shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override
+ {
+ return nullptr;
+ }
+ shared_qobject_ptr< Task > createUpdateTask(Net::Mode mode) override
+ {
+ return nullptr;
+ }
+ QProcessEnvironment createEnvironment() override
+ {
+ return QProcessEnvironment();
+ }
+ QMap<QString, QString> getVariables() const override
+ {
+ return QMap<QString, QString>();
+ }
+ IPathMatcher::Ptr getLogFileMatcher() override
+ {
+ return nullptr;
+ }
+ QString getLogFileRoot() override
+ {
+ return instanceRoot();
+ }
+ QString typeName() const override
+ {
+ return "Null";
+ }
+ bool canExport() const override
+ {
+ return false;
+ }
+ bool canEdit() const override
+ {
+ return false;
+ }
+ bool canLaunch() const override
+ {
+ return false;
+ }
+ QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override
+ {
+ QStringList out;
+ out << "Null instance - placeholder.";
+ return out;
+ }
+};
diff --git a/launcher/ProblemProvider.h b/launcher/ProblemProvider.h
new file mode 100644
index 00000000..cd4745fa
--- /dev/null
+++ b/launcher/ProblemProvider.h
@@ -0,0 +1,47 @@
+#pragma once
+
+enum class ProblemSeverity
+{
+ None,
+ Warning,
+ Error
+};
+
+struct PatchProblem
+{
+ ProblemSeverity m_severity;
+ QString m_description;
+};
+
+class ProblemProvider
+{
+public:
+ virtual ~ProblemProvider() {};
+ virtual const QList<PatchProblem> getProblems() const = 0;
+ virtual ProblemSeverity getProblemSeverity() const = 0;
+};
+
+class ProblemContainer : public ProblemProvider
+{
+public:
+ const QList<PatchProblem> getProblems() const override
+ {
+ return m_problems;
+ }
+ ProblemSeverity getProblemSeverity() const override
+ {
+ return m_problemSeverity;
+ }
+ virtual void addProblem(ProblemSeverity severity, const QString &description)
+ {
+ if(severity > m_problemSeverity)
+ {
+ m_problemSeverity = severity;
+ }
+ m_problems.append({severity, description});
+ }
+
+private:
+ QList<PatchProblem> m_problems;
+ ProblemSeverity m_problemSeverity = ProblemSeverity::None;
+};
diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h
new file mode 100644
index 00000000..0ff51136
--- /dev/null
+++ b/launcher/QObjectPtr.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <functional>
+#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(const shared_qobject_ptr<T> &other)
+ {
+ m_ptr = other.m_ptr;
+ }
+ 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/launcher/RWStorage.h b/launcher/RWStorage.h
new file mode 100644
index 00000000..3028388e
--- /dev/null
+++ b/launcher/RWStorage.h
@@ -0,0 +1,66 @@
+#pragma once
+#include <QWriteLocker>
+#include <QReadLocker>
+#include <QMap>
+#include <QSet>
+
+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)
+ {
+ QWriteLocker l(&lock);
+ if(cache.contains(key))
+ {
+ stale_entries.insert(key);
+ }
+ }
+ void clear()
+ {
+ QWriteLocker l(&lock);
+ cache.clear();
+ stale_entries.clear();
+ }
+private:
+ QReadWriteLock lock;
+ QMap<K, V> cache;
+ QSet<K> stale_entries;
+};
diff --git a/launcher/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp
new file mode 100644
index 00000000..b7417cdf
--- /dev/null
+++ b/launcher/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/launcher/RecursiveFileSystemWatcher.h b/launcher/RecursiveFileSystemWatcher.h
new file mode 100644
index 00000000..cc837d60
--- /dev/null
+++ b/launcher/RecursiveFileSystemWatcher.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <QFileSystemWatcher>
+#include <QDir>
+#include "pathmatcher/IPathMatcher.h"
+
+class 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/launcher/SeparatorPrefixTree.h b/launcher/SeparatorPrefixTree.h
new file mode 100644
index 00000000..7a841cb7
--- /dev/null
+++ b/launcher/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/launcher/SkinUtils.cpp b/launcher/SkinUtils.cpp
new file mode 100644
index 00000000..ec969889
--- /dev/null
+++ b/launcher/SkinUtils.cpp
@@ -0,0 +1,52 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "SkinUtils.h"
+#include "net/HttpMetaCache.h"
+#include "Env.h"
+
+#include <QFile>
+#include <QPainter>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+namespace SkinUtils
+{
+/*
+ * Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise
+ */
+QPixmap getFaceFromCache(QString username, int height, int width)
+{
+ QFile fskin(ENV.metacache()
+ ->resolveEntry("skins", username + ".png")
+ ->getFullPath());
+
+ if (fskin.exists())
+ {
+ QPixmap skinTexture(fskin.fileName());
+ if(!skinTexture.isNull())
+ {
+ QPixmap skin = QPixmap(8, 8);
+ QPainter painter(&skin);
+ painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
+ painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
+ return skin.scaled(height, width, Qt::KeepAspectRatio);
+ }
+ }
+
+ return QPixmap();
+}
+}
diff --git a/launcher/SkinUtils.h b/launcher/SkinUtils.h
new file mode 100644
index 00000000..c1f437ab
--- /dev/null
+++ b/launcher/SkinUtils.h
@@ -0,0 +1,23 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QPixmap>
+
+namespace SkinUtils
+{
+QPixmap getFaceFromCache(QString id, int height = 64, int width = 64);
+}
diff --git a/launcher/UpdateController.cpp b/launcher/UpdateController.cpp
new file mode 100644
index 00000000..0309ad93
--- /dev/null
+++ b/launcher/UpdateController.cpp
@@ -0,0 +1,449 @@
+#include <QFile>
+#include <QMessageBox>
+#include <FileSystem.h>
+#include <updater/GoUpdate.h>
+#include "UpdateController.h"
+#include <QApplication>
+#include <thread>
+#include <chrono>
+#include <LocalPeer.h>
+
+// from <sys/stat.h>
+#ifndef S_IRUSR
+#define __S_IREAD 0400 /* Read by owner. */
+#define __S_IWRITE 0200 /* Write by owner. */
+#define __S_IEXEC 0100 /* Execute by owner. */
+#define S_IRUSR __S_IREAD /* Read by owner. */
+#define S_IWUSR __S_IWRITE /* Write by owner. */
+#define S_IXUSR __S_IEXEC /* Execute by owner. */
+
+#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */
+#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */
+#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */
+
+#define S_IROTH (S_IRGRP >> 3) /* Read by others. */
+#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */
+#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */
+#endif
+static QFile::Permissions unixModeToPermissions(const int mode)
+{
+ QFile::Permissions perms;
+
+ if (mode & S_IRUSR)
+ {
+ perms |= QFile::ReadUser;
+ }
+ if (mode & S_IWUSR)
+ {
+ perms |= QFile::WriteUser;
+ }
+ if (mode & S_IXUSR)
+ {
+ perms |= QFile::ExeUser;
+ }
+
+ if (mode & S_IRGRP)
+ {
+ perms |= QFile::ReadGroup;
+ }
+ if (mode & S_IWGRP)
+ {
+ perms |= QFile::WriteGroup;
+ }
+ if (mode & S_IXGRP)
+ {
+ perms |= QFile::ExeGroup;
+ }
+
+ if (mode & S_IROTH)
+ {
+ perms |= QFile::ReadOther;
+ }
+ if (mode & S_IWOTH)
+ {
+ perms |= QFile::WriteOther;
+ }
+ if (mode & S_IXOTH)
+ {
+ perms |= QFile::ExeOther;
+ }
+ return perms;
+}
+
+static const QLatin1String liveCheckFile("live.check");
+
+UpdateController::UpdateController(QWidget * parent, const QString& root, const QString updateFilesDir, GoUpdate::OperationList operations)
+{
+ m_parent = parent;
+ m_root = root;
+ m_updateFilesDir = updateFilesDir;
+ m_operations = operations;
+}
+
+
+void UpdateController::installUpdates()
+{
+ qint64 pid = -1;
+ QStringList args;
+ bool started = false;
+
+ qDebug() << "Installing updates.";
+#ifdef Q_OS_WIN
+ QString finishCmd = QApplication::applicationFilePath();
+#elif defined Q_OS_LINUX
+ QString finishCmd = FS::PathCombine(m_root, "MultiMC");
+#elif defined Q_OS_MAC
+ QString finishCmd = QApplication::applicationFilePath();
+#else
+#error Unsupported operating system.
+#endif
+
+ QString backupPath = FS::PathCombine(m_root, "update", "backup");
+ QDir origin(m_root);
+
+ // clean up the backup folder. it should be empty before we start
+ if(!FS::deletePath(backupPath))
+ {
+ qWarning() << "couldn't remove previous backup folder" << backupPath;
+ }
+ // and it should exist.
+ if(!FS::ensureFolderPathExists(backupPath))
+ {
+ qWarning() << "couldn't create folder" << backupPath;
+ return;
+ }
+
+ bool useXPHack = false;
+ QString exePath;
+ QString exeOrigin;
+ QString exeBackup;
+
+ // perform the update operations
+ for(auto op: m_operations)
+ {
+ switch(op.type)
+ {
+ // replace = move original out to backup, if it exists, move the new file in its place
+ case GoUpdate::Operation::OP_REPLACE:
+ {
+#ifdef Q_OS_WIN32
+ // hack for people renaming the .exe because ... reasons :)
+ if(op.destination == "MultiMC.exe")
+ {
+ op.destination = QFileInfo(QApplication::applicationFilePath()).fileName();
+ }
+#endif
+ QFileInfo destination (FS::PathCombine(m_root, op.destination));
+#ifdef Q_OS_WIN32
+ if(QSysInfo::windowsVersion() < QSysInfo::WV_VISTA)
+ {
+ if(destination.fileName() == "MultiMC.exe")
+ {
+ QDir rootDir(m_root);
+ exeOrigin = rootDir.relativeFilePath(op.source);
+ exePath = rootDir.relativeFilePath(op.destination);
+ exeBackup = rootDir.relativeFilePath(FS::PathCombine(backupPath, destination.fileName()));
+ useXPHack = true;
+ continue;
+ }
+ }
+#endif
+ if(destination.exists())
+ {
+ QString backupName = op.destination;
+ backupName.replace('/', '_');
+ QString backupFilePath = FS::PathCombine(backupPath, backupName);
+ if(!QFile::rename(destination.absoluteFilePath(), backupFilePath))
+ {
+ qWarning() << "Couldn't move:" << destination.absoluteFilePath() << "to" << backupFilePath;
+ m_failedOperationType = Replace;
+ m_failedFile = op.destination;
+ fail();
+ return;
+ }
+ BackupEntry be;
+ be.original = destination.absoluteFilePath();
+ be.backup = backupFilePath;
+ be.update = op.source;
+ m_replace_backups.append(be);
+ }
+ // make sure the folder we are putting this into exists
+ if(!FS::ensureFilePathExists(destination.absoluteFilePath()))
+ {
+ qWarning() << "REPLACE: Couldn't create folder:" << destination.absoluteFilePath();
+ m_failedOperationType = Replace;
+ m_failedFile = op.destination;
+ fail();
+ return;
+ }
+ // now move the new file in
+ if(!QFile::rename(op.source, destination.absoluteFilePath()))
+ {
+ qWarning() << "REPLACE: Couldn't move:" << op.source << "to" << destination.absoluteFilePath();
+ m_failedOperationType = Replace;
+ m_failedFile = op.destination;
+ fail();
+ return;
+ }
+ QFile::setPermissions(destination.absoluteFilePath(), unixModeToPermissions(op.destinationMode));
+ }
+ break;
+ // delete = move original to backup
+ case GoUpdate::Operation::OP_DELETE:
+ {
+ QString destFilePath = FS::PathCombine(m_root, op.destination);
+ if(QFile::exists(destFilePath))
+ {
+ QString backupName = op.destination;
+ backupName.replace('/', '_');
+ QString trashFilePath = FS::PathCombine(backupPath, backupName);
+
+ if(!QFile::rename(destFilePath, trashFilePath))
+ {
+ qWarning() << "DELETE: Couldn't move:" << op.destination << "to" << trashFilePath;
+ m_failedFile = op.destination;
+ m_failedOperationType = Delete;
+ fail();
+ return;
+ }
+ BackupEntry be;
+ be.original = destFilePath;
+ be.backup = trashFilePath;
+ m_delete_backups.append(be);
+ }
+ }
+ break;
+ }
+ }
+
+ // try to start the new binary
+ args = qApp->arguments();
+ args.removeFirst();
+
+ // on old Windows, do insane things... no error checking here, this is just to have something.
+ if(useXPHack)
+ {
+ QString script;
+ auto nativePath = QDir::toNativeSeparators(exePath);
+ auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin);
+ auto nativeBackupPath = QDir::toNativeSeparators(exeBackup);
+
+ // so we write this vbscript thing...
+ QTextStream out(&script);
+ out << "WScript.Sleep 1000\n";
+ out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n";
+ out << "Set shell=CreateObject(\"WScript.Shell\")\n";
+ out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n";
+ out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n";
+ out << "shell.Run \"" << nativePath << "\"\n";
+
+ QString scriptPath = FS::PathCombine(m_root, "update", "update.vbs");
+
+ // we save it
+ QFile scriptFile(scriptPath);
+ scriptFile.open(QIODevice::WriteOnly);
+ scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n"));
+ scriptFile.close();
+
+ // we run it
+ started = QProcess::startDetached("wscript", {scriptPath}, m_root);
+
+ // and we quit. conscious thought.
+ qApp->quit();
+ return;
+ }
+ bool doLiveCheck = true;
+ bool startFailed = false;
+
+ // remove live check file, if any
+ if(QFile::exists(liveCheckFile))
+ {
+ if(!QFile::remove(liveCheckFile))
+ {
+ qWarning() << "Couldn't remove the" << liveCheckFile << "file! We will proceed without :(";
+ doLiveCheck = false;
+ }
+ }
+
+ if(doLiveCheck)
+ {
+ if(!args.contains("--alive"))
+ {
+ args.append("--alive");
+ }
+ }
+
+ // FIXME: reparse args and construct a safe variant from scratch. This is a workaround for GH-1874:
+ QStringList realargs;
+ int skip = 0;
+ for(auto & arg: args)
+ {
+ if(skip)
+ {
+ skip--;
+ continue;
+ }
+ if(arg == "-l")
+ {
+ skip = 1;
+ continue;
+ }
+ realargs.append(arg);
+ }
+
+ // start the updated application
+ started = QProcess::startDetached(finishCmd, realargs, QDir::currentPath(), &pid);
+ // much dumber check - just find out if the call
+ if(!started || pid == -1)
+ {
+ qWarning() << "Couldn't start new process properly!";
+ startFailed = true;
+ }
+ if(!startFailed && doLiveCheck)
+ {
+ int attempts = 0;
+ while(attempts < 10)
+ {
+ attempts++;
+ QString key;
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
+ if(!QFile::exists(liveCheckFile))
+ {
+ qWarning() << "Couldn't find the" << liveCheckFile << "file!";
+ startFailed = true;
+ continue;
+ }
+ try
+ {
+ key = QString::fromUtf8(FS::read(liveCheckFile));
+ auto id = ApplicationId::fromRawString(key);
+ LocalPeer peer(nullptr, id);
+ if(peer.isClient())
+ {
+ startFailed = false;
+ qDebug() << "Found process started with key " << key;
+ break;
+ }
+ else
+ {
+ startFailed = true;
+ qDebug() << "Process started with key " << key << "apparently died or is not reponding...";
+ break;
+ }
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Couldn't read the" << liveCheckFile << "file!";
+ startFailed = true;
+ continue;
+ }
+ }
+ }
+ if(startFailed)
+ {
+ m_failedOperationType = Start;
+ fail();
+ return;
+ }
+ else
+ {
+ origin.rmdir(m_updateFilesDir);
+ qApp->quit();
+ return;
+ }
+}
+
+void UpdateController::fail()
+{
+ qWarning() << "Update failed!";
+
+ QString msg;
+ bool doRollback = false;
+ QString failTitle = QObject::tr("Update failed!");
+ QString rollFailTitle = QObject::tr("Rollback failed!");
+ switch (m_failedOperationType)
+ {
+ case Replace:
+ {
+ msg = QObject::tr("Couldn't replace file %1. Changes will be reverted.\n"
+ "See the MultiMC log file for details.").arg(m_failedFile);
+ doRollback = true;
+ QMessageBox::critical(m_parent, failTitle, msg);
+ break;
+ }
+ case Delete:
+ {
+ msg = QObject::tr("Couldn't remove file %1. Changes will be reverted.\n"
+ "See the MultiMC log file for details.").arg(m_failedFile);
+ doRollback = true;
+ QMessageBox::critical(m_parent, failTitle, msg);
+ break;
+ }
+ case Start:
+ {
+ msg = QObject::tr("The new version didn't start or is too old and doesn't respond to startup checks.\n"
+ "\n"
+ "Roll back to previous version?");
+ auto result = QMessageBox::critical(
+ m_parent,
+ failTitle,
+ msg,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::Yes
+ );
+ doRollback = (result == QMessageBox::Yes);
+ break;
+ }
+ case Nothing:
+ default:
+ return;
+ }
+ if(doRollback)
+ {
+ auto rollbackOK = rollback();
+ if(!rollbackOK)
+ {
+ msg = QObject::tr("The rollback failed too.\n"
+ "You will have to repair MultiMC manually.\n"
+ "Please let us know why and how this happened.").arg(m_failedFile);
+ QMessageBox::critical(m_parent, rollFailTitle, msg);
+ qApp->quit();
+ }
+ }
+ else
+ {
+ qApp->quit();
+ }
+}
+
+bool UpdateController::rollback()
+{
+ bool revertOK = true;
+ // if the above failed, roll back changes
+ for(auto backup:m_replace_backups)
+ {
+ qWarning() << "restoring" << backup.original << "from" << backup.backup;
+ if(!QFile::rename(backup.original, backup.update))
+ {
+ revertOK = false;
+ qWarning() << "moving new" << backup.original << "back to" << backup.update << "failed!";
+ continue;
+ }
+
+ if(!QFile::rename(backup.backup, backup.original))
+ {
+ revertOK = false;
+ qWarning() << "restoring" << backup.original << "failed!";
+ }
+ }
+ for(auto backup:m_delete_backups)
+ {
+ qWarning() << "restoring" << backup.original << "from" << backup.backup;
+ if(!QFile::rename(backup.backup, backup.original))
+ {
+ revertOK = false;
+ qWarning() << "restoring" << backup.original << "failed!";
+ }
+ }
+ return revertOK;
+}
diff --git a/launcher/UpdateController.h b/launcher/UpdateController.h
new file mode 100644
index 00000000..715554e5
--- /dev/null
+++ b/launcher/UpdateController.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <QString>
+#include <QList>
+#include <updater/GoUpdate.h>
+
+class QWidget;
+
+class UpdateController
+{
+public:
+ UpdateController(QWidget * parent, const QString &root, const QString updateFilesDir, GoUpdate::OperationList operations);
+ void installUpdates();
+
+private:
+ void fail();
+ bool rollback();
+
+private:
+ QString m_root;
+ QString m_updateFilesDir;
+ GoUpdate::OperationList m_operations;
+ QWidget * m_parent;
+
+ struct BackupEntry
+ {
+ // path where we got the new file from
+ QString update;
+ // path of what is being actually updated
+ QString original;
+ // path where the backup of the updated file was placed
+ QString backup;
+ };
+ QList <BackupEntry> m_replace_backups;
+ QList <BackupEntry> m_delete_backups;
+ enum Failure
+ {
+ Replace,
+ Delete,
+ Start,
+ Nothing
+ } m_failedOperationType = Nothing;
+ QString m_failedFile;
+};
diff --git a/launcher/Usable.h b/launcher/Usable.h
new file mode 100644
index 00000000..83dd083d
--- /dev/null
+++ b/launcher/Usable.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <cstddef>
+#include <memory>
+
+class Usable;
+
+/**
+ * Base class for things that can be used by multiple other things and we want to track the use count.
+ *
+ * @see UseLock
+ */
+class Usable
+{
+ friend class UseLock;
+public:
+ std::size_t useCount()
+ {
+ return m_useCount;
+ }
+ bool isInUse()
+ {
+ return m_useCount > 0;
+ }
+protected:
+ virtual void decrementUses()
+ {
+ m_useCount--;
+ }
+ virtual void incrementUses()
+ {
+ m_useCount++;
+ }
+private:
+ std::size_t m_useCount = 0;
+};
+
+/**
+ * Lock class to use for keeping track of uses of other things derived from Usable
+ *
+ * @see Usable
+ */
+class UseLock
+{
+public:
+ UseLock(std::shared_ptr<Usable> usable)
+ : m_usable(usable)
+ {
+ // this doesn't use shared pointer use count, because that wouldn't be correct. this count is separate.
+ m_usable->incrementUses();
+ }
+ ~UseLock()
+ {
+ m_usable->decrementUses();
+ }
+private:
+ std::shared_ptr<Usable> m_usable;
+};
diff --git a/launcher/Version.cpp b/launcher/Version.cpp
new file mode 100644
index 00000000..6392a50f
--- /dev/null
+++ b/launcher/Version.cpp
@@ -0,0 +1,85 @@
+#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();
+
+ // FIXME: this is bad. versions can contain a lot more separators...
+ QStringList parts = m_string.split('.');
+
+ for (const auto &part : parts)
+ {
+ m_sections.append(Section(part));
+ }
+}
diff --git a/launcher/Version.h b/launcher/Version.h
new file mode 100644
index 00000000..9fe12d6d
--- /dev/null
+++ b/launcher/Version.h
@@ -0,0 +1,105 @@
+#pragma once
+
+#include <QString>
+#include <QList>
+
+class QUrl;
+
+class Version
+{
+public:
+ 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();
+};
diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp
new file mode 100644
index 00000000..5587136f
--- /dev/null
+++ b/launcher/VersionProxyModel.cpp
@@ -0,0 +1,447 @@
+#include "VersionProxyModel.h"
+#include "MultiMC.h"
+#include <QSortFilterProxyModel>
+#include <QPixmapCache>
+#include <Version.h>
+#include <meta/VersionList.h>
+
+class VersionFilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+ VersionFilterModel(VersionProxyModel *parent) : QSortFilterProxyModel(parent)
+ {
+ m_parent = parent;
+ setSortRole(BaseVersionList::SortRole);
+ sort(0, Qt::DescendingOrder);
+ }
+
+ bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+ {
+ const auto &filters = m_parent->filters();
+ for (auto it = filters.begin(); it != filters.end(); ++it)
+ {
+ auto idx = sourceModel()->index(source_row, 0, source_parent);
+ auto data = sourceModel()->data(idx, it.key());
+ auto match = data.toString();
+ if(!it.value()->accepts(match))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ void filterChanged()
+ {
+ invalidateFilter();
+ }
+private:
+ VersionProxyModel *m_parent;
+};
+
+VersionProxyModel::VersionProxyModel(QObject *parent) : QAbstractProxyModel(parent)
+{
+ filterModel = new VersionFilterModel(this);
+ connect(filterModel, &QAbstractItemModel::dataChanged, this, &VersionProxyModel::sourceDataChanged);
+ connect(filterModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &VersionProxyModel::sourceRowsAboutToBeInserted);
+ connect(filterModel, &QAbstractItemModel::rowsInserted, this, &VersionProxyModel::sourceRowsInserted);
+ connect(filterModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &VersionProxyModel::sourceRowsAboutToBeRemoved);
+ connect(filterModel, &QAbstractItemModel::rowsRemoved, this, &VersionProxyModel::sourceRowsRemoved);
+ // FIXME: implement when needed
+ /*
+ connect(replacing, &QAbstractItemModel::rowsAboutToBeMoved, this, &VersionProxyModel::sourceRowsAboutToBeMoved);
+ connect(replacing, &QAbstractItemModel::rowsMoved, this, &VersionProxyModel::sourceRowsMoved);
+ connect(replacing, &QAbstractItemModel::layoutAboutToBeChanged, this, &VersionProxyModel::sourceLayoutAboutToBeChanged);
+ connect(replacing, &QAbstractItemModel::layoutChanged, this, &VersionProxyModel::sourceLayoutChanged);
+ */
+ connect(filterModel, &QAbstractItemModel::modelAboutToBeReset, this, &VersionProxyModel::sourceAboutToBeReset);
+ connect(filterModel, &QAbstractItemModel::modelReset, this, &VersionProxyModel::sourceReset);
+
+ QAbstractProxyModel::setSourceModel(filterModel);
+}
+
+QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if(section < 0 || section >= m_columns.size())
+ return QVariant();
+ if(orientation != Qt::Horizontal)
+ return QVariant();
+ auto column = m_columns[section];
+ if(role == Qt::DisplayRole)
+ {
+ switch(column)
+ {
+ case Name:
+ return tr("Version");
+ case ParentVersion:
+ return tr("Minecraft"); //FIXME: this should come from metadata
+ case Branch:
+ return tr("Branch");
+ case Type:
+ return tr("Type");
+ case Architecture:
+ return tr("Architecture");
+ case Path:
+ return tr("Path");
+ case Time:
+ return tr("Released");
+ }
+ }
+ else if(role == Qt::ToolTipRole)
+ {
+ switch(column)
+ {
+ case Name:
+ return tr("The name of the version.");
+ case ParentVersion:
+ return tr("Minecraft version"); //FIXME: this should come from metadata
+ case Branch:
+ return tr("The version's branch");
+ case Type:
+ return tr("The version's type");
+ case Architecture:
+ return tr("CPU Architecture");
+ case Path:
+ return tr("Filesystem path to this version");
+ case Time:
+ return tr("Release date of this version");
+ }
+ }
+ return QVariant();
+}
+
+QVariant VersionProxyModel::data(const QModelIndex &index, int role) const
+{
+ if(!index.isValid())
+ {
+ return QVariant();
+ }
+ auto column = m_columns[index.column()];
+ auto parentIndex = mapToSource(index);
+ switch(role)
+ {
+ case Qt::DisplayRole:
+ {
+ switch(column)
+ {
+ case Name:
+ {
+ QString version = sourceModel()->data(parentIndex, BaseVersionList::VersionRole).toString();
+ if(version == m_currentVersion)
+ {
+ return tr("%1 (installed)").arg(version);
+ }
+ return version;
+ }
+ case ParentVersion:
+ return sourceModel()->data(parentIndex, BaseVersionList::ParentVersionRole);
+ case Branch:
+ return sourceModel()->data(parentIndex, BaseVersionList::BranchRole);
+ case Type:
+ return sourceModel()->data(parentIndex, BaseVersionList::TypeRole);
+ case Architecture:
+ return sourceModel()->data(parentIndex, BaseVersionList::ArchitectureRole);
+ case Path:
+ return sourceModel()->data(parentIndex, BaseVersionList::PathRole);
+ case Time:
+ return sourceModel()->data(parentIndex, Meta::VersionList::TimeRole).toDate();
+ default:
+ return QVariant();
+ }
+ }
+ case Qt::ToolTipRole:
+ {
+ switch(column)
+ {
+ case Name:
+ {
+ if(hasRecommended)
+ {
+ auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
+ if(value.toBool())
+ {
+ return tr("Recommended");
+ }
+ else if(hasLatest)
+ {
+ auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
+ if(value.toBool())
+ {
+ return tr("Latest");
+ }
+ }
+ else if(index.row() == 0)
+ {
+ return tr("Latest");
+ }
+ }
+ }
+ default:
+ {
+ return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole);
+ }
+ }
+ }
+ case Qt::DecorationRole:
+ {
+ switch(column)
+ {
+ case Name:
+ {
+ if(hasRecommended)
+ {
+ auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole);
+ if(value.toBool())
+ {
+ return MMC->getThemedIcon("star");
+ }
+ else if(hasLatest)
+ {
+ auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole);
+ if(value.toBool())
+ {
+ return MMC->getThemedIcon("bug");
+ }
+ }
+ else if(index.row() == 0)
+ {
+ return MMC->getThemedIcon("bug");
+ }
+ auto pixmap = QPixmapCache::find("placeholder");
+ if(!pixmap)
+ {
+ QPixmap px(16,16);
+ px.fill(Qt::transparent);
+ QPixmapCache::insert("placeholder", px);
+ return px;
+ }
+ return *pixmap;
+ }
+ }
+ default:
+ {
+ return QVariant();
+ }
+ }
+ }
+ default:
+ {
+ if(roles.contains((BaseVersionList::ModelRoles)role))
+ {
+ return sourceModel()->data(parentIndex, role);
+ }
+ return QVariant();
+ }
+ }
+}
+
+QModelIndex VersionProxyModel::parent(const QModelIndex &child) const
+{
+ return QModelIndex();
+}
+
+QModelIndex VersionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
+{
+ if(sourceIndex.isValid())
+ {
+ return index(sourceIndex.row(), 0);
+ }
+ return QModelIndex();
+}
+
+QModelIndex VersionProxyModel::mapToSource(const QModelIndex &proxyIndex) const
+{
+ if(proxyIndex.isValid())
+ {
+ return sourceModel()->index(proxyIndex.row(), 0);
+ }
+ return QModelIndex();
+}
+
+QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex &parent) const
+{
+ // no trees here... shoo
+ if(parent.isValid())
+ {
+ return QModelIndex();
+ }
+ if(row < 0 || row >= sourceModel()->rowCount())
+ return QModelIndex();
+ if(column < 0 || column >= columnCount())
+ return QModelIndex();
+ return QAbstractItemModel::createIndex(row, column);
+}
+
+int VersionProxyModel::columnCount(const QModelIndex &parent) const
+{
+ return m_columns.size();
+}
+
+int VersionProxyModel::rowCount(const QModelIndex &parent) const
+{
+ if(sourceModel())
+ {
+ return sourceModel()->rowCount();
+ }
+ return 0;
+}
+
+void VersionProxyModel::sourceDataChanged(const QModelIndex &source_top_left,
+ const QModelIndex &source_bottom_right)
+{
+ if(source_top_left.parent() != source_bottom_right.parent())
+ return;
+
+ // whole row is getting changed
+ auto topLeft = createIndex(source_top_left.row(), 0);
+ auto bottomRight = createIndex(source_bottom_right.row(), columnCount() - 1);
+ emit dataChanged(topLeft, bottomRight);
+}
+
+void VersionProxyModel::setSourceModel(QAbstractItemModel *replacingRaw)
+{
+ auto replacing = dynamic_cast<BaseVersionList *>(replacingRaw);
+ beginResetModel();
+
+ m_columns.clear();
+ if(!replacing)
+ {
+ roles.clear();
+ filterModel->setSourceModel(replacing);
+ return;
+ }
+
+ roles = replacing->providesRoles();
+ if(roles.contains(BaseVersionList::VersionRole))
+ {
+ m_columns.push_back(Name);
+ }
+ /*
+ if(roles.contains(BaseVersionList::ParentVersionRole))
+ {
+ m_columns.push_back(ParentVersion);
+ }
+ */
+ if(roles.contains(BaseVersionList::ArchitectureRole))
+ {
+ m_columns.push_back(Architecture);
+ }
+ if(roles.contains(BaseVersionList::PathRole))
+ {
+ m_columns.push_back(Path);
+ }
+ if(roles.contains(Meta::VersionList::TimeRole))
+ {
+ m_columns.push_back(Time);
+ }
+ if(roles.contains(BaseVersionList::BranchRole))
+ {
+ m_columns.push_back(Branch);
+ }
+ if(roles.contains(BaseVersionList::TypeRole))
+ {
+ m_columns.push_back(Type);
+ }
+ if(roles.contains(BaseVersionList::RecommendedRole))
+ {
+ hasRecommended = true;
+ }
+ if(roles.contains(BaseVersionList::LatestRole))
+ {
+ hasLatest = true;
+ }
+ filterModel->setSourceModel(replacing);
+
+ endResetModel();
+}
+
+QModelIndex VersionProxyModel::getRecommended() const
+{
+ if(!roles.contains(BaseVersionList::RecommendedRole))
+ {
+ return index(0, 0);
+ }
+ int recommended = 0;
+ for (int i = 0; i < rowCount(); i++)
+ {
+ auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::RecommendedRole);
+ if (value.toBool())
+ {
+ recommended = i;
+ }
+ }
+ return index(recommended, 0);
+}
+
+QModelIndex VersionProxyModel::getVersion(const QString& version) const
+{
+ int found = -1;
+ for (int i = 0; i < rowCount(); i++)
+ {
+ auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::VersionRole);
+ if (value.toString() == version)
+ {
+ found = i;
+ }
+ }
+ if(found == -1)
+ {
+ return QModelIndex();
+ }
+ return index(found, 0);
+}
+
+void VersionProxyModel::clearFilters()
+{
+ m_filters.clear();
+ filterModel->filterChanged();
+}
+
+void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter * f)
+{
+ m_filters[column].reset(f);
+ filterModel->filterChanged();
+}
+
+const VersionProxyModel::FilterMap &VersionProxyModel::filters() const
+{
+ return m_filters;
+}
+
+void VersionProxyModel::sourceAboutToBeReset()
+{
+ beginResetModel();
+}
+
+void VersionProxyModel::sourceReset()
+{
+ endResetModel();
+}
+
+void VersionProxyModel::sourceRowsAboutToBeInserted(const QModelIndex& parent, int first, int last)
+{
+ beginInsertRows(parent, first, last);
+}
+
+void VersionProxyModel::sourceRowsInserted(const QModelIndex& parent, int first, int last)
+{
+ endInsertRows();
+}
+
+void VersionProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last)
+{
+ beginRemoveRows(parent, first, last);
+}
+
+void VersionProxyModel::sourceRowsRemoved(const QModelIndex& parent, int first, int last)
+{
+ endRemoveRows();
+}
+
+void VersionProxyModel::setCurrentVersion(const QString &version)
+{
+ m_currentVersion = version;
+}
+
+#include "VersionProxyModel.moc"
diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h
new file mode 100644
index 00000000..8991c31b
--- /dev/null
+++ b/launcher/VersionProxyModel.h
@@ -0,0 +1,67 @@
+#pragma once
+#include <QAbstractProxyModel>
+#include "BaseVersionList.h"
+
+#include <Filter.h>
+
+class VersionFilterModel;
+
+class VersionProxyModel: public QAbstractProxyModel
+{
+ Q_OBJECT
+public:
+
+ enum Column
+ {
+ Name,
+ ParentVersion,
+ Branch,
+ Type,
+ Architecture,
+ Path,
+ Time
+ };
+ typedef QHash<BaseVersionList::ModelRoles, std::shared_ptr<Filter>> FilterMap;
+
+public:
+ VersionProxyModel ( QObject* parent = 0 );
+ virtual ~VersionProxyModel() {};
+
+ virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ virtual QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
+ virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
+ virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ virtual QModelIndex parent(const QModelIndex &child) const override;
+ virtual void setSourceModel(QAbstractItemModel *sourceModel) override;
+
+ const FilterMap &filters() const;
+ void setFilter(const BaseVersionList::ModelRoles column, Filter * filter);
+ void clearFilters();
+ QModelIndex getRecommended() const;
+ QModelIndex getVersion(const QString & version) const;
+ void setCurrentVersion(const QString &version);
+private slots:
+
+ void sourceDataChanged(const QModelIndex &source_top_left,const QModelIndex &source_bottom_right);
+
+ void sourceAboutToBeReset();
+ void sourceReset();
+
+ void sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last);
+ void sourceRowsInserted(const QModelIndex &parent, int first, int last);
+
+ void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
+ void sourceRowsRemoved(const QModelIndex &parent, int first, int last);
+
+private:
+ QList<Column> m_columns;
+ FilterMap m_filters;
+ BaseVersionList::RoleList roles;
+ VersionFilterModel * filterModel;
+ bool hasRecommended = false;
+ bool hasLatest = false;
+ QString m_currentVersion;
+};
diff --git a/launcher/Version_test.cpp b/launcher/Version_test.cpp
new file mode 100644
index 00000000..b2d657a6
--- /dev/null
+++ b/launcher/Version_test.cpp
@@ -0,0 +1,85 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QTest>
+
+#include "TestUtil.h"
+#include <Version.h>
+
+class ModUtilsTest : public QObject
+{
+ Q_OBJECT
+ void setupVersions()
+ {
+ QTest::addColumn<QString>("first");
+ QTest::addColumn<QString>("second");
+ QTest::addColumn<bool>("lessThan");
+ QTest::addColumn<bool>("equal");
+
+ QTest::newRow("equal, explicit") << "1.2.0" << "1.2.0" << false << true;
+ QTest::newRow("equal, implicit 1") << "1.2" << "1.2.0" << false << true;
+ QTest::newRow("equal, implicit 2") << "1.2.0" << "1.2" << false << true;
+ QTest::newRow("equal, two-digit") << "1.42" << "1.42" << false << true;
+
+ QTest::newRow("lessThan, explicit 1") << "1.2.0" << "1.2.1" << true << false;
+ QTest::newRow("lessThan, explicit 2") << "1.2.0" << "1.3.0" << true << false;
+ QTest::newRow("lessThan, explicit 3") << "1.2.0" << "2.2.0" << true << false;
+ QTest::newRow("lessThan, implicit 1") << "1.2" << "1.2.1" << true << false;
+ QTest::newRow("lessThan, implicit 2") << "1.2" << "1.3.0" << true << false;
+ QTest::newRow("lessThan, implicit 3") << "1.2" << "2.2.0" << true << false;
+ QTest::newRow("lessThan, two-digit") << "1.41" << "1.42" << true << false;
+
+ QTest::newRow("greaterThan, explicit 1") << "1.2.1" << "1.2.0" << false << false;
+ QTest::newRow("greaterThan, explicit 2") << "1.3.0" << "1.2.0" << false << false;
+ QTest::newRow("greaterThan, explicit 3") << "2.2.0" << "1.2.0" << false << false;
+ QTest::newRow("greaterThan, implicit 1") << "1.2.1" << "1.2" << false << false;
+ QTest::newRow("greaterThan, implicit 2") << "1.3.0" << "1.2" << false << false;
+ QTest::newRow("greaterThan, implicit 3") << "2.2.0" << "1.2" << false << false;
+ QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false;
+ }
+
+private slots:
+ void initTestCase()
+ {
+
+ }
+ void cleanupTestCase()
+ {
+
+ }
+
+ void test_versionCompare_data()
+ {
+ setupVersions();
+ }
+ void test_versionCompare()
+ {
+ QFETCH(QString, first);
+ QFETCH(QString, second);
+ QFETCH(bool, lessThan);
+ QFETCH(bool, equal);
+
+ const auto v1 = Version(first);
+ const auto v2 = Version(second);
+
+ QCOMPARE(v1 < v2, lessThan);
+ QCOMPARE(v1 > v2, !lessThan && !equal);
+ QCOMPARE(v1 == v2, equal);
+ }
+};
+
+QTEST_GUILESS_MAIN(ModUtilsTest)
+
+#include "Version_test.moc"
diff --git a/launcher/WatchLock.h b/launcher/WatchLock.h
new file mode 100644
index 00000000..3e08b413
--- /dev/null
+++ b/launcher/WatchLock.h
@@ -0,0 +1,20 @@
+
+#pragma once
+
+#include <QString>
+#include <QFileSystemWatcher>
+
+struct WatchLock
+{
+ WatchLock(QFileSystemWatcher * watcher, const QString& directory)
+ : m_watcher(watcher), m_directory(directory)
+ {
+ m_watcher->removePath(m_directory);
+ }
+ ~WatchLock()
+ {
+ m_watcher->addPath(m_directory);
+ }
+ QFileSystemWatcher * m_watcher;
+ QString m_directory;
+};
diff --git a/launcher/dialogs/AboutDialog.cpp b/launcher/dialogs/AboutDialog.cpp
new file mode 100644
index 00000000..c97c471e
--- /dev/null
+++ b/launcher/dialogs/AboutDialog.cpp
@@ -0,0 +1,138 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "AboutDialog.h"
+#include "ui_AboutDialog.h"
+#include <QIcon>
+#include "MultiMC.h"
+#include "BuildConfig.h"
+
+#include <net/NetJob.h>
+
+#include "HoeDown.h"
+
+namespace {
+// Credits
+// This is a hack, but I can't think of a better way to do this easily without screwing with QTextDocument...
+QString getCreditsHtml(QStringList patrons)
+{
+ QString patronsHeading = QObject::tr("Patrons", "About Credits");
+ QString output;
+ QTextStream stream(&output);
+ stream << "<center>\n";
+ // TODO: possibly retrieve from git history at build time?
+ stream << "<h3>" << QObject::tr("MultiMC Developers", "About Credits") << "</h3>\n";
+ stream << "<p>Andrew Okin &lt;<a href='mailto:forkk@forkk.net'>forkk@forkk.net</a>&gt;</p>\n";
+ stream << "<p>Petr Mrázek &lt;<a href='mailto:peterix@gmail.com'>peterix@gmail.com</a>&gt;</p>\n";
+ stream << "<p>Sky Welch &lt;<a href='mailto:multimc@bunnies.io'>multimc@bunnies.io</a>&gt;</p>\n";
+ stream << "<p>Jan (02JanDal) &lt;<a href='mailto:02jandal@gmail.com'>02jandal@gmail.com</a>&gt;</p>\n";
+ stream << "<p>RoboSky &lt;<a href='https://twitter.com/RoboSky_'>@RoboSky_</a>&gt;</p>\n";
+ stream << "<br />\n";
+
+ stream << "<h3>" << QObject::tr("With thanks to", "About Credits") << "</h3>\n";
+ stream << "<p>Orochimarufan &lt;<a href='mailto:orochimarufan.x3@gmail.com'>orochimarufan.x3@gmail.com</a>&gt;</p>\n";
+ stream << "<p>TakSuyu &lt;<a href='mailto:taksuyu@gmail.com'>taksuyu@gmail.com</a>&gt;</p>\n";
+ stream << "<p>Kilobyte &lt;<a href='mailto:stiepen22@gmx.de'>stiepen22@gmx.de</a>&gt;</p>\n";
+ stream << "<p>Rootbear75 &lt;<a href='https://twitter.com/rootbear75'>@rootbear75</a>&gt;</p>\n";
+ stream << "<p>Zeker Zhayard &lt;<a href='https://twitter.com/zeker_zhayard'>@Zeker_Zhayard</a>&gt;</p>\n";
+ stream << "<br />\n";
+
+ if(!patrons.isEmpty()) {
+ stream << "<h3>" << QObject::tr("Patrons", "About Credits") << "</h3>\n";
+ for (QString patron : patrons)
+ {
+ stream << "<p>" << patron << "</p>\n";
+ }
+ }
+ stream << "</center>\n";
+ return output;
+}
+
+QString getLicenseHtml()
+{
+ HoeDown hoedown;
+ QFile dataFile(":/documents/COPYING.md");
+ dataFile.open(QIODevice::ReadOnly);
+ QString output = hoedown.process(dataFile.readAll());
+ return output;
+}
+
+}
+
+AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDialog)
+{
+ ui->setupUi(this);
+
+ QString chtml = getCreditsHtml(QStringList());
+ ui->creditsText->setHtml(chtml);
+
+ QString lhtml = getLicenseHtml();
+ ui->licenseText->setHtml(lhtml);
+
+ ui->urlLabel->setOpenExternalLinks(true);
+
+ ui->icon->setPixmap(MMC->getThemedIcon("logo").pixmap(64));
+ ui->title->setText("MultiMC 5");
+
+ ui->versionLabel->setText(tr("Version") +": " + BuildConfig.printableVersionString());
+ ui->platformLabel->setText(tr("Platform") +": " + BuildConfig.BUILD_PLATFORM);
+
+ if (BuildConfig.VERSION_BUILD >= 0)
+ ui->buildNumLabel->setText(tr("Build Number") +": " + QString::number(BuildConfig.VERSION_BUILD));
+ else
+ ui->buildNumLabel->setVisible(false);
+
+ if (!BuildConfig.VERSION_CHANNEL.isEmpty())
+ ui->channelLabel->setText(tr("Channel") +": " + BuildConfig.VERSION_CHANNEL);
+ else
+ ui->channelLabel->setVisible(false);
+
+ ui->redistributionText->setHtml(tr(
+"<p>We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.</p>\n"
+"<p>Part of the reason for using the Apache license is we don't want people using the &quot;MultiMC&quot; name when redistributing the project. "
+"This means people must take the time to go through the source code and remove all references to &quot;MultiMC&quot;, including but not limited to the project "
+"icon and the title of windows, (no <b>MultiMC-fork</b> in the title).</p>\n"
+"<p>The Apache license covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. "
+"However, it should be abundantly clear that the project is a fork <b>without</b> implying that you have our blessing.</p>"
+ ));
+
+ connect(ui->closeButton, SIGNAL(clicked()), SLOT(close()));
+
+ connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt);
+
+ loadPatronList();
+}
+
+AboutDialog::~AboutDialog()
+{
+ delete ui;
+}
+
+void AboutDialog::loadPatronList()
+{
+ netJob.reset(new NetJob("Patreon Patron List"));
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl("https://files.multimc.org/patrons.txt"), &dataSink));
+ connect(netJob.get(), &NetJob::succeeded, this, &AboutDialog::patronListLoaded);
+ netJob->start();
+}
+
+void AboutDialog::patronListLoaded()
+{
+ QString patronListStr(dataSink);
+ dataSink.clear();
+ QString html = getCreditsHtml(patronListStr.split("\n", QString::SkipEmptyParts));
+ ui->creditsText->setHtml(html);
+}
+
diff --git a/launcher/dialogs/AboutDialog.h b/launcher/dialogs/AboutDialog.h
new file mode 100644
index 00000000..c7621c37
--- /dev/null
+++ b/launcher/dialogs/AboutDialog.h
@@ -0,0 +1,47 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <net/NetJob.h>
+
+namespace Ui
+{
+class AboutDialog;
+}
+
+class AboutDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit AboutDialog(QWidget *parent = 0);
+ ~AboutDialog();
+
+public
+slots:
+ /// Starts loading a list of Patreon patrons.
+ void loadPatronList();
+
+ /// Slot for when the patron list loads successfully.
+ void patronListLoaded();
+
+private:
+ Ui::AboutDialog *ui;
+
+ NetJobPtr netJob;
+ QByteArray dataSink;
+};
diff --git a/launcher/dialogs/AboutDialog.ui b/launcher/dialogs/AboutDialog.ui
new file mode 100644
index 00000000..c6de9ebb
--- /dev/null
+++ b/launcher/dialogs/AboutDialog.ui
@@ -0,0 +1,312 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AboutDialog</class>
+ <widget class="QDialog" name="AboutDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>783</width>
+ <height>843</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>450</width>
+ <height>400</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>About MultiMC</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="icon">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="title">
+ <property name="font">
+ <font>
+ <pointsize>15</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true">MultiMC 5</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="aboutTab">
+ <attribute name="title">
+ <string>About</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QLabel" name="versionLabel">
+ <property name="text">
+ <string>Version:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="platformLabel">
+ <property name="text">
+ <string>Platform:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="buildNumLabel">
+ <property name="text">
+ <string>Build Number:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="channelLabel">
+ <property name="text">
+ <string>Channel:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="aboutLabel">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;MultiMC is a custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="copyLabel">
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ <kerning>true</kerning>
+ </font>
+ </property>
+ <property name="text">
+ <string>© 2012-2021 MultiMC Contributors</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="urlLabel">
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/MultiMC/MultiMC5&quot;&gt;https://github.com/MultiMC/MultiMC5&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>212</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="creditsTab">
+ <attribute name="title">
+ <string>Credits</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QTextEdit" name="creditsText">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="licenseTab">
+ <attribute name="title">
+ <string>License</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QTextEdit" name="licenseText">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <family>DejaVu Sans Mono</family>
+ </font>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="forkingTab">
+ <attribute name="title">
+ <string>Forking/Redistribution</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QTextEdit" name="redistributionText">
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="aboutQt">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>About Qt</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeButton">
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>creditsText</tabstop>
+ <tabstop>licenseText</tabstop>
+ <tabstop>redistributionText</tabstop>
+ <tabstop>aboutQt</tabstop>
+ <tabstop>closeButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/CopyInstanceDialog.cpp b/launcher/dialogs/CopyInstanceDialog.cpp
new file mode 100644
index 00000000..5fe90334
--- /dev/null
+++ b/launcher/dialogs/CopyInstanceDialog.cpp
@@ -0,0 +1,144 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QLayout>
+#include <QPushButton>
+
+#include "MultiMC.h"
+#include "CopyInstanceDialog.h"
+#include "ui_CopyInstanceDialog.h"
+
+#include "dialogs/IconPickerDialog.h"
+
+#include "BaseVersion.h"
+#include "icons/IconList.h"
+#include "tasks/Task.h"
+#include "BaseInstance.h"
+#include "InstanceList.h"
+
+CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent)
+ :QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original)
+{
+ ui->setupUi(this);
+ resize(minimumSizeHint());
+ layout()->setSizeConstraint(QLayout::SetFixedSize);
+
+ InstIconKey = original->iconKey();
+ ui->iconButton->setIcon(MMC->icons()->getIcon(InstIconKey));
+ ui->instNameTextBox->setText(original->name());
+ ui->instNameTextBox->setFocus();
+ auto groups = MMC->instances()->getGroups().toSet();
+ auto groupList = QStringList(groups.toList());
+ groupList.sort(Qt::CaseInsensitive);
+ groupList.removeOne("");
+ groupList.push_front("");
+ ui->groupBox->addItems(groupList);
+ int index = groupList.indexOf(MMC->instances()->getInstanceGroup(m_original->id()));
+ if(index == -1)
+ {
+ index = 0;
+ }
+ ui->groupBox->setCurrentIndex(index);
+ ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
+ ui->copySavesCheckbox->setChecked(m_copySaves);
+ ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime);
+}
+
+CopyInstanceDialog::~CopyInstanceDialog()
+{
+ delete ui;
+}
+
+void CopyInstanceDialog::updateDialogState()
+{
+ auto allowOK = !instName().isEmpty();
+ auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok);
+ if(OkButton->isEnabled() != allowOK)
+ {
+ OkButton->setEnabled(allowOK);
+ }
+}
+
+QString CopyInstanceDialog::instName() const
+{
+ auto result = ui->instNameTextBox->text().trimmed();
+ if(result.size())
+ {
+ return result;
+ }
+ return QString();
+}
+
+QString CopyInstanceDialog::iconKey() const
+{
+ return InstIconKey;
+}
+
+QString CopyInstanceDialog::instGroup() const
+{
+ return ui->groupBox->currentText();
+}
+
+void CopyInstanceDialog::on_iconButton_clicked()
+{
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(InstIconKey);
+
+ if (dlg.result() == QDialog::Accepted)
+ {
+ InstIconKey = dlg.selectedIconKey;
+ ui->iconButton->setIcon(MMC->icons()->getIcon(InstIconKey));
+ }
+}
+
+void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1)
+{
+ updateDialogState();
+}
+
+bool CopyInstanceDialog::shouldCopySaves() const
+{
+ return m_copySaves;
+}
+
+void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state)
+{
+ if(state == Qt::Unchecked)
+ {
+ m_copySaves = false;
+ }
+ else if(state == Qt::Checked)
+ {
+ m_copySaves = true;
+ }
+}
+
+bool CopyInstanceDialog::shouldKeepPlaytime() const
+{
+ return m_keepPlaytime;
+}
+
+
+void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state)
+{
+ if(state == Qt::Unchecked)
+ {
+ m_keepPlaytime = false;
+ }
+ else if(state == Qt::Checked)
+ {
+ m_keepPlaytime = true;
+ }
+}
diff --git a/launcher/dialogs/CopyInstanceDialog.h b/launcher/dialogs/CopyInstanceDialog.h
new file mode 100644
index 00000000..bf3cd920
--- /dev/null
+++ b/launcher/dialogs/CopyInstanceDialog.h
@@ -0,0 +1,58 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include "BaseVersion.h"
+#include <BaseInstance.h>
+
+class BaseInstance;
+
+namespace Ui
+{
+class CopyInstanceDialog;
+}
+
+class CopyInstanceDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit CopyInstanceDialog(InstancePtr original, QWidget *parent = 0);
+ ~CopyInstanceDialog();
+
+ void updateDialogState();
+
+ QString instName() const;
+ QString instGroup() const;
+ QString iconKey() const;
+ bool shouldCopySaves() const;
+ bool shouldKeepPlaytime() const;
+
+private
+slots:
+ void on_iconButton_clicked();
+ void on_instNameTextBox_textChanged(const QString &arg1);
+ void on_copySavesCheckbox_stateChanged(int state);
+ void on_keepPlaytimeCheckbox_stateChanged(int state);
+
+private:
+ Ui::CopyInstanceDialog *ui;
+ QString InstIconKey;
+ InstancePtr m_original;
+ bool m_copySaves = true;
+ bool m_keepPlaytime = true;
+};
diff --git a/launcher/dialogs/CopyInstanceDialog.ui b/launcher/dialogs/CopyInstanceDialog.ui
new file mode 100644
index 00000000..fa675455
--- /dev/null
+++ b/launcher/dialogs/CopyInstanceDialog.ui
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CopyInstanceDialog</class>
+ <widget class="QDialog" name="CopyInstanceDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>345</width>
+ <height>323</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Copy Instance</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="iconBtnLayout">
+ <item>
+ <spacer name="iconBtnLeftSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QToolButton" name="iconButton">
+ <property name="icon">
+ <iconset>
+ <normaloff>:/icons/instances/infinity</normaloff>:/icons/instances/infinity</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="iconBtnRightSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="instNameTextBox">
+ <property name="placeholderText">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelVersion_3">
+ <property name="text">
+ <string>&amp;Group</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" colspan="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="copySavesCheckbox">
+ <property name="text">
+ <string>Copy saves</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="keepPlaytimeCheckbox">
+ <property name="text">
+ <string>Keep play time</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>iconButton</tabstop>
+ <tabstop>instNameTextBox</tabstop>
+ <tabstop>groupBox</tabstop>
+ <tabstop>copySavesCheckbox</tabstop>
+ <tabstop>keepPlaytimeCheckbox</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../graphics.qrc"/>
+ </resources>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>CopyInstanceDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>CopyInstanceDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/dialogs/CustomMessageBox.cpp b/launcher/dialogs/CustomMessageBox.cpp
new file mode 100644
index 00000000..19029f68
--- /dev/null
+++ b/launcher/dialogs/CustomMessageBox.cpp
@@ -0,0 +1,35 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "CustomMessageBox.h"
+
+namespace CustomMessageBox
+{
+QMessageBox *selectable(QWidget *parent, const QString &title, const QString &text,
+ QMessageBox::Icon icon, QMessageBox::StandardButtons buttons,
+ QMessageBox::StandardButton defaultButton)
+{
+ QMessageBox *messageBox = new QMessageBox(parent);
+ messageBox->setWindowTitle(title);
+ messageBox->setText(text);
+ messageBox->setStandardButtons(buttons);
+ messageBox->setDefaultButton(defaultButton);
+ messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ messageBox->setIcon(icon);
+ messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
+
+ return messageBox;
+}
+}
diff --git a/launcher/dialogs/CustomMessageBox.h b/launcher/dialogs/CustomMessageBox.h
new file mode 100644
index 00000000..712c6518
--- /dev/null
+++ b/launcher/dialogs/CustomMessageBox.h
@@ -0,0 +1,26 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMessageBox>
+
+namespace CustomMessageBox
+{
+QMessageBox *selectable(QWidget *parent, const QString &title, const QString &text,
+ QMessageBox::Icon icon = QMessageBox::NoIcon,
+ QMessageBox::StandardButtons buttons = QMessageBox::Ok,
+ QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
+}
diff --git a/launcher/dialogs/EditAccountDialog.cpp b/launcher/dialogs/EditAccountDialog.cpp
new file mode 100644
index 00000000..002c064b
--- /dev/null
+++ b/launcher/dialogs/EditAccountDialog.cpp
@@ -0,0 +1,61 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "EditAccountDialog.h"
+#include "ui_EditAccountDialog.h"
+#include <DesktopServices.h>
+#include <QUrl>
+
+EditAccountDialog::EditAccountDialog(const QString &text, QWidget *parent, int flags)
+ : QDialog(parent), ui(new Ui::EditAccountDialog)
+{
+ ui->setupUi(this);
+
+ ui->label->setText(text);
+ ui->label->setVisible(!text.isEmpty());
+
+ ui->userTextBox->setEnabled(flags & UsernameField);
+ ui->passTextBox->setEnabled(flags & PasswordField);
+}
+
+EditAccountDialog::~EditAccountDialog()
+{
+ delete ui;
+}
+
+void EditAccountDialog::on_label_linkActivated(const QString &link)
+{
+ DesktopServices::openUrl(QUrl(link));
+}
+
+void EditAccountDialog::setUsername(const QString & user) const
+{
+ ui->userTextBox->setText(user);
+}
+
+QString EditAccountDialog::username() const
+{
+ return ui->userTextBox->text();
+}
+
+void EditAccountDialog::setPassword(const QString & pass) const
+{
+ ui->passTextBox->setText(pass);
+}
+
+QString EditAccountDialog::password() const
+{
+ return ui->passTextBox->text();
+}
diff --git a/launcher/dialogs/EditAccountDialog.h b/launcher/dialogs/EditAccountDialog.h
new file mode 100644
index 00000000..6b5eb4aa
--- /dev/null
+++ b/launcher/dialogs/EditAccountDialog.h
@@ -0,0 +1,56 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+namespace Ui
+{
+class EditAccountDialog;
+}
+
+class EditAccountDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit EditAccountDialog(const QString &text = "", QWidget *parent = 0,
+ int flags = UsernameField | PasswordField);
+ ~EditAccountDialog();
+
+ void setUsername(const QString & user) const;
+ void setPassword(const QString & pass) const;
+
+ QString username() const;
+ QString password() const;
+
+ enum Flags
+ {
+ NoFlags = 0,
+
+ //! Specifies that the dialog should have a username field.
+ UsernameField,
+
+ //! Specifies that the dialog should have a password field.
+ PasswordField,
+ };
+
+private slots:
+ void on_label_linkActivated(const QString &link);
+
+private:
+ Ui::EditAccountDialog *ui;
+};
diff --git a/launcher/dialogs/EditAccountDialog.ui b/launcher/dialogs/EditAccountDialog.ui
new file mode 100644
index 00000000..e87509bc
--- /dev/null
+++ b/launcher/dialogs/EditAccountDialog.ui
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EditAccountDialog</class>
+ <widget class="QDialog" name="EditAccountDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>148</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Login</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true">Message label placeholder.</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="userTextBox">
+ <property name="placeholderText">
+ <string>Email</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="passTextBox">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ <property name="placeholderText">
+ <string>Password</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>EditAccountDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>EditAccountDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/dialogs/ExportInstanceDialog.cpp b/launcher/dialogs/ExportInstanceDialog.cpp
new file mode 100644
index 00000000..a42779d4
--- /dev/null
+++ b/launcher/dialogs/ExportInstanceDialog.cpp
@@ -0,0 +1,482 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ExportInstanceDialog.h"
+#include "ui_ExportInstanceDialog.h"
+#include <BaseInstance.h>
+#include <MMCZip.h>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <qfilesystemmodel.h>
+
+#include <QSortFilterProxyModel>
+#include <QDebug>
+#include <qstack.h>
+#include <QSaveFile>
+#include "MMCStrings.h"
+#include "SeparatorPrefixTree.h"
+#include "MultiMC.h"
+#include <icons/IconList.h>
+#include <FileSystem.h>
+
+class PackIgnoreProxy : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ PackIgnoreProxy(InstancePtr instance, QObject *parent) : QSortFilterProxyModel(parent)
+ {
+ m_instance = instance;
+ }
+ // NOTE: Sadly, we have to do sorting ourselves.
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const
+ {
+ QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
+ if (!fsm)
+ {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
+
+ QFileInfo leftFileInfo = fsm->fileInfo(left);
+ QFileInfo rightFileInfo = fsm->fileInfo(right);
+
+ if (!leftFileInfo.isDir() && rightFileInfo.isDir())
+ {
+ return !asc;
+ }
+ if (leftFileInfo.isDir() && !rightFileInfo.isDir())
+ {
+ return asc;
+ }
+
+ // sort and proxy model breaks the original model...
+ if (sortColumn() == 0)
+ {
+ return Strings::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(),
+ Qt::CaseInsensitive) < 0;
+ }
+ if (sortColumn() == 1)
+ {
+ auto leftSize = leftFileInfo.size();
+ auto rightSize = rightFileInfo.size();
+ if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir()))
+ {
+ return Strings::naturalCompare(leftFileInfo.fileName(),
+ rightFileInfo.fileName(),
+ Qt::CaseInsensitive) < 0
+ ? asc
+ : !asc;
+ }
+ return leftSize < rightSize;
+ }
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const
+ {
+ if (!index.isValid())
+ return Qt::NoItemFlags;
+
+ auto sourceIndex = mapToSource(index);
+ Qt::ItemFlags flags = sourceIndex.flags();
+ if (index.column() == 0)
+ {
+ flags |= Qt::ItemIsUserCheckable;
+ if (sourceIndex.model()->hasChildren(sourceIndex))
+ {
+ flags |= Qt::ItemIsTristate;
+ }
+ }
+
+ return flags;
+ }
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
+ {
+ QModelIndex sourceIndex = mapToSource(index);
+
+ if (index.column() == 0 && role == Qt::CheckStateRole)
+ {
+ QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto cover = blocked.cover(blockedPath);
+ if (!cover.isNull())
+ {
+ return QVariant(Qt::Unchecked);
+ }
+ else if (blocked.exists(blockedPath))
+ {
+ return QVariant(Qt::PartiallyChecked);
+ }
+ else
+ {
+ return QVariant(Qt::Checked);
+ }
+ }
+
+ return sourceIndex.data(role);
+ }
+
+ virtual bool setData(const QModelIndex &index, const QVariant &value,
+ int role = Qt::EditRole)
+ {
+ if (index.column() == 0 && role == Qt::CheckStateRole)
+ {
+ Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
+ return setFilterState(index, state);
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role);
+ }
+
+ QString relPath(const QString &path) const
+ {
+ QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot());
+ prefix += '/';
+ if (!path.startsWith(prefix))
+ {
+ return QString();
+ }
+ return path.mid(prefix.size());
+ }
+
+ bool setFilterState(QModelIndex index, Qt::CheckState state)
+ {
+ QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
+
+ if (!fsm)
+ {
+ return false;
+ }
+
+ QModelIndex sourceIndex = mapToSource(index);
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ bool changed = false;
+ if (state == Qt::Unchecked)
+ {
+ // blocking a path
+ auto &node = blocked.insert(blockedPath);
+ // get rid of all blocked nodes below
+ node.clear();
+ changed = true;
+ }
+ else if (state == Qt::Checked || state == Qt::PartiallyChecked)
+ {
+ if (!blocked.remove(blockedPath))
+ {
+ auto cover = blocked.cover(blockedPath);
+ qDebug() << "Blocked by cover" << cover;
+ // uncover
+ blocked.remove(cover);
+ // block all contents, except for any cover
+ QModelIndex rootIndex =
+ fsm->index(FS::PathCombine(m_instance->instanceRoot(), cover));
+ QModelIndex doing = rootIndex;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1)
+ {
+ auto node = doing.child(row, 0);
+ if (!node.isValid())
+ {
+ if (!todo.size())
+ {
+ break;
+ }
+ else
+ {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ auto relpath = relPath(fsm->filePath(node));
+ if (blockedPath.startsWith(relpath)) // cover found?
+ {
+ // continue processing cover later
+ todo.push(node);
+ }
+ else
+ {
+ // or just block this one.
+ blocked.insert(relpath);
+ }
+ row++;
+ }
+ }
+ changed = true;
+ }
+ if (changed)
+ {
+ // update the thing
+ emit dataChanged(index, index, {Qt::CheckStateRole});
+ // update everything above index
+ QModelIndex up = index.parent();
+ while (1)
+ {
+ if (!up.isValid())
+ break;
+ emit dataChanged(up, up, {Qt::CheckStateRole});
+ up = up.parent();
+ }
+ // and everything below the index
+ QModelIndex doing = index;
+ int row = 0;
+ QStack<QModelIndex> todo;
+ while (1)
+ {
+ auto node = doing.child(row, 0);
+ if (!node.isValid())
+ {
+ if (!todo.size())
+ {
+ break;
+ }
+ else
+ {
+ doing = todo.pop();
+ row = 0;
+ continue;
+ }
+ }
+ emit dataChanged(node, node, {Qt::CheckStateRole});
+ todo.push(node);
+ row++;
+ }
+ // siblings and unrelated nodes are ignored
+ }
+ return true;
+ }
+
+ bool shouldExpand(QModelIndex index)
+ {
+ QModelIndex sourceIndex = mapToSource(index);
+ QFileSystemModel *fsm = qobject_cast<QFileSystemModel *>(sourceModel());
+ if (!fsm)
+ {
+ return false;
+ }
+ auto blockedPath = relPath(fsm->filePath(sourceIndex));
+ auto found = blocked.find(blockedPath);
+ if(found)
+ {
+ return !found->leaf();
+ }
+ return false;
+ }
+
+ void setBlockedPaths(QStringList paths)
+ {
+ beginResetModel();
+ blocked.clear();
+ blocked.insert(paths);
+ endResetModel();
+ }
+
+ const SeparatorPrefixTree<'/'> & blockedPaths() const
+ {
+ return blocked;
+ }
+
+protected:
+ bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const
+ {
+ Q_UNUSED(source_parent)
+
+ // adjust the columns you want to filter out here
+ // return false for those that will be hidden
+ if (source_column == 2 || source_column == 3)
+ return false;
+
+ return true;
+ }
+
+private:
+ InstancePtr m_instance;
+ SeparatorPrefixTree<'/'> blocked;
+};
+
+ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent)
+ : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance)
+{
+ ui->setupUi(this);
+ auto model = new QFileSystemModel(this);
+ proxyModel = new PackIgnoreProxy(m_instance, this);
+ loadPackIgnore();
+ proxyModel->setSourceModel(model);
+ auto root = instance->instanceRoot();
+ ui->treeView->setModel(proxyModel);
+ ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root)));
+ ui->treeView->sortByColumn(0, Qt::AscendingOrder);
+
+ connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int)));
+
+ model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden);
+ model->setRootPath(root);
+ auto headerView = ui->treeView->header();
+ headerView->setSectionResizeMode(QHeaderView::ResizeToContents);
+ headerView->setSectionResizeMode(0, QHeaderView::Stretch);
+}
+
+ExportInstanceDialog::~ExportInstanceDialog()
+{
+ delete ui;
+}
+
+/// Save icon to instance's folder is needed
+void SaveIcon(InstancePtr m_instance)
+{
+ auto iconKey = m_instance->iconKey();
+ auto iconList = MMC->icons();
+ auto mmcIcon = iconList->icon(iconKey);
+ if(!mmcIcon || mmcIcon->isBuiltIn()) {
+ return;
+ }
+ auto path = mmcIcon->getFilePath();
+ if(!path.isNull()) {
+ QFileInfo inInfo (path);
+ FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName())) ();
+ return;
+ }
+ auto & image = mmcIcon->m_images[mmcIcon->type()];
+ auto & icon = image.icon;
+ auto sizes = icon.availableSizes();
+ if(sizes.size() == 0)
+ {
+ return;
+ }
+ auto areaOf = [](QSize size)
+ {
+ return size.width() * size.height();
+ };
+ QSize largest = sizes[0];
+ // find variant with largest area
+ for(auto size: sizes)
+ {
+ if(areaOf(largest) < areaOf(size))
+ {
+ largest = size;
+ }
+ }
+ auto pixmap = icon.pixmap(largest);
+ pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png"));
+}
+
+bool ExportInstanceDialog::doExport()
+{
+ auto name = FS::RemoveInvalidFilenameChars(m_instance->name());
+
+ const QString output = QFileDialog::getSaveFileName(
+ this, tr("Export %1").arg(m_instance->name()),
+ FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite);
+ if (output.isEmpty())
+ {
+ return false;
+ }
+ if (QFile::exists(output))
+ {
+ int ret =
+ QMessageBox::question(this, tr("Overwrite?"),
+ tr("This file already exists. Do you want to overwrite it?"),
+ QMessageBox::No, QMessageBox::Yes);
+ if (ret == QMessageBox::No)
+ {
+ return false;
+ }
+ }
+
+ SaveIcon(m_instance);
+
+ auto & blocked = proxyModel->blockedPaths();
+ using std::placeholders::_1;
+ if (!JlCompress::compressDir(output, m_instance->instanceRoot(), name, std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1)))
+ {
+ QMessageBox::warning(this, tr("Error"), tr("Unable to export instance"));
+ return false;
+ }
+ return true;
+}
+
+void ExportInstanceDialog::done(int result)
+{
+ savePackIgnore();
+ if (result == QDialog::Accepted)
+ {
+ if (doExport())
+ {
+ QDialog::done(QDialog::Accepted);
+ return;
+ }
+ else
+ {
+ return;
+ }
+ }
+ QDialog::done(result);
+}
+
+void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom)
+{
+ //WARNING: possible off-by-one?
+ for(int i = top; i < bottom; i++)
+ {
+ auto node = parent.child(i, 0);
+ if(proxyModel->shouldExpand(node))
+ {
+ auto expNode = node.parent();
+ if(!expNode.isValid())
+ {
+ continue;
+ }
+ ui->treeView->expand(node);
+ }
+ }
+}
+
+QString ExportInstanceDialog::ignoreFileName()
+{
+ return FS::PathCombine(m_instance->instanceRoot(), ".packignore");
+}
+
+void ExportInstanceDialog::loadPackIgnore()
+{
+ auto filename = ignoreFileName();
+ QFile ignoreFile(filename);
+ if(!ignoreFile.open(QIODevice::ReadOnly))
+ {
+ return;
+ }
+ auto data = ignoreFile.readAll();
+ auto string = QString::fromUtf8(data);
+ proxyModel->setBlockedPaths(string.split('\n', QString::SkipEmptyParts));
+}
+
+void ExportInstanceDialog::savePackIgnore()
+{
+ auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8();
+ auto filename = ignoreFileName();
+ try
+ {
+ FS::write(filename, data);
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << e.cause();
+ }
+}
+
+#include "ExportInstanceDialog.moc"
diff --git a/launcher/dialogs/ExportInstanceDialog.h b/launcher/dialogs/ExportInstanceDialog.h
new file mode 100644
index 00000000..dea02d1b
--- /dev/null
+++ b/launcher/dialogs/ExportInstanceDialog.h
@@ -0,0 +1,54 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QModelIndex>
+#include <memory>
+
+class BaseInstance;
+class PackIgnoreProxy;
+typedef std::shared_ptr<BaseInstance> InstancePtr;
+
+namespace Ui
+{
+class ExportInstanceDialog;
+}
+
+class ExportInstanceDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ExportInstanceDialog(InstancePtr instance, QWidget *parent = 0);
+ ~ExportInstanceDialog();
+
+ virtual void done(int result);
+
+private:
+ bool doExport();
+ void loadPackIgnore();
+ void savePackIgnore();
+ QString ignoreFileName();
+
+private:
+ Ui::ExportInstanceDialog *ui;
+ InstancePtr m_instance;
+ PackIgnoreProxy * proxyModel;
+
+private slots:
+ void rowsInserted(QModelIndex parent, int top, int bottom);
+};
diff --git a/launcher/dialogs/ExportInstanceDialog.ui b/launcher/dialogs/ExportInstanceDialog.ui
new file mode 100644
index 00000000..bcd4e84a
--- /dev/null
+++ b/launcher/dialogs/ExportInstanceDialog.ui
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportInstanceDialog</class>
+ <widget class="QDialog" name="ExportInstanceDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>720</width>
+ <height>625</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Export Instance</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTreeView" name="treeView">
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>treeView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>ExportInstanceDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>ExportInstanceDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/dialogs/IconPickerDialog.cpp b/launcher/dialogs/IconPickerDialog.cpp
new file mode 100644
index 00000000..90436554
--- /dev/null
+++ b/launcher/dialogs/IconPickerDialog.cpp
@@ -0,0 +1,163 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QKeyEvent>
+#include <QPushButton>
+#include <QFileDialog>
+
+#include "MultiMC.h"
+
+#include "IconPickerDialog.h"
+#include "ui_IconPickerDialog.h"
+
+#include "groupview/InstanceDelegate.h"
+
+#include "icons/IconList.h"
+#include "icons/IconUtils.h"
+#include <DesktopServices.h>
+
+IconPickerDialog::IconPickerDialog(QWidget *parent)
+ : QDialog(parent), ui(new Ui::IconPickerDialog)
+{
+ ui->setupUi(this);
+ setWindowModality(Qt::WindowModal);
+
+ auto contentsWidget = ui->iconView;
+ contentsWidget->setViewMode(QListView::IconMode);
+ contentsWidget->setFlow(QListView::LeftToRight);
+ contentsWidget->setIconSize(QSize(48, 48));
+ contentsWidget->setMovement(QListView::Static);
+ contentsWidget->setResizeMode(QListView::Adjust);
+ contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection);
+ contentsWidget->setSpacing(5);
+ contentsWidget->setWordWrap(false);
+ contentsWidget->setWrapping(true);
+ contentsWidget->setUniformItemSizes(true);
+ contentsWidget->setTextElideMode(Qt::ElideRight);
+ contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ contentsWidget->setItemDelegate(new ListViewDelegate());
+
+ // contentsWidget->setAcceptDrops(true);
+ contentsWidget->setDropIndicatorShown(true);
+ contentsWidget->viewport()->setAcceptDrops(true);
+ contentsWidget->setDragDropMode(QAbstractItemView::DropOnly);
+ contentsWidget->setDefaultDropAction(Qt::CopyAction);
+
+ contentsWidget->installEventFilter(this);
+
+ contentsWidget->setModel(MMC->icons().get());
+
+ // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win.
+ auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole);
+ auto buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole);
+
+ connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon()));
+ connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon()));
+
+ connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex)));
+
+ connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection)));
+
+ auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole);
+ connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder);
+}
+
+bool IconPickerDialog::eventFilter(QObject *obj, QEvent *evt)
+{
+ if (obj != ui->iconView)
+ return QDialog::eventFilter(obj, evt);
+ if (evt->type() != QEvent::KeyPress)
+ {
+ return QDialog::eventFilter(obj, evt);
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt);
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete:
+ removeSelectedIcon();
+ return true;
+ case Qt::Key_Plus:
+ addNewIcon();
+ return true;
+ default:
+ break;
+ }
+ return QDialog::eventFilter(obj, evt);
+}
+
+void IconPickerDialog::addNewIcon()
+{
+ //: The title of the select icons open file dialog
+ QString selectIcons = tr("Select Icons");
+ //: The type of icon files
+ auto filter = IconUtils::getIconFilter();
+ QStringList fileNames = QFileDialog::getOpenFileNames(this, selectIcons, QString(), tr("Icons %1").arg(filter));
+ MMC->icons()->installIcons(fileNames);
+}
+
+void IconPickerDialog::removeSelectedIcon()
+{
+ MMC->icons()->deleteIcon(selectedIconKey);
+}
+
+void IconPickerDialog::activated(QModelIndex index)
+{
+ selectedIconKey = index.data(Qt::UserRole).toString();
+ accept();
+}
+
+void IconPickerDialog::selectionChanged(QItemSelection selected, QItemSelection deselected)
+{
+ if (selected.empty())
+ return;
+
+ QString key = selected.first().indexes().first().data(Qt::UserRole).toString();
+ if (!key.isEmpty())
+ selectedIconKey = key;
+}
+
+int IconPickerDialog::execWithSelection(QString selection)
+{
+ auto list = MMC->icons();
+ auto contentsWidget = ui->iconView;
+ selectedIconKey = selection;
+
+ int index_nr = list->getIconIndex(selection);
+ auto model_index = list->index(index_nr);
+ contentsWidget->selectionModel()->select(
+ model_index, QItemSelectionModel::Current | QItemSelectionModel::Select);
+
+ QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection,
+ Q_ARG(QModelIndex, model_index));
+ return QDialog::exec();
+}
+
+void IconPickerDialog::delayed_scroll(QModelIndex model_index)
+{
+ auto contentsWidget = ui->iconView;
+ contentsWidget->scrollTo(model_index);
+}
+
+IconPickerDialog::~IconPickerDialog()
+{
+ delete ui;
+}
+
+void IconPickerDialog::openFolder()
+{
+ DesktopServices::openDirectory(MMC->icons()->getDirectory(), true);
+}
diff --git a/launcher/dialogs/IconPickerDialog.h b/launcher/dialogs/IconPickerDialog.h
new file mode 100644
index 00000000..9af6a678
--- /dev/null
+++ b/launcher/dialogs/IconPickerDialog.h
@@ -0,0 +1,49 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QDialog>
+#include <QItemSelection>
+
+namespace Ui
+{
+class IconPickerDialog;
+}
+
+class IconPickerDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit IconPickerDialog(QWidget *parent = 0);
+ ~IconPickerDialog();
+ int execWithSelection(QString selection);
+ QString selectedIconKey;
+
+protected:
+ virtual bool eventFilter(QObject *, QEvent *);
+
+private:
+ Ui::IconPickerDialog *ui;
+
+private
+slots:
+ void selectionChanged(QItemSelection, QItemSelection);
+ void activated(QModelIndex);
+ void delayed_scroll(QModelIndex);
+ void addNewIcon();
+ void removeSelectedIcon();
+ void openFolder();
+};
diff --git a/launcher/dialogs/IconPickerDialog.ui b/launcher/dialogs/IconPickerDialog.ui
new file mode 100644
index 00000000..c548edfb
--- /dev/null
+++ b/launcher/dialogs/IconPickerDialog.ui
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>IconPickerDialog</class>
+ <widget class="QDialog" name="IconPickerDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>676</width>
+ <height>555</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Pick icon</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QListView" name="iconView"/>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>IconPickerDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>IconPickerDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp
new file mode 100644
index 00000000..32f8a48f
--- /dev/null
+++ b/launcher/dialogs/LoginDialog.cpp
@@ -0,0 +1,110 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "LoginDialog.h"
+#include "ui_LoginDialog.h"
+
+#include "minecraft/auth/YggdrasilTask.h"
+
+#include <QtWidgets/QPushButton>
+
+LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LoginDialog)
+{
+ ui->setupUi(this);
+ ui->progressBar->setVisible(false);
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+}
+
+LoginDialog::~LoginDialog()
+{
+ delete ui;
+}
+
+// Stage 1: User interaction
+void LoginDialog::accept()
+{
+ setUserInputsEnabled(false);
+ ui->progressBar->setVisible(true);
+
+ // Setup the login task and start it
+ m_account = MojangAccount::createFromUsername(ui->userTextBox->text());
+ m_loginTask = m_account->login(nullptr, ui->passTextBox->text());
+ connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed);
+ connect(m_loginTask.get(), &Task::succeeded, this,
+ &LoginDialog::onTaskSucceeded);
+ connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus);
+ connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress);
+ m_loginTask->start();
+}
+
+void LoginDialog::setUserInputsEnabled(bool enable)
+{
+ ui->userTextBox->setEnabled(enable);
+ ui->passTextBox->setEnabled(enable);
+ ui->buttonBox->setEnabled(enable);
+}
+
+// Enable the OK button only when both textboxes contain something.
+void LoginDialog::on_userTextBox_textEdited(const QString &newText)
+{
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty());
+}
+void LoginDialog::on_passTextBox_textEdited(const QString &newText)
+{
+ ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty());
+}
+
+void LoginDialog::onTaskFailed(const QString &reason)
+{
+ // Set message
+ ui->label->setText("<span style='color:red'>" + reason + "</span>");
+
+ // Re-enable user-interaction
+ setUserInputsEnabled(true);
+ ui->progressBar->setVisible(false);
+}
+
+void LoginDialog::onTaskSucceeded()
+{
+ QDialog::accept();
+}
+
+void LoginDialog::onTaskStatus(const QString &status)
+{
+ ui->label->setText(status);
+}
+
+void LoginDialog::onTaskProgress(qint64 current, qint64 total)
+{
+ ui->progressBar->setMaximum(total);
+ ui->progressBar->setValue(current);
+}
+
+// Public interface
+MojangAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg)
+{
+ LoginDialog dlg(parent);
+ dlg.ui->label->setText(msg);
+ if (dlg.exec() == QDialog::Accepted)
+ {
+ return dlg.m_account;
+ }
+ return 0;
+}
diff --git a/launcher/dialogs/LoginDialog.h b/launcher/dialogs/LoginDialog.h
new file mode 100644
index 00000000..16bdddfb
--- /dev/null
+++ b/launcher/dialogs/LoginDialog.h
@@ -0,0 +1,58 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QtWidgets/QDialog>
+#include <QtCore/QEventLoop>
+
+#include "minecraft/auth/MojangAccount.h"
+
+namespace Ui
+{
+class LoginDialog;
+}
+
+class LoginDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ ~LoginDialog();
+
+ static MojangAccountPtr newAccount(QWidget *parent, QString message);
+
+private:
+ explicit LoginDialog(QWidget *parent = 0);
+
+ void setUserInputsEnabled(bool enable);
+
+protected
+slots:
+ void accept();
+
+ void onTaskFailed(const QString &reason);
+ void onTaskSucceeded();
+ void onTaskStatus(const QString &status);
+ void onTaskProgress(qint64 current, qint64 total);
+
+ void on_userTextBox_textEdited(const QString &newText);
+ void on_passTextBox_textEdited(const QString &newText);
+
+private:
+ Ui::LoginDialog *ui;
+ MojangAccountPtr m_account;
+ std::shared_ptr<Task> m_loginTask;
+};
diff --git a/launcher/dialogs/LoginDialog.ui b/launcher/dialogs/LoginDialog.ui
new file mode 100644
index 00000000..dbdb3b93
--- /dev/null
+++ b/launcher/dialogs/LoginDialog.ui
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LoginDialog</class>
+ <widget class="QDialog" name="LoginDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>421</width>
+ <height>238</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Add Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="microsoftAccountsNoticeLabel">
+ <property name="text">
+ <string>NOTICE: MultiMC does not currently support Microsoft accounts. This means that accounts created from December 2020 onwards cannot be used.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string notr="true">Message label placeholder.</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="userTextBox">
+ <property name="placeholderText">
+ <string>Email</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="passTextBox">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ <property name="placeholderText">
+ <string>Password</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/NewComponentDialog.cpp b/launcher/dialogs/NewComponentDialog.cpp
new file mode 100644
index 00000000..f4d6274f
--- /dev/null
+++ b/launcher/dialogs/NewComponentDialog.cpp
@@ -0,0 +1,106 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MultiMC.h"
+#include "NewComponentDialog.h"
+#include "ui_NewComponentDialog.h"
+
+#include <BaseVersion.h>
+#include <icons/IconList.h>
+#include <tasks/Task.h>
+#include <InstanceList.h>
+
+#include "VersionSelectDialog.h"
+#include "ProgressDialog.h"
+#include "IconPickerDialog.h"
+
+#include <QLayout>
+#include <QPushButton>
+#include <QFileDialog>
+#include <QValidator>
+
+#include <meta/Index.h>
+#include <meta/VersionList.h>
+
+NewComponentDialog::NewComponentDialog(const QString & initialName, const QString & initialUid, QWidget *parent)
+ : QDialog(parent), ui(new Ui::NewComponentDialog)
+{
+ ui->setupUi(this);
+ resize(minimumSizeHint());
+
+ ui->nameTextBox->setText(initialName);
+ ui->uidTextBox->setText(initialUid);
+
+ connect(ui->nameTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState);
+ connect(ui->uidTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState);
+
+ auto groups = MMC->instances()->getGroups().toSet();
+ ui->nameTextBox->setFocus();
+
+ originalPlaceholderText = ui->uidTextBox->placeholderText();
+ updateDialogState();
+}
+
+NewComponentDialog::~NewComponentDialog()
+{
+ delete ui;
+}
+
+void NewComponentDialog::updateDialogState()
+{
+ auto protoUid = ui->nameTextBox->text().toLower();
+ protoUid.remove(QRegularExpression("[^a-z]"));
+ if(protoUid.isEmpty())
+ {
+ ui->uidTextBox->setPlaceholderText(originalPlaceholderText);
+ }
+ else
+ {
+ QString suggestedUid = "org.multimc.custom." + protoUid;
+ ui->uidTextBox->setPlaceholderText(suggestedUid);
+ }
+ bool allowOK = !name().isEmpty() && !uid().isEmpty() && !uidBlacklist.contains(uid());
+ ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowOK);
+}
+
+QString NewComponentDialog::name() const
+{
+ auto result = ui->nameTextBox->text();
+ if(result.size())
+ {
+ return result.trimmed();
+ }
+ return QString();
+}
+
+QString NewComponentDialog::uid() const
+{
+ auto result = ui->uidTextBox->text();
+ if(result.size())
+ {
+ return result.trimmed();
+ }
+ result = ui->uidTextBox->placeholderText();
+ if(result.size() && result != originalPlaceholderText)
+ {
+ return result.trimmed();
+ }
+ return QString();
+}
+
+void NewComponentDialog::setBlacklist(QStringList badUids)
+{
+ uidBlacklist = badUids;
+}
diff --git a/launcher/dialogs/NewComponentDialog.h b/launcher/dialogs/NewComponentDialog.h
new file mode 100644
index 00000000..8c790beb
--- /dev/null
+++ b/launcher/dialogs/NewComponentDialog.h
@@ -0,0 +1,48 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+#include <QString>
+#include <QStringList>
+
+namespace Ui
+{
+class NewComponentDialog;
+}
+
+class NewComponentDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit NewComponentDialog(const QString & initialName = QString(), const QString & initialUid = QString(), QWidget *parent = 0);
+ virtual ~NewComponentDialog();
+ void setBlacklist(QStringList badUids);
+
+ QString name() const;
+ QString uid() const;
+
+private slots:
+ void updateDialogState();
+
+private:
+ Ui::NewComponentDialog *ui;
+
+ QString originalPlaceholderText;
+ QStringList uidBlacklist;
+};
diff --git a/launcher/dialogs/NewComponentDialog.ui b/launcher/dialogs/NewComponentDialog.ui
new file mode 100644
index 00000000..03b0d222
--- /dev/null
+++ b/launcher/dialogs/NewComponentDialog.ui
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewComponentDialog</class>
+ <widget class="QDialog" name="NewComponentDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>345</width>
+ <height>146</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Add Empty Component</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/copy</normaloff>:/icons/toolbar/copy</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLineEdit" name="nameTextBox">
+ <property name="placeholderText">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="uidTextBox">
+ <property name="placeholderText">
+ <string>uid</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>nameTextBox</tabstop>
+ <tabstop>uidTextBox</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../graphics.qrc"/>
+ </resources>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>NewComponentDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>NewComponentDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/launcher/dialogs/NewInstanceDialog.cpp b/launcher/dialogs/NewInstanceDialog.cpp
new file mode 100644
index 00000000..86963149
--- /dev/null
+++ b/launcher/dialogs/NewInstanceDialog.cpp
@@ -0,0 +1,255 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MultiMC.h"
+#include "NewInstanceDialog.h"
+#include "ui_NewInstanceDialog.h"
+
+#include <BaseVersion.h>
+#include <icons/IconList.h>
+#include <tasks/Task.h>
+#include <InstanceList.h>
+
+#include "VersionSelectDialog.h"
+#include "ProgressDialog.h"
+#include "IconPickerDialog.h"
+
+#include <QLayout>
+#include <QPushButton>
+#include <QFileDialog>
+#include <QValidator>
+#include <QDialogButtonBox>
+
+#include "widgets/PageContainer.h"
+#include <pages/modplatform/VanillaPage.h>
+#include <pages/modplatform/atlauncher/AtlPage.h>
+#include <pages/modplatform/ftb/FtbPage.h>
+#include <pages/modplatform/legacy_ftb/Page.h>
+#include <pages/modplatform/flame/FlamePage.h>
+#include <pages/modplatform/ImportPage.h>
+#include <pages/modplatform/technic/TechnicPage.h>
+
+
+
+NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent)
+ : QDialog(parent), ui(new Ui::NewInstanceDialog)
+{
+ ui->setupUi(this);
+
+ setWindowIcon(MMC->getThemedIcon("new"));
+
+ InstIconKey = "default";
+ ui->iconButton->setIcon(MMC->icons()->getIcon(InstIconKey));
+
+ auto groups = MMC->instances()->getGroups().toSet();
+ auto groupList = QStringList(groups.toList());
+ groupList.sort(Qt::CaseInsensitive);
+ groupList.removeOne("");
+ groupList.push_front(initialGroup);
+ groupList.push_front("");
+ ui->groupBox->addItems(groupList);
+ int index = groupList.indexOf(initialGroup);
+ if(index == -1)
+ {
+ index = 0;
+ }
+ ui->groupBox->setCurrentIndex(index);
+ ui->groupBox->lineEdit()->setPlaceholderText(tr("No group"));
+
+
+ // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below.
+ m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+
+ m_container = new PageContainer(this);
+ m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
+ m_container->layout()->setContentsMargins(0, 0, 0, 0);
+ ui->verticalLayout->insertWidget(2, m_container);
+
+ m_container->addButtons(m_buttons);
+
+ // Bonk Qt over its stupid head and make sure it understands which button is the default one...
+ // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
+ auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
+ OkButton->setDefault(true);
+ OkButton->setAutoDefault(true);
+ connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept);
+
+ auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
+ CancelButton->setDefault(false);
+ CancelButton->setAutoDefault(false);
+ connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject);
+
+ auto HelpButton = m_buttons->button(QDialogButtonBox::Help);
+ HelpButton->setDefault(false);
+ HelpButton->setAutoDefault(false);
+ connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help);
+
+ if(!url.isEmpty())
+ {
+ QUrl actualUrl(url);
+ m_container->selectPage("import");
+ importPage->setUrl(url);
+ }
+
+ updateDialogState();
+
+ restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("NewInstanceGeometry").toByteArray()));
+}
+
+void NewInstanceDialog::reject()
+{
+ MMC->settings()->set("NewInstanceGeometry", saveGeometry().toBase64());
+ QDialog::reject();
+}
+
+void NewInstanceDialog::accept()
+{
+ MMC->settings()->set("NewInstanceGeometry", saveGeometry().toBase64());
+ importIconNow();
+ QDialog::accept();
+}
+
+QList<BasePage *> NewInstanceDialog::getPages()
+{
+ importPage = new ImportPage(this);
+ flamePage = new FlamePage(this);
+ auto technicPage = new TechnicPage(this);
+ return
+ {
+ new VanillaPage(this),
+ importPage,
+ new AtlPage(this),
+ flamePage,
+ new FtbPage(this),
+ new LegacyFTB::Page(this),
+ technicPage
+ };
+}
+
+QString NewInstanceDialog::dialogTitle()
+{
+ return tr("New Instance");
+}
+
+NewInstanceDialog::~NewInstanceDialog()
+{
+ delete ui;
+}
+
+void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task)
+{
+ creationTask.reset(task);
+ ui->instNameTextBox->setPlaceholderText(name);
+
+ if(!task)
+ {
+ ui->iconButton->setIcon(MMC->icons()->getIcon("default"));
+ importIcon = false;
+ }
+
+ auto allowOK = task && !instName().isEmpty();
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK);
+}
+
+void NewInstanceDialog::setSuggestedIconFromFile(const QString &path, const QString &name)
+{
+ importIcon = true;
+ importIconPath = path;
+ importIconName = name;
+
+ //Hmm, for some reason they can be to small
+ ui->iconButton->setIcon(QIcon(path));
+}
+
+void NewInstanceDialog::setSuggestedIcon(const QString &key)
+{
+ auto icon = MMC->icons()->getIcon(key);
+ importIcon = false;
+
+ ui->iconButton->setIcon(icon);
+}
+
+InstanceTask * NewInstanceDialog::extractTask()
+{
+ InstanceTask * extracted = creationTask.get();
+ creationTask.release();
+ extracted->setName(instName());
+ extracted->setGroup(instGroup());
+ extracted->setIcon(iconKey());
+ return extracted;
+}
+
+void NewInstanceDialog::updateDialogState()
+{
+ auto allowOK = creationTask && !instName().isEmpty();
+ auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
+ if(OkButton->isEnabled() != allowOK)
+ {
+ OkButton->setEnabled(allowOK);
+ }
+}
+
+QString NewInstanceDialog::instName() const
+{
+ auto result = ui->instNameTextBox->text().trimmed();
+ if(result.size())
+ {
+ return result;
+ }
+ result = ui->instNameTextBox->placeholderText().trimmed();
+ if(result.size())
+ {
+ return result;
+ }
+ return QString();
+}
+
+QString NewInstanceDialog::instGroup() const
+{
+ return ui->groupBox->currentText();
+}
+QString NewInstanceDialog::iconKey() const
+{
+ return InstIconKey;
+}
+
+void NewInstanceDialog::on_iconButton_clicked()
+{
+ importIconNow(); //so the user can switch back
+ IconPickerDialog dlg(this);
+ dlg.execWithSelection(InstIconKey);
+
+ if (dlg.result() == QDialog::Accepted)
+ {
+ InstIconKey = dlg.selectedIconKey;
+ ui->iconButton->setIcon(MMC->icons()->getIcon(InstIconKey));
+ importIcon = false;
+ }
+}
+
+void NewInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1)
+{
+ updateDialogState();
+}
+
+void NewInstanceDialog::importIconNow()
+{
+ if(importIcon) {
+ MMC->icons()->installIcon(importIconPath, importIconName);
+ InstIconKey = importIconName;
+ importIcon = false;
+ }
+ MMC->settings()->set("NewInstanceGeometry", saveGeometry().toBase64());
+}
diff --git a/launcher/dialogs/NewInstanceDialog.h b/launcher/dialogs/NewInstanceDialog.h
new file mode 100644
index 00000000..53abf8cf
--- /dev/null
+++ b/launcher/dialogs/NewInstanceDialog.h
@@ -0,0 +1,80 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+#include "BaseVersion.h"
+#include "pages/BasePageProvider.h"
+#include "InstanceTask.h"
+
+namespace Ui
+{
+class NewInstanceDialog;
+}
+
+class PageContainer;
+class QDialogButtonBox;
+class ImportPage;
+class FlamePage;
+
+class NewInstanceDialog : public QDialog, public BasePageProvider
+{
+ Q_OBJECT
+
+public:
+ explicit NewInstanceDialog(const QString & initialGroup, const QString & url = QString(), QWidget *parent = 0);
+ ~NewInstanceDialog();
+
+ void updateDialogState();
+
+ void setSuggestedPack(const QString & name = QString(), InstanceTask * task = nullptr);
+ void setSuggestedIconFromFile(const QString &path, const QString &name);
+ void setSuggestedIcon(const QString &key);
+
+ InstanceTask * extractTask();
+
+ QString dialogTitle() override;
+ QList<BasePage *> getPages() override;
+
+ QString instName() const;
+ QString instGroup() const;
+ QString iconKey() const;
+
+public slots:
+ void accept() override;
+ void reject() override;
+
+private slots:
+ void on_iconButton_clicked();
+ void on_instNameTextBox_textChanged(const QString &arg1);
+
+private:
+ Ui::NewInstanceDialog *ui = nullptr;
+ PageContainer * m_container = nullptr;
+ QDialogButtonBox * m_buttons = nullptr;
+
+ QString InstIconKey;
+ ImportPage *importPage = nullptr;
+ FlamePage *flamePage = nullptr;
+ std::unique_ptr<InstanceTask> creationTask;
+
+ bool importIcon = false;
+ QString importIconPath;
+ QString importIconName;
+
+ void importIconNow();
+};
diff --git a/launcher/dialogs/NewInstanceDialog.ui b/launcher/dialogs/NewInstanceDialog.ui
new file mode 100644
index 00000000..7fb19ff5
--- /dev/null
+++ b/launcher/dialogs/NewInstanceDialog.ui
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NewInstanceDialog</class>
+ <widget class="QDialog" name="NewInstanceDialog">
+ <property name="windowModality">
+ <enum>Qt::ApplicationModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>730</width>
+ <height>127</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>New Instance</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/new</normaloff>:/icons/toolbar/new</iconset>
+ </property>
+ <property name="modal">
+ <bool>true</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="groupLabel">
+ <property name="text">
+ <string>&amp;Group:</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLineEdit" name="instNameTextBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name:</string>
+ </property>
+ <property name="buddy">
+ <cstring>instNameTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" rowspan="2">
+ <widget class="QToolButton" name="iconButton">
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>iconButton</tabstop>
+ <tabstop>instNameTextBox</tabstop>
+ <tabstop>groupBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/NotificationDialog.cpp b/launcher/dialogs/NotificationDialog.cpp
new file mode 100644
index 00000000..f2a35ae2
--- /dev/null
+++ b/launcher/dialogs/NotificationDialog.cpp
@@ -0,0 +1,86 @@
+#include "NotificationDialog.h"
+#include "ui_NotificationDialog.h"
+
+#include <QTimerEvent>
+#include <QStyle>
+
+NotificationDialog::NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent) :
+ QDialog(parent, Qt::MSWindowsFixedSizeDialogHint | Qt::WindowTitleHint | Qt::CustomizeWindowHint),
+ ui(new Ui::NotificationDialog)
+{
+ ui->setupUi(this);
+
+ QStyle::StandardPixmap icon;
+ switch (entry.type)
+ {
+ case NotificationChecker::NotificationEntry::Critical:
+ icon = QStyle::SP_MessageBoxCritical;
+ break;
+ case NotificationChecker::NotificationEntry::Warning:
+ icon = QStyle::SP_MessageBoxWarning;
+ break;
+ default:
+ case NotificationChecker::NotificationEntry::Information:
+ icon = QStyle::SP_MessageBoxInformation;
+ break;
+ }
+ ui->iconLabel->setPixmap(style()->standardPixmap(icon, 0, this));
+ ui->messageLabel->setText(entry.message);
+
+ m_dontShowAgainText = tr("Don't show again");
+ m_closeText = tr("Close");
+
+ ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime));
+ ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime));
+
+ startTimer(1000);
+}
+
+NotificationDialog::~NotificationDialog()
+{
+ delete ui;
+}
+
+void NotificationDialog::timerEvent(QTimerEvent *event)
+{
+ if (m_dontShowAgainTime > 0)
+ {
+ m_dontShowAgainTime--;
+ if (m_dontShowAgainTime == 0)
+ {
+ ui->dontShowAgainBtn->setText(m_dontShowAgainText);
+ ui->dontShowAgainBtn->setEnabled(true);
+ }
+ else
+ {
+ ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime));
+ }
+ }
+ if (m_closeTime > 0)
+ {
+ m_closeTime--;
+ if (m_closeTime == 0)
+ {
+ ui->closeBtn->setText(m_closeText);
+ ui->closeBtn->setEnabled(true);
+ }
+ else
+ {
+ ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime));
+ }
+ }
+
+ if (m_closeTime == 0 && m_dontShowAgainTime == 0)
+ {
+ killTimer(event->timerId());
+ }
+}
+
+void NotificationDialog::on_dontShowAgainBtn_clicked()
+{
+ done(DontShowAgain);
+}
+void NotificationDialog::on_closeBtn_clicked()
+{
+ done(Normal);
+}
diff --git a/launcher/dialogs/NotificationDialog.h b/launcher/dialogs/NotificationDialog.h
new file mode 100644
index 00000000..e1cbb9fa
--- /dev/null
+++ b/launcher/dialogs/NotificationDialog.h
@@ -0,0 +1,44 @@
+#ifndef NOTIFICATIONDIALOG_H
+#define NOTIFICATIONDIALOG_H
+
+#include <QDialog>
+
+#include "notifications/NotificationChecker.h"
+
+namespace Ui {
+class NotificationDialog;
+}
+
+class NotificationDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent = 0);
+ ~NotificationDialog();
+
+ enum ExitCode
+ {
+ Normal,
+ DontShowAgain
+ };
+
+protected:
+ void timerEvent(QTimerEvent *event);
+
+private:
+ Ui::NotificationDialog *ui;
+
+ int m_dontShowAgainTime = 10;
+ int m_closeTime = 5;
+
+ QString m_dontShowAgainText;
+ QString m_closeText;
+
+private
+slots:
+ void on_dontShowAgainBtn_clicked();
+ void on_closeBtn_clicked();
+};
+
+#endif // NOTIFICATIONDIALOG_H
diff --git a/launcher/dialogs/NotificationDialog.ui b/launcher/dialogs/NotificationDialog.ui
new file mode 100644
index 00000000..3e6c22bc
--- /dev/null
+++ b/launcher/dialogs/NotificationDialog.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NotificationDialog</class>
+ <widget class="QDialog" name="NotificationDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>320</width>
+ <height>240</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Notification</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
+ <item>
+ <widget class="QLabel" name="iconLabel">
+ <property name="text">
+ <string notr="true">TextLabel</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="messageLabel">
+ <property name="text">
+ <string notr="true">TextLabel</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextBrowserInteraction</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="dontShowAgainBtn">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Don't show again</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeBtn">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/ProfileSelectDialog.cpp b/launcher/dialogs/ProfileSelectDialog.cpp
new file mode 100644
index 00000000..ae34709f
--- /dev/null
+++ b/launcher/dialogs/ProfileSelectDialog.cpp
@@ -0,0 +1,116 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ProfileSelectDialog.h"
+#include <SkinUtils.h>
+#include "ui_ProfileSelectDialog.h"
+
+#include <QItemSelectionModel>
+
+#include <QDebug>
+
+#include <dialogs/ProgressDialog.h>
+
+#include <MultiMC.h>
+
+ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWidget *parent)
+ : QDialog(parent), ui(new Ui::ProfileSelectDialog)
+{
+ ui->setupUi(this);
+
+ m_accounts = MMC->accounts();
+ auto view = ui->listView;
+ //view->setModel(m_accounts.get());
+ //view->hideColumn(MojangAccountList::ActiveColumn);
+ view->setColumnCount(1);
+ view->setRootIsDecorated(false);
+ if(QTreeWidgetItem* header = view->headerItem())
+ {
+ header->setText(0, tr("Name"));
+ }
+ else
+ {
+ view->setHeaderLabel(tr("Name"));
+ }
+ QList <QTreeWidgetItem *> items;
+ for (int i = 0; i < m_accounts->count(); i++)
+ {
+ MojangAccountPtr account = m_accounts->at(i);
+ for (auto profile : account->profiles())
+ {
+ auto profileLabel = profile.name;
+ if(account->isInUse())
+ {
+ profileLabel += tr(" (in use)");
+ }
+ auto item = new QTreeWidgetItem(view);
+ item->setText(0, profileLabel);
+ item->setIcon(0, SkinUtils::getFaceFromCache(profile.id));
+ item->setData(0, MojangAccountList::PointerRole, QVariant::fromValue(account));
+ items.append(item);
+ }
+ }
+ view->addTopLevelItems(items);
+
+ // Set the message label.
+ ui->msgLabel->setVisible(!message.isEmpty());
+ ui->msgLabel->setText(message);
+
+ // Flags...
+ ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox);
+ ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox);
+ qDebug() << flags;
+
+ // Select the first entry in the list.
+ ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0));
+
+ connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(on_buttonBox_accepted()));
+}
+
+ProfileSelectDialog::~ProfileSelectDialog()
+{
+ delete ui;
+}
+
+MojangAccountPtr ProfileSelectDialog::selectedAccount() const
+{
+ return m_selected;
+}
+
+bool ProfileSelectDialog::useAsGlobalDefault() const
+{
+ return ui->globalDefaultCheck->isChecked();
+}
+
+bool ProfileSelectDialog::useAsInstDefaullt() const
+{
+ return ui->instDefaultCheck->isChecked();
+}
+
+void ProfileSelectDialog::on_buttonBox_accepted()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ m_selected = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
+ }
+ close();
+}
+
+void ProfileSelectDialog::on_buttonBox_rejected()
+{
+ close();
+}
diff --git a/launcher/dialogs/ProfileSelectDialog.h b/launcher/dialogs/ProfileSelectDialog.h
new file mode 100644
index 00000000..9f95830c
--- /dev/null
+++ b/launcher/dialogs/ProfileSelectDialog.h
@@ -0,0 +1,90 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+#include <memory>
+
+#include "minecraft/auth/MojangAccountList.h"
+
+namespace Ui
+{
+class ProfileSelectDialog;
+}
+
+class ProfileSelectDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ enum Flags
+ {
+ NoFlags = 0,
+
+ /*!
+ * Shows a check box on the dialog that allows the user to specify that the account
+ * they've selected should be used as the global default for all instances.
+ */
+ GlobalDefaultCheckbox,
+
+ /*!
+ * Shows a check box on the dialog that allows the user to specify that the account
+ * they've selected should be used as the default for the instance they are currently launching.
+ * This is not currently implemented.
+ */
+ InstanceDefaultCheckbox,
+ };
+
+ /*!
+ * Constructs a new account select dialog with the given parent and message.
+ * The message will be shown at the top of the dialog. It is an empty string by default.
+ */
+ explicit ProfileSelectDialog(const QString& message="", int flags=0, QWidget *parent = 0);
+ ~ProfileSelectDialog();
+
+ /*!
+ * Gets a pointer to the account that the user selected.
+ * This is null if the user clicked cancel or hasn't clicked OK yet.
+ */
+ MojangAccountPtr selectedAccount() const;
+
+ /*!
+ * Returns true if the user checked the "use as global default" checkbox.
+ * If the checkbox wasn't shown, this function returns false.
+ */
+ bool useAsGlobalDefault() const;
+
+ /*!
+ * Returns true if the user checked the "use as instance default" checkbox.
+ * If the checkbox wasn't shown, this function returns false.
+ */
+ bool useAsInstDefaullt() const;
+
+public
+slots:
+ void on_buttonBox_accepted();
+
+ void on_buttonBox_rejected();
+
+protected:
+ std::shared_ptr<MojangAccountList> m_accounts;
+
+ //! The account that was selected when the user clicked OK.
+ MojangAccountPtr m_selected;
+
+private:
+ Ui::ProfileSelectDialog *ui;
+};
diff --git a/launcher/dialogs/ProfileSelectDialog.ui b/launcher/dialogs/ProfileSelectDialog.ui
new file mode 100644
index 00000000..e779b51b
--- /dev/null
+++ b/launcher/dialogs/ProfileSelectDialog.ui
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProfileSelectDialog</class>
+ <widget class="QDialog" name="ProfileSelectDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>465</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Select an Account</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="msgLabel">
+ <property name="text">
+ <string>Select a profile.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTreeWidget" name="listView">
+ <column>
+ <property name="text">
+ <string notr="true">1</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QCheckBox" name="globalDefaultCheck">
+ <property name="text">
+ <string>Use as default?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="instDefaultCheck">
+ <property name="text">
+ <string>Use as default for this instance only?</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/ProgressDialog.cpp b/launcher/dialogs/ProgressDialog.cpp
new file mode 100644
index 00000000..4b092859
--- /dev/null
+++ b/launcher/dialogs/ProgressDialog.cpp
@@ -0,0 +1,196 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ProgressDialog.h"
+#include "ui_ProgressDialog.h"
+
+#include <QKeyEvent>
+#include <QDebug>
+
+#include "tasks/Task.h"
+
+ProgressDialog::ProgressDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ProgressDialog)
+{
+ ui->setupUi(this);
+ this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ setSkipButton(false);
+ changeProgress(0, 100);
+}
+
+void ProgressDialog::setSkipButton(bool present, QString label)
+{
+ ui->skipButton->setAutoDefault(false);
+ ui->skipButton->setDefault(false);
+ ui->skipButton->setFocusPolicy(Qt::ClickFocus);
+ ui->skipButton->setEnabled(present);
+ ui->skipButton->setVisible(present);
+ ui->skipButton->setText(label);
+ updateSize();
+}
+
+void ProgressDialog::on_skipButton_clicked(bool checked)
+{
+ Q_UNUSED(checked);
+ task->abort();
+}
+
+ProgressDialog::~ProgressDialog()
+{
+ delete ui;
+}
+
+void ProgressDialog::updateSize()
+{
+ QSize qSize = QSize(480, minimumSizeHint().height());
+ resize(qSize);
+ setFixedSize(qSize);
+}
+
+int ProgressDialog::execWithTask(Task *task)
+{
+ this->task = task;
+ QDialog::DialogCode result;
+
+ if(!task)
+ {
+ qDebug() << "Programmer error: progress dialog created with null task.";
+ return Accepted;
+ }
+
+ if(handleImmediateResult(result))
+ {
+ return result;
+ }
+
+ // Connect signals.
+ connect(task, SIGNAL(started()), SLOT(onTaskStarted()));
+ connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString)));
+ connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded()));
+ connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &)));
+ connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64)));
+
+ // if this didn't connect to an already running task, invoke start
+ if(!task->isRunning())
+ {
+ task->start();
+ }
+ if(task->isRunning())
+ {
+ changeProgress(task->getProgress(), task->getTotalProgress());
+ changeStatus(task->getStatus());
+ return QDialog::exec();
+ }
+ else if(handleImmediateResult(result))
+ {
+ return result;
+ }
+ else
+ {
+ return QDialog::Rejected;
+ }
+}
+
+// TODO: only provide the unique_ptr overloads
+int ProgressDialog::execWithTask(std::unique_ptr<Task> &&task)
+{
+ connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
+ return execWithTask(task.release());
+}
+int ProgressDialog::execWithTask(std::unique_ptr<Task> &task)
+{
+ connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater);
+ return execWithTask(task.release());
+}
+
+bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result)
+{
+ if(task->isFinished())
+ {
+ if(task->wasSuccessful())
+ {
+ result = QDialog::Accepted;
+ }
+ else
+ {
+ result = QDialog::Rejected;
+ }
+ return true;
+ }
+ return false;
+}
+
+Task *ProgressDialog::getTask()
+{
+ return task;
+}
+
+void ProgressDialog::onTaskStarted()
+{
+}
+
+void ProgressDialog::onTaskFailed(QString failure)
+{
+ reject();
+}
+
+void ProgressDialog::onTaskSucceeded()
+{
+ accept();
+}
+
+void ProgressDialog::changeStatus(const QString &status)
+{
+ ui->statusLabel->setText(status);
+ updateSize();
+}
+
+void ProgressDialog::changeProgress(qint64 current, qint64 total)
+{
+ ui->taskProgressBar->setMaximum(total);
+ ui->taskProgressBar->setValue(current);
+}
+
+void ProgressDialog::keyPressEvent(QKeyEvent *e)
+{
+ if(ui->skipButton->isVisible())
+ {
+ if (e->key() == Qt::Key_Escape)
+ {
+ on_skipButton_clicked(true);
+ return;
+ }
+ else if(e->key() == Qt::Key_Tab)
+ {
+ ui->skipButton->setFocusPolicy(Qt::StrongFocus);
+ ui->skipButton->setFocus();
+ ui->skipButton->setAutoDefault(true);
+ ui->skipButton->setDefault(true);
+ return;
+ }
+ }
+ QDialog::keyPressEvent(e);
+}
+
+void ProgressDialog::closeEvent(QCloseEvent *e)
+{
+ if (task && task->isRunning())
+ {
+ e->ignore();
+ }
+ else
+ {
+ QDialog::closeEvent(e);
+ }
+}
diff --git a/launcher/dialogs/ProgressDialog.h b/launcher/dialogs/ProgressDialog.h
new file mode 100644
index 00000000..b28ad4fa
--- /dev/null
+++ b/launcher/dialogs/ProgressDialog.h
@@ -0,0 +1,71 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <memory>
+
+class Task;
+
+namespace Ui
+{
+class ProgressDialog;
+}
+
+class ProgressDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ProgressDialog(QWidget *parent = 0);
+ ~ProgressDialog();
+
+ void updateSize();
+
+ int execWithTask(Task *task);
+ int execWithTask(std::unique_ptr<Task> &&task);
+ int execWithTask(std::unique_ptr<Task> &task);
+
+ void setSkipButton(bool present, QString label = QString());
+
+ Task *getTask();
+
+public
+slots:
+ void onTaskStarted();
+ void onTaskFailed(QString failure);
+ void onTaskSucceeded();
+
+ void changeStatus(const QString &status);
+ void changeProgress(qint64 current, qint64 total);
+
+
+private
+slots:
+ void on_skipButton_clicked(bool checked);
+
+protected:
+ virtual void keyPressEvent(QKeyEvent *e);
+ virtual void closeEvent(QCloseEvent *e);
+
+private:
+ bool handleImmediateResult(QDialog::DialogCode &result);
+
+private:
+ Ui::ProgressDialog *ui;
+
+ Task *task;
+};
diff --git a/launcher/dialogs/ProgressDialog.ui b/launcher/dialogs/ProgressDialog.ui
new file mode 100644
index 00000000..04b8fef3
--- /dev/null
+++ b/launcher/dialogs/ProgressDialog.ui
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProgressDialog</class>
+ <widget class="QDialog" name="ProgressDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>100</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>400</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>600</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>Please wait...</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="statusLabel">
+ <property name="text">
+ <string>Task Status...</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QProgressBar" name="taskProgressBar">
+ <property name="value">
+ <number>24</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QPushButton" name="skipButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Skip</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp
new file mode 100644
index 00000000..56133529
--- /dev/null
+++ b/launcher/dialogs/SkinUploadDialog.cpp
@@ -0,0 +1,114 @@
+#include <QFileInfo>
+#include <QFileDialog>
+#include <FileSystem.h>
+#include <minecraft/services/SkinUpload.h>
+#include "SkinUploadDialog.h"
+#include "ui_SkinUploadDialog.h"
+#include "ProgressDialog.h"
+#include "CustomMessageBox.h"
+
+void SkinUploadDialog::on_buttonBox_rejected()
+{
+ close();
+}
+
+void SkinUploadDialog::on_buttonBox_accepted()
+{
+ AuthSessionPtr session = std::make_shared<AuthSession>();
+ auto login = m_acct->login(session);
+ ProgressDialog prog(this);
+ if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted)
+ {
+ //FIXME: recover with password prompt
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to login!"), QMessageBox::Warning)->exec();
+ close();
+ return;
+ }
+ QString fileName;
+ QString input = ui->skinPathTextBox->text();
+ QRegExp urlPrefixMatcher("^([a-z]+)://.+$");
+ bool isLocalFile = false;
+ // it has an URL prefix -> it is an URL
+ if(urlPrefixMatcher.exactMatch(input))
+ {
+ QUrl fileURL = input;
+ if(fileURL.isValid())
+ {
+ // local?
+ if(fileURL.isLocalFile())
+ {
+ isLocalFile = true;
+ fileName = fileURL.toLocalFile();
+ }
+ else
+ {
+ CustomMessageBox::selectable(
+ this,
+ tr("Skin Upload"),
+ tr("Using remote URLs for setting skins is not implemented yet."),
+ QMessageBox::Warning
+ )->exec();
+ close();
+ return;
+ }
+ }
+ else
+ {
+ CustomMessageBox::selectable(
+ this,
+ tr("Skin Upload"),
+ tr("You cannot use an invalid URL for uploading skins."),
+ QMessageBox::Warning
+ )->exec();
+ close();
+ return;
+ }
+ }
+ else
+ {
+ // just assume it's a path then
+ isLocalFile = true;
+ fileName = ui->skinPathTextBox->text();
+ }
+ if (isLocalFile && !QFile::exists(fileName))
+ {
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
+ close();
+ return;
+ }
+ SkinUpload::Model model = SkinUpload::STEVE;
+ if (ui->steveBtn->isChecked())
+ {
+ model = SkinUpload::STEVE;
+ }
+ else if (ui->alexBtn->isChecked())
+ {
+ model = SkinUpload::ALEX;
+ }
+ SkinUploadPtr upload = std::make_shared<SkinUpload>(this, session, FS::read(fileName), model);
+ if (prog.execWithTask((Task*)upload.get()) != QDialog::Accepted)
+ {
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec();
+ close();
+ return;
+ }
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec();
+ close();
+}
+
+void SkinUploadDialog::on_skinBrowseBtn_clicked()
+{
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), "*.png");
+ if (raw_path.isEmpty() || !QFileInfo::exists(raw_path))
+ {
+ return;
+ }
+ QString cooked_path = FS::NormalizePath(raw_path);
+ ui->skinPathTextBox->setText(cooked_path);
+}
+
+SkinUploadDialog::SkinUploadDialog(MojangAccountPtr acct, QWidget *parent)
+ :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog)
+{
+ ui->setupUi(this);
+}
diff --git a/launcher/dialogs/SkinUploadDialog.h b/launcher/dialogs/SkinUploadDialog.h
new file mode 100644
index 00000000..deb44eac
--- /dev/null
+++ b/launcher/dialogs/SkinUploadDialog.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <QDialog>
+#include <minecraft/auth/MojangAccount.h>
+
+namespace Ui
+{
+ class SkinUploadDialog;
+}
+
+class SkinUploadDialog : public QDialog {
+ Q_OBJECT
+public:
+ explicit SkinUploadDialog(MojangAccountPtr acct, QWidget *parent = 0);
+ virtual ~SkinUploadDialog() {};
+
+public slots:
+ void on_buttonBox_accepted();
+
+ void on_buttonBox_rejected();
+
+ void on_skinBrowseBtn_clicked();
+
+protected:
+ MojangAccountPtr m_acct;
+
+private:
+ Ui::SkinUploadDialog *ui;
+};
diff --git a/launcher/dialogs/SkinUploadDialog.ui b/launcher/dialogs/SkinUploadDialog.ui
new file mode 100644
index 00000000..6f5307e3
--- /dev/null
+++ b/launcher/dialogs/SkinUploadDialog.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SkinUploadDialog</class>
+ <widget class="QDialog" name="SkinUploadDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>413</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Skin Upload</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="fileBox">
+ <property name="title">
+ <string>Skin File</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLineEdit" name="skinPathTextBox"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="skinBrowseBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>28</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="modelBox">
+ <property name="title">
+ <string>Player Model</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_1">
+ <item>
+ <widget class="QRadioButton" name="steveBtn">
+ <property name="text">
+ <string>Steve Model</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="alexBtn">
+ <property name="text">
+ <string>Alex Model</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/UpdateDialog.cpp b/launcher/dialogs/UpdateDialog.cpp
new file mode 100644
index 00000000..2baaf5e9
--- /dev/null
+++ b/launcher/dialogs/UpdateDialog.cpp
@@ -0,0 +1,182 @@
+#include "UpdateDialog.h"
+#include "ui_UpdateDialog.h"
+#include <QDebug>
+#include "MultiMC.h"
+#include <settings/SettingsObject.h>
+#include <Json.h>
+
+#include "BuildConfig.h"
+#include "HoeDown.h"
+
+UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog)
+{
+ ui->setupUi(this);
+ auto channel = MMC->settings()->get("UpdateChannel").toString();
+ if(hasUpdate)
+ {
+ ui->label->setText(tr("A new %1 update is available!").arg(channel));
+ }
+ else
+ {
+ ui->label->setText(tr("No %1 updates found. You are running the latest version.").arg(channel));
+ ui->btnUpdateNow->setHidden(true);
+ ui->btnUpdateLater->setText(tr("Close"));
+ }
+ ui->changelogBrowser->setHtml(tr("<center><h1>Loading changelog...</h1></center>"));
+ loadChangelog();
+ restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("UpdateDialogGeometry").toByteArray()));
+}
+
+UpdateDialog::~UpdateDialog()
+{
+}
+
+void UpdateDialog::loadChangelog()
+{
+ auto channel = MMC->settings()->get("UpdateChannel").toString();
+ dljob.reset(new NetJob("Changelog"));
+ QString url;
+ if(channel == "stable")
+ {
+ url = QString("https://raw.githubusercontent.com/MultiMC/MultiMC5/%1/changelog.md").arg(channel);
+ m_changelogType = CHANGELOG_MARKDOWN;
+ }
+ else
+ {
+ url = QString("https://api.github.com/repos/MultiMC/MultiMC5/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel);
+ m_changelogType = CHANGELOG_COMMITS;
+ }
+ dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData));
+ connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded);
+ connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed);
+ dljob->start();
+}
+
+QString reprocessMarkdown(QByteArray markdown)
+{
+ HoeDown hoedown;
+ QString output = hoedown.process(markdown);
+
+ // HACK: easier than customizing hoedown
+ output.replace(QRegExp("GH-([0-9]+)"), "<a href=\"https://github.com/MultiMC/MultiMC5/issues/\\1\">GH-\\1</a>");
+ qDebug() << output;
+ return output;
+}
+
+QString reprocessCommits(QByteArray json)
+{
+ auto channel = MMC->settings()->get("UpdateChannel").toString();
+ try
+ {
+ QString result;
+ auto document = Json::requireDocument(json);
+ auto rootobject = Json::requireObject(document);
+ auto status = Json::requireString(rootobject, "status");
+ auto diff_url = Json::requireString(rootobject, "html_url");
+
+ auto print_commits = [&]()
+ {
+ result += "<table cellspacing=0 cellpadding=2 style='border-width: 1px; border-style: solid'>";
+ auto commitarray = Json::requireArray(rootobject, "commits");
+ for(int i = commitarray.size() - 1; i >= 0; i--)
+ {
+ const auto & commitval = commitarray[i];
+ auto commitobj = Json::requireObject(commitval);
+ auto parents_info = Json::ensureArray(commitobj, "parents");
+ // NOTE: this ignores merge commits, because they have more than one parent
+ if(parents_info.size() > 1)
+ {
+ continue;
+ }
+ auto commit_url = Json::requireString(commitobj, "html_url");
+ auto commit_info = Json::requireObject(commitobj, "commit");
+ auto commit_message = Json::requireString(commit_info, "message");
+ auto lines = commit_message.split('\n');
+ QRegularExpression regexp("(?<prefix>(GH-(?<issuenr>[0-9]+))|(NOISSUE)|(SCRATCH))? *(?<rest>.*) *");
+ auto match = regexp.match(lines.takeFirst(), 0, QRegularExpression::NormalMatch);
+ auto issuenr = match.captured("issuenr");
+ auto prefix = match.captured("prefix");
+ auto rest = match.captured("rest");
+ result += "<tr><td>";
+ if(issuenr.length())
+ {
+ result += QString("<a href=\"https://github.com/MultiMC/MultiMC5/issues/%1\">GH-%2</a>").arg(issuenr, issuenr);
+ }
+ else if(prefix.length())
+ {
+ result += QString("<a href=\"%1\">%2</a>").arg(commit_url, prefix);
+ }
+ else
+ {
+ result += QString("<a href=\"%1\">NOISSUE</a>").arg(commit_url);
+ }
+ result += "</td>";
+ lines.prepend(rest);
+ result += "<td><p>" + lines.join("<br />") + "</p></td></tr>";
+ }
+ result += "</table>";
+ };
+
+ if(status == "identical")
+ {
+ return QObject::tr("<p>There are no code changes between your current version and latest %1.</p>").arg(channel);
+ }
+ else if(status == "ahead")
+ {
+ result += QObject::tr("<p>Following commits were added since last update:</p>");
+ print_commits();
+ }
+ else if(status == "diverged")
+ {
+ auto commit_ahead = Json::requireInteger(rootobject, "ahead_by");
+ auto commit_behind = Json::requireInteger(rootobject, "behind_by");
+ result += QObject::tr("<p>The update removes %1 commits and adds the following %2:</p>").arg(commit_behind).arg(commit_ahead);
+ print_commits();
+ }
+ result += QObject::tr("<p>You can <a href=\"%1\">look at the changes on github</a>.</p>").arg(diff_url);
+ return result;
+ }
+ catch (const JSONValidationError &e)
+ {
+ qWarning() << "Got an unparseable commit log from github:" << e.what();
+ qDebug() << json;
+ }
+ return QString();
+}
+
+void UpdateDialog::changelogLoaded()
+{
+ QString result;
+ switch(m_changelogType)
+ {
+ case CHANGELOG_COMMITS:
+ result = reprocessCommits(changelogData);
+ break;
+ case CHANGELOG_MARKDOWN:
+ result = reprocessMarkdown(changelogData);
+ break;
+ }
+ changelogData.clear();
+ ui->changelogBrowser->setHtml(result);
+}
+
+void UpdateDialog::changelogFailed(QString reason)
+{
+ ui->changelogBrowser->setHtml(tr("<p align=\"center\" <span style=\"font-size:22pt;\">Failed to fetch changelog... Error: %1</span></p>").arg(reason));
+}
+
+void UpdateDialog::on_btnUpdateLater_clicked()
+{
+ reject();
+}
+
+void UpdateDialog::on_btnUpdateNow_clicked()
+{
+ done(UPDATE_NOW);
+}
+
+void UpdateDialog::closeEvent(QCloseEvent* evt)
+{
+ MMC->settings()->set("UpdateDialogGeometry", saveGeometry().toBase64());
+ QDialog::closeEvent(evt);
+}
diff --git a/launcher/dialogs/UpdateDialog.h b/launcher/dialogs/UpdateDialog.h
new file mode 100644
index 00000000..ae1799c3
--- /dev/null
+++ b/launcher/dialogs/UpdateDialog.h
@@ -0,0 +1,67 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include "net/NetJob.h"
+
+namespace Ui
+{
+class UpdateDialog;
+}
+
+enum UpdateAction
+{
+ UPDATE_LATER = QDialog::Rejected,
+ UPDATE_NOW = QDialog::Accepted,
+};
+
+enum ChangelogType
+{
+ CHANGELOG_MARKDOWN,
+ CHANGELOG_COMMITS
+};
+
+class UpdateDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit UpdateDialog(bool hasUpdate = true, QWidget *parent = 0);
+ ~UpdateDialog();
+
+public slots:
+ void on_btnUpdateNow_clicked();
+ void on_btnUpdateLater_clicked();
+
+ /// Starts loading the changelog
+ void loadChangelog();
+
+ /// Slot for when the chengelog loads successfully.
+ void changelogLoaded();
+
+ /// Slot for when the chengelog fails to load...
+ void changelogFailed(QString reason);
+
+protected:
+ void closeEvent(QCloseEvent * ) override;
+
+private:
+ Ui::UpdateDialog *ui;
+ QByteArray changelogData;
+ NetJobPtr dljob;
+ ChangelogType m_changelogType = CHANGELOG_MARKDOWN;
+};
diff --git a/launcher/dialogs/UpdateDialog.ui b/launcher/dialogs/UpdateDialog.ui
new file mode 100644
index 00000000..b0b3dd83
--- /dev/null
+++ b/launcher/dialogs/UpdateDialog.ui
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>UpdateDialog</class>
+ <widget class="QDialog" name="UpdateDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>657</width>
+ <height>673</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>MultiMC Update</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normaloff>:/icons/toolbar/checkupdate</normaloff>:/icons/toolbar/checkupdate</iconset>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="font">
+ <font>
+ <pointsize>14</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>changelogBrowser</cstring>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTextBrowser" name="changelogBrowser">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QPushButton" name="btnUpdateNow">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Update now</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="btnUpdateLater">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Don't update yet</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>changelogBrowser</tabstop>
+ <tabstop>btnUpdateNow</tabstop>
+ <tabstop>btnUpdateLater</tabstop>
+ </tabstops>
+ <resources>
+ <include location="../../resources/multimc/multimc.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/launcher/dialogs/VersionSelectDialog.cpp b/launcher/dialogs/VersionSelectDialog.cpp
new file mode 100644
index 00000000..ed1210ba
--- /dev/null
+++ b/launcher/dialogs/VersionSelectDialog.cpp
@@ -0,0 +1,141 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "VersionSelectDialog.h"
+
+#include <QtWidgets/QButtonGroup>
+#include <QtWidgets/QDialogButtonBox>
+#include <QtWidgets/QHBoxLayout>
+#include <QtWidgets/QPushButton>
+#include <QtWidgets/QVBoxLayout>
+
+#include <dialogs/ProgressDialog.h>
+#include "CustomMessageBox.h"
+
+#include <BaseVersion.h>
+#include <BaseVersionList.h>
+#include <tasks/Task.h>
+#include <QDebug>
+#include "MultiMC.h"
+#include <VersionProxyModel.h>
+#include <widgets/VersionSelectWidget.h>
+
+VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent, bool cancelable)
+ : QDialog(parent)
+{
+ setObjectName(QStringLiteral("VersionSelectDialog"));
+ resize(400, 347);
+ m_verticalLayout = new QVBoxLayout(this);
+ m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+
+ m_versionWidget = new VersionSelectWidget(parent);
+ m_verticalLayout->addWidget(m_versionWidget);
+
+ m_horizontalLayout = new QHBoxLayout();
+ m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+
+ m_refreshButton = new QPushButton(this);
+ m_refreshButton->setObjectName(QStringLiteral("refreshButton"));
+ m_horizontalLayout->addWidget(m_refreshButton);
+
+ m_buttonBox = new QDialogButtonBox(this);
+ m_buttonBox->setObjectName(QStringLiteral("buttonBox"));
+ m_buttonBox->setOrientation(Qt::Horizontal);
+ m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
+ m_horizontalLayout->addWidget(m_buttonBox);
+
+ m_verticalLayout->addLayout(m_horizontalLayout);
+
+ retranslate();
+
+ QObject::connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
+ QObject::connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
+
+ QMetaObject::connectSlotsByName(this);
+ setWindowModality(Qt::WindowModal);
+ setWindowTitle(title);
+
+ m_vlist = vlist;
+
+ if (!cancelable)
+ {
+ m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
+ }
+}
+
+void VersionSelectDialog::retranslate()
+{
+ // FIXME: overrides custom title given in constructor!
+ setWindowTitle(tr("Choose Version"));
+ m_refreshButton->setToolTip(tr("Reloads the version list."));
+ m_refreshButton->setText(tr("&Refresh"));
+}
+
+void VersionSelectDialog::setCurrentVersion(const QString& version)
+{
+ m_currentVersion = version;
+ m_versionWidget->setCurrentVersion(version);
+}
+
+void VersionSelectDialog::setEmptyString(QString emptyString)
+{
+ m_versionWidget->setEmptyString(emptyString);
+}
+
+void VersionSelectDialog::setEmptyErrorString(QString emptyErrorString)
+{
+ m_versionWidget->setEmptyErrorString(emptyErrorString);
+}
+
+void VersionSelectDialog::setResizeOn(int column)
+{
+ resizeOnColumn = column;
+}
+
+int VersionSelectDialog::exec()
+{
+ QDialog::open();
+ m_versionWidget->initialize(m_vlist);
+ if(resizeOnColumn != -1)
+ {
+ m_versionWidget->setResizeOn(resizeOnColumn);
+ }
+ return QDialog::exec();
+}
+
+void VersionSelectDialog::selectRecommended()
+{
+ m_versionWidget->selectRecommended();
+}
+
+BaseVersionPtr VersionSelectDialog::selectedVersion() const
+{
+ return m_versionWidget->selectedVersion();
+}
+
+void VersionSelectDialog::on_refreshButton_clicked()
+{
+ m_versionWidget->loadList();
+}
+
+void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_versionWidget->setExactFilter(role, filter);
+}
+
+void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_versionWidget->setFuzzyFilter(role, filter);
+}
diff --git a/launcher/dialogs/VersionSelectDialog.h b/launcher/dialogs/VersionSelectDialog.h
new file mode 100644
index 00000000..ed30d3f3
--- /dev/null
+++ b/launcher/dialogs/VersionSelectDialog.h
@@ -0,0 +1,78 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include <QSortFilterProxyModel>
+
+
+#include "BaseVersionList.h"
+
+class QVBoxLayout;
+class QHBoxLayout;
+class QDialogButtonBox;
+class VersionSelectWidget;
+class QPushButton;
+
+namespace Ui
+{
+class VersionSelectDialog;
+}
+
+class VersionProxyModel;
+
+class VersionSelectDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent = 0, bool cancelable = true);
+ virtual ~VersionSelectDialog() {};
+
+ int exec() override;
+
+ BaseVersionPtr selectedVersion() const;
+
+ void setCurrentVersion(const QString & version);
+ void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setResizeOn(int column);
+
+private slots:
+ void on_refreshButton_clicked();
+
+private:
+ void retranslate();
+ void selectRecommended();
+
+private:
+ QString m_currentVersion;
+ VersionSelectWidget *m_versionWidget = nullptr;
+ QVBoxLayout *m_verticalLayout = nullptr;
+ QHBoxLayout *m_horizontalLayout = nullptr;
+ QPushButton *m_refreshButton = nullptr;
+ QDialogButtonBox *m_buttonBox = nullptr;
+
+ BaseVersionList *m_vlist = nullptr;
+
+ VersionProxyModel *m_proxyModel = nullptr;
+
+ int resizeOnColumn = -1;
+
+ Task * loadTask = nullptr;
+};
diff --git a/launcher/groupview/AccessibleGroupView.cpp b/launcher/groupview/AccessibleGroupView.cpp
new file mode 100644
index 00000000..c6541f18
--- /dev/null
+++ b/launcher/groupview/AccessibleGroupView.cpp
@@ -0,0 +1,778 @@
+#include "GroupView.h"
+#include "AccessibleGroupView.h"
+#include "AccessibleGroupView_p.h"
+
+#include <qvariant.h>
+#include <qaccessible.h>
+#include <qheaderview.h>
+
+#ifndef QT_NO_ACCESSIBILITY
+
+QAccessibleInterface *groupViewAccessibleFactory(const QString &classname, QObject *object)
+{
+ QAccessibleInterface *iface = 0;
+ if (!object || !object->isWidgetType())
+ return iface;
+
+ QWidget *widget = static_cast<QWidget*>(object);
+
+ if (classname == QLatin1String("GroupView")) {
+ iface = new AccessibleGroupView((GroupView *)widget);
+ }
+ return iface;
+}
+
+
+QAbstractItemView *AccessibleGroupView::view() const
+{
+ return qobject_cast<QAbstractItemView*>(object());
+}
+
+int AccessibleGroupView::logicalIndex(const QModelIndex &index) const
+{
+ if (!view()->model() || !index.isValid())
+ return -1;
+ return index.row() * (index.model()->columnCount()) + index.column();
+}
+
+AccessibleGroupView::AccessibleGroupView(QWidget *w)
+ : QAccessibleObject(w)
+{
+ Q_ASSERT(view());
+}
+
+bool AccessibleGroupView::isValid() const
+{
+ return view();
+}
+
+AccessibleGroupView::~AccessibleGroupView()
+{
+ for (QAccessible::Id id : childToId) {
+ QAccessible::deleteAccessibleInterface(id);
+ }
+}
+
+QAccessibleInterface *AccessibleGroupView::cellAt(int row, int column) const
+{
+ if (!view()->model()) {
+ return 0;
+ }
+
+ QModelIndex index = view()->model()->index(row, column, view()->rootIndex());
+ if (Q_UNLIKELY(!index.isValid())) {
+ qWarning() << "AccessibleGroupView::cellAt: invalid index: " << index << " for " << view();
+ return 0;
+ }
+
+ return child(logicalIndex(index));
+}
+
+QAccessibleInterface *AccessibleGroupView::caption() const
+{
+ return 0;
+}
+
+QString AccessibleGroupView::columnDescription(int column) const
+{
+ if (!view()->model())
+ return QString();
+
+ return view()->model()->headerData(column, Qt::Horizontal).toString();
+}
+
+int AccessibleGroupView::columnCount() const
+{
+ if (!view()->model())
+ return 0;
+ return 1;
+}
+
+int AccessibleGroupView::rowCount() const
+{
+ if (!view()->model())
+ return 0;
+ return view()->model()->rowCount();
+}
+
+int AccessibleGroupView::selectedCellCount() const
+{
+ if (!view()->selectionModel())
+ return 0;
+ return view()->selectionModel()->selectedIndexes().count();
+}
+
+int AccessibleGroupView::selectedColumnCount() const
+{
+ if (!view()->selectionModel())
+ return 0;
+ return view()->selectionModel()->selectedColumns().count();
+}
+
+int AccessibleGroupView::selectedRowCount() const
+{
+ if (!view()->selectionModel())
+ return 0;
+ return view()->selectionModel()->selectedRows().count();
+}
+
+QString AccessibleGroupView::rowDescription(int row) const
+{
+ if (!view()->model())
+ return QString();
+ return view()->model()->headerData(row, Qt::Vertical).toString();
+}
+
+QList<QAccessibleInterface *> AccessibleGroupView::selectedCells() const
+{
+ QList<QAccessibleInterface*> cells;
+ if (!view()->selectionModel())
+ return cells;
+ const QModelIndexList selectedIndexes = view()->selectionModel()->selectedIndexes();
+ cells.reserve(selectedIndexes.size());
+ for (const QModelIndex &index : selectedIndexes)
+ cells.append(child(logicalIndex(index)));
+ return cells;
+}
+
+QList<int> AccessibleGroupView::selectedColumns() const
+{
+ if (!view()->selectionModel()) {
+ return QList<int>();
+ }
+
+ const QModelIndexList selectedColumns = view()->selectionModel()->selectedColumns();
+
+ QList<int> columns;
+ columns.reserve(selectedColumns.size());
+ for (const QModelIndex &index : selectedColumns) {
+ columns.append(index.column());
+ }
+
+ return columns;
+}
+
+QList<int> AccessibleGroupView::selectedRows() const
+{
+ if (!view()->selectionModel()) {
+ return QList<int>();
+ }
+
+ QList<int> rows;
+
+ const QModelIndexList selectedRows = view()->selectionModel()->selectedRows();
+
+ rows.reserve(selectedRows.size());
+ for (const QModelIndex &index : selectedRows) {
+ rows.append(index.row());
+ }
+
+ return rows;
+}
+
+QAccessibleInterface *AccessibleGroupView::summary() const
+{
+ return 0;
+}
+
+bool AccessibleGroupView::isColumnSelected(int column) const
+{
+ if (!view()->selectionModel()) {
+ return false;
+ }
+
+ return view()->selectionModel()->isColumnSelected(column, QModelIndex());
+}
+
+bool AccessibleGroupView::isRowSelected(int row) const
+{
+ if (!view()->selectionModel()) {
+ return false;
+ }
+
+ return view()->selectionModel()->isRowSelected(row, QModelIndex());
+}
+
+bool AccessibleGroupView::selectRow(int row)
+{
+ if (!view()->model() || !view()->selectionModel()) {
+ return false;
+ }
+ QModelIndex index = view()->model()->index(row, 0, view()->rootIndex());
+
+ if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectColumns) {
+ return false;
+ }
+
+ switch (view()->selectionMode()) {
+ case QAbstractItemView::NoSelection: {
+ return false;
+ }
+ case QAbstractItemView::SingleSelection: {
+ if (view()->selectionBehavior() != QAbstractItemView::SelectRows && columnCount() > 1 )
+ return false;
+ view()->clearSelection();
+ break;
+ }
+ case QAbstractItemView::ContiguousSelection: {
+ if ((!row || !view()->selectionModel()->isRowSelected(row - 1, view()->rootIndex())) && !view()->selectionModel()->isRowSelected(row + 1, view()->rootIndex())) {
+ view()->clearSelection();
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
+ return true;
+}
+
+bool AccessibleGroupView::selectColumn(int column)
+{
+ if (!view()->model() || !view()->selectionModel()) {
+ return false;
+ }
+ QModelIndex index = view()->model()->index(0, column, view()->rootIndex());
+
+ if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectRows) {
+ return false;
+ }
+
+ switch (view()->selectionMode()) {
+ case QAbstractItemView::NoSelection: {
+ return false;
+ }
+ case QAbstractItemView::SingleSelection: {
+ if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) {
+ return false;
+ }
+ // fallthrough intentional
+ }
+ case QAbstractItemView::ContiguousSelection: {
+ if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
+ view()->clearSelection();
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Columns);
+ return true;
+}
+
+bool AccessibleGroupView::unselectRow(int row)
+{
+ if (!view()->model() || !view()->selectionModel()) {
+ return false;
+ }
+
+ QModelIndex index = view()->model()->index(row, 0, view()->rootIndex());
+ if (!index.isValid()) {
+ return false;
+ }
+
+ QItemSelection selection(index, index);
+ auto selectionModel = view()->selectionModel();
+
+ switch (view()->selectionMode()) {
+ case QAbstractItemView::SingleSelection:
+ // no unselect
+ if (selectedRowCount() == 1) {
+ return false;
+ }
+ break;
+ case QAbstractItemView::ContiguousSelection: {
+ // no unselect
+ if (selectedRowCount() == 1) {
+ return false;
+ }
+
+
+ if ((!row || selectionModel->isRowSelected(row - 1, view()->rootIndex())) && selectionModel->isRowSelected(row + 1, view()->rootIndex())) {
+ //If there are rows selected both up the current row and down the current rown,
+ //the ones which are down the current row will be deselected
+ selection = QItemSelection(index, view()->model()->index(rowCount() - 1, 0, view()->rootIndex()));
+ }
+ }
+ default: {
+ break;
+ }
+ }
+
+ selectionModel->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Rows);
+ return true;
+}
+
+bool AccessibleGroupView::unselectColumn(int column)
+{
+ auto model = view()->model();
+ if (!model || !view()->selectionModel()) {
+ return false;
+ }
+
+ QModelIndex index = model->index(0, column, view()->rootIndex());
+ if (!index.isValid()) {
+ return false;
+ }
+
+ QItemSelection selection(index, index);
+
+ switch (view()->selectionMode()) {
+ case QAbstractItemView::SingleSelection: {
+ //In SingleSelection and ContiguousSelection once an item
+ //is selected, there's no way for the user to unselect all items
+ if (selectedColumnCount() == 1) {
+ return false;
+ }
+ break;
+ }
+ case QAbstractItemView::ContiguousSelection:
+ if (selectedColumnCount() == 1) {
+ return false;
+ }
+
+ if ((!column || view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex()))
+ && view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) {
+ //If there are columns selected both at the left of the current row and at the right
+ //of the current row, the ones which are at the right will be deselected
+ selection = QItemSelection(index, model->index(0, columnCount() - 1, view()->rootIndex()));
+ }
+ default:
+ break;
+ }
+
+ view()->selectionModel()->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Columns);
+ return true;
+}
+
+QAccessible::Role AccessibleGroupView::role() const
+{
+ return QAccessible::List;
+}
+
+QAccessible::State AccessibleGroupView::state() const
+{
+ return QAccessible::State();
+}
+
+QAccessibleInterface *AccessibleGroupView::childAt(int x, int y) const
+{
+ QPoint viewportOffset = view()->viewport()->mapTo(view(), QPoint(0,0));
+ QPoint indexPosition = view()->mapFromGlobal(QPoint(x, y) - viewportOffset);
+ // FIXME: if indexPosition < 0 in one coordinate, return header
+
+ QModelIndex index = view()->indexAt(indexPosition);
+ if (index.isValid()) {
+ return child(logicalIndex(index));
+ }
+ return 0;
+}
+
+int AccessibleGroupView::childCount() const
+{
+ if (!view()->model()) {
+ return 0;
+ }
+ return (view()->model()->rowCount()) * (view()->model()->columnCount());
+}
+
+int AccessibleGroupView::indexOfChild(const QAccessibleInterface *iface) const
+{
+ if (!view()->model())
+ return -1;
+ QAccessibleInterface *parent = iface->parent();
+ if (parent->object() != view())
+ return -1;
+
+ Q_ASSERT(iface->role() != QAccessible::TreeItem); // should be handled by tree class
+ if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) {
+ const AccessibleGroupViewItem* cell = static_cast<const AccessibleGroupViewItem*>(iface);
+ return logicalIndex(cell->m_index);
+ } else if (iface->role() == QAccessible::Pane) {
+ return 0; // corner button
+ } else {
+ qWarning() << "AccessibleGroupView::indexOfChild has a child with unknown role..." << iface->role() << iface->text(QAccessible::Name);
+ }
+ // FIXME: we are in denial of our children. this should stop.
+ return -1;
+}
+
+QString AccessibleGroupView::text(QAccessible::Text t) const
+{
+ if (t == QAccessible::Description)
+ return view()->accessibleDescription();
+ return view()->accessibleName();
+}
+
+QRect AccessibleGroupView::rect() const
+{
+ if (!view()->isVisible())
+ return QRect();
+ QPoint pos = view()->mapToGlobal(QPoint(0, 0));
+ return QRect(pos.x(), pos.y(), view()->width(), view()->height());
+}
+
+QAccessibleInterface *AccessibleGroupView::parent() const
+{
+ if (view() && view()->parent()) {
+ if (qstrcmp("QComboBoxPrivateContainer", view()->parent()->metaObject()->className()) == 0) {
+ return QAccessible::queryAccessibleInterface(view()->parent()->parent());
+ }
+ return QAccessible::queryAccessibleInterface(view()->parent());
+ }
+ return 0;
+}
+
+QAccessibleInterface *AccessibleGroupView::child(int logicalIndex) const
+{
+ if (!view()->model())
+ return 0;
+
+ auto id = childToId.constFind(logicalIndex);
+ if (id != childToId.constEnd())
+ return QAccessible::accessibleInterface(id.value());
+
+ int columns = view()->model()->columnCount();
+
+ int row = logicalIndex / columns;
+ int column = logicalIndex % columns;
+
+ QAccessibleInterface *iface = 0;
+
+ QModelIndex index = view()->model()->index(row, column, view()->rootIndex());
+ if (Q_UNLIKELY(!index.isValid())) {
+ qWarning("AccessibleGroupView::child: Invalid index at: %d %d", row, column);
+ return 0;
+ }
+ iface = new AccessibleGroupViewItem(view(), index);
+
+ QAccessible::registerAccessibleInterface(iface);
+ childToId.insert(logicalIndex, QAccessible::uniqueId(iface));
+ return iface;
+}
+
+void *AccessibleGroupView::interface_cast(QAccessible::InterfaceType t)
+{
+ if (t == QAccessible::TableInterface)
+ return static_cast<QAccessibleTableInterface*>(this);
+ return 0;
+}
+
+void AccessibleGroupView::modelChange(QAccessibleTableModelChangeEvent *event)
+{
+ // if there is no cache yet, we don't update anything
+ if (childToId.isEmpty())
+ return;
+
+ switch (event->modelChangeType()) {
+ case QAccessibleTableModelChangeEvent::ModelReset:
+ for (QAccessible::Id id : childToId)
+ QAccessible::deleteAccessibleInterface(id);
+ childToId.clear();
+ break;
+
+ // rows are inserted: move every row after that
+ case QAccessibleTableModelChangeEvent::RowsInserted:
+ case QAccessibleTableModelChangeEvent::ColumnsInserted: {
+
+ ChildCache newCache;
+ ChildCache::ConstIterator iter = childToId.constBegin();
+
+ while (iter != childToId.constEnd()) {
+ QAccessible::Id id = iter.value();
+ QAccessibleInterface *iface = QAccessible::accessibleInterface(id);
+ Q_ASSERT(iface);
+ if (indexOfChild(iface) >= 0) {
+ newCache.insert(indexOfChild(iface), id);
+ } else {
+ // ### This should really not happen,
+ // but it might if the view has a root index set.
+ // This needs to be fixed.
+ QAccessible::deleteAccessibleInterface(id);
+ }
+ ++iter;
+ }
+ childToId = newCache;
+ break;
+ }
+
+ case QAccessibleTableModelChangeEvent::ColumnsRemoved:
+ case QAccessibleTableModelChangeEvent::RowsRemoved: {
+ ChildCache newCache;
+ ChildCache::ConstIterator iter = childToId.constBegin();
+ while (iter != childToId.constEnd()) {
+ QAccessible::Id id = iter.value();
+ QAccessibleInterface *iface = QAccessible::accessibleInterface(id);
+ Q_ASSERT(iface);
+ if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) {
+ Q_ASSERT(iface->tableCellInterface());
+ AccessibleGroupViewItem *cell = static_cast<AccessibleGroupViewItem*>(iface->tableCellInterface());
+ // Since it is a QPersistentModelIndex, we only need to check if it is valid
+ if (cell->m_index.isValid())
+ newCache.insert(indexOfChild(cell), id);
+ else
+ QAccessible::deleteAccessibleInterface(id);
+ }
+ ++iter;
+ }
+ childToId = newCache;
+ break;
+ }
+
+ case QAccessibleTableModelChangeEvent::DataChanged:
+ // nothing to do in this case
+ break;
+ }
+}
+
+// TABLE CELL
+
+AccessibleGroupViewItem::AccessibleGroupViewItem(QAbstractItemView *view_, const QModelIndex &index_)
+ : view(view_), m_index(index_)
+{
+ if (Q_UNLIKELY(!index_.isValid()))
+ qWarning() << "AccessibleGroupViewItem::AccessibleGroupViewItem with invalid index: " << index_;
+}
+
+void *AccessibleGroupViewItem::interface_cast(QAccessible::InterfaceType t)
+{
+ if (t == QAccessible::TableCellInterface)
+ return static_cast<QAccessibleTableCellInterface*>(this);
+ if (t == QAccessible::ActionInterface)
+ return static_cast<QAccessibleActionInterface*>(this);
+ return 0;
+}
+
+int AccessibleGroupViewItem::columnExtent() const { return 1; }
+int AccessibleGroupViewItem::rowExtent() const { return 1; }
+
+QList<QAccessibleInterface*> AccessibleGroupViewItem::rowHeaderCells() const
+{
+ return {};
+}
+
+QList<QAccessibleInterface*> AccessibleGroupViewItem::columnHeaderCells() const
+{
+ return {};
+}
+
+int AccessibleGroupViewItem::columnIndex() const
+{
+ if (!isValid()) {
+ return -1;
+ }
+
+ return m_index.column();
+}
+
+int AccessibleGroupViewItem::rowIndex() const
+{
+ if (!isValid()) {
+ return -1;
+ }
+
+ return m_index.row();
+}
+
+bool AccessibleGroupViewItem::isSelected() const
+{
+ if (!isValid()) {
+ return false;
+ }
+
+ return view->selectionModel()->isSelected(m_index);
+}
+
+QStringList AccessibleGroupViewItem::actionNames() const
+{
+ QStringList names;
+ names << toggleAction();
+ return names;
+}
+
+void AccessibleGroupViewItem::doAction(const QString& actionName)
+{
+ if (actionName == toggleAction()) {
+ if (isSelected()) {
+ unselectCell();
+ }
+ else {
+ selectCell();
+ }
+ }
+}
+
+QStringList AccessibleGroupViewItem::keyBindingsForAction(const QString &) const
+{
+ return QStringList();
+}
+
+
+void AccessibleGroupViewItem::selectCell()
+{
+ if (!isValid()) {
+ return;
+ }
+ QAbstractItemView::SelectionMode selectionMode = view->selectionMode();
+ if (selectionMode == QAbstractItemView::NoSelection) {
+ return;
+ }
+
+ Q_ASSERT(table());
+ QAccessibleTableInterface *cellTable = table()->tableInterface();
+
+ switch (view->selectionBehavior()) {
+ case QAbstractItemView::SelectItems:
+ break;
+ case QAbstractItemView::SelectColumns:
+ if (cellTable)
+ cellTable->selectColumn(m_index.column());
+ return;
+ case QAbstractItemView::SelectRows:
+ if (cellTable)
+ cellTable->selectRow(m_index.row());
+ return;
+ }
+
+ if (selectionMode == QAbstractItemView::SingleSelection) {
+ view->clearSelection();
+ }
+
+ view->selectionModel()->select(m_index, QItemSelectionModel::Select);
+}
+
+void AccessibleGroupViewItem::unselectCell()
+{
+ if (!isValid())
+ return;
+ QAbstractItemView::SelectionMode selectionMode = view->selectionMode();
+ if (selectionMode == QAbstractItemView::NoSelection)
+ return;
+
+ QAccessibleTableInterface *cellTable = table()->tableInterface();
+
+ switch (view->selectionBehavior()) {
+ case QAbstractItemView::SelectItems:
+ break;
+ case QAbstractItemView::SelectColumns:
+ if (cellTable)
+ cellTable->unselectColumn(m_index.column());
+ return;
+ case QAbstractItemView::SelectRows:
+ if (cellTable)
+ cellTable->unselectRow(m_index.row());
+ return;
+ }
+
+ //If the mode is not MultiSelection or ExtendedSelection and only
+ //one cell is selected it cannot be unselected by the user
+ if ((selectionMode != QAbstractItemView::MultiSelection) && (selectionMode != QAbstractItemView::ExtendedSelection) && (view->selectionModel()->selectedIndexes().count() <= 1))
+ return;
+
+ view->selectionModel()->select(m_index, QItemSelectionModel::Deselect);
+}
+
+QAccessibleInterface *AccessibleGroupViewItem::table() const
+{
+ return QAccessible::queryAccessibleInterface(view);
+}
+
+QAccessible::Role AccessibleGroupViewItem::role() const
+{
+ return QAccessible::ListItem;
+}
+
+QAccessible::State AccessibleGroupViewItem::state() const
+{
+ QAccessible::State st;
+ if (!isValid())
+ return st;
+
+ QRect globalRect = view->rect();
+ globalRect.translate(view->mapToGlobal(QPoint(0,0)));
+ if (!globalRect.intersects(rect()))
+ st.invisible = true;
+
+ if (view->selectionModel()->isSelected(m_index))
+ st.selected = true;
+ if (view->selectionModel()->currentIndex() == m_index)
+ st.focused = true;
+ if (m_index.model()->data(m_index, Qt::CheckStateRole).toInt() == Qt::Checked)
+ st.checked = true;
+
+ Qt::ItemFlags flags = m_index.flags();
+ if (flags & Qt::ItemIsSelectable) {
+ st.selectable = true;
+ st.focusable = true;
+ if (view->selectionMode() == QAbstractItemView::MultiSelection)
+ st.multiSelectable = true;
+ if (view->selectionMode() == QAbstractItemView::ExtendedSelection)
+ st.extSelectable = true;
+ }
+ return st;
+}
+
+
+QRect AccessibleGroupViewItem::rect() const
+{
+ QRect r;
+ if (!isValid())
+ return r;
+ r = view->visualRect(m_index);
+
+ if (!r.isNull()) {
+ r.translate(view->viewport()->mapTo(view, QPoint(0,0)));
+ r.translate(view->mapToGlobal(QPoint(0, 0)));
+ }
+ return r;
+}
+
+QString AccessibleGroupViewItem::text(QAccessible::Text t) const
+{
+ QString value;
+ if (!isValid())
+ return value;
+ QAbstractItemModel *model = view->model();
+ switch (t) {
+ case QAccessible::Name:
+ value = model->data(m_index, Qt::AccessibleTextRole).toString();
+ if (value.isEmpty())
+ value = model->data(m_index, Qt::DisplayRole).toString();
+ break;
+ case QAccessible::Description:
+ value = model->data(m_index, Qt::AccessibleDescriptionRole).toString();
+ break;
+ default:
+ break;
+ }
+ return value;
+}
+
+void AccessibleGroupViewItem::setText(QAccessible::Text /*t*/, const QString &text)
+{
+ if (!isValid() || !(m_index.flags() & Qt::ItemIsEditable))
+ return;
+ view->model()->setData(m_index, text);
+}
+
+bool AccessibleGroupViewItem::isValid() const
+{
+ return view && view->model() && m_index.isValid();
+}
+
+QAccessibleInterface *AccessibleGroupViewItem::parent() const
+{
+ return QAccessible::queryAccessibleInterface(view);
+}
+
+QAccessibleInterface *AccessibleGroupViewItem::child(int) const
+{
+ return 0;
+}
+
+#endif /* !QT_NO_ACCESSIBILITY */
diff --git a/launcher/groupview/AccessibleGroupView.h b/launcher/groupview/AccessibleGroupView.h
new file mode 100644
index 00000000..9bfd1745
--- /dev/null
+++ b/launcher/groupview/AccessibleGroupView.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <QString>
+class QAccessibleInterface;
+
+QAccessibleInterface *groupViewAccessibleFactory(const QString &classname, QObject *object);
diff --git a/launcher/groupview/AccessibleGroupView_p.h b/launcher/groupview/AccessibleGroupView_p.h
new file mode 100644
index 00000000..e74da3be
--- /dev/null
+++ b/launcher/groupview/AccessibleGroupView_p.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include "QtCore/qpointer.h"
+#include <QtGui/qaccessible.h>
+#include <QAccessibleWidget>
+#include <QAbstractItemView>
+#ifndef QT_NO_ACCESSIBILITY
+#include "GroupView.h"
+// #include <QHeaderView>
+
+class QAccessibleTableCell;
+class QAccessibleTableHeaderCell;
+
+class AccessibleGroupView :public QAccessibleTableInterface, public QAccessibleObject
+{
+public:
+ explicit AccessibleGroupView(QWidget *w);
+ bool isValid() const override;
+
+ QAccessible::Role role() const override;
+ QAccessible::State state() const override;
+ QString text(QAccessible::Text t) const override;
+ QRect rect() const override;
+
+ QAccessibleInterface *childAt(int x, int y) const override;
+ int childCount() const override;
+ int indexOfChild(const QAccessibleInterface *) const override;
+
+ QAccessibleInterface *parent() const override;
+ QAccessibleInterface *child(int index) const override;
+
+ void *interface_cast(QAccessible::InterfaceType t) override;
+
+ // table interface
+ QAccessibleInterface *cellAt(int row, int column) const override;
+ QAccessibleInterface *caption() const override;
+ QAccessibleInterface *summary() const override;
+ QString columnDescription(int column) const override;
+ QString rowDescription(int row) const override;
+ int columnCount() const override;
+ int rowCount() const override;
+
+ // selection
+ int selectedCellCount() const override;
+ int selectedColumnCount() const override;
+ int selectedRowCount() const override;
+ QList<QAccessibleInterface*> selectedCells() const override;
+ QList<int> selectedColumns() const override;
+ QList<int> selectedRows() const override;
+ bool isColumnSelected(int column) const override;
+ bool isRowSelected(int row) const override;
+ bool selectRow(int row) override;
+ bool selectColumn(int column) override;
+ bool unselectRow(int row) override;
+ bool unselectColumn(int column) override;
+
+ QAbstractItemView *view() const;
+
+ void modelChange(QAccessibleTableModelChangeEvent *event) override;
+
+protected:
+ // maybe vector
+ typedef QHash<int, QAccessible::Id> ChildCache;
+ mutable ChildCache childToId;
+
+ virtual ~AccessibleGroupView();
+
+private:
+ inline int logicalIndex(const QModelIndex &index) const;
+};
+
+class AccessibleGroupViewItem: public QAccessibleInterface, public QAccessibleTableCellInterface, public QAccessibleActionInterface
+{
+public:
+ AccessibleGroupViewItem(QAbstractItemView *view, const QModelIndex &m_index);
+
+ void *interface_cast(QAccessible::InterfaceType t) override;
+ QObject *object() const override { return nullptr; }
+ QAccessible::Role role() const override;
+ QAccessible::State state() const override;
+ QRect rect() const override;
+ bool isValid() const override;
+
+ QAccessibleInterface *childAt(int, int) const override { return nullptr; }
+ int childCount() const override { return 0; }
+ int indexOfChild(const QAccessibleInterface *) const override { return -1; }
+
+ QString text(QAccessible::Text t) const override;
+ void setText(QAccessible::Text t, const QString &text) override;
+
+ QAccessibleInterface *parent() const override;
+ QAccessibleInterface *child(int) const override;
+
+ // cell interface
+ int columnExtent() const override;
+ QList<QAccessibleInterface*> columnHeaderCells() const override;
+ int columnIndex() const override;
+ int rowExtent() const override;
+ QList<QAccessibleInterface*> rowHeaderCells() const override;
+ int rowIndex() const override;
+ bool isSelected() const override;
+ QAccessibleInterface* table() const override;
+
+ //action interface
+ QStringList actionNames() const override;
+ void doAction(const QString &actionName) override;
+ QStringList keyBindingsForAction(const QString &actionName) const override;
+
+private:
+ QPointer<QAbstractItemView > view;
+ QPersistentModelIndex m_index;
+
+ void selectCell();
+ void unselectCell();
+
+ friend class AccessibleGroupView;
+};
+#endif /* !QT_NO_ACCESSIBILITY */
diff --git a/launcher/groupview/GroupView.cpp b/launcher/groupview/GroupView.cpp
new file mode 100644
index 00000000..6bfc9381
--- /dev/null
+++ b/launcher/groupview/GroupView.cpp
@@ -0,0 +1,1020 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "GroupView.h"
+
+#include <QPainter>
+#include <QApplication>
+#include <QtMath>
+#include <QMouseEvent>
+#include <QListView>
+#include <QPersistentModelIndex>
+#include <QDrag>
+#include <QMimeData>
+#include <QCache>
+#include <QScrollBar>
+#include <QAccessible>
+
+#include "VisualGroup.h"
+#include <QDebug>
+
+template <typename T> bool listsIntersect(const QList<T> &l1, const QList<T> t2)
+{
+ for (auto &item : l1)
+ {
+ if (t2.contains(item))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+GroupView::GroupView(QWidget *parent)
+ : QAbstractItemView(parent)
+{
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ setAcceptDrops(true);
+ setAutoScroll(true);
+}
+
+GroupView::~GroupView()
+{
+ qDeleteAll(m_groups);
+ m_groups.clear();
+}
+
+void GroupView::setModel(QAbstractItemModel *model)
+{
+ QAbstractItemView::setModel(model);
+ connect(model, &QAbstractItemModel::modelReset, this, &GroupView::modelReset);
+ connect(model, &QAbstractItemModel::rowsRemoved, this, &GroupView::rowsRemoved);
+}
+
+void GroupView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
+ const QVector<int> &roles)
+{
+ scheduleDelayedItemsLayout();
+}
+void GroupView::rowsInserted(const QModelIndex &parent, int start, int end)
+{
+ scheduleDelayedItemsLayout();
+}
+
+void GroupView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
+{
+ scheduleDelayedItemsLayout();
+}
+
+void GroupView::modelReset()
+{
+ scheduleDelayedItemsLayout();
+}
+
+void GroupView::rowsRemoved()
+{
+ scheduleDelayedItemsLayout();
+}
+
+void GroupView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
+{
+ QAbstractItemView::currentChanged(current, previous);
+ // TODO: for accessibility support, implement+register a factory, steal QAccessibleTable from Qt and return an instance of it for GroupView.
+#ifndef QT_NO_ACCESSIBILITY
+ if (QAccessible::isActive() && current.isValid()) {
+ QAccessibleEvent event(this, QAccessible::Focus);
+ event.setChild(current.row());
+ QAccessible::updateAccessibility(&event);
+ }
+#endif /* !QT_NO_ACCESSIBILITY */
+}
+
+
+class LocaleString : public QString
+{
+public:
+ LocaleString(const char *s) : QString(s)
+ {
+ }
+ LocaleString(const QString &s) : QString(s)
+ {
+ }
+};
+
+inline bool operator<(const LocaleString &lhs, const LocaleString &rhs)
+{
+ return (QString::localeAwareCompare(lhs, rhs) < 0);
+}
+
+void GroupView::updateScrollbar()
+{
+ int previousScroll = verticalScrollBar()->value();
+ if (m_groups.isEmpty())
+ {
+ verticalScrollBar()->setRange(0, 0);
+ }
+ else
+ {
+ int totalHeight = 0;
+ // top margin
+ totalHeight += m_categoryMargin;
+ int itemScroll = 0;
+ for (auto category : m_groups)
+ {
+ category->m_verticalPosition = totalHeight;
+ totalHeight += category->totalHeight() + m_categoryMargin;
+ if(!itemScroll && category->totalHeight() != 0)
+ {
+ itemScroll = category->contentHeight() / category->numRows();
+ }
+ }
+ // do not divide by zero
+ if(itemScroll == 0)
+ itemScroll = 64;
+
+ totalHeight += m_bottomMargin;
+ verticalScrollBar()->setSingleStep ( itemScroll );
+ const int rowsPerPage = qMax ( viewport()->height() / itemScroll, 1 );
+ verticalScrollBar()->setPageStep ( rowsPerPage * itemScroll );
+
+ verticalScrollBar()->setRange(0, totalHeight - height());
+ }
+
+ verticalScrollBar()->setValue(qMin(previousScroll, verticalScrollBar()->maximum()));
+}
+
+void GroupView::updateGeometries()
+{
+ geometryCache.clear();
+
+ QMap<LocaleString, VisualGroup *> cats;
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ const QString groupName = model()->index(i, 0).data(GroupViewRoles::GroupRole).toString();
+ if (!cats.contains(groupName))
+ {
+ VisualGroup *old = this->category(groupName);
+ if (old)
+ {
+ auto cat = new VisualGroup(old);
+ cats.insert(groupName, cat);
+ cat->update();
+ }
+ else
+ {
+ auto cat = new VisualGroup(groupName, this);
+ if(fVisibility) {
+ cat->collapsed = fVisibility(groupName);
+ }
+ cats.insert(groupName, cat);
+ cat->update();
+ }
+ }
+ }
+
+ qDeleteAll(m_groups);
+ m_groups = cats.values();
+ updateScrollbar();
+ viewport()->update();
+}
+
+bool GroupView::isIndexHidden(const QModelIndex &index) const
+{
+ VisualGroup *cat = category(index);
+ if (cat)
+ {
+ return cat->collapsed;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+VisualGroup *GroupView::category(const QModelIndex &index) const
+{
+ return category(index.data(GroupViewRoles::GroupRole).toString());
+}
+
+VisualGroup *GroupView::category(const QString &cat) const
+{
+ for (auto group : m_groups)
+ {
+ if (group->text == cat)
+ {
+ return group;
+ }
+ }
+ return nullptr;
+}
+
+VisualGroup *GroupView::categoryAt(const QPoint &pos, VisualGroup::HitResults & result) const
+{
+ for (auto group : m_groups)
+ {
+ result = group->hitScan(pos);
+ if(result != VisualGroup::NoHit)
+ {
+ return group;
+ }
+ }
+ result = VisualGroup::NoHit;
+ return nullptr;
+}
+
+QString GroupView::groupNameAt(const QPoint &point)
+{
+ executeDelayedItemsLayout();
+
+ VisualGroup::HitResults hitresult;
+ auto group = categoryAt(point + offset(), hitresult);
+ if(group && (hitresult & (VisualGroup::HeaderHit | VisualGroup::BodyHit)))
+ {
+ return group->text;
+ }
+ return QString();
+}
+
+int GroupView::calculateItemsPerRow() const
+{
+ return qFloor((qreal)(contentWidth()) / (qreal)(itemWidth() + m_spacing));
+}
+
+int GroupView::contentWidth() const
+{
+ return width() - m_leftMargin - m_rightMargin;
+}
+
+int GroupView::itemWidth() const
+{
+ return m_itemWidth;
+}
+
+void GroupView::mousePressEvent(QMouseEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+
+ QPersistentModelIndex index = indexAt(visualPos);
+
+ m_pressedIndex = index;
+ m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex);
+ m_pressedPosition = geometryPos;
+
+ VisualGroup::HitResults hitresult;
+ m_pressedCategory = categoryAt(geometryPos, hitresult);
+ if (m_pressedCategory && hitresult & VisualGroup::CheckboxHit)
+ {
+ setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState);
+ event->accept();
+ return;
+ }
+
+ if (index.isValid() && (index.flags() & Qt::ItemIsEnabled))
+ {
+ if(index != currentIndex())
+ {
+ // FIXME: better!
+ m_currentCursorColumn = -1;
+ }
+ // we disable scrollTo for mouse press so the item doesn't change position
+ // when the user is interacting with it (ie. clicking on it)
+ bool autoScroll = hasAutoScroll();
+ setAutoScroll(false);
+ selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
+
+ setAutoScroll(autoScroll);
+ QRect rect(visualPos, visualPos);
+ setSelection(rect, QItemSelectionModel::ClearAndSelect);
+
+ // signal handlers may change the model
+ emit pressed(index);
+ }
+ else
+ {
+ // Forces a finalize() even if mouse is pressed, but not on a item
+ selectionModel()->select(QModelIndex(), QItemSelectionModel::Select);
+ }
+}
+
+void GroupView::mouseMoveEvent(QMouseEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QPoint topLeft;
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+
+ if (state() == ExpandingState || state() == CollapsingState)
+ {
+ return;
+ }
+
+ if (state() == DraggingState)
+ {
+ topLeft = m_pressedPosition - offset();
+ if ((topLeft - event->pos()).manhattanLength() > QApplication::startDragDistance())
+ {
+ m_pressedIndex = QModelIndex();
+ startDrag(model()->supportedDragActions());
+ setState(NoState);
+ stopAutoScroll();
+ }
+ return;
+ }
+
+ if (selectionMode() != SingleSelection)
+ {
+ topLeft = m_pressedPosition - offset();
+ }
+ else
+ {
+ topLeft = geometryPos;
+ }
+
+ if (m_pressedIndex.isValid() && (state() != DragSelectingState) &&
+ (event->buttons() != Qt::NoButton) && !selectedIndexes().isEmpty())
+ {
+ setState(DraggingState);
+ return;
+ }
+
+ if ((event->buttons() & Qt::LeftButton) && selectionModel())
+ {
+ setState(DragSelectingState);
+
+ setSelection(QRect(visualPos, visualPos), QItemSelectionModel::ClearAndSelect);
+ QModelIndex index = indexAt(visualPos);
+
+ // set at the end because it might scroll the view
+ if (index.isValid() && (index != selectionModel()->currentIndex()) &&
+ (index.flags() & Qt::ItemIsEnabled))
+ {
+ selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
+ }
+ }
+}
+
+void GroupView::mouseReleaseEvent(QMouseEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QPoint visualPos = event->pos();
+ QPoint geometryPos = event->pos() + offset();
+ QPersistentModelIndex index = indexAt(visualPos);
+
+ VisualGroup::HitResults hitresult;
+
+ bool click = (index == m_pressedIndex && index.isValid()) ||
+ (m_pressedCategory && m_pressedCategory == categoryAt(geometryPos, hitresult));
+
+ if (click && m_pressedCategory)
+ {
+ if (state() == ExpandingState)
+ {
+ m_pressedCategory->collapsed = false;
+ emit groupStateChanged(m_pressedCategory->text, false);
+
+ updateGeometries();
+ viewport()->update();
+ event->accept();
+ m_pressedCategory = nullptr;
+ setState(NoState);
+ return;
+ }
+ else if (state() == CollapsingState)
+ {
+ m_pressedCategory->collapsed = true;
+ emit groupStateChanged(m_pressedCategory->text, true);
+
+ updateGeometries();
+ viewport()->update();
+ event->accept();
+ m_pressedCategory = nullptr;
+ setState(NoState);
+ return;
+ }
+ }
+
+ m_ctrlDragSelectionFlag = QItemSelectionModel::NoUpdate;
+
+ setState(NoState);
+
+ if (click)
+ {
+ if (event->button() == Qt::LeftButton)
+ {
+ emit clicked(index);
+ }
+ QStyleOptionViewItem option = viewOptions();
+ if (m_pressedAlreadySelected)
+ {
+ option.state |= QStyle::State_Selected;
+ }
+ if ((model()->flags(index) & Qt::ItemIsEnabled) &&
+ style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this))
+ {
+ emit activated(index);
+ }
+ }
+}
+
+void GroupView::mouseDoubleClickEvent(QMouseEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QModelIndex index = indexAt(event->pos());
+ if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index))
+ {
+ QMouseEvent me(QEvent::MouseButtonPress, event->localPos(), event->windowPos(),
+ event->screenPos(), event->button(), event->buttons(),
+ event->modifiers());
+ mousePressEvent(&me);
+ return;
+ }
+ // signal handlers may change the model
+ QPersistentModelIndex persistent = index;
+ emit doubleClicked(persistent);
+
+ QStyleOptionViewItem option = viewOptions();
+ if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this))
+ {
+ emit activated(index);
+ }
+}
+
+void GroupView::paintEvent(QPaintEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ QPainter painter(this->viewport());
+
+ QStyleOptionViewItem option(viewOptions());
+ option.widget = this;
+
+ int wpWidth = viewport()->width();
+ option.rect.setWidth(wpWidth);
+ for (int i = 0; i < m_groups.size(); ++i)
+ {
+ VisualGroup *category = m_groups.at(i);
+ int y = category->verticalPosition();
+ y -= verticalOffset();
+ QRect backup = option.rect;
+ int height = category->totalHeight();
+ option.rect.setTop(y);
+ option.rect.setHeight(height);
+ option.rect.setLeft(m_leftMargin);
+ option.rect.setRight(wpWidth - m_rightMargin);
+ category->drawHeader(&painter, option);
+ y += category->totalHeight() + m_categoryMargin;
+ option.rect = backup;
+ }
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ const QModelIndex index = model()->index(i, 0);
+ if (isIndexHidden(index))
+ {
+ continue;
+ }
+ Qt::ItemFlags flags = index.flags();
+ option.rect = visualRect(index);
+ option.features |= QStyleOptionViewItem::WrapText;
+ if (flags & Qt::ItemIsSelectable && selectionModel()->isSelected(index))
+ {
+ option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected
+ : QStyle::State_None;
+ }
+ else
+ {
+ option.state &= ~QStyle::State_Selected;
+ }
+ option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None;
+ if (!(flags & Qt::ItemIsEnabled))
+ {
+ option.state &= ~QStyle::State_Enabled;
+ }
+ itemDelegate()->paint(&painter, option, index);
+ }
+
+ /*
+ * Drop indicators for manual reordering...
+ */
+#if 0
+ if (!m_lastDragPosition.isNull())
+ {
+ QPair<Group *, int> pair = rowDropPos(m_lastDragPosition);
+ Group *category = pair.first;
+ int row = pair.second;
+ if (category)
+ {
+ int internalRow = row - category->firstItemIndex;
+ QLine line;
+ if (internalRow >= category->numItems())
+ {
+ QRect toTheRightOfRect = visualRect(category->lastItem());
+ line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight());
+ }
+ else
+ {
+ QRect toTheLeftOfRect = visualRect(model()->index(row, 0));
+ line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft());
+ }
+ painter.save();
+ painter.setPen(QPen(Qt::black, 3));
+ painter.drawLine(line);
+ painter.restore();
+ }
+ }
+#endif
+}
+
+void GroupView::resizeEvent(QResizeEvent *event)
+{
+ int newItemsPerRow = calculateItemsPerRow();
+ if(newItemsPerRow != m_currentItemsPerRow)
+ {
+ m_currentCursorColumn = -1;
+ m_currentItemsPerRow = newItemsPerRow;
+ updateGeometries();
+ }
+ else
+ {
+ updateScrollbar();
+ }
+}
+
+void GroupView::dragEnterEvent(QDragEnterEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ if (!isDragEventAccepted(event))
+ {
+ return;
+ }
+ m_lastDragPosition = event->pos() + offset();
+ viewport()->update();
+ event->accept();
+}
+
+void GroupView::dragMoveEvent(QDragMoveEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ if (!isDragEventAccepted(event))
+ {
+ return;
+ }
+ m_lastDragPosition = event->pos() + offset();
+ viewport()->update();
+ event->accept();
+}
+
+void GroupView::dragLeaveEvent(QDragLeaveEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ m_lastDragPosition = QPoint();
+ viewport()->update();
+}
+
+void GroupView::dropEvent(QDropEvent *event)
+{
+ executeDelayedItemsLayout();
+
+ m_lastDragPosition = QPoint();
+
+ stopAutoScroll();
+ setState(NoState);
+
+ if (event->source() == this)
+ {
+ if(event->possibleActions() & Qt::MoveAction)
+ {
+ QPair<VisualGroup *, int> dropPos = rowDropPos(event->pos() + offset());
+ const VisualGroup *category = dropPos.first;
+ const int row = dropPos.second;
+
+ if (row == -1)
+ {
+ viewport()->update();
+ return;
+ }
+
+ const QString categoryText = category->text;
+ if (model()->dropMimeData(event->mimeData(), Qt::MoveAction, row, 0, QModelIndex()))
+ {
+ model()->setData(model()->index(row, 0), categoryText, GroupViewRoles::GroupRole);
+ event->setDropAction(Qt::MoveAction);
+ event->accept();
+ }
+ updateGeometries();
+ viewport()->update();
+ }
+ }
+ auto mimedata = event->mimeData();
+
+ // check if the action is supported
+ if (!mimedata)
+ {
+ return;
+ }
+
+ // files dropped from outside?
+ if (mimedata->hasUrls())
+ {
+ auto urls = mimedata->urls();
+ event->accept();
+ emit droppedURLs(urls);
+ }
+}
+
+void GroupView::startDrag(Qt::DropActions supportedActions)
+{
+ executeDelayedItemsLayout();
+
+ QModelIndexList indexes = selectionModel()->selectedIndexes();
+ if(indexes.count() == 0)
+ return;
+
+ QMimeData *data = model()->mimeData(indexes);
+ if (!data)
+ {
+ return;
+ }
+ QRect rect;
+ QPixmap pixmap = renderToPixmap(indexes, &rect);
+ //rect.translate(offset());
+ // rect.adjust(horizontalOffset(), verticalOffset(), 0, 0);
+ QDrag *drag = new QDrag(this);
+ drag->setPixmap(pixmap);
+ drag->setMimeData(data);
+ Qt::DropAction defaultDropAction = Qt::IgnoreAction;
+ if (this->defaultDropAction() != Qt::IgnoreAction &&
+ (supportedActions & this->defaultDropAction()))
+ {
+ defaultDropAction = this->defaultDropAction();
+ }
+ if (drag->exec(supportedActions, defaultDropAction) == Qt::MoveAction)
+ {
+ const QItemSelection selection = selectionModel()->selection();
+
+ for (auto it = selection.constBegin(); it != selection.constEnd(); ++it)
+ {
+ QModelIndex parent = (*it).parent();
+ if ((*it).left() != 0)
+ {
+ continue;
+ }
+ if ((*it).right() != (model()->columnCount(parent) - 1))
+ {
+ continue;
+ }
+ int count = (*it).bottom() - (*it).top() + 1;
+ model()->removeRows((*it).top(), count, parent);
+ }
+ }
+}
+
+QRect GroupView::visualRect(const QModelIndex &index) const
+{
+ const_cast<GroupView*>(this)->executeDelayedItemsLayout();
+
+ return geometryRect(index).translated(-offset());
+}
+
+QRect GroupView::geometryRect(const QModelIndex &index) const
+{
+ const_cast<GroupView*>(this)->executeDelayedItemsLayout();
+
+ if (!index.isValid() || isIndexHidden(index) || index.column() > 0)
+ {
+ return QRect();
+ }
+
+ int row = index.row();
+ if(geometryCache.contains(row))
+ {
+ return *geometryCache[row];
+ }
+
+ const VisualGroup *cat = category(index);
+ QPair<int, int> pos = cat->positionOf(index);
+ int x = pos.first;
+ // int y = pos.second;
+
+ QRect out;
+ out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index));
+ out.setLeft(m_spacing + x * (itemWidth() + m_spacing));
+ out.setSize(itemDelegate()->sizeHint(viewOptions(), index));
+ geometryCache.insert(row, new QRect(out));
+ return out;
+}
+
+QModelIndex GroupView::indexAt(const QPoint &point) const
+{
+ const_cast<GroupView*>(this)->executeDelayedItemsLayout();
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ QModelIndex index = model()->index(i, 0);
+ if (visualRect(index).contains(point))
+ {
+ return index;
+ }
+ }
+ return QModelIndex();
+}
+
+void GroupView::setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands)
+{
+ executeDelayedItemsLayout();
+
+ for (int i = 0; i < model()->rowCount(); ++i)
+ {
+ QModelIndex index = model()->index(i, 0);
+ QRect itemRect = visualRect(index);
+ if (itemRect.intersects(rect))
+ {
+ selectionModel()->select(index, commands);
+ update(itemRect.translated(-offset()));
+ }
+ }
+}
+
+QPixmap GroupView::renderToPixmap(const QModelIndexList &indices, QRect *r) const
+{
+ Q_ASSERT(r);
+ auto paintPairs = draggablePaintPairs(indices, r);
+ if (paintPairs.isEmpty())
+ {
+ return QPixmap();
+ }
+ QPixmap pixmap(r->size());
+ pixmap.fill(Qt::transparent);
+ QPainter painter(&pixmap);
+ QStyleOptionViewItem option = viewOptions();
+ option.state |= QStyle::State_Selected;
+ for (int j = 0; j < paintPairs.count(); ++j)
+ {
+ option.rect = paintPairs.at(j).first.translated(-r->topLeft());
+ const QModelIndex &current = paintPairs.at(j).second;
+ itemDelegate()->paint(&painter, option, current);
+ }
+ return pixmap;
+}
+
+QList<QPair<QRect, QModelIndex>> GroupView::draggablePaintPairs(const QModelIndexList &indices, QRect *r) const
+{
+ Q_ASSERT(r);
+ QRect &rect = *r;
+ QList<QPair<QRect, QModelIndex>> ret;
+ for (int i = 0; i < indices.count(); ++i)
+ {
+ const QModelIndex &index = indices.at(i);
+ const QRect current = geometryRect(index);
+ ret += qMakePair(current, index);
+ rect |= current;
+ }
+ return ret;
+}
+
+bool GroupView::isDragEventAccepted(QDropEvent *event)
+{
+ return true;
+}
+
+QPair<VisualGroup *, int> GroupView::rowDropPos(const QPoint &pos)
+{
+ return qMakePair<VisualGroup*, int>(nullptr, -1);
+}
+
+QPoint GroupView::offset() const
+{
+ return QPoint(horizontalOffset(), verticalOffset());
+}
+
+QRegion GroupView::visualRegionForSelection(const QItemSelection &selection) const
+{
+ QRegion region;
+ for (auto &range : selection)
+ {
+ int start_row = range.top();
+ int end_row = range.bottom();
+ for (int row = start_row; row <= end_row; ++row)
+ {
+ int start_column = range.left();
+ int end_column = range.right();
+ for (int column = start_column; column <= end_column; ++column)
+ {
+ QModelIndex index = model()->index(row, column, rootIndex());
+ region += visualRect(index); // OK
+ }
+ }
+ }
+ return region;
+}
+
+QModelIndex GroupView::moveCursor(QAbstractItemView::CursorAction cursorAction,
+ Qt::KeyboardModifiers modifiers)
+{
+ auto current = currentIndex();
+ if(!current.isValid())
+ {
+ return current;
+ }
+ auto cat = category(current);
+ int group_index = m_groups.indexOf(cat);
+ if(group_index < 0)
+ return current;
+
+ auto real_group = m_groups[group_index];
+ int beginning_row = 0;
+ for(auto group: m_groups)
+ {
+ if(group == real_group)
+ break;
+ beginning_row += group->numRows();
+ }
+
+ QPair<int, int> pos = cat->positionOf(current);
+ int column = pos.first;
+ int row = pos.second;
+ if(m_currentCursorColumn < 0)
+ {
+ m_currentCursorColumn = column;
+ }
+ switch(cursorAction)
+ {
+ case MoveUp:
+ {
+ if(row == 0)
+ {
+ int prevgroupindex = group_index-1;
+ while(prevgroupindex >= 0)
+ {
+ auto prevgroup = m_groups[prevgroupindex];
+ if(prevgroup->collapsed)
+ {
+ prevgroupindex--;
+ continue;
+ }
+ int newRow = prevgroup->numRows() - 1;
+ int newRowSize = prevgroup->rows[newRow].size();
+ int newColumn = m_currentCursorColumn;
+ if (m_currentCursorColumn >= newRowSize)
+ {
+ newColumn = newRowSize - 1;
+ }
+ return prevgroup->rows[newRow][newColumn];
+ }
+ }
+ else
+ {
+ int newRow = row - 1;
+ int newRowSize = cat->rows[newRow].size();
+ int newColumn = m_currentCursorColumn;
+ if (m_currentCursorColumn >= newRowSize)
+ {
+ newColumn = newRowSize - 1;
+ }
+ return cat->rows[newRow][newColumn];
+ }
+ return current;
+ }
+ case MoveDown:
+ {
+ if(row == cat->rows.size() - 1)
+ {
+ int nextgroupindex = group_index+1;
+ while (nextgroupindex < m_groups.size())
+ {
+ auto nextgroup = m_groups[nextgroupindex];
+ if(nextgroup->collapsed)
+ {
+ nextgroupindex++;
+ continue;
+ }
+ int newRowSize = nextgroup->rows[0].size();
+ int newColumn = m_currentCursorColumn;
+ if (m_currentCursorColumn >= newRowSize)
+ {
+ newColumn = newRowSize - 1;
+ }
+ return nextgroup->rows[0][newColumn];
+ }
+ }
+ else
+ {
+ int newRow = row + 1;
+ int newRowSize = cat->rows[newRow].size();
+ int newColumn = m_currentCursorColumn;
+ if (m_currentCursorColumn >= newRowSize)
+ {
+ newColumn = newRowSize - 1;
+ }
+ return cat->rows[newRow][newColumn];
+ }
+ return current;
+ }
+ case MoveLeft:
+ {
+ if(column > 0)
+ {
+ m_currentCursorColumn = column - 1;
+ return cat->rows[row][column - 1];
+ }
+ // TODO: moving to previous line
+ return current;
+ }
+ case MoveRight:
+ {
+ if(column < cat->rows[row].size() - 1)
+ {
+ m_currentCursorColumn = column + 1;
+ return cat->rows[row][column + 1];
+ }
+ // TODO: moving to next line
+ return current;
+ }
+ case MoveHome:
+ {
+ m_currentCursorColumn = 0;
+ return cat->rows[row][0];
+ }
+ case MoveEnd:
+ {
+ auto last = cat->rows[row].size() - 1;
+ m_currentCursorColumn = last;
+ return cat->rows[row][last];
+ }
+ default:
+ break;
+ }
+ return current;
+}
+
+int GroupView::horizontalOffset() const
+{
+ return horizontalScrollBar()->value();
+}
+
+int GroupView::verticalOffset() const
+{
+ return verticalScrollBar()->value();
+}
+
+void GroupView::scrollContentsBy(int dx, int dy)
+{
+ scrollDirtyRegion(dx, dy);
+ viewport()->scroll(dx, dy);
+}
+
+void GroupView::scrollTo(const QModelIndex &index, ScrollHint hint)
+{
+ if (!index.isValid())
+ return;
+
+ const QRect rect = visualRect(index);
+ if (hint == EnsureVisible && viewport()->rect().contains(rect))
+ {
+ viewport()->update(rect);
+ return;
+ }
+
+ verticalScrollBar()->setValue(verticalScrollToValue(index, rect, hint));
+}
+
+int GroupView::verticalScrollToValue(const QModelIndex &index, const QRect &rect,
+ QListView::ScrollHint hint) const
+{
+ const QRect area = viewport()->rect();
+ const bool above = (hint == QListView::EnsureVisible && rect.top() < area.top());
+ const bool below = (hint == QListView::EnsureVisible && rect.bottom() > area.bottom());
+
+ int verticalValue = verticalScrollBar()->value();
+ QRect adjusted = rect.adjusted(-spacing(), -spacing(), spacing(), spacing());
+ if (hint == QListView::PositionAtTop || above)
+ verticalValue += adjusted.top();
+ else if (hint == QListView::PositionAtBottom || below)
+ verticalValue += qMin(adjusted.top(), adjusted.bottom() - area.height() + 1);
+ else if (hint == QListView::PositionAtCenter)
+ verticalValue += adjusted.top() - ((area.height() - adjusted.height()) / 2);
+ return verticalValue;
+}
diff --git a/launcher/groupview/GroupView.h b/launcher/groupview/GroupView.h
new file mode 100644
index 00000000..cc5a58aa
--- /dev/null
+++ b/launcher/groupview/GroupView.h
@@ -0,0 +1,157 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QListView>
+#include <QLineEdit>
+#include <QScrollBar>
+#include <QCache>
+#include "VisualGroup.h"
+#include <functional>
+
+struct GroupViewRoles
+{
+ enum
+ {
+ GroupRole = Qt::UserRole,
+ ProgressValueRole,
+ ProgressMaximumRole
+ };
+};
+
+class GroupView : public QAbstractItemView
+{
+ Q_OBJECT
+
+public:
+ GroupView(QWidget *parent = 0);
+ ~GroupView();
+
+ void setModel(QAbstractItemModel *model) override;
+
+ using visibilityFunction = std::function<bool(const QString &)>;
+ void setSourceOfGroupCollapseStatus(visibilityFunction f) {
+ fVisibility = f;
+ }
+
+ /// return geometry rectangle occupied by the specified model item
+ QRect geometryRect(const QModelIndex &index) const;
+ /// return visual rectangle occupied by the specified model item
+ virtual QRect visualRect(const QModelIndex &index) const override;
+ /// get the model index at the specified visual point
+ virtual QModelIndex indexAt(const QPoint &point) const override;
+ QString groupNameAt(const QPoint &point);
+ void setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands) override;
+
+ virtual int horizontalOffset() const override;
+ virtual int verticalOffset() const override;
+ virtual void scrollContentsBy(int dx, int dy) override;
+ virtual void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible) override;
+
+ virtual QModelIndex moveCursor(CursorAction cursorAction,
+ Qt::KeyboardModifiers modifiers) override;
+
+ virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override;
+
+ int spacing() const
+ {
+ return m_spacing;
+ };
+
+public slots:
+ virtual void updateGeometries() override;
+
+protected slots:
+ virtual void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
+ const QVector<int> &roles) override;
+ virtual void rowsInserted(const QModelIndex &parent, int start, int end) override;
+ virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) override;
+ void modelReset();
+ void rowsRemoved();
+ void currentChanged(const QModelIndex &current, const QModelIndex &previous) override;
+
+signals:
+ void droppedURLs(QList<QUrl> urls);
+ void groupStateChanged(QString group, bool collapsed);
+
+protected:
+ virtual bool isIndexHidden(const QModelIndex &index) const override;
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+ void mouseReleaseEvent(QMouseEvent *event) override;
+ void mouseDoubleClickEvent(QMouseEvent *event) override;
+ void paintEvent(QPaintEvent *event) override;
+ void resizeEvent(QResizeEvent *event) override;
+
+ void dragEnterEvent(QDragEnterEvent *event) override;
+ void dragMoveEvent(QDragMoveEvent *event) override;
+ void dragLeaveEvent(QDragLeaveEvent *event) override;
+ void dropEvent(QDropEvent *event) override;
+
+ void startDrag(Qt::DropActions supportedActions) override;
+
+ void updateScrollbar();
+
+private:
+ friend struct VisualGroup;
+ QList<VisualGroup *> m_groups;
+
+ visibilityFunction fVisibility;
+
+ // geometry
+ int m_leftMargin = 5;
+ int m_rightMargin = 5;
+ int m_bottomMargin = 5;
+ int m_categoryMargin = 5;
+ int m_spacing = 5;
+ int m_itemWidth = 100;
+ int m_currentItemsPerRow = -1;
+ int m_currentCursorColumn= -1;
+ mutable QCache<int, QRect> geometryCache;
+
+ // point where the currently active mouse action started in geometry coordinates
+ QPoint m_pressedPosition;
+ QPersistentModelIndex m_pressedIndex;
+ bool m_pressedAlreadySelected;
+ VisualGroup *m_pressedCategory;
+ QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag;
+ QPoint m_lastDragPosition;
+
+ VisualGroup *category(const QModelIndex &index) const;
+ VisualGroup *category(const QString &cat) const;
+ VisualGroup *categoryAt(const QPoint &pos, VisualGroup::HitResults & result) const;
+
+ int itemsPerRow() const
+ {
+ return m_currentItemsPerRow;
+ };
+ int contentWidth() const;
+
+private: /* methods */
+ int itemWidth() const;
+ int calculateItemsPerRow() const;
+ int verticalScrollToValue(const QModelIndex &index, const QRect &rect,
+ QListView::ScrollHint hint) const;
+ QPixmap renderToPixmap(const QModelIndexList &indices, QRect *r) const;
+ QList<QPair<QRect, QModelIndex>> draggablePaintPairs(const QModelIndexList &indices,
+ QRect *r) const;
+
+ bool isDragEventAccepted(QDropEvent *event);
+
+ QPair<VisualGroup *, int> rowDropPos(const QPoint &pos);
+
+ QPoint offset() const;
+};
diff --git a/launcher/groupview/GroupedProxyModel.cpp b/launcher/groupview/GroupedProxyModel.cpp
new file mode 100644
index 00000000..dc4212d5
--- /dev/null
+++ b/launcher/groupview/GroupedProxyModel.cpp
@@ -0,0 +1,48 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "GroupedProxyModel.h"
+
+#include "GroupView.h"
+#include <QDebug>
+
+GroupedProxyModel::GroupedProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+}
+
+bool GroupedProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ const QString leftCategory = left.data(GroupViewRoles::GroupRole).toString();
+ const QString rightCategory = right.data(GroupViewRoles::GroupRole).toString();
+ if (leftCategory == rightCategory)
+ {
+ return subSortLessThan(left, right);
+ }
+ else
+ {
+ // FIXME: real group sorting happens in GroupView::updateGeometries(), see LocaleString
+ auto result = leftCategory.localeAwareCompare(rightCategory);
+ if(result == 0)
+ {
+ return subSortLessThan(left, right);
+ }
+ return result < 0;
+ }
+}
+
+bool GroupedProxyModel::subSortLessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ return left.row() < right.row();
+}
diff --git a/launcher/groupview/GroupedProxyModel.h b/launcher/groupview/GroupedProxyModel.h
new file mode 100644
index 00000000..fabf11c1
--- /dev/null
+++ b/launcher/groupview/GroupedProxyModel.h
@@ -0,0 +1,30 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QSortFilterProxyModel>
+
+class GroupedProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ GroupedProxyModel(QObject *parent = 0);
+
+protected:
+ virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+ virtual bool subSortLessThan(const QModelIndex &left, const QModelIndex &right) const;
+};
diff --git a/launcher/groupview/InstanceDelegate.cpp b/launcher/groupview/InstanceDelegate.cpp
new file mode 100644
index 00000000..fc959565
--- /dev/null
+++ b/launcher/groupview/InstanceDelegate.cpp
@@ -0,0 +1,428 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "InstanceDelegate.h"
+#include <QPainter>
+#include <QTextOption>
+#include <QTextLayout>
+#include <QApplication>
+#include <QtMath>
+#include <QDebug>
+
+#include "GroupView.h"
+#include "BaseInstance.h"
+#include "InstanceList.h"
+#include <xdgicon.h>
+#include <QTextEdit>
+
+// Origin: Qt
+static void viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed)
+{
+ height = 0;
+ widthUsed = 0;
+ textLayout.beginLayout();
+ QString str = textLayout.text();
+ while (true)
+ {
+ QTextLine line = textLayout.createLine();
+ if (!line.isValid())
+ break;
+ if (line.textLength() == 0)
+ break;
+ line.setLineWidth(lineWidth);
+ line.setPosition(QPointF(0, height));
+ height += line.height();
+ widthUsed = qMax(widthUsed, line.naturalTextWidth());
+ }
+ textLayout.endLayout();
+}
+
+ListViewDelegate::ListViewDelegate(QObject *parent) : QStyledItemDelegate(parent)
+{
+}
+
+void drawSelectionRect(QPainter *painter, const QStyleOptionViewItem &option,
+ const QRect &rect)
+{
+ if ((option.state & QStyle::State_Selected))
+ painter->fillRect(rect, option.palette.brush(QPalette::Highlight));
+ else
+ {
+ QColor backgroundColor = option.palette.color(QPalette::Background);
+ backgroundColor.setAlpha(160);
+ painter->fillRect(rect, QBrush(backgroundColor));
+ }
+}
+
+void drawFocusRect(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect)
+{
+ if (!(option.state & QStyle::State_HasFocus))
+ return;
+ QStyleOptionFocusRect opt;
+ opt.direction = option.direction;
+ opt.fontMetrics = option.fontMetrics;
+ opt.palette = option.palette;
+ opt.rect = rect;
+ // opt.state = option.state | QStyle::State_KeyboardFocusChange |
+ // QStyle::State_Item;
+ auto col = option.state & QStyle::State_Selected ? QPalette::Highlight : QPalette::Base;
+ opt.backgroundColor = option.palette.color(col);
+ // Apparently some widget styles expect this hint to not be set
+ painter->setRenderHint(QPainter::Antialiasing, false);
+
+ QStyle *style = option.widget ? option.widget->style() : QApplication::style();
+
+ style->drawPrimitive(QStyle::PE_FrameFocusRect, &opt, painter, option.widget);
+
+ painter->setRenderHint(QPainter::Antialiasing);
+}
+
+// TODO this can be made a lot prettier
+void drawProgressOverlay(QPainter *painter, const QStyleOptionViewItem &option,
+ const int value, const int maximum)
+{
+ if (maximum == 0 || value == maximum)
+ {
+ return;
+ }
+
+ painter->save();
+
+ qreal percent = (qreal)value / (qreal)maximum;
+ QColor color = option.palette.color(QPalette::Dark);
+ color.setAlphaF(0.70f);
+ painter->setBrush(color);
+ painter->setPen(QPen(QBrush(), 0));
+ painter->drawPie(option.rect, 90 * 16, -percent * 360 * 16);
+
+ painter->restore();
+}
+
+void drawBadges(QPainter *painter, const QStyleOptionViewItem &option, BaseInstance *instance, QIcon::Mode mode, QIcon::State state)
+{
+ QList<QString> pixmaps;
+ if (instance->isRunning())
+ {
+ pixmaps.append("status-running");
+ }
+ else if (instance->hasCrashed() || instance->hasVersionBroken())
+ {
+ pixmaps.append("status-bad");
+ }
+ if (instance->hasUpdateAvailable())
+ {
+ pixmaps.append("checkupdate");
+ }
+
+ static const int itemSide = 24;
+ static const int spacing = 1;
+ const int itemsPerRow = qMax(1, qFloor(double(option.rect.width() + spacing) / double(itemSide + spacing)));
+ const int rows = qCeil((double)pixmaps.size() / (double)itemsPerRow);
+ QListIterator<QString> it(pixmaps);
+ painter->translate(option.rect.topLeft());
+ for (int y = 0; y < rows; ++y)
+ {
+ for (int x = 0; x < itemsPerRow; ++x)
+ {
+ if (!it.hasNext())
+ {
+ return;
+ }
+ // FIXME: inject this.
+ auto icon = XdgIcon::fromTheme(it.next());
+ // opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state);
+ const QPixmap pixmap;
+ // itemSide
+ QRect badgeRect(
+ option.rect.width() - x * itemSide + qMax(x - 1, 0) * spacing - itemSide,
+ y * itemSide + qMax(y - 1, 0) * spacing,
+ itemSide,
+ itemSide
+ );
+ icon.paint(painter, badgeRect, Qt::AlignCenter, mode, state);
+ }
+ }
+ painter->translate(-option.rect.topLeft());
+}
+
+static QSize viewItemTextSize(const QStyleOptionViewItem *option)
+{
+ QStyle *style = option->widget ? option->widget->style() : QApplication::style();
+ QTextOption textOption;
+ textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ QTextLayout textLayout;
+ textLayout.setTextOption(textOption);
+ textLayout.setFont(option->font);
+ textLayout.setText(option->text);
+ const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, option, option->widget) + 1;
+ QRect bounds(0, 0, 100 - 2 * textMargin, 600);
+ qreal height = 0, widthUsed = 0;
+ viewItemTextLayout(textLayout, bounds.width(), height, widthUsed);
+ const QSize size(qCeil(widthUsed), qCeil(height));
+ return QSize(size.width() + 2 * textMargin, size.height());
+}
+
+void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, index);
+ painter->save();
+ painter->setClipRect(opt.rect);
+
+ opt.features |= QStyleOptionViewItem::WrapText;
+ opt.text = index.data().toString();
+ opt.textElideMode = Qt::ElideRight;
+ opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter;
+
+ QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
+
+ // const int iconSize = style->pixelMetric(QStyle::PM_IconViewIconSize);
+ const int iconSize = 48;
+ QRect iconbox = opt.rect;
+ const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, opt.widget) + 1;
+ QRect textRect = opt.rect;
+ QRect textHighlightRect = textRect;
+ // clip the decoration on top, remove width padding
+ textRect.adjust(textMargin, iconSize + textMargin + 5, -textMargin, 0);
+
+ textHighlightRect.adjust(0, iconSize + 5, 0, 0);
+
+ // draw background
+ {
+ // FIXME: unused
+ // QSize textSize = viewItemTextSize ( &opt );
+ drawSelectionRect(painter, opt, textHighlightRect);
+ /*
+ QPalette::ColorGroup cg;
+ QStyleOptionViewItem opt2(opt);
+
+ if ((opt.widget && opt.widget->isEnabled()) || (opt.state & QStyle::State_Enabled))
+ {
+ if (!(opt.state & QStyle::State_Active))
+ cg = QPalette::Inactive;
+ else
+ cg = QPalette::Normal;
+ }
+ else
+ {
+ cg = QPalette::Disabled;
+ }
+ */
+ /*
+ opt2.palette.setCurrentColorGroup(cg);
+
+ // fill in background, if any
+
+
+ if (opt.backgroundBrush.style() != Qt::NoBrush)
+ {
+ QPointF oldBO = painter->brushOrigin();
+ painter->setBrushOrigin(opt.rect.topLeft());
+ painter->fillRect(opt.rect, opt.backgroundBrush);
+ painter->setBrushOrigin(oldBO);
+ }
+
+ drawSelectionRect(painter, opt2, textHighlightRect);
+ */
+
+ /*
+ if (opt.showDecorationSelected)
+ {
+ drawSelectionRect(painter, opt2, opt.rect);
+ drawFocusRect(painter, opt2, opt.rect);
+ // painter->fillRect ( opt.rect, opt.palette.brush ( cg, QPalette::Highlight ) );
+ }
+ else
+ {
+
+ // if ( opt.state & QStyle::State_Selected )
+ {
+ // QRect textRect = subElementRect ( QStyle::SE_ItemViewItemText, opt,
+ // opt.widget );
+ // painter->fillRect ( textHighlightRect, opt.palette.brush ( cg,
+ // QPalette::Highlight ) );
+ drawSelectionRect(painter, opt2, textHighlightRect);
+ drawFocusRect(painter, opt2, textHighlightRect);
+ }
+ }
+ */
+ }
+
+ // icon mode and state, also used for badges
+ QIcon::Mode mode = QIcon::Normal;
+ if (!(opt.state & QStyle::State_Enabled))
+ mode = QIcon::Disabled;
+ else if (opt.state & QStyle::State_Selected)
+ mode = QIcon::Selected;
+ QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off;
+
+ // draw the icon
+ {
+ iconbox.setHeight(iconSize);
+ opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state);
+ }
+ // set the text colors
+ QPalette::ColorGroup cg =
+ opt.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled;
+ if (cg == QPalette::Normal && !(opt.state & QStyle::State_Active))
+ cg = QPalette::Inactive;
+ if (opt.state & QStyle::State_Selected)
+ {
+ painter->setPen(opt.palette.color(cg, QPalette::HighlightedText));
+ }
+ else
+ {
+ painter->setPen(opt.palette.color(cg, QPalette::Text));
+ }
+
+ // draw the text
+ QTextOption textOption;
+ textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ textOption.setTextDirection(opt.direction);
+ textOption.setAlignment(QStyle::visualAlignment(opt.direction, opt.displayAlignment));
+ QTextLayout textLayout;
+ textLayout.setTextOption(textOption);
+ textLayout.setFont(opt.font);
+ textLayout.setText(opt.text);
+
+ qreal width, height;
+ viewItemTextLayout(textLayout, textRect.width(), height, width);
+
+ const int lineCount = textLayout.lineCount();
+
+ const QRect layoutRect = QStyle::alignedRect(
+ opt.direction, opt.displayAlignment, QSize(textRect.width(), int(height)), textRect);
+ const QPointF position = layoutRect.topLeft();
+ for (int i = 0; i < lineCount; ++i)
+ {
+ const QTextLine line = textLayout.lineAt(i);
+ line.draw(painter, position);
+ }
+
+ // FIXME: this really has no business of being here. Make generic.
+ auto instance = (BaseInstance*)index.data(InstanceList::InstancePointerRole)
+ .value<void *>();
+ if (instance)
+ {
+ drawBadges(painter, opt, instance, mode, state);
+ }
+
+ drawProgressOverlay(painter, opt, index.data(GroupViewRoles::ProgressValueRole).toInt(),
+ index.data(GroupViewRoles::ProgressMaximumRole).toInt());
+
+ painter->restore();
+}
+
+QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, index);
+ opt.features |= QStyleOptionViewItem::WrapText;
+ opt.text = index.data().toString();
+ opt.textElideMode = Qt::ElideRight;
+ opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter;
+
+ QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
+ const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, opt.widget) + 1;
+ int height = 48 + textMargin * 2 + 5; // TODO: turn constants into variables
+ QSize szz = viewItemTextSize(&opt);
+ height += szz.height();
+ // FIXME: maybe the icon items could scale and keep proportions?
+ QSize sz(100, height);
+ return sz;
+}
+
+class NoReturnTextEdit: public QTextEdit
+{
+ Q_OBJECT
+public:
+ explicit NoReturnTextEdit(QWidget *parent) : QTextEdit(parent)
+ {
+ setTextInteractionFlags(Qt::TextEditorInteraction);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
+ }
+ bool event(QEvent * event) override
+ {
+ auto eventType = event->type();
+ if(eventType == QEvent::KeyPress || eventType == QEvent::KeyRelease)
+ {
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+ auto key = keyEvent->key();
+ if (key == Qt::Key_Return || key == Qt::Key_Enter)
+ {
+ emit editingDone();
+ return true;
+ }
+ if(key == Qt::Key_Tab)
+ {
+ return true;
+ }
+ }
+ return QTextEdit::event(event);
+ }
+signals:
+ void editingDone();
+};
+
+void ListViewDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ const int iconSize = 48;
+ QRect textRect = option.rect;
+ // QStyle *style = option.widget ? option.widget->style() : QApplication::style();
+ textRect.adjust(0, iconSize + 5, 0, 0);
+ editor->setGeometry(textRect);
+}
+
+void ListViewDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
+{
+ auto text = index.data(Qt::EditRole).toString();
+ QTextEdit * realeditor = qobject_cast<NoReturnTextEdit *>(editor);
+ realeditor->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
+ realeditor->append(text);
+ realeditor->selectAll();
+ realeditor->document()->clearUndoRedoStacks();
+}
+
+void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
+{
+ QTextEdit * realeditor = qobject_cast<NoReturnTextEdit *>(editor);
+ QString text = realeditor->toPlainText();
+ text.replace(QChar('\n'), QChar(' '));
+ text = text.trimmed();
+ if(text.size() != 0)
+ {
+ model->setData(index, text);
+ }
+}
+
+QWidget * ListViewDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ auto editor = new NoReturnTextEdit(parent);
+ connect(editor, &NoReturnTextEdit::editingDone, this, &ListViewDelegate::editingDone);
+ return editor;
+}
+
+void ListViewDelegate::editingDone()
+{
+ NoReturnTextEdit *editor = qobject_cast<NoReturnTextEdit *>(sender());
+ emit commitData(editor);
+ emit closeEditor(editor);
+}
+
+#include "InstanceDelegate.moc"
diff --git a/launcher/groupview/InstanceDelegate.h b/launcher/groupview/InstanceDelegate.h
new file mode 100644
index 00000000..d95279f3
--- /dev/null
+++ b/launcher/groupview/InstanceDelegate.h
@@ -0,0 +1,39 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QStyledItemDelegate>
+#include <QCache>
+
+class ListViewDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit ListViewDelegate(QObject *parent = 0);
+ virtual ~ListViewDelegate() {}
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+ void updateEditorGeometry(QWidget * editor, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+ QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+
+ void setEditorData(QWidget * editor, const QModelIndex & index) const override;
+ void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
+
+private slots:
+ void editingDone();
+};
diff --git a/launcher/groupview/VisualGroup.cpp b/launcher/groupview/VisualGroup.cpp
new file mode 100644
index 00000000..76bf8678
--- /dev/null
+++ b/launcher/groupview/VisualGroup.cpp
@@ -0,0 +1,317 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "VisualGroup.h"
+
+#include <QModelIndex>
+#include <QPainter>
+#include <QtMath>
+#include <QApplication>
+#include <QDebug>
+
+#include "GroupView.h"
+
+VisualGroup::VisualGroup(const QString &text, GroupView *view) : view(view), text(text), collapsed(false)
+{
+}
+
+VisualGroup::VisualGroup(const VisualGroup *other)
+ : view(other->view), text(other->text), collapsed(other->collapsed)
+{
+}
+
+void VisualGroup::update()
+{
+ auto temp_items = items();
+ auto itemsPerRow = view->itemsPerRow();
+
+ int numRows = qMax(1, qCeil((qreal)temp_items.size() / (qreal)itemsPerRow));
+ rows = QVector<VisualRow>(numRows);
+
+ int maxRowHeight = 0;
+ int positionInRow = 0;
+ int currentRow = 0;
+ int offsetFromTop = 0;
+ for (auto item: temp_items)
+ {
+ if(positionInRow == itemsPerRow)
+ {
+ rows[currentRow].height = maxRowHeight;
+ rows[currentRow].top = offsetFromTop;
+ currentRow ++;
+ offsetFromTop += maxRowHeight + 5;
+ positionInRow = 0;
+ maxRowHeight = 0;
+ }
+ auto itemHeight = view->itemDelegate()->sizeHint(view->viewOptions(), item).height();
+ if(itemHeight > maxRowHeight)
+ {
+ maxRowHeight = itemHeight;
+ }
+ rows[currentRow].items.append(item);
+ positionInRow++;
+ }
+ rows[currentRow].height = maxRowHeight;
+ rows[currentRow].top = offsetFromTop;
+}
+
+QPair<int, int> VisualGroup::positionOf(const QModelIndex &index) const
+{
+ int y = 0;
+ for (auto & row: rows)
+ {
+ for(auto x = 0; x < row.items.size(); x++)
+ {
+ if(row.items[x] == index)
+ {
+ return qMakePair(x,y);
+ }
+ }
+ y++;
+ }
+ qWarning() << "Item" << index.row() << index.data(Qt::DisplayRole).toString() << "not found in visual group" << text;
+ return qMakePair(0, 0);
+}
+
+int VisualGroup::rowTopOf(const QModelIndex &index) const
+{
+ auto position = positionOf(index);
+ return rows[position.second].top;
+}
+
+int VisualGroup::rowHeightOf(const QModelIndex &index) const
+{
+ auto position = positionOf(index);
+ return rows[position.second].height;
+}
+
+VisualGroup::HitResults VisualGroup::hitScan(const QPoint &pos) const
+{
+ VisualGroup::HitResults results = VisualGroup::NoHit;
+ int y_start = verticalPosition();
+ int body_start = y_start + headerHeight();
+ int body_end = body_start + contentHeight() + 5; // FIXME: wtf is this 5?
+ int y = pos.y();
+ // int x = pos.x();
+ if (y < y_start)
+ {
+ results = VisualGroup::NoHit;
+ }
+ else if (y < body_start)
+ {
+ results = VisualGroup::HeaderHit;
+ int collapseSize = headerHeight() - 4;
+
+ // the icon
+ QRect iconRect = QRect(view->m_leftMargin + 2, 2 + y_start, collapseSize, collapseSize);
+ if (iconRect.contains(pos))
+ {
+ results |= VisualGroup::CheckboxHit;
+ }
+ }
+ else if (y < body_end)
+ {
+ results |= VisualGroup::BodyHit;
+ }
+ return results;
+}
+
+void VisualGroup::drawHeader(QPainter *painter, const QStyleOptionViewItem &option)
+{
+ painter->setRenderHint(QPainter::Antialiasing);
+
+ const QRect optRect = option.rect;
+ QFont font(QApplication::font());
+ font.setBold(true);
+ const QFontMetrics fontMetrics = QFontMetrics(font);
+
+ QColor outlineColor = option.palette.text().color();
+ outlineColor.setAlphaF(0.35);
+
+ //BEGIN: top left corner
+ {
+ painter->save();
+ painter->setPen(outlineColor);
+ const QPointF topLeft(optRect.topLeft());
+ QRectF arc(topLeft, QSizeF(4, 4));
+ arc.translate(0.5, 0.5);
+ painter->drawArc(arc, 1440, 1440);
+ painter->restore();
+ }
+ //END: top left corner
+
+ //BEGIN: left vertical line
+ {
+ QPoint start(optRect.topLeft());
+ start.ry() += 3;
+ QPoint verticalGradBottom(optRect.topLeft());
+ verticalGradBottom.ry() += fontMetrics.height() + 5;
+ QLinearGradient gradient(start, verticalGradBottom);
+ gradient.setColorAt(0, outlineColor);
+ gradient.setColorAt(1, Qt::transparent);
+ painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient);
+ }
+ //END: left vertical line
+
+ //BEGIN: horizontal line
+ {
+ QPoint start(optRect.topLeft());
+ start.rx() += 3;
+ QPoint horizontalGradTop(optRect.topLeft());
+ horizontalGradTop.rx() += optRect.width() - 6;
+ painter->fillRect(QRect(start, QSize(optRect.width() - 6, 1)), outlineColor);
+ }
+ //END: horizontal line
+
+ //BEGIN: top right corner
+ {
+ painter->save();
+ painter->setPen(outlineColor);
+ QPointF topRight(optRect.topRight());
+ topRight.rx() -= 4;
+ QRectF arc(topRight, QSizeF(4, 4));
+ arc.translate(0.5, 0.5);
+ painter->drawArc(arc, 0, 1440);
+ painter->restore();
+ }
+ //END: top right corner
+
+ //BEGIN: right vertical line
+ {
+ QPoint start(optRect.topRight());
+ start.ry() += 3;
+ QPoint verticalGradBottom(optRect.topRight());
+ verticalGradBottom.ry() += fontMetrics.height() + 5;
+ QLinearGradient gradient(start, verticalGradBottom);
+ gradient.setColorAt(0, outlineColor);
+ gradient.setColorAt(1, Qt::transparent);
+ painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient);
+ }
+ //END: right vertical line
+
+ //BEGIN: checkboxy thing
+ {
+ painter->save();
+ painter->setRenderHint(QPainter::Antialiasing, false);
+ painter->setFont(font);
+ QColor penColor(option.palette.text().color());
+ penColor.setAlphaF(0.6);
+ painter->setPen(penColor);
+ QRect iconSubRect(option.rect);
+ iconSubRect.setTop(iconSubRect.top() + 7);
+ iconSubRect.setLeft(iconSubRect.left() + 7);
+
+ int sizing = fontMetrics.height();
+ int even = ( (sizing - 1) % 2 );
+
+ iconSubRect.setHeight(sizing - even);
+ iconSubRect.setWidth(sizing - even);
+ painter->drawRect(iconSubRect);
+
+
+ /*
+ if(collapsed)
+ painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "+");
+ else
+ painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "-");
+ */
+ painter->setBrush(option.palette.text());
+ painter->fillRect(iconSubRect.x(), iconSubRect.y() + iconSubRect.height() / 2,
+ iconSubRect.width(), 2, penColor);
+ if (collapsed)
+ {
+ painter->fillRect(iconSubRect.x() + iconSubRect.width() / 2, iconSubRect.y(), 2,
+ iconSubRect.height(), penColor);
+ }
+
+ painter->restore();
+ }
+ //END: checkboxy thing
+
+ //BEGIN: text
+ {
+ QRect textRect(option.rect);
+ textRect.setTop(textRect.top() + 7);
+ textRect.setLeft(textRect.left() + 7 + fontMetrics.height() + 7);
+ textRect.setHeight(fontMetrics.height());
+ textRect.setRight(textRect.right() - 7);
+
+ painter->save();
+ painter->setFont(font);
+ QColor penColor(option.palette.text().color());
+ penColor.setAlphaF(0.6);
+ painter->setPen(penColor);
+ painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text);
+ painter->restore();
+ }
+ //END: text
+}
+
+int VisualGroup::totalHeight() const
+{
+ return headerHeight() + 5 + contentHeight(); // FIXME: wtf is that '5'?
+}
+
+int VisualGroup::headerHeight() const
+{
+ QFont font(QApplication::font());
+ font.setBold(true);
+ QFontMetrics fontMetrics(font);
+
+ const int height = fontMetrics.height() + 1 /* 1 pixel-width gradient */
+ + 11 /* top and bottom separation */;
+ return height;
+ /*
+ int raw = view->viewport()->fontMetrics().height() + 4;
+ // add english. maybe. depends on font height.
+ if (raw % 2 == 0)
+ raw++;
+ return std::min(raw, 25);
+ */
+}
+
+int VisualGroup::contentHeight() const
+{
+ if (collapsed)
+ {
+ return 0;
+ }
+ auto last = rows[numRows() - 1];
+ return last.top + last.height;
+}
+
+int VisualGroup::numRows() const
+{
+ return rows.size();
+}
+
+int VisualGroup::verticalPosition() const
+{
+ return m_verticalPosition;
+}
+
+QList<QModelIndex> VisualGroup::items() const
+{
+ QList<QModelIndex> indices;
+ for (int i = 0; i < view->model()->rowCount(); ++i)
+ {
+ const QModelIndex index = view->model()->index(i, 0);
+ if (index.data(GroupViewRoles::GroupRole).toString() == text)
+ {
+ indices.append(index);
+ }
+ }
+ return indices;
+}
diff --git a/launcher/groupview/VisualGroup.h b/launcher/groupview/VisualGroup.h
new file mode 100644
index 00000000..239ee9d7
--- /dev/null
+++ b/launcher/groupview/VisualGroup.h
@@ -0,0 +1,106 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QRect>
+#include <QVector>
+#include <QStyleOption>
+
+class GroupView;
+class QPainter;
+class QModelIndex;
+
+struct VisualRow
+{
+ QList<QModelIndex> items;
+ int height = 0;
+ int top = 0;
+ inline int size() const
+ {
+ return items.size();
+ }
+ inline QModelIndex &operator[](int i)
+ {
+ return items[i];
+ }
+};
+
+struct VisualGroup
+{
+/* constructors */
+ VisualGroup(const QString &text, GroupView *view);
+ VisualGroup(const VisualGroup *other);
+
+/* data */
+ GroupView *view = nullptr;
+ QString text;
+ bool collapsed = false;
+ QVector<VisualRow> rows;
+ int firstItemIndex = 0;
+ int m_verticalPosition = 0;
+
+/* logic */
+ /// update the internal list of items and flow them into the rows.
+ void update();
+
+ /// draw the header at y-position.
+ void drawHeader(QPainter *painter, const QStyleOptionViewItem &option);
+
+ /// height of the group, in total. includes a small bit of padding.
+ int totalHeight() const;
+
+ /// height of the group header, in pixels
+ int headerHeight() const;
+
+ /// height of the group content, in pixels
+ int contentHeight() const;
+
+ /// the number of visual rows this group has
+ int numRows() const;
+
+ /// actually calculate the above value
+ int calculateNumRows() const;
+
+ /// the height at which this group starts, in pixels
+ int verticalPosition() const;
+
+ /// relative geometry - top of the row of the given item
+ int rowTopOf(const QModelIndex &index) const;
+
+ /// height of the row of the given item
+ int rowHeightOf(const QModelIndex &index) const;
+
+ /// x/y position of the given item inside the group (in items!)
+ QPair<int, int> positionOf(const QModelIndex &index) const;
+
+ enum HitResult
+ {
+ NoHit = 0x0,
+ TextHit = 0x1,
+ CheckboxHit = 0x2,
+ HeaderHit = 0x4,
+ BodyHit = 0x8
+ };
+ Q_DECLARE_FLAGS(HitResults, HitResult)
+
+ /// shoot! BANG! what did we hit?
+ HitResults hitScan (const QPoint &pos) const;
+
+ QList<QModelIndex> items() const;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(VisualGroup::HitResults)
diff --git a/launcher/icons/IIconList.cpp b/launcher/icons/IIconList.cpp
new file mode 100644
index 00000000..b3a8fb43
--- /dev/null
+++ b/launcher/icons/IIconList.cpp
@@ -0,0 +1,7 @@
+#include "IIconList.h"
+
+// blargh
+IIconList::~IIconList()
+{
+}
+
diff --git a/launcher/icons/IIconList.h b/launcher/icons/IIconList.h
new file mode 100644
index 00000000..15d7dd15
--- /dev/null
+++ b/launcher/icons/IIconList.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <QString>
+#include <QStringList>
+
+enum IconType : unsigned
+{
+ Builtin,
+ Transient,
+ FileBased,
+ ICONS_TOTAL,
+ ToBeDeleted
+};
+
+class IIconList
+{
+public:
+ virtual ~IIconList();
+ virtual bool addIcon(const QString &key, const QString &name, const QString &path, const IconType type) = 0;
+ virtual bool deleteIcon(const QString &key) = 0;
+ virtual void saveIcon(const QString &key, const QString &path, const char * format) const = 0;
+ virtual bool iconFileExists(const QString &key) const = 0;
+ virtual void installIcons(const QStringList &iconFiles) = 0;
+ virtual void installIcon(const QString &file, const QString &name) = 0;
+};
diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp
new file mode 100644
index 00000000..70350534
--- /dev/null
+++ b/launcher/icons/IconList.cpp
@@ -0,0 +1,419 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "IconList.h"
+#include <FileSystem.h>
+#include <QMap>
+#include <QEventLoop>
+#include <QMimeData>
+#include <QUrl>
+#include <QFileSystemWatcher>
+#include <QSet>
+#include <QDebug>
+
+#define MAX_SIZE 1024
+
+IconList::IconList(const QStringList &builtinPaths, QString path, QObject *parent) : QAbstractListModel(parent)
+{
+ QSet<QString> builtinNames;
+
+ // add builtin icons
+ for(auto & builtinPath: builtinPaths)
+ {
+ QDir instance_icons(builtinPath);
+ auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name);
+ for (auto file_info : file_info_list)
+ {
+ builtinNames.insert(file_info.baseName());
+ }
+ }
+ for(auto & builtinName : builtinNames)
+ {
+ addThemeIcon(builtinName);
+ }
+
+ m_watcher.reset(new QFileSystemWatcher());
+ is_watching = false;
+ connect(m_watcher.get(), SIGNAL(directoryChanged(QString)),
+ SLOT(directoryChanged(QString)));
+ connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
+
+ directoryChanged(path);
+}
+
+void IconList::directoryChanged(const QString &path)
+{
+ QDir new_dir (path);
+ if(m_dir.absolutePath() != new_dir.absolutePath())
+ {
+ m_dir.setPath(path);
+ m_dir.refresh();
+ if(is_watching)
+ stopWatching();
+ startWatching();
+ }
+ if(!m_dir.exists())
+ if(!FS::ensureFolderPathExists(m_dir.absolutePath()))
+ return;
+ m_dir.refresh();
+ auto new_list = m_dir.entryList(QDir::Files, QDir::Name);
+ for (auto it = new_list.begin(); it != new_list.end(); it++)
+ {
+ QString &foo = (*it);
+ foo = m_dir.filePath(foo);
+ }
+ auto new_set = new_list.toSet();
+ QList<QString> current_list;
+ for (auto &it : icons)
+ {
+ if (!it.has(IconType::FileBased))
+ continue;
+ current_list.push_back(it.m_images[IconType::FileBased].filename);
+ }
+ QSet<QString> current_set = current_list.toSet();
+
+ QSet<QString> to_remove = current_set;
+ to_remove -= new_set;
+
+ QSet<QString> to_add = new_set;
+ to_add -= current_set;
+
+ for (auto remove : to_remove)
+ {
+ qDebug() << "Removing " << remove;
+ QFileInfo rmfile(remove);
+ QString key = rmfile.baseName();
+ int idx = getIconIndex(key);
+ if (idx == -1)
+ continue;
+ icons[idx].remove(IconType::FileBased);
+ if (icons[idx].type() == IconType::ToBeDeleted)
+ {
+ beginRemoveRows(QModelIndex(), idx, idx);
+ icons.remove(idx);
+ reindex();
+ endRemoveRows();
+ }
+ else
+ {
+ dataChanged(index(idx), index(idx));
+ }
+ m_watcher->removePath(remove);
+ emit iconUpdated(key);
+ }
+
+ for (auto add : to_add)
+ {
+ qDebug() << "Adding " << add;
+ QFileInfo addfile(add);
+ QString key = addfile.baseName();
+ if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased))
+ {
+ m_watcher->addPath(add);
+ emit iconUpdated(key);
+ }
+ }
+}
+
+void IconList::fileChanged(const QString &path)
+{
+ qDebug() << "Checking " << path;
+ QFileInfo checkfile(path);
+ if (!checkfile.exists())
+ return;
+ QString key = checkfile.baseName();
+ int idx = getIconIndex(key);
+ if (idx == -1)
+ return;
+ QIcon icon(path);
+ if (!icon.availableSizes().size())
+ return;
+
+ icons[idx].m_images[IconType::FileBased].icon = icon;
+ dataChanged(index(idx), index(idx));
+ emit iconUpdated(key);
+}
+
+void IconList::SettingChanged(const Setting &setting, QVariant value)
+{
+ if(setting.id() != "IconsDir")
+ return;
+
+ directoryChanged(value.toString());
+}
+
+void IconList::startWatching()
+{
+ auto abs_path = m_dir.absolutePath();
+ FS::ensureFolderPathExists(abs_path);
+ is_watching = m_watcher->addPath(abs_path);
+ if (is_watching)
+ {
+ qDebug() << "Started watching " << abs_path;
+ }
+ else
+ {
+ qDebug() << "Failed to start watching " << abs_path;
+ }
+}
+
+void IconList::stopWatching()
+{
+ m_watcher->removePaths(m_watcher->files());
+ m_watcher->removePaths(m_watcher->directories());
+ is_watching = false;
+}
+
+QStringList IconList::mimeTypes() const
+{
+ QStringList types;
+ types << "text/uri-list";
+ return types;
+}
+Qt::DropActions IconList::supportedDropActions() const
+{
+ return Qt::CopyAction;
+}
+
+bool IconList::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())
+ {
+ auto urls = data->urls();
+ QStringList iconFiles;
+ for (auto url : urls)
+ {
+ // only local files may be dropped...
+ if (!url.isLocalFile())
+ continue;
+ iconFiles += url.toLocalFile();
+ }
+ installIcons(iconFiles);
+ return true;
+ }
+ return false;
+}
+
+Qt::ItemFlags IconList::flags(const QModelIndex &index) const
+{
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ if (index.isValid())
+ return Qt::ItemIsDropEnabled | defaultFlags;
+ else
+ return Qt::ItemIsDropEnabled | defaultFlags;
+}
+
+QVariant IconList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+
+ if (row < 0 || row >= icons.size())
+ return QVariant();
+
+ switch (role)
+ {
+ case Qt::DecorationRole:
+ return icons[row].icon();
+ case Qt::DisplayRole:
+ return icons[row].name();
+ case Qt::UserRole:
+ return icons[row].m_key;
+ default:
+ return QVariant();
+ }
+}
+
+int IconList::rowCount(const QModelIndex &parent) const
+{
+ return icons.size();
+}
+
+void IconList::installIcons(const QStringList &iconFiles)
+{
+ for (QString file : iconFiles)
+ {
+ QFileInfo fileinfo(file);
+ if (!fileinfo.isReadable() || !fileinfo.isFile())
+ continue;
+ QString target = FS::PathCombine(m_dir.dirName(), fileinfo.fileName());
+
+ QString suffix = fileinfo.suffix();
+ if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif")
+ continue;
+
+ if (!QFile::copy(file, target))
+ continue;
+ }
+}
+
+void IconList::installIcon(const QString &file, const QString &name)
+{
+ QFileInfo fileinfo(file);
+ if(!fileinfo.isReadable() || !fileinfo.isFile())
+ return;
+
+ QString target = FS::PathCombine(m_dir.dirName(), name);
+
+ QFile::copy(file, target);
+}
+
+bool IconList::iconFileExists(const QString &key) const
+{
+ auto iconEntry = icon(key);
+ if(!iconEntry)
+ {
+ return false;
+ }
+ return iconEntry->has(IconType::FileBased);
+}
+
+const MMCIcon *IconList::icon(const QString &key) const
+{
+ int iconIdx = getIconIndex(key);
+ if (iconIdx == -1)
+ return nullptr;
+ return &icons[iconIdx];
+}
+
+bool IconList::deleteIcon(const QString &key)
+{
+ int iconIdx = getIconIndex(key);
+ if (iconIdx == -1)
+ return false;
+ auto &iconEntry = icons[iconIdx];
+ if (iconEntry.has(IconType::FileBased))
+ {
+ return QFile::remove(iconEntry.m_images[IconType::FileBased].filename);
+ }
+ return false;
+}
+
+bool IconList::addThemeIcon(const QString& key)
+{
+ auto iter = name_index.find(key);
+ if (iter != name_index.end())
+ {
+ auto &oldOne = icons[*iter];
+ oldOne.replace(Builtin, key);
+ dataChanged(index(*iter), index(*iter));
+ return true;
+ }
+ else
+ {
+ // add a new icon
+ beginInsertRows(QModelIndex(), icons.size(), icons.size());
+ {
+ MMCIcon mmc_icon;
+ mmc_icon.m_name = key;
+ mmc_icon.m_key = key;
+ mmc_icon.replace(Builtin, key);
+ icons.push_back(mmc_icon);
+ name_index[key] = icons.size() - 1;
+ }
+ endInsertRows();
+ return true;
+ }
+}
+
+bool IconList::addIcon(const QString &key, const QString &name, const QString &path, const IconType type)
+{
+ // replace the icon even? is the input valid?
+ QIcon icon(path);
+ if (icon.isNull())
+ return false;
+ auto iter = name_index.find(key);
+ if (iter != name_index.end())
+ {
+ auto &oldOne = icons[*iter];
+ oldOne.replace(type, icon, path);
+ dataChanged(index(*iter), index(*iter));
+ return true;
+ }
+ else
+ {
+ // add a new icon
+ beginInsertRows(QModelIndex(), icons.size(), icons.size());
+ {
+ MMCIcon mmc_icon;
+ mmc_icon.m_name = name;
+ mmc_icon.m_key = key;
+ mmc_icon.replace(type, icon, path);
+ icons.push_back(mmc_icon);
+ name_index[key] = icons.size() - 1;
+ }
+ endInsertRows();
+ return true;
+ }
+}
+
+void IconList::saveIcon(const QString &key, const QString &path, const char * format) const
+{
+ auto icon = getIcon(key);
+ auto pixmap = icon.pixmap(128, 128);
+ pixmap.save(path, format);
+}
+
+
+void IconList::reindex()
+{
+ name_index.clear();
+ int i = 0;
+ for (auto &iter : icons)
+ {
+ name_index[iter.m_key] = i;
+ i++;
+ }
+}
+
+QIcon IconList::getIcon(const QString &key) const
+{
+ int icon_index = getIconIndex(key);
+
+ if (icon_index != -1)
+ return icons[icon_index].icon();
+
+ // Fallback for icons that don't exist.
+ icon_index = getIconIndex("infinity");
+
+ if (icon_index != -1)
+ return icons[icon_index].icon();
+ return QIcon();
+}
+
+int IconList::getIconIndex(const QString &key) const
+{
+ auto iter = name_index.find(key == "default" ? "infinity" : key);
+ if (iter != name_index.end())
+ return *iter;
+
+ return -1;
+}
+
+QString IconList::getDirectory() const
+{
+ return m_dir.absolutePath();
+}
+
+//#include "IconList.moc"
diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h
new file mode 100644
index 00000000..70266ebb
--- /dev/null
+++ b/launcher/icons/IconList.h
@@ -0,0 +1,86 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMutex>
+#include <QAbstractListModel>
+#include <QFile>
+#include <QDir>
+#include <QtGui/QIcon>
+#include <memory>
+#include "MMCIcon.h"
+#include "settings/Setting.h"
+#include "Env.h" // there is a global icon list inside Env.
+#include <icons/IIconList.h>
+
+class QFileSystemWatcher;
+
+class IconList : public QAbstractListModel, public IIconList
+{
+ Q_OBJECT
+public:
+ explicit IconList(const QStringList &builtinPaths, QString path, QObject *parent = 0);
+ virtual ~IconList() {};
+
+ QIcon getIcon(const QString &key) const;
+ int getIconIndex(const QString &key) const;
+ QString getDirectory() const;
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+ bool addThemeIcon(const QString &key);
+ bool addIcon(const QString &key, const QString &name, const QString &path, const IconType type) override;
+ void saveIcon(const QString &key, const QString &path, const char * format) const override;
+ bool deleteIcon(const QString &key) override;
+ bool iconFileExists(const QString &key) const override;
+
+ virtual QStringList mimeTypes() const override;
+ virtual Qt::DropActions supportedDropActions() const override;
+ virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ void installIcons(const QStringList &iconFiles) override;
+ void installIcon(const QString &file, const QString &name) override;
+
+ const MMCIcon * icon(const QString &key) const;
+
+ void startWatching();
+ void stopWatching();
+
+signals:
+ void iconUpdated(QString key);
+
+private:
+ // hide copy constructor
+ IconList(const IconList &) = delete;
+ // hide assign op
+ IconList &operator=(const IconList &) = delete;
+ void reindex();
+
+public slots:
+ void directoryChanged(const QString &path);
+
+protected slots:
+ void fileChanged(const QString &path);
+ void SettingChanged(const Setting & setting, QVariant value);
+private:
+ shared_qobject_ptr<QFileSystemWatcher> m_watcher;
+ bool is_watching;
+ QMap<QString, int> name_index;
+ QVector<MMCIcon> icons;
+ QDir m_dir;
+};
diff --git a/launcher/icons/IconUtils.cpp b/launcher/icons/IconUtils.cpp
new file mode 100644
index 00000000..bf530c16
--- /dev/null
+++ b/launcher/icons/IconUtils.cpp
@@ -0,0 +1,62 @@
+#include "IconUtils.h"
+
+#include "FileSystem.h"
+#include <QDirIterator>
+
+#include <array>
+
+namespace {
+std::array<const char *, 6> validIconExtensions = {{
+ "svg",
+ "png",
+ "ico",
+ "gif",
+ "jpg",
+ "jpeg"
+}};
+}
+
+namespace IconUtils{
+
+QString findBestIconIn(const QString &folder, const QString & iconKey) {
+ int best_found = validIconExtensions.size();
+ QString best_filename;
+
+ QDirIterator it(folder, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::NoIteratorFlags);
+ while (it.hasNext()) {
+ it.next();
+ auto fileInfo = it.fileInfo();
+
+ if(fileInfo.completeBaseName() != iconKey)
+ continue;
+
+ auto extension = fileInfo.suffix();
+
+ for(int i = 0; i < best_found; i++) {
+ if(extension == validIconExtensions[i]) {
+ best_found = i;
+ qDebug() << i << " : " << fileInfo.fileName();
+ best_filename = fileInfo.fileName();
+ }
+ }
+ }
+ return FS::PathCombine(folder, best_filename);
+}
+
+QString getIconFilter() {
+ QString out;
+ QTextStream stream(&out);
+ stream << '(';
+ for(size_t i = 0; i < validIconExtensions.size() - 1; i++) {
+ if(i > 0) {
+ stream << " ";
+ }
+ stream << "*." << validIconExtensions[i];
+ }
+ stream << " *." << validIconExtensions[validIconExtensions.size() - 1];
+ stream << ')';
+ return out;
+}
+
+}
+
diff --git a/launcher/icons/IconUtils.h b/launcher/icons/IconUtils.h
new file mode 100644
index 00000000..be93d914
--- /dev/null
+++ b/launcher/icons/IconUtils.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <QString>
+
+namespace IconUtils {
+
+// Given a folder and an icon key, find 'best' of the icons with the given key in there and return its path
+QString findBestIconIn(const QString &folder, const QString & iconKey);
+
+// Get icon file type filter for file browser dialogs
+QString getIconFilter();
+
+}
diff --git a/launcher/icons/MMCIcon.cpp b/launcher/icons/MMCIcon.cpp
new file mode 100644
index 00000000..f0b82ec9
--- /dev/null
+++ b/launcher/icons/MMCIcon.cpp
@@ -0,0 +1,118 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MMCIcon.h"
+#include <QFileInfo>
+#include <xdgicon.h>
+
+IconType operator--(IconType &t, int)
+{
+ IconType temp = t;
+ switch (t)
+ {
+ case IconType::Builtin:
+ t = IconType::ToBeDeleted;
+ break;
+ case IconType::Transient:
+ t = IconType::Builtin;
+ break;
+ case IconType::FileBased:
+ t = IconType::Transient;
+ break;
+ default:
+ {
+ }
+ }
+ return temp;
+}
+
+IconType MMCIcon::type() const
+{
+ return m_current_type;
+}
+
+QString MMCIcon::name() const
+{
+ if (m_name.size())
+ return m_name;
+ return m_key;
+}
+
+bool MMCIcon::has(IconType _type) const
+{
+ return m_images[_type].present();
+}
+
+QIcon MMCIcon::icon() const
+{
+ if (m_current_type == IconType::ToBeDeleted)
+ return QIcon();
+ auto & icon = m_images[m_current_type].icon;
+ if(!icon.isNull())
+ return icon;
+ // FIXME: inject this.
+ return XdgIcon::fromTheme(m_images[m_current_type].key);
+}
+
+void MMCIcon::remove(IconType rm_type)
+{
+ m_images[rm_type].filename = QString();
+ m_images[rm_type].icon = QIcon();
+ for (auto iter = rm_type; iter != IconType::ToBeDeleted; iter--)
+ {
+ if (m_images[iter].present())
+ {
+ m_current_type = iter;
+ return;
+ }
+ }
+ m_current_type = IconType::ToBeDeleted;
+}
+
+void MMCIcon::replace(IconType new_type, QIcon icon, QString path)
+{
+ if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted)
+ {
+ m_current_type = new_type;
+ }
+ m_images[new_type].icon = icon;
+ m_images[new_type].filename = path;
+ m_images[new_type].key = QString();
+}
+
+void MMCIcon::replace(IconType new_type, const QString& key)
+{
+ if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted)
+ {
+ m_current_type = new_type;
+ }
+ m_images[new_type].icon = QIcon();
+ m_images[new_type].filename = QString();
+ m_images[new_type].key = key;
+}
+
+QString MMCIcon::getFilePath() const
+{
+ if(m_current_type == IconType::ToBeDeleted){
+ return QString();
+ }
+ return m_images[m_current_type].filename;
+}
+
+
+bool MMCIcon::isBuiltIn() const
+{
+ return m_current_type == IconType::Builtin;
+}
diff --git a/launcher/icons/MMCIcon.h b/launcher/icons/MMCIcon.h
new file mode 100644
index 00000000..1f05f28e
--- /dev/null
+++ b/launcher/icons/MMCIcon.h
@@ -0,0 +1,49 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QString>
+#include <QDateTime>
+#include <QIcon>
+#include <icons/IIconList.h>
+
+struct MMCImage
+{
+ QIcon icon;
+ QString key;
+ QString filename;
+ bool present() const
+ {
+ return !icon.isNull() || !key.isEmpty();
+ }
+};
+
+struct MMCIcon
+{
+ QString m_key;
+ QString m_name;
+ MMCImage m_images[ICONS_TOTAL];
+ IconType m_current_type = ToBeDeleted;
+
+ IconType type() const;
+ QString name() const;
+ bool has(IconType _type) const;
+ QIcon icon() const;
+ void remove(IconType rm_type);
+ void replace(IconType new_type, QIcon icon, QString path = QString());
+ void replace(IconType new_type, const QString &key);
+ bool isBuiltIn() const;
+ QString getFilePath() const;
+};
diff --git a/launcher/install_prereqs.cmake.in b/launcher/install_prereqs.cmake.in
new file mode 100644
index 00000000..e4408d16
--- /dev/null
+++ b/launcher/install_prereqs.cmake.in
@@ -0,0 +1,27 @@
+set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@")
+
+file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@")
+function(gp_resolved_file_type_override resolved_file type_var)
+ if(resolved_file MATCHES "^/(usr/)?lib/libQt")
+ set(${type_var} other PARENT_SCOPE)
+ elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libxcb-")
+ set(${type_var} other PARENT_SCOPE)
+ elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libicu")
+ set(${type_var} other PARENT_SCOPE)
+ elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libpng")
+ set(${type_var} other PARENT_SCOPE)
+ elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libproxy")
+ set(${type_var} other PARENT_SCOPE)
+ elseif((resolved_file MATCHES "^/(usr/)?lib(.+)?/libstdc\\+\\+") AND (UNIX AND NOT APPLE))
+ set(${type_var} other PARENT_SCOPE)
+ endif()
+endfunction()
+
+set(gp_tool "@CMAKE_GP_TOOL@")
+set(gp_cmd_paths ${gp_cmd_paths}
+ "@CMAKE_GP_CMD_PATHS@"
+)
+
+include(BundleUtilities)
+fixup_bundle("@APPS@" "${QTPLUGINS}" "@DIRS@")
+
diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp
new file mode 100644
index 00000000..d78d6505
--- /dev/null
+++ b/launcher/java/JavaChecker.cpp
@@ -0,0 +1,166 @@
+#include "JavaChecker.h"
+#include "JavaUtils.h"
+#include <FileSystem.h>
+#include <Commandline.h>
+#include <QFile>
+#include <QProcess>
+#include <QMap>
+#include <QCoreApplication>
+#include <QDebug>
+
+#include "Env.h"
+
+JavaChecker::JavaChecker(QObject *parent) : QObject(parent)
+{
+}
+
+void JavaChecker::performCheck()
+{
+ QString checkerJar = FS::PathCombine(ENV.getJarsPath(), "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);
+ process->setProcessEnvironment(CleanEnviroment());
+ 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;
+ process.reset();
+
+ JavaCheckResult result;
+ {
+ result.path = m_path;
+ result.id = m_id;
+ }
+ result.errorLog = m_stderr;
+ result.outLog = m_stdout;
+ qDebug() << "STDOUT" << m_stdout;
+ qWarning() << "STDERR" << m_stderr;
+ qDebug() << "Java checker finished with status " << status << " exit code " << exitcode;
+
+ if (status == QProcess::CrashExit || exitcode == 1)
+ {
+ result.validity = JavaCheckResult::Validity::Errored;
+ 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") || !results.contains("java.vendor") || !success)
+ {
+ result.validity = JavaCheckResult::Validity::ReturnedInvalidData;
+ emit checkFinished(result);
+ return;
+ }
+
+ auto os_arch = results["os.arch"];
+ auto java_version = results["java.version"];
+ auto java_vendor = results["java.vendor"];
+ bool is_64 = os_arch == "x86_64" || os_arch == "amd64";
+
+
+ result.validity = JavaCheckResult::Validity::Valid;
+ result.is_64bit = is_64;
+ result.mojangPlatform = is_64 ? "64" : "32";
+ result.realPlatform = os_arch;
+ result.javaVersion = java_version;
+ result.javaVendor = java_vendor;
+ 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/launcher/java/JavaChecker.h b/launcher/java/JavaChecker.h
new file mode 100644
index 00000000..122861cf
--- /dev/null
+++ b/launcher/java/JavaChecker.h
@@ -0,0 +1,61 @@
+#pragma once
+#include <QProcess>
+#include <QTimer>
+#include <memory>
+
+#include "QObjectPtr.h"
+
+#include "JavaVersion.h"
+
+class JavaChecker;
+
+struct JavaCheckResult
+{
+ QString path;
+ QString mojangPlatform;
+ QString realPlatform;
+ JavaVersion javaVersion;
+ QString javaVendor;
+ QString outLog;
+ QString errorLog;
+ bool is_64bit = false;
+ int id;
+ enum class Validity
+ {
+ Errored,
+ ReturnedInvalidData,
+ Valid
+ } validity = Validity::Errored;
+};
+
+typedef shared_qobject_ptr<QProcess> QProcessPtr;
+typedef shared_qobject_ptr<JavaChecker> JavaCheckerPtr;
+class 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/launcher/java/JavaCheckerJob.cpp b/launcher/java/JavaCheckerJob.cpp
new file mode 100644
index 00000000..67d70066
--- /dev/null
+++ b/launcher/java/JavaCheckerJob.cpp
@@ -0,0 +1,44 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "JavaCheckerJob.h"
+
+#include <QDebug>
+
+void JavaCheckerJob::partFinished(JavaCheckResult result)
+{
+ num_finished++;
+ qDebug() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/"
+ << javacheckers.size();
+ setProgress(num_finished, javacheckers.size());
+
+ javaresults.replace(result.id, result);
+
+ if (num_finished == javacheckers.size())
+ {
+ emitSucceeded();
+ }
+}
+
+void JavaCheckerJob::executeTask()
+{
+ qDebug() << m_job_name.toLocal8Bit() << " started.";
+ for (auto iter : javacheckers)
+ {
+ javaresults.append(JavaCheckResult());
+ connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult)));
+ iter->performCheck();
+ }
+}
diff --git a/launcher/java/JavaCheckerJob.h b/launcher/java/JavaCheckerJob.h
new file mode 100644
index 00000000..c0986420
--- /dev/null
+++ b/launcher/java/JavaCheckerJob.h
@@ -0,0 +1,61 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QtNetwork>
+#include "JavaChecker.h"
+#include "tasks/Task.h"
+
+class JavaCheckerJob;
+typedef shared_qobject_ptr<JavaCheckerJob> JavaCheckerJobPtr;
+
+// FIXME: this just seems horribly redundant
+class JavaCheckerJob : public Task
+{
+ Q_OBJECT
+public:
+ explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name) {};
+ virtual ~JavaCheckerJob() {};
+
+ bool addJavaCheckerAction(JavaCheckerPtr base)
+ {
+ javacheckers.append(base);
+ // if this is already running, the action needs to be started right away!
+ if (isRunning())
+ {
+ setProgress(num_finished, javacheckers.size());
+ connect(base.get(), &JavaChecker::checkFinished, this, &JavaCheckerJob::partFinished);
+ base->performCheck();
+ }
+ return true;
+ }
+ QList<JavaCheckResult> getResults()
+ {
+ return javaresults;
+ }
+
+private slots:
+ void partFinished(JavaCheckResult result);
+
+protected:
+ virtual void executeTask() override;
+
+private:
+ QString m_job_name;
+ QList<JavaCheckerPtr> javacheckers;
+ QList<JavaCheckResult> javaresults;
+ int num_finished = 0;
+};
diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp
new file mode 100644
index 00000000..5bcf7bcb
--- /dev/null
+++ b/launcher/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/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h
new file mode 100644
index 00000000..64be40d1
--- /dev/null
+++ b/launcher/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/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp
new file mode 100644
index 00000000..0bded03c
--- /dev/null
+++ b/launcher/java/JavaInstallList.cpp
@@ -0,0 +1,208 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <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)
+{
+}
+
+shared_qobject_ptr<Task> JavaInstallList::getLoadTask()
+{
+ load();
+ return getCurrentTask();
+}
+
+shared_qobject_ptr<Task> JavaInstallList::getCurrentTask()
+{
+ if(m_status == Status::InProgress)
+ {
+ return m_loadTask;
+ }
+ return nullptr;
+}
+
+void JavaInstallList::load()
+{
+ if(m_status != Status::InProgress)
+ {
+ m_status = Status::InProgress;
+ m_loadTask = new JavaListLoadTask(this);
+ m_loadTask->start();
+ }
+}
+
+const BaseVersionPtr JavaInstallList::at(int i) const
+{
+ return m_vlist.at(i);
+}
+
+bool JavaInstallList::isLoaded()
+{
+ return m_status == JavaInstallList::Status::Done;
+}
+
+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;
+ sortVersions();
+ if(m_vlist.size())
+ {
+ auto best = std::dynamic_pointer_cast<JavaInstall>(m_vlist[0]);
+ best->recommended = true;
+ }
+ endResetModel();
+ m_status = Status::Done;
+ m_loadTask.reset();
+}
+
+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 = new JavaCheckerJob("Java detection");
+ connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished);
+ 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<JavaInstallPtr> candidates;
+ auto results = m_job->getResults();
+
+ qDebug() << "Found the following valid Java installations:";
+ for(JavaCheckResult result : results)
+ {
+ if(result.validity == JavaCheckResult::Validity::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/launcher/java/JavaInstallList.h b/launcher/java/JavaInstallList.h
new file mode 100644
index 00000000..e5c72b17
--- /dev/null
+++ b/launcher/java/JavaInstallList.h
@@ -0,0 +1,81 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QAbstractListModel>
+
+#include "BaseVersionList.h"
+#include "tasks/Task.h"
+
+#include "JavaCheckerJob.h"
+#include "JavaInstall.h"
+
+#include "QObjectPtr.h"
+
+class JavaListLoadTask;
+
+class JavaInstallList : public BaseVersionList
+{
+ Q_OBJECT
+ enum class Status
+ {
+ NotDone,
+ InProgress,
+ Done
+ };
+public:
+ explicit JavaInstallList(QObject *parent = 0);
+
+ shared_qobject_ptr<Task> getLoadTask() override;
+ bool isLoaded() override;
+ const BaseVersionPtr at(int i) const override;
+ int count() const override;
+ void sortVersions() override;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ RoleList providesRoles() const override;
+
+public slots:
+ void updateListData(QList<BaseVersionPtr> versions) override;
+
+protected:
+ void load();
+ shared_qobject_ptr<Task> getCurrentTask();
+
+protected:
+ Status m_status = Status::NotDone;
+ shared_qobject_ptr<JavaListLoadTask> m_loadTask;
+ QList<BaseVersionPtr> m_vlist;
+};
+
+class JavaListLoadTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit JavaListLoadTask(JavaInstallList *vlist);
+ virtual ~JavaListLoadTask();
+
+ void executeTask() override;
+public slots:
+ void javaCheckerFinished();
+
+protected:
+ shared_qobject_ptr<JavaCheckerJob> m_job;
+ JavaInstallList *m_list;
+ JavaInstall *m_currentRecommended;
+};
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
new file mode 100644
index 00000000..4b231e6a
--- /dev/null
+++ b/launcher/java/JavaUtils.cpp
@@ -0,0 +1,399 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QStringList>
+#include <QString>
+#include <QDir>
+#include <QStringList>
+
+#include <settings/Setting.h>
+
+#include <QDebug>
+#include "java/JavaUtils.h"
+#include "java/JavaInstallList.h"
+#include "FileSystem.h"
+
+#define IBUS "@im=ibus"
+
+JavaUtils::JavaUtils()
+{
+}
+
+#ifdef Q_OS_LINUX
+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(':');
+}
+#endif
+
+QProcessEnvironment CleanEnviroment()
+{
+ // 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
+
+ return env;
+}
+
+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";
+#if defined(Q_OS_WIN32)
+ javaVersion->path = "javaw";
+#else
+ javaVersion->path = "java";
+#endif
+
+ return javaVersion;
+}
+
+#if defined(Q_OS_WIN32)
+QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix)
+{
+ 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);
+ }
+
+ 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 (DWORD 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 + subkeySuffix;
+
+ 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, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value,
+ &valueSz) == ERROR_MORE_DATA)
+ {
+ value = new char[valueSz];
+ RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), 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;
+
+ // Oracle
+ QList<JavaInstallPtr> JRE64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome");
+ QList<JavaInstallPtr> JDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome");
+ QList<JavaInstallPtr> JRE32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome");
+ QList<JavaInstallPtr> JDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome");
+
+ // Oracle for Java 9 and newer
+ QList<JavaInstallPtr> NEWJRE64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome");
+ QList<JavaInstallPtr> NEWJDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome");
+ QList<JavaInstallPtr> NEWJRE32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome");
+ QList<JavaInstallPtr> NEWJDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome");
+
+ // AdoptOpenJDK
+ QList<JavaInstallPtr> ADOPTOPENJRE32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTOPENJRE64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTOPENJDK32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI");
+ QList<JavaInstallPtr> ADOPTOPENJDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI");
+
+ // Microsoft
+ QList<JavaInstallPtr> MICROSOFTJDK64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI");
+
+ // Azul Zulu
+ QList<JavaInstallPtr> ZULU64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath");
+ QList<JavaInstallPtr> ZULU32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath");
+
+ // BellSoft Liberica
+ QList<JavaInstallPtr> LIBERICA64s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath");
+ QList<JavaInstallPtr> LIBERICA32s = this->FindJavaFromRegistryKey(
+ KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath");
+
+ // List x64 before x86
+ java_candidates.append(JRE64s);
+ java_candidates.append(NEWJRE64s);
+ java_candidates.append(ADOPTOPENJRE64s);
+ java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe"));
+ java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe"));
+ java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe"));
+ java_candidates.append(JDK64s);
+ java_candidates.append(NEWJDK64s);
+ java_candidates.append(ADOPTOPENJDK64s);
+ java_candidates.append(MICROSOFTJDK64s);
+ java_candidates.append(ZULU64s);
+ java_candidates.append(LIBERICA64s);
+
+ java_candidates.append(JRE32s);
+ java_candidates.append(NEWJRE32s);
+ java_candidates.append(ADOPTOPENJRE32s);
+ java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe"));
+ java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe"));
+ java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe"));
+ java_candidates.append(JDK32s);
+ java_candidates.append(NEWJDK32s);
+ java_candidates.append(ADOPTOPENJDK32s);
+ java_candidates.append(ZULU32s);
+ java_candidates.append(LIBERICA32s);
+
+ java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path));
+
+ QList<QString> candidates;
+ 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);
+ auto scanJavaDir = [&](const QString & dirPath)
+ {
+ QDir dir(dirPath);
+ if(!dir.exists())
+ return;
+ auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
+ for(auto & entry: entries)
+ {
+
+ QString prefix;
+ if(entry.isAbsolute())
+ {
+ prefix = entry.absoluteFilePath();
+ }
+ else
+ {
+ prefix = entry.filePath();
+ }
+
+ javas.append(FS::PathCombine(prefix, "jre/bin/java"));
+ javas.append(FS::PathCombine(prefix, "bin/java"));
+ }
+ };
+ // oracle RPMs
+ scanJavaDir("/usr/java");
+ // general locations used by distro packaging
+ scanJavaDir("/usr/lib/jvm");
+ scanJavaDir("/usr/lib32/jvm");
+ // javas stored in MultiMC's folder
+ scanJavaDir("java");
+ // manually installed JDKs in /opt
+ scanJavaDir("/opt/jdk");
+ scanJavaDir("/opt/jdks");
+ return javas;
+}
+#else
+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/launcher/java/JavaUtils.h b/launcher/java/JavaUtils.h
new file mode 100644
index 00000000..3152d143
--- /dev/null
+++ b/launcher/java/JavaUtils.h
@@ -0,0 +1,42 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QStringList>
+
+#include "JavaChecker.h"
+#include "JavaInstallList.h"
+
+#ifdef Q_OS_WIN
+#include <windows.h>
+#endif
+
+QProcessEnvironment CleanEnviroment();
+
+class 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, QString keyJavaDir, QString subkeySuffix = "");
+#endif
+};
diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp
new file mode 100644
index 00000000..179ccd8d
--- /dev/null
+++ b/launcher/java/JavaVersion.cpp
@@ -0,0 +1,121 @@
+#include "JavaVersion.h"
+#include <MMCStrings.h>
+
+#include <QRegularExpression>
+#include <QString>
+
+JavaVersion & JavaVersion::operator=(const QString & javaVersionString)
+{
+ m_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(m_string);
+ m_parseable = match.hasMatch();
+ m_major = getCapturedInteger(match, "major");
+ m_minor = getCapturedInteger(match, "minor");
+ m_security = getCapturedInteger(match, "security");
+ m_prerelease = match.captured("prerelease");
+ return *this;
+}
+
+JavaVersion::JavaVersion(const QString &rhs)
+{
+ operator=(rhs);
+}
+
+QString JavaVersion::toString()
+{
+ return m_string;
+}
+
+bool JavaVersion::requiresPermGen()
+{
+ if(m_parseable)
+ {
+ return m_major < 8;
+ }
+ return true;
+}
+
+bool JavaVersion::operator<(const JavaVersion &rhs)
+{
+ if(m_parseable && rhs.m_parseable)
+ {
+ auto major = m_major;
+ auto rmajor = rhs.m_major;
+
+ // HACK: discourage using java 9
+ if(major > 8)
+ major = -major;
+ if(rmajor > 8)
+ rmajor = -rmajor;
+
+ if(major < rmajor)
+ return true;
+ if(major > rmajor)
+ return false;
+ if(m_minor < rhs.m_minor)
+ return true;
+ if(m_minor > rhs.m_minor)
+ return false;
+ if(m_security < rhs.m_security)
+ return true;
+ if(m_security > rhs.m_security)
+ return false;
+
+ // everything else being equal, consider prerelease status
+ bool thisPre = !m_prerelease.isEmpty();
+ bool rhsPre = !rhs.m_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(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0;
+ }
+ // neither is prerelease, so they are the same -> this cannot be less than rhs
+ return false;
+ }
+ else return Strings::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0;
+}
+
+bool JavaVersion::operator==(const JavaVersion &rhs)
+{
+ if(m_parseable && rhs.m_parseable)
+ {
+ return m_major == rhs.m_major && m_minor == rhs.m_minor && m_security == rhs.m_security && m_prerelease == rhs.m_prerelease;
+ }
+ return m_string == rhs.m_string;
+}
+
+bool JavaVersion::operator>(const JavaVersion &rhs)
+{
+ return (!operator<(rhs)) && (!operator==(rhs));
+}
diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h
new file mode 100644
index 00000000..9bbf0642
--- /dev/null
+++ b/launcher/java/JavaVersion.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <QString>
+
+// NOTE: apparently the GNU C library pollutes the global namespace with these... undef them.
+#ifdef major
+ #undef major
+#endif
+#ifdef minor
+ #undef minor
+#endif
+
+class 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();
+
+ int major()
+ {
+ return m_major;
+ }
+ int minor()
+ {
+ return m_minor;
+ }
+ int security()
+ {
+ return m_security;
+ }
+private:
+ QString m_string;
+ int m_major = 0;
+ int m_minor = 0;
+ int m_security = 0;
+ bool m_parseable = false;
+ QString m_prerelease;
+};
diff --git a/launcher/java/JavaVersion_test.cpp b/launcher/java/JavaVersion_test.cpp
new file mode 100644
index 00000000..10ae13a7
--- /dev/null
+++ b/launcher/java/JavaVersion_test.cpp
@@ -0,0 +1,116 @@
+#include <QTest>
+#include "TestUtil.h"
+
+#include "java/JavaVersion.h"
+
+class JavaVersionTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void test_Parse_data()
+ {
+ QTest::addColumn<QString>("string");
+ QTest::addColumn<int>("major");
+ QTest::addColumn<int>("minor");
+ QTest::addColumn<int>("security");
+ QTest::addColumn<QString>("prerelease");
+
+ QTest::newRow("old format") << "1.6.0_33" << 6 << 0 << 33 << QString();
+ QTest::newRow("old format prerelease") << "1.9.0_1-ea" << 9 << 0 << 1 << "ea";
+
+ QTest::newRow("new format major") << "9" << 9 << 0 << 0 << QString();
+ QTest::newRow("new format minor") << "9.1" << 9 << 1 << 0 << QString();
+ QTest::newRow("new format security") << "9.0.1" << 9 << 0 << 1 << QString();
+ QTest::newRow("new format prerelease") << "9-ea" << 9 << 0 << 0 << "ea";
+ QTest::newRow("new format long prerelease") << "9.0.1-ea" << 9 << 0 << 1 << "ea";
+ }
+ void test_Parse()
+ {
+ QFETCH(QString, string);
+ QFETCH(int, major);
+ QFETCH(int, minor);
+ QFETCH(int, security);
+ QFETCH(QString, prerelease);
+
+ JavaVersion test(string);
+ QCOMPARE(test.m_string, string);
+ QCOMPARE(test.toString(), string);
+ QCOMPARE(test.m_major, major);
+ QCOMPARE(test.m_minor, minor);
+ QCOMPARE(test.m_security, security);
+ QCOMPARE(test.m_prerelease, prerelease);
+ }
+
+ void test_Sort_data()
+ {
+ QTest::addColumn<QString>("lhs");
+ QTest::addColumn<QString>("rhs");
+ QTest::addColumn<bool>("smaller");
+ QTest::addColumn<bool>("equal");
+ QTest::addColumn<bool>("bigger");
+
+ // old format and new format equivalence
+ QTest::newRow("1.6.0_33 == 6.0.33") << "1.6.0_33" << "6.0.33" << false << true << false;
+ // old format major version
+ QTest::newRow("1.5.0_33 < 1.6.0_33") << "1.5.0_33" << "1.6.0_33" << true << false << false;
+ // new format - first release vs first security patch
+ QTest::newRow("9 < 9.0.1") << "9" << "9.0.1" << true << false << false;
+ QTest::newRow("9.0.1 > 9") << "9.0.1" << "9" << false << false << true;
+ // new format - first minor vs first release/security patch
+ QTest::newRow("9.1 > 9.0.1") << "9.1" << "9.0.1" << false << false << true;
+ QTest::newRow("9.0.1 < 9.1") << "9.0.1" << "9.1" << true << false << false;
+ QTest::newRow("9.1 > 9") << "9.1" << "9" << false << false << true;
+ QTest::newRow("9 > 9.1") << "9" << "9.1" << true << false << false;
+ // new format - omitted numbers
+ QTest::newRow("9 == 9.0") << "9" << "9.0" << false << true << false;
+ QTest::newRow("9 == 9.0.0") << "9" << "9.0.0" << false << true << false;
+ QTest::newRow("9.0 == 9.0.0") << "9.0" << "9.0.0" << false << true << false;
+ // early access and prereleases compared to final release
+ QTest::newRow("9-ea < 9") << "9-ea" << "9" << true << false << false;
+ QTest::newRow("9 < 9.0.1-ea") << "9" << "9.0.1-ea" << true << false << false;
+ QTest::newRow("9.0.1-ea > 9") << "9.0.1-ea" << "9" << false << false << true;
+ // prerelease difference only testing
+ QTest::newRow("9-1 == 9-1") << "9-1" << "9-1" << false << true << false;
+ QTest::newRow("9-1 < 9-2") << "9-1" << "9-2" << true << false << false;
+ QTest::newRow("9-5 < 9-20") << "9-5" << "9-20" << true << false << false;
+ QTest::newRow("9-rc1 < 9-rc2") << "9-rc1" << "9-rc2" << true << false << false;
+ QTest::newRow("9-rc5 < 9-rc20") << "9-rc5" << "9-rc20" << true << false << false;
+ QTest::newRow("9-rc < 9-rc2") << "9-rc" << "9-rc2" << true << false << false;
+ QTest::newRow("9-ea < 9-rc") << "9-ea" << "9-rc" << true << false << false;
+ }
+ void test_Sort()
+ {
+ QFETCH(QString, lhs);
+ QFETCH(QString, rhs);
+ QFETCH(bool, smaller);
+ QFETCH(bool, equal);
+ QFETCH(bool, bigger);
+ JavaVersion lver(lhs);
+ JavaVersion rver(rhs);
+ QCOMPARE(lver < rver, smaller);
+ QCOMPARE(lver == rver, equal);
+ QCOMPARE(lver > rver, bigger);
+ }
+ void test_PermGen_data()
+ {
+ QTest::addColumn<QString>("version");
+ QTest::addColumn<bool>("needs_permgen");
+ QTest::newRow("1.6.0_33") << "1.6.0_33" << true;
+ QTest::newRow("1.7.0_60") << "1.7.0_60" << true;
+ QTest::newRow("1.8.0_22") << "1.8.0_22" << false;
+ QTest::newRow("9-ea") << "9-ea" << false;
+ QTest::newRow("9.2.4") << "9.2.4" << false;
+ }
+ void test_PermGen()
+ {
+ QFETCH(QString, version);
+ QFETCH(bool, needs_permgen);
+ JavaVersion v(version);
+ QCOMPARE(needs_permgen, v.requiresPermGen());
+ }
+};
+
+QTEST_GUILESS_MAIN(JavaVersionTest)
+
+#include "JavaVersion_test.moc"
diff --git a/launcher/java/launch/CheckJava.cpp b/launcher/java/launch/CheckJava.cpp
new file mode 100644
index 00000000..f58602f0
--- /dev/null
+++ b/launcher/java/launch/CheckJava.cpp
@@ -0,0 +1,139 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "CheckJava.h"
+#include <launch/LaunchTask.h>
+#include <FileSystem.h>
+#include <QStandardPaths>
+#include <QFileInfo>
+#include <sys.h>
+
+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(
+ QString("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(QString("The java binary \"%1\" couldn't be found. Please set up java in "
+ "the settings.").arg(m_javaPath),
+ MessageLevel::Warning);
+ }
+ emitFailed(QString("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();
+ auto storedArchitecture = settings->get("JavaArchitecture").toString();
+ auto storedVersion = settings->get("JavaVersion").toString();
+ auto storedVendor = settings->get("JavaVendor").toString();
+ m_javaUnixTime = javaUnixTime;
+ // if timestamps are not the same, or something is missing, check!
+ if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedVendor.size() == 0)
+ {
+ m_JavaChecker = new JavaChecker();
+ emit logLine(QString("Checking Java version..."), MessageLevel::MultiMC);
+ connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished);
+ m_JavaChecker->m_path = realJavaPath;
+ m_JavaChecker->performCheck();
+ return;
+ }
+ else
+ {
+ auto verString = instance->settings()->get("JavaVersion").toString();
+ auto archString = instance->settings()->get("JavaArchitecture").toString();
+ auto vendorString = instance->settings()->get("JavaVendor").toString();
+ printJavaInfo(verString, archString, vendorString);
+ }
+ emitSucceeded();
+}
+
+void CheckJava::checkJavaFinished(JavaCheckResult result)
+{
+ switch (result.validity)
+ {
+ case JavaCheckResult::Validity::Errored:
+ {
+ // Error message displayed if java can't start
+ emit logLine(QString("Could not start java:"), MessageLevel::Error);
+ emit logLines(result.errorLog.split('\n'), MessageLevel::Error);
+ emit logLine("\nCheck your MultiMC Java settings.", MessageLevel::MultiMC);
+ printSystemInfo(false, false);
+ emitFailed(QString("Could not start java!"));
+ return;
+ }
+ case JavaCheckResult::Validity::ReturnedInvalidData:
+ {
+ emit logLine(QString("Java checker returned some invalid data MultiMC doesn't understand:"), MessageLevel::Error);
+ emit logLines(result.outLog.split('\n'), MessageLevel::Warning);
+ emit logLine("\nMinecraft might not start properly.", MessageLevel::MultiMC);
+ printSystemInfo(false, false);
+ emitSucceeded();
+ return;
+ }
+ case JavaCheckResult::Validity::Valid:
+ {
+ auto instance = m_parent->instance();
+ printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.javaVendor);
+ instance->settings()->set("JavaVersion", result.javaVersion.toString());
+ instance->settings()->set("JavaArchitecture", result.mojangPlatform);
+ instance->settings()->set("JavaVendor", result.javaVendor);
+ instance->settings()->set("JavaTimestamp", m_javaUnixTime);
+ emitSucceeded();
+ return;
+ }
+ }
+}
+
+void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString & vendor)
+{
+ emit logLine(QString("Java is version %1, using %2-bit architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::MultiMC);
+ printSystemInfo(true, architecture == "64");
+}
+
+void CheckJava::printSystemInfo(bool javaIsKnown, bool javaIs64bit)
+{
+ auto cpu64 = Sys::isCPU64bit();
+ auto system64 = Sys::isSystem64bit();
+ if(cpu64 != system64)
+ {
+ emit logLine(QString("Your CPU architecture is not matching your system architecture. You might want to install a 64bit Operating System.\n\n"), MessageLevel::Error);
+ }
+ if(javaIsKnown)
+ {
+ if(javaIs64bit != system64)
+ {
+ emit logLine(QString("Your Java architecture is not matching your system architecture. You might want to install a 64bit Java version.\n\n"), MessageLevel::Error);
+ }
+ }
+}
diff --git a/launcher/java/launch/CheckJava.h b/launcher/java/launch/CheckJava.h
new file mode 100644
index 00000000..68cd618b
--- /dev/null
+++ b/launcher/java/launch/CheckJava.h
@@ -0,0 +1,45 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <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:
+ void printJavaInfo(const QString & version, const QString & architecture, const QString & vendor);
+ void printSystemInfo(bool javaIsKnown, bool javaIs64bit);
+
+private:
+ QString m_javaPath;
+ qlonglong m_javaUnixTime;
+ JavaCheckerPtr m_JavaChecker;
+};
diff --git a/launcher/launch/LaunchStep.cpp b/launcher/launch/LaunchStep.cpp
new file mode 100644
index 00000000..d6bb6e88
--- /dev/null
+++ b/launcher/launch/LaunchStep.cpp
@@ -0,0 +1,27 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/launch/LaunchStep.h b/launcher/launch/LaunchStep.h
new file mode 100644
index 00000000..3939f960
--- /dev/null
+++ b/launcher/launch/LaunchStep.h
@@ -0,0 +1,50 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "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() {};
+
+private: /* methods */
+ 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() {};
+ // called in the opposite order than the Task launch(), used to clean up or otherwise undo things after the launch ends
+ virtual void finalize() {};
+
+protected: /* data */
+ LaunchTask *m_parent;
+};
diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp
new file mode 100644
index 00000000..e6f6bbac
--- /dev/null
+++ b/launcher/launch/LaunchTask.cpp
@@ -0,0 +1,280 @@
+/* Copyright 2013-2021 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);
+}
+
+shared_qobject_ptr<LaunchTask> LaunchTask::create(InstancePtr inst)
+{
+ shared_qobject_ptr<LaunchTask> proc(new LaunchTask(inst));
+ proc->init();
+ return proc;
+}
+
+LaunchTask::LaunchTask(InstancePtr instance): m_instance(instance)
+{
+}
+
+void LaunchTask::appendStep(shared_qobject_ptr<LaunchStep> step)
+{
+ m_steps.append(step);
+}
+
+void LaunchTask::prependStep(shared_qobject_ptr<LaunchStep> step)
+{
+ m_steps.prepend(step);
+}
+
+void LaunchTask::executeTask()
+{
+ m_instance->setCrashed(false);
+ 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->wasSuccessful())
+ {
+ // end?
+ if(currentStep == m_steps.size() - 1)
+ {
+ finalizeSteps(true, QString());
+ }
+ else
+ {
+ currentStep ++;
+ step = m_steps[currentStep];
+ step->start();
+ }
+ }
+ else
+ {
+ finalizeSteps(false, step->failReason());
+ }
+}
+
+void LaunchTask::finalizeSteps(bool successful, const QString& error)
+{
+ for(auto step = currentStep; step >= 0; step--)
+ {
+ m_steps[step]->finalize();
+ }
+ if(successful)
+ {
+ emitSucceeded();
+ }
+ else
+ {
+ emitFailed(error);
+ }
+}
+
+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::canAbort() const
+{
+ switch(state)
+ {
+ case LaunchTask::Aborted:
+ case LaunchTask::Failed:
+ case LaunchTask::Finished:
+ return false;
+ case LaunchTask::NotStarted:
+ return true;
+ case LaunchTask::Running:
+ case LaunchTask::Waiting:
+ {
+ auto step = m_steps[currentStep];
+ return step->canAbort();
+ }
+ }
+ return false;
+}
+
+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;
+}
+
+shared_qobject_ptr<LogModel> LaunchTask::getLogModel()
+{
+ if(!m_logModel)
+ {
+ m_logModel.reset(new LogModel());
+ m_logModel->setMaxLines(m_instance->getConsoleMaxLines());
+ m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow());
+ // FIXME: should this really be here?
+ m_logModel->setOverflowMessage(tr("MultiMC stopped watching the game log because the log length surpassed %1 lines.\n"
+ "You may have to fix your mods because the game is still logging to files and"
+ " likely wasting harddrive space at an alarming rate!").arg(m_logModel->getMaxLines()));
+ }
+ return m_logModel;
+}
+
+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);
+
+ auto &model = *getLogModel();
+ model.append(level, line);
+}
+
+void LaunchTask::emitSucceeded()
+{
+ m_instance->setRunning(false);
+ Task::emitSucceeded();
+}
+
+void LaunchTask::emitFailed(QString reason)
+{
+ m_instance->setRunning(false);
+ m_instance->setCrashed(true);
+ 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/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h
new file mode 100644
index 00000000..ae81462f
--- /dev/null
+++ b/launcher/launch/LaunchTask.h
@@ -0,0 +1,123 @@
+/* Copyright 2013-2021 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 <QObjectPtr.h>
+#include "LogModel.h"
+#include "BaseInstance.h"
+#include "MessageLevel.h"
+#include "LoggedProcess.h"
+#include "LaunchStep.h"
+
+class LaunchTask: public Task
+{
+ Q_OBJECT
+protected:
+ explicit LaunchTask(InstancePtr instance);
+ void init();
+
+public:
+ enum State
+ {
+ NotStarted,
+ Running,
+ Waiting,
+ Failed,
+ Aborted,
+ Finished
+ };
+
+public: /* methods */
+ static shared_qobject_ptr<LaunchTask> create(InstancePtr inst);
+ virtual ~LaunchTask() {};
+
+ void appendStep(shared_qobject_ptr<LaunchStep> step);
+ void prependStep(shared_qobject_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
+ */
+ bool abort() override;
+
+ bool canAbort() const override;
+
+ shared_qobject_ptr<LogModel> getLogModel();
+
+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();
+
+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();
+
+private: /*methods */
+ void finalizeSteps(bool successful, const QString & error);
+
+protected: /* data */
+ InstancePtr m_instance;
+ shared_qobject_ptr<LogModel> m_logModel;
+ QList <shared_qobject_ptr<LaunchStep>> m_steps;
+ QMap<QString, QString> m_censorFilter;
+ int currentStep = -1;
+ State state = NotStarted;
+ qint64 m_pid = -1;
+};
diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp
new file mode 100644
index 00000000..92f9487a
--- /dev/null
+++ b/launcher/launch/LogModel.cpp
@@ -0,0 +1,167 @@
+#include "LogModel.h"
+
+LogModel::LogModel(QObject *parent):QAbstractListModel(parent)
+{
+ m_content.resize(m_maxLines);
+}
+
+int LogModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return 0;
+
+ return m_numLines;
+}
+
+QVariant LogModel::data(const QModelIndex &index, int role) const
+{
+ if (index.row() < 0 || index.row() >= m_numLines)
+ return QVariant();
+
+ auto row = index.row();
+ auto realRow = (row + m_firstLine) % m_maxLines;
+ if (role == Qt::DisplayRole || role == Qt::EditRole)
+ {
+ return m_content[realRow].line;
+ }
+ if(role == LevelRole)
+ {
+ return m_content[realRow].level;
+ }
+
+ return QVariant();
+}
+
+void LogModel::append(MessageLevel::Enum level, QString line)
+{
+ if(m_suspended)
+ {
+ return;
+ }
+ int lineNum = (m_firstLine + m_numLines) % m_maxLines;
+ // overflow
+ if(m_numLines == m_maxLines)
+ {
+ if(m_stopOnOverflow)
+ {
+ // nothing more to do, the buffer is full
+ return;
+ }
+ beginRemoveRows(QModelIndex(), 0, 0);
+ m_firstLine = (m_firstLine + 1) % m_maxLines;
+ m_numLines --;
+ endRemoveRows();
+ }
+ else if (m_numLines == m_maxLines - 1 && m_stopOnOverflow)
+ {
+ level = MessageLevel::Fatal;
+ line = m_overflowMessage;
+ }
+ beginInsertRows(QModelIndex(), m_numLines, m_numLines);
+ m_numLines ++;
+ m_content[lineNum].level = level;
+ m_content[lineNum].line = line;
+ endInsertRows();
+}
+
+void LogModel::suspend(bool suspend)
+{
+ m_suspended = suspend;
+}
+
+bool LogModel::suspended()
+{
+ return m_suspended;
+}
+
+void LogModel::clear()
+{
+ beginResetModel();
+ m_firstLine = 0;
+ m_numLines = 0;
+ endResetModel();
+}
+
+QString LogModel::toPlainText()
+{
+ QString out;
+ out.reserve(m_numLines * 80);
+ for(int i = 0; i < m_numLines; i++)
+ {
+ QString & line = m_content[(m_firstLine + i) % m_maxLines].line;
+ out.append(line + '\n');
+ }
+ out.squeeze();
+ return out;
+}
+
+void LogModel::setMaxLines(int maxLines)
+{
+ // no-op
+ if(maxLines == m_maxLines)
+ {
+ return;
+ }
+ // if it all still fits in the buffer, just resize it
+ if(m_firstLine + m_numLines < m_maxLines)
+ {
+ m_maxLines = maxLines;
+ m_content.resize(maxLines);
+ return;
+ }
+ // otherwise, we need to reorganize the data because it crosses the wrap boundary
+ QVector<entry> newContent;
+ newContent.resize(maxLines);
+ if(m_numLines <= maxLines)
+ {
+ // if it all fits in the new buffer, just copy it over
+ for(int i = 0; i < m_numLines; i++)
+ {
+ newContent[i] = m_content[(m_firstLine + i) % m_maxLines];
+ }
+ m_content.swap(newContent);
+ }
+ else
+ {
+ // if it doesn't fit, part of the data needs to be thrown away (the oldest log messages)
+ int lead = m_numLines - maxLines;
+ beginRemoveRows(QModelIndex(), 0, lead - 1);
+ for(int i = 0; i < maxLines; i++)
+ {
+ newContent[i] = m_content[(m_firstLine + lead + i) % m_maxLines];
+ }
+ m_numLines = m_maxLines;
+ m_content.swap(newContent);
+ endRemoveRows();
+ }
+ m_firstLine = 0;
+ m_maxLines = maxLines;
+}
+
+int LogModel::getMaxLines()
+{
+ return m_maxLines;
+}
+
+void LogModel::setStopOnOverflow(bool stop)
+{
+ m_stopOnOverflow = stop;
+}
+
+void LogModel::setOverflowMessage(const QString& overflowMessage)
+{
+ m_overflowMessage = overflowMessage;
+}
+
+void LogModel::setLineWrap(bool state)
+{
+ if(m_lineWrap != state)
+ {
+ m_lineWrap = state;
+ }
+}
+
+bool LogModel::wrapLines() const
+{
+ return m_lineWrap;
+}
diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h
new file mode 100644
index 00000000..6aabc823
--- /dev/null
+++ b/launcher/launch/LogModel.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QString>
+#include "MessageLevel.h"
+
+class LogModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ explicit LogModel(QObject *parent = 0);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const;
+ QVariant data(const QModelIndex &index, int role) const;
+
+ void append(MessageLevel::Enum, QString line);
+ void clear();
+
+ void suspend(bool suspend);
+ bool suspended();
+
+ QString toPlainText();
+
+ int getMaxLines();
+ void setMaxLines(int maxLines);
+ void setStopOnOverflow(bool stop);
+ void setOverflowMessage(const QString & overflowMessage);
+
+ void setLineWrap(bool state);
+ bool wrapLines() const;
+
+ enum Roles
+ {
+ LevelRole = Qt::UserRole
+ };
+
+private /* types */:
+ struct entry
+ {
+ MessageLevel::Enum level;
+ QString line;
+ };
+
+private: /* data */
+ QVector <entry> m_content;
+ int m_maxLines = 1000;
+ // first line in the circular buffer
+ int m_firstLine = 0;
+ // number of lines occupied in the circular buffer
+ int m_numLines = 0;
+ bool m_stopOnOverflow = false;
+ QString m_overflowMessage = "OVERFLOW";
+ bool m_suspended = false;
+ bool m_lineWrap = true;
+
+private:
+ Q_DISABLE_COPY(LogModel)
+};
diff --git a/launcher/launch/steps/LookupServerAddress.cpp b/launcher/launch/steps/LookupServerAddress.cpp
new file mode 100644
index 00000000..de56c28a
--- /dev/null
+++ b/launcher/launch/steps/LookupServerAddress.cpp
@@ -0,0 +1,95 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#include "LookupServerAddress.h"
+
+#include <launch/LaunchTask.h>
+
+LookupServerAddress::LookupServerAddress(LaunchTask *parent) :
+ LaunchStep(parent), m_dnsLookup(new QDnsLookup(this))
+{
+ connect(m_dnsLookup, &QDnsLookup::finished, this, &LookupServerAddress::on_dnsLookupFinished);
+
+ m_dnsLookup->setType(QDnsLookup::SRV);
+}
+
+void LookupServerAddress::setLookupAddress(const QString &lookupAddress)
+{
+ m_lookupAddress = lookupAddress;
+ m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress));
+}
+
+void LookupServerAddress::setOutputAddressPtr(MinecraftServerTargetPtr output)
+{
+ m_output = std::move(output);
+}
+
+bool LookupServerAddress::abort()
+{
+ m_dnsLookup->abort();
+ emitFailed("Aborted");
+ return true;
+}
+
+void LookupServerAddress::executeTask()
+{
+ m_dnsLookup->lookup();
+}
+
+void LookupServerAddress::on_dnsLookupFinished()
+{
+ if (isFinished())
+ {
+ // Aborted
+ return;
+ }
+
+ if (m_dnsLookup->error() != QDnsLookup::NoError)
+ {
+ emit logLine(QString("Failed to resolve server address (this is NOT an error!) %1: %2\n")
+ .arg(m_dnsLookup->name(), m_dnsLookup->errorString()), MessageLevel::MultiMC);
+ resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch
+ // and leave it up to minecraft to fail (or maybe not) when connecting
+ return;
+ }
+
+ const auto records = m_dnsLookup->serviceRecords();
+ if (records.empty())
+ {
+ emit logLine(
+ QString("Failed to resolve server address %1: the DNS lookup succeeded, but no records were returned.\n")
+ .arg(m_dnsLookup->name()), MessageLevel::Warning);
+ resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch
+ // and leave it up to minecraft to fail (or maybe not) when connecting
+ return;
+ }
+
+ const auto &firstRecord = records.at(0);
+ quint16 port = firstRecord.port();
+
+ emit logLine(QString("Resolved server address %1 to %2 with port %3\n").arg(
+ m_dnsLookup->name(), firstRecord.target(), QString::number(port)),MessageLevel::MultiMC);
+ resolve(firstRecord.target(), port);
+}
+
+void LookupServerAddress::resolve(const QString &address, quint16 port)
+{
+ m_output->address = address;
+ m_output->port = port;
+
+ emitSucceeded();
+ m_dnsLookup->deleteLater();
+}
diff --git a/launcher/launch/steps/LookupServerAddress.h b/launcher/launch/steps/LookupServerAddress.h
new file mode 100644
index 00000000..5a5c3de1
--- /dev/null
+++ b/launcher/launch/steps/LookupServerAddress.h
@@ -0,0 +1,49 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <QObjectPtr.h>
+#include <QDnsLookup>
+
+#include "minecraft/launch/MinecraftServerTarget.h"
+
+class LookupServerAddress: public LaunchStep {
+Q_OBJECT
+public:
+ explicit LookupServerAddress(LaunchTask *parent);
+ virtual ~LookupServerAddress() {};
+
+ virtual void executeTask();
+ virtual bool abort();
+ virtual bool canAbort() const
+ {
+ return true;
+ }
+
+ void setLookupAddress(const QString &lookupAddress);
+ void setOutputAddressPtr(MinecraftServerTargetPtr output);
+
+private slots:
+ void on_dnsLookupFinished();
+
+private:
+ void resolve(const QString &address, quint16 port);
+
+ QDnsLookup *m_dnsLookup;
+ QString m_lookupAddress;
+ MinecraftServerTargetPtr m_output;
+};
diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp
new file mode 100644
index 00000000..d48d03d1
--- /dev/null
+++ b/launcher/launch/steps/PostLaunchCommand.cpp
@@ -0,0 +1,84 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/launch/steps/PostLaunchCommand.h b/launcher/launch/steps/PostLaunchCommand.h
new file mode 100644
index 00000000..ab4c494f
--- /dev/null
+++ b/launcher/launch/steps/PostLaunchCommand.h
@@ -0,0 +1,41 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <LoggedProcess.h>
+
+class PostLaunchCommand: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit PostLaunchCommand(LaunchTask *parent);
+ virtual ~PostLaunchCommand() {};
+
+ 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/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp
new file mode 100644
index 00000000..20e089e2
--- /dev/null
+++ b/launcher/launch/steps/PreLaunchCommand.cpp
@@ -0,0 +1,85 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/launch/steps/PreLaunchCommand.h b/launcher/launch/steps/PreLaunchCommand.h
new file mode 100644
index 00000000..dc069f71
--- /dev/null
+++ b/launcher/launch/steps/PreLaunchCommand.h
@@ -0,0 +1,41 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "launch/LaunchStep.h"
+#include "LoggedProcess.h"
+
+class PreLaunchCommand: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit PreLaunchCommand(LaunchTask *parent);
+ virtual ~PreLaunchCommand() {};
+
+ 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/launcher/launch/steps/TextPrint.cpp b/launcher/launch/steps/TextPrint.cpp
new file mode 100644
index 00000000..0c1f320c
--- /dev/null
+++ b/launcher/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/launcher/launch/steps/TextPrint.h b/launcher/launch/steps/TextPrint.h
new file mode 100644
index 00000000..36fa7f9a
--- /dev/null
+++ b/launcher/launch/steps/TextPrint.h
@@ -0,0 +1,41 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <LoggedProcess.h>
+#include <java/JavaChecker.h>
+
+/*
+ * FIXME: maybe do not export
+ */
+
+class 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/launcher/launch/steps/Update.cpp b/launcher/launch/steps/Update.cpp
new file mode 100644
index 00000000..28bd153d
--- /dev/null
+++ b/launcher/launch/steps/Update.cpp
@@ -0,0 +1,80 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Update.h"
+#include <launch/LaunchTask.h>
+
+void Update::executeTask()
+{
+ if(m_aborted)
+ {
+ emitFailed(tr("Task aborted."));
+ return;
+ }
+ m_updateTask.reset(m_parent->instance()->createUpdateTask(m_mode));
+ 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->wasSuccessful())
+ {
+ m_updateTask.reset();
+ emitSucceeded();
+ }
+ else
+ {
+ QString reason = tr("Instance update failed because: %1\n\n").arg(m_updateTask->failReason());
+ m_updateTask.reset();
+ emit logLine(reason, MessageLevel::Fatal);
+ emitFailed(reason);
+ }
+}
+
+bool Update::canAbort() const
+{
+ if(m_updateTask)
+ {
+ return m_updateTask->canAbort();
+ }
+ return true;
+}
+
+
+bool Update::abort()
+{
+ m_aborted = true;
+ if(m_updateTask)
+ {
+ if(m_updateTask->canAbort())
+ {
+ return m_updateTask->abort();
+ }
+ }
+ return true;
+}
diff --git a/launcher/launch/steps/Update.h b/launcher/launch/steps/Update.h
new file mode 100644
index 00000000..0c9d91e0
--- /dev/null
+++ b/launcher/launch/steps/Update.h
@@ -0,0 +1,45 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <QObjectPtr.h>
+#include <LoggedProcess.h>
+#include <java/JavaChecker.h>
+#include <net/Mode.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, Net::Mode mode):LaunchStep(parent), m_mode(mode) {};
+ virtual ~Update() {};
+
+ void executeTask() override;
+ bool canAbort() const override;
+ void proceed() override;
+public slots:
+ bool abort() override;
+
+private slots:
+ void updateFinished();
+
+private:
+ shared_qobject_ptr<Task> m_updateTask;
+ bool m_aborted = false;
+ Net::Mode m_mode = Net::Mode::Offline;
+};
diff --git a/launcher/main.cpp b/launcher/main.cpp
new file mode 100644
index 00000000..b0360c7e
--- /dev/null
+++ b/launcher/main.cpp
@@ -0,0 +1,61 @@
+#include "MultiMC.h"
+#include "MainWindow.h"
+#include "LaunchController.h"
+#include <InstanceList.h>
+#include <QDebug>
+
+// #define BREAK_INFINITE_LOOP
+// #define BREAK_EXCEPTION
+// #define BREAK_RETURN
+
+#ifdef BREAK_INFINITE_LOOP
+#include <thread>
+#include <chrono>
+#endif
+
+int main(int argc, char *argv[])
+{
+#ifdef BREAK_INFINITE_LOOP
+ while(true)
+ {
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
+ }
+#endif
+#ifdef BREAK_EXCEPTION
+ throw 42;
+#endif
+#ifdef BREAK_RETURN
+ return 42;
+#endif
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
+ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
+#endif
+
+ // initialize Qt
+ MultiMC app(argc, argv);
+
+ switch (app.status())
+ {
+ case MultiMC::StartingUp:
+ case MultiMC::Initialized:
+ {
+ Q_INIT_RESOURCE(multimc);
+ Q_INIT_RESOURCE(backgrounds);
+
+ Q_INIT_RESOURCE(pe_dark);
+ Q_INIT_RESOURCE(pe_light);
+ Q_INIT_RESOURCE(pe_blue);
+ Q_INIT_RESOURCE(pe_colored);
+ Q_INIT_RESOURCE(OSX);
+ Q_INIT_RESOURCE(iOS);
+ Q_INIT_RESOURCE(flat);
+ return app.exec();
+ }
+ case MultiMC::Failed:
+ return 1;
+ case MultiMC::Succeeded:
+ return 0;
+ }
+}
diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp
new file mode 100644
index 00000000..5ff7a59a
--- /dev/null
+++ b/launcher/meta/BaseEntity.cpp
@@ -0,0 +1,168 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "BaseEntity.h"
+
+#include "Json.h"
+
+#include "net/Download.h"
+#include "net/HttpMetaCache.h"
+#include "net/NetJob.h"
+
+#include "Env.h"
+#include "Json.h"
+
+#include "BuildConfig.h"
+
+class ParsingValidator : public Net::Validator
+{
+public: /* con/des */
+ ParsingValidator(Meta::BaseEntity *entity) : m_entity(entity)
+ {
+ };
+ virtual ~ParsingValidator()
+ {
+ };
+
+public: /* methods */
+ bool init(QNetworkRequest &) override
+ {
+ return true;
+ }
+ bool write(QByteArray & data) override
+ {
+ this->data.append(data);
+ return true;
+ }
+ bool abort() override
+ {
+ return true;
+ }
+ bool validate(QNetworkReply &) override
+ {
+ auto fname = m_entity->localFilename();
+ try
+ {
+ auto doc = Json::requireDocument(data, fname);
+ auto obj = Json::requireObject(doc, fname);
+ m_entity->parse(obj);
+ return true;
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Unable to parse response:" << e.cause();
+ return false;
+ }
+ }
+
+private: /* data */
+ QByteArray data;
+ Meta::BaseEntity *m_entity;
+};
+
+Meta::BaseEntity::~BaseEntity()
+{
+}
+
+QUrl Meta::BaseEntity::url() const
+{
+ return QUrl(BuildConfig.META_URL).resolved(localFilename());
+}
+
+bool Meta::BaseEntity::loadLocalFile()
+{
+ const QString fname = QDir("meta").absoluteFilePath(localFilename());
+ if (!QFile::exists(fname))
+ {
+ return false;
+ }
+ // TODO: check if the file has the expected checksum
+ try
+ {
+ auto doc = Json::requireDocument(fname, fname);
+ auto obj = Json::requireObject(doc, fname);
+ parse(obj);
+ return true;
+ }
+ catch (const Exception &e)
+ {
+ qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause());
+ // just make sure it's gone and we never consider it again.
+ QFile::remove(fname);
+ return false;
+ }
+}
+
+void Meta::BaseEntity::load(Net::Mode loadType)
+{
+ // load local file if nothing is loaded yet
+ if(!isLoaded())
+ {
+ if(loadLocalFile())
+ {
+ m_loadStatus = LoadStatus::Local;
+ }
+ }
+ // if we need remote update, run the update task
+ if(loadType == Net::Mode::Offline || !shouldStartRemoteUpdate())
+ {
+ return;
+ }
+ NetJob *job = new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()));
+ auto url = this->url();
+ auto entry = ENV.metacache()->resolveEntry("meta", localFilename());
+ entry->setStale(true);
+ auto dl = Net::Download::makeCached(url, entry);
+ /*
+ * The validator parses the file and loads it into the object.
+ * If that fails, the file is not written to storage.
+ */
+ dl->addValidator(new ParsingValidator(this));
+ job->addNetAction(dl);
+ m_updateStatus = UpdateStatus::InProgress;
+ m_updateTask.reset(job);
+ QObject::connect(job, &NetJob::succeeded, [&]()
+ {
+ m_loadStatus = LoadStatus::Remote;
+ m_updateStatus = UpdateStatus::Succeeded;
+ m_updateTask.reset();
+ });
+ QObject::connect(job, &NetJob::failed, [&]()
+ {
+ m_updateStatus = UpdateStatus::Failed;
+ m_updateTask.reset();
+ });
+ m_updateTask->start();
+}
+
+bool Meta::BaseEntity::isLoaded() const
+{
+ return m_loadStatus > LoadStatus::NotLoaded;
+}
+
+bool Meta::BaseEntity::shouldStartRemoteUpdate() const
+{
+ // TODO: version-locks and offline mode?
+ return m_updateStatus != UpdateStatus::InProgress;
+}
+
+shared_qobject_ptr<Task> Meta::BaseEntity::getCurrentTask()
+{
+ if(m_updateStatus == UpdateStatus::InProgress)
+ {
+ return m_updateTask;
+ }
+ return nullptr;
+}
diff --git a/launcher/meta/BaseEntity.h b/launcher/meta/BaseEntity.h
new file mode 100644
index 00000000..eff43879
--- /dev/null
+++ b/launcher/meta/BaseEntity.h
@@ -0,0 +1,67 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 <QObject>
+#include "QObjectPtr.h"
+
+#include "net/Mode.h"
+
+class Task;
+namespace Meta
+{
+class BaseEntity
+{
+public: /* types */
+ using Ptr = std::shared_ptr<BaseEntity>;
+ enum class LoadStatus
+ {
+ NotLoaded,
+ Local,
+ Remote
+ };
+ enum class UpdateStatus
+ {
+ NotDone,
+ InProgress,
+ Failed,
+ Succeeded
+ };
+
+public:
+ virtual ~BaseEntity();
+
+ virtual void parse(const QJsonObject &obj) = 0;
+
+ virtual QString localFilename() const = 0;
+ virtual QUrl url() const;
+
+ bool isLoaded() const;
+ bool shouldStartRemoteUpdate() const;
+
+ void load(Net::Mode loadType);
+ shared_qobject_ptr<Task> getCurrentTask();
+
+protected: /* methods */
+ bool loadLocalFile();
+
+private:
+ LoadStatus m_loadStatus = LoadStatus::NotLoaded;
+ UpdateStatus m_updateStatus = UpdateStatus::NotDone;
+ shared_qobject_ptr<Task> m_updateTask;
+};
+}
diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp
new file mode 100644
index 00000000..6802470d
--- /dev/null
+++ b/launcher/meta/Index.cpp
@@ -0,0 +1,148 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "Index.h"
+
+#include "VersionList.h"
+#include "JsonFormat.h"
+
+namespace Meta
+{
+Index::Index(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+Index::Index(const QVector<VersionListPtr> &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 Index::data(const QModelIndex &index, int role) const
+{
+ if (index.parent().isValid() || index.row() < 0 || index.row() >= m_lists.size())
+ {
+ return QVariant();
+ }
+
+ VersionListPtr 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 Index::rowCount(const QModelIndex &parent) const
+{
+ return m_lists.size();
+}
+int Index::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0)
+ {
+ return tr("Name");
+ }
+ else
+ {
+ return QVariant();
+ }
+}
+
+bool Index::hasUid(const QString &uid) const
+{
+ return m_uids.contains(uid);
+}
+
+VersionListPtr Index::get(const QString &uid)
+{
+ VersionListPtr out = m_uids.value(uid, nullptr);
+ if(!out)
+ {
+ out = std::make_shared<VersionList>(uid);
+ m_uids[uid] = out;
+ }
+ return out;
+}
+
+VersionPtr Index::get(const QString &uid, const QString &version)
+{
+ auto list = get(uid);
+ return list->getVersion(version);
+}
+
+void Index::parse(const QJsonObject& obj)
+{
+ parseIndex(obj, this);
+}
+
+void Index::merge(const std::shared_ptr<Index> &other)
+{
+ const QVector<VersionListPtr> lists = std::dynamic_pointer_cast<Index>(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 VersionListPtr &list : lists)
+ {
+ if (m_uids.contains(list->uid()))
+ {
+ m_uids[list->uid()]->mergeFromIndex(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 Index::connectVersionList(const int row, const VersionListPtr &list)
+{
+ connect(list.get(), &VersionList::nameChanged, this, [this, row]()
+ {
+ emit dataChanged(index(row), index(row), QVector<int>() << Qt::DisplayRole);
+ });
+}
+}
diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h
new file mode 100644
index 00000000..d33ab0c8
--- /dev/null
+++ b/launcher/meta/Index.h
@@ -0,0 +1,69 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "BaseEntity.h"
+
+class Task;
+
+namespace Meta
+{
+using VersionListPtr = std::shared_ptr<class VersionList>;
+using VersionPtr = std::shared_ptr<class Version>;
+
+class Index : public QAbstractListModel, public BaseEntity
+{
+ Q_OBJECT
+public:
+ explicit Index(QObject *parent = nullptr);
+ explicit Index(const QVector<VersionListPtr> &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;
+
+ QString localFilename() const override { return "index.json"; }
+
+ // queries
+ VersionListPtr get(const QString &uid);
+ VersionPtr get(const QString &uid, const QString &version);
+ bool hasUid(const QString &uid) const;
+
+ QVector<VersionListPtr> lists() const { return m_lists; }
+
+public: // for usage by parsers only
+ void merge(const std::shared_ptr<Index> &other);
+ void parse(const QJsonObject &obj) override;
+
+private:
+ QVector<VersionListPtr> m_lists;
+ QHash<QString, VersionListPtr> m_uids;
+
+ void connectVersionList(const int row, const VersionListPtr &list);
+};
+}
+
diff --git a/launcher/meta/Index_test.cpp b/launcher/meta/Index_test.cpp
new file mode 100644
index 00000000..b0892070
--- /dev/null
+++ b/launcher/meta/Index_test.cpp
@@ -0,0 +1,44 @@
+#include <QTest>
+#include "TestUtil.h"
+
+#include "meta/Index.h"
+#include "meta/VersionList.h"
+#include "Env.h"
+
+class IndexTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void test_isProvidedByEnv()
+ {
+ QVERIFY(ENV.metadataIndex());
+ QCOMPARE(ENV.metadataIndex(), ENV.metadataIndex());
+ }
+
+ void test_hasUid_and_getList()
+ {
+ Meta::Index windex({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")});
+ QVERIFY(windex.hasUid("list1"));
+ QVERIFY(!windex.hasUid("asdf"));
+ QVERIFY(windex.get("list2") != nullptr);
+ QCOMPARE(windex.get("list2")->uid(), QString("list2"));
+ QVERIFY(windex.get("adsf") != nullptr);
+ }
+
+ void test_merge()
+ {
+ Meta::Index windex({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")});
+ QCOMPARE(windex.lists().size(), 3);
+ windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list1"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list3")})));
+ QCOMPARE(windex.lists().size(), 3);
+ windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list4"), std::make_shared<Meta::VersionList>("list2"), std::make_shared<Meta::VersionList>("list5")})));
+ QCOMPARE(windex.lists().size(), 5);
+ windex.merge(std::shared_ptr<Meta::Index>(new Meta::Index({std::make_shared<Meta::VersionList>("list6")})));
+ QCOMPARE(windex.lists().size(), 6);
+ }
+};
+
+QTEST_GUILESS_MAIN(IndexTest)
+
+#include "Index_test.moc"
diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp
new file mode 100644
index 00000000..796da4bb
--- /dev/null
+++ b/launcher/meta/JsonFormat.cpp
@@ -0,0 +1,218 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "JsonFormat.h"
+
+// FIXME: remove this from here... somehow
+#include "minecraft/OneSixVersionFormat.h"
+#include "Json.h"
+
+#include "Index.h"
+#include "Version.h"
+#include "VersionList.h"
+
+using namespace Json;
+
+namespace Meta
+{
+
+MetadataVersion currentFormatVersion()
+{
+ return MetadataVersion::InitialRelease;
+}
+
+// Index
+static std::shared_ptr<Index> parseIndexInternal(const QJsonObject &obj)
+{
+ const QVector<QJsonObject> objects = requireIsArrayOf<QJsonObject>(obj, "packages");
+ QVector<VersionListPtr> lists;
+ lists.reserve(objects.size());
+ std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject &obj)
+ {
+ VersionListPtr list = std::make_shared<VersionList>(requireString(obj, "uid"));
+ list->setName(ensureString(obj, "name", QString()));
+ return list;
+ });
+ return std::make_shared<Index>(lists);
+}
+
+// Version
+static VersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj)
+{
+ VersionPtr version = std::make_shared<Version>(uid, requireString(obj, "version"));
+ version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000);
+ version->setType(ensureString(obj, "type", QString()));
+ version->setRecommended(ensureBoolean(obj, QString("recommended"), false));
+ version->setVolatile(ensureBoolean(obj, QString("volatile"), false));
+ RequireSet requires, conflicts;
+ parseRequires(obj, &requires, "requires");
+ parseRequires(obj, &conflicts, "conflicts");
+ version->setRequires(requires, conflicts);
+ return version;
+}
+
+static std::shared_ptr<Version> parseVersionInternal(const QJsonObject &obj)
+{
+ VersionPtr 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;
+}
+
+// Version list / package
+static std::shared_ptr<VersionList> parseVersionListInternal(const QJsonObject &obj)
+{
+ const QString uid = requireString(obj, "uid");
+
+ const QVector<QJsonObject> versionsRaw = requireIsArrayOf<QJsonObject>(obj, "versions");
+ QVector<VersionPtr> versions;
+ versions.reserve(versionsRaw.size());
+ std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject &vObj)
+ {
+ auto version = parseCommonVersion(uid, vObj);
+ version->setProvidesRecommendations();
+ return version;
+ });
+
+ VersionListPtr list = std::make_shared<VersionList>(uid);
+ list->setName(ensureString(obj, "name", QString()));
+ list->setVersions(versions);
+ return list;
+}
+
+
+MetadataVersion parseFormatVersion(const QJsonObject &obj, bool required)
+{
+ if (!obj.contains("formatVersion"))
+ {
+ if(required)
+ {
+ return MetadataVersion::Invalid;
+ }
+ return MetadataVersion::InitialRelease;
+ }
+ if (!obj.value("formatVersion").isDouble())
+ {
+ return MetadataVersion::Invalid;
+ }
+ switch(obj.value("formatVersion").toInt())
+ {
+ case 0:
+ case 1:
+ return MetadataVersion::InitialRelease;
+ default:
+ return MetadataVersion::Invalid;
+ }
+}
+
+void serializeFormatVersion(QJsonObject& obj, Meta::MetadataVersion version)
+{
+ if(version == MetadataVersion::Invalid)
+ {
+ return;
+ }
+ obj.insert("formatVersion", int(version));
+}
+
+void parseIndex(const QJsonObject &obj, Index *ptr)
+{
+ const MetadataVersion version = parseFormatVersion(obj);
+ switch (version)
+ {
+ case MetadataVersion::InitialRelease:
+ ptr->merge(parseIndexInternal(obj));
+ break;
+ case MetadataVersion::Invalid:
+ throw ParseException(QObject::tr("Unknown format version!"));
+ }
+}
+
+void parseVersionList(const QJsonObject &obj, VersionList *ptr)
+{
+ const MetadataVersion version = parseFormatVersion(obj);
+ switch (version)
+ {
+ case MetadataVersion::InitialRelease:
+ ptr->merge(parseVersionListInternal(obj));
+ break;
+ case MetadataVersion::Invalid:
+ throw ParseException(QObject::tr("Unknown format version!"));
+ }
+}
+
+void parseVersion(const QJsonObject &obj, Version *ptr)
+{
+ const MetadataVersion version = parseFormatVersion(obj);
+ switch (version)
+ {
+ case MetadataVersion::InitialRelease:
+ ptr->merge(parseVersionInternal(obj));
+ break;
+ case MetadataVersion::Invalid:
+ throw ParseException(QObject::tr("Unknown format version!"));
+ }
+}
+
+/*
+[
+{"uid":"foo", "equals":"version"}
+]
+*/
+void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName)
+{
+ if(obj.contains(keyName))
+ {
+ QSet<QString> requires;
+ auto reqArray = requireArray(obj, keyName);
+ auto iter = reqArray.begin();
+ while(iter != reqArray.end())
+ {
+ auto reqObject = requireObject(*iter);
+ auto uid = requireString(reqObject, "uid");
+ auto equals = ensureString(reqObject, "equals", QString());
+ auto suggests = ensureString(reqObject, "suggests", QString());
+ ptr->insert({uid, equals, suggests});
+ iter++;
+ }
+ }
+}
+void serializeRequires(QJsonObject& obj, RequireSet* ptr, const char * keyName)
+{
+ if(!ptr || ptr->empty())
+ {
+ return;
+ }
+ QJsonArray arrOut;
+ for(auto &iter: *ptr)
+ {
+ QJsonObject reqOut;
+ reqOut.insert("uid", iter.uid);
+ if(!iter.equalsVersion.isEmpty())
+ {
+ reqOut.insert("equals", iter.equalsVersion);
+ }
+ if(!iter.suggests.isEmpty())
+ {
+ reqOut.insert("suggests", iter.suggests);
+ }
+ arrOut.append(reqOut);
+ }
+ obj.insert(keyName, arrOut);
+}
+
+}
+
diff --git a/launcher/meta/JsonFormat.h b/launcher/meta/JsonFormat.h
new file mode 100644
index 00000000..93217b7e
--- /dev/null
+++ b/launcher/meta/JsonFormat.h
@@ -0,0 +1,83 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "meta/BaseEntity.h"
+#include <set>
+
+namespace Meta
+{
+class Index;
+class Version;
+class VersionList;
+
+enum class MetadataVersion
+{
+ Invalid = -1,
+ InitialRelease = 1
+};
+
+class ParseException : public Exception
+{
+public:
+ using Exception::Exception;
+};
+struct Require
+{
+ bool operator==(const Require & rhs) const
+ {
+ return uid == rhs.uid;
+ }
+ bool operator<(const Require & rhs) const
+ {
+ return uid < rhs.uid;
+ }
+ bool deepEquals(const Require & rhs) const
+ {
+ return uid == rhs.uid
+ && equalsVersion == rhs.equalsVersion
+ && suggests == rhs.suggests;
+ }
+ QString uid;
+ QString equalsVersion;
+ QString suggests;
+};
+
+inline Q_DECL_PURE_FUNCTION uint qHash(const Require &key, uint seed = 0) Q_DECL_NOTHROW
+{
+ return qHash(key.uid, seed);
+}
+
+using RequireSet = std::set<Require>;
+
+void parseIndex(const QJsonObject &obj, Index *ptr);
+void parseVersion(const QJsonObject &obj, Version *ptr);
+void parseVersionList(const QJsonObject &obj, VersionList *ptr);
+
+MetadataVersion parseFormatVersion(const QJsonObject &obj, bool required = true);
+void serializeFormatVersion(QJsonObject &obj, MetadataVersion version);
+
+// FIXME: this has a different shape than the others...FIX IT!?
+void parseRequires(const QJsonObject &obj, RequireSet * ptr, const char * keyName = "requires");
+void serializeRequires(QJsonObject & objOut, RequireSet* ptr, const char * keyName = "requires");
+MetadataVersion currentFormatVersion();
+}
+
+Q_DECLARE_METATYPE(std::set<Meta::Require>)
diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp
new file mode 100644
index 00000000..a8dc3169
--- /dev/null
+++ b/launcher/meta/Version.cpp
@@ -0,0 +1,140 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "Version.h"
+
+#include <QDateTime>
+
+#include "JsonFormat.h"
+#include "minecraft/PackProfile.h"
+
+Meta::Version::Version(const QString &uid, const QString &version)
+ : BaseVersion(), m_uid(uid), m_version(version)
+{
+}
+
+Meta::Version::~Version()
+{
+}
+
+QString Meta::Version::descriptor()
+{
+ return m_version;
+}
+QString Meta::Version::name()
+{
+ if(m_data)
+ return m_data->name;
+ return m_uid;
+}
+QString Meta::Version::typeString() const
+{
+ return m_type;
+}
+
+QDateTime Meta::Version::time() const
+{
+ return QDateTime::fromMSecsSinceEpoch(m_time * 1000, Qt::UTC);
+}
+
+void Meta::Version::parse(const QJsonObject& obj)
+{
+ parseVersion(obj, this);
+}
+
+void Meta::Version::mergeFromList(const Meta::VersionPtr& other)
+{
+ if(other->m_providesRecommendations)
+ {
+ if(m_recommended != other->m_recommended)
+ {
+ setRecommended(other->m_recommended);
+ }
+ }
+ if (m_type != other->m_type)
+ {
+ setType(other->m_type);
+ }
+ if (m_time != other->m_time)
+ {
+ setTime(other->m_time);
+ }
+ if (m_requires != other->m_requires)
+ {
+ m_requires = other->m_requires;
+ }
+ if (m_conflicts != other->m_conflicts)
+ {
+ m_conflicts = other->m_conflicts;
+ }
+ if(m_volatile != other->m_volatile)
+ {
+ setVolatile(other->m_volatile);
+ }
+}
+
+void Meta::Version::merge(const VersionPtr &other)
+{
+ mergeFromList(other);
+ if(other->m_data)
+ {
+ setData(other->m_data);
+ }
+}
+
+QString Meta::Version::localFilename() const
+{
+ return m_uid + '/' + m_version + ".json";
+}
+
+void Meta::Version::setType(const QString &type)
+{
+ m_type = type;
+ emit typeChanged();
+}
+
+void Meta::Version::setTime(const qint64 time)
+{
+ m_time = time;
+ emit timeChanged();
+}
+
+void Meta::Version::setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts)
+{
+ m_requires = requires;
+ m_conflicts = conflicts;
+ emit requiresChanged();
+}
+
+void Meta::Version::setVolatile(bool volatile_)
+{
+ m_volatile = volatile_;
+}
+
+
+void Meta::Version::setData(const VersionFilePtr &data)
+{
+ m_data = data;
+}
+
+void Meta::Version::setProvidesRecommendations()
+{
+ m_providesRecommendations = true;
+}
+
+void Meta::Version::setRecommended(bool recommended)
+{
+ m_recommended = recommended;
+}
diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h
new file mode 100644
index 00000000..dea8dc8a
--- /dev/null
+++ b/launcher/meta/Version.h
@@ -0,0 +1,116 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 <QJsonObject>
+#include <QStringList>
+#include <QVector>
+#include <memory>
+
+#include "minecraft/VersionFile.h"
+
+#include "BaseEntity.h"
+
+#include "JsonFormat.h"
+
+namespace Meta
+{
+using VersionPtr = std::shared_ptr<class Version>;
+
+class Version : public QObject, public BaseVersion, public BaseEntity
+{
+ Q_OBJECT
+
+public: /* con/des */
+ explicit Version(const QString &uid, const QString &version);
+ virtual ~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;
+ }
+ const Meta::RequireSet &requires() const
+ {
+ return m_requires;
+ }
+ VersionFilePtr data() const
+ {
+ return m_data;
+ }
+ bool isRecommended() const
+ {
+ return m_recommended;
+ }
+ bool isLoaded() const
+ {
+ return m_data != nullptr;
+ }
+
+ void merge(const VersionPtr &other);
+ void mergeFromList(const VersionPtr &other);
+ void parse(const QJsonObject &obj) override;
+
+ QString localFilename() const override;
+
+public: // for usage by format parsers only
+ void setType(const QString &type);
+ void setTime(const qint64 time);
+ void setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts);
+ void setVolatile(bool volatile_);
+ void setRecommended(bool recommended);
+ void setProvidesRecommendations();
+ void setData(const VersionFilePtr &data);
+
+signals:
+ void typeChanged();
+ void timeChanged();
+ void requiresChanged();
+
+private:
+ bool m_providesRecommendations = false;
+ bool m_recommended = false;
+ QString m_name;
+ QString m_uid;
+ QString m_version;
+ QString m_type;
+ qint64 m_time = 0;
+ Meta::RequireSet m_requires;
+ Meta::RequireSet m_conflicts;
+ bool m_volatile = false;
+ VersionFilePtr m_data;
+};
+}
+
+Q_DECLARE_METATYPE(Meta::VersionPtr)
diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp
new file mode 100644
index 00000000..607007eb
--- /dev/null
+++ b/launcher/meta/VersionList.cpp
@@ -0,0 +1,245 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "VersionList.h"
+
+#include <QDateTime>
+
+#include "Version.h"
+#include "JsonFormat.h"
+#include "Version.h"
+
+namespace Meta
+{
+VersionList::VersionList(const QString &uid, QObject *parent)
+ : BaseVersionList(parent), m_uid(uid)
+{
+ setObjectName("Version list: " + uid);
+}
+
+shared_qobject_ptr<Task> VersionList::getLoadTask()
+{
+ load(Net::Mode::Online);
+ return getCurrentTask();
+}
+
+bool VersionList::isLoaded()
+{
+ return BaseEntity::isLoaded();
+}
+
+const BaseVersionPtr VersionList::at(int i) const
+{
+ return m_versions.at(i);
+}
+int VersionList::count() const
+{
+ return m_versions.size();
+}
+
+void VersionList::sortVersions()
+{
+ beginResetModel();
+ std::sort(m_versions.begin(), m_versions.end(), [](const VersionPtr &a, const VersionPtr &b)
+ {
+ return *a.get() < *b.get();
+ });
+ endResetModel();
+}
+
+QVariant VersionList::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_versions.size() || index.parent().isValid())
+ {
+ return QVariant();
+ }
+
+ VersionPtr 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 ParentVersionRole:
+ {
+ // FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'.
+ auto & reqs = version->requires();
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req)
+ {
+ return req.uid == "net.minecraft";
+ });
+ if (iter != reqs.end())
+ {
+ return (*iter).equalsVersion;
+ }
+ 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 VersionPtrRole: return QVariant::fromValue(version);
+ case RecommendedRole: return version->isRecommended();
+ // FIXME: this should be determined in whatever view/proxy is used...
+ // case LatestRole: return version == getLatestStable();
+ default: return QVariant();
+ }
+}
+
+BaseVersionList::RoleList VersionList::providesRoles() const
+{
+ return {VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole,
+ TypeRole, UidRole, TimeRole, RequiresRole, SortRole,
+ RecommendedRole, LatestRole, VersionPtrRole};
+}
+
+QHash<int, QByteArray> VersionList::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;
+}
+
+QString VersionList::localFilename() const
+{
+ return m_uid + "/index.json";
+}
+
+QString VersionList::humanReadable() const
+{
+ return m_name.isEmpty() ? m_uid : m_name;
+}
+
+VersionPtr VersionList::getVersion(const QString &version)
+{
+ VersionPtr out = m_lookup.value(version, nullptr);
+ if(!out)
+ {
+ out = std::make_shared<Version>(m_uid, version);
+ m_lookup[version] = out;
+ }
+ return out;
+}
+
+void VersionList::setName(const QString &name)
+{
+ m_name = name;
+ emit nameChanged(name);
+}
+
+void VersionList::setVersions(const QVector<VersionPtr> &versions)
+{
+ beginResetModel();
+ m_versions = versions;
+ std::sort(m_versions.begin(), m_versions.end(), [](const VersionPtr &a, const VersionPtr &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));
+ }
+
+ // FIXME: this is dumb, we have 'recommended' as part of the metadata already...
+ auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const VersionPtr &ptr) { return ptr->type() == "release"; });
+ m_recommended = recommendedIt == m_versions.constEnd() ? nullptr : *recommendedIt;
+ endResetModel();
+}
+
+void VersionList::parse(const QJsonObject& obj)
+{
+ parseVersionList(obj, this);
+}
+
+// FIXME: this is dumb, we have 'recommended' as part of the metadata already...
+static const Meta::VersionPtr &getBetterVersion(const Meta::VersionPtr &a, const Meta::VersionPtr &b)
+{
+ if(!a)
+ return b;
+ if(!b)
+ return a;
+ if(a->type() == b->type())
+ {
+ // newer of same type wins
+ return (a->rawTime() > b->rawTime() ? a : b);
+ }
+ // 'release' type wins
+ return (a->type() == "release" ? a : b);
+}
+
+void VersionList::mergeFromIndex(const VersionListPtr &other)
+{
+ if (m_name != other->m_name)
+ {
+ setName(other->m_name);
+ }
+}
+
+void VersionList::merge(const VersionListPtr &other)
+{
+ if (m_name != other->m_name)
+ {
+ setName(other->m_name);
+ }
+
+ // TODO: do not reset the whole model. maybe?
+ beginResetModel();
+ m_versions.clear();
+ if(other->m_versions.isEmpty())
+ {
+ qWarning() << "Empty list loaded ...";
+ }
+ for (const VersionPtr &version : other->m_versions)
+ {
+ // we already have the version. merge the contents
+ if (m_lookup.contains(version->version()))
+ {
+ m_lookup.value(version->version())->mergeFromList(version);
+ }
+ else
+ {
+ m_lookup.insert(version->uid(), version);
+ }
+ // connect it.
+ setupAddedVersion(m_versions.size(), version);
+ m_versions.append(version);
+ m_recommended = getBetterVersion(m_recommended, version);
+ }
+ endResetModel();
+}
+
+void VersionList::setupAddedVersion(const int row, const VersionPtr &version)
+{
+ // FIXME: do not disconnect from everythin, disconnect only the lambdas here
+ version->disconnect();
+ connect(version.get(), &Version::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << RequiresRole); });
+ connect(version.get(), &Version::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TimeRole << SortRole); });
+ connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector<int>() << TypeRole); });
+}
+
+BaseVersionPtr VersionList::getRecommended() const
+{
+ return m_recommended;
+}
+
+}
diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h
new file mode 100644
index 00000000..58cdafe7
--- /dev/null
+++ b/launcher/meta/VersionList.h
@@ -0,0 +1,101 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "BaseEntity.h"
+#include "BaseVersionList.h"
+#include <QJsonObject>
+#include <memory>
+
+namespace Meta
+{
+using VersionPtr = std::shared_ptr<class Version>;
+using VersionListPtr = std::shared_ptr<class VersionList>;
+
+class VersionList : public BaseVersionList, public BaseEntity
+{
+ Q_OBJECT
+ Q_PROPERTY(QString uid READ uid CONSTANT)
+ Q_PROPERTY(QString name READ name NOTIFY nameChanged)
+public:
+ explicit VersionList(const QString &uid, QObject *parent = nullptr);
+
+ enum Roles
+ {
+ UidRole = Qt::UserRole + 100,
+ TimeRole,
+ RequiresRole,
+ VersionPtrRole
+ };
+
+ shared_qobject_ptr<Task> getLoadTask() override;
+ bool isLoaded() override;
+ const BaseVersionPtr at(int i) const override;
+ int count() const override;
+ void sortVersions() override;
+
+ BaseVersionPtr getRecommended() const override;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ RoleList providesRoles() const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ QString localFilename() const override;
+
+ QString uid() const
+ {
+ return m_uid;
+ }
+ QString name() const
+ {
+ return m_name;
+ }
+ QString humanReadable() const;
+
+ VersionPtr getVersion(const QString &version);
+
+ QVector<VersionPtr> versions() const
+ {
+ return m_versions;
+ }
+
+public: // for usage only by parsers
+ void setName(const QString &name);
+ void setVersions(const QVector<VersionPtr> &versions);
+ void merge(const VersionListPtr &other);
+ void mergeFromIndex(const VersionListPtr &other);
+ void parse(const QJsonObject &obj) override;
+
+signals:
+ void nameChanged(const QString &name);
+
+protected slots:
+ void updateListData(QList<BaseVersionPtr>) override
+ {
+ }
+
+private:
+ QVector<VersionPtr> m_versions;
+ QHash<QString, VersionPtr> m_lookup;
+ QString m_uid;
+ QString m_name;
+
+ VersionPtr m_recommended;
+
+ void setupAddedVersion(const int row, const VersionPtr &version);
+};
+}
+Q_DECLARE_METATYPE(Meta::VersionListPtr)
diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp
new file mode 100644
index 00000000..c01733b6
--- /dev/null
+++ b/launcher/minecraft/AssetsUtils.cpp
@@ -0,0 +1,333 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <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/Download.h"
+#include "net/ChecksumValidator.h"
+#include "BuildConfig.h"
+
+namespace {
+QSet<QString> collectPathsFromDir(QString dirPath)
+{
+ QFileInfo dirInfo(dirPath);
+
+ if (!dirInfo.exists())
+ {
+ return {};
+ }
+
+ QSet<QString> out;
+
+ QDirIterator iter(dirPath, QDirIterator::Subdirectories);
+ while (iter.hasNext())
+ {
+ QString value = iter.next();
+ QFileInfo info(value);
+ if(info.isFile())
+ {
+ out.insert(value);
+ qDebug() << value;
+ }
+ }
+ return out;
+}
+}
+
+
+namespace AssetsUtils
+{
+
+/*
+ * Returns true on success, with index populated
+ * index is undefined otherwise
+ */
+bool loadAssetsIndexJson(const QString &assetsId, const 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 mapToResources = root.value("map_to_resources");
+ if (!mapToResources.isUndefined())
+ {
+ index.mapToResources = mapToResources.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;
+}
+
+// FIXME: ugly code duplication
+QDir getAssetsDir(const QString &assetsId, const QString &resourcesFolder)
+{
+ QDir assetsDir = QDir("assets/");
+ QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes"));
+ QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects"));
+ QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual"));
+
+ QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json");
+ QFile indexFile(indexPath);
+ QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId));
+
+ if (!indexFile.exists())
+ {
+ qCritical() << "No assets index file" << indexPath << "; can't determine assets path!";
+ return virtualRoot;
+ }
+
+ AssetsIndex index;
+ if(!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index))
+ {
+ qCritical() << "Failed to load asset index file" << indexPath << "; can't determine assets path!";
+ return virtualRoot;
+ }
+
+ QString targetPath;
+ if(index.isVirtual)
+ {
+ return virtualRoot;
+ }
+ else if(index.mapToResources)
+ {
+ return QDir(resourcesFolder);
+ }
+ return virtualRoot;
+}
+
+// FIXME: ugly code duplication
+bool reconstructAssets(QString assetsId, QString resourcesFolder)
+{
+ QDir assetsDir = QDir("assets/");
+ QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes"));
+ QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects"));
+ QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual"));
+
+ QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json");
+ QFile indexFile(indexPath);
+ QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId));
+
+ if (!indexFile.exists())
+ {
+ qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets!";
+ return false;
+ }
+
+ qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() << objectDir.path() << virtualDir.path() << virtualRoot.path();
+
+ AssetsIndex index;
+ if(!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index))
+ {
+ qCritical() << "Failed to load asset index file" << indexPath << "; can't reconstruct assets!";
+ return false;
+ }
+
+ QString targetPath;
+ bool removeLeftovers = false;
+ if(index.isVirtual)
+ {
+ targetPath = virtualRoot.path();
+ removeLeftovers = true;
+ qDebug() << "Reconstructing virtual assets folder at" << targetPath;
+ }
+ else if(index.mapToResources)
+ {
+ targetPath = resourcesFolder;
+ qDebug() << "Reconstructing resources folder at" << targetPath;
+ }
+
+ if (!targetPath.isNull())
+ {
+ auto presentFiles = collectPathsFromDir(targetPath);
+ for (QString map : index.objects.keys())
+ {
+ AssetObject asset_object = index.objects.value(map);
+ QString target_path = FS::PathCombine(targetPath, 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;
+
+ presentFiles.remove(target_path);
+
+ if (!target.exists())
+ {
+ QFileInfo info(target_path);
+ QDir target_dir = info.dir();
+
+ qDebug() << target_dir.path();
+ FS::ensureFolderPathExists(target_dir.path());
+
+ bool couldCopy = original.copy(target_path);
+ qDebug() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy);
+ }
+ }
+
+ // TODO: Write last used time to virtualRoot/.lastused
+ if(removeLeftovers)
+ {
+ for(auto & file: presentFiles)
+ {
+ qDebug() << "Would remove" << file;
+ }
+ }
+ }
+ return true;
+}
+
+}
+
+NetActionPtr AssetObject::getDownloadAction()
+{
+ QFileInfo objectFile(getLocalPath());
+ if ((!objectFile.isFile()) || (objectFile.size() != size))
+ {
+ auto objectDL = Net::Download::makeFile(getUrl(), objectFile.filePath());
+ if(hash.size())
+ {
+ auto rawHash = QByteArray::fromHex(hash.toLatin1());
+ objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash));
+ }
+ objectDL->m_total_progress = size;
+ return objectDL;
+ }
+ return nullptr;
+}
+
+QString AssetObject::getLocalPath()
+{
+ return "assets/objects/" + getRelPath();
+}
+
+QUrl AssetObject::getUrl()
+{
+ return BuildConfig.RESOURCE_BASE + 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/launcher/minecraft/AssetsUtils.h b/launcher/minecraft/AssetsUtils.h
new file mode 100644
index 00000000..32e57060
--- /dev/null
+++ b/launcher/minecraft/AssetsUtils.h
@@ -0,0 +1,53 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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;
+ bool mapToResources = false;
+};
+
+/// FIXME: this is absolutely horrendous. REDO!!!!
+namespace AssetsUtils
+{
+bool loadAssetsIndexJson(const QString &id, const QString &file, AssetsIndex& index);
+
+QDir getAssetsDir(const QString &assetsId, const QString &resourcesFolder);
+
+/// Reconstruct a virtual assets folder for the given assets ID and return the folder
+bool reconstructAssets(QString assetsId, QString resourcesFolder);
+}
diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp
new file mode 100644
index 00000000..92821065
--- /dev/null
+++ b/launcher/minecraft/Component.cpp
@@ -0,0 +1,439 @@
+#include <meta/VersionList.h>
+#include <meta/Index.h>
+#include <Env.h>
+#include "Component.h"
+
+#include "meta/Version.h"
+#include "VersionFile.h"
+#include "minecraft/PackProfile.h"
+#include <FileSystem.h>
+#include <QSaveFile>
+#include "OneSixVersionFormat.h"
+#include <assert.h>
+
+Component::Component(PackProfile * parent, const QString& uid)
+{
+ assert(parent);
+ m_parent = parent;
+
+ m_uid = uid;
+}
+
+Component::Component(PackProfile * parent, std::shared_ptr<Meta::Version> version)
+{
+ assert(parent);
+ m_parent = parent;
+
+ m_metaVersion = version;
+ m_uid = version->uid();
+ m_version = m_cachedVersion = version->version();
+ m_cachedName = version->name();
+ m_loaded = version->isLoaded();
+}
+
+Component::Component(PackProfile * parent, const QString& uid, std::shared_ptr<VersionFile> file)
+{
+ assert(parent);
+ m_parent = parent;
+
+ m_file = file;
+ m_uid = uid;
+ m_cachedVersion = m_file->version;
+ m_cachedName = m_file->name;
+ m_loaded = true;
+}
+
+std::shared_ptr<Meta::Version> Component::getMeta()
+{
+ return m_metaVersion;
+}
+
+void Component::applyTo(LaunchProfile* profile)
+{
+ // do not apply disabled components
+ if(!isEnabled())
+ {
+ return;
+ }
+ auto vfile = getVersionFile();
+ if(vfile)
+ {
+ vfile->applyTo(profile);
+ }
+ else
+ {
+ profile->applyProblemSeverity(getProblemSeverity());
+ }
+}
+
+std::shared_ptr<class VersionFile> Component::getVersionFile() const
+{
+ if(m_metaVersion)
+ {
+ if(!m_metaVersion->isLoaded())
+ {
+ m_metaVersion->load(Net::Mode::Online);
+ }
+ return m_metaVersion->data();
+ }
+ else
+ {
+ return m_file;
+ }
+}
+
+std::shared_ptr<class Meta::VersionList> Component::getVersionList() const
+{
+ // FIXME: what if the metadata index isn't loaded yet?
+ if(ENV.metadataIndex()->hasUid(m_uid))
+ {
+ return ENV.metadataIndex()->get(m_uid);
+ }
+ return nullptr;
+}
+
+int Component::getOrder()
+{
+ if(m_orderOverride)
+ return m_order;
+
+ auto vfile = getVersionFile();
+ if(vfile)
+ {
+ return vfile->order;
+ }
+ return 0;
+}
+void Component::setOrder(int order)
+{
+ m_orderOverride = true;
+ m_order = order;
+}
+QString Component::getID()
+{
+ return m_uid;
+}
+QString Component::getName()
+{
+ if (!m_cachedName.isEmpty())
+ return m_cachedName;
+ return m_uid;
+}
+QString Component::getVersion()
+{
+ return m_cachedVersion;
+}
+QString Component::getFilename()
+{
+ return m_parent->patchFilePathForUid(m_uid);
+}
+QDateTime Component::getReleaseDateTime()
+{
+ if(m_metaVersion)
+ {
+ return m_metaVersion->time();
+ }
+ auto vfile = getVersionFile();
+ if(vfile)
+ {
+ return vfile->releaseTime;
+ }
+ // FIXME: fake
+ return QDateTime::currentDateTime();
+}
+
+bool Component::isEnabled()
+{
+ return !canBeDisabled() || !m_disabled;
+}
+
+bool Component::canBeDisabled()
+{
+ return isRemovable() && !m_dependencyOnly;
+}
+
+bool Component::setEnabled(bool state)
+{
+ bool intendedDisabled = !state;
+ if (!canBeDisabled())
+ {
+ intendedDisabled = false;
+ }
+ if(intendedDisabled != m_disabled)
+ {
+ m_disabled = intendedDisabled;
+ emit dataChanged();
+ return true;
+ }
+ return false;
+}
+
+bool Component::isCustom()
+{
+ return m_file != nullptr;
+}
+
+bool Component::isCustomizable()
+{
+ if(m_metaVersion)
+ {
+ if(getVersionFile())
+ {
+ return true;
+ }
+ }
+ return false;
+}
+bool Component::isRemovable()
+{
+ return !m_important;
+}
+bool Component::isRevertible()
+{
+ if (isCustom())
+ {
+ if(ENV.metadataIndex()->hasUid(m_uid))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+bool Component::isMoveable()
+{
+ // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'.
+ return true;
+}
+bool Component::isVersionChangeable()
+{
+ auto list = getVersionList();
+ if(list)
+ {
+ if(!list->isLoaded())
+ {
+ list->load(Net::Mode::Online);
+ }
+ return list->count() != 0;
+ }
+ return false;
+}
+
+void Component::setImportant(bool state)
+{
+ if(m_important != state)
+ {
+ m_important = state;
+ emit dataChanged();
+ }
+}
+
+ProblemSeverity Component::getProblemSeverity() const
+{
+ auto file = getVersionFile();
+ if(file)
+ {
+ return file->getProblemSeverity();
+ }
+ return ProblemSeverity::Error;
+}
+
+const QList<PatchProblem> Component::getProblems() const
+{
+ auto file = getVersionFile();
+ if(file)
+ {
+ return file->getProblems();
+ }
+ return {{ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.")}};
+}
+
+void Component::setVersion(const QString& version)
+{
+ if(version == m_version)
+ {
+ return;
+ }
+ m_version = version;
+ if(m_loaded)
+ {
+ // we are loaded and potentially have state to invalidate
+ if(m_file)
+ {
+ // we have a file... explicit version has been changed and there is nothing else to do.
+ }
+ else
+ {
+ // we don't have a file, therefore we are loaded with metadata
+ m_cachedVersion = version;
+ // see if the meta version is loaded
+ auto metaVersion = ENV.metadataIndex()->get(m_uid, version);
+ if(metaVersion->isLoaded())
+ {
+ // if yes, we can continue with that.
+ m_metaVersion = metaVersion;
+ }
+ else
+ {
+ // if not, we need loading
+ m_metaVersion.reset();
+ m_loaded = false;
+ }
+ updateCachedData();
+ }
+ }
+ else
+ {
+ // not loaded... assume it will be sorted out later by the update task
+ }
+ emit dataChanged();
+}
+
+bool Component::customize()
+{
+ if(isCustom())
+ {
+ return false;
+ }
+
+ auto filename = getFilename();
+ if(!FS::ensureFilePathExists(filename))
+ {
+ return false;
+ }
+ // FIXME: get rid of this try-catch.
+ try
+ {
+ QSaveFile jsonFile(filename);
+ if(!jsonFile.open(QIODevice::WriteOnly))
+ {
+ return false;
+ }
+ auto vfile = getVersionFile();
+ if(!vfile)
+ {
+ return false;
+ }
+ auto document = OneSixVersionFormat::versionFileToJson(vfile);
+ jsonFile.write(document.toJson());
+ if(!jsonFile.commit())
+ {
+ return false;
+ }
+ m_file = vfile;
+ m_metaVersion.reset();
+ emit dataChanged();
+ }
+ catch (const Exception &error)
+ {
+ qWarning() << "Version could not be loaded:" << error.cause();
+ }
+ return true;
+}
+
+bool Component::revert()
+{
+ if(!isCustom())
+ {
+ // already not custom
+ return true;
+ }
+ auto filename = getFilename();
+ bool result = true;
+ // just kill the file and reload
+ if(QFile::exists(filename))
+ {
+ result = QFile::remove(filename);
+ }
+ if(result)
+ {
+ // file gone...
+ m_file.reset();
+
+ // check local cache for metadata...
+ auto version = ENV.metadataIndex()->get(m_uid, m_version);
+ if(version->isLoaded())
+ {
+ m_metaVersion = version;
+ }
+ else
+ {
+ m_metaVersion.reset();
+ m_loaded = false;
+ }
+ emit dataChanged();
+ }
+ return result;
+}
+
+/**
+ * deep inspecting compare for requirement sets
+ * By default, only uids are compared for set operations.
+ * This compares all fields of the Require structs in the sets.
+ */
+static bool deepCompare(const std::set<Meta::Require> & a, const std::set<Meta::Require> & b)
+{
+ // NOTE: this needs to be rewritten if the type of Meta::RequireSet changes
+ if(a.size() != b.size())
+ {
+ return false;
+ }
+ for(const auto & reqA :a)
+ {
+ const auto &iter2 = b.find(reqA);
+ if(iter2 == b.cend())
+ {
+ return false;
+ }
+ const auto & reqB = *iter2;
+ if(!reqA.deepEquals(reqB))
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+void Component::updateCachedData()
+{
+ auto file = getVersionFile();
+ if(file)
+ {
+ bool changed = false;
+ if(m_cachedName != file->name)
+ {
+ m_cachedName = file->name;
+ changed = true;
+ }
+ if(m_cachedVersion != file->version)
+ {
+ m_cachedVersion = file->version;
+ changed = true;
+ }
+ if(m_cachedVolatile != file->m_volatile)
+ {
+ m_cachedVolatile = file->m_volatile;
+ changed = true;
+ }
+ if(!deepCompare(m_cachedRequires, file->requires))
+ {
+ m_cachedRequires = file->requires;
+ changed = true;
+ }
+ if(!deepCompare(m_cachedConflicts, file->conflicts))
+ {
+ m_cachedConflicts = file->conflicts;
+ changed = true;
+ }
+ if(changed)
+ {
+ emit dataChanged();
+ }
+ }
+ else
+ {
+ // in case we removed all the metadata
+ m_cachedRequires.clear();
+ m_cachedConflicts.clear();
+ emit dataChanged();
+ }
+}
diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h
new file mode 100644
index 00000000..ef7c9947
--- /dev/null
+++ b/launcher/minecraft/Component.h
@@ -0,0 +1,110 @@
+#pragma once
+
+#include <memory>
+#include <QList>
+#include <QJsonDocument>
+#include <QDateTime>
+#include "meta/JsonFormat.h"
+#include "ProblemProvider.h"
+#include "QObjectPtr.h"
+
+class PackProfile;
+class LaunchProfile;
+namespace Meta
+{
+ class Version;
+ class VersionList;
+}
+class VersionFile;
+
+class Component : public QObject, public ProblemProvider
+{
+Q_OBJECT
+public:
+ Component(PackProfile * parent, const QString &uid);
+
+ // DEPRECATED: remove these constructors?
+ Component(PackProfile * parent, std::shared_ptr<Meta::Version> version);
+ Component(PackProfile * parent, const QString & uid, std::shared_ptr<VersionFile> file);
+
+ virtual ~Component(){};
+ void applyTo(LaunchProfile *profile);
+
+ bool isEnabled();
+ bool setEnabled (bool state);
+ bool canBeDisabled();
+
+ bool isMoveable();
+ bool isCustomizable();
+ bool isRevertible();
+ bool isRemovable();
+ bool isCustom();
+ bool isVersionChangeable();
+
+ // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code
+ void setOrder(int order);
+ int getOrder();
+
+ QString getID();
+ QString getName();
+ QString getVersion();
+ std::shared_ptr<Meta::Version> getMeta();
+ QDateTime getReleaseDateTime();
+
+ QString getFilename();
+
+ std::shared_ptr<class VersionFile> getVersionFile() const;
+ std::shared_ptr<class Meta::VersionList> getVersionList() const;
+
+ void setImportant (bool state);
+
+
+ const QList<PatchProblem> getProblems() const override;
+ ProblemSeverity getProblemSeverity() const override;
+
+ void setVersion(const QString & version);
+ bool customize();
+ bool revert();
+
+ void updateCachedData();
+
+signals:
+ void dataChanged();
+
+public: /* data */
+ PackProfile * m_parent;
+
+ // BEGIN: persistent component list properties
+ /// ID of the component
+ QString m_uid;
+ /// version of the component - when there's a custom json override, this is also the version the component reverts to
+ QString m_version;
+ /// if true, this has been added automatically to satisfy dependencies and may be automatically removed
+ bool m_dependencyOnly = false;
+ /// if true, the component is either the main component of the instance, or otherwise important and cannot be removed.
+ bool m_important = false;
+ /// if true, the component is disabled
+ bool m_disabled = false;
+
+ /// cached name for display purposes, taken from the version file (meta or local override)
+ QString m_cachedName;
+ /// cached version for display AND other purposes, taken from the version file (meta or local override)
+ QString m_cachedVersion;
+ /// cached set of requirements, taken from the version file (meta or local override)
+ Meta::RequireSet m_cachedRequires;
+ Meta::RequireSet m_cachedConflicts;
+ /// if true, the component is volatile and may be automatically removed when no longer needed
+ bool m_cachedVolatile = false;
+ // END: persistent component list properties
+
+ // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code
+ bool m_orderOverride = false;
+ int m_order = 0;
+
+ // load state
+ std::shared_ptr<Meta::Version> m_metaVersion;
+ std::shared_ptr<VersionFile> m_file;
+ bool m_loaded = false;
+};
+
+typedef shared_qobject_ptr<Component> ComponentPtr;
diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp
new file mode 100644
index 00000000..241d9a49
--- /dev/null
+++ b/launcher/minecraft/ComponentUpdateTask.cpp
@@ -0,0 +1,704 @@
+#include "ComponentUpdateTask.h"
+
+#include "PackProfile_p.h"
+#include "PackProfile.h"
+#include "Component.h"
+#include <Env.h>
+#include <meta/Index.h>
+#include <meta/VersionList.h>
+#include <meta/Version.h>
+#include "ComponentUpdateTask_p.h"
+#include <cassert>
+#include <Version.h>
+#include "net/Mode.h"
+#include "OneSixVersionFormat.h"
+
+/*
+ * This is responsible for loading the components of a component list AND resolving dependency issues between them
+ */
+
+/*
+ * FIXME: the 'one shot async task' nature of this does not fit the intended usage
+ * Really, it should be a reactor/state machine that receives input from the application
+ * and dynamically adapts to changing requirements...
+ *
+ * The reactor should be the only entry into manipulating the PackProfile.
+ * See: https://en.wikipedia.org/wiki/Reactor_pattern
+ */
+
+/*
+ * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and PackProfile didn't change?
+ * If the component list changes, start over.
+ */
+
+ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent)
+ : Task(parent)
+{
+ d.reset(new ComponentUpdateTaskData);
+ d->m_list = list;
+ d->mode = mode;
+ d->netmode = netmode;
+}
+
+ComponentUpdateTask::~ComponentUpdateTask()
+{
+}
+
+void ComponentUpdateTask::executeTask()
+{
+ qDebug() << "Loading components";
+ loadComponents();
+}
+
+namespace
+{
+enum class LoadResult
+{
+ LoadedLocal,
+ RequiresRemote,
+ Failed
+};
+
+LoadResult composeLoadResult(LoadResult a, LoadResult b)
+{
+ if (a < b)
+ {
+ return b;
+ }
+ return a;
+}
+
+static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
+{
+ if(component->m_loaded)
+ {
+ qDebug() << component->getName() << "is already loaded";
+ return LoadResult::LoadedLocal;
+ }
+
+ LoadResult result = LoadResult::Failed;
+ auto customPatchFilename = component->getFilename();
+ if(QFile::exists(customPatchFilename))
+ {
+ // if local file exists...
+
+ // check for uid problems inside...
+ bool fileChanged = false;
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false);
+ if(file->uid != component->m_uid)
+ {
+ file->uid = component->m_uid;
+ fileChanged = true;
+ }
+ if(fileChanged)
+ {
+ // FIXME: @QUALITY do not ignore return value
+ ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename);
+ }
+
+ component->m_file = file;
+ component->m_loaded = true;
+ result = LoadResult::LoadedLocal;
+ }
+ else
+ {
+ auto metaVersion = ENV.metadataIndex()->get(component->m_uid, component->m_version);
+ component->m_metaVersion = metaVersion;
+ if(metaVersion->isLoaded())
+ {
+ component->m_loaded = true;
+ result = LoadResult::LoadedLocal;
+ }
+ else
+ {
+ metaVersion->load(netmode);
+ loadTask = metaVersion->getCurrentTask();
+ if(loadTask)
+ result = LoadResult::RequiresRemote;
+ else if (metaVersion->isLoaded())
+ result = LoadResult::LoadedLocal;
+ else
+ result = LoadResult::Failed;
+ }
+ }
+ return result;
+}
+
+// FIXME: dead code. determine if this can still be useful?
+/*
+static LoadResult loadPackProfile(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
+{
+ if(component->m_loaded)
+ {
+ qDebug() << component->getName() << "is already loaded";
+ return LoadResult::LoadedLocal;
+ }
+
+ LoadResult result = LoadResult::Failed;
+ auto metaList = ENV.metadataIndex()->get(component->m_uid);
+ if(metaList->isLoaded())
+ {
+ component->m_loaded = true;
+ result = LoadResult::LoadedLocal;
+ }
+ else
+ {
+ metaList->load(netmode);
+ loadTask = metaList->getCurrentTask();
+ result = LoadResult::RequiresRemote;
+ }
+ return result;
+}
+*/
+
+static LoadResult loadIndex(shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
+{
+ // FIXME: DECIDE. do we want to run the update task anyway?
+ if(ENV.metadataIndex()->isLoaded())
+ {
+ qDebug() << "Index is already loaded";
+ return LoadResult::LoadedLocal;
+ }
+ ENV.metadataIndex()->load(netmode);
+ loadTask = ENV.metadataIndex()->getCurrentTask();
+ if(loadTask)
+ {
+ return LoadResult::RequiresRemote;
+ }
+ // FIXME: this is assuming the load succeeded... did it really?
+ return LoadResult::LoadedLocal;
+}
+}
+
+void ComponentUpdateTask::loadComponents()
+{
+ LoadResult result = LoadResult::LoadedLocal;
+ size_t taskIndex = 0;
+ size_t componentIndex = 0;
+ d->remoteLoadSuccessful = true;
+ // load the main index (it is needed to determine if components can revert)
+ {
+ // FIXME: tear out as a method? or lambda?
+ shared_qobject_ptr<Task> indexLoadTask;
+ auto singleResult = loadIndex(indexLoadTask, d->netmode);
+ result = composeLoadResult(result, singleResult);
+ if(indexLoadTask)
+ {
+ qDebug() << "Remote loading is being run for metadata index";
+ RemoteLoadStatus status;
+ status.type = RemoteLoadStatus::Type::Index;
+ d->remoteLoadStatusList.append(status);
+ connect(indexLoadTask.get(), &Task::succeeded, [=]()
+ {
+ remoteLoadSucceeded(taskIndex);
+ });
+ connect(indexLoadTask.get(), &Task::failed, [=](const QString & error)
+ {
+ remoteLoadFailed(taskIndex, error);
+ });
+ taskIndex++;
+ }
+ }
+ // load all the components OR their lists...
+ for (auto component: d->m_list->d->components)
+ {
+ shared_qobject_ptr<Task> loadTask;
+ LoadResult singleResult;
+ RemoteLoadStatus::Type loadType;
+ // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, ignore all that...
+#if 0
+ switch(d->mode)
+ {
+ case Mode::Launch:
+ {
+ singleResult = loadComponent(component, loadTask, d->netmode);
+ loadType = RemoteLoadStatus::Type::Version;
+ break;
+ }
+ case Mode::Resolution:
+ {
+ singleResult = loadPackProfile(component, loadTask, d->netmode);
+ loadType = RemoteLoadStatus::Type::List;
+ break;
+ }
+ }
+#else
+ singleResult = loadComponent(component, loadTask, d->netmode);
+ loadType = RemoteLoadStatus::Type::Version;
+#endif
+ if(singleResult == LoadResult::LoadedLocal)
+ {
+ component->updateCachedData();
+ }
+ result = composeLoadResult(result, singleResult);
+ if (loadTask)
+ {
+ qDebug() << "Remote loading is being run for" << component->getName();
+ connect(loadTask.get(), &Task::succeeded, [=]()
+ {
+ remoteLoadSucceeded(taskIndex);
+ });
+ connect(loadTask.get(), &Task::failed, [=](const QString & error)
+ {
+ remoteLoadFailed(taskIndex, error);
+ });
+ RemoteLoadStatus status;
+ status.type = loadType;
+ status.PackProfileIndex = componentIndex;
+ d->remoteLoadStatusList.append(status);
+ taskIndex++;
+ }
+ componentIndex++;
+ }
+ d->remoteTasksInProgress = taskIndex;
+ switch(result)
+ {
+ case LoadResult::LoadedLocal:
+ {
+ // Everything got loaded. Advance to dependency resolution.
+ resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline);
+ break;
+ }
+ case LoadResult::RequiresRemote:
+ {
+ // we wait for signals.
+ break;
+ }
+ case LoadResult::Failed:
+ {
+ emitFailed(tr("Some component metadata load tasks failed."));
+ break;
+ }
+ }
+}
+
+namespace
+{
+ struct RequireEx : public Meta::Require
+ {
+ size_t indexOfFirstDependee = 0;
+ };
+ struct RequireCompositionResult
+ {
+ bool ok;
+ RequireEx outcome;
+ };
+ using RequireExSet = std::set<RequireEx>;
+}
+
+static RequireCompositionResult composeRequirement(const RequireEx & a, const RequireEx & b)
+{
+ assert(a.uid == b.uid);
+ RequireEx out;
+ out.uid = a.uid;
+ out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee);
+ if(a.equalsVersion.isEmpty())
+ {
+ out.equalsVersion = b.equalsVersion;
+ }
+ else if (b.equalsVersion.isEmpty())
+ {
+ out.equalsVersion = a.equalsVersion;
+ }
+ else if (a.equalsVersion == b.equalsVersion)
+ {
+ out.equalsVersion = a.equalsVersion;
+ }
+ else
+ {
+ // FIXME: mark error as explicit version conflict
+ return {false, out};
+ }
+
+ if(a.suggests.isEmpty())
+ {
+ out.suggests = b.suggests;
+ }
+ else if (b.suggests.isEmpty())
+ {
+ out.suggests = a.suggests;
+ }
+ else
+ {
+ Version aVer(a.suggests);
+ Version bVer(b.suggests);
+ out.suggests = (aVer < bVer ? b.suggests : a.suggests);
+ }
+ return {true, out};
+}
+
+// gather the requirements from all components, finding any obvious conflicts
+static bool gatherRequirementsFromComponents(const ComponentContainer & input, RequireExSet & output)
+{
+ bool succeeded = true;
+ size_t componentNum = 0;
+ for(auto component: input)
+ {
+ auto &componentRequires = component->m_cachedRequires;
+ for(const auto & componentRequire: componentRequires)
+ {
+ auto found = std::find_if(output.cbegin(), output.cend(), [componentRequire](const Meta::Require & req){
+ return req.uid == componentRequire.uid;
+ });
+
+ RequireEx componenRequireEx;
+ componenRequireEx.uid = componentRequire.uid;
+ componenRequireEx.suggests = componentRequire.suggests;
+ componenRequireEx.equalsVersion = componentRequire.equalsVersion;
+ componenRequireEx.indexOfFirstDependee = componentNum;
+
+ if(found != output.cend())
+ {
+ // found... process it further
+ auto result = composeRequirement(componenRequireEx, *found);
+ if(result.ok)
+ {
+ output.erase(componenRequireEx);
+ output.insert(result.outcome);
+ }
+ else
+ {
+ qCritical()
+ << "Conflicting requirements:"
+ << componentRequire.uid
+ << "versions:"
+ << componentRequire.equalsVersion
+ << ";"
+ << (*found).equalsVersion;
+ }
+ succeeded &= result.ok;
+ }
+ else
+ {
+ // not found, accumulate
+ output.insert(componenRequireEx);
+ }
+ }
+ componentNum++;
+ }
+ return succeeded;
+}
+
+/// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps)
+static void getTrivialRemovals(const ComponentContainer & components, const RequireExSet & reqs, QStringList &toRemove)
+{
+ for(const auto & component: components)
+ {
+ if(!component->m_dependencyOnly)
+ continue;
+ if(!component->m_cachedVolatile)
+ continue;
+ RequireEx reqNeedle;
+ reqNeedle.uid = component->m_uid;
+ const auto iter = reqs.find(reqNeedle);
+ if(iter == reqs.cend())
+ {
+ toRemove.append(component->m_uid);
+ }
+ }
+}
+
+/**
+ * handles:
+ * - trivial addition (there is an unmet requirement and it can be trivially met by adding something)
+ * - trivial version conflict of dependencies == explicit version required and installed is different
+ *
+ * toAdd - set of requirements than mean adding a new component
+ * toChange - set of requirements that mean changing version of an existing component
+ */
+static bool getTrivialComponentChanges(const ComponentIndex & index, const RequireExSet & input, RequireExSet & toAdd, RequireExSet & toChange)
+{
+ enum class Decision
+ {
+ Undetermined,
+ Met,
+ Missing,
+ VersionNotSame,
+ LockedVersionNotSame
+ } decision = Decision::Undetermined;
+
+ QString reqStr;
+ bool succeeded = true;
+ // list the composed requirements and say if they are met or unmet
+ for(auto & req: input)
+ {
+ do
+ {
+ if(req.equalsVersion.isEmpty())
+ {
+ reqStr = QString("Req: %1").arg(req.uid);
+ if(index.contains(req.uid))
+ {
+ decision = Decision::Met;
+ }
+ else
+ {
+ toAdd.insert(req);
+ decision = Decision::Missing;
+ }
+ break;
+ }
+ else
+ {
+ reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion);
+ const auto & compIter = index.find(req.uid);
+ if(compIter == index.cend())
+ {
+ toAdd.insert(req);
+ decision = Decision::Missing;
+ break;
+ }
+ auto & comp = (*compIter);
+ if(comp->getVersion() != req.equalsVersion)
+ {
+ if(comp->isCustom()) {
+ decision = Decision::LockedVersionNotSame;
+ } else {
+ if(comp->m_dependencyOnly)
+ {
+ decision = Decision::VersionNotSame;
+ }
+ else
+ {
+ decision = Decision::LockedVersionNotSame;
+ }
+ }
+ break;
+ }
+ decision = Decision::Met;
+ }
+ } while(false);
+ switch(decision)
+ {
+ case Decision::Undetermined:
+ qCritical() << "No decision for" << reqStr;
+ succeeded = false;
+ break;
+ case Decision::Met:
+ qDebug() << reqStr << "Is met.";
+ break;
+ case Decision::Missing:
+ qDebug() << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee;
+ toAdd.insert(req);
+ break;
+ case Decision::VersionNotSame:
+ qDebug() << reqStr << "already has different version that can be changed.";
+ toChange.insert(req);
+ break;
+ case Decision::LockedVersionNotSame:
+ qDebug() << reqStr << "already has different version that cannot be changed.";
+ succeeded = false;
+ break;
+ }
+ }
+ return succeeded;
+}
+
+// FIXME, TODO: decouple dependency resolution from loading
+// FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses.
+// FIXME: throw all this away and use a graph
+void ComponentUpdateTask::resolveDependencies(bool checkOnly)
+{
+ qDebug() << "Resolving dependencies";
+ /*
+ * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways:
+ * 1. There are conflicting dependencies on the same uid with different exact version numbers
+ * -> hard error
+ * 2. A dependency has non-matching exact version number
+ * -> hard error
+ * 3. A dependency is entirely missing and needs to be injected before the dependee(s)
+ * -> requirements are injected
+ *
+ * NOTE: this is a placeholder and should eventually be replaced with something 'serious'
+ */
+ auto & components = d->m_list->d->components;
+ auto & componentIndex = d->m_list->d->componentIndex;
+
+ RequireExSet allRequires;
+ QStringList toRemove;
+ do
+ {
+ allRequires.clear();
+ toRemove.clear();
+ if(!gatherRequirementsFromComponents(components, allRequires))
+ {
+ emitFailed(tr("Conflicting requirements detected during dependency checking!"));
+ return;
+ }
+ getTrivialRemovals(components, allRequires, toRemove);
+ if(!toRemove.isEmpty())
+ {
+ qDebug() << "Removing obsolete components...";
+ for(auto & remove : toRemove)
+ {
+ qDebug() << "Removing" << remove;
+ d->m_list->remove(remove);
+ }
+ }
+ } while (!toRemove.isEmpty());
+ RequireExSet toAdd;
+ RequireExSet toChange;
+ bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange);
+ if(!succeeded)
+ {
+ emitFailed(tr("Instance has conflicting dependencies."));
+ return;
+ }
+ if(checkOnly)
+ {
+ if(toAdd.size() || toChange.size())
+ {
+ emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch."));
+ }
+ else
+ {
+ emitSucceeded();
+ }
+ return;
+ }
+
+ bool recursionNeeded = false;
+ if(toAdd.size())
+ {
+ // add stuff...
+ for(auto &add: toAdd)
+ {
+ ComponentPtr component = new Component(d->m_list, add.uid);
+ if(!add.equalsVersion.isEmpty())
+ {
+ // exact version
+ qDebug() << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee;
+ component->m_version = add.equalsVersion;
+ }
+ else
+ {
+ // version needs to be decided
+ qDebug() << "Adding" << add.uid << "at position" << add.indexOfFirstDependee;
+// ############################################################################################################
+// HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded.
+ if(!add.suggests.isEmpty())
+ {
+ component->m_version = add.suggests;
+ }
+ else
+ {
+ if(add.uid == "org.lwjgl")
+ {
+ component->m_version = "2.9.1";
+ }
+ else if (add.uid == "org.lwjgl3")
+ {
+ component->m_version = "3.1.2";
+ }
+ else if (add.uid == "net.fabricmc.intermediary")
+ {
+ auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){
+ return cmp->getID() == "net.minecraft";
+ });
+ if(minecraft != components.end()) {
+ component->m_version = (*minecraft)->getVersion();
+ }
+ }
+ }
+// HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded.
+// ############################################################################################################
+ }
+ component->m_dependencyOnly = true;
+ // FIXME: this should not work directly with the component list
+ d->m_list->insertComponent(add.indexOfFirstDependee, component);
+ componentIndex[add.uid] = component;
+ }
+ recursionNeeded = true;
+ }
+ if(toChange.size())
+ {
+ // change a version of something that exists
+ for(auto &change: toChange)
+ {
+ // FIXME: this should not work directly with the component list
+ qDebug() << "Setting version of " << change.uid << "to" << change.equalsVersion;
+ auto component = componentIndex[change.uid];
+ component->setVersion(change.equalsVersion);
+ }
+ recursionNeeded = true;
+ }
+
+ if(recursionNeeded)
+ {
+ loadComponents();
+ }
+ else
+ {
+ emitSucceeded();
+ }
+}
+
+void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex)
+{
+ auto &taskSlot = d->remoteLoadStatusList[taskIndex];
+ if(taskSlot.finished)
+ {
+ qWarning() << "Got multiple results from remote load task" << taskIndex;
+ return;
+ }
+ qDebug() << "Remote task" << taskIndex << "succeeded";
+ taskSlot.succeeded = false;
+ taskSlot.finished = true;
+ d->remoteTasksInProgress --;
+ // update the cached data of the component from the downloaded version file.
+ if (taskSlot.type == RemoteLoadStatus::Type::Version)
+ {
+ auto component = d->m_list->getComponent(taskSlot.PackProfileIndex);
+ component->m_loaded = true;
+ component->updateCachedData();
+ }
+ checkIfAllFinished();
+}
+
+
+void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg)
+{
+ auto &taskSlot = d->remoteLoadStatusList[taskIndex];
+ if(taskSlot.finished)
+ {
+ qWarning() << "Got multiple results from remote load task" << taskIndex;
+ return;
+ }
+ qDebug() << "Remote task" << taskIndex << "failed: " << msg;
+ d->remoteLoadSuccessful = false;
+ taskSlot.succeeded = false;
+ taskSlot.finished = true;
+ taskSlot.error = msg;
+ d->remoteTasksInProgress --;
+ checkIfAllFinished();
+}
+
+void ComponentUpdateTask::checkIfAllFinished()
+{
+ if(d->remoteTasksInProgress)
+ {
+ // not yet...
+ return;
+ }
+ if(d->remoteLoadSuccessful)
+ {
+ // nothing bad happened... clear the temp load status and proceed with looking at dependencies
+ d->remoteLoadStatusList.clear();
+ resolveDependencies(d->mode == Mode::Launch);
+ }
+ else
+ {
+ // remote load failed... report error and bail
+ QStringList allErrorsList;
+ for(auto & item: d->remoteLoadStatusList)
+ {
+ if(!item.succeeded)
+ {
+ allErrorsList.append(item.error);
+ }
+ }
+ auto allErrors = allErrorsList.join("\n");
+ emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors));
+ d->remoteLoadStatusList.clear();
+ }
+}
diff --git a/launcher/minecraft/ComponentUpdateTask.h b/launcher/minecraft/ComponentUpdateTask.h
new file mode 100644
index 00000000..4274cabb
--- /dev/null
+++ b/launcher/minecraft/ComponentUpdateTask.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/Mode.h"
+
+#include <memory>
+class PackProfile;
+struct ComponentUpdateTaskData;
+
+class ComponentUpdateTask : public Task
+{
+ Q_OBJECT
+public:
+ enum class Mode
+ {
+ Launch,
+ Resolution
+ };
+
+public:
+ explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile * list, QObject *parent = 0);
+ virtual ~ComponentUpdateTask();
+
+protected:
+ void executeTask();
+
+private:
+ void loadComponents();
+ void resolveDependencies(bool checkOnly);
+
+ void remoteLoadSucceeded(size_t index);
+ void remoteLoadFailed(size_t index, const QString &msg);
+ void checkIfAllFinished();
+
+private:
+ std::unique_ptr<ComponentUpdateTaskData> d;
+};
diff --git a/launcher/minecraft/ComponentUpdateTask_p.h b/launcher/minecraft/ComponentUpdateTask_p.h
new file mode 100644
index 00000000..5b02431b
--- /dev/null
+++ b/launcher/minecraft/ComponentUpdateTask_p.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <cstddef>
+#include <QString>
+#include <QList>
+#include "net/Mode.h"
+
+class PackProfile;
+
+struct RemoteLoadStatus
+{
+ enum class Type
+ {
+ Index,
+ List,
+ Version
+ } type = Type::Version;
+ size_t PackProfileIndex = 0;
+ bool finished = false;
+ bool succeeded = false;
+ QString error;
+};
+
+struct ComponentUpdateTaskData
+{
+ PackProfile * m_list = nullptr;
+ QList<RemoteLoadStatus> remoteLoadStatusList;
+ bool remoteLoadSuccessful = true;
+ size_t remoteTasksInProgress = 0;
+ ComponentUpdateTask::Mode mode;
+ Net::Mode netmode;
+};
diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h
new file mode 100644
index 00000000..60e0a726
--- /dev/null
+++ b/launcher/minecraft/GradleSpecifier.h
@@ -0,0 +1,151 @@
+#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
+ 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar"
+ 1 "org.gradle.test.classifiers"
+ 2 "service"
+ 3 "1.0"
+ 4 "jdk15"
+ 5 "jar"
+ */
+ QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?");
+ m_valid = matcher.exactMatch(value);
+ if(!m_valid) {
+ m_invalidValue = value;
+ return *this;
+ }
+ auto elements = matcher.capturedTexts();
+ m_groupId = elements[1];
+ m_artifactId = elements[2];
+ m_version = elements[3];
+ m_classifier = elements[4];
+ if(!elements[5].isEmpty())
+ {
+ m_extension = elements[5];
+ }
+ return *this;
+ }
+ QString serialize() const
+ {
+ if(!m_valid) {
+ return m_invalidValue;
+ }
+ 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 getFileName() const
+ {
+ if(!m_valid) {
+ return QString();
+ }
+ QString filename = m_artifactId + '-' + m_version;
+ if(!m_classifier.isEmpty())
+ {
+ filename += "-" + m_classifier;
+ }
+ filename += "." + m_extension;
+ return filename;
+ }
+ QString toPath(const QString & filenameOverride = QString()) const
+ {
+ if(!m_valid) {
+ return QString();
+ }
+ QString filename;
+ if(filenameOverride.isEmpty())
+ {
+ filename = getFileName();
+ }
+ else
+ {
+ filename = filenameOverride;
+ }
+ QString path = m_groupId;
+ path.replace('.', '/');
+ path += '/' + m_artifactId + '/' + m_version + '/' + filename;
+ 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_invalidValue;
+ 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/launcher/minecraft/GradleSpecifier_test.cpp b/launcher/minecraft/GradleSpecifier_test.cpp
new file mode 100644
index 00000000..0900c9d8
--- /dev/null
+++ b/launcher/minecraft/GradleSpecifier_test.cpp
@@ -0,0 +1,78 @@
+#include <QTest>
+#include "TestUtil.h"
+
+#include "minecraft/GradleSpecifier.h"
+
+class GradleSpecifierTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void initTestCase()
+ {
+
+ }
+ void cleanupTestCase()
+ {
+
+ }
+
+ void test_Positive_data()
+ {
+ QTest::addColumn<QString>("through");
+
+ QTest::newRow("3 parter") << "org.gradle.test.classifiers:service:1.0";
+ QTest::newRow("classifier") << "org.gradle.test.classifiers:service:1.0:jdk15";
+ QTest::newRow("jarextension") << "org.gradle.test.classifiers:service:1.0@jar";
+ QTest::newRow("jarboth") << "org.gradle.test.classifiers:service:1.0:jdk15@jar";
+ QTest::newRow("packxz") << "org.gradle.test.classifiers:service:1.0:jdk15@jar.pack.xz";
+ }
+ void test_Positive()
+ {
+ QFETCH(QString, through);
+
+ QString converted = GradleSpecifier(through).serialize();
+
+ QCOMPARE(converted, through);
+ }
+
+ void test_Path_data()
+ {
+ QTest::addColumn<QString>("spec");
+ QTest::addColumn<QString>("expected");
+
+ QTest::newRow("3 parter") << "group.id:artifact:1.0" << "group/id/artifact/1.0/artifact-1.0.jar";
+ QTest::newRow("doom") << "id.software:doom:1.666:demons@wad" << "id/software/doom/1.666/doom-1.666-demons.wad";
+ }
+ void test_Path()
+ {
+ QFETCH(QString, spec);
+ QFETCH(QString, expected);
+
+ QString converted = GradleSpecifier(spec).toPath();
+
+ QCOMPARE(converted, expected);
+ }
+ void test_Negative_data()
+ {
+ QTest::addColumn<QString>("input");
+
+ QTest::newRow("too many :") << "org:gradle.test:class:::ifiers:service:1.0::";
+ QTest::newRow("nonsense") << "I like turtles";
+ QTest::newRow("empty string") << "";
+ QTest::newRow("missing version") << "herp.derp:artifact";
+ }
+ void test_Negative()
+ {
+ QFETCH(QString, input);
+
+ GradleSpecifier spec(input);
+ QVERIFY(!spec.valid());
+ QCOMPARE(spec.serialize(), input);
+ QCOMPARE(spec.toPath(), QString());
+ }
+};
+
+QTEST_GUILESS_MAIN(GradleSpecifierTest)
+
+#include "GradleSpecifier_test.moc"
diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp
new file mode 100644
index 00000000..41705187
--- /dev/null
+++ b/launcher/minecraft/LaunchProfile.cpp
@@ -0,0 +1,319 @@
+#include "LaunchProfile.h"
+#include <Version.h>
+
+void LaunchProfile::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_mavenFiles.clear();
+ m_traits.clear();
+ m_jarMods.clear();
+ m_mainJar.reset();
+ m_problemSeverity = ProblemSeverity::None;
+}
+
+static void applyString(const QString & from, QString & to)
+{
+ if(from.isEmpty())
+ return;
+ to = from;
+}
+
+void LaunchProfile::applyMinecraftVersion(const QString& id)
+{
+ applyString(id, this->m_minecraftVersion);
+}
+
+void LaunchProfile::applyAppletClass(const QString& appletClass)
+{
+ applyString(appletClass, this->m_appletClass);
+}
+
+void LaunchProfile::applyMainClass(const QString& mainClass)
+{
+ applyString(mainClass, this->m_mainClass);
+}
+
+void LaunchProfile::applyMinecraftArguments(const QString& minecraftArguments)
+{
+ applyString(minecraftArguments, this->m_minecraftArguments);
+}
+
+void LaunchProfile::applyMinecraftVersionType(const QString& type)
+{
+ applyString(type, this->m_minecraftVersionType);
+}
+
+void LaunchProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets)
+{
+ if(assets)
+ {
+ m_minecraftAssets = assets;
+ }
+}
+
+void LaunchProfile::applyTraits(const QSet<QString>& traits)
+{
+ this->m_traits.unite(traits);
+}
+
+void LaunchProfile::applyTweakers(const QStringList& tweakers)
+{
+ // if the applied tweakers override an existing one, skip it. this effectively moves it later in the sequence
+ QStringList newTweakers;
+ for(auto & tweaker: m_tweakers)
+ {
+ if (tweakers.contains(tweaker))
+ {
+ continue;
+ }
+ newTweakers.append(tweaker);
+ }
+ // then just append the new tweakers (or moved original ones)
+ newTweakers += tweakers;
+ m_tweakers = newTweakers;
+}
+
+void LaunchProfile::applyJarMods(const QList<LibraryPtr>& 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 LaunchProfile::applyMods(const QList<LibraryPtr>& mods)
+{
+ QList<LibraryPtr> * list = &m_mods;
+ for(auto & mod: mods)
+ {
+ auto modCopy = Library::limitedCopy(mod);
+
+ // find the mod by name.
+ const int index = findLibraryByName(list, mod->rawName());
+ // mod not found? just add it.
+ if (index < 0)
+ {
+ list->append(modCopy);
+ return;
+ }
+
+ auto existingLibrary = list->at(index);
+ // if we are higher it means we should update
+ if (Version(mod->version()) > Version(existingLibrary->version()))
+ {
+ list->replace(index, modCopy);
+ }
+ }
+}
+
+void LaunchProfile::applyLibrary(LibraryPtr library)
+{
+ if(!library->isActive())
+ {
+ return;
+ }
+
+ QList<LibraryPtr> * list = &m_libraries;
+ if(library->isNative())
+ {
+ list = &m_nativeLibraries;
+ }
+
+ auto libraryCopy = Library::limitedCopy(library);
+
+ // find the library by name.
+ const int index = findLibraryByName(list, library->rawName());
+ // library not found? just add it.
+ if (index < 0)
+ {
+ list->append(libraryCopy);
+ return;
+ }
+
+ auto existingLibrary = list->at(index);
+ // if we are higher it means we should update
+ if (Version(library->version()) > Version(existingLibrary->version()))
+ {
+ list->replace(index, libraryCopy);
+ }
+}
+
+void LaunchProfile::applyMavenFile(LibraryPtr mavenFile)
+{
+ if(!mavenFile->isActive())
+ {
+ return;
+ }
+
+ if(mavenFile->isNative())
+ {
+ return;
+ }
+
+ // unlike libraries, we do not keep only one version or try to dedupe them
+ m_mavenFiles.append(Library::limitedCopy(mavenFile));
+}
+
+const LibraryPtr LaunchProfile::getMainJar() const
+{
+ return m_mainJar;
+}
+
+void LaunchProfile::applyMainJar(LibraryPtr jar)
+{
+ if(jar)
+ {
+ m_mainJar = jar;
+ }
+}
+
+void LaunchProfile::applyProblemSeverity(ProblemSeverity severity)
+{
+ if (m_problemSeverity < severity)
+ {
+ m_problemSeverity = severity;
+ }
+}
+
+const QList<PatchProblem> LaunchProfile::getProblems() const
+{
+ // FIXME: implement something that actually makes sense here
+ return {};
+}
+
+QString LaunchProfile::getMinecraftVersion() const
+{
+ return m_minecraftVersion;
+}
+
+QString LaunchProfile::getAppletClass() const
+{
+ return m_appletClass;
+}
+
+QString LaunchProfile::getMainClass() const
+{
+ return m_mainClass;
+}
+
+const QSet<QString> &LaunchProfile::getTraits() const
+{
+ return m_traits;
+}
+
+const QStringList & LaunchProfile::getTweakers() const
+{
+ return m_tweakers;
+}
+
+bool LaunchProfile::hasTrait(const QString& trait) const
+{
+ return m_traits.contains(trait);
+}
+
+ProblemSeverity LaunchProfile::getProblemSeverity() const
+{
+ return m_problemSeverity;
+}
+
+QString LaunchProfile::getMinecraftVersionType() const
+{
+ return m_minecraftVersionType;
+}
+
+std::shared_ptr<MojangAssetIndexInfo> LaunchProfile::getMinecraftAssets() const
+{
+ if(!m_minecraftAssets)
+ {
+ return std::make_shared<MojangAssetIndexInfo>("legacy");
+ }
+ return m_minecraftAssets;
+}
+
+QString LaunchProfile::getMinecraftArguments() const
+{
+ return m_minecraftArguments;
+}
+
+const QList<LibraryPtr> & LaunchProfile::getJarMods() const
+{
+ return m_jarMods;
+}
+
+const QList<LibraryPtr> & LaunchProfile::getLibraries() const
+{
+ return m_libraries;
+}
+
+const QList<LibraryPtr> & LaunchProfile::getNativeLibraries() const
+{
+ return m_nativeLibraries;
+}
+
+const QList<LibraryPtr> & LaunchProfile::getMavenFiles() const
+{
+ return m_mavenFiles;
+}
+
+void LaunchProfile::getLibraryFiles(
+ const QString& architecture,
+ QStringList& jars,
+ QStringList& nativeJars,
+ const QString& overridePath,
+ const QString& tempPath
+) const
+{
+ QStringList native32, native64;
+ jars.clear();
+ nativeJars.clear();
+ for (auto lib : getLibraries())
+ {
+ lib->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath);
+ }
+ // NOTE: order is important here, add main jar last to the lists
+ if(m_mainJar)
+ {
+ // FIXME: HACK!! jar modding is weird and unsystematic!
+ if(m_jarMods.size())
+ {
+ QDir tempDir(tempPath);
+ jars.append(tempDir.absoluteFilePath("minecraft.jar"));
+ }
+ else
+ {
+ m_mainJar->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath);
+ }
+ }
+ for (auto lib : getNativeLibraries())
+ {
+ lib->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath);
+ }
+ if(architecture == "32")
+ {
+ nativeJars.append(native32);
+ }
+ else if(architecture == "64")
+ {
+ nativeJars.append(native64);
+ }
+}
diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h
new file mode 100644
index 00000000..c1752531
--- /dev/null
+++ b/launcher/minecraft/LaunchProfile.h
@@ -0,0 +1,104 @@
+#pragma once
+#include <QString>
+#include "Library.h"
+#include <ProblemProvider.h>
+
+class LaunchProfile: public ProblemProvider
+{
+public:
+ virtual ~LaunchProfile() {};
+
+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<LibraryPtr> &jarMods);
+ void applyMods(const QList<LibraryPtr> &jarMods);
+ void applyLibrary(LibraryPtr library);
+ void applyMavenFile(LibraryPtr library);
+ void applyMainJar(LibraryPtr jar);
+ void applyProblemSeverity(ProblemSeverity severity);
+ /// clear the profile
+ void clear();
+
+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<LibraryPtr> & getJarMods() const;
+ const QList<LibraryPtr> & getLibraries() const;
+ const QList<LibraryPtr> & getNativeLibraries() const;
+ const QList<LibraryPtr> & getMavenFiles() const;
+ const LibraryPtr getMainJar() const;
+ void getLibraryFiles(
+ const QString & architecture,
+ QStringList & jars,
+ QStringList & nativeJars,
+ const QString & overridePath,
+ const QString & tempPath
+ ) const;
+ bool hasTrait(const QString & trait) const;
+ ProblemSeverity getProblemSeverity() const override;
+ const QList<PatchProblem> getProblems() const override;
+
+private:
+ /// 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;
+
+ /**
+ * 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;
+
+ /// the list of maven files to be placed in the libraries folder, but not acted upon
+ QList<LibraryPtr> m_mavenFiles;
+
+ /// the main jar
+ LibraryPtr m_mainJar;
+
+ /// the list of native libraries
+ QList<LibraryPtr> m_nativeLibraries;
+
+ /// 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<LibraryPtr> m_jarMods;
+
+ /// the list of mods
+ QList<LibraryPtr> m_mods;
+
+ ProblemSeverity m_problemSeverity = ProblemSeverity::None;
+
+};
diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp
new file mode 100644
index 00000000..f2293679
--- /dev/null
+++ b/launcher/minecraft/Library.cpp
@@ -0,0 +1,309 @@
+#include "Library.h"
+#include "MinecraftInstance.h"
+
+#include <net/Download.h>
+#include <net/ChecksumValidator.h>
+#include <Env.h>
+#include <FileSystem.h>
+#include <BuildConfig.h>
+
+
+void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& native, QStringList& native32,
+ QStringList& native64, const QString &overridePath) const
+{
+ bool local = isLocal();
+ auto actualPath = [&](QString relPath)
+ {
+ QFileInfo out(FS::PathCombine(storagePrefix(), relPath));
+ if(local && !overridePath.isEmpty())
+ {
+ QString fileName = out.fileName();
+ return QFileInfo(FS::PathCombine(overridePath, fileName)).absoluteFilePath();
+ }
+ return out.absoluteFilePath();
+ };
+ 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< std::shared_ptr< NetAction > > Library::getDownloads(
+ OpSys system,
+ class HttpMetaCache* cache,
+ QStringList& failedLocalFiles,
+ const QString & overridePath
+) const
+{
+ QList<NetActionPtr> out;
+ bool stale = isAlwaysStale();
+ bool local = isLocal();
+
+ auto check_local_file = [&](QString storage)
+ {
+ QFileInfo fileinfo(storage);
+ QString fileName = fileinfo.fileName();
+ auto fullPath = FS::PathCombine(overridePath, fileName);
+ QFileInfo localFileInfo(fullPath);
+ if(!localFileInfo.exists())
+ {
+ failedLocalFiles.append(localFileInfo.filePath());
+ return false;
+ }
+ return true;
+ };
+
+ auto add_download = [&](QString storage, QString url, QString sha1)
+ {
+ if(local)
+ {
+ return check_local_file(storage);
+ }
+ auto entry = cache->resolveEntry("libraries", storage);
+ if(stale)
+ {
+ entry->setStale(true);
+ }
+ if (!entry->isStale())
+ return true;
+ Net::Download::Options options;
+ if(stale)
+ {
+ options |= Net::Download::Option::AcceptLocalFiles;
+ }
+
+ if(sha1.size())
+ {
+ auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
+ auto dl = Net::Download::makeCached(url, entry, options);
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url;
+ out.append(dl);
+ }
+ else
+ {
+ out.append(Net::Download::makeCached(url, entry, options));
+ qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url;
+ }
+ return true;
+ };
+
+ QString raw_storage = storageSuffix(system);
+ if(m_mojangDownloads)
+ {
+ if(isNative())
+ {
+ 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)
+ {
+ auto cooked_storage = raw_storage;
+ cooked_storage.replace("${arch}", "32");
+ add_download(cooked_storage, nat32info->url, nat32info->sha1);
+ }
+ auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier);
+ if(nat64info)
+ {
+ auto cooked_storage = raw_storage;
+ cooked_storage.replace("${arch}", "64");
+ add_download(cooked_storage, nat64info->url, nat64info->sha1);
+ }
+ }
+ else
+ {
+ auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier);
+ if(info)
+ {
+ add_download(raw_storage, info->url, info->sha1);
+ }
+ }
+ }
+ else
+ {
+ qDebug() << "Ignoring native library" << m_name.serialize() << "because it has no classifier for current OS";
+ }
+ }
+ else
+ {
+ if(m_mojangDownloads->artifact)
+ {
+ auto artifact = m_mojangDownloads->artifact;
+ add_download(raw_storage, artifact->url, artifact->sha1);
+ }
+ else
+ {
+ qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact";
+ }
+ }
+ }
+ else
+ {
+ auto raw_dl = [&]()
+ {
+ if (!m_absoluteURL.isEmpty())
+ {
+ return m_absoluteURL;
+ }
+
+ if (m_repositoryURL.isEmpty())
+ {
+ return BuildConfig.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"), QString());
+ cooked_storage = raw_storage;
+ cooked_dl = raw_dl;
+ add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64"), QString());
+ }
+ else
+ {
+ add_download(raw_storage, raw_dl, QString());
+ }
+ }
+ 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;
+}
+
+bool Library::isLocal() const
+{
+ return m_hint == "local";
+}
+
+bool Library::isAlwaysStale() const
+{
+ return m_hint == "always-stale";
+}
+
+void Library::setStoragePrefix(QString prefix)
+{
+ m_storagePrefix = prefix;
+}
+
+QString Library::defaultStoragePrefix()
+{
+ return "libraries/";
+}
+
+QString Library::storagePrefix() const
+{
+ if(m_storagePrefix.isEmpty())
+ {
+ return defaultStoragePrefix();
+ }
+ return m_storagePrefix;
+}
+
+QString Library::filename(OpSys system) const
+{
+ if(!m_filename.isEmpty())
+ {
+ return m_filename;
+ }
+ // non-native? use only the gradle specifier
+ if (!isNative())
+ {
+ return m_name.getFileName();
+ }
+
+ // 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.getFileName();
+}
+
+QString Library::displayName(OpSys system) const
+{
+ if(!m_displayname.isEmpty())
+ return m_displayname;
+ return filename(system);
+}
+
+QString Library::storageSuffix(OpSys system) const
+{
+ // non-native? use only the gradle specifier
+ if (!isNative())
+ {
+ return m_name.toPath(m_filename);
+ }
+
+ // 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(m_filename);
+}
diff --git a/launcher/minecraft/Library.h b/launcher/minecraft/Library.h
new file mode 100644
index 00000000..119b4a86
--- /dev/null
+++ b/launcher/minecraft/Library.h
@@ -0,0 +1,217 @@
+#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 "MojangDownloadInfo.h"
+
+class Library;
+class MinecraftInstance;
+
+typedef std::shared_ptr<Library> LibraryPtr;
+
+class 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;
+ newlib->m_filename = base->m_filename;
+ 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 QString & overridePath) const;
+
+ void setAbsoluteUrl(const QString &absolute_url)
+ {
+ m_absoluteURL = absolute_url;
+ }
+
+ void setFilename(const QString &filename)
+ {
+ m_filename = filename;
+ }
+
+ /// Get the file name of the library
+ QString filename(OpSys system) const;
+
+ // DEPRECATED: set a display name, used by jar mods only
+ void setDisplayName(const QString & displayName)
+ {
+ m_displayname = displayName;
+ }
+
+ /// Get the file name of the library
+ QString displayName(OpSys system) const;
+
+ 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;
+
+ /// Returns true if the library is contained in an instance and false if it is shared
+ bool isLocal() const;
+
+ /// Returns true if the library is to always be checked for updates
+ bool isAlwaysStale() const;
+
+ /// Return true if the library requires forge XZ hacks
+ bool isForge() const;
+
+ // Get a list of downloads for this library
+ QList<NetActionPtr> getDownloads(OpSys system, class HttpMetaCache * cache,
+ QStringList & failedLocalFiles, const QString & overridePath) 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 file 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 extension - filename override
+ QString m_filename;
+
+ /// DEPRECATED MultiMC extension - display name
+ QString m_displayname;
+
+ /**
+ * 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/launcher/minecraft/Library_test.cpp b/launcher/minecraft/Library_test.cpp
new file mode 100644
index 00000000..75bb4db1
--- /dev/null
+++ b/launcher/minecraft/Library_test.cpp
@@ -0,0 +1,272 @@
+#include <QTest>
+#include "TestUtil.h"
+
+#include "minecraft/MojangVersionFormat.h"
+#include "minecraft/OneSixVersionFormat.h"
+#include "minecraft/Library.h"
+#include "net/HttpMetaCache.h"
+#include "FileSystem.h"
+
+class LibraryTest : public QObject
+{
+ Q_OBJECT
+private:
+ LibraryPtr readMojangJson(const char *file)
+ {
+ auto path = QFINDTESTDATA(file);
+ QFile jsonFile(path);
+ jsonFile.open(QIODevice::ReadOnly);
+ auto data = jsonFile.readAll();
+ jsonFile.close();
+ ProblemContainer problems;
+ return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), file);
+ }
+ // get absolute path to expected storage, assuming default cache prefix
+ QStringList getStorage(QString relative)
+ {
+ return {FS::PathCombine(cache->getBasePath("libraries"), relative)};
+ }
+private
+slots:
+ void initTestCase()
+ {
+ cache.reset(new HttpMetaCache());
+ cache->addBase("libraries", QDir("libraries").absolutePath());
+ dataDir = QDir("data").absolutePath();
+ }
+ void test_legacy()
+ {
+ Library test("test.package:testname:testversion");
+ QCOMPARE(test.artifactPrefix(), QString("test.package:testname"));
+ QCOMPARE(test.isNative(), false);
+
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(currentSystem, jar, native, native32, native64, QString());
+ QCOMPARE(jar, getStorage("test/package/testname/testversion/testname-testversion.jar"));
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ void test_legacy_url()
+ {
+ QStringList failedFiles;
+ Library test("test.package:testname:testversion");
+ test.setRepositoryURL("file://foo/bar");
+ auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString());
+ QCOMPARE(downloads.size(), 1);
+ QCOMPARE(failedFiles, {});
+ NetActionPtr dl = downloads[0];
+ QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion.jar"));
+ }
+ void test_legacy_url_local_broken()
+ {
+ Library test("test.package:testname:testversion");
+ QCOMPARE(test.isNative(), false);
+ QStringList failedFiles;
+ test.setHint("local");
+ auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString());
+ QCOMPARE(downloads.size(), 0);
+ QCOMPARE(failedFiles, {"testname-testversion.jar"});
+ }
+ void test_legacy_url_local_override()
+ {
+ Library test("com.paulscode:codecwav:20101023");
+ QCOMPARE(test.isNative(), false);
+ QStringList failedFiles;
+ test.setHint("local");
+ auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString("data"));
+ QCOMPARE(downloads.size(), 0);
+ qDebug() << failedFiles;
+ QCOMPARE(failedFiles.size(), 0);
+
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(currentSystem, jar, native, native32, native64, QString("data"));
+ QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ void test_legacy_native()
+ {
+ Library test("test.package:testname:testversion");
+ test.m_nativeClassifiers[OpSys::Os_Linux]="linux";
+ QCOMPARE(test.isNative(), true);
+ test.setRepositoryURL("file://foo/bar");
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, getStorage("test/package/testname/testversion/testname-testversion-linux.jar"));
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 1);
+ QCOMPARE(failedFiles, {});
+ auto dl = dls[0];
+ QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux.jar"));
+ }
+ }
+ void test_legacy_native_arch()
+ {
+ Library test("test.package:testname:testversion");
+ test.m_nativeClassifiers[OpSys::Os_Linux]="linux-${arch}";
+ test.m_nativeClassifiers[OpSys::Os_OSX]="osx-${arch}";
+ test.m_nativeClassifiers[OpSys::Os_Windows]="windows-${arch}";
+ QCOMPARE(test.isNative(), true);
+ test.setRepositoryURL("file://foo/bar");
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-linux-32.jar"));
+ QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar"));
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 2);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-32.jar"));
+ QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-64.jar"));
+ }
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_Windows, jar, native, native32, native64, QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-windows-32.jar"));
+ QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-windows-64.jar"));
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_Windows, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 2);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-32.jar"));
+ QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-64.jar"));
+ }
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_OSX, jar, native, native32, native64, QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-osx-32.jar"));
+ QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-osx-64.jar"));
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_OSX, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 2);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-32.jar"));
+ QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-64.jar"));
+ }
+ }
+ void test_legacy_native_arch_local_override()
+ {
+ Library test("test.package:testname:testversion");
+ test.m_nativeClassifiers[OpSys::Os_Linux]="linux-${arch}";
+ test.setHint("local");
+ QCOMPARE(test.isNative(), true);
+ test.setRepositoryURL("file://foo/bar");
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString("data"));
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {QFileInfo("data/testname-testversion-linux-32.jar").absoluteFilePath()});
+ QCOMPARE(native64, {QFileInfo("data/testname-testversion-linux-64.jar").absoluteFilePath()});
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString("data"));
+ QCOMPARE(dls.size(), 0);
+ QCOMPARE(failedFiles, {"data/testname-testversion-linux-64.jar"});
+ }
+ }
+ void test_onenine()
+ {
+ auto test = readMojangJson("data/lib-simple.json");
+ {
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString());
+ QCOMPARE(jar, getStorage("com/paulscode/codecwav/20101023/codecwav-20101023.jar"));
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ {
+ QStringList failedFiles;
+ auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 1);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar"));
+ }
+ test->setHint("local");
+ {
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString("data"));
+ QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ {
+ QStringList failedFiles;
+ auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString("data"));
+ QCOMPARE(dls.size(), 0);
+ QCOMPARE(failedFiles, {});
+ }
+ }
+ void test_onenine_local_override()
+ {
+ auto test = readMojangJson("data/lib-simple.json");
+ test->setHint("local");
+ {
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString("data"));
+ QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ {
+ QStringList failedFiles;
+ auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString("data"));
+ QCOMPARE(dls.size(), 0);
+ QCOMPARE(failedFiles, {});
+ }
+ }
+ void test_onenine_native()
+ {
+ auto test = readMojangJson("data/lib-native.json");
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString());
+ QCOMPARE(jar, QStringList());
+ QCOMPARE(native, getStorage("org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"));
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ QStringList failedFiles;
+ auto dls = test->getDownloads(Os_OSX, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 1);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"));
+ }
+ void test_onenine_native_arch()
+ {
+ auto test = readMojangJson("data/lib-native-arch.json");
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_Windows, jar, native, native32, native64, QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar"));
+ QCOMPARE(native64, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar"));
+ QStringList failedFiles;
+ auto dls = test->getDownloads(Os_Windows, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 2);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar"));
+ QCOMPARE(dls[1]->m_url, QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar"));
+ }
+private:
+ std::unique_ptr<HttpMetaCache> cache;
+ QString dataDir;
+};
+
+QTEST_GUILESS_MAIN(LibraryTest)
+
+#include "Library_test.moc"
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
new file mode 100644
index 00000000..dbf9f816
--- /dev/null
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -0,0 +1,1054 @@
+#include "MinecraftInstance.h"
+#include <minecraft/launch/CreateGameFolders.h>
+#include <minecraft/launch/ExtractNatives.h>
+#include <minecraft/launch/PrintInstanceInfo.h>
+#include <settings/Setting.h>
+#include "settings/SettingsObject.h"
+#include "Env.h"
+#include <MMCStrings.h>
+#include <pathmatcher/RegexpMatcher.h>
+#include <pathmatcher/MultiMatcher.h>
+#include <FileSystem.h>
+#include <java/JavaVersion.h>
+
+#include "launch/LaunchTask.h"
+#include "launch/steps/LookupServerAddress.h"
+#include "launch/steps/PostLaunchCommand.h"
+#include "launch/steps/Update.h"
+#include "launch/steps/PreLaunchCommand.h"
+#include "launch/steps/TextPrint.h"
+#include "minecraft/launch/LauncherPartLaunch.h"
+#include "minecraft/launch/DirectJavaLaunch.h"
+#include "minecraft/launch/ModMinecraftJar.h"
+#include "minecraft/launch/ClaimAccount.h"
+#include "minecraft/launch/ReconstructAssets.h"
+#include "minecraft/launch/ScanModFolders.h"
+#include "minecraft/launch/VerifyJavaInstall.h"
+#include "java/launch/CheckJava.h"
+#include "java/JavaUtils.h"
+#include "meta/Index.h"
+#include "meta/VersionList.h"
+
+#include "mod/ModFolderModel.h"
+#include "mod/ResourcePackFolderModel.h"
+#include "mod/TexturePackFolderModel.h"
+#include "WorldList.h"
+
+#include "icons/IIconList.h"
+
+#include <QCoreApplication>
+#include "PackProfile.h"
+#include "AssetsUtils.h"
+#include "MinecraftUpdate.h"
+#include "MinecraftLoadAndCheck.h"
+#include <minecraft/gameoptions/GameOptions.h>
+#include <minecraft/update/FoldersTask.h>
+
+#define IBUS "@im=ibus"
+
+// 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);
+ m_settings->registerPassthrough(globalSettings->getSetting("JavaArchitecture"), 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);
+
+ // Minecraft launch method
+ auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false);
+ m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride);
+
+ // Native library workarounds
+ auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false);
+ m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride);
+ m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride);
+
+ // Game time
+ auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false);
+ m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride);
+ m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride);
+
+ // Join server on launch, this does not have a global override
+ m_settings->registerSetting("JoinServerOnLaunch", false);
+ m_settings->registerSetting("JoinServerOnLaunchAddress", "");
+
+ // DEPRECATED: Read what versions the user configuration thinks should be used
+ m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, "");
+ m_settings->registerSetting("LWJGLVersion", "");
+ m_settings->registerSetting("ForgeVersion", "");
+ m_settings->registerSetting("LiteloaderVersion", "");
+
+ m_components.reset(new PackProfile(this));
+ m_components->setOldConfigVersion("net.minecraft", m_settings->get("IntendedVersion").toString());
+ auto setting = m_settings->getSetting("LWJGLVersion");
+ m_components->setOldConfigVersion("org.lwjgl", m_settings->get("LWJGLVersion").toString());
+ m_components->setOldConfigVersion("net.minecraftforge", m_settings->get("ForgeVersion").toString());
+ m_components->setOldConfigVersion("com.mumfrey.liteloader", m_settings->get("LiteloaderVersion").toString());
+}
+
+void MinecraftInstance::saveNow()
+{
+ m_components->saveNow();
+}
+
+QString MinecraftInstance::typeName() const
+{
+ return "Minecraft";
+}
+
+std::shared_ptr<PackProfile> MinecraftInstance::getPackProfile() const
+{
+ return m_components;
+}
+
+QSet<QString> MinecraftInstance::traits() const
+{
+ auto components = getPackProfile();
+ if (!components)
+ {
+ return {"version-incomplete"};
+ }
+ auto profile = components->getProfile();
+ if (!profile)
+ {
+ return {"version-incomplete"};
+ }
+ return profile->getTraits();
+}
+
+QString MinecraftInstance::gameRoot() const
+{
+ QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
+ QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
+
+ if (mcDir.exists() && !dotMCDir.exists())
+ return mcDir.filePath();
+ else
+ return dotMCDir.filePath();
+}
+
+QString MinecraftInstance::binRoot() const
+{
+ return FS::PathCombine(gameRoot(), "bin");
+}
+
+QString MinecraftInstance::getNativePath() const
+{
+ QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/"));
+ return natives_dir.absolutePath();
+}
+
+QString MinecraftInstance::getLocalLibraryPath() const
+{
+ QDir libraries_dir(FS::PathCombine(instanceRoot(), "libraries/"));
+ return libraries_dir.absolutePath();
+}
+
+QString MinecraftInstance::jarModsDir() const
+{
+ QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/"));
+ return jarmods_dir.absolutePath();
+}
+
+QString MinecraftInstance::loaderModsDir() const
+{
+ return FS::PathCombine(gameRoot(), "mods");
+}
+
+QString MinecraftInstance::modsCacheLocation() const
+{
+ return FS::PathCombine(instanceRoot(), "mods.cache");
+}
+
+QString MinecraftInstance::coreModsDir() const
+{
+ return FS::PathCombine(gameRoot(), "coremods");
+}
+
+QString MinecraftInstance::resourcePacksDir() const
+{
+ return FS::PathCombine(gameRoot(), "resourcepacks");
+}
+
+QString MinecraftInstance::texturePacksDir() const
+{
+ return FS::PathCombine(gameRoot(), "texturepacks");
+}
+
+QString MinecraftInstance::instanceConfigFolder() const
+{
+ return FS::PathCombine(gameRoot(), "config");
+}
+
+QString MinecraftInstance::libDir() const
+{
+ return FS::PathCombine(gameRoot(), "lib");
+}
+
+QString MinecraftInstance::worldDir() const
+{
+ return FS::PathCombine(gameRoot(), "saves");
+}
+
+QString MinecraftInstance::resourcesDir() const
+{
+ return FS::PathCombine(gameRoot(), "resources");
+}
+
+QDir MinecraftInstance::librariesPath() const
+{
+ return QDir::current().absoluteFilePath("libraries");
+}
+
+QDir MinecraftInstance::jarmodsPath() const
+{
+ return QDir(jarModsDir());
+}
+
+QDir MinecraftInstance::versionsPath() const
+{
+ return QDir::current().absoluteFilePath("versions");
+}
+
+QStringList MinecraftInstance::getClassPath() const
+{
+ QStringList jars, nativeJars;
+ auto javaArchitecture = settings()->get("JavaArchitecture").toString();
+ auto profile = m_components->getProfile();
+ profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
+ return jars;
+}
+
+QString MinecraftInstance::getMainClass() const
+{
+ auto profile = m_components->getProfile();
+ return profile->getMainClass();
+}
+
+QStringList MinecraftInstance::getNativeJars() const
+{
+ QStringList jars, nativeJars;
+ auto javaArchitecture = settings()->get("JavaArchitecture").toString();
+ auto profile = m_components->getProfile();
+ profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
+ return nativeJars;
+}
+
+QStringList MinecraftInstance::extraArguments() const
+{
+ auto list = BaseInstance::extraArguments();
+ auto version = getPackProfile();
+ if (!version)
+ return list;
+ auto jarMods = getJarMods();
+ if (!jarMods.isEmpty())
+ {
+ list.append({"-Dfml.ignoreInvalidMinecraftCertificates=true",
+ "-Dfml.ignorePatchDiscrepancies=true"});
+ }
+ return list;
+}
+
+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
+ auto traits_ = traits();
+ // HACK: fix issues on macOS with 1.13 snapshots
+ // NOTE: Oracle Java option. if there are alternate jvm implementations, this would be the place to customize this for them
+#ifdef Q_OS_MAC
+ if(traits_.contains("FirstThreadOnMacOS"))
+ {
+ args << QString("-XstartOnFirstThread");
+ }
+#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
+
+ int min = settings()->get("MinMemAlloc").toInt();
+ int max = settings()->get("MaxMemAlloc").toInt();
+ if(min < max)
+ {
+ args << QString("-Xms%1m").arg(min);
+ args << QString("-Xmx%1m").arg(max);
+ }
+ else
+ {
+ args << QString("-Xms%1m").arg(max);
+ args << QString("-Xmx%1m").arg(min);
+ }
+
+ // No PermGen in newer java.
+ JavaVersion javaVersion = getJavaVersion();
+ if(javaVersion.requiresPermGen())
+ {
+ auto permgen = settings()->get("PermGen").toInt();
+ if (permgen != 64)
+ {
+ args << QString("-XX:PermSize=%1m").arg(permgen);
+ }
+ }
+
+ args << "-Duser.language=en";
+
+ 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(gameRoot()).absolutePath());
+ out.insert("INST_JAVA", settings()->get("JavaPath").toString());
+ out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
+ return out;
+}
+
+QProcessEnvironment MinecraftInstance::createEnvironment()
+{
+ // prepare the process environment
+ QProcessEnvironment env = CleanEnviroment();
+
+ // export some infos
+ auto variables = getVariables();
+ for (auto it = variables.begin(); it != variables.end(); ++it)
+ {
+ env.insert(it.key(), it.value());
+ }
+ return env;
+}
+
+static 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 MinecraftInstance::processMinecraftArgs(
+ AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const
+{
+ auto profile = m_components->getProfile();
+ QString args_pattern = profile->getMinecraftArguments();
+ for (auto tweaker : profile->getTweakers())
+ {
+ args_pattern += " --tweakClass " + tweaker;
+ }
+
+ if (serverToJoin && !serverToJoin->address.isEmpty())
+ {
+ args_pattern += " --server " + serverToJoin->address;
+ args_pattern += " --port " + QString::number(serverToJoin->port);
+ }
+
+ QMap<QString, QString> token_mapping;
+ // yggdrasil!
+ if(session)
+ {
+ 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;
+ token_mapping["user_properties"] = session->serializeUserProperties();
+ token_mapping["user_type"] = session->user_type;
+ }
+
+ // blatant self-promotion.
+ token_mapping["profile_name"] = token_mapping["version_name"] = "MultiMC5";
+
+ token_mapping["version_type"] = profile->getMinecraftVersionType();
+
+ QString absRootDir = QDir(gameRoot()).absolutePath();
+ token_mapping["game_directory"] = absRootDir;
+ QString absAssetsDir = QDir("assets/").absolutePath();
+ auto assets = profile->getMinecraftAssets();
+ token_mapping["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath();
+
+ // 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 MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
+{
+ QString launchScript;
+
+ if (!m_components)
+ return QString();
+ auto profile = m_components->getProfile();
+ if(!profile)
+ return QString();
+
+ auto mainClass = getMainClass();
+ if (!mainClass.isEmpty())
+ {
+ launchScript += "mainClass " + mainClass + "\n";
+ }
+ auto appletClass = profile->getAppletClass();
+ if (!appletClass.isEmpty())
+ {
+ launchScript += "appletClass " + appletClass + "\n";
+ }
+
+ if (serverToJoin && !serverToJoin->address.isEmpty())
+ {
+ launchScript += "serverAddress " + serverToJoin->address + "\n";
+ launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n";
+ }
+
+ // generic minecraft params
+ for (auto param : processMinecraftArgs(
+ session,
+ nullptr /* When using a launch script, the server parameters are handled by it*/
+ ))
+ {
+ 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
+ if(session)
+ {
+ launchScript += "userName " + session->player_name + "\n";
+ launchScript += "sessionId " + session->session + "\n";
+ }
+
+ // libraries and class path.
+ {
+ QStringList jars, nativeJars;
+ auto javaArchitecture = settings()->get("JavaArchitecture").toString();
+ profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
+ for(auto file: jars)
+ {
+ launchScript += "cp " + file + "\n";
+ }
+ for(auto file: nativeJars)
+ {
+ launchScript += "ext " + file + "\n";
+ }
+ launchScript += "natives " + getNativePath() + "\n";
+ }
+
+ for (auto trait : profile->getTraits())
+ {
+ launchScript += "traits " + trait + "\n";
+ }
+ launchScript += "launcher onesix\n";
+ // qDebug() << "Generated launch script:" << launchScript;
+ return launchScript;
+}
+
+QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
+{
+ QStringList out;
+ out << "Main Class:" << " " + getMainClass() << "";
+ out << "Native path:" << " " + getNativePath() << "";
+
+ auto profile = m_components->getProfile();
+
+ auto alltraits = traits();
+ if(alltraits.size())
+ {
+ out << "Traits:";
+ for (auto trait : alltraits)
+ {
+ out << "traits " + trait;
+ }
+ out << "";
+ }
+
+ auto settings = this->settings();
+ bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool();
+ bool nativeGLFW = settings->get("UseNativeGLFW").toBool();
+ if (nativeOpenAL || nativeGLFW)
+ {
+ if (nativeOpenAL)
+ out << "Using system OpenAL.";
+ if (nativeGLFW)
+ out << "Using system GLFW.";
+ out << "";
+ }
+
+ // libraries and class path.
+ {
+ out << "Libraries:";
+ QStringList jars, nativeJars;
+ auto javaArchitecture = settings->get("JavaArchitecture").toString();
+ profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot());
+ auto printLibFile = [&](const QString & path)
+ {
+ QFileInfo info(path);
+ if(info.exists())
+ {
+ out << " " + path;
+ }
+ else
+ {
+ out << " " + path + " (missing)";
+ }
+ };
+ for(auto file: jars)
+ {
+ printLibFile(file);
+ }
+ out << "";
+ out << "Native libraries:";
+ for(auto file: nativeJars)
+ {
+ printLibFile(file);
+ }
+ out << "";
+ }
+
+ auto printModList = [&](const QString & label, ModFolderModel & model) {
+ if(model.size())
+ {
+ out << QString("%1:").arg(label);
+ auto modList = model.allMods();
+ std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) {
+ auto aName = a.filename().completeBaseName();
+ auto bName = b.filename().completeBaseName();
+ return aName.localeAwareCompare(bName) < 0;
+ });
+ for(auto & mod: modList)
+ {
+ if(mod.type() == Mod::MOD_FOLDER)
+ {
+ out << u8" [📁] " + mod.filename().completeBaseName() + " (folder)";
+ continue;
+ }
+
+ if(mod.enabled()) {
+ out << u8" [✔️] " + mod.filename().completeBaseName();
+ }
+ else {
+ out << u8" [❌] " + mod.filename().completeBaseName() + " (disabled)";
+ }
+
+ }
+ out << "";
+ }
+ };
+
+ printModList("Mods", *(loaderModList().get()));
+ printModList("Core Mods", *(coreModList().get()));
+
+ auto & jarMods = profile->getJarMods();
+ if(jarMods.size())
+ {
+ out << "Jar Mods:";
+ for(auto & jarmod: jarMods)
+ {
+ auto displayname = jarmod->displayName(currentSystem);
+ auto realname = jarmod->filename(currentSystem);
+ if(displayname != realname)
+ {
+ out << " " + displayname + " (" + realname + ")";
+ }
+ else
+ {
+ out << " " + realname;
+ }
+ }
+ out << "";
+ }
+
+ auto params = processMinecraftArgs(nullptr, serverToJoin);
+ out << "Params:";
+ out << " " + params.join(' ');
+ out << "";
+
+ QString windowParams;
+ if (settings->get("LaunchMaximized").toBool())
+ {
+ out << "Window size: max (if available)";
+ }
+ else
+ {
+ auto width = settings->get("MinecraftWinWidth").toInt();
+ auto height = settings->get("MinecraftWinHeight").toInt();
+ out << "Window size: " + QString::number(width) + " x " + QString::number(height);
+ }
+ out << "";
+ return out;
+}
+
+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>"));
+
+ auto i = sessionRef.u.properties.begin();
+ while (i != sessionRef.u.properties.end())
+ {
+ if(i.value().length() <= 3) {
+ ++i;
+ continue;
+ }
+ 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 gameRoot();
+}
+
+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 (hasVersionBroken())
+ {
+ traits.append(tr("broken"));
+ }
+
+ QString description;
+ description.append(tr("Minecraft %1 (%2)").arg(m_components->getComponentVersion("net.minecraft")).arg(typeName()));
+ if(m_settings->get("ShowGameTime").toBool())
+ {
+ if (lastTimePlayed() > 0) {
+ description.append(tr(", last played for %1").arg(prettifyTimeDuration(lastTimePlayed())));
+ }
+
+ if (totalTimePlayed() > 0) {
+ description.append(tr(", total played for %1").arg(prettifyTimeDuration(totalTimePlayed())));
+ }
+ }
+ if(hasCrashed())
+ {
+ description.append(tr(", has crashed."));
+ }
+ return description;
+}
+
+shared_qobject_ptr<Task> MinecraftInstance::createUpdateTask(Net::Mode mode)
+{
+ switch (mode)
+ {
+ case Net::Mode::Offline:
+ {
+ return shared_qobject_ptr<Task>(new MinecraftLoadAndCheck(this));
+ }
+ case Net::Mode::Online:
+ {
+ return shared_qobject_ptr<Task>(new MinecraftUpdate(this));
+ }
+ }
+ return nullptr;
+}
+
+shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
+{
+ // FIXME: get rid of shared_from_this ...
+ auto process = LaunchTask::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this()));
+ auto pptr = process.get();
+
+ ENV.icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG");
+
+ // print a header
+ {
+ process->appendStep(new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::MultiMC));
+ }
+
+ // check java
+ {
+ process->appendStep(new CheckJava(pptr));
+ }
+
+ // check launch method
+ QStringList validMethods = {"LauncherPart", "DirectJava"};
+ QString method = launchMethod();
+ if(!validMethods.contains(method))
+ {
+ process->appendStep(new TextPrint(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal));
+ return process;
+ }
+
+ // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732)
+ {
+ process->appendStep(new CreateGameFolders(pptr));
+ }
+
+ if (!serverToJoin && m_settings->get("JoinServerOnLaunch").toBool())
+ {
+ QString fullAddress = m_settings->get("JoinServerOnLaunchAddress").toString();
+ serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress)));
+ }
+
+ if(serverToJoin && serverToJoin->port == 25565)
+ {
+ // Resolve server address to join on launch
+ auto *step = new LookupServerAddress(pptr);
+ step->setLookupAddress(serverToJoin->address);
+ step->setOutputAddressPtr(serverToJoin);
+ process->appendStep(step);
+ }
+
+ // run pre-launch command if that's needed
+ if(getPreLaunchCommand().size())
+ {
+ auto step = new PreLaunchCommand(pptr);
+ step->setWorkingDirectory(gameRoot());
+ process->appendStep(step);
+ }
+
+ // if we aren't in offline mode,.
+ if(session->status != AuthSession::PlayableOffline)
+ {
+ process->appendStep(new ClaimAccount(pptr, session));
+ process->appendStep(new Update(pptr, Net::Mode::Online));
+ }
+ else
+ {
+ process->appendStep(new Update(pptr, Net::Mode::Offline));
+ }
+
+ // if there are any jar mods
+ {
+ process->appendStep(new ModMinecraftJar(pptr));
+ }
+
+ // Scan mods folders for mods
+ {
+ process->appendStep(new ScanModFolders(pptr));
+ }
+
+ // print some instance info here...
+ {
+ process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin));
+ }
+
+ // extract native jars if needed
+ {
+ process->appendStep(new ExtractNatives(pptr));
+ }
+
+ // reconstruct assets if needed
+ {
+ process->appendStep(new ReconstructAssets(pptr));
+ }
+
+ // verify that minimum Java requirements are met
+ {
+ process->appendStep(new VerifyJavaInstall(pptr));
+ }
+
+ {
+ // actually launch the game
+ auto method = launchMethod();
+ if(method == "LauncherPart")
+ {
+ auto step = new LauncherPartLaunch(pptr);
+ step->setWorkingDirectory(gameRoot());
+ step->setAuthSession(session);
+ step->setServerToJoin(serverToJoin);
+ process->appendStep(step);
+ }
+ else if (method == "DirectJava")
+ {
+ auto step = new DirectJavaLaunch(pptr);
+ step->setWorkingDirectory(gameRoot());
+ step->setAuthSession(session);
+ step->setServerToJoin(serverToJoin);
+ process->appendStep(step);
+ }
+ }
+
+ // run post-exit command if that's needed
+ if(getPostExitCommand().size())
+ {
+ auto step = new PostLaunchCommand(pptr);
+ step->setWorkingDirectory(gameRoot());
+ process->appendStep(step);
+ }
+ if (session)
+ {
+ process->setCensorFilter(createCensorFilterFromSession(session));
+ }
+ m_launchProcess = process;
+ emit launchTaskChanged(m_launchProcess);
+ return m_launchProcess;
+}
+
+QString MinecraftInstance::launchMethod()
+{
+ return m_settings->get("MCLaunchMethod").toString();
+}
+
+JavaVersion MinecraftInstance::getJavaVersion() const
+{
+ return JavaVersion(settings()->get("JavaVersion").toString());
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const
+{
+ if (!m_loader_mod_list)
+ {
+ m_loader_mod_list.reset(new ModFolderModel(loaderModsDir()));
+ m_loader_mod_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
+ }
+ return m_loader_mod_list;
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
+{
+ if (!m_core_mod_list)
+ {
+ m_core_mod_list.reset(new ModFolderModel(coreModsDir()));
+ m_core_mod_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction);
+ }
+ return m_core_mod_list;
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
+{
+ if (!m_resource_pack_list)
+ {
+ m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
+ m_resource_pack_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction);
+ }
+ return m_resource_pack_list;
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
+{
+ if (!m_texture_pack_list)
+ {
+ m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir()));
+ m_texture_pack_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction);
+ }
+ return m_texture_pack_list;
+}
+
+std::shared_ptr<WorldList> MinecraftInstance::worldList() const
+{
+ if (!m_world_list)
+ {
+ m_world_list.reset(new WorldList(worldDir()));
+ }
+ return m_world_list;
+}
+
+std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const
+{
+ if (!m_game_options)
+ {
+ m_game_options.reset(new GameOptions(FS::PathCombine(gameRoot(), "options.txt")));
+ }
+ return m_game_options;
+}
+
+QList< Mod > MinecraftInstance::getJarMods() const
+{
+ auto profile = m_components->getProfile();
+ QList<Mod> mods;
+ for (auto jarmod : profile->getJarMods())
+ {
+ QStringList jar, temp1, temp2, temp3;
+ jarmod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, jarmodsPath().absolutePath());
+ // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem));
+ mods.push_back(Mod(QFileInfo(jar[0])));
+ }
+ return mods;
+}
+
+
+#include "MinecraftInstance.moc"
diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h
new file mode 100644
index 00000000..b55a2776
--- /dev/null
+++ b/launcher/minecraft/MinecraftInstance.h
@@ -0,0 +1,132 @@
+#pragma once
+#include "BaseInstance.h"
+#include <java/JavaVersion.h>
+#include "minecraft/mod/Mod.h"
+#include <QProcess>
+#include <QDir>
+#include "minecraft/launch/MinecraftServerTarget.h"
+
+class ModFolderModel;
+class WorldList;
+class GameOptions;
+class LaunchStep;
+class PackProfile;
+
+class MinecraftInstance: public BaseInstance
+{
+ Q_OBJECT
+public:
+ MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+ virtual ~MinecraftInstance() {};
+ virtual void saveNow() override;
+
+ // FIXME: remove
+ QString typeName() const override;
+ // FIXME: remove
+ QSet<QString> traits() const override;
+
+ bool canEdit() const override
+ {
+ return true;
+ }
+
+ bool canExport() const override
+ {
+ return true;
+ }
+
+ ////// Directories and files //////
+ QString jarModsDir() const;
+ QString resourcePacksDir() const;
+ QString texturePacksDir() const;
+ QString loaderModsDir() const;
+ QString coreModsDir() const;
+ QString modsCacheLocation() const;
+ QString libDir() const;
+ QString worldDir() const;
+ QString resourcesDir() const;
+ QDir jarmodsPath() const;
+ QDir librariesPath() const;
+ QDir versionsPath() const;
+ QString instanceConfigFolder() const override;
+
+ // Path to the instance's minecraft directory.
+ QString gameRoot() const override;
+
+ // Path to the instance's minecraft bin directory.
+ QString binRoot() const;
+
+ // where to put the natives during/before launch
+ QString getNativePath() const;
+
+ // where the instance-local libraries should be
+ QString getLocalLibraryPath() const;
+
+
+ ////// Profile management //////
+ std::shared_ptr<PackProfile> getPackProfile() const;
+
+ ////// Mod Lists //////
+ std::shared_ptr<ModFolderModel> loaderModList() const;
+ std::shared_ptr<ModFolderModel> coreModList() const;
+ std::shared_ptr<ModFolderModel> resourcePackList() const;
+ std::shared_ptr<ModFolderModel> texturePackList() const;
+ std::shared_ptr<WorldList> worldList() const;
+ std::shared_ptr<GameOptions> gameOptionsModel() const;
+
+ ////// Launch stuff //////
+ shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) override;
+ shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override;
+ QStringList extraArguments() const override;
+ QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
+ QList<Mod> getJarMods() const;
+ QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
+ /// get arguments passed to java
+ QStringList javaArguments() const;
+
+ /// get variables for launch command variable substitution/environment
+ QMap<QString, QString> getVariables() const override;
+
+ /// create an environment for launching processes
+ QProcessEnvironment createEnvironment() override;
+
+ /// guess log level from a line of minecraft log
+ MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) override;
+
+ IPathMatcher::Ptr getLogFileMatcher() override;
+
+ QString getLogFileRoot() override;
+
+ QString getStatusbarDescription() override;
+
+ // FIXME: remove
+ virtual QStringList getClassPath() const;
+ // FIXME: remove
+ virtual QStringList getNativeJars() const;
+ // FIXME: remove
+ virtual QString getMainClass() const;
+
+ // FIXME: remove
+ virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const;
+
+ virtual JavaVersion getJavaVersion() const;
+
+protected:
+ QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session);
+ QStringList validLaunchMethods();
+ QString launchMethod();
+
+private:
+ QString prettifyTimeDuration(int64_t duration);
+
+protected: // data
+ std::shared_ptr<PackProfile> m_components;
+ mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
+ mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
+ mutable std::shared_ptr<ModFolderModel> m_resource_pack_list;
+ mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
+ mutable std::shared_ptr<WorldList> m_world_list;
+ mutable std::shared_ptr<GameOptions> m_game_options;
+};
+
+typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr;
diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp
new file mode 100644
index 00000000..79b0c484
--- /dev/null
+++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp
@@ -0,0 +1,45 @@
+#include "MinecraftLoadAndCheck.h"
+#include "MinecraftInstance.h"
+#include "PackProfile.h"
+
+MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst)
+{
+}
+
+void MinecraftLoadAndCheck::executeTask()
+{
+ // add offline metadata load task
+ auto components = m_inst->getPackProfile();
+ components->reload(Net::Mode::Offline);
+ m_task = components->getCurrentTask();
+
+ if(!m_task)
+ {
+ emitSucceeded();
+ return;
+ }
+ connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::subtaskSucceeded);
+ connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed);
+ connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress);
+ connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus);
+}
+
+void MinecraftLoadAndCheck::subtaskSucceeded()
+{
+ if(isFinished())
+ {
+ qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!";
+ return;
+ }
+ emitSucceeded();
+}
+
+void MinecraftLoadAndCheck::subtaskFailed(QString error)
+{
+ if(isFinished())
+ {
+ qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!";
+ return;
+ }
+ emitFailed(error);
+}
diff --git a/launcher/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h
new file mode 100644
index 00000000..3435b52b
--- /dev/null
+++ b/launcher/minecraft/MinecraftLoadAndCheck.h
@@ -0,0 +1,48 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QList>
+#include <QUrl>
+
+#include "tasks/Task.h"
+#include <quazip.h>
+
+#include "QObjectPtr.h"
+
+class MinecraftVersion;
+class MinecraftInstance;
+
+class MinecraftLoadAndCheck : public Task
+{
+ Q_OBJECT
+public:
+ explicit MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent = 0);
+ virtual ~MinecraftLoadAndCheck() {};
+ void executeTask() override;
+
+private slots:
+ void subtaskSucceeded();
+ void subtaskFailed(QString error);
+
+private:
+ MinecraftInstance *m_inst = nullptr;
+ shared_qobject_ptr<Task> m_task;
+ QString m_preFailure;
+ QString m_fail_reason;
+};
+
diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp
new file mode 100644
index 00000000..8f1565b0
--- /dev/null
+++ b/launcher/minecraft/MinecraftUpdate.cpp
@@ -0,0 +1,182 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Env.h"
+#include "MinecraftUpdate.h"
+#include "MinecraftInstance.h"
+
+#include <QFile>
+#include <QFileInfo>
+#include <QTextStream>
+#include <QDataStream>
+
+#include "BaseInstance.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/Library.h"
+#include <FileSystem.h>
+
+#include "update/FoldersTask.h"
+#include "update/LibrariesTask.h"
+#include "update/FMLLibrariesTask.h"
+#include "update/AssetUpdateTask.h"
+
+#include <meta/Index.h>
+#include <meta/Version.h>
+
+MinecraftUpdate::MinecraftUpdate(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst)
+{
+}
+
+void MinecraftUpdate::executeTask()
+{
+ m_tasks.clear();
+ // create folders
+ {
+ m_tasks.append(std::make_shared<FoldersTask>(m_inst));
+ }
+
+ // add metadata update task if necessary
+ {
+ auto components = m_inst->getPackProfile();
+ components->reload(Net::Mode::Online);
+ auto task = components->getCurrentTask();
+ if(task)
+ {
+ m_tasks.append(task.unwrap());
+ }
+ }
+
+ // libraries download
+ {
+ m_tasks.append(std::make_shared<LibrariesTask>(m_inst));
+ }
+
+ // FML libraries download and copy into the instance
+ {
+ m_tasks.append(std::make_shared<FMLLibrariesTask>(m_inst));
+ }
+
+ // assets update
+ {
+ m_tasks.append(std::make_shared<AssetUpdateTask>(m_inst));
+ }
+
+ if(!m_preFailure.isEmpty())
+ {
+ emitFailed(m_preFailure);
+ return;
+ }
+ next();
+}
+
+void MinecraftUpdate::next()
+{
+ if(m_abort)
+ {
+ emitFailed(tr("Aborted by user."));
+ return;
+ }
+ if(m_failed_out_of_order)
+ {
+ emitFailed(m_fail_reason);
+ return;
+ }
+ m_currentTask ++;
+ if(m_currentTask > 0)
+ {
+ auto task = m_tasks[m_currentTask - 1];
+ disconnect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded);
+ disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
+ disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
+ disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
+ }
+ if(m_currentTask == m_tasks.size())
+ {
+ emitSucceeded();
+ return;
+ }
+ auto task = m_tasks[m_currentTask];
+ // if the task is already finished by the time we look at it, skip it
+ if(task->isFinished())
+ {
+ qCritical() << "MinecraftUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get();
+ next();
+ }
+ connect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded);
+ connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
+ connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
+ connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
+ // if the task is already running, do not start it again
+ if(!task->isRunning())
+ {
+ task->start();
+ }
+}
+
+void MinecraftUpdate::subtaskSucceeded()
+{
+ if(isFinished())
+ {
+ qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!";
+ return;
+ }
+ auto senderTask = QObject::sender();
+ auto currentTask = m_tasks[m_currentTask].get();
+ if(senderTask != currentTask)
+ {
+ qDebug() << "MinecraftUpdate: Subtask" << sender() << "succeeded out of order.";
+ return;
+ }
+ next();
+}
+
+void MinecraftUpdate::subtaskFailed(QString error)
+{
+ if(isFinished())
+ {
+ qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!";
+ return;
+ }
+ auto senderTask = QObject::sender();
+ auto currentTask = m_tasks[m_currentTask].get();
+ if(senderTask != currentTask)
+ {
+ qDebug() << "MinecraftUpdate: Subtask" << sender() << "failed out of order.";
+ m_failed_out_of_order = true;
+ m_fail_reason = error;
+ return;
+ }
+ emitFailed(error);
+}
+
+
+bool MinecraftUpdate::abort()
+{
+ if(!m_abort)
+ {
+ m_abort = true;
+ auto task = m_tasks[m_currentTask];
+ if(task->canAbort())
+ {
+ return task->abort();
+ }
+ }
+ return true;
+}
+
+bool MinecraftUpdate::canAbort() const
+{
+ return true;
+}
diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h
new file mode 100644
index 00000000..fadebff9
--- /dev/null
+++ b/launcher/minecraft/MinecraftUpdate.h
@@ -0,0 +1,57 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QList>
+#include <QUrl>
+
+#include "net/NetJob.h"
+#include "tasks/Task.h"
+#include "minecraft/VersionFilterData.h"
+#include <quazip.h>
+
+class MinecraftVersion;
+class MinecraftInstance;
+
+class MinecraftUpdate : public Task
+{
+ Q_OBJECT
+public:
+ explicit MinecraftUpdate(MinecraftInstance *inst, QObject *parent = 0);
+ virtual ~MinecraftUpdate() {};
+
+ void executeTask() override;
+ bool canAbort() const override;
+
+private
+slots:
+ bool abort() override;
+ void subtaskSucceeded();
+ void subtaskFailed(QString error);
+
+private:
+ void next();
+
+private:
+ MinecraftInstance *m_inst = nullptr;
+ QList<std::shared_ptr<Task>> m_tasks;
+ QString m_preFailure;
+ int m_currentTask = -1;
+ bool m_abort = false;
+ bool m_failed_out_of_order = false;
+ QString m_fail_reason;
+};
diff --git a/launcher/minecraft/MojangDownloadInfo.h b/launcher/minecraft/MojangDownloadInfo.h
new file mode 100644
index 00000000..88f87287
--- /dev/null
+++ b/launcher/minecraft/MojangDownloadInfo.h
@@ -0,0 +1,82 @@
+#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;
+ // HACK: ignore assets from other version files than Minecraft
+ // workaround for stupid assets issue caused by amazon:
+ // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/
+ if(id == "legacy")
+ {
+ url = "https://launchermeta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json";
+ }
+ // HACK
+ else
+ {
+ url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id + ".json";
+ }
+ known = false;
+ }
+
+ // data
+ int totalSize;
+ QString id;
+ bool known = true;
+};
diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp
new file mode 100644
index 00000000..f9cb2228
--- /dev/null
+++ b/launcher/minecraft/MojangVersionFormat.cpp
@@ -0,0 +1,383 @@
+#include "MojangVersionFormat.h"
+#include "OneSixVersionFormat.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(ProblemSeverity::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->releaseTime = timeFromS3Time(in.value("releaseTime").toString(""));
+ out->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(
+ ProblemSeverity::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->uid = "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(*out, 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->releaseTime.isNull())
+ {
+ writeString(out, "releaseTime", timeToS3Time(in->releaseTime));
+ }
+ if(!in->updateTime.isNull())
+ {
+ writeString(out, "time", timeToS3Time(in->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(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename)
+{
+ LibraryPtr out(new Library());
+ if (!libObj.contains("name"))
+ {
+ throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field");
+ }
+ auto rawName = libObj.value("name").toString();
+ out->m_name = rawName;
+ if(!out->m_name.valid()) {
+ problems.addProblem(ProblemSeverity::Error, QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName));
+ }
+
+ Bits::readString(libObj, "url", out->m_repositoryURL);
+ if (libObj.contains("extract"))
+ {
+ 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", library->m_name.serialize());
+ 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/launcher/minecraft/MojangVersionFormat.h b/launcher/minecraft/MojangVersionFormat.h
new file mode 100644
index 00000000..d38f0a2f
--- /dev/null
+++ b/launcher/minecraft/MojangVersionFormat.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <minecraft/VersionFile.h>
+#include <minecraft/Library.h>
+#include <QJsonDocument>
+#include <ProblemProvider.h>
+
+class 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(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename);
+ static QJsonObject libraryToJson(Library *library);
+};
diff --git a/launcher/minecraft/MojangVersionFormat_test.cpp b/launcher/minecraft/MojangVersionFormat_test.cpp
new file mode 100644
index 00000000..9d095340
--- /dev/null
+++ b/launcher/minecraft/MojangVersionFormat_test.cpp
@@ -0,0 +1,55 @@
+#include <QTest>
+#include <QDebug>
+#include "TestUtil.h"
+
+#include "minecraft/MojangVersionFormat.h"
+
+class MojangVersionFormatTest : public QObject
+{
+ Q_OBJECT
+
+ static QJsonDocument readJson(const char *file)
+ {
+ auto path = QFINDTESTDATA(file);
+ QFile jsonFile(path);
+ jsonFile.open(QIODevice::ReadOnly);
+ auto data = jsonFile.readAll();
+ jsonFile.close();
+ return QJsonDocument::fromJson(data);
+ }
+ static void writeJson(const char *file, QJsonDocument doc)
+ {
+ QFile jsonFile(file);
+ jsonFile.open(QIODevice::WriteOnly | QIODevice::Text);
+ auto data = doc.toJson(QJsonDocument::Indented);
+ qDebug() << QString::fromUtf8(data);
+ jsonFile.write(data);
+ jsonFile.close();
+ }
+
+private
+slots:
+ void test_Through_Simple()
+ {
+ QJsonDocument doc = readJson("data/1.9-simple.json");
+ auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json");
+ auto doc2 = MojangVersionFormat::versionFileToJson(vfile);
+ writeJson("1.9-simple-passthorugh.json", doc2);
+
+ QCOMPARE(doc.toJson(), doc2.toJson());
+ }
+
+ void test_Through()
+ {
+ QJsonDocument doc = readJson("data/1.9.json");
+ auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json");
+ auto doc2 = MojangVersionFormat::versionFileToJson(vfile);
+ writeJson("1.9-passthorugh.json", doc2);
+ QCOMPARE(doc.toJson(), doc2.toJson());
+ }
+};
+
+QTEST_GUILESS_MAIN(MojangVersionFormatTest)
+
+#include "MojangVersionFormat_test.moc"
+
diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp
new file mode 100644
index 00000000..0329d70e
--- /dev/null
+++ b/launcher/minecraft/OneSixVersionFormat.cpp
@@ -0,0 +1,391 @@
+#include "OneSixVersionFormat.h"
+#include <Json.h>
+#include "minecraft/ParseUtils.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(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename)
+{
+ LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename);
+ readString(libObj, "MMC-hint", out->m_hint);
+ readString(libObj, "MMC-absulute_url", out->m_absoluteURL);
+ readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL);
+ readString(libObj, "MMC-filename", out->m_filename);
+ readString(libObj, "MMC-displayname", out->m_displayname);
+ 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);
+ if (library->m_filename.size())
+ libRoot.insert("MMC-filename", library->m_filename);
+ if (library->m_displayname.size())
+ libRoot.insert("MMC-displayname", library->m_displayname);
+ 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();
+
+ Meta::MetadataVersion formatVersion = Meta::parseFormatVersion(root, false);
+ switch(formatVersion)
+ {
+ case Meta::MetadataVersion::InitialRelease:
+ break;
+ case Meta::MetadataVersion::Invalid:
+ throw JSONValidationError(filename + " does not contain a recognizable version of the metadata format.");
+ }
+
+ 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();
+
+ if(root.contains("uid"))
+ {
+ out->uid = root.value("uid").toString();
+ }
+ else
+ {
+ out->uid = root.value("fileId").toString();
+ }
+
+ out->version = root.value("version").toString();
+
+ 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(*out, libObj, filename);
+ // and add to jar mods
+ out->jarMods.append(lib);
+ }
+ }
+ else if (root.contains("+jarMods")) // DEPRECATED: old style '+jarMods' are only here for backwards compatibility
+ {
+ for (auto libVal : requireArray(root.value("+jarMods")))
+ {
+ QJsonObject libObj = requireObject(libVal);
+ // parse the jarmod
+ auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name);
+ // and add to jar mods
+ out->jarMods.append(lib);
+ }
+ }
+
+ if (root.contains("mods"))
+ {
+ for (auto libVal : requireArray(root.value("mods")))
+ {
+ QJsonObject libObj = requireObject(libVal);
+ // parse the jarmod
+ auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename);
+ // and add to jar mods
+ out->mods.append(lib);
+ }
+ }
+
+ auto readLibs = [&](const char * which, QList<LibraryPtr> & outList)
+ {
+ for (auto libVal : requireArray(root.value(which)))
+ {
+ QJsonObject libObj = requireObject(libVal);
+ // parse the library
+ auto lib = libraryFromJson(*out, libObj, filename);
+ outList.append(lib);
+ }
+ };
+ bool hasPlusLibs = root.contains("+libraries");
+ bool hasLibs = root.contains("libraries");
+ if (hasPlusLibs && hasLibs)
+ {
+ out->addProblem(ProblemSeverity::Warning,
+ QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported."));
+ readLibs("libraries", out->libraries);
+ readLibs("+libraries", out->libraries);
+ }
+ else if (hasLibs)
+ {
+ readLibs("libraries", out->libraries);
+ }
+ else if(hasPlusLibs)
+ {
+ readLibs("+libraries", out->libraries);
+ }
+
+ if(root.contains("mavenFiles")) {
+ readLibs("mavenFiles", out->mavenFiles);
+ }
+
+ // if we have mainJar, just use it
+ if(root.contains("mainJar"))
+ {
+ QJsonObject libObj = requireObject(root, "mainJar");
+ out->mainJar = libraryFromJson(*out, libObj, filename);
+ }
+ // else reconstruct it from downloads and id ... if that's available
+ else if(!out->minecraftVersion.isEmpty())
+ {
+ auto lib = std::make_shared<Library>();
+ lib->setRawName(GradleSpecifier(QString("com.mojang:minecraft:%1:client").arg(out->minecraftVersion)));
+ // we have a reliable client download, use it.
+ if(out->mojangDownloads.contains("client"))
+ {
+ auto LibDLInfo = std::make_shared<MojangLibraryDownloadInfo>();
+ LibDLInfo->artifact = out->mojangDownloads["client"];
+ lib->setMojangDownloadInfo(LibDLInfo);
+ }
+ // we got nothing...
+ else
+ {
+ out->addProblem(
+ ProblemSeverity::Error,
+ QObject::tr("URL for the main jar could not be determined - Mojang removed the server that we used as fallback.")
+ );
+ }
+ out->mainJar = lib;
+ }
+
+ if (root.contains("requires"))
+ {
+ Meta::parseRequires(root, &out->requires);
+ }
+ QString dependsOnMinecraftVersion = root.value("mcVersion").toString();
+ if(!dependsOnMinecraftVersion.isEmpty())
+ {
+ Meta::Require mcReq;
+ mcReq.uid = "net.minecraft";
+ mcReq.equalsVersion = dependsOnMinecraftVersion;
+ if (out->requires.count(mcReq) == 0)
+ {
+ out->requires.insert(mcReq);
+ }
+ }
+ if (root.contains("conflicts"))
+ {
+ Meta::parseRequires(root, &out->conflicts);
+ }
+ if (root.contains("volatile"))
+ {
+ out->m_volatile = requireBoolean(root, "volatile");
+ }
+
+ /* removed features that shouldn't be used */
+ if (root.contains("tweakers"))
+ {
+ out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element 'tweakers'"));
+ }
+ if (root.contains("-libraries"))
+ {
+ out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-libraries'"));
+ }
+ if (root.contains("-tweakers"))
+ {
+ out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-tweakers'"));
+ }
+ if (root.contains("-minecraftArguments"))
+ {
+ out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-minecraftArguments'"));
+ }
+ if (root.contains("+minecraftArguments"))
+ {
+ out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '+minecraftArguments'"));
+ }
+ return out;
+}
+
+QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch)
+{
+ QJsonObject root;
+ writeString(root, "name", patch->name);
+
+ writeString(root, "uid", patch->uid);
+
+ writeString(root, "version", patch->version);
+
+ Meta::serializeFormatVersion(root, Meta::MetadataVersion::InitialRelease);
+
+ MojangVersionFormat::writeVersionProperties(patch.get(), root);
+
+ if(patch->mainJar)
+ {
+ root.insert("mainJar", libraryToJson(patch->mainJar.get()));
+ }
+ 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->mavenFiles.isEmpty())
+ {
+ QJsonArray array;
+ for (auto value: patch->mavenFiles)
+ {
+ array.append(OneSixVersionFormat::libraryToJson(value.get()));
+ }
+ root.insert("mavenFiles", array);
+ }
+ if (!patch->jarMods.isEmpty())
+ {
+ QJsonArray array;
+ for (auto value: patch->jarMods)
+ {
+ array.append(OneSixVersionFormat::jarModtoJson(value.get()));
+ }
+ root.insert("jarMods", array);
+ }
+ if (!patch->mods.isEmpty())
+ {
+ QJsonArray array;
+ for (auto value: patch->jarMods)
+ {
+ array.append(OneSixVersionFormat::modtoJson(value.get()));
+ }
+ root.insert("mods", array);
+ }
+ if(!patch->requires.empty())
+ {
+ Meta::serializeRequires(root, &patch->requires, "requires");
+ }
+ if(!patch->conflicts.empty())
+ {
+ Meta::serializeRequires(root, &patch->conflicts, "conflicts");
+ }
+ if(patch->m_volatile)
+ {
+ root.insert("volatile", true);
+ }
+ // write the contents to a json document.
+ {
+ QJsonDocument out;
+ out.setObject(root);
+ return out;
+ }
+}
+
+LibraryPtr OneSixVersionFormat::plusJarModFromJson(
+ ProblemContainer & problems,
+ const QJsonObject &libObj,
+ const QString &filename,
+ const QString &originalName
+) {
+ LibraryPtr out(new Library());
+ if (!libObj.contains("name"))
+ {
+ throw JSONValidationError(filename +
+ "contains a jarmod that doesn't have a 'name' field");
+ }
+
+ // just make up something unique on the spot for the library name.
+ auto uuid = QUuid::createUuid();
+ QString id = uuid.toString().remove('{').remove('}');
+ out->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1"));
+
+ // filename override is the old name
+ out->setFilename(libObj.value("name").toString());
+
+ // it needs to be local, it is stored in the instance jarmods folder
+ out->setHint("local");
+
+ // read the original name if present - some versions did not set it
+ // it is the original jar mod filename before it got renamed at the point of addition
+ auto displayName = libObj.value("originalName").toString();
+ if(displayName.isEmpty())
+ {
+ auto fixed = originalName;
+ fixed.remove(" (jar mod)");
+ out->setDisplayName(fixed);
+ }
+ else
+ {
+ out->setDisplayName(displayName);
+ }
+ return out;
+}
+
+LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename)
+{
+ return libraryFromJson(problems, libObj, filename);
+}
+
+
+QJsonObject OneSixVersionFormat::jarModtoJson(Library *jarmod)
+{
+ return libraryToJson(jarmod);
+}
+
+LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename)
+{
+ return libraryFromJson(problems, libObj, filename);
+}
+
+QJsonObject OneSixVersionFormat::modtoJson(Library *jarmod)
+{
+ return libraryToJson(jarmod);
+}
diff --git a/launcher/minecraft/OneSixVersionFormat.h b/launcher/minecraft/OneSixVersionFormat.h
new file mode 100644
index 00000000..1a091d88
--- /dev/null
+++ b/launcher/minecraft/OneSixVersionFormat.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <minecraft/VersionFile.h>
+#include <minecraft/PackProfile.h>
+#include <minecraft/Library.h>
+#include <QJsonDocument>
+#include <ProblemProvider.h>
+
+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);
+
+ // libraries
+ static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename);
+ static QJsonObject libraryToJson(Library *library);
+
+ // DEPRECATED: old 'plus' jar mods generated by the application
+ static LibraryPtr plusJarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename, const QString &originalName);
+
+ // new jar mods derived from libraries
+ static LibraryPtr jarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename);
+ static QJsonObject jarModtoJson(Library * jarmod);
+
+ // mods, also derived from libraries
+ static LibraryPtr modFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename);
+ static QJsonObject modtoJson(Library * jarmod);
+};
diff --git a/launcher/minecraft/OpSys.cpp b/launcher/minecraft/OpSys.cpp
new file mode 100644
index 00000000..f6a4ed1c
--- /dev/null
+++ b/launcher/minecraft/OpSys.cpp
@@ -0,0 +1,42 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#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/launcher/minecraft/OpSys.h b/launcher/minecraft/OpSys.h
new file mode 100644
index 00000000..63c750b1
--- /dev/null
+++ b/launcher/minecraft/OpSys.h
@@ -0,0 +1,37 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <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/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp
new file mode 100644
index 00000000..f6918116
--- /dev/null
+++ b/launcher/minecraft/PackProfile.cpp
@@ -0,0 +1,1225 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QFile>
+#include <QCryptographicHash>
+#include <Version.h>
+#include <QDir>
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QDebug>
+
+#include "Exception.h"
+#include <minecraft/OneSixVersionFormat.h>
+#include <FileSystem.h>
+#include <QSaveFile>
+#include <Env.h>
+#include <meta/Index.h>
+#include <minecraft/MinecraftInstance.h>
+#include <QUuid>
+#include <QTimer>
+#include <Json.h>
+
+#include "PackProfile.h"
+#include "PackProfile_p.h"
+#include "ComponentUpdateTask.h"
+
+PackProfile::PackProfile(MinecraftInstance * instance)
+ : QAbstractListModel()
+{
+ d.reset(new PackProfileData);
+ d->m_instance = instance;
+ d->m_saveTimer.setSingleShot(true);
+ d->m_saveTimer.setInterval(5000);
+ d->interactionDisabled = instance->isRunning();
+ connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction);
+ connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal);
+}
+
+PackProfile::~PackProfile()
+{
+ saveNow();
+}
+
+// BEGIN: component file format
+
+static const int currentComponentsFileVersion = 1;
+
+static QJsonObject componentToJsonV1(ComponentPtr component)
+{
+ QJsonObject obj;
+ // critical
+ obj.insert("uid", component->m_uid);
+ if(!component->m_version.isEmpty())
+ {
+ obj.insert("version", component->m_version);
+ }
+ if(component->m_dependencyOnly)
+ {
+ obj.insert("dependencyOnly", true);
+ }
+ if(component->m_important)
+ {
+ obj.insert("important", true);
+ }
+ if(component->m_disabled)
+ {
+ obj.insert("disabled", true);
+ }
+
+ // cached
+ if(!component->m_cachedVersion.isEmpty())
+ {
+ obj.insert("cachedVersion", component->m_cachedVersion);
+ }
+ if(!component->m_cachedName.isEmpty())
+ {
+ obj.insert("cachedName", component->m_cachedName);
+ }
+ Meta::serializeRequires(obj, &component->m_cachedRequires, "cachedRequires");
+ Meta::serializeRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
+ if(component->m_cachedVolatile)
+ {
+ obj.insert("cachedVolatile", true);
+ }
+ return obj;
+}
+
+static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & componentJsonPattern, const QJsonObject &obj)
+{
+ // critical
+ auto uid = Json::requireString(obj.value("uid"));
+ auto filePath = componentJsonPattern.arg(uid);
+ auto component = new Component(parent, uid);
+ component->m_version = Json::ensureString(obj.value("version"));
+ component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false);
+ component->m_important = Json::ensureBoolean(obj.value("important"), false);
+
+ // cached
+ // TODO @RESILIENCE: ignore invalid values/structure here?
+ component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion"));
+ component->m_cachedName = Json::ensureString(obj.value("cachedName"));
+ Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires");
+ Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
+ component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false);
+ bool disabled = Json::ensureBoolean(obj.value("disabled"), false);
+ component->setEnabled(!disabled);
+ return component;
+}
+
+// Save the given component container data to a file
+static bool savePackProfile(const QString & filename, const ComponentContainer & container)
+{
+ QJsonObject obj;
+ obj.insert("formatVersion", currentComponentsFileVersion);
+ QJsonArray orderArray;
+ for(auto component: container)
+ {
+ orderArray.append(componentToJsonV1(component));
+ }
+ obj.insert("components", orderArray);
+ QSaveFile outFile(filename);
+ if (!outFile.open(QFile::WriteOnly))
+ {
+ qCritical() << "Couldn't open" << outFile.fileName()
+ << "for writing:" << outFile.errorString();
+ return false;
+ }
+ auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented);
+ if(outFile.write(data) != data.size())
+ {
+ qCritical() << "Couldn't write all the data into" << outFile.fileName()
+ << "because:" << outFile.errorString();
+ return false;
+ }
+ if(!outFile.commit())
+ {
+ qCritical() << "Couldn't save" << outFile.fileName()
+ << "because:" << outFile.errorString();
+ }
+ return true;
+}
+
+// Read the given file into component containers
+static bool loadPackProfile(PackProfile * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container)
+{
+ QFile componentsFile(filename);
+ if (!componentsFile.exists())
+ {
+ qWarning() << "Components file doesn't exist. This should never happen.";
+ return false;
+ }
+ if (!componentsFile.open(QFile::ReadOnly))
+ {
+ qCritical() << "Couldn't open" << componentsFile.fileName()
+ << " for reading:" << componentsFile.errorString();
+ qWarning() << "Ignoring overriden order";
+ return false;
+ }
+
+ // and it's valid JSON
+ QJsonParseError error;
+ QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
+ if (error.error != QJsonParseError::NoError)
+ {
+ qCritical() << "Couldn't parse" << componentsFile.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("formatVersion"));
+ if (version != currentComponentsFileVersion)
+ {
+ throw JSONValidationError(QObject::tr("Invalid component file version, expected %1")
+ .arg(currentComponentsFileVersion));
+ }
+ auto orderArray = Json::requireArray(obj.value("components"));
+ for(auto item: orderArray)
+ {
+ auto obj = Json::requireObject(item, "Component must be an object.");
+ container.append(componentFromJsonV1(parent, componentJsonPattern, obj));
+ }
+ }
+ catch (const JSONValidationError &err)
+ {
+ qCritical() << "Couldn't parse" << componentsFile.fileName() << ": bad file format";
+ container.clear();
+ return false;
+ }
+ return true;
+}
+
+// END: component file format
+
+// BEGIN: save/load logic
+
+void PackProfile::saveNow()
+{
+ if(saveIsScheduled())
+ {
+ d->m_saveTimer.stop();
+ save_internal();
+ }
+}
+
+bool PackProfile::saveIsScheduled() const
+{
+ return d->dirty;
+}
+
+void PackProfile::buildingFromScratch()
+{
+ d->loaded = true;
+ d->dirty = true;
+}
+
+void PackProfile::scheduleSave()
+{
+ if(!d->loaded)
+ {
+ qDebug() << "Component list should never save if it didn't successfully load, instance:" << d->m_instance->name();
+ return;
+ }
+ if(!d->dirty)
+ {
+ d->dirty = true;
+ qDebug() << "Component list save is scheduled for" << d->m_instance->name();
+ }
+ d->m_saveTimer.start();
+}
+
+QString PackProfile::componentsFilePath() const
+{
+ return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json");
+}
+
+QString PackProfile::patchesPattern() const
+{
+ return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json");
+}
+
+QString PackProfile::patchFilePathForUid(const QString& uid) const
+{
+ return patchesPattern().arg(uid);
+}
+
+void PackProfile::save_internal()
+{
+ qDebug() << "Component list save performed now for" << d->m_instance->name();
+ auto filename = componentsFilePath();
+ savePackProfile(filename, d->components);
+ d->dirty = false;
+}
+
+bool PackProfile::load()
+{
+ auto filename = componentsFilePath();
+ QFile componentsFile(filename);
+
+ // migrate old config to new one, if needed
+ if(!componentsFile.exists())
+ {
+ if(!migratePreComponentConfig())
+ {
+ // FIXME: the user should be notified...
+ qCritical() << "Failed to convert old pre-component config for instance" << d->m_instance->name();
+ return false;
+ }
+ }
+
+ // load the new component list and swap it with the current one...
+ ComponentContainer newComponents;
+ if(!loadPackProfile(this, filename, patchesPattern(), newComponents))
+ {
+ qCritical() << "Failed to load the component config for instance" << d->m_instance->name();
+ return false;
+ }
+ else
+ {
+ // FIXME: actually use fine-grained updates, not this...
+ beginResetModel();
+ // disconnect all the old components
+ for(auto component: d->components)
+ {
+ disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
+ }
+ d->components.clear();
+ d->componentIndex.clear();
+ for(auto component: newComponents)
+ {
+ if(d->componentIndex.contains(component->m_uid))
+ {
+ qWarning() << "Ignoring duplicate component entry" << component->m_uid;
+ continue;
+ }
+ connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
+ d->components.append(component);
+ d->componentIndex[component->m_uid] = component;
+ }
+ endResetModel();
+ d->loaded = true;
+ return true;
+ }
+}
+
+void PackProfile::reload(Net::Mode netmode)
+{
+ // Do not reload when the update/resolve task is running. It is in control.
+ if(d->m_updateTask)
+ {
+ return;
+ }
+
+ // flush any scheduled saves to not lose state
+ saveNow();
+
+ // FIXME: differentiate when a reapply is required by propagating state from components
+ invalidateLaunchProfile();
+
+ if(load())
+ {
+ resolve(netmode);
+ }
+}
+
+shared_qobject_ptr<Task> PackProfile::getCurrentTask()
+{
+ return d->m_updateTask;
+}
+
+void PackProfile::resolve(Net::Mode netmode)
+{
+ auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this);
+ d->m_updateTask.reset(updateTask);
+ connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded);
+ connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed);
+ d->m_updateTask->start();
+}
+
+
+void PackProfile::updateSucceeded()
+{
+ qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name();
+ d->m_updateTask.reset();
+ invalidateLaunchProfile();
+}
+
+void PackProfile::updateFailed(const QString& error)
+{
+ qDebug() << "Component list update/resolve task failed for" << d->m_instance->name() << "Reason:" << error;
+ d->m_updateTask.reset();
+ invalidateLaunchProfile();
+}
+
+// NOTE this is really old stuff, and only needs to be used when loading the old hardcoded component-unaware format (loadPreComponentConfig).
+static void upgradeDeprecatedFiles(QString root, QString instanceName)
+{
+ auto versionJsonPath = FS::PathCombine(root, "version.json");
+ auto customJsonPath = FS::PathCombine(root, "custom.json");
+ auto mcJson = FS::PathCombine(root, "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" << instanceName;
+ return;
+ }
+ if(!renameFile.isEmpty() && QFile::exists(renameFile))
+ {
+ if(!QFile::rename(renameFile, renameFile + ".old"))
+ {
+ qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << instanceName;
+ return;
+ }
+ }
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false);
+ ProfileUtils::removeLwjglFromPatch(file);
+ file->uid = "net.minecraft";
+ file->version = file->minecraftVersion;
+ file->name = "Minecraft";
+
+ Meta::Require needsLwjgl;
+ needsLwjgl.uid = "org.lwjgl";
+ file->requires.insert(needsLwjgl);
+
+ if(!ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), mcJson))
+ {
+ return;
+ }
+ if(!QFile::rename(sourceFile, sourceFile + ".old"))
+ {
+ qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << instanceName;
+ return;
+ }
+ }
+}
+
+/*
+ * Migrate old layout to the component based one...
+ * - Part of the version information is taken from `instance.cfg` (fed to this class from outside).
+ * - Part is taken from the old order.json file.
+ * - Part is loaded from loose json files in the instance's `patches` directory.
+ */
+bool PackProfile::migratePreComponentConfig()
+{
+ // upgrade the very old files from the beginnings of MultiMC 5
+ upgradeDeprecatedFiles(d->m_instance->instanceRoot(), d->m_instance->name());
+
+ QList<ComponentPtr> components;
+ QSet<QString> loaded;
+
+ auto addBuiltinPatch = [&](const QString &uid, bool asDependency, const QString & emptyVersion, const Meta::Require & req, const Meta::Require & conflict)
+ {
+ auto jsonFilePath = FS::PathCombine(d->m_instance->instanceRoot(), "patches" , uid + ".json");
+ auto intendedVersion = d->getOldConfigVersion(uid);
+ // load up the base minecraft patch
+ ComponentPtr component;
+ if(QFile::exists(jsonFilePath))
+ {
+ if(intendedVersion.isEmpty())
+ {
+ intendedVersion = emptyVersion;
+ }
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false);
+ // fix uid
+ file->uid = uid;
+ // if version is missing, add it from the outside.
+ if(file->version.isEmpty())
+ {
+ file->version = intendedVersion;
+ }
+ // if this is a dependency (LWJGL), mark it also as volatile
+ if(asDependency)
+ {
+ file->m_volatile = true;
+ }
+ // insert requirements if needed
+ if(!req.uid.isEmpty())
+ {
+ file->requires.insert(req);
+ }
+ // insert conflicts if needed
+ if(!conflict.uid.isEmpty())
+ {
+ file->conflicts.insert(conflict);
+ }
+ // FIXME: @QUALITY do not ignore return value
+ ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), jsonFilePath);
+ component = new Component(this, uid, file);
+ component->m_version = intendedVersion;
+ }
+ else if(!intendedVersion.isEmpty())
+ {
+ auto metaVersion = ENV.metadataIndex()->get(uid, intendedVersion);
+ component = new Component(this, metaVersion);
+ }
+ else
+ {
+ return;
+ }
+ component->m_dependencyOnly = asDependency;
+ component->m_important = !asDependency;
+ components.append(component);
+ };
+ // TODO: insert depends and conflicts here if these are customized files...
+ Meta::Require reqLwjgl;
+ reqLwjgl.uid = "org.lwjgl";
+ reqLwjgl.suggests = "2.9.1";
+ Meta::Require conflictLwjgl3;
+ conflictLwjgl3.uid = "org.lwjgl3";
+ Meta::Require nullReq;
+ addBuiltinPatch("org.lwjgl", true, "2.9.1", nullReq, conflictLwjgl3);
+ addBuiltinPatch("net.minecraft", false, QString(), reqLwjgl, nullReq);
+
+ // first, collect all other file-based patches and load them
+ QMap<QString, ComponentPtr> loadedComponents;
+ QDir patchesDir(FS::PathCombine(d->m_instance->instanceRoot(),"patches"));
+ for (auto info : patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files))
+ {
+ // parse the file
+ qDebug() << "Reading" << info.fileName();
+ auto file = ProfileUtils::parseJsonFile(info, true);
+
+ // correct missing or wrong uid based on the file name
+ QString uid = info.completeBaseName();
+
+ // ignore builtins, they've been handled already
+ if (uid == "net.minecraft")
+ continue;
+ if (uid == "org.lwjgl")
+ continue;
+
+ // handle horrible corner cases
+ if(uid.isEmpty())
+ {
+ // if you have a file named '.json', make it just go away.
+ // FIXME: @QUALITY do not ignore return value
+ QFile::remove(info.absoluteFilePath());
+ continue;
+ }
+ file->uid = uid;
+ // FIXME: @QUALITY do not ignore return value
+ ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), info.absoluteFilePath());
+
+ auto component = new Component(this, file->uid, file);
+ auto version = d->getOldConfigVersion(file->uid);
+ if(!version.isEmpty())
+ {
+ component->m_version = version;
+ }
+ loadedComponents[file->uid] = component;
+ }
+ // try to load the other 'hardcoded' patches (forge, liteloader), if they weren't loaded from files
+ auto loadSpecial = [&](const QString & uid, int order)
+ {
+ auto patchVersion = d->getOldConfigVersion(uid);
+ if(!patchVersion.isEmpty() && !loadedComponents.contains(uid))
+ {
+ auto patch = new Component(this, ENV.metadataIndex()->get(uid, patchVersion));
+ patch->setOrder(order);
+ loadedComponents[uid] = patch;
+ }
+ };
+ loadSpecial("net.minecraftforge", 5);
+ loadSpecial("com.mumfrey.liteloader", 10);
+
+ // load the old order.json file, if present
+ ProfileUtils::PatchOrder userOrder;
+ ProfileUtils::readOverrideOrders(FS::PathCombine(d->m_instance->instanceRoot(), "order.json"), userOrder);
+
+ // now add all the patches by user sort order
+ for (auto uid : userOrder)
+ {
+ // ignore builtins
+ if (uid == "net.minecraft")
+ continue;
+ if (uid == "org.lwjgl")
+ continue;
+ // ordering has a patch that is gone?
+ if(!loadedComponents.contains(uid))
+ {
+ continue;
+ }
+ components.append(loadedComponents.take(uid));
+ }
+
+ // is there anything left to sort? - this is used when there are leftover components that aren't part of the order.json
+ if(!loadedComponents.isEmpty())
+ {
+ // inserting into multimap by order number as key sorts the patches and detects duplicates
+ QMultiMap<int, ComponentPtr> files;
+ auto iter = loadedComponents.begin();
+ while(iter != loadedComponents.end())
+ {
+ files.insert((*iter)->getOrder(), *iter);
+ iter++;
+ }
+
+ // then just extract the patches and put them in the list
+ for (auto order : files.keys())
+ {
+ const auto &values = files.values(order);
+ for(auto &value: values)
+ {
+ // TODO: put back the insertion of problem messages here, so the user knows about the id duplication
+ components.append(value);
+ }
+ }
+ }
+ // new we have a complete list of components...
+ return savePackProfile(componentsFilePath(), components);
+}
+
+// END: save/load
+
+void PackProfile::appendComponent(ComponentPtr component)
+{
+ insertComponent(d->components.size(), component);
+}
+
+void PackProfile::insertComponent(size_t index, ComponentPtr component)
+{
+ auto id = component->getID();
+ if(id.isEmpty())
+ {
+ qWarning() << "Attempt to add a component with empty ID!";
+ return;
+ }
+ if(d->componentIndex.contains(id))
+ {
+ qWarning() << "Attempt to add a component that is already present!";
+ return;
+ }
+ beginInsertRows(QModelIndex(), index, index);
+ d->components.insert(index, component);
+ d->componentIndex[id] = component;
+ endInsertRows();
+ connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
+ scheduleSave();
+}
+
+void PackProfile::componentDataChanged()
+{
+ auto objPtr = qobject_cast<Component *>(sender());
+ if(!objPtr)
+ {
+ qWarning() << "PackProfile got dataChenged signal from a non-Component!";
+ return;
+ }
+ if(objPtr->getID() == "net.minecraft") {
+ emit minecraftChanged();
+ }
+ // figure out which one is it... in a seriously dumb way.
+ int index = 0;
+ for (auto component: d->components)
+ {
+ if(component.get() == objPtr)
+ {
+ emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1));
+ scheduleSave();
+ return;
+ }
+ index++;
+ }
+ qWarning() << "PackProfile got dataChenged signal from a Component which does not belong to it!";
+}
+
+bool PackProfile::remove(const int index)
+{
+ auto patch = getComponent(index);
+ if (!patch->isRemovable())
+ {
+ qWarning() << "Patch" << patch->getID() << "is non-removable";
+ return false;
+ }
+
+ if(!removeComponent_internal(patch))
+ {
+ qCritical() << "Patch" << patch->getID() << "could not be removed";
+ return false;
+ }
+
+ beginRemoveRows(QModelIndex(), index, index);
+ d->components.removeAt(index);
+ d->componentIndex.remove(patch->getID());
+ endRemoveRows();
+ invalidateLaunchProfile();
+ scheduleSave();
+ return true;
+}
+
+bool PackProfile::remove(const QString id)
+{
+ int i = 0;
+ for (auto patch : d->components)
+ {
+ if (patch->getID() == id)
+ {
+ return remove(i);
+ }
+ i++;
+ }
+ return false;
+}
+
+bool PackProfile::customize(int index)
+{
+ auto patch = getComponent(index);
+ if (!patch->isCustomizable())
+ {
+ qDebug() << "Patch" << patch->getID() << "is not customizable";
+ return false;
+ }
+ if(!patch->customize())
+ {
+ qCritical() << "Patch" << patch->getID() << "could not be customized";
+ return false;
+ }
+ invalidateLaunchProfile();
+ scheduleSave();
+ return true;
+}
+
+bool PackProfile::revertToBase(int index)
+{
+ auto patch = getComponent(index);
+ if (!patch->isRevertible())
+ {
+ qDebug() << "Patch" << patch->getID() << "is not revertible";
+ return false;
+ }
+ if(!patch->revert())
+ {
+ qCritical() << "Patch" << patch->getID() << "could not be reverted";
+ return false;
+ }
+ invalidateLaunchProfile();
+ scheduleSave();
+ return true;
+}
+
+Component * PackProfile::getComponent(const QString &id)
+{
+ auto iter = d->componentIndex.find(id);
+ if (iter == d->componentIndex.end())
+ {
+ return nullptr;
+ }
+ return (*iter).get();
+}
+
+Component * PackProfile::getComponent(int index)
+{
+ if(index < 0 || index >= d->components.size())
+ {
+ return nullptr;
+ }
+ return d->components[index].get();
+}
+
+QVariant PackProfile::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+
+ if (row < 0 || row >= d->components.size())
+ return QVariant();
+
+ auto patch = d->components.at(row);
+
+ switch (role)
+ {
+ case Qt::CheckStateRole:
+ {
+ switch (column)
+ {
+ case NameColumn: {
+ return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
+ }
+ default:
+ return QVariant();
+ }
+ }
+ case Qt::DisplayRole:
+ {
+ switch (column)
+ {
+ case NameColumn:
+ return patch->getName();
+ case VersionColumn:
+ {
+ if(patch->isCustom())
+ {
+ return QString("%1 (Custom)").arg(patch->getVersion());
+ }
+ else
+ {
+ return patch->getVersion();
+ }
+ }
+ default:
+ return QVariant();
+ }
+ }
+ case Qt::DecorationRole:
+ {
+ switch(column)
+ {
+ case NameColumn:
+ {
+ auto severity = patch->getProblemSeverity();
+ switch (severity)
+ {
+ case ProblemSeverity::Warning:
+ return "warning";
+ case ProblemSeverity::Error:
+ return "error";
+ default:
+ return QVariant();
+ }
+ }
+ default:
+ {
+ return QVariant();
+ }
+ }
+ }
+ }
+ return QVariant();
+}
+
+bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index))
+ {
+ return false;
+ }
+
+ if (role == Qt::CheckStateRole)
+ {
+ auto component = d->components[index.row()];
+ if (component->setEnabled(!component->isEnabled()))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation == Qt::Horizontal)
+ {
+ if (role == Qt::DisplayRole)
+ {
+ switch (section)
+ {
+ case NameColumn:
+ return tr("Name");
+ case VersionColumn:
+ return tr("Version");
+ default:
+ return QVariant();
+ }
+ }
+ }
+ return QVariant();
+}
+
+// FIXME: zero precision mess
+Qt::ItemFlags PackProfile::flags(const QModelIndex &index) const
+{
+ if (!index.isValid()) {
+ return Qt::NoItemFlags;
+ }
+
+ Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+
+ int row = index.row();
+
+ if (row < 0 || row >= d->components.size()) {
+ return Qt::NoItemFlags;
+ }
+
+ auto patch = d->components.at(row);
+ // TODO: this will need fine-tuning later...
+ if(patch->canBeDisabled() && !d->interactionDisabled)
+ {
+ outFlags |= Qt::ItemIsUserCheckable;
+ }
+ return outFlags;
+}
+
+int PackProfile::rowCount(const QModelIndex &parent) const
+{
+ return d->components.size();
+}
+
+int PackProfile::columnCount(const QModelIndex &parent) const
+{
+ return NUM_COLUMNS;
+}
+
+void PackProfile::move(const int index, const MoveDirection direction)
+{
+ int theirIndex;
+ if (direction == MoveUp)
+ {
+ theirIndex = index - 1;
+ }
+ else
+ {
+ theirIndex = index + 1;
+ }
+
+ if (index < 0 || index >= d->components.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 = getComponent(index);
+ auto to = getComponent(theirIndex);
+
+ if (!from || !to || !to->isMoveable() || !from->isMoveable())
+ {
+ return;
+ }
+ beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap);
+ d->components.swap(index, theirIndex);
+ endMoveRows();
+ invalidateLaunchProfile();
+ scheduleSave();
+}
+
+void PackProfile::invalidateLaunchProfile()
+{
+ d->m_profile.reset();
+}
+
+void PackProfile::installJarMods(QStringList selectedFiles)
+{
+ installJarMods_internal(selectedFiles);
+}
+
+void PackProfile::installCustomJar(QString selectedFile)
+{
+ installCustomJar_internal(selectedFile);
+}
+
+bool PackProfile::installEmpty(const QString& uid, const QString& name)
+{
+ QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
+ if(!FS::ensureFolderPathExists(patchDir))
+ {
+ return false;
+ }
+ auto f = std::make_shared<VersionFile>();
+ f->name = name;
+ f->uid = uid;
+ f->version = "1";
+ QString patchFileName = FS::PathCombine(patchDir, uid + ".json");
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(new Component(this, f->uid, f));
+ scheduleSave();
+ invalidateLaunchProfile();
+ return true;
+}
+
+bool PackProfile::removeComponent_internal(ComponentPtr 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;
+ }
+ }
+
+ // FIXME: we need a generic way of removing local resources, not just jar mods...
+ auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool
+ {
+ if (!jarMod->isLocal())
+ {
+ return true;
+ }
+ QStringList jar, temp1, temp2, temp3;
+ jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath());
+ QFileInfo finfo (jar[0]);
+ if(finfo.exists())
+ {
+ QFile jarModFile(jar[0]);
+ if(!jarModFile.remove())
+ {
+ qCritical() << "File" << jar[0] << "could not be removed because:" << jarModFile.errorString();
+ return false;
+ }
+ return true;
+ }
+ return true;
+ };
+
+ auto vFile = patch->getVersionFile();
+ if(vFile)
+ {
+ auto &jarMods = vFile->jarMods;
+ for(auto &jarmod: jarMods)
+ {
+ ok &= preRemoveJarMod(jarmod);
+ }
+ }
+ return ok;
+}
+
+bool PackProfile::installJarMods_internal(QStringList filepaths)
+{
+ QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
+ if(!FS::ensureFolderPathExists(patchDir))
+ {
+ return false;
+ }
+
+ if (!FS::ensureFolderPathExists(d->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(d->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<Library>();
+ jarMod->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1"));
+ jarMod->setFilename(target_filename);
+ jarMod->setDisplayName(sourceInfo.completeBaseName());
+ jarMod->setHint("local");
+ f->jarMods.append(jarMod);
+ f->name = target_name;
+ f->uid = target_id;
+ QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(new Component(this, f->uid, f));
+ }
+ scheduleSave();
+ invalidateLaunchProfile();
+ return true;
+}
+
+bool PackProfile::installCustomJar_internal(QString filepath)
+{
+ QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
+ if(!FS::ensureFolderPathExists(patchDir))
+ {
+ return false;
+ }
+
+ QString libDir = d->m_instance->getLocalLibraryPath();
+ if (!FS::ensureFolderPathExists(libDir))
+ {
+ return false;
+ }
+
+ auto specifier = GradleSpecifier("org.multimc:customjar:1");
+ QFileInfo sourceInfo(filepath);
+ QString target_filename = specifier.getFileName();
+ QString target_id = specifier.artifactId();
+ QString target_name = sourceInfo.completeBaseName() + " (custom jar)";
+ QString finalPath = FS::PathCombine(libDir, target_filename);
+
+ QFileInfo jarInfo(finalPath);
+ if (jarInfo.exists())
+ {
+ if(!QFile::remove(finalPath))
+ {
+ return false;
+ }
+ }
+ if (!QFile::copy(filepath, finalPath))
+ {
+ return false;
+ }
+
+ auto f = std::make_shared<VersionFile>();
+ auto jarMod = std::make_shared<Library>();
+ jarMod->setRawName(specifier);
+ jarMod->setDisplayName(sourceInfo.completeBaseName());
+ jarMod->setHint("local");
+ f->mainJar = jarMod;
+ f->name = target_name;
+ f->uid = target_id;
+ QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(new Component(this, f->uid, f));
+
+ scheduleSave();
+ invalidateLaunchProfile();
+ return true;
+}
+
+std::shared_ptr<LaunchProfile> PackProfile::getProfile() const
+{
+ if(!d->m_profile)
+ {
+ try
+ {
+ auto profile = std::make_shared<LaunchProfile>();
+ for(auto file: d->components)
+ {
+ qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD");
+ file->applyTo(profile.get());
+ }
+ d->m_profile = profile;
+ }
+ catch (const Exception &error)
+ {
+ qWarning() << "Couldn't apply profile patches because: " << error.cause();
+ }
+ }
+ return d->m_profile;
+}
+
+void PackProfile::setOldConfigVersion(const QString& uid, const QString& version)
+{
+ if(version.isEmpty())
+ {
+ return;
+ }
+ d->m_oldConfigVersions[uid] = version;
+}
+
+bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important)
+{
+ auto iter = d->componentIndex.find(uid);
+ if(iter != d->componentIndex.end())
+ {
+ ComponentPtr component = *iter;
+ // set existing
+ if(component->revert())
+ {
+ component->setVersion(version);
+ component->setImportant(important);
+ return true;
+ }
+ return false;
+ }
+ else
+ {
+ // add new
+ auto component = new Component(this, uid);
+ component->m_version = version;
+ component->m_important = important;
+ appendComponent(component);
+ return true;
+ }
+}
+
+QString PackProfile::getComponentVersion(const QString& uid) const
+{
+ const auto iter = d->componentIndex.find(uid);
+ if (iter != d->componentIndex.end())
+ {
+ return (*iter)->getVersion();
+ }
+ return QString();
+}
+
+void PackProfile::disableInteraction(bool disable)
+{
+ if(d->interactionDisabled != disable) {
+ d->interactionDisabled = disable;
+ auto size = d->components.size();
+ if(size) {
+ emit dataChanged(index(0), index(size - 1));
+ }
+ }
+}
diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h
new file mode 100644
index 00000000..3d6cc6c3
--- /dev/null
+++ b/launcher/minecraft/PackProfile.h
@@ -0,0 +1,151 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QAbstractListModel>
+
+#include <QString>
+#include <QList>
+#include <memory>
+
+#include "Library.h"
+#include "LaunchProfile.h"
+#include "Component.h"
+#include "ProfileUtils.h"
+#include "BaseVersion.h"
+#include "MojangDownloadInfo.h"
+#include "net/Mode.h"
+
+class MinecraftInstance;
+struct PackProfileData;
+class ComponentUpdateTask;
+
+class PackProfile : public QAbstractListModel
+{
+ Q_OBJECT
+ friend ComponentUpdateTask;
+public:
+ enum Columns
+ {
+ NameColumn = 0,
+ VersionColumn,
+ NUM_COLUMNS
+ };
+
+ explicit PackProfile(MinecraftInstance * instance);
+ virtual ~PackProfile();
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+ 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;
+
+ /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch.
+ void buildingFromScratch();
+
+ /// install more jar mods
+ void installJarMods(QStringList selectedFiles);
+
+ /// install a jar/zip as a replacement for the main jar
+ void installCustomJar(QString selectedFile);
+
+ enum MoveDirection { MoveUp, MoveDown };
+ /// move component file # up or down the list
+ void move(const int index, const MoveDirection direction);
+
+ /// remove component file # - including files/records
+ bool remove(const int index);
+
+ /// remove component file by id - including files/records
+ bool remove(const QString id);
+
+ bool customize(int index);
+
+ bool revertToBase(int index);
+
+ /// reload the list, reload all components, resolve dependencies
+ void reload(Net::Mode netmode);
+
+ // reload all components, resolve dependencies
+ void resolve(Net::Mode netmode);
+
+ /// get current running task...
+ shared_qobject_ptr<Task> getCurrentTask();
+
+ std::shared_ptr<LaunchProfile> getProfile() const;
+
+ // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config
+ void setOldConfigVersion(const QString &uid, const QString &version);
+
+ QString getComponentVersion(const QString &uid) const;
+
+ bool setComponentVersion(const QString &uid, const QString &version, bool important = false);
+
+ bool installEmpty(const QString &uid, const QString &name);
+
+ QString patchFilePathForUid(const QString &uid) const;
+
+ /// if there is a save scheduled, do it now.
+ void saveNow();
+
+signals:
+ void minecraftChanged();
+
+public:
+ /// get the profile component by id
+ Component * getComponent(const QString &id);
+
+ /// get the profile component by index
+ Component * getComponent(int index);
+
+ /// Add the component to the internal list of patches
+ // todo(merged): is this the best approach
+ void appendComponent(ComponentPtr component);
+
+private:
+ void scheduleSave();
+ bool saveIsScheduled() const;
+
+ /// apply the component patches. Catches all the errors and returns true/false for success/failure
+ void invalidateLaunchProfile();
+
+ /// insert component so that its index is ideally the specified one (returns real index)
+ void insertComponent(size_t index, ComponentPtr component);
+
+ QString componentsFilePath() const;
+ QString patchesPattern() const;
+
+private slots:
+ void save_internal();
+ void updateSucceeded();
+ void updateFailed(const QString & error);
+ void componentDataChanged();
+ void disableInteraction(bool disable);
+
+private:
+ bool load();
+ bool installJarMods_internal(QStringList filepaths);
+ bool installCustomJar_internal(QString filepath);
+ bool removeComponent_internal(ComponentPtr patch);
+
+ bool migratePreComponentConfig();
+
+private: /* data */
+
+ std::unique_ptr<PackProfileData> d;
+};
diff --git a/launcher/minecraft/PackProfile_p.h b/launcher/minecraft/PackProfile_p.h
new file mode 100644
index 00000000..6cd2a4e5
--- /dev/null
+++ b/launcher/minecraft/PackProfile_p.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "Component.h"
+#include <map>
+#include <QTimer>
+#include <QList>
+#include <QMap>
+
+class MinecraftInstance;
+using ComponentContainer = QList<ComponentPtr>;
+using ComponentIndex = QMap<QString, ComponentPtr>;
+
+struct PackProfileData
+{
+ // the instance this belongs to
+ MinecraftInstance *m_instance;
+
+ // the launch profile (volatile, temporary thing created on demand)
+ std::shared_ptr<LaunchProfile> m_profile;
+
+ // version information migrated from instance.cfg file. Single use on migration!
+ std::map<QString, QString> m_oldConfigVersions;
+ QString getOldConfigVersion(const QString& uid) const
+ {
+ const auto iter = m_oldConfigVersions.find(uid);
+ if(iter != m_oldConfigVersions.cend())
+ {
+ return (*iter).second;
+ }
+ return QString();
+ }
+
+ // persistent list of components and related machinery
+ ComponentContainer components;
+ ComponentIndex componentIndex;
+ bool dirty = false;
+ QTimer m_saveTimer;
+ shared_qobject_ptr<Task> m_updateTask;
+ bool loaded = false;
+ bool interactionDisabled = true;
+};
+
diff --git a/launcher/minecraft/ParseUtils.cpp b/launcher/minecraft/ParseUtils.cpp
new file mode 100644
index 00000000..c9640e77
--- /dev/null
+++ b/launcher/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/launcher/minecraft/ParseUtils.h b/launcher/minecraft/ParseUtils.h
new file mode 100644
index 00000000..aad82748
--- /dev/null
+++ b/launcher/minecraft/ParseUtils.h
@@ -0,0 +1,9 @@
+#pragma once
+#include <QString>
+#include <QDateTime>
+
+/// take the timestamp used by S3 and turn it into QDateTime
+QDateTime timeFromS3Time(QString str);
+
+/// take a timestamp and convert it into an S3 timestamp
+QString timeToS3Time(QDateTime);
diff --git a/launcher/minecraft/ParseUtils_test.cpp b/launcher/minecraft/ParseUtils_test.cpp
new file mode 100644
index 00000000..fcc137e5
--- /dev/null
+++ b/launcher/minecraft/ParseUtils_test.cpp
@@ -0,0 +1,45 @@
+#include <QTest>
+#include "TestUtil.h"
+
+#include "minecraft/ParseUtils.h"
+
+class ParseUtilsTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void test_Through_data()
+ {
+ QTest::addColumn<QString>("timestamp");
+ const char * timestamps[] =
+ {
+ "2016-02-29T13:49:54+01:00",
+ "2016-02-26T15:21:11+00:01",
+ "2016-02-24T15:52:36+01:13",
+ "2016-02-18T17:41:00+00:00",
+ "2016-02-17T15:23:19+00:00",
+ "2016-02-16T15:22:39+09:22",
+ "2016-02-10T15:06:41+00:00",
+ "2016-02-04T15:28:02-05:33"
+ };
+ for(unsigned i = 0; i < (sizeof(timestamps) / sizeof(const char *)); i++)
+ {
+ QTest::newRow(timestamps[i]) << QString(timestamps[i]);
+ }
+ }
+ void test_Through()
+ {
+ QFETCH(QString, timestamp);
+
+ auto time_parsed = timeFromS3Time(timestamp);
+ auto time_serialized = timeToS3Time(time_parsed);
+
+ QCOMPARE(time_serialized, timestamp);
+ }
+
+};
+
+QTEST_GUILESS_MAIN(ParseUtilsTest)
+
+#include "ParseUtils_test.moc"
+
diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp
new file mode 100644
index 00000000..8ca24cc8
--- /dev/null
+++ b/launcher/minecraft/ProfileUtils.cpp
@@ -0,0 +1,178 @@
+#include "ProfileUtils.h"
+#include "minecraft/VersionFilterData.h"
+#include "minecraft/OneSixVersionFormat.h"
+#include "Json.h"
+#include <QDebug>
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QRegularExpression>
+#include <QSaveFile>
+
+namespace ProfileUtils
+{
+
+static const int currentOrderFileVersion = 1;
+
+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 (const 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->uid = outError->name = fileId;
+ // outError->filename = filepath;
+ outError->addProblem(ProblemSeverity::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 (const 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);
+}
+
+bool saveJsonFile(const QJsonDocument doc, const QString & filename)
+{
+ auto data = doc.toJson();
+ QSaveFile jsonFile(filename);
+ if(!jsonFile.open(QIODevice::WriteOnly))
+ {
+ jsonFile.cancelWriting();
+ qWarning() << "Couldn't open" << filename << "for writing";
+ return false;
+ }
+ jsonFile.write(data);
+ if(!jsonFile.commit())
+ {
+ qWarning() << "Couldn't save" << filename;
+ return false;
+ }
+ return true;
+}
+
+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/launcher/minecraft/ProfileUtils.h b/launcher/minecraft/ProfileUtils.h
new file mode 100644
index 00000000..351c36cb
--- /dev/null
+++ b/launcher/minecraft/ProfileUtils.h
@@ -0,0 +1,28 @@
+#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);
+
+/// Save a JSON file (in any format)
+bool saveJsonFile(const QJsonDocument doc, const QString & filename);
+
+/// 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/launcher/minecraft/Rule.cpp b/launcher/minecraft/Rule.cpp
new file mode 100644
index 00000000..af2861e3
--- /dev/null
+++ b/launcher/minecraft/Rule.cpp
@@ -0,0 +1,93 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <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/launcher/minecraft/Rule.h b/launcher/minecraft/Rule.h
new file mode 100644
index 00000000..7aa34d96
--- /dev/null
+++ b/launcher/minecraft/Rule.h
@@ -0,0 +1,101 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp
new file mode 100644
index 00000000..d0a1a507
--- /dev/null
+++ b/launcher/minecraft/VersionFile.cpp
@@ -0,0 +1,60 @@
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+#include "minecraft/VersionFile.h"
+#include "minecraft/Library.h"
+#include "minecraft/PackProfile.h"
+#include "ParseUtils.h"
+
+#include <Version.h>
+
+static bool isMinecraftVersion(const QString &uid)
+{
+ return uid == "net.minecraft";
+}
+
+void VersionFile::applyTo(LaunchProfile *profile)
+{
+ // Only real Minecraft can set those. Don't let anything override them.
+ if (isMinecraftVersion(uid))
+ {
+ profile->applyMinecraftVersion(minecraftVersion);
+ profile->applyMinecraftVersionType(type);
+ // HACK: ignore assets from other version files than Minecraft
+ // workaround for stupid assets issue caused by amazon:
+ // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/
+ profile->applyMinecraftAssets(mojangAssetIndex);
+ }
+
+ profile->applyMainJar(mainJar);
+ profile->applyMainClass(mainClass);
+ profile->applyAppletClass(appletClass);
+ profile->applyMinecraftArguments(minecraftArguments);
+ profile->applyTweakers(addTweakers);
+ profile->applyJarMods(jarMods);
+ profile->applyMods(mods);
+ profile->applyTraits(traits);
+
+ for (auto library : libraries)
+ {
+ profile->applyLibrary(library);
+ }
+ for (auto mavenFile : mavenFiles)
+ {
+ profile->applyMavenFile(mavenFile);
+ }
+ profile->applyProblemSeverity(getProblemSeverity());
+}
+
+/*
+ auto theirVersion = profile->getMinecraftVersion();
+ if (!theirVersion.isNull() && !dependsOnMinecraftVersion.isNull())
+ {
+ if (QRegExp(dependsOnMinecraftVersion, Qt::CaseInsensitive, QRegExp::Wildcard).indexIn(theirVersion) == -1)
+ {
+ throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion);
+ }
+ }
+*/
diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h
new file mode 100644
index 00000000..b79fcd4f
--- /dev/null
+++ b/launcher/minecraft/VersionFile.h
@@ -0,0 +1,114 @@
+#pragma once
+
+#include <QString>
+#include <QStringList>
+#include <QDateTime>
+#include <QSet>
+
+#include <memory>
+#include "minecraft/OpSys.h"
+#include "minecraft/Rule.h"
+#include "ProblemProvider.h"
+#include "Library.h"
+#include <meta/JsonFormat.h>
+
+class PackProfile;
+class VersionFile;
+class LaunchProfile;
+struct MojangDownloadInfo;
+struct MojangAssetIndexInfo;
+
+using VersionFilePtr = std::shared_ptr<VersionFile>;
+class VersionFile : public ProblemContainer
+{
+ friend class MojangVersionFormat;
+ friend class OneSixVersionFormat;
+public: /* methods */
+ void applyTo(LaunchProfile* profile);
+
+public: /* data */
+ /// MultiMC: order hint for this version file if no explicit order is set
+ int order = 0;
+
+ /// MultiMC: human readable name of this package
+ QString name;
+
+ /// MultiMC: package ID of this package
+ QString uid;
+
+ /// MultiMC: version of this package
+ QString version;
+
+ /// MultiMC: DEPRECATED dependency on a Minecraft version
+ QString dependsOnMinecraftVersion;
+
+ /// Mojang: DEPRECATED used to version the Mojang version format
+ int minimumLauncherVersion = -1;
+
+ /// Mojang: DEPRECATED version of Minecraft this is
+ QString minecraftVersion;
+
+ /// Mojang: class to launch Minecraft with
+ QString mainClass;
+
+ /// MultiMC: class to launch legacy Minecraft with (embed 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 releaseTime;
+
+ /// Mojang: DEPRECATED the time this version was last updated by Mojang
+ QDateTime 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 maven files to put in the libraries folder, but not in classpath
+ QList<LibraryPtr> mavenFiles;
+
+ /// The main jar (Minecraft version library, normally)
+ LibraryPtr mainJar;
+
+ /// 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<LibraryPtr> jarMods;
+
+ /// MultiMC: list of mods added to this version
+ QList<LibraryPtr> mods;
+
+ /**
+ * MultiMC: set of packages this depends on
+ * NOTE: this is shared with the meta format!!!
+ */
+ Meta::RequireSet requires;
+
+ /**
+ * MultiMC: set of packages this conflicts with
+ * NOTE: this is shared with the meta format!!!
+ */
+ Meta::RequireSet conflicts;
+
+ /// is volatile -- may be removed as soon as it is no longer needed by something else
+ bool m_volatile = false;
+
+public:
+ // Mojang: DEPRECATED 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/launcher/minecraft/VersionFilterData.cpp b/launcher/minecraft/VersionFilterData.cpp
new file mode 100644
index 00000000..38e7b60c
--- /dev/null
+++ b/launcher/minecraft/VersionFilterData.cpp
@@ -0,0 +1,71 @@
+#include "VersionFilterData.h"
+#include "ParseUtils.h"
+
+VersionFilterData g_VersionFilterData = VersionFilterData();
+
+VersionFilterData::VersionFilterData()
+{
+ // 1.3.*
+ auto libs13 =
+ QList<FMLlib>{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"},
+ {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"},
+ {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}};
+
+ fmlLibsMapping["1.3.2"] = libs13;
+
+ // 1.4.*
+ auto libs14 = QList<FMLlib>{
+ {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"},
+ {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"},
+ {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"},
+ {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb"}};
+
+ fmlLibsMapping["1.4"] = libs14;
+ fmlLibsMapping["1.4.1"] = libs14;
+ 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"},
+ {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"},
+ {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"},
+ {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"},
+ {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8"},
+ {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}};
+
+ // 1.5.1
+ fmlLibsMapping["1.5.1"] = QList<FMLlib>{
+ {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"},
+ {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"},
+ {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"},
+ {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"},
+ {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6"},
+ {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}};
+
+ // 1.5.2
+ fmlLibsMapping["1.5.2"] = QList<FMLlib>{
+ {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"},
+ {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"},
+ {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"},
+ {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"},
+ {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9"},
+ {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}};
+
+ // don't use installers for those.
+ forgeInstallerBlacklist = QSet<QString>({"1.5.2"});
+
+ // FIXME: remove, used for deciding when core mods should display
+ 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"};
+
+ java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00");
+ java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00");
+}
diff --git a/launcher/minecraft/VersionFilterData.h b/launcher/minecraft/VersionFilterData.h
new file mode 100644
index 00000000..79756c3f
--- /dev/null
+++ b/launcher/minecraft/VersionFilterData.h
@@ -0,0 +1,29 @@
+#pragma once
+#include <QMap>
+#include <QString>
+#include <QSet>
+#include <QDateTime>
+
+struct FMLlib
+{
+ QString filename;
+ QString checksum;
+};
+
+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;
+ // no new versions below this date will be accepted from Mojang servers
+ QDateTime legacyCutoffDate;
+ // Libraries that belong to LWJGL
+ QSet<QString> lwjglWhitelist;
+ // release date of first version to require Java 8 (17w13a)
+ QDateTime java8BeginsDate;
+ // release data of first version to require Java 16 (21w19a)
+ QDateTime java16BeginsDate;
+};
+extern VersionFilterData g_VersionFilterData;
diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp
new file mode 100644
index 00000000..a2b4dac7
--- /dev/null
+++ b/launcher/minecraft/World.cpp
@@ -0,0 +1,520 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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>
+
+#include <QCoreApplication>
+
+#include <nonstd/optional>
+
+using nonstd::optional;
+using nonstd::nullopt;
+
+GameType::GameType(nonstd::optional<int> original):
+ original(original)
+{
+ if(!original) {
+ return;
+ }
+ switch(*original) {
+ case 0:
+ type = GameType::Survival;
+ break;
+ case 1:
+ type = GameType::Creative;
+ break;
+ case 2:
+ type = GameType::Adventure;
+ break;
+ case 3:
+ type = GameType::Spectator;
+ break;
+ default:
+ break;
+ }
+}
+
+QString GameType::toTranslatedString() const
+{
+ switch (type)
+ {
+ case GameType::Survival:
+ return QCoreApplication::translate("GameType", "Survival");
+ case GameType::Creative:
+ return QCoreApplication::translate("GameType", "Creative");
+ case GameType::Adventure:
+ return QCoreApplication::translate("GameType", "Adventure");
+ case GameType::Spectator:
+ return QCoreApplication::translate("GameType", "Spectator");
+ default:
+ break;
+ }
+ if(original) {
+ return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original);
+ }
+ return QCoreApplication::translate("GameType", "Undefined");
+}
+
+QString GameType::toLogString() const
+{
+ switch (type)
+ {
+ case GameType::Survival:
+ return "Survival";
+ case GameType::Creative:
+ return "Creative";
+ case GameType::Adventure:
+ return "Adventure";
+ case GameType::Spectator:
+ return "Spectator";
+ default:
+ break;
+ }
+ if(original) {
+ return QString("Unknown (%1)").arg(*original);
+ }
+ return "Undefined";
+}
+
+std::unique_ptr <nbt::tag_compound> parseLevelDat(QByteArray data)
+{
+ QByteArray output;
+ if(!GZip::unzip(data, output))
+ {
+ return nullptr;
+ }
+ std::istringstream foo(std::string(output.constData(), output.size()));
+ try {
+ auto pair = nbt::io::read_compound(foo);
+
+ if(pair.first != "")
+ return nullptr;
+
+ if(pair.second == nullptr)
+ return nullptr;
+
+ return std::move(pair.second);
+ }
+ catch (const nbt::io::input_error &e)
+ {
+ qWarning() << "Unable to parse level.dat:" << e.what();
+ return nullptr;
+ }
+}
+
+QByteArray serializeLevelDat(nbt::tag_compound * levelInfo)
+{
+ 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")
+ {
+ m_iconFile = QString();
+ readFromZip(file);
+ }
+ else if(file.isDir())
+ {
+ QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png");
+ if(assumedIconPath.exists()) {
+ m_iconFile = assumedIconPath.absoluteFilePath();
+ }
+ readFromFS(file);
+ }
+}
+
+bool World::resetIcon()
+{
+ if(m_iconFile.isNull()) {
+ return false;
+ }
+ if(QFile(m_iconFile).remove()) {
+ m_iconFile = QString();
+ return true;
+ }
+ return false;
+}
+
+void World::readFromFS(const QFileInfo &file)
+{
+ auto bytes = getLevelDatDataFromFS(file);
+ 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::findFolderOfFileInZip(&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);
+ }
+ 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;
+}
+
+namespace {
+
+optional<QString> read_string (nbt::value& parent, const char * name)
+{
+ try
+ {
+ auto &namedValue = parent.at(name);
+ if(namedValue.get_type() != nbt::tag_type::String)
+ {
+ return nullopt;
+ }
+ auto & tag_str = namedValue.as<nbt::tag_string>();
+ return QString::fromStdString(tag_str.get());
+ }
+ catch (const std::out_of_range &e)
+ {
+ // fallback for old world formats
+ qWarning() << "String NBT tag" << name << "could not be found.";
+ return nullopt;
+ }
+ catch (const std::bad_cast &e)
+ {
+ // type mismatch
+ qWarning() << "NBT tag" << name << "could not be converted to string.";
+ return nullopt;
+ }
+}
+
+optional<int64_t> read_long (nbt::value& parent, const char * name)
+{
+ try
+ {
+ auto &namedValue = parent.at(name);
+ if(namedValue.get_type() != nbt::tag_type::Long)
+ {
+ return nullopt;
+ }
+ auto & tag_str = namedValue.as<nbt::tag_long>();
+ return tag_str.get();
+ }
+ catch (const std::out_of_range &e)
+ {
+ // fallback for old world formats
+ qWarning() << "Long NBT tag" << name << "could not be found.";
+ return nullopt;
+ }
+ catch (const std::bad_cast &e)
+ {
+ // type mismatch
+ qWarning() << "NBT tag" << name << "could not be converted to long.";
+ return nullopt;
+ }
+}
+
+optional<int> read_int (nbt::value& parent, const char * name)
+{
+ try
+ {
+ auto &namedValue = parent.at(name);
+ if(namedValue.get_type() != nbt::tag_type::Int)
+ {
+ return nullopt;
+ }
+ auto & tag_str = namedValue.as<nbt::tag_int>();
+ return tag_str.get();
+ }
+ catch (const std::out_of_range &e)
+ {
+ // fallback for old world formats
+ qWarning() << "Int NBT tag" << name << "could not be found.";
+ return nullopt;
+ }
+ catch (const std::bad_cast &e)
+ {
+ // type mismatch
+ qWarning() << "NBT tag" << name << "could not be converted to int.";
+ return nullopt;
+ }
+}
+
+GameType read_gametype(nbt::value& parent, const char * name) {
+ return GameType(read_int(parent, name));
+}
+
+}
+
+void World::loadFromLevelDat(QByteArray data)
+{
+ auto levelData = parseLevelDat(data);
+ if(!levelData)
+ {
+ is_valid = false;
+ return;
+ }
+
+ nbt::value * valPtr = nullptr;
+ try {
+ valPtr = &levelData->at("Data");
+ }
+ catch (const std::out_of_range &e) {
+ qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what();
+ is_valid = false;
+ return;
+ }
+ nbt::value &val = *valPtr;
+
+ is_valid = val.get_type() == nbt::tag_type::Compound;
+ if(!is_valid)
+ return;
+
+ auto name = read_string(val, "LevelName");
+ m_actualName = name ? *name : m_folderName;
+
+ auto timestamp = read_long(val, "LastPlayed");
+ m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime;
+
+ m_gameType = read_gametype(val, "GameType");
+
+ optional<int64_t> randomSeed;
+ try {
+ auto &WorldGen_val = val.at("WorldGenSettings");
+ randomSeed = read_long(WorldGen_val, "seed");
+ }
+ catch (const std::out_of_range &) {}
+ if(!randomSeed) {
+ randomSeed = read_long(val, "RandomSeed");
+ }
+ m_randomSeed = randomSeed ? *randomSeed : 0;
+
+ qDebug() << "World Name:" << m_actualName;
+ qDebug() << "Last Played:" << m_lastPlayed.toString();
+ if(randomSeed) {
+ qDebug() << "Seed:" << *randomSeed;
+ }
+ qDebug() << "GameType:" << m_gameType.toLogString();
+}
+
+bool World::replace(World &with)
+{
+ 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();
+}
diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h
new file mode 100644
index 00000000..35e32788
--- /dev/null
+++ b/launcher/minecraft/World.h
@@ -0,0 +1,111 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 <nonstd/optional>
+
+struct GameType {
+ GameType() = default;
+ GameType (nonstd::optional<int> original);
+
+ QString toTranslatedString() const;
+ QString toLogString() const;
+
+ enum
+ {
+ Unknown = -1,
+ Survival = 0,
+ Creative,
+ Adventure,
+ Spectator
+ } type = Unknown;
+ nonstd::optional<int> original;
+};
+
+class World
+{
+public:
+ World(const QFileInfo &file);
+ QString folderName() const
+ {
+ return m_folderName;
+ }
+ QString name() const
+ {
+ return m_actualName;
+ }
+ QString iconFile() const
+ {
+ return m_iconFile;
+ }
+ QDateTime lastPlayed() const
+ {
+ return m_lastPlayed;
+ }
+ GameType gameType() const
+ {
+ return m_gameType;
+ }
+ int64_t seed() const
+ {
+ return m_randomSeed;
+ }
+ 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);
+ // remove the icon file, if any
+ bool resetIcon();
+
+ bool rename(const QString &to);
+ bool install(const QString &to, const QString &name= QString());
+
+ // WEAK compare operator - used for replacing worlds
+ bool operator==(const World &other) const;
+
+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;
+ QString m_iconFile;
+ QDateTime levelDatTime;
+ QDateTime m_lastPlayed;
+ int64_t m_randomSeed = 0;
+ GameType m_gameType;
+ bool is_valid = false;
+};
diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp
new file mode 100644
index 00000000..f6309dbd
--- /dev/null
+++ b/launcher/minecraft/WorldList.cpp
@@ -0,0 +1,387 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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()
+{
+ if(is_watching)
+ {
+ return;
+ }
+ update();
+ is_watching = m_watcher->addPath(m_dir.absolutePath());
+ if (is_watching)
+ {
+ qDebug() << "Started watching " << m_dir.absolutePath();
+ }
+ else
+ {
+ qDebug() << "Failed to start watching " << m_dir.absolutePath();
+ }
+}
+
+void WorldList::stopWatching()
+{
+ if(!is_watching)
+ {
+ return;
+ }
+ is_watching = !m_watcher->removePath(m_dir.absolutePath());
+ if (!is_watching)
+ {
+ qDebug() << "Stopped watching " << m_dir.absolutePath();
+ }
+ else
+ {
+ qDebug() << "Failed to stop watching " << m_dir.absolutePath();
+ }
+}
+
+bool 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;
+}
+
+bool WorldList::resetIcon(int row)
+{
+ if (row >= worlds.size() || row < 0)
+ return false;
+ World &m = worlds[row];
+ if(m.resetIcon()) {
+ emit dataChanged(index(row), index(row), {WorldList::IconFileRole});
+ return true;
+ }
+ return false;
+}
+
+
+int WorldList::columnCount(const QModelIndex &parent) const
+{
+ return 3;
+}
+
+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 GameModeColumn:
+ return world.gameType().toTranslatedString();
+
+ 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();
+ }
+ case IconFileRole:
+ {
+ return world.iconFile();
+ }
+ 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 GameModeColumn:
+ return tr("Game Mode");
+ case LastPlayedColumn:
+ return tr("Last Played");
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case NameColumn:
+ return tr("The name of the world.");
+ case GameModeColumn:
+ return tr("Game mode of the world.");
+ case LastPlayedColumn:
+ return tr("Date and time the world was last played.");
+ default:
+ 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/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h
new file mode 100644
index 00000000..8e238ee3
--- /dev/null
+++ b/launcher/minecraft/WorldList.h
@@ -0,0 +1,129 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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"
+
+class QFileSystemWatcher;
+
+class WorldList : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Columns
+ {
+ NameColumn,
+ GameModeColumn,
+ LastPlayedColumn
+ };
+
+ enum Roles
+ {
+ ObjectRole = Qt::UserRole + 1,
+ FolderRole,
+ SeedRole,
+ NameRole,
+ GameModeRole,
+ LastPlayedRole,
+ IconFileRole
+ };
+
+ 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);
+
+ /// Removes the world icon, if any
+ virtual bool resetIcon(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/launcher/minecraft/auth-msa/BuildConfig.cpp.in b/launcher/minecraft/auth-msa/BuildConfig.cpp.in
new file mode 100644
index 00000000..8f470e25
--- /dev/null
+++ b/launcher/minecraft/auth-msa/BuildConfig.cpp.in
@@ -0,0 +1,9 @@
+#include "BuildConfig.h"
+#include <QObject>
+
+const Config BuildConfig;
+
+Config::Config()
+{
+ CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@";
+}
diff --git a/launcher/minecraft/auth-msa/BuildConfig.h b/launcher/minecraft/auth-msa/BuildConfig.h
new file mode 100644
index 00000000..7a01d704
--- /dev/null
+++ b/launcher/minecraft/auth-msa/BuildConfig.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <QString>
+
+class Config
+{
+public:
+ Config();
+ QString CLIENT_ID;
+};
+
+extern const Config BuildConfig;
diff --git a/launcher/minecraft/auth-msa/CMakeLists.txt b/launcher/minecraft/auth-msa/CMakeLists.txt
new file mode 100644
index 00000000..22777d1b
--- /dev/null
+++ b/launcher/minecraft/auth-msa/CMakeLists.txt
@@ -0,0 +1,28 @@
+find_package(Qt5 COMPONENTS Core Gui Network Widgets REQUIRED)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
+
+
+set(MOJANGDEMO_CLIENT_ID "" CACHE STRING "Client ID used for OAuth2 in mojangdemo")
+
+configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp")
+
+set(mojang_SRCS
+ main.cpp
+ context.cpp
+ context.h
+
+ mainwindow.cpp
+ mainwindow.h
+ mainwindow.ui
+
+ ${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp
+ BuildConfig.h
+)
+
+add_executable( mojangdemo ${mojang_SRCS} )
+target_link_libraries( mojangdemo Katabasis Qt5::Gui Qt5::Widgets )
+target_include_directories(mojangdemo PRIVATE logic)
diff --git a/launcher/minecraft/auth-msa/context.cpp b/launcher/minecraft/auth-msa/context.cpp
new file mode 100644
index 00000000..d7ecda30
--- /dev/null
+++ b/launcher/minecraft/auth-msa/context.cpp
@@ -0,0 +1,938 @@
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QDesktopServices>
+#include <QMetaEnum>
+#include <QDebug>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include <QUrlQuery>
+
+#include <QPixmap>
+#include <QPainter>
+
+#include "context.h"
+#include "katabasis/Globals.h"
+#include "katabasis/StoreQSettings.h"
+#include "katabasis/Requestor.h"
+#include "BuildConfig.h"
+
+using OAuth2 = Katabasis::OAuth2;
+using Requestor = Katabasis::Requestor;
+using Activity = Katabasis::Activity;
+
+Context::Context(QObject *parent) :
+ QObject(parent)
+{
+ mgr = new QNetworkAccessManager(this);
+
+ Katabasis::OAuth2::Options opts;
+ opts.scope = "XboxLive.signin offline_access";
+ opts.clientIdentifier = BuildConfig.CLIENT_ID;
+ opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf";
+ opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf";
+ opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
+
+ oauth2 = new OAuth2(opts, m_account.msaToken, this, mgr);
+
+ connect(oauth2, &OAuth2::linkingFailed, this, &Context::onLinkingFailed);
+ connect(oauth2, &OAuth2::linkingSucceeded, this, &Context::onLinkingSucceeded);
+ connect(oauth2, &OAuth2::openBrowser, this, &Context::onOpenBrowser);
+ connect(oauth2, &OAuth2::closeBrowser, this, &Context::onCloseBrowser);
+ connect(oauth2, &OAuth2::activityChanged, this, &Context::onOAuthActivityChanged);
+}
+
+void Context::beginActivity(Activity activity) {
+ if(isBusy()) {
+ throw 0;
+ }
+ activity_ = activity;
+ emit activityChanged(activity_);
+}
+
+void Context::finishActivity() {
+ if(!isBusy()) {
+ throw 0;
+ }
+ activity_ = Katabasis::Activity::Idle;
+ m_account.validity_ = m_account.minecraftProfile.validity;
+ emit activityChanged(activity_);
+}
+
+QString Context::gameToken() {
+ return m_account.minecraftToken.token;
+}
+
+QString Context::userId() {
+ return m_account.minecraftProfile.id;
+}
+
+QString Context::userName() {
+ return m_account.minecraftProfile.name;
+}
+
+bool Context::silentSignIn() {
+ if(isBusy()) {
+ return false;
+ }
+ beginActivity(Activity::Refreshing);
+ if(!oauth2->refresh()) {
+ finishActivity();
+ return false;
+ }
+
+ requestsDone = 0;
+ xboxProfileSucceeded = false;
+ mcAuthSucceeded = false;
+
+ return true;
+}
+
+bool Context::signIn() {
+ if(isBusy()) {
+ return false;
+ }
+
+ requestsDone = 0;
+ xboxProfileSucceeded = false;
+ mcAuthSucceeded = false;
+
+ beginActivity(Activity::LoggingIn);
+ oauth2->unlink();
+ m_account = AccountData();
+ oauth2->link();
+ return true;
+}
+
+bool Context::signOut() {
+ if(isBusy()) {
+ return false;
+ }
+ beginActivity(Activity::LoggingOut);
+ oauth2->unlink();
+ m_account = AccountData();
+ finishActivity();
+ return true;
+}
+
+
+void Context::onOpenBrowser(const QUrl &url) {
+ QDesktopServices::openUrl(url);
+}
+
+void Context::onCloseBrowser() {
+
+}
+
+void Context::onLinkingFailed() {
+ finishActivity();
+}
+
+void Context::onLinkingSucceeded() {
+ auto *o2t = qobject_cast<OAuth2 *>(sender());
+ if (!o2t->linked()) {
+ finishActivity();
+ return;
+ }
+ QVariantMap extraTokens = o2t->extraTokens();
+ if (!extraTokens.isEmpty()) {
+ qDebug() << "Extra tokens in response:";
+ foreach (QString key, extraTokens.keys()) {
+ qDebug() << "\t" << key << ":" << extraTokens.value(key);
+ }
+ }
+ doUserAuth();
+}
+
+void Context::onOAuthActivityChanged(Katabasis::Activity activity) {
+ // respond to activity change here
+}
+
+void Context::doUserAuth() {
+ QString xbox_auth_template = R"XXX(
+{
+ "Properties": {
+ "AuthMethod": "RPS",
+ "SiteName": "user.auth.xboxlive.com",
+ "RpsTicket": "d=%1"
+ },
+ "RelyingParty": "http://auth.xboxlive.com",
+ "TokenType": "JWT"
+}
+)XXX";
+ auto xbox_auth_data = xbox_auth_template.arg(m_account.msaToken.token);
+
+ QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ auto *requestor = new Katabasis::Requestor(mgr, oauth2, this);
+ requestor->setAddAccessTokenInQuery(false);
+
+ connect(requestor, &Requestor::finished, this, &Context::onUserAuthDone);
+ requestor->post(request, xbox_auth_data.toUtf8());
+ qDebug() << "First layer of XBox auth ... commencing.";
+}
+
+namespace {
+bool getDateTime(QJsonValue value, QDateTime & out) {
+ if(!value.isString()) {
+ return false;
+ }
+ out = QDateTime::fromString(value.toString(), Qt::ISODateWithMs);
+ return out.isValid();
+}
+
+bool getString(QJsonValue value, QString & out) {
+ if(!value.isString()) {
+ return false;
+ }
+ out = value.toString();
+ return true;
+}
+
+bool getNumber(QJsonValue value, double & out) {
+ if(!value.isDouble()) {
+ return false;
+ }
+ out = value.toDouble();
+ return true;
+}
+
+/*
+{
+ "IssueInstant":"2020-12-07T19:52:08.4463796Z",
+ "NotAfter":"2020-12-21T19:52:08.4463796Z",
+ "Token":"token",
+ "DisplayClaims":{
+ "xui":[
+ {
+ "uhs":"userhash"
+ }
+ ]
+ }
+ }
+*/
+// TODO: handle error responses ...
+/*
+{
+ "Identity":"0",
+ "XErr":2148916238,
+ "Message":"",
+ "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
+}
+// 2148916233 = missing XBox account
+// 2148916238 = child account not linked to a family
+*/
+
+bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) {
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if(jsonError.error) {
+ qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
+ qDebug() << data;
+ return false;
+ }
+
+ auto obj = doc.object();
+ if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) {
+ qWarning() << "User IssueInstant is not a timestamp";
+ qDebug() << data;
+ return false;
+ }
+ if(!getDateTime(obj.value("NotAfter"), output.notAfter)) {
+ qWarning() << "User NotAfter is not a timestamp";
+ qDebug() << data;
+ return false;
+ }
+ if(!getString(obj.value("Token"), output.token)) {
+ qWarning() << "User Token is not a timestamp";
+ qDebug() << data;
+ return false;
+ }
+ auto arrayVal = obj.value("DisplayClaims").toObject().value("xui");
+ if(!arrayVal.isArray()) {
+ qWarning() << "Missing xui claims array";
+ qDebug() << data;
+ return false;
+ }
+ bool foundUHS = false;
+ for(auto item: arrayVal.toArray()) {
+ if(!item.isObject()) {
+ continue;
+ }
+ auto obj = item.toObject();
+ if(obj.contains("uhs")) {
+ foundUHS = true;
+ } else {
+ continue;
+ }
+ // consume all 'display claims' ... whatever that means
+ for(auto iter = obj.begin(); iter != obj.end(); iter++) {
+ QString claim;
+ if(!getString(obj.value(iter.key()), claim)) {
+ qWarning() << "display claim " << iter.key() << " is not a string...";
+ qDebug() << data;
+ return false;
+ }
+ output.extra[iter.key()] = claim;
+ }
+
+ break;
+ }
+ if(!foundUHS) {
+ qWarning() << "Missing uhs";
+ qDebug() << data;
+ return false;
+ }
+ output.validity = Katabasis::Validity::Certain;
+ qDebug() << data;
+ return true;
+}
+
+}
+
+void Context::onUserAuthDone(
+ int requestId,
+ QNetworkReply::NetworkError error,
+ QByteArray replyData,
+ QList<QNetworkReply::RawHeaderPair> headers
+) {
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ finishActivity();
+ return;
+ }
+
+ Katabasis::Token temp;
+ if(!parseXTokenResponse(replyData, temp)) {
+ qWarning() << "Could not parse user authentication response...";
+ finishActivity();
+ return;
+ }
+ m_account.userToken = temp;
+
+ doSTSAuthMinecraft();
+ doSTSAuthGeneric();
+}
+/*
+ url = "https://xsts.auth.xboxlive.com/xsts/authorize"
+ headers = {"x-xbl-contract-version": "1"}
+ data = {
+ "RelyingParty": relying_party,
+ "TokenType": "JWT",
+ "Properties": {
+ "UserTokens": [self.user_token.token],
+ "SandboxId": "RETAIL",
+ },
+ }
+*/
+void Context::doSTSAuthMinecraft() {
+ QString xbox_auth_template = R"XXX(
+{
+ "Properties": {
+ "SandboxId": "RETAIL",
+ "UserTokens": [
+ "%1"
+ ]
+ },
+ "RelyingParty": "rp://api.minecraftservices.com/",
+ "TokenType": "JWT"
+}
+)XXX";
+ auto xbox_auth_data = xbox_auth_template.arg(m_account.userToken.token);
+
+ QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ Requestor *requestor = new Requestor(mgr, oauth2, this);
+ requestor->setAddAccessTokenInQuery(false);
+
+ connect(requestor, &Requestor::finished, this, &Context::onSTSAuthMinecraftDone);
+ requestor->post(request, xbox_auth_data.toUtf8());
+ qDebug() << "Second layer of XBox auth ... commencing.";
+}
+
+void Context::onSTSAuthMinecraftDone(
+ int requestId,
+ QNetworkReply::NetworkError error,
+ QByteArray replyData,
+ QList<QNetworkReply::RawHeaderPair> headers
+) {
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ finishActivity();
+ return;
+ }
+
+ Katabasis::Token temp;
+ if(!parseXTokenResponse(replyData, temp)) {
+ qWarning() << "Could not parse authorization response for access to mojang services...";
+ finishActivity();
+ return;
+ }
+
+ if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
+ qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
+ qDebug() << replyData;
+ finishActivity();
+ return;
+ }
+ m_account.mojangservicesToken = temp;
+
+ doMinecraftAuth();
+}
+
+void Context::doSTSAuthGeneric() {
+ QString xbox_auth_template = R"XXX(
+{
+ "Properties": {
+ "SandboxId": "RETAIL",
+ "UserTokens": [
+ "%1"
+ ]
+ },
+ "RelyingParty": "http://xboxlive.com",
+ "TokenType": "JWT"
+}
+)XXX";
+ auto xbox_auth_data = xbox_auth_template.arg(m_account.userToken.token);
+
+ QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ Requestor *requestor = new Requestor(mgr, oauth2, this);
+ requestor->setAddAccessTokenInQuery(false);
+
+ connect(requestor, &Requestor::finished, this, &Context::onSTSAuthGenericDone);
+ requestor->post(request, xbox_auth_data.toUtf8());
+ qDebug() << "Second layer of XBox auth ... commencing.";
+}
+
+void Context::onSTSAuthGenericDone(
+ int requestId,
+ QNetworkReply::NetworkError error,
+ QByteArray replyData,
+ QList<QNetworkReply::RawHeaderPair> headers
+) {
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ finishActivity();
+ return;
+ }
+
+ Katabasis::Token temp;
+ if(!parseXTokenResponse(replyData, temp)) {
+ qWarning() << "Could not parse authorization response for access to xbox API...";
+ finishActivity();
+ return;
+ }
+
+ if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
+ qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
+ qDebug() << replyData;
+ finishActivity();
+ return;
+ }
+ m_account.xboxApiToken = temp;
+
+ doXBoxProfile();
+}
+
+
+void Context::doMinecraftAuth() {
+ QString mc_auth_template = R"XXX(
+{
+ "identityToken": "XBL3.0 x=%1;%2"
+}
+)XXX";
+ auto data = mc_auth_template.arg(m_account.mojangservicesToken.extra["uhs"].toString(), m_account.mojangservicesToken.token);
+
+ QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox"));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ Requestor *requestor = new Requestor(mgr, oauth2, this);
+ requestor->setAddAccessTokenInQuery(false);
+
+ connect(requestor, &Requestor::finished, this, &Context::onMinecraftAuthDone);
+ requestor->post(request, data.toUtf8());
+ qDebug() << "Getting Minecraft access token...";
+}
+
+namespace {
+bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) {
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if(jsonError.error) {
+ qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
+ qDebug() << data;
+ return false;
+ }
+
+ auto obj = doc.object();
+ double expires_in = 0;
+ if(!getNumber(obj.value("expires_in"), expires_in)) {
+ qWarning() << "expires_in is not a valid number";
+ qDebug() << data;
+ return false;
+ }
+ auto currentTime = QDateTime::currentDateTimeUtc();
+ output.issueInstant = currentTime;
+ output.notAfter = currentTime.addSecs(expires_in);
+
+ QString username;
+ if(!getString(obj.value("username"), username)) {
+ qWarning() << "username is not valid";
+ qDebug() << data;
+ return false;
+ }
+
+ // TODO: it's a JWT... validate it?
+ if(!getString(obj.value("access_token"), output.token)) {
+ qWarning() << "access_token is not valid";
+ qDebug() << data;
+ return false;
+ }
+ output.validity = Katabasis::Validity::Certain;
+ qDebug() << data;
+ return true;
+}
+}
+
+void Context::onMinecraftAuthDone(
+ int requestId,
+ QNetworkReply::NetworkError error,
+ QByteArray replyData,
+ QList<QNetworkReply::RawHeaderPair> headers
+) {
+ requestsDone++;
+
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ qDebug() << replyData;
+ finishActivity();
+ return;
+ }
+
+ if(!parseMojangResponse(replyData, m_account.minecraftToken)) {
+ qWarning() << "Could not parse login_with_xbox response...";
+ qDebug() << replyData;
+ finishActivity();
+ return;
+ }
+ mcAuthSucceeded = true;
+
+ checkResult();
+}
+
+void Context::doXBoxProfile() {
+ auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
+ QUrlQuery q;
+ q.addQueryItem(
+ "settings",
+ "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
+ "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
+ "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
+ "PreferredColor,Location,Bio,Watermarks,"
+ "RealName,RealNameOverride,IsQuarantined"
+ );
+ url.setQuery(q);
+
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader("x-xbl-contract-version", "3");
+ request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_account.userToken.extra["uhs"].toString(), m_account.xboxApiToken.token).toUtf8());
+ Requestor *requestor = new Requestor(mgr, oauth2, this);
+ requestor->setAddAccessTokenInQuery(false);
+
+ connect(requestor, &Requestor::finished, this, &Context::onXBoxProfileDone);
+ requestor->get(request);
+ qDebug() << "Getting Xbox profile...";
+}
+
+void Context::onXBoxProfileDone(
+ int requestId,
+ QNetworkReply::NetworkError error,
+ QByteArray replyData,
+ QList<QNetworkReply::RawHeaderPair> headers
+) {
+ requestsDone ++;
+
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ qDebug() << replyData;
+ finishActivity();
+ return;
+ }
+
+ qDebug() << "XBox profile: " << replyData;
+
+ xboxProfileSucceeded = true;
+ checkResult();
+}
+
+void Context::checkResult() {
+ if(requestsDone != 2) {
+ return;
+ }
+ if(mcAuthSucceeded && xboxProfileSucceeded) {
+ doMinecraftProfile();
+ }
+ else {
+ finishActivity();
+ }
+}
+
+namespace {
+bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if(jsonError.error) {
+ qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
+ qDebug() << data;
+ return false;
+ }
+
+ auto obj = doc.object();
+ if(!getString(obj.value("id"), output.id)) {
+ qWarning() << "minecraft profile id is not a string";
+ qDebug() << data;
+ return false;
+ }
+
+ if(!getString(obj.value("name"), output.name)) {
+ qWarning() << "minecraft profile name is not a string";
+ qDebug() << data;
+ return false;
+ }
+
+ auto skinsArray = obj.value("skins").toArray();
+ for(auto skin: skinsArray) {
+ auto skinObj = skin.toObject();
+ Skin skinOut;
+ if(!getString(skinObj.value("id"), skinOut.id)) {
+ continue;
+ }
+ QString state;
+ if(!getString(skinObj.value("state"), state)) {
+ continue;
+ }
+ if(state != "ACTIVE") {
+ continue;
+ }
+ if(!getString(skinObj.value("url"), skinOut.url)) {
+ continue;
+ }
+ if(!getString(skinObj.value("variant"), skinOut.variant)) {
+ continue;
+ }
+ // we deal with only the active skin
+ output.skin = skinOut;
+ break;
+ }
+ auto capesArray = obj.value("capes").toArray();
+ int i = -1;
+ int currentCape = -1;
+ for(auto cape: capesArray) {
+ i++;
+ auto capeObj = cape.toObject();
+ Cape capeOut;
+ if(!getString(capeObj.value("id"), capeOut.id)) {
+ continue;
+ }
+ QString state;
+ if(!getString(capeObj.value("state"), state)) {
+ continue;
+ }
+ if(state == "ACTIVE") {
+ currentCape = i;
+ }
+ if(!getString(capeObj.value("url"), capeOut.url)) {
+ continue;
+ }
+ if(!getString(capeObj.value("alias"), capeOut.alias)) {
+ continue;
+ }
+
+ // we deal with only the active skin
+ output.capes.push_back(capeOut);
+ }
+ output.currentCape = currentCape;
+ output.validity = Katabasis::Validity::Certain;
+ return true;
+}
+}
+
+void Context::doMinecraftProfile() {
+ auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ // request.setRawHeader("Accept", "application/json");
+ request.setRawHeader("Authorization", QString("Bearer %1").arg(m_account.minecraftToken.token).toUtf8());
+
+ Requestor *requestor = new Requestor(mgr, oauth2, this);
+ requestor->setAddAccessTokenInQuery(false);
+
+ connect(requestor, &Requestor::finished, this, &Context::onMinecraftProfileDone);
+ requestor->get(request);
+}
+
+void Context::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
+ qDebug() << data;
+ if (error == QNetworkReply::ContentNotFoundError) {
+ m_account.minecraftProfile = MinecraftProfile();
+ finishActivity();
+ return;
+ }
+ if (error != QNetworkReply::NoError) {
+ finishActivity();
+ return;
+ }
+ if(!parseMinecraftProfile(data, m_account.minecraftProfile)) {
+ m_account.minecraftProfile = MinecraftProfile();
+ finishActivity();
+ return;
+ }
+ doGetSkin();
+}
+
+void Context::doGetSkin() {
+ auto url = QUrl(m_account.minecraftProfile.skin.url);
+ QNetworkRequest request = QNetworkRequest(url);
+ Requestor *requestor = new Requestor(mgr, oauth2, this);
+ requestor->setAddAccessTokenInQuery(false);
+ connect(requestor, &Requestor::finished, this, &Context::onSkinDone);
+ requestor->get(request);
+}
+
+void Context::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
+ if (error == QNetworkReply::NoError) {
+ m_account.minecraftProfile.skin.data = data;
+ }
+ finishActivity();
+}
+
+namespace {
+void tokenToJSON(QJsonObject &parent, Katabasis::Token t, const char * tokenName) {
+ if(t.validity == Katabasis::Validity::None || !t.persistent) {
+ return;
+ }
+ QJsonObject out;
+ if(t.issueInstant.isValid()) {
+ out["iat"] = QJsonValue(t.issueInstant.toSecsSinceEpoch());
+ }
+
+ if(t.notAfter.isValid()) {
+ out["exp"] = QJsonValue(t.notAfter.toSecsSinceEpoch());
+ }
+
+ if(!t.token.isEmpty()) {
+ out["token"] = QJsonValue(t.token);
+ }
+ if(!t.refresh_token.isEmpty()) {
+ out["refresh_token"] = QJsonValue(t.refresh_token);
+ }
+ if(t.extra.size()) {
+ out["extra"] = QJsonObject::fromVariantMap(t.extra);
+ }
+ if(out.size()) {
+ parent[tokenName] = out;
+ }
+}
+
+Katabasis::Token tokenFromJSON(const QJsonObject &parent, const char * tokenName) {
+ Katabasis::Token out;
+ auto tokenObject = parent.value(tokenName).toObject();
+ if(tokenObject.isEmpty()) {
+ return out;
+ }
+ auto issueInstant = tokenObject.value("iat");
+ if(issueInstant.isDouble()) {
+ out.issueInstant = QDateTime::fromSecsSinceEpoch((int64_t) issueInstant.toDouble());
+ }
+
+ auto notAfter = tokenObject.value("exp");
+ if(notAfter.isDouble()) {
+ out.notAfter = QDateTime::fromSecsSinceEpoch((int64_t) notAfter.toDouble());
+ }
+
+ auto token = tokenObject.value("token");
+ if(token.isString()) {
+ out.token = token.toString();
+ out.validity = Katabasis::Validity::Assumed;
+ }
+
+ auto refresh_token = tokenObject.value("refresh_token");
+ if(refresh_token.isString()) {
+ out.refresh_token = refresh_token.toString();
+ }
+
+ auto extra = tokenObject.value("extra");
+ if(extra.isObject()) {
+ out.extra = extra.toObject().toVariantMap();
+ }
+ return out;
+}
+
+void profileToJSON(QJsonObject &parent, MinecraftProfile p, const char * tokenName) {
+ if(p.id.isEmpty()) {
+ return;
+ }
+ QJsonObject out;
+ out["id"] = QJsonValue(p.id);
+ out["name"] = QJsonValue(p.name);
+ if(p.currentCape != -1) {
+ out["cape"] = p.capes[p.currentCape].id;
+ }
+
+ {
+ QJsonObject skinObj;
+ skinObj["id"] = p.skin.id;
+ skinObj["url"] = p.skin.url;
+ skinObj["variant"] = p.skin.variant;
+ if(p.skin.data.size()) {
+ skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64());
+ }
+ out["skin"] = skinObj;
+ }
+
+ QJsonArray capesArray;
+ for(auto & cape: p.capes) {
+ QJsonObject capeObj;
+ capeObj["id"] = cape.id;
+ capeObj["url"] = cape.url;
+ capeObj["alias"] = cape.alias;
+ if(cape.data.size()) {
+ capeObj["data"] = QString::fromLatin1(cape.data.toBase64());
+ }
+ capesArray.push_back(capeObj);
+ }
+ out["capes"] = capesArray;
+ parent[tokenName] = out;
+}
+
+MinecraftProfile profileFromJSON(const QJsonObject &parent, const char * tokenName) {
+ MinecraftProfile out;
+ auto tokenObject = parent.value(tokenName).toObject();
+ if(tokenObject.isEmpty()) {
+ return out;
+ }
+ {
+ auto idV = tokenObject.value("id");
+ auto nameV = tokenObject.value("name");
+ if(!idV.isString() || !nameV.isString()) {
+ qWarning() << "mandatory profile attributes are missing or of unexpected type";
+ return MinecraftProfile();
+ }
+ out.name = nameV.toString();
+ out.id = idV.toString();
+ }
+
+ {
+ auto skinV = tokenObject.value("skin");
+ if(!skinV.isObject()) {
+ qWarning() << "skin is missing";
+ return MinecraftProfile();
+ }
+ auto skinObj = skinV.toObject();
+ auto idV = skinObj.value("id");
+ auto urlV = skinObj.value("url");
+ auto variantV = skinObj.value("variant");
+ if(!idV.isString() || !urlV.isString() || !variantV.isString()) {
+ qWarning() << "mandatory skin attributes are missing or of unexpected type";
+ return MinecraftProfile();
+ }
+ out.skin.id = idV.toString();
+ out.skin.url = urlV.toString();
+ out.skin.variant = variantV.toString();
+
+ // data for skin is optional
+ auto dataV = skinObj.value("data");
+ if(dataV.isString()) {
+ // TODO: validate base64
+ out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1());
+ }
+ else if (!dataV.isUndefined()) {
+ qWarning() << "skin data is something unexpected";
+ return MinecraftProfile();
+ }
+ }
+
+ auto capesV = tokenObject.value("capes");
+ if(!capesV.isArray()) {
+ qWarning() << "capes is not an array!";
+ return MinecraftProfile();
+ }
+ auto capesArray = capesV.toArray();
+ for(auto capeV: capesArray) {
+ if(!capeV.isObject()) {
+ qWarning() << "cape is not an object!";
+ return MinecraftProfile();
+ }
+ auto capeObj = capeV.toObject();
+ auto idV = capeObj.value("id");
+ auto urlV = capeObj.value("url");
+ auto aliasV = capeObj.value("alias");
+ if(!idV.isString() || !urlV.isString() || !aliasV.isString()) {
+ qWarning() << "mandatory skin attributes are missing or of unexpected type";
+ return MinecraftProfile();
+ }
+ Cape cape;
+ cape.id = idV.toString();
+ cape.url = urlV.toString();
+ cape.alias = aliasV.toString();
+
+ // data for cape is optional.
+ auto dataV = capeObj.value("data");
+ if(dataV.isString()) {
+ // TODO: validate base64
+ cape.data = QByteArray::fromBase64(dataV.toString().toLatin1());
+ }
+ else if (!dataV.isUndefined()) {
+ qWarning() << "cape data is something unexpected";
+ return MinecraftProfile();
+ }
+ out.capes.push_back(cape);
+ }
+ out.validity = Katabasis::Validity::Assumed;
+ return out;
+}
+
+}
+
+bool Context::resumeFromState(QByteArray data) {
+ QJsonParseError error;
+ auto doc = QJsonDocument::fromJson(data, &error);
+ if(error.error != QJsonParseError::NoError) {
+ qWarning() << "Failed to parse account data as JSON.";
+ return false;
+ }
+ auto docObject = doc.object();
+ m_account.msaToken = tokenFromJSON(docObject, "msa");
+ m_account.userToken = tokenFromJSON(docObject, "utoken");
+ m_account.xboxApiToken = tokenFromJSON(docObject, "xrp-main");
+ m_account.mojangservicesToken = tokenFromJSON(docObject, "xrp-mc");
+ m_account.minecraftToken = tokenFromJSON(docObject, "ygg");
+
+ m_account.minecraftProfile = profileFromJSON(docObject, "profile");
+
+ m_account.validity_ = m_account.minecraftProfile.validity;
+
+ return true;
+}
+
+QByteArray Context::saveState() {
+ QJsonDocument doc;
+ QJsonObject output;
+ tokenToJSON(output, m_account.msaToken, "msa");
+ tokenToJSON(output, m_account.userToken, "utoken");
+ tokenToJSON(output, m_account.xboxApiToken, "xrp-main");
+ tokenToJSON(output, m_account.mojangservicesToken, "xrp-mc");
+ tokenToJSON(output, m_account.minecraftToken, "ygg");
+ profileToJSON(output, m_account.minecraftProfile, "profile");
+ doc.setObject(output);
+ return doc.toJson(QJsonDocument::Indented);
+}
diff --git a/launcher/minecraft/auth-msa/context.h b/launcher/minecraft/auth-msa/context.h
new file mode 100644
index 00000000..f1ac99b8
--- /dev/null
+++ b/launcher/minecraft/auth-msa/context.h
@@ -0,0 +1,128 @@
+#pragma once
+
+#include <QObject>
+#include <QList>
+#include <QVector>
+#include <QNetworkReply>
+#include <QImage>
+
+#include <katabasis/OAuth2.h>
+
+struct Skin {
+ QString id;
+ QString url;
+ QString variant;
+
+ QByteArray data;
+};
+
+struct Cape {
+ QString id;
+ QString url;
+ QString alias;
+
+ QByteArray data;
+};
+
+struct MinecraftProfile {
+ QString id;
+ QString name;
+ Skin skin;
+ int currentCape = -1;
+ QVector<Cape> capes;
+ Katabasis::Validity validity = Katabasis::Validity::None;
+};
+
+enum class AccountType {
+ MSA,
+ Mojang
+};
+
+struct AccountData {
+ AccountType type = AccountType::MSA;
+
+ Katabasis::Token msaToken;
+ Katabasis::Token userToken;
+ Katabasis::Token xboxApiToken;
+ Katabasis::Token mojangservicesToken;
+ Katabasis::Token minecraftToken;
+
+ MinecraftProfile minecraftProfile;
+ Katabasis::Validity validity_ = Katabasis::Validity::None;
+};
+
+class Context : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit Context(QObject *parent = 0);
+
+ QByteArray saveState();
+ bool resumeFromState(QByteArray data);
+
+ bool isBusy() {
+ return activity_ != Katabasis::Activity::Idle;
+ };
+ Katabasis::Validity validity() {
+ return m_account.validity_;
+ };
+
+ bool signIn();
+ bool silentSignIn();
+ bool signOut();
+
+ QString userName();
+ QString userId();
+ QString gameToken();
+signals:
+ void succeeded();
+ void failed();
+ void activityChanged(Katabasis::Activity activity);
+
+private slots:
+ void onLinkingSucceeded();
+ void onLinkingFailed();
+ void onOpenBrowser(const QUrl &url);
+ void onCloseBrowser();
+ void onOAuthActivityChanged(Katabasis::Activity activity);
+
+private:
+ void doUserAuth();
+ Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
+
+ void doSTSAuthMinecraft();
+ Q_SLOT void onSTSAuthMinecraftDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
+ void doMinecraftAuth();
+ Q_SLOT void onMinecraftAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
+
+ void doSTSAuthGeneric();
+ Q_SLOT void onSTSAuthGenericDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
+ void doXBoxProfile();
+ Q_SLOT void onXBoxProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
+
+ void doMinecraftProfile();
+ Q_SLOT void onMinecraftProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
+
+ void doGetSkin();
+ Q_SLOT void onSkinDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
+
+ void checkResult();
+
+private:
+ void beginActivity(Katabasis::Activity activity);
+ void finishActivity();
+ void clearTokens();
+
+private:
+ Katabasis::OAuth2 *oauth2 = nullptr;
+
+ int requestsDone = 0;
+ bool xboxProfileSucceeded = false;
+ bool mcAuthSucceeded = false;
+ Katabasis::Activity activity_ = Katabasis::Activity::Idle;
+
+ AccountData m_account;
+
+ QNetworkAccessManager *mgr = nullptr;
+};
diff --git a/launcher/minecraft/auth-msa/main.cpp b/launcher/minecraft/auth-msa/main.cpp
new file mode 100644
index 00000000..481e0126
--- /dev/null
+++ b/launcher/minecraft/auth-msa/main.cpp
@@ -0,0 +1,100 @@
+#include <QApplication>
+#include <QStringList>
+#include <QTimer>
+#include <QDebug>
+#include <QFile>
+#include <QSaveFile>
+
+#include "context.h"
+#include "mainwindow.h"
+
+void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+{
+ QByteArray localMsg = msg.toLocal8Bit();
+ const char *file = context.file ? context.file : "";
+ const char *function = context.function ? context.function : "";
+ switch (type) {
+ case QtDebugMsg:
+ fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+ break;
+ case QtInfoMsg:
+ fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+ break;
+ case QtWarningMsg:
+ fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+ break;
+ case QtCriticalMsg:
+ fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+ break;
+ case QtFatalMsg:
+ fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+ break;
+ }
+}
+
+class Helper : public QObject {
+ Q_OBJECT
+
+public:
+ Helper(Context * context) : QObject(), context_(context), msg_(QString()) {
+ QFile tokenCache("usercache.dat");
+ if(tokenCache.open(QIODevice::ReadOnly)) {
+ context_->resumeFromState(tokenCache.readAll());
+ }
+ }
+
+public slots:
+ void run() {
+ connect(context_, &Context::activityChanged, this, &Helper::onActivityChanged);
+ context_->silentSignIn();
+ }
+
+ void onFailed() {
+ qDebug() << "Login failed";
+ }
+
+ void onActivityChanged(Katabasis::Activity activity) {
+ if(activity == Katabasis::Activity::Idle) {
+ switch(context_->validity()) {
+ case Katabasis::Validity::None: {
+ // account is gone, remove it.
+ QFile::remove("usercache.dat");
+ }
+ break;
+ case Katabasis::Validity::Assumed: {
+ // this is basically a soft-failed refresh. do nothing.
+ }
+ break;
+ case Katabasis::Validity::Certain: {
+ // stuff got refreshed / signed in. Save.
+ auto data = context_->saveState();
+ QSaveFile tokenCache("usercache.dat");
+ if(tokenCache.open(QIODevice::WriteOnly)) {
+ tokenCache.write(context_->saveState());
+ tokenCache.commit();
+ }
+ }
+ break;
+ }
+ }
+ }
+
+private:
+ Context *context_;
+ QString msg_;
+};
+
+int main(int argc, char *argv[]) {
+ qInstallMessageHandler(myMessageOutput);
+ QApplication a(argc, argv);
+ QCoreApplication::setOrganizationName("MultiMC");
+ QCoreApplication::setApplicationName("MultiMC");
+ Context c;
+ Helper helper(&c);
+ MainWindow window(&c);
+ window.show();
+ QTimer::singleShot(0, &helper, &Helper::run);
+ return a.exec();
+}
+
+#include "main.moc"
diff --git a/launcher/minecraft/auth-msa/mainwindow.cpp b/launcher/minecraft/auth-msa/mainwindow.cpp
new file mode 100644
index 00000000..d4e18dc0
--- /dev/null
+++ b/launcher/minecraft/auth-msa/mainwindow.cpp
@@ -0,0 +1,97 @@
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+#include <QDebug>
+
+#include <QDesktopServices>
+
+#include "BuildConfig.h"
+
+MainWindow::MainWindow(Context * context, QWidget *parent) :
+ QMainWindow(parent),
+ m_context(context),
+ m_ui(new Ui::MainWindow)
+{
+ m_ui->setupUi(this);
+ connect(m_ui->signInButton_MSA, &QPushButton::clicked, this, &MainWindow::SignInMSAClicked);
+ connect(m_ui->signInButton_Mojang, &QPushButton::clicked, this, &MainWindow::SignInMojangClicked);
+ connect(m_ui->signOutButton, &QPushButton::clicked, this, &MainWindow::SignOutClicked);
+ connect(m_ui->refreshButton, &QPushButton::clicked, this, &MainWindow::RefreshClicked);
+
+ // connect(m_context, &Context::linkingSucceeded, this, &MainWindow::SignInSucceeded);
+ // connect(m_context, &Context::linkingFailed, this, &MainWindow::SignInFailed);
+ connect(m_context, &Context::activityChanged, this, &MainWindow::ActivityChanged);
+ ActivityChanged(Katabasis::Activity::Idle);
+}
+
+MainWindow::~MainWindow() = default;
+
+void MainWindow::ActivityChanged(Katabasis::Activity activity) {
+ switch(activity) {
+ case Katabasis::Activity::Idle: {
+ if(m_context->validity() != Katabasis::Validity::None) {
+ m_ui->signInButton_Mojang->setEnabled(false);
+ m_ui->signInButton_MSA->setEnabled(false);
+ m_ui->signOutButton->setEnabled(true);
+ m_ui->refreshButton->setEnabled(true);
+ m_ui->statusBar->showMessage(QString("Hello %1!").arg(m_context->userName()));
+ }
+ else {
+ m_ui->signInButton_Mojang->setEnabled(true);
+ m_ui->signInButton_MSA->setEnabled(true);
+ m_ui->signOutButton->setEnabled(false);
+ m_ui->refreshButton->setEnabled(false);
+ m_ui->statusBar->showMessage("Press the login button to start.");
+ }
+ }
+ break;
+ case Katabasis::Activity::LoggingIn: {
+ m_ui->signInButton_Mojang->setEnabled(false);
+ m_ui->signInButton_MSA->setEnabled(false);
+ m_ui->signOutButton->setEnabled(false);
+ m_ui->refreshButton->setEnabled(false);
+ m_ui->statusBar->showMessage("Logging in...");
+ }
+ break;
+ case Katabasis::Activity::LoggingOut: {
+ m_ui->signInButton_Mojang->setEnabled(false);
+ m_ui->signInButton_MSA->setEnabled(false);
+ m_ui->signOutButton->setEnabled(false);
+ m_ui->refreshButton->setEnabled(false);
+ m_ui->statusBar->showMessage("Logging out...");
+ }
+ break;
+ case Katabasis::Activity::Refreshing: {
+ m_ui->signInButton_Mojang->setEnabled(false);
+ m_ui->signInButton_MSA->setEnabled(false);
+ m_ui->signOutButton->setEnabled(false);
+ m_ui->refreshButton->setEnabled(false);
+ m_ui->statusBar->showMessage("Refreshing login...");
+ }
+ break;
+ }
+}
+
+void MainWindow::SignInMSAClicked() {
+ qDebug() << "Sign In MSA";
+ // signIn({{"prompt", "select_account"}})
+ // FIXME: wrong. very wrong. this should not be operating on the current context
+ m_context->signIn();
+}
+
+void MainWindow::SignInMojangClicked() {
+ qDebug() << "Sign In Mojang";
+ // signIn({{"prompt", "select_account"}})
+ // FIXME: wrong. very wrong. this should not be operating on the current context
+ m_context->signIn();
+}
+
+
+void MainWindow::SignOutClicked() {
+ qDebug() << "Sign Out";
+ m_context->signOut();
+}
+
+void MainWindow::RefreshClicked() {
+ qDebug() << "Refresh";
+ m_context->silentSignIn();
+}
diff --git a/launcher/minecraft/auth-msa/mainwindow.h b/launcher/minecraft/auth-msa/mainwindow.h
new file mode 100644
index 00000000..abde52d8
--- /dev/null
+++ b/launcher/minecraft/auth-msa/mainwindow.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <QMainWindow>
+#include <QScopedPointer>
+#include <QtNetwork>
+#include <katabasis/Bits.h>
+
+#include "context.h"
+
+namespace Ui {
+class MainWindow;
+}
+
+class MainWindow : public QMainWindow {
+ Q_OBJECT
+
+public:
+ explicit MainWindow(Context * context, QWidget *parent = nullptr);
+ ~MainWindow() override;
+
+private slots:
+ void SignInMojangClicked();
+ void SignInMSAClicked();
+
+ void SignOutClicked();
+ void RefreshClicked();
+
+ void ActivityChanged(Katabasis::Activity activity);
+
+private:
+ Context* m_context;
+ QScopedPointer<Ui::MainWindow> m_ui;
+};
+
diff --git a/launcher/minecraft/auth-msa/mainwindow.ui b/launcher/minecraft/auth-msa/mainwindow.ui
new file mode 100644
index 00000000..32b34128
--- /dev/null
+++ b/launcher/minecraft/auth-msa/mainwindow.ui
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1037</width>
+ <height>511</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>SmartMapsClient</string>
+ </property>
+ <property name="dockNestingEnabled">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="centralWidget">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="3">
+ <widget class="QPushButton" name="signInButton_Mojang">
+ <property name="text">
+ <string>SignIn Mojang</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="3">
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" rowspan="7" colspan="3">
+ <widget class="QTreeView" name="accountView"/>
+ </item>
+ <item row="5" column="3">
+ <widget class="QPushButton" name="refreshButton">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <widget class="QPushButton" name="signInButton_MSA">
+ <property name="text">
+ <string>SignIn MSA</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="3">
+ <widget class="QPushButton" name="signOutButton">
+ <property name="text">
+ <string>SignOut</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="3">
+ <widget class="QPushButton" name="makeActiveButton">
+ <property name="text">
+ <string>Make Active</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QStatusBar" name="statusBar"/>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/minecraft/auth/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp
new file mode 100644
index 00000000..4e858796
--- /dev/null
+++ b/launcher/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/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h
new file mode 100644
index 00000000..29958597
--- /dev/null
+++ b/launcher/minecraft/auth/AuthSession.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <QString>
+#include <QMultiMap>
+#include <memory>
+
+class MojangAccount;
+
+struct User
+{
+ QString id;
+ QMultiMap<QString, QString> properties;
+};
+
+struct 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;
+ std::shared_ptr<MojangAccount> m_accountPtr;
+};
+
+typedef std::shared_ptr<AuthSession> AuthSessionPtr;
diff --git a/launcher/minecraft/auth/MojangAccount.cpp b/launcher/minecraft/auth/MojangAccount.cpp
new file mode 100644
index 00000000..f5853fe3
--- /dev/null
+++ b/launcher/minecraft/auth/MojangAccount.cpp
@@ -0,0 +1,315 @@
+/* Copyright 2013-2021 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(accountStatus() == Verified && !session->wants_online)
+ {
+ session->status = AuthSession::PlayableOffline;
+ session->auth_server_online = false;
+ fillSession(session);
+ return nullptr;
+ }
+ else
+ {
+ 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();
+ session->m_accountPtr = shared_from_this();
+}
+
+void MojangAccount::decrementUses()
+{
+ Usable::decrementUses();
+ if(!isInUse())
+ {
+ emit changed();
+ qWarning() << "Account" << m_username << "is no longer in use.";
+ }
+}
+
+void MojangAccount::incrementUses()
+{
+ bool wasInUse = isInUse();
+ Usable::incrementUses();
+ if(!wasInUse)
+ {
+ emit changed();
+ qWarning() << "Account" << m_username << "is now in use.";
+ }
+}
+
+void MojangAccount::invalidateClientToken()
+{
+ m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
+ emit changed();
+}
diff --git a/launcher/minecraft/auth/MojangAccount.h b/launcher/minecraft/auth/MojangAccount.h
new file mode 100644
index 00000000..3f6cbedd
--- /dev/null
+++ b/launcher/minecraft/auth/MojangAccount.h
@@ -0,0 +1,180 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QList>
+#include <QJsonObject>
+#include <QPair>
+#include <QMap>
+
+#include <memory>
+#include "AuthSession.h"
+#include "Usable.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 MojangAccount :
+ public QObject,
+ public Usable,
+ public std::enable_shared_from_this<MojangAccount>
+{
+ 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());
+ void invalidateClientToken();
+
+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;
+
+protected: /* methods */
+
+ void incrementUses() override;
+ void decrementUses() override;
+
+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/launcher/minecraft/auth/MojangAccountList.cpp b/launcher/minecraft/auth/MojangAccountList.cpp
new file mode 100644
index 00000000..e584cb3b
--- /dev/null
+++ b/launcher/minecraft/auth/MojangAccountList.cpp
@@ -0,0 +1,468 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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)
+{
+ int row = m_accounts.count();
+ beginInsertRows(QModelIndex(), row, row);
+ connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
+ m_accounts.append(account);
+ endInsertRows();
+ onListChanged();
+}
+
+void MojangAccountList::removeAccount(const QString &username)
+{
+ int idx = 0;
+ for (auto account : m_accounts)
+ {
+ if (account->username() == username)
+ {
+ beginRemoveRows(QModelIndex(), idx, idx);
+ m_accounts.removeOne(account);
+ endRemoveRows();
+ return;
+ }
+ idx++;
+ }
+ onListChanged();
+}
+
+void MojangAccountList::removeAccount(QModelIndex index)
+{
+ int row = index.row();
+ if(index.isValid() && row >= 0 && row < m_accounts.size())
+ {
+ auto & account = m_accounts[row];
+ if(account == m_activeAccount)
+ {
+ m_activeAccount = nullptr;
+ onActiveChanged();
+ }
+ beginRemoveRows(QModelIndex(), row, row);
+ m_accounts.removeAt(index.row());
+ endRemoveRows();
+ onListChanged();
+ }
+}
+
+MojangAccountPtr MojangAccountList::activeAccount() const
+{
+ return m_activeAccount;
+}
+
+void MojangAccountList::setActiveAccount(const QString &username)
+{
+ if (username.isEmpty() && m_activeAccount)
+ {
+ int idx = 0;
+ auto prevActiveAcc = m_activeAccount;
+ m_activeAccount = nullptr;
+ for (MojangAccountPtr account : m_accounts)
+ {
+ if (account == prevActiveAcc)
+ {
+ emit dataChanged(index(idx), index(idx));
+ }
+ idx ++;
+ }
+ onActiveChanged();
+ }
+ else
+ {
+ auto currentActiveAccount = m_activeAccount;
+ int currentActiveAccountIdx = -1;
+ auto newActiveAccount = m_activeAccount;
+ int newActiveAccountIdx = -1;
+ int idx = 0;
+ for (MojangAccountPtr account : m_accounts)
+ {
+ if (account->username() == username)
+ {
+ newActiveAccount = account;
+ newActiveAccountIdx = idx;
+ }
+ if(currentActiveAccount == account)
+ {
+ currentActiveAccountIdx = idx;
+ }
+ idx++;
+ }
+ if(currentActiveAccount != newActiveAccount)
+ {
+ emit dataChanged(index(currentActiveAccountIdx), index(currentActiveAccountIdx));
+ emit dataChanged(index(newActiveAccountIdx), index(newActiveAccountIdx));
+ m_activeAccount = newActiveAccount;
+ 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 ? Qt::Checked : Qt::Unchecked;
+ }
+
+ 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 &) const
+{
+ // Return count
+ return count();
+}
+
+int MojangAccountList::columnCount(const QModelIndex &) 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/launcher/minecraft/auth/MojangAccountList.h b/launcher/minecraft/auth/MojangAccountList.h
new file mode 100644
index 00000000..99d2988e
--- /dev/null
+++ b/launcher/minecraft/auth/MojangAccountList.h
@@ -0,0 +1,199 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "MojangAccount.h"
+
+#include <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+#include <QSharedPointer>
+
+/*!
+ * \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 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/launcher/minecraft/auth/YggdrasilTask.cpp b/launcher/minecraft/auth/YggdrasilTask.cpp
new file mode 100644
index 00000000..0857b46b
--- /dev/null
+++ b/launcher/minecraft/auth/YggdrasilTask.cpp
@@ -0,0 +1,255 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "YggdrasilTask.h"
+#include "MojangAccount.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QByteArray>
+
+#include <Env.h>
+
+#include <BuildConfig.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());
+
+ QUrl reqUrl(BuildConfig.AUTH_BASE + getEndpoint());
+ QNetworkRequest netRequest(reqUrl);
+ netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+ QByteArray requestData = doc.toJson();
+ m_netReply = ENV.qnam().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=\"https://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
+ "your root certificates</a></li>"
+ "<li>Some device on your network is interfering with SSL traffic. In that case, "
+ "you have bigger worries than Minecraft not starting.</li>"
+ "<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::ContentAccessDenied:
+ 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/launcher/minecraft/auth/YggdrasilTask.h b/launcher/minecraft/auth/YggdrasilTask.h
new file mode 100644
index 00000000..8af2e132
--- /dev/null
+++ b/launcher/minecraft/auth/YggdrasilTask.h
@@ -0,0 +1,151 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#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);
+ virtual ~YggdrasilTask() {};
+
+ /**
+ * 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/launcher/minecraft/auth/flows/AuthenticateTask.cpp b/launcher/minecraft/auth/flows/AuthenticateTask.cpp
new file mode 100644
index 00000000..2e8dc859
--- /dev/null
+++ b/launcher/minecraft/auth/flows/AuthenticateTask.cpp
@@ -0,0 +1,202 @@
+
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/minecraft/auth/flows/AuthenticateTask.h b/launcher/minecraft/auth/flows/AuthenticateTask.h
new file mode 100644
index 00000000..4c14eec7
--- /dev/null
+++ b/launcher/minecraft/auth/flows/AuthenticateTask.h
@@ -0,0 +1,46 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "../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/launcher/minecraft/auth/flows/RefreshTask.cpp b/launcher/minecraft/auth/flows/RefreshTask.cpp
new file mode 100644
index 00000000..ecba178d
--- /dev/null
+++ b/launcher/minecraft/auth/flows/RefreshTask.cpp
@@ -0,0 +1,144 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/minecraft/auth/flows/RefreshTask.h b/launcher/minecraft/auth/flows/RefreshTask.h
new file mode 100644
index 00000000..f0840dda
--- /dev/null
+++ b/launcher/minecraft/auth/flows/RefreshTask.h
@@ -0,0 +1,44 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "../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/launcher/minecraft/auth/flows/ValidateTask.cpp b/launcher/minecraft/auth/flows/ValidateTask.cpp
new file mode 100644
index 00000000..6b3f0a65
--- /dev/null
+++ b/launcher/minecraft/auth/flows/ValidateTask.cpp
@@ -0,0 +1,61 @@
+
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/minecraft/auth/flows/ValidateTask.h b/launcher/minecraft/auth/flows/ValidateTask.h
new file mode 100644
index 00000000..986c2e9f
--- /dev/null
+++ b/launcher/minecraft/auth/flows/ValidateTask.h
@@ -0,0 +1,47 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * :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/launcher/minecraft/gameoptions/GameOptions.cpp b/launcher/minecraft/gameoptions/GameOptions.cpp
new file mode 100644
index 00000000..e547b32a
--- /dev/null
+++ b/launcher/minecraft/gameoptions/GameOptions.cpp
@@ -0,0 +1,144 @@
+#include "GameOptions.h"
+#include "FileSystem.h"
+#include <QDebug>
+#include <QSaveFile>
+
+namespace {
+bool load(const QString& path, std::vector<GameOptionItem> &contents, int & version)
+{
+ contents.clear();
+ QFile file(path);
+ if (!file.open(QFile::ReadOnly))
+ {
+ qWarning() << "Failed to read options file.";
+ return false;
+ }
+ version = 0;
+ while(!file.atEnd())
+ {
+ auto line = file.readLine();
+ if(line.endsWith('\n'))
+ {
+ line.chop(1);
+ }
+ auto separatorIndex = line.indexOf(':');
+ if(separatorIndex == -1)
+ {
+ continue;
+ }
+ auto key = QString::fromUtf8(line.data(), separatorIndex);
+ auto value = QString::fromUtf8(line.data() + separatorIndex + 1, line.size() - 1 - separatorIndex);
+ qDebug() << "!!" << key << "!!";
+ if(key == "version")
+ {
+ version = value.toInt();
+ continue;
+ }
+ contents.emplace_back(GameOptionItem{key, value});
+ }
+ qDebug() << "Loaded" << path << "with version:" << version;
+ return true;
+}
+bool save(const QString& path, std::vector<GameOptionItem> &mapping, int version)
+{
+ QSaveFile out(path);
+ if(!out.open(QIODevice::WriteOnly))
+ {
+ return false;
+ }
+ if(version != 0)
+ {
+ QString versionLine = QString("version:%1\n").arg(version);
+ out.write(versionLine.toUtf8());
+ }
+ auto iter = mapping.begin();
+ while (iter != mapping.end())
+ {
+ out.write(iter->key.toUtf8());
+ out.write(":");
+ out.write(iter->value.toUtf8());
+ out.write("\n");
+ iter++;
+ }
+ return out.commit();
+}
+}
+
+GameOptions::GameOptions(const QString& path):
+ path(path)
+{
+ reload();
+}
+
+QVariant GameOptions::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if(role != Qt::DisplayRole)
+ {
+ return QAbstractListModel::headerData(section, orientation, role);
+ }
+ switch(section)
+ {
+ case 0:
+ return tr("Key");
+ case 1:
+ return tr("Value");
+ default:
+ return QVariant();
+ }
+}
+
+QVariant GameOptions::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+
+ if (row < 0 || row >= int(contents.size()))
+ return QVariant();
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ if(column == 0)
+ {
+ return contents[row].key;
+ }
+ else
+ {
+ return contents[row].value;
+ }
+ default:
+ return QVariant();
+ }
+ return QVariant();
+}
+
+int GameOptions::rowCount(const QModelIndex&) const
+{
+ return contents.size();
+}
+
+int GameOptions::columnCount(const QModelIndex&) const
+{
+ return 2;
+}
+
+bool GameOptions::isLoaded() const
+{
+ return loaded;
+}
+
+bool GameOptions::reload()
+{
+ beginResetModel();
+ loaded = load(path, contents, version);
+ endResetModel();
+ return loaded;
+}
+
+bool GameOptions::save()
+{
+ return ::save(path, contents, version);
+}
diff --git a/launcher/minecraft/gameoptions/GameOptions.h b/launcher/minecraft/gameoptions/GameOptions.h
new file mode 100644
index 00000000..c6d25492
--- /dev/null
+++ b/launcher/minecraft/gameoptions/GameOptions.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <map>
+#include <QString>
+#include <QAbstractListModel>
+
+struct GameOptionItem
+{
+ QString key;
+ QString value;
+};
+
+class GameOptions : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ explicit GameOptions(const QString& path);
+ virtual ~GameOptions() = default;
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex & parent) const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ bool isLoaded() const;
+ bool reload();
+ bool save();
+
+private:
+ std::vector<GameOptionItem> contents;
+ bool loaded = false;
+ QString path;
+ int version = 0;
+};
diff --git a/launcher/minecraft/launch/ClaimAccount.cpp b/launcher/minecraft/launch/ClaimAccount.cpp
new file mode 100644
index 00000000..a1180f0a
--- /dev/null
+++ b/launcher/minecraft/launch/ClaimAccount.cpp
@@ -0,0 +1,24 @@
+#include "ClaimAccount.h"
+#include <launch/LaunchTask.h>
+
+ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session): LaunchStep(parent)
+{
+ if(session->status == AuthSession::Status::PlayableOnline)
+ {
+ m_account = session->m_accountPtr;
+ }
+}
+
+void ClaimAccount::executeTask()
+{
+ if(m_account)
+ {
+ lock.reset(new UseLock(m_account));
+ emitSucceeded();
+ }
+}
+
+void ClaimAccount::finalize()
+{
+ lock.reset();
+}
diff --git a/launcher/minecraft/launch/ClaimAccount.h b/launcher/minecraft/launch/ClaimAccount.h
new file mode 100644
index 00000000..c5bd75f3
--- /dev/null
+++ b/launcher/minecraft/launch/ClaimAccount.h
@@ -0,0 +1,37 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <minecraft/auth/MojangAccount.h>
+
+class ClaimAccount: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit ClaimAccount(LaunchTask *parent, AuthSessionPtr session);
+ virtual ~ClaimAccount() {};
+
+ void executeTask() override;
+ void finalize() override;
+ bool canAbort() const override
+ {
+ return false;
+ }
+private:
+ std::unique_ptr<UseLock> lock;
+ MojangAccountPtr m_account;
+};
diff --git a/launcher/minecraft/launch/CreateGameFolders.cpp b/launcher/minecraft/launch/CreateGameFolders.cpp
new file mode 100644
index 00000000..4081e72e
--- /dev/null
+++ b/launcher/minecraft/launch/CreateGameFolders.cpp
@@ -0,0 +1,28 @@
+#include "CreateGameFolders.h"
+#include "minecraft/MinecraftInstance.h"
+#include "launch/LaunchTask.h"
+#include "FileSystem.h"
+
+CreateGameFolders::CreateGameFolders(LaunchTask* parent): LaunchStep(parent)
+{
+}
+
+void CreateGameFolders::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance);
+
+ if(!FS::ensureFolderPathExists(minecraftInstance->gameRoot()))
+ {
+ emit logLine("Couldn't create the main game folder", MessageLevel::Error);
+ emitFailed(tr("Couldn't create the main game folder"));
+ return;
+ }
+
+ // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created.
+ if(!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs")))
+ {
+ emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error);
+ }
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/launch/CreateGameFolders.h b/launcher/minecraft/launch/CreateGameFolders.h
new file mode 100644
index 00000000..9c7d3c94
--- /dev/null
+++ b/launcher/minecraft/launch/CreateGameFolders.h
@@ -0,0 +1,37 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <LoggedProcess.h>
+#include <minecraft/auth/AuthSession.h>
+
+// Create the main .minecraft for the instance and any other necessary folders
+class CreateGameFolders: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit CreateGameFolders(LaunchTask *parent);
+ virtual ~CreateGameFolders() {};
+
+ virtual void executeTask();
+ virtual bool canAbort() const
+ {
+ return false;
+ }
+};
+
+
diff --git a/launcher/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp
new file mode 100644
index 00000000..2110384f
--- /dev/null
+++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp
@@ -0,0 +1,148 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "DirectJavaLaunch.h"
+#include <launch/LaunchTask.h>
+#include <minecraft/MinecraftInstance.h>
+#include <FileSystem.h>
+#include <Commandline.h>
+#include <QStandardPaths>
+
+DirectJavaLaunch::DirectJavaLaunch(LaunchTask *parent) : LaunchStep(parent)
+{
+ connect(&m_process, &LoggedProcess::log, this, &DirectJavaLaunch::logLines);
+ connect(&m_process, &LoggedProcess::stateChanged, this, &DirectJavaLaunch::on_state);
+}
+
+void DirectJavaLaunch::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance);
+ QStringList args = minecraftInstance->javaArguments();
+
+ args.append("-Djava.library.path=" + minecraftInstance->getNativePath());
+
+ auto classPathEntries = minecraftInstance->getClassPath();
+ args.append("-cp");
+ QString classpath;
+#ifdef Q_OS_WIN32
+ classpath = classPathEntries.join(';');
+#else
+ classpath = classPathEntries.join(':');
+#endif
+ args.append(classpath);
+ args.append(minecraftInstance->getMainClass());
+
+ 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());
+
+ // make detachable - this will keep the process running even if the object is destroyed
+ m_process.setDetachable(true);
+
+ auto mcArgs = minecraftInstance->processMinecraftArgs(m_session, m_serverToJoin);
+ args.append(mcArgs);
+
+ QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
+ if(!wrapperCommandStr.isEmpty())
+ {
+ auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
+ auto wrapperCommand = wrapperArgs.takeFirst();
+ auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);
+ if (realWrapperCommand.isEmpty())
+ {
+ const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found.");
+ emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal);
+ emitFailed(tr(reason).arg(wrapperCommand));
+ return;
+ }
+ emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::MultiMC);
+ args.prepend(javaPath);
+ m_process.start(wrapperCommand, wrapperArgs + args);
+ }
+ else
+ {
+ m_process.start(javaPath, args);
+ }
+}
+
+void DirectJavaLaunch::on_state(LoggedProcess::State state)
+{
+ switch(state)
+ {
+ case LoggedProcess::FailedToStart:
+ {
+ //: Error message displayed if instance can't start
+ const char *reason = QT_TR_NOOP("Could not launch minecraft!");
+ emit logLine(reason, MessageLevel::Fatal);
+ emitFailed(tr(reason));
+ return;
+ }
+ case LoggedProcess::Aborted:
+ case LoggedProcess::Crashed:
+ {
+ m_parent->setPid(-1);
+ emitFailed(tr("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(tr("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(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC);
+ m_parent->setPid(m_process.processId());
+ m_parent->instance()->setLastLaunch();
+ break;
+ default:
+ break;
+ }
+}
+
+void DirectJavaLaunch::setWorkingDirectory(const QString &wd)
+{
+ m_process.setWorkingDirectory(wd);
+}
+
+void DirectJavaLaunch::proceed()
+{
+ // nil
+}
+
+bool DirectJavaLaunch::abort()
+{
+ auto state = m_process.state();
+ if (state == LoggedProcess::Running || state == LoggedProcess::Starting)
+ {
+ m_process.kill();
+ }
+ return true;
+}
+
diff --git a/launcher/minecraft/launch/DirectJavaLaunch.h b/launcher/minecraft/launch/DirectJavaLaunch.h
new file mode 100644
index 00000000..58b119b8
--- /dev/null
+++ b/launcher/minecraft/launch/DirectJavaLaunch.h
@@ -0,0 +1,58 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <LoggedProcess.h>
+#include <minecraft/auth/AuthSession.h>
+
+#include "MinecraftServerTarget.h"
+
+class DirectJavaLaunch: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit DirectJavaLaunch(LaunchTask *parent);
+ virtual ~DirectJavaLaunch() {};
+
+ 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;
+ }
+
+ void setServerToJoin(MinecraftServerTargetPtr serverToJoin)
+ {
+ m_serverToJoin = std::move(serverToJoin);
+ }
+
+private slots:
+ void on_state(LoggedProcess::State state);
+
+private:
+ LoggedProcess m_process;
+ QString m_command;
+ AuthSessionPtr m_session;
+ MinecraftServerTargetPtr m_serverToJoin;
+};
+
diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp
new file mode 100644
index 00000000..d57499aa
--- /dev/null
+++ b/launcher/minecraft/launch/ExtractNatives.cpp
@@ -0,0 +1,111 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ExtractNatives.h"
+#include <minecraft/MinecraftInstance.h>
+#include <launch/LaunchTask.h>
+
+#include <quazip.h>
+#include <quazipdir.h>
+#include "MMCZip.h"
+#include "FileSystem.h"
+#include <QDir>
+
+static QString replaceSuffix (QString target, const QString &suffix, const QString &replacement)
+{
+ if (!target.endsWith(suffix))
+ {
+ return target;
+ }
+ target.resize(target.length() - suffix.length());
+ return target + replacement;
+}
+
+static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack, bool nativeOpenAL, bool nativeGLFW)
+{
+ QuaZip zip(source);
+ if(!zip.open(QuaZip::mdUnzip))
+ {
+ return false;
+ }
+ QDir directory(targetFolder);
+ if (!zip.goToFirstFile())
+ {
+ return false;
+ }
+ do
+ {
+ QString name = zip.getCurrentFileName();
+ auto lowercase = name.toLower();
+ if (nativeGLFW && name.contains("glfw")) {
+ continue;
+ }
+ if (nativeOpenAL && name.contains("openal")) {
+ continue;
+ }
+ if(applyJnilibHack)
+ {
+ name = replaceSuffix(name, ".jnilib", ".dylib");
+ }
+ QString absFilePath = directory.absoluteFilePath(name);
+ if (!JlCompress::extractFile(&zip, "", absFilePath))
+ {
+ return false;
+ }
+ } while (zip.goToNextFile());
+ zip.close();
+ if(zip.getZipError()!=0)
+ {
+ return false;
+ }
+ return true;
+}
+
+void ExtractNatives::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance);
+ auto toExtract = minecraftInstance->getNativeJars();
+ if(toExtract.isEmpty())
+ {
+ emitSucceeded();
+ return;
+ }
+ auto settings = minecraftInstance->settings();
+ bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool();
+ bool nativeGLFW = settings->get("UseNativeGLFW").toBool();
+
+ auto outputPath = minecraftInstance->getNativePath();
+ auto javaVersion = minecraftInstance->getJavaVersion();
+ bool jniHackEnabled = javaVersion.major() >= 8;
+ for(const auto &source: toExtract)
+ {
+ if(!unzipNatives(source, outputPath, jniHackEnabled, nativeOpenAL, nativeGLFW))
+ {
+ const char *reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'");
+ emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal);
+ emitFailed(tr(reason).arg(source, outputPath));
+ }
+ }
+ emitSucceeded();
+}
+
+void ExtractNatives::finalize()
+{
+ auto instance = m_parent->instance();
+ QString target_dir = FS::PathCombine(instance->instanceRoot(), "natives/");
+ QDir dir(target_dir);
+ dir.removeRecursively();
+}
diff --git a/launcher/minecraft/launch/ExtractNatives.h b/launcher/minecraft/launch/ExtractNatives.h
new file mode 100644
index 00000000..094fcd6b
--- /dev/null
+++ b/launcher/minecraft/launch/ExtractNatives.h
@@ -0,0 +1,38 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <memory>
+#include "minecraft/auth/AuthSession.h"
+
+// FIXME: temporary wrapper for existing task.
+class ExtractNatives: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit ExtractNatives(LaunchTask *parent) : LaunchStep(parent){};
+ virtual ~ExtractNatives(){};
+
+ void executeTask() override;
+ bool canAbort() const override
+ {
+ return false;
+ }
+ void finalize() override;
+};
+
+
diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp
new file mode 100644
index 00000000..ee469770
--- /dev/null
+++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp
@@ -0,0 +1,218 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "LauncherPartLaunch.h"
+#include <QCoreApplication>
+#include <launch/LaunchTask.h>
+#include <minecraft/MinecraftInstance.h>
+#include <FileSystem.h>
+#include <Commandline.h>
+#include <QStandardPaths>
+#include "Env.h"
+
+LauncherPartLaunch::LauncherPartLaunch(LaunchTask *parent) : LaunchStep(parent)
+{
+ connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines);
+ connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state);
+}
+
+#ifdef Q_OS_WIN
+// returns 8.3 file format from long path
+#include <windows.h>
+QString shortPathName(const QString & file)
+{
+ auto input = file.toStdWString();
+ std::wstring output;
+ long length = GetShortPathNameW(input.c_str(), NULL, 0);
+ // NOTE: this resizing might seem weird...
+ // when GetShortPathNameW fails, it returns length including null character
+ // when it succeeds, it returns length excluding null character
+ // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx
+ output.resize(length);
+ GetShortPathNameW(input.c_str(),(LPWSTR)output.c_str(),length);
+ output.resize(length-1);
+ QString ret = QString::fromStdWString(output);
+ return ret;
+}
+#endif
+
+// if the string survives roundtrip through local 8bit encoding...
+bool fitsInLocal8bit(const QString & string)
+{
+ return string == QString::fromLocal8Bit(string.toLocal8Bit());
+}
+
+void LauncherPartLaunch::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance);
+
+ m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin);
+ QStringList args = minecraftInstance->javaArguments();
+ 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());
+
+ // make detachable - this will keep the process running even if the object is destroyed
+ m_process.setDetachable(true);
+
+ auto classPath = minecraftInstance->getClassPath();
+ classPath.prepend(FS::PathCombine(ENV.getJarsPath(), "NewLaunch.jar"));
+
+ auto natPath = minecraftInstance->getNativePath();
+#ifdef Q_OS_WIN
+ if (!fitsInLocal8bit(natPath))
+ {
+ args << "-Djava.library.path=" + shortPathName(natPath);
+ }
+ else
+ {
+ args << "-Djava.library.path=" + natPath;
+ }
+#else
+ args << "-Djava.library.path=" + natPath;
+#endif
+
+ args << "-cp";
+#ifdef Q_OS_WIN
+ QStringList processed;
+ for(auto & item: classPath)
+ {
+ if (!fitsInLocal8bit(item))
+ {
+ processed << shortPathName(item);
+ }
+ else
+ {
+ processed << item;
+ }
+ }
+ args << processed.join(';');
+#else
+ args << classPath.join(':');
+#endif
+ args << "org.multimc.EntryPoint";
+
+ qDebug() << args.join(' ');
+
+ QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
+ if(!wrapperCommandStr.isEmpty())
+ {
+ auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
+ auto wrapperCommand = wrapperArgs.takeFirst();
+ auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);
+ if (realWrapperCommand.isEmpty())
+ {
+ const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found.");
+ emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal);
+ emitFailed(tr(reason).arg(wrapperCommand));
+ return;
+ }
+ emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::MultiMC);
+ args.prepend(javaPath);
+ m_process.start(wrapperCommand, wrapperArgs + args);
+ }
+ else
+ {
+ m_process.start(javaPath, args);
+ }
+}
+
+void LauncherPartLaunch::on_state(LoggedProcess::State state)
+{
+ switch(state)
+ {
+ case LoggedProcess::FailedToStart:
+ {
+ //: Error message displayed if instace can't start
+ const char *reason = QT_TR_NOOP("Could not launch minecraft!");
+ emit logLine(reason, MessageLevel::Fatal);
+ emitFailed(tr(reason));
+ return;
+ }
+ case LoggedProcess::Aborted:
+ case LoggedProcess::Crashed:
+ {
+ m_parent->setPid(-1);
+ emitFailed(tr("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(tr("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(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC);
+ m_parent->setPid(m_process.processId());
+ m_parent->instance()->setLastLaunch();
+ // send the launch script to the launcher part
+ m_process.write(m_launchScript.toUtf8());
+
+ mayProceed = true;
+ emit readyForLaunch();
+ break;
+ default:
+ break;
+ }
+}
+
+void LauncherPartLaunch::setWorkingDirectory(const QString &wd)
+{
+ m_process.setWorkingDirectory(wd);
+}
+
+void LauncherPartLaunch::proceed()
+{
+ if(mayProceed)
+ {
+ QString launchString("launch\n");
+ m_process.write(launchString.toUtf8());
+ mayProceed = false;
+ }
+}
+
+bool LauncherPartLaunch::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/launcher/minecraft/launch/LauncherPartLaunch.h b/launcher/minecraft/launch/LauncherPartLaunch.h
new file mode 100644
index 00000000..6a7ee0e5
--- /dev/null
+++ b/launcher/minecraft/launch/LauncherPartLaunch.h
@@ -0,0 +1,60 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <LoggedProcess.h>
+#include <minecraft/auth/AuthSession.h>
+
+#include "MinecraftServerTarget.h"
+
+class LauncherPartLaunch: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit LauncherPartLaunch(LaunchTask *parent);
+ virtual ~LauncherPartLaunch() {};
+
+ 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;
+ }
+
+ void setServerToJoin(MinecraftServerTargetPtr serverToJoin)
+ {
+ m_serverToJoin = std::move(serverToJoin);
+ }
+
+private slots:
+ void on_state(LoggedProcess::State state);
+
+private:
+ LoggedProcess m_process;
+ QString m_command;
+ AuthSessionPtr m_session;
+ QString m_launchScript;
+ MinecraftServerTargetPtr m_serverToJoin;
+
+ bool mayProceed = false;
+};
diff --git a/launcher/minecraft/launch/MinecraftServerTarget.cpp b/launcher/minecraft/launch/MinecraftServerTarget.cpp
new file mode 100644
index 00000000..569273b6
--- /dev/null
+++ b/launcher/minecraft/launch/MinecraftServerTarget.cpp
@@ -0,0 +1,66 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MinecraftServerTarget.h"
+
+#include <QStringList>
+
+MinecraftServerTarget MinecraftServerTarget::parse(const QString &fullAddress) {
+ QStringList split = fullAddress.split(":");
+
+ // The logic below replicates the exact logic minecraft uses for parsing server addresses.
+ // While the conversion is not lossless and eats errors, it ensures the same behavior
+ // within Minecraft and MultiMC when entering server addresses.
+ if (fullAddress.startsWith("["))
+ {
+ int bracket = fullAddress.indexOf("]");
+ if (bracket > 0)
+ {
+ QString ipv6 = fullAddress.mid(1, bracket - 1);
+ QString port = fullAddress.mid(bracket + 1).trimmed();
+
+ if (port.startsWith(":") && !ipv6.isEmpty())
+ {
+ port = port.mid(1);
+ split = QStringList({ ipv6, port });
+ }
+ else
+ {
+ split = QStringList({ipv6});
+ }
+ }
+ }
+
+ if (split.size() > 2)
+ {
+ split = QStringList({fullAddress});
+ }
+
+ QString realAddress = split[0];
+
+ quint16 realPort = 25565;
+ if (split.size() > 1)
+ {
+ bool ok;
+ realPort = split[1].toUInt(&ok);
+
+ if (!ok)
+ {
+ realPort = 25565;
+ }
+ }
+
+ return MinecraftServerTarget { realAddress, realPort };
+}
diff --git a/launcher/minecraft/launch/MinecraftServerTarget.h b/launcher/minecraft/launch/MinecraftServerTarget.h
new file mode 100644
index 00000000..a402421a
--- /dev/null
+++ b/launcher/minecraft/launch/MinecraftServerTarget.h
@@ -0,0 +1,29 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <QString>
+
+struct MinecraftServerTarget {
+ QString address;
+ quint16 port;
+
+ static MinecraftServerTarget parse(const QString &fullAddress);
+};
+
+typedef std::shared_ptr<MinecraftServerTarget> MinecraftServerTargetPtr;
diff --git a/launcher/minecraft/launch/ModMinecraftJar.cpp b/launcher/minecraft/launch/ModMinecraftJar.cpp
new file mode 100644
index 00000000..93de9d59
--- /dev/null
+++ b/launcher/minecraft/launch/ModMinecraftJar.cpp
@@ -0,0 +1,82 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ModMinecraftJar.h"
+#include "launch/LaunchTask.h"
+#include "MMCZip.h"
+#include "minecraft/OpSys.h"
+#include "FileSystem.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+void ModMinecraftJar::executeTask()
+{
+ auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
+
+ if(!m_inst->getJarMods().size())
+ {
+ emitSucceeded();
+ return;
+ }
+ // nuke obsolete stripped jar(s) if needed
+ if(!FS::ensureFolderPathExists(m_inst->binRoot()))
+ {
+ emitFailed(tr("Couldn't create the bin folder for Minecraft.jar"));
+ }
+
+ auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar");
+ if(!removeJar())
+ {
+ emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath));
+ }
+
+ // create temporary modded jar, if needed
+ auto components = m_inst->getPackProfile();
+ auto profile = components->getProfile();
+ auto jarMods = m_inst->getJarMods();
+ if(jarMods.size())
+ {
+ auto mainJar = profile->getMainJar();
+ QStringList jars, temp1, temp2, temp3, temp4;
+ mainJar->getApplicableFiles(currentSystem, jars, temp1, temp2, temp3, m_inst->getLocalLibraryPath());
+ auto sourceJarPath = jars[0];
+ if(!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods))
+ {
+ emitFailed(tr("Failed to create the custom Minecraft jar file."));
+ return;
+ }
+ }
+ emitSucceeded();
+}
+
+void ModMinecraftJar::finalize()
+{
+ removeJar();
+}
+
+bool ModMinecraftJar::removeJar()
+{
+ auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
+ auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar");
+ QFile finalJar(finalJarPath);
+ if(finalJar.exists())
+ {
+ if(!finalJar.remove())
+ {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/launcher/minecraft/launch/ModMinecraftJar.h b/launcher/minecraft/launch/ModMinecraftJar.h
new file mode 100644
index 00000000..081c6a91
--- /dev/null
+++ b/launcher/minecraft/launch/ModMinecraftJar.h
@@ -0,0 +1,36 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <memory>
+
+class ModMinecraftJar: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit ModMinecraftJar(LaunchTask *parent) : LaunchStep(parent) {};
+ virtual ~ModMinecraftJar(){};
+
+ virtual void executeTask() override;
+ virtual bool canAbort() const override
+ {
+ return false;
+ }
+ void finalize() override;
+private:
+ bool removeJar();
+};
diff --git a/launcher/minecraft/launch/PrintInstanceInfo.cpp b/launcher/minecraft/launch/PrintInstanceInfo.cpp
new file mode 100644
index 00000000..0b9611ad
--- /dev/null
+++ b/launcher/minecraft/launch/PrintInstanceInfo.cpp
@@ -0,0 +1,106 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <fstream>
+#include <string>
+
+#include "PrintInstanceInfo.h"
+#include <launch/LaunchTask.h>
+
+#ifdef Q_OS_LINUX
+namespace {
+void probeProcCpuinfo(QStringList &log)
+{
+ std::ifstream cpuin("/proc/cpuinfo");
+ for (std::string line; std::getline(cpuin, line);)
+ {
+ if (strncmp(line.c_str(), "model name", 10) == 0)
+ {
+ log << QString::fromStdString(line.substr(13, std::string::npos));
+ break;
+ }
+ }
+}
+
+void runLspci(QStringList &log)
+{
+ // FIXME: fixed size buffers...
+ char buff[512];
+ int gpuline = -1;
+ int cline = 0;
+ FILE * lspci = popen("lspci -k", "r");
+
+ if (!lspci)
+ return;
+
+ while (fgets(buff, 512, lspci) != NULL)
+ {
+ std::string str(buff);
+ if (str.length() < 9)
+ continue;
+ if (str.substr(8, 3) == "VGA")
+ {
+ gpuline = cline;
+ log << QString::fromStdString(str.substr(35, std::string::npos));
+ }
+ if (gpuline > -1 && gpuline != cline)
+ {
+ if (cline - gpuline < 3)
+ {
+ log << QString::fromStdString(str.substr(1, std::string::npos));
+ }
+ }
+ cline++;
+ }
+ pclose(lspci);
+}
+
+void runGlxinfo(QStringList & log)
+{
+ // FIXME: fixed size buffers...
+ char buff[512];
+ FILE *glxinfo = popen("glxinfo", "r");
+ if (!glxinfo)
+ return;
+
+ while (fgets(buff, 512, glxinfo) != NULL)
+ {
+ if (strncmp(buff, "OpenGL version string:", 22) == 0)
+ {
+ log << QString::fromUtf8(buff);
+ break;
+ }
+ }
+ pclose(glxinfo);
+}
+
+}
+#endif
+
+void PrintInstanceInfo::executeTask()
+{
+ auto instance = m_parent->instance();
+ QStringList log;
+
+#ifdef Q_OS_LINUX
+ ::probeProcCpuinfo(log);
+ ::runLspci(log);
+ ::runGlxinfo(log);
+#endif
+
+ logLines(log, MessageLevel::MultiMC);
+ logLines(instance->verboseDescription(m_session, m_serverToJoin), MessageLevel::MultiMC);
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/launch/PrintInstanceInfo.h b/launcher/minecraft/launch/PrintInstanceInfo.h
new file mode 100644
index 00000000..fdc30f31
--- /dev/null
+++ b/launcher/minecraft/launch/PrintInstanceInfo.h
@@ -0,0 +1,41 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <memory>
+#include "minecraft/auth/AuthSession.h"
+#include "minecraft/launch/MinecraftServerTarget.h"
+
+// FIXME: temporary wrapper for existing task.
+class PrintInstanceInfo: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit PrintInstanceInfo(LaunchTask *parent, AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) :
+ LaunchStep(parent), m_session(session), m_serverToJoin(serverToJoin) {};
+ virtual ~PrintInstanceInfo(){};
+
+ virtual void executeTask();
+ virtual bool canAbort() const
+ {
+ return false;
+ }
+private:
+ AuthSessionPtr m_session;
+ MinecraftServerTargetPtr m_serverToJoin;
+};
+
diff --git a/launcher/minecraft/launch/ReconstructAssets.cpp b/launcher/minecraft/launch/ReconstructAssets.cpp
new file mode 100644
index 00000000..4d206665
--- /dev/null
+++ b/launcher/minecraft/launch/ReconstructAssets.cpp
@@ -0,0 +1,36 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ReconstructAssets.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/AssetsUtils.h"
+#include "launch/LaunchTask.h"
+
+void ReconstructAssets::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance);
+ auto components = minecraftInstance->getPackProfile();
+ auto profile = components->getProfile();
+ auto assets = profile->getMinecraftAssets();
+
+ if(!AssetsUtils::reconstructAssets(assets->id, minecraftInstance->resourcesDir()))
+ {
+ emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error);
+ }
+
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/launch/ReconstructAssets.h b/launcher/minecraft/launch/ReconstructAssets.h
new file mode 100644
index 00000000..58d7febd
--- /dev/null
+++ b/launcher/minecraft/launch/ReconstructAssets.h
@@ -0,0 +1,33 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <memory>
+
+class ReconstructAssets: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit ReconstructAssets(LaunchTask *parent) : LaunchStep(parent){};
+ virtual ~ReconstructAssets(){};
+
+ void executeTask() override;
+ bool canAbort() const override
+ {
+ return false;
+ }
+};
diff --git a/launcher/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp
new file mode 100644
index 00000000..2a0e21b3
--- /dev/null
+++ b/launcher/minecraft/launch/ScanModFolders.cpp
@@ -0,0 +1,59 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ScanModFolders.h"
+#include "launch/LaunchTask.h"
+#include "MMCZip.h"
+#include "minecraft/OpSys.h"
+#include "FileSystem.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/mod/ModFolderModel.h"
+
+void ScanModFolders::executeTask()
+{
+ auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
+
+ auto loaders = m_inst->loaderModList();
+ connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone);
+ if(!loaders->update()) {
+ m_modsDone = true;
+ }
+
+ auto cores = m_inst->coreModList();
+ connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone);
+ if(!cores->update()) {
+ m_coreModsDone = true;
+ }
+ checkDone();
+}
+
+void ScanModFolders::modsDone()
+{
+ m_modsDone = true;
+ checkDone();
+}
+
+void ScanModFolders::coreModsDone()
+{
+ m_coreModsDone = true;
+ checkDone();
+}
+
+void ScanModFolders::checkDone()
+{
+ if(m_modsDone && m_coreModsDone) {
+ emitSucceeded();
+ }
+}
diff --git a/launcher/minecraft/launch/ScanModFolders.h b/launcher/minecraft/launch/ScanModFolders.h
new file mode 100644
index 00000000..d5989170
--- /dev/null
+++ b/launcher/minecraft/launch/ScanModFolders.h
@@ -0,0 +1,42 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#include <memory>
+
+class ScanModFolders: public LaunchStep
+{
+ Q_OBJECT
+public:
+ explicit ScanModFolders(LaunchTask *parent) : LaunchStep(parent) {};
+ virtual ~ScanModFolders(){};
+
+ virtual void executeTask() override;
+ virtual bool canAbort() const override
+ {
+ return false;
+ }
+private slots:
+ void coreModsDone();
+ void modsDone();
+private:
+ void checkDone();
+
+private: // DATA
+ bool m_modsDone = false;
+ bool m_coreModsDone = false;
+};
diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp
new file mode 100644
index 00000000..657669af
--- /dev/null
+++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp
@@ -0,0 +1,34 @@
+#include "VerifyJavaInstall.h"
+
+#include <launch/LaunchTask.h>
+#include <minecraft/MinecraftInstance.h>
+#include <minecraft/PackProfile.h>
+#include <minecraft/VersionFilterData.h>
+
+void VerifyJavaInstall::executeTask() {
+ auto m_inst = std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
+
+ auto javaVersion = m_inst->getJavaVersion();
+ auto minecraftComponent = m_inst->getPackProfile()->getComponent("net.minecraft");
+
+ // Java 16 requirement
+ if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate) {
+ if (javaVersion.major() < 16) {
+ emit logLine("Minecraft 21w19a and above require the use of Java 16",
+ MessageLevel::Fatal);
+ emitFailed(tr("Minecraft 21w19a and above require the use of Java 16"));
+ return;
+ }
+ }
+ // Java 8 requirement
+ else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java8BeginsDate) {
+ if (javaVersion.major() < 8) {
+ emit logLine("Minecraft 17w13a and above require the use of Java 8",
+ MessageLevel::Fatal);
+ emitFailed(tr("Minecraft 17w13a and above require the use of Java 8"));
+ return;
+ }
+ }
+
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/launch/VerifyJavaInstall.h b/launcher/minecraft/launch/VerifyJavaInstall.h
new file mode 100644
index 00000000..a553106d
--- /dev/null
+++ b/launcher/minecraft/launch/VerifyJavaInstall.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <launch/LaunchStep.h>
+
+class VerifyJavaInstall : public LaunchStep {
+ Q_OBJECT
+
+public:
+ explicit VerifyJavaInstall(LaunchTask *parent) : LaunchStep(parent) {
+ };
+ ~VerifyJavaInstall() override = default;
+
+ void executeTask() override;
+ bool canAbort() const override {
+ return false;
+ }
+};
diff --git a/launcher/minecraft/legacy/LegacyInstance.cpp b/launcher/minecraft/legacy/LegacyInstance.cpp
new file mode 100644
index 00000000..9f9bda5a
--- /dev/null
+++ b/launcher/minecraft/legacy/LegacyInstance.cpp
@@ -0,0 +1,256 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QFileInfo>
+#include <minecraft/launch/LauncherPartLaunch.h>
+#include <QDir>
+#include <settings/Setting.h>
+
+#include "LegacyInstance.h"
+
+#include "minecraft/legacy/LegacyModList.h"
+#include "minecraft/WorldList.h"
+#include <MMCZip.h>
+#include <FileSystem.h>
+
+LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir)
+ : BaseInstance(globalSettings, settings, rootDir)
+{
+ settings->registerSetting("NeedsRebuild", true);
+ settings->registerSetting("ShouldUpdate", false);
+ settings->registerSetting("JarVersion", QString());
+ settings->registerSetting("IntendedJarVersion", QString());
+ /*
+ * 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::mainJarToPreserve() const
+{
+ bool customJar = m_settings->get("UseCustomBaseJar").toBool();
+ if(customJar)
+ {
+ auto base = baseJar();
+ if(QFile::exists(base))
+ {
+ return base;
+ }
+ }
+ auto runnable = runnableJar();
+ if(QFile::exists(runnable))
+ {
+ return runnable;
+ }
+ return QString();
+}
+
+
+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;
+}
+
+bool LegacyInstance::shouldUseCustomBaseJar() const
+{
+ return m_settings->get("UseCustomBaseJar").toBool();
+}
+
+
+shared_qobject_ptr<Task> LegacyInstance::createUpdateTask(Net::Mode)
+{
+ return nullptr;
+}
+
+std::shared_ptr<LegacyModList> LegacyInstance::jarModList() const
+{
+ if (!jar_mod_list)
+ {
+ auto list = new LegacyModList(jarModsDir(), modListFile());
+ jar_mod_list.reset(list);
+ }
+ jar_mod_list->update();
+ return jar_mod_list;
+}
+
+QString LegacyInstance::gameRoot() const
+{
+ QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
+ QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
+
+ if (mcDir.exists() && !dotMCDir.exists())
+ return mcDir.filePath();
+ else
+ return dotMCDir.filePath();
+}
+
+QString LegacyInstance::binRoot() const
+{
+ return FS::PathCombine(gameRoot(), "bin");
+}
+
+QString LegacyInstance::jarModsDir() const
+{
+ return FS::PathCombine(instanceRoot(), "instMods");
+}
+
+QString LegacyInstance::libDir() const
+{
+ return FS::PathCombine(gameRoot(), "lib");
+}
+
+QString LegacyInstance::savesDir() const
+{
+ return FS::PathCombine(gameRoot(), "saves");
+}
+
+QString LegacyInstance::loaderModsDir() const
+{
+ return FS::PathCombine(gameRoot(), "mods");
+}
+
+QString LegacyInstance::coreModsDir() const
+{
+ return FS::PathCombine(gameRoot(), "coremods");
+}
+
+QString LegacyInstance::resourceDir() const
+{
+ return FS::PathCombine(gameRoot(), "resources");
+}
+QString LegacyInstance::texturePacksDir() const
+{
+ return FS::PathCombine(gameRoot(), "texturepacks");
+}
+
+QString LegacyInstance::runnableJar() const
+{
+ return FS::PathCombine(binRoot(), "minecraft.jar");
+}
+
+QString LegacyInstance::modListFile() const
+{
+ return FS::PathCombine(instanceRoot(), "modlist");
+}
+
+QString LegacyInstance::instanceConfigFolder() const
+{
+ return FS::PathCombine(gameRoot(), "config");
+}
+
+bool LegacyInstance::shouldRebuild() const
+{
+ return m_settings->get("NeedsRebuild").toBool();
+}
+
+QString LegacyInstance::currentVersionId() const
+{
+ return m_settings->get("JarVersion").toString();
+}
+
+QString LegacyInstance::intendedVersionId() const
+{
+ return m_settings->get("IntendedJarVersion").toString();
+}
+
+bool LegacyInstance::shouldUpdate() const
+{
+ QVariant var = settings()->get("ShouldUpdate");
+ if (!var.isValid() || var.toBool() == false)
+ {
+ return intendedVersionId() != currentVersionId();
+ }
+ return true;
+}
+
+QString LegacyInstance::defaultBaseJar() const
+{
+ return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar";
+}
+
+QString LegacyInstance::defaultCustomBaseJar() const
+{
+ return FS::PathCombine(binRoot(), "mcbackup.jar");
+}
+
+std::shared_ptr<WorldList> LegacyInstance::worldList() const
+{
+ if (!m_world_list)
+ {
+ m_world_list.reset(new WorldList(savesDir()));
+ }
+ return m_world_list;
+}
+
+QString LegacyInstance::typeName() const
+{
+ return tr("Legacy");
+}
+
+QString LegacyInstance::getStatusbarDescription()
+{
+ return tr("Instance from previous versions.");
+}
+
+QStringList LegacyInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin)
+{
+ QStringList out;
+
+ auto alltraits = traits();
+ if(alltraits.size())
+ {
+ out << "Traits:";
+ for (auto trait : alltraits)
+ {
+ out << " " + trait;
+ }
+ out << "";
+ }
+
+ QString windowParams;
+ if (settings()->get("LaunchMaximized").toBool())
+ {
+ out << "Window size: max (if available)";
+ }
+ else
+ {
+ auto width = settings()->get("MinecraftWinWidth").toInt();
+ auto height = settings()->get("MinecraftWinHeight").toInt();
+ out << "Window size: " + QString::number(width) + " x " + QString::number(height);
+ }
+ out << "";
+ return out;
+}
diff --git a/launcher/minecraft/legacy/LegacyInstance.h b/launcher/minecraft/legacy/LegacyInstance.h
new file mode 100644
index 00000000..ac2a8543
--- /dev/null
+++ b/launcher/minecraft/legacy/LegacyInstance.h
@@ -0,0 +1,140 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "BaseInstance.h"
+#include "launch/LaunchTask.h"
+
+class ModFolderModel;
+class LegacyModList;
+class WorldList;
+class Task;
+/*
+ * WHY: Legacy instances - from MultiMC 3 and 4 - are here only to provide a way to upgrade them to the current format.
+ */
+class LegacyInstance : public BaseInstance
+{
+ Q_OBJECT
+public:
+
+ explicit LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
+
+ virtual void saveNow() override {}
+
+ /// Path to the instance's minecraft.jar
+ QString runnableJar() const;
+
+ //! Path to the instance's modlist file.
+ QString modListFile() const;
+
+ ////// Directories //////
+ QString libDir() const;
+ QString savesDir() const;
+ QString texturePacksDir() const;
+ QString jarModsDir() const;
+ QString loaderModsDir() const;
+ QString coreModsDir() const;
+ QString resourceDir() const;
+ virtual QString instanceConfigFolder() const override;
+ QString gameRoot() const override; // Path to the instance's minecraft directory.
+ QString binRoot() const; // Path to the instance's minecraft bin directory.
+
+ /// 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;
+
+ // the main jar that we actually want to keep when migrating the instance
+ QString mainJarToPreserve() const;
+
+ /*!
+ * Whether or not custom base jar is used
+ */
+ bool shouldUseCustomBaseJar() const;
+
+ /*!
+ * The value of the custom base jar
+ */
+ QString customBaseJar() const;
+
+ std::shared_ptr<LegacyModList> jarModList() const;
+ std::shared_ptr<WorldList> worldList() const;
+
+ /*!
+ * 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;
+
+ QString currentVersionId() const;
+ QString intendedVersionId() const;
+
+ QSet<QString> traits() const override
+ {
+ return {"legacy-instance", "texturepacks"};
+ };
+
+ virtual bool shouldUpdate() const;
+ virtual shared_qobject_ptr<Task> createUpdateTask(Net::Mode mode) override;
+
+ virtual QString typeName() const override;
+
+ bool canLaunch() const override
+ {
+ return false;
+ }
+ bool canEdit() const override
+ {
+ return true;
+ }
+ bool canExport() const override
+ {
+ return false;
+ }
+ shared_qobject_ptr<LaunchTask> createLaunchTask(
+ AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override
+ {
+ return nullptr;
+ }
+ IPathMatcher::Ptr getLogFileMatcher() override
+ {
+ return nullptr;
+ }
+ QString getLogFileRoot() override
+ {
+ return gameRoot();
+ }
+
+ QString getStatusbarDescription() override;
+ QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
+
+ QProcessEnvironment createEnvironment() override
+ {
+ return QProcessEnvironment();
+ }
+ QMap<QString, QString> getVariables() const override
+ {
+ return {};
+ }
+protected:
+ mutable std::shared_ptr<LegacyModList> jar_mod_list;
+ mutable std::shared_ptr<WorldList> m_world_list;
+};
diff --git a/launcher/minecraft/legacy/LegacyModList.cpp b/launcher/minecraft/legacy/LegacyModList.cpp
new file mode 100644
index 00000000..7301eb8c
--- /dev/null
+++ b/launcher/minecraft/legacy/LegacyModList.cpp
@@ -0,0 +1,136 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "LegacyModList.h"
+#include <FileSystem.h>
+#include <QString>
+#include <QDebug>
+
+LegacyModList::LegacyModList(const QString &dir, const QString &list_file)
+ : m_dir(dir), m_list_file(list_file)
+{
+ FS::ensureFolderPathExists(m_dir.absolutePath());
+ m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks);
+ m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
+}
+
+ struct OrderItem
+ {
+ QString id;
+ bool enabled = false;
+ };
+ typedef QList<OrderItem> OrderList;
+
+static void internalSort(QList<LegacyModList::Mod> &what)
+{
+ auto predicate = [](const LegacyModList::Mod &left, const LegacyModList::Mod &right)
+ {
+ return left.fileName().localeAwareCompare(right.fileName()) < 0;
+ };
+ std::sort(what.begin(), what.end(), predicate);
+}
+
+static OrderList readListFile(const QString &m_list_file)
+{
+ 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 LegacyModList::update()
+{
+ if (!m_dir.exists() || !m_dir.isReadable())
+ return false;
+
+ QList<Mod> orderedMods;
+ QList<Mod> newMods;
+ m_dir.refresh();
+ auto folderContents = m_dir.entryInfoList();
+
+ // first, process the ordered items (if any)
+ OrderList listOrder = readListFile(m_list_file);
+ 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(info);
+ }
+ }
+ // if there are any untracked files... append them sorted at the end
+ if (folderContents.size())
+ {
+ for (auto entry : folderContents)
+ {
+ newMods.append(entry);
+ }
+ internalSort(newMods);
+ orderedMods.append(newMods);
+ }
+ mods.swap(orderedMods);
+ return true;
+}
diff --git a/launcher/minecraft/legacy/LegacyModList.h b/launcher/minecraft/legacy/LegacyModList.h
new file mode 100644
index 00000000..fade736e
--- /dev/null
+++ b/launcher/minecraft/legacy/LegacyModList.h
@@ -0,0 +1,47 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QList>
+#include <QString>
+#include <QDir>
+
+class LegacyModList
+{
+public:
+
+ using Mod = QFileInfo;
+
+ LegacyModList(const QString &dir, const QString &list_file = QString());
+
+ /// Reloads the mod list and returns true if the list changed.
+ bool update();
+
+ QDir dir()
+ {
+ return m_dir;
+ }
+
+ const QList<Mod> & allMods()
+ {
+ return mods;
+ }
+
+protected:
+ QDir m_dir;
+ QString m_list_file;
+ QList<Mod> mods;
+};
diff --git a/launcher/minecraft/legacy/LegacyUpgradeTask.cpp b/launcher/minecraft/legacy/LegacyUpgradeTask.cpp
new file mode 100644
index 00000000..a4ea60cd
--- /dev/null
+++ b/launcher/minecraft/legacy/LegacyUpgradeTask.cpp
@@ -0,0 +1,138 @@
+#include "LegacyUpgradeTask.h"
+#include "settings/INISettingsObject.h"
+#include "FileSystem.h"
+#include "NullInstance.h"
+#include "pathmatcher/RegexpMatcher.h"
+#include <QtConcurrentRun>
+#include "LegacyInstance.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "LegacyModList.h"
+#include "classparser.h"
+
+LegacyUpgradeTask::LegacyUpgradeTask(InstancePtr origInstance)
+{
+ m_origInstance = origInstance;
+}
+
+void LegacyUpgradeTask::executeTask()
+{
+ setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
+
+ FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
+ folderCopy.followSymlinks(true);
+
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this, &LegacyUpgradeTask::copyFinished);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this, &LegacyUpgradeTask::copyAborted);
+ m_copyFutureWatcher.setFuture(m_copyFuture);
+}
+
+static QString decideVersion(const QString& currentVersion, const QString& intendedVersion)
+{
+ if(intendedVersion != currentVersion)
+ {
+ if(!intendedVersion.isEmpty())
+ {
+ return intendedVersion;
+ }
+ else if(!currentVersion.isEmpty())
+ {
+ return currentVersion;
+ }
+ }
+ else
+ {
+ if(!intendedVersion.isEmpty())
+ {
+ return intendedVersion;
+ }
+ }
+ return QString();
+}
+
+void LegacyUpgradeTask::copyFinished()
+{
+ auto successful = m_copyFuture.result();
+ if(!successful)
+ {
+ emitFailed(tr("Instance folder copy failed."));
+ return;
+ }
+ auto legacyInst = std::dynamic_pointer_cast<LegacyInstance>(m_origInstance);
+
+ auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg"));
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ // NOTE: this scope ensures the instance is fully saved before we emitSucceeded
+ {
+ MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath);
+ inst.setName(m_instName);
+
+ QString preferredVersionNumber = decideVersion(legacyInst->currentVersionId(), legacyInst->intendedVersionId());
+ if(preferredVersionNumber.isNull())
+ {
+ // try to decide version based on the jar(s?)
+ preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->baseJar());
+ if(preferredVersionNumber.isNull())
+ {
+ preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->runnableJar());
+ if(preferredVersionNumber.isNull())
+ {
+ emitFailed(tr("Could not decide Minecraft version."));
+ return;
+ }
+ }
+ }
+ auto components = inst.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", preferredVersionNumber, true);
+
+ QString jarPath = legacyInst->mainJarToPreserve();
+ if(!jarPath.isNull())
+ {
+ qDebug() << "Preserving base jar! : " << jarPath;
+ // FIXME: handle case when the jar is unreadable?
+ // TODO: check the hash, if it's the same as the upstream jar, do not do this
+ components->installCustomJar(jarPath);
+ }
+
+ auto jarMods = legacyInst->jarModList()->allMods();
+ for(auto & jarMod: jarMods)
+ {
+ QString modPath = jarMod.absoluteFilePath();
+ qDebug() << "jarMod: " << modPath;
+ components->installJarMods({modPath});
+ }
+
+ // remove all the extra garbage we no longer need
+ auto removeAll = [&](const QString &root, const QStringList &things)
+ {
+ for(auto &thing : things)
+ {
+ auto removePath = FS::PathCombine(root, thing);
+ QFileInfo stat(removePath);
+ if(stat.isDir())
+ {
+ FS::deletePath(removePath);
+ }
+ else
+ {
+ QFile::remove(removePath);
+ }
+ }
+ };
+ QStringList rootRemovables = {"modlist", "version", "instMods"};
+ QStringList mcRemovables = {"bin", "MultiMCLauncher.jar", "icon.png"};
+ removeAll(inst.instanceRoot(), rootRemovables);
+ removeAll(inst.gameRoot(), mcRemovables);
+ }
+ emitSucceeded();
+}
+
+void LegacyUpgradeTask::copyAborted()
+{
+ emitFailed(tr("Instance folder copy has been aborted."));
+ return;
+}
+
diff --git a/launcher/minecraft/legacy/LegacyUpgradeTask.h b/launcher/minecraft/legacy/LegacyUpgradeTask.h
new file mode 100644
index 00000000..542e17b8
--- /dev/null
+++ b/launcher/minecraft/legacy/LegacyUpgradeTask.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+#include <QUrl>
+#include <QFuture>
+#include <QFutureWatcher>
+#include "settings/SettingsObject.h"
+#include "BaseVersion.h"
+#include "BaseInstance.h"
+
+
+class LegacyUpgradeTask : public InstanceTask
+{
+ Q_OBJECT
+public:
+ explicit LegacyUpgradeTask(InstancePtr origInstance);
+
+protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+ void copyFinished();
+ void copyAborted();
+
+private: /* data */
+ InstancePtr m_origInstance;
+ QFuture<bool> m_copyFuture;
+ QFutureWatcher<bool> m_copyFutureWatcher;
+};
diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp
new file mode 100644
index 00000000..0d6972fb
--- /dev/null
+++ b/launcher/minecraft/mod/LocalModParseTask.cpp
@@ -0,0 +1,467 @@
+#include "LocalModParseTask.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <quazip.h>
+#include <quazipfile.h>
+#include <toml.h>
+
+#include "settings/INIFile.h"
+#include "FileSystem.h"
+
+namespace {
+
+// NEW format
+// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3
+
+// OLD format:
+// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
+std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
+{
+ auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails>
+ {
+ if (!arr.at(0).isObject()) {
+ return nullptr;
+ }
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ auto firstObj = arr.at(0).toObject();
+ details->mod_id = firstObj.value("modid").toString();
+ auto name = firstObj.value("name").toString();
+ // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
+ if(name != "Example Mod") {
+ details->name = name;
+ }
+ details->version = firstObj.value("version").toString();
+ details->updateurl = firstObj.value("updateUrl").toString();
+ auto homeurl = firstObj.value("url").toString().trimmed();
+ if(!homeurl.isEmpty())
+ {
+ // fix up url.
+ if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://"))
+ {
+ homeurl.prepend("http://");
+ }
+ }
+ details->homeurl = homeurl;
+ details->description = firstObj.value("description").toString();
+ QJsonArray authors = firstObj.value("authorList").toArray();
+ if (authors.size() == 0) {
+ // FIXME: what is the format of this? is there any?
+ authors = firstObj.value("authors").toArray();
+ }
+
+ for (auto author: authors)
+ {
+ details->authors.append(author.toString());
+ }
+ details->credits = firstObj.value("credits").toString();
+ return details;
+ };
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
+ // this is the very old format that had just the array
+ if (jsonDoc.isArray())
+ {
+ return getInfoFromArray(jsonDoc.array());
+ }
+ else if (jsonDoc.isObject())
+ {
+ auto val = jsonDoc.object().value("modinfoversion");
+ if(val.isUndefined()) {
+ val = jsonDoc.object().value("modListVersion");
+ }
+ int version = val.toDouble();
+ if (version != 2)
+ {
+ qCritical() << "BAD stuff happened to mod json:";
+ qCritical() << contents;
+ return nullptr;
+ }
+ auto arrVal = jsonDoc.object().value("modlist");
+ if(arrVal.isUndefined()) {
+ arrVal = jsonDoc.object().value("modList");
+ }
+ if (arrVal.isArray())
+ {
+ return getInfoFromArray(arrVal.toArray());
+ }
+ }
+ return nullptr;
+}
+
+// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
+std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
+{
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+
+ char errbuf[200];
+ // top-level table
+ toml_table_t* tomlData = toml_parse(contents.data(), errbuf, sizeof(errbuf));
+
+ if(!tomlData)
+ {
+ return nullptr;
+ }
+
+ // array defined by [[mods]]
+ toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods");
+ // we only really care about the first element, since multiple mods in one file is not supported by us at the moment
+ toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0);
+
+ // mandatory properties - always in [[mods]]
+ toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
+ if(modIdDatum.ok)
+ {
+ details->mod_id = modIdDatum.u.s;
+ // library says this is required for strings
+ free(modIdDatum.u.s);
+ }
+ toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
+ if(versionDatum.ok)
+ {
+ details->version = versionDatum.u.s;
+ free(versionDatum.u.s);
+ }
+ toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
+ if(displayNameDatum.ok)
+ {
+ details->name = displayNameDatum.u.s;
+ free(displayNameDatum.u.s);
+ }
+ toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
+ if(descriptionDatum.ok)
+ {
+ details->description = descriptionDatum.u.s;
+ free(descriptionDatum.u.s);
+ }
+
+ // optional properties - can be in the root table or [[mods]]
+ toml_datum_t authorsDatum = toml_string_in(tomlData, "authors");
+ QString authors = "";
+ if(authorsDatum.ok)
+ {
+ authors = authorsDatum.u.s;
+ free(authorsDatum.u.s);
+ }
+ else
+ {
+ authorsDatum = toml_string_in(tomlModsTable0, "authors");
+ if(authorsDatum.ok)
+ {
+ authors = authorsDatum.u.s;
+ free(authorsDatum.u.s);
+ }
+ }
+ if(!authors.isEmpty())
+ {
+ // author information is stored as a string now, not a list
+ details->authors.append(authors);
+ }
+ // is credits even used anywhere? including this for completion/parity with old data version
+ toml_datum_t creditsDatum = toml_string_in(tomlData, "credits");
+ QString credits = "";
+ if(creditsDatum.ok)
+ {
+ authors = creditsDatum.u.s;
+ free(creditsDatum.u.s);
+ }
+ else
+ {
+ creditsDatum = toml_string_in(tomlModsTable0, "credits");
+ if(creditsDatum.ok)
+ {
+ credits = creditsDatum.u.s;
+ free(creditsDatum.u.s);
+ }
+ }
+ details->credits = credits;
+ toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
+ QString homeurl = "";
+ if(homeurlDatum.ok)
+ {
+ homeurl = homeurlDatum.u.s;
+ free(homeurlDatum.u.s);
+ }
+ else
+ {
+ homeurlDatum = toml_string_in(tomlModsTable0, "displayURL");
+ if(homeurlDatum.ok)
+ {
+ homeurl = homeurlDatum.u.s;
+ free(homeurlDatum.u.s);
+ }
+ }
+ if(!homeurl.isEmpty())
+ {
+ // fix up url.
+ if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://"))
+ {
+ homeurl.prepend("http://");
+ }
+ }
+ details->homeurl = homeurl;
+
+ // this seems to be recursive, so it should free everything
+ toml_free(tomlData);
+
+ return details;
+}
+
+// https://fabricmc.net/wiki/documentation:fabric_mod_json
+std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
+{
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
+ auto object = jsonDoc.object();
+ auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
+
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+
+ details->mod_id = object.value("id").toString();
+ details->version = object.value("version").toString();
+
+ details->name = object.contains("name") ? object.value("name").toString() : details->mod_id;
+ details->description = object.value("description").toString();
+
+ if (schemaVersion >= 1)
+ {
+ QJsonArray authors = object.value("authors").toArray();
+ for (auto author: authors)
+ {
+ if(author.isObject()) {
+ details->authors.append(author.toObject().value("name").toString());
+ }
+ else {
+ details->authors.append(author.toString());
+ }
+ }
+
+ if (object.contains("contact"))
+ {
+ QJsonObject contact = object.value("contact").toObject();
+
+ if (contact.contains("homepage"))
+ {
+ details->homeurl = contact.value("homepage").toString();
+ }
+ }
+ }
+ return details;
+}
+
+std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
+{
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ // Read the data
+ details->name = "Minecraft Forge";
+ details->mod_id = "Forge";
+ details->homeurl = "http://www.minecraftforge.net/forum/";
+ INIFile ini;
+ if (!ini.loadFile(contents))
+ return details;
+
+ QString major = ini.get("forge.major.number", "0").toString();
+ QString minor = ini.get("forge.minor.number", "0").toString();
+ QString revision = ini.get("forge.revision.number", "0").toString();
+ QString build = ini.get("forge.build.number", "0").toString();
+
+ details->version = major + "." + minor + "." + revision + "." + build;
+ return details;
+}
+
+std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
+{
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
+ auto object = jsonDoc.object();
+ if (object.contains("name"))
+ {
+ details->mod_id = details->name = object.value("name").toString();
+ }
+ if (object.contains("version"))
+ {
+ details->version = object.value("version").toString("");
+ }
+ else
+ {
+ details->version = object.value("revision").toString("");
+ }
+ details->mcversion = object.value("mcversion").toString();
+ auto author = object.value("author").toString();
+ if(!author.isEmpty()) {
+ details->authors.append(author);
+ }
+ details->description = object.value("description").toString();
+ details->homeurl = object.value("url").toString();
+ return details;
+}
+
+}
+
+LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile):
+ m_token(token),
+ m_type(type),
+ m_modFile(modFile),
+ m_result(new Result())
+{
+}
+
+void LocalModParseTask::processAsZip()
+{
+ QuaZip zip(m_modFile.filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return;
+
+ QuaZipFile file(&zip);
+
+ if (zip.setCurrentFile("META-INF/mods.toml"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ m_result->details = ReadMCModTOML(file.readAll());
+ file.close();
+
+ // to replace ${file.jarVersion} with the actual version, as needed
+ if (m_result->details && m_result->details->version == "${file.jarVersion}")
+ {
+ if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ // quick and dirty line-by-line parser
+ auto manifestLines = file.readAll().split('\n');
+ QString manifestVersion = "";
+ for (auto &line : manifestLines)
+ {
+ if (QString(line).startsWith("Implementation-Version: "))
+ {
+ manifestVersion = QString(line).remove("Implementation-Version: ");
+ break;
+ }
+ }
+
+ // some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF
+ // also keep with forge's behavior of setting the version to "NONE" if none is found
+ if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "")
+ {
+ manifestVersion = "NONE";
+ }
+
+ m_result->details->version = manifestVersion;
+
+ file.close();
+ }
+ }
+
+ zip.close();
+ return;
+ }
+ else if (zip.setCurrentFile("mcmod.info"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ m_result->details = ReadMCModInfo(file.readAll());
+ file.close();
+ zip.close();
+ return;
+ }
+ else if (zip.setCurrentFile("fabric.mod.json"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ m_result->details = ReadFabricModInfo(file.readAll());
+ file.close();
+ zip.close();
+ return;
+ }
+ else if (zip.setCurrentFile("forgeversion.properties"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ m_result->details = ReadForgeInfo(file.readAll());
+ file.close();
+ zip.close();
+ return;
+ }
+
+ zip.close();
+}
+
+void LocalModParseTask::processAsFolder()
+{
+ QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info"));
+ if (mcmod_info.isFile())
+ {
+ QFile mcmod(mcmod_info.filePath());
+ if (!mcmod.open(QIODevice::ReadOnly))
+ return;
+ auto data = mcmod.readAll();
+ if (data.isEmpty() || data.isNull())
+ return;
+ m_result->details = ReadMCModInfo(data);
+ }
+}
+
+void LocalModParseTask::processAsLitemod()
+{
+ QuaZip zip(m_modFile.filePath());
+ if (!zip.open(QuaZip::mdUnzip))
+ return;
+
+ QuaZipFile file(&zip);
+
+ if (zip.setCurrentFile("litemod.json"))
+ {
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ zip.close();
+ return;
+ }
+
+ m_result->details = ReadLiteModInfo(file.readAll());
+ file.close();
+ }
+ zip.close();
+}
+
+void LocalModParseTask::run()
+{
+ switch(m_type)
+ {
+ case Mod::MOD_ZIPFILE:
+ processAsZip();
+ break;
+ case Mod::MOD_FOLDER:
+ processAsFolder();
+ break;
+ case Mod::MOD_LITEMOD:
+ processAsLitemod();
+ break;
+ default:
+ break;
+ }
+ emit finished(m_token);
+}
diff --git a/launcher/minecraft/mod/LocalModParseTask.h b/launcher/minecraft/mod/LocalModParseTask.h
new file mode 100644
index 00000000..0f119ba6
--- /dev/null
+++ b/launcher/minecraft/mod/LocalModParseTask.h
@@ -0,0 +1,37 @@
+#pragma once
+#include <QRunnable>
+#include <QDebug>
+#include <QObject>
+#include "Mod.h"
+#include "ModDetails.h"
+
+class LocalModParseTask : public QObject, public QRunnable
+{
+ Q_OBJECT
+public:
+ struct Result {
+ QString id;
+ std::shared_ptr<ModDetails> details;
+ };
+ using ResultPtr = std::shared_ptr<Result>;
+ ResultPtr result() const {
+ return m_result;
+ }
+
+ LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
+ void run();
+
+signals:
+ void finished(int token);
+
+private:
+ void processAsZip();
+ void processAsFolder();
+ void processAsLitemod();
+
+private:
+ int m_token;
+ Mod::ModType m_type;
+ QFileInfo m_modFile;
+ ResultPtr m_result;
+};
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
new file mode 100644
index 00000000..b6bff29b
--- /dev/null
+++ b/launcher/minecraft/mod/Mod.cpp
@@ -0,0 +1,151 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QDir>
+#include <QString>
+
+#include "Mod.h"
+#include <QDebug>
+#include <FileSystem.h>
+
+namespace {
+
+ModDetails invalidDetails;
+
+}
+
+
+Mod::Mod(const QFileInfo &file)
+{
+ repath(file);
+ m_changedDateTime = file.lastModified();
+}
+
+void Mod::repath(const QFileInfo &file)
+{
+ m_file = file;
+ QString name_base = file.fileName();
+
+ m_type = Mod::MOD_UNKNOWN;
+
+ m_mmc_id = name_base;
+
+ if (m_file.isDir())
+ {
+ m_type = MOD_FOLDER;
+ m_name = name_base;
+ }
+ else if (m_file.isFile())
+ {
+ if (name_base.endsWith(".disabled"))
+ {
+ m_enabled = false;
+ name_base.chop(9);
+ }
+ else
+ {
+ m_enabled = true;
+ }
+ if (name_base.endsWith(".zip") || name_base.endsWith(".jar"))
+ {
+ m_type = MOD_ZIPFILE;
+ name_base.chop(4);
+ }
+ else if (name_base.endsWith(".litemod"))
+ {
+ m_type = MOD_LITEMOD;
+ name_base.chop(8);
+ }
+ else
+ {
+ m_type = MOD_SINGLEFILE;
+ }
+ m_name = name_base;
+ }
+}
+
+bool Mod::enable(bool value)
+{
+ if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
+ return false;
+
+ if (m_enabled == value)
+ return false;
+
+ QString path = m_file.absoluteFilePath();
+ if (value)
+ {
+ QFile foo(path);
+ if (!path.endsWith(".disabled"))
+ return false;
+ path.chop(9);
+ if (!foo.rename(path))
+ return false;
+ }
+ else
+ {
+ QFile foo(path);
+ path += ".disabled";
+ if (!foo.rename(path))
+ return false;
+ }
+ repath(QFileInfo(path));
+ m_enabled = value;
+ return true;
+}
+
+bool Mod::destroy()
+{
+ m_type = MOD_UNKNOWN;
+ return FS::deletePath(m_file.filePath());
+}
+
+
+const ModDetails & Mod::details() const
+{
+ if(!m_localDetails)
+ return invalidDetails;
+ return *m_localDetails;
+}
+
+
+QString Mod::version() const
+{
+ return details().version;
+}
+
+QString Mod::name() const
+{
+ auto & d = details();
+ if(!d.name.isEmpty()) {
+ return d.name;
+ }
+ return m_name;
+}
+
+QString Mod::homeurl() const
+{
+ return details().homeurl;
+}
+
+QString Mod::description() const
+{
+ return details().description;
+}
+
+QStringList Mod::authors() const
+{
+ return details().authors;
+}
diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h
new file mode 100644
index 00000000..921faeb1
--- /dev/null
+++ b/launcher/minecraft/mod/Mod.h
@@ -0,0 +1,115 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QFileInfo>
+#include <QDateTime>
+#include <QList>
+#include <memory>
+
+#include "ModDetails.h"
+
+
+
+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() = default;
+ Mod(const QFileInfo &file);
+
+ QFileInfo filename() const
+ {
+ return m_file;
+ }
+ QString mmc_id() const
+ {
+ return m_mmc_id;
+ }
+ ModType type() const
+ {
+ return m_type;
+ }
+ bool valid()
+ {
+ return m_type != MOD_UNKNOWN;
+ }
+
+ QDateTime dateTimeChanged() const
+ {
+ return m_changedDateTime;
+ }
+
+ bool enabled() const
+ {
+ return m_enabled;
+ }
+
+ const ModDetails &details() const;
+
+ QString name() const;
+ QString version() const;
+ QString homeurl() const;
+ QString description() const;
+ QStringList authors() const;
+
+ bool enable(bool value);
+
+ // delete all the files of this mod
+ bool destroy();
+
+ // change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
+ void repath(const QFileInfo &file);
+
+ bool shouldResolve() {
+ return !m_resolving && !m_resolved;
+ }
+ bool isResolving() {
+ return m_resolving;
+ }
+ int resolutionTicket()
+ {
+ return m_resolutionTicket;
+ }
+ void setResolving(bool resolving, int resolutionTicket) {
+ m_resolving = resolving;
+ m_resolutionTicket = resolutionTicket;
+ }
+ void finishResolvingWithDetails(std::shared_ptr<ModDetails> details){
+ m_resolving = false;
+ m_resolved = true;
+ m_localDetails = details;
+ }
+
+protected:
+ QFileInfo m_file;
+ QDateTime m_changedDateTime;
+ QString m_mmc_id;
+ QString m_name;
+ bool m_enabled = true;
+ bool m_resolving = false;
+ bool m_resolved = false;
+ int m_resolutionTicket = 0;
+ ModType m_type = MOD_UNKNOWN;
+ std::shared_ptr<ModDetails> m_localDetails;
+};
diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h
new file mode 100644
index 00000000..6ab4aee7
--- /dev/null
+++ b/launcher/minecraft/mod/ModDetails.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <QString>
+#include <QStringList>
+
+struct ModDetails
+{
+ QString mod_id;
+ QString name;
+ QString version;
+ QString mcversion;
+ QString homeurl;
+ QString updateurl;
+ QString description;
+ QStringList authors;
+ QString credits;
+};
diff --git a/launcher/minecraft/mod/ModFolderLoadTask.cpp b/launcher/minecraft/mod/ModFolderLoadTask.cpp
new file mode 100644
index 00000000..88349877
--- /dev/null
+++ b/launcher/minecraft/mod/ModFolderLoadTask.cpp
@@ -0,0 +1,18 @@
+#include "ModFolderLoadTask.h"
+#include <QDebug>
+
+ModFolderLoadTask::ModFolderLoadTask(QDir dir) :
+ m_dir(dir), m_result(new Result())
+{
+}
+
+void ModFolderLoadTask::run()
+{
+ m_dir.refresh();
+ for (auto entry : m_dir.entryInfoList())
+ {
+ Mod m(entry);
+ m_result->mods[m.mmc_id()] = m;
+ }
+ emit succeeded();
+}
diff --git a/launcher/minecraft/mod/ModFolderLoadTask.h b/launcher/minecraft/mod/ModFolderLoadTask.h
new file mode 100644
index 00000000..8d720e65
--- /dev/null
+++ b/launcher/minecraft/mod/ModFolderLoadTask.h
@@ -0,0 +1,29 @@
+#pragma once
+#include <QRunnable>
+#include <QObject>
+#include <QDir>
+#include <QMap>
+#include "Mod.h"
+#include <memory>
+
+class ModFolderLoadTask : public QObject, public QRunnable
+{
+ Q_OBJECT
+public:
+ struct Result {
+ QMap<QString, Mod> mods;
+ };
+ using ResultPtr = std::shared_ptr<Result>;
+ ResultPtr result() const {
+ return m_result;
+ }
+
+public:
+ ModFolderLoadTask(QDir dir);
+ void run();
+signals:
+ void succeeded();
+private:
+ QDir m_dir;
+ ResultPtr m_result;
+};
diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp
new file mode 100644
index 00000000..031eebe5
--- /dev/null
+++ b/launcher/minecraft/mod/ModFolderModel.cpp
@@ -0,0 +1,554 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ModFolderModel.h"
+#include <FileSystem.h>
+#include <QMimeData>
+#include <QUrl>
+#include <QUuid>
+#include <QString>
+#include <QFileSystemWatcher>
+#include <QDebug>
+#include "ModFolderLoadTask.h"
+#include <QThreadPool>
+#include <algorithm>
+#include "LocalModParseTask.h"
+
+ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir)
+{
+ FS::ensureFolderPathExists(m_dir.absolutePath());
+ m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks);
+ m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
+ m_watcher = new QFileSystemWatcher(this);
+ connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
+}
+
+void ModFolderModel::startWatching()
+{
+ if(is_watching)
+ return;
+
+ update();
+
+ is_watching = m_watcher->addPath(m_dir.absolutePath());
+ if (is_watching)
+ {
+ qDebug() << "Started watching " << m_dir.absolutePath();
+ }
+ else
+ {
+ qDebug() << "Failed to start watching " << m_dir.absolutePath();
+ }
+}
+
+void ModFolderModel::stopWatching()
+{
+ if(!is_watching)
+ return;
+
+ is_watching = !m_watcher->removePath(m_dir.absolutePath());
+ if (!is_watching)
+ {
+ qDebug() << "Stopped watching " << m_dir.absolutePath();
+ }
+ else
+ {
+ qDebug() << "Failed to stop watching " << m_dir.absolutePath();
+ }
+}
+
+bool ModFolderModel::update()
+{
+ if (!isValid()) {
+ return false;
+ }
+ if(m_update) {
+ scheduled_update = true;
+ return true;
+ }
+
+ auto task = new ModFolderLoadTask(m_dir);
+ m_update = task->result();
+ QThreadPool *threadPool = QThreadPool::globalInstance();
+ connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
+ threadPool->start(task);
+ return true;
+}
+
+void ModFolderModel::finishUpdate()
+{
+ QSet<QString> currentSet = modsIndex.keys().toSet();
+ auto & newMods = m_update->mods;
+ QSet<QString> newSet = newMods.keys().toSet();
+
+ // see if the kept mods changed in some way
+ {
+ QSet<QString> kept = currentSet;
+ kept.intersect(newSet);
+ for(auto & keptMod: kept) {
+ auto & newMod = newMods[keptMod];
+ auto row = modsIndex[keptMod];
+ auto & currentMod = mods[row];
+ if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) {
+ // no significant change, ignore...
+ continue;
+ }
+ auto & oldMod = mods[row];
+ if(oldMod.isResolving()) {
+ activeTickets.remove(oldMod.resolutionTicket());
+ }
+ oldMod = newMod;
+ resolveMod(mods[row]);
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ }
+ }
+
+ // remove mods no longer present
+ {
+ QSet<QString> removed = currentSet;
+ QList<int> removedRows;
+ removed.subtract(newSet);
+ for(auto & removedMod: removed) {
+ removedRows.append(modsIndex[removedMod]);
+ }
+ std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
+ for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) {
+ int removedIndex = *iter;
+ beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
+ auto removedIter = mods.begin() + removedIndex;
+ if(removedIter->isResolving()) {
+ activeTickets.remove(removedIter->resolutionTicket());
+ }
+ mods.erase(removedIter);
+ endRemoveRows();
+ }
+ }
+
+ // add new mods to the end
+ {
+ QSet<QString> added = newSet;
+ added.subtract(currentSet);
+ beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1);
+ for(auto & addedMod: added) {
+ mods.append(newMods[addedMod]);
+ resolveMod(mods.last());
+ }
+ endInsertRows();
+ }
+
+ // update index
+ {
+ modsIndex.clear();
+ int idx = 0;
+ for(auto & mod: mods) {
+ modsIndex[mod.mmc_id()] = idx;
+ idx++;
+ }
+ }
+
+ m_update.reset();
+
+ emit updateFinished();
+
+ if(scheduled_update) {
+ scheduled_update = false;
+ update();
+ }
+}
+
+void ModFolderModel::resolveMod(Mod& m)
+{
+ if(!m.shouldResolve()) {
+ return;
+ }
+
+ auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename());
+ auto result = task->result();
+ result->id = m.mmc_id();
+ activeTickets.insert(nextResolutionTicket, result);
+ m.setResolving(true, nextResolutionTicket);
+ nextResolutionTicket++;
+ QThreadPool *threadPool = QThreadPool::globalInstance();
+ connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
+ threadPool->start(task);
+}
+
+void ModFolderModel::finishModParse(int token)
+{
+ auto iter = activeTickets.find(token);
+ if(iter == activeTickets.end()) {
+ return;
+ }
+ auto result = *iter;
+ activeTickets.remove(token);
+ int row = modsIndex[result->id];
+ auto & mod = mods[row];
+ mod.finishResolvingWithDetails(result->details);
+ emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
+}
+
+void ModFolderModel::disableInteraction(bool disabled)
+{
+ if (interaction_disabled == disabled) {
+ return;
+ }
+ interaction_disabled = disabled;
+ if(size()) {
+ emit dataChanged(index(0), index(size() - 1));
+ }
+}
+
+void ModFolderModel::directoryChanged(QString path)
+{
+ update();
+}
+
+bool ModFolderModel::isValid()
+{
+ return m_dir.exists() && m_dir.isReadable();
+}
+
+// FIXME: this does not take disabled mod (with extra .disable extension) into account...
+bool ModFolderModel::installMod(const QString &filename)
+{
+ if(interaction_disabled) {
+ return false;
+ }
+
+ // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
+ auto originalPath = FS::NormalizePath(filename);
+ QFileInfo fileinfo(originalPath);
+
+ if (!fileinfo.exists() || !fileinfo.isReadable())
+ {
+ qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
+ return false;
+ }
+ qDebug() << "installing: " << fileinfo.absoluteFilePath();
+
+ Mod installedMod(fileinfo);
+ if (!installedMod.valid())
+ {
+ qDebug() << originalPath << "is not a valid mod. Ignoring it.";
+ return false;
+ }
+
+ auto type = installedMod.type();
+ if (type == Mod::MOD_UNKNOWN)
+ {
+ qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it.";
+ return false;
+ }
+
+ auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
+ if(originalPath == newpath)
+ {
+ qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
+ return false;
+ }
+
+ if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
+ {
+ if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled")))
+ {
+ if(!QFile::remove(newpath))
+ {
+ // FIXME: report error in a user-visible way
+ qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
+ return false;
+ }
+ qDebug() << newpath << "has been deleted.";
+ }
+ if (!QFile::copy(fileinfo.filePath(), newpath))
+ {
+ qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
+ // FIXME: report error in a user-visible way
+ return false;
+ }
+ FS::updateTimestamp(newpath);
+ installedMod.repath(newpath);
+ update();
+ return true;
+ }
+ else if (type == Mod::MOD_FOLDER)
+ {
+ QString from = fileinfo.filePath();
+ if(QFile::exists(newpath))
+ {
+ qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath;
+ return false;
+ }
+
+ if (!FS::copy(from, newpath)())
+ {
+ qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed.";
+ return false;
+ }
+ installedMod.repath(newpath);
+ update();
+ return true;
+ }
+ return false;
+}
+
+bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
+{
+ if(interaction_disabled) {
+ return false;
+ }
+
+ if(indexes.isEmpty())
+ return true;
+
+ for (auto index: indexes)
+ {
+ if(index.column() != 0) {
+ continue;
+ }
+ setModStatus(index.row(), enable);
+ }
+ return true;
+}
+
+bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
+{
+ if(interaction_disabled) {
+ return false;
+ }
+
+ if(indexes.isEmpty())
+ return true;
+
+ for (auto i: indexes)
+ {
+ Mod &m = mods[i.row()];
+ m.destroy();
+ }
+ return true;
+}
+
+int ModFolderModel::columnCount(const QModelIndex &parent) const
+{
+ return NUM_COLUMNS;
+}
+
+QVariant ModFolderModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+
+ if (row < 0 || row >= mods.size())
+ return QVariant();
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (column)
+ {
+ case NameColumn:
+ return mods[row].name();
+ case VersionColumn: {
+ switch(mods[row].type()) {
+ case Mod::MOD_FOLDER:
+ return tr("Folder");
+ case Mod::MOD_SINGLEFILE:
+ return tr("File");
+ default:
+ break;
+ }
+ return mods[row].version();
+ }
+ case DateColumn:
+ return mods[row].dateTimeChanged();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return mods[row].mmc_id();
+
+ case Qt::CheckStateRole:
+ switch (column)
+ {
+ case ActiveColumn:
+ return mods[row].enabled() ? Qt::Checked : Qt::Unchecked;
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+}
+
+bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
+ {
+ return false;
+ }
+
+ if (role == Qt::CheckStateRole)
+ {
+ return setModStatus(index.row(), Toggle);
+ }
+ return false;
+}
+
+bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
+{
+ if(row < 0 || row >= mods.size()) {
+ return false;
+ }
+
+ auto &mod = mods[row];
+ bool desiredStatus;
+ switch(action) {
+ case Enable:
+ desiredStatus = true;
+ break;
+ case Disable:
+ desiredStatus = false;
+ break;
+ case Toggle:
+ default:
+ desiredStatus = !mod.enabled();
+ break;
+ }
+
+ if(desiredStatus == mod.enabled()) {
+ return true;
+ }
+
+ // preserve the row, but change its ID
+ auto oldId = mod.mmc_id();
+ if(!mod.enable(!mod.enabled())) {
+ return false;
+ }
+ auto newId = mod.mmc_id();
+ if(modsIndex.contains(newId)) {
+ // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled
+ // But is it necessary?
+ }
+ modsIndex.remove(oldId);
+ modsIndex[newId] = row;
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ return true;
+}
+
+QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ switch (section)
+ {
+ case ActiveColumn:
+ return QString();
+ case NameColumn:
+ return tr("Name");
+ case VersionColumn:
+ return tr("Version");
+ case DateColumn:
+ return tr("Last changed");
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section)
+ {
+ case ActiveColumn:
+ return tr("Is the mod enabled?");
+ case NameColumn:
+ return tr("The name of the mod.");
+ case VersionColumn:
+ return tr("The version of the mod.");
+ case DateColumn:
+ return tr("The date and time this mod was last changed (or added).");
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ return QVariant();
+}
+
+Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
+{
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ auto flags = defaultFlags;
+ if(interaction_disabled) {
+ flags &= ~Qt::ItemIsDropEnabled;
+ }
+ else
+ {
+ flags |= Qt::ItemIsDropEnabled;
+ if(index.isValid()) {
+ flags |= Qt::ItemIsUserCheckable;
+ }
+ }
+ return flags;
+}
+
+Qt::DropActions ModFolderModel::supportedDropActions() const
+{
+ // copy from outside, move from within and other mod lists
+ return Qt::CopyAction | Qt::MoveAction;
+}
+
+QStringList ModFolderModel::mimeTypes() const
+{
+ QStringList types;
+ types << "text/uri-list";
+ return types;
+}
+
+bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
+{
+ if (action == Qt::IgnoreAction)
+ {
+ return true;
+ }
+
+ // check if the action is supported
+ if (!data || !(action & supportedDropActions()))
+ {
+ return false;
+ }
+
+ // files dropped from outside?
+ if (data->hasUrls())
+ {
+ auto urls = data->urls();
+ for (auto url : urls)
+ {
+ // only local files may be dropped...
+ if (!url.isLocalFile())
+ {
+ continue;
+ }
+ // TODO: implement not only copy, but also move
+ // FIXME: handle errors here
+ installMod(url.toLocalFile());
+ }
+ return true;
+ }
+ return false;
+}
diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h
new file mode 100644
index 00000000..62c504df
--- /dev/null
+++ b/launcher/minecraft/mod/ModFolderModel.h
@@ -0,0 +1,148 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QList>
+#include <QMap>
+#include <QSet>
+#include <QString>
+#include <QDir>
+#include <QAbstractListModel>
+
+#include "Mod.h"
+
+#include "ModFolderLoadTask.h"
+#include "LocalModParseTask.h"
+
+class LegacyInstance;
+class BaseInstance;
+class QFileSystemWatcher;
+
+/**
+ * A legacy mod list.
+ * Backed by a folder.
+ */
+class ModFolderModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Columns
+ {
+ ActiveColumn = 0,
+ NameColumn,
+ VersionColumn,
+ DateColumn,
+ NUM_COLUMNS
+ };
+ enum ModStatusAction {
+ Disable,
+ Enable,
+ Toggle
+ };
+ ModFolderModel(const QString &dir);
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+ Qt::DropActions supportedDropActions() const override;
+
+ /// flags, mostly to support drag&drop
+ virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
+ QStringList mimeTypes() const override;
+ bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
+
+ virtual int rowCount(const QModelIndex &) const override
+ {
+ return size();
+ }
+
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ virtual int columnCount(const QModelIndex &parent) const override;
+
+ size_t size() const
+ {
+ return mods.size();
+ }
+ ;
+ bool empty() const
+ {
+ return size() == 0;
+ }
+ Mod &operator[](size_t index)
+ {
+ return mods[index];
+ }
+ const Mod &at(size_t index) const
+ {
+ return mods.at(index);
+ }
+
+ /// Reloads the mod list and returns true if the list changed.
+ bool update();
+
+ /**
+ * Adds the given mod to the list at the given index - if the list supports custom ordering
+ */
+ bool installMod(const QString& filename);
+
+ /// Deletes all the selected mods
+ bool deleteMods(const QModelIndexList &indexes);
+
+ /// Enable or disable listed mods
+ bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
+
+ void startWatching();
+ void stopWatching();
+
+ bool isValid();
+
+ QDir dir()
+ {
+ return m_dir;
+ }
+
+ const QList<Mod> & allMods()
+ {
+ return mods;
+ }
+
+public slots:
+ void disableInteraction(bool disabled);
+
+private
+slots:
+ void directoryChanged(QString path);
+ void finishUpdate();
+ void finishModParse(int token);
+
+signals:
+ void updateFinished();
+
+private:
+ void resolveMod(Mod& m);
+ bool setModStatus(int index, ModStatusAction action);
+
+protected:
+ QFileSystemWatcher *m_watcher;
+ bool is_watching = false;
+ ModFolderLoadTask::ResultPtr m_update;
+ bool scheduled_update = false;
+ bool interaction_disabled = false;
+ QDir m_dir;
+ QMap<QString, int> modsIndex;
+ QMap<int, LocalModParseTask::ResultPtr> activeTickets;
+ int nextResolutionTicket = 0;
+ QList<Mod> mods;
+};
diff --git a/launcher/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp
new file mode 100644
index 00000000..76f16ed5
--- /dev/null
+++ b/launcher/minecraft/mod/ModFolderModel_test.cpp
@@ -0,0 +1,53 @@
+
+#include <QTest>
+#include <QTemporaryDir>
+#include "TestUtil.h"
+
+#include "FileSystem.h"
+#include "minecraft/mod/ModFolderModel.h"
+
+class ModFolderModelTest : public QObject
+{
+ Q_OBJECT
+
+private
+slots:
+ // test for GH-1178 - install a folder with files to a mod list
+ void test_1178()
+ {
+ // source
+ QString source = QFINDTESTDATA("data/test_folder");
+
+ // sanity check
+ QVERIFY(!source.endsWith('/'));
+
+ auto verify = [](QString path)
+ {
+ QDir target_dir(FS::PathCombine(path, "test_folder"));
+ QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
+ QVERIFY(target_dir.entryList().contains("assets"));
+ };
+
+ // 1. test with no trailing /
+ {
+ QString folder = source;
+ QTemporaryDir tempDir;
+ ModFolderModel m(tempDir.path());
+ m.installMod(folder);
+ verify(tempDir.path());
+ }
+
+ // 2. test with trailing /
+ {
+ QString folder = source + '/';
+ QTemporaryDir tempDir;
+ ModFolderModel m(tempDir.path());
+ m.installMod(folder);
+ verify(tempDir.path());
+ }
+ }
+};
+
+QTEST_GUILESS_MAIN(ModFolderModelTest)
+
+#include "ModFolderModel_test.moc"
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
new file mode 100644
index 00000000..f3d7f566
--- /dev/null
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp
@@ -0,0 +1,23 @@
+#include "ResourcePackFolderModel.h"
+
+ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) {
+}
+
+QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
+ if (role == Qt::ToolTipRole) {
+ switch (section) {
+ case ActiveColumn:
+ return tr("Is the resource pack enabled?");
+ case NameColumn:
+ return tr("The name of the resource pack.");
+ case VersionColumn:
+ return tr("The version of the resource pack.");
+ case DateColumn:
+ return tr("The date and time this resource pack was last changed (or added).");
+ default:
+ return QVariant();
+ }
+ }
+
+ return ModFolderModel::headerData(section, orientation, role);
+}
diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h
new file mode 100644
index 00000000..0cd6214b
--- /dev/null
+++ b/launcher/minecraft/mod/ResourcePackFolderModel.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "ModFolderModel.h"
+
+class ResourcePackFolderModel : public ModFolderModel
+{
+ Q_OBJECT
+
+public:
+ explicit ResourcePackFolderModel(const QString &dir);
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+};
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp
new file mode 100644
index 00000000..d5956da1
--- /dev/null
+++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -0,0 +1,23 @@
+#include "TexturePackFolderModel.h"
+
+TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) {
+}
+
+QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
+ if (role == Qt::ToolTipRole) {
+ switch (section) {
+ case ActiveColumn:
+ return tr("Is the texture pack enabled?");
+ case NameColumn:
+ return tr("The name of the texture pack.");
+ case VersionColumn:
+ return tr("The version of the texture pack.");
+ case DateColumn:
+ return tr("The date and time this texture pack was last changed (or added).");
+ default:
+ return QVariant();
+ }
+ }
+
+ return ModFolderModel::headerData(section, orientation, role);
+}
diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h
new file mode 100644
index 00000000..a59d5119
--- /dev/null
+++ b/launcher/minecraft/mod/TexturePackFolderModel.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "ModFolderModel.h"
+
+class TexturePackFolderModel : public ModFolderModel
+{
+ Q_OBJECT
+
+public:
+ explicit TexturePackFolderModel(const QString &dir);
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+};
diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp
new file mode 100644
index 00000000..34977257
--- /dev/null
+++ b/launcher/minecraft/services/SkinDelete.cpp
@@ -0,0 +1,42 @@
+#include "SkinDelete.h"
+#include <QNetworkRequest>
+#include <QHttpMultiPart>
+#include <Env.h>
+
+SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session)
+ : Task(parent), m_session(session)
+{
+}
+
+void SkinDelete::executeTask()
+{
+ QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"));
+ request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit());
+ QNetworkReply *rep = ENV.qnam().deleteResource(request);
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+
+ setStatus(tr("Deleting skin"));
+ connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void SkinDelete::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "Network error: " << error;
+ emitFailed(m_reply->errorString());
+}
+
+void SkinDelete::downloadFinished()
+{
+ // if the download failed
+ if (m_reply->error() != QNetworkReply::NetworkError::NoError)
+ {
+ emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
+ m_reply.reset();
+ return;
+ }
+ emitSucceeded();
+}
+
diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h
new file mode 100644
index 00000000..839bf9bc
--- /dev/null
+++ b/launcher/minecraft/services/SkinDelete.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <QFile>
+#include <QtNetwork/QtNetwork>
+#include <memory>
+#include <minecraft/auth/AuthSession.h>
+#include "tasks/Task.h"
+
+typedef std::shared_ptr<class SkinDelete> SkinDeletePtr;
+
+class SkinDelete : public Task
+{
+ Q_OBJECT
+public:
+ SkinDelete(QObject *parent, AuthSessionPtr session);
+ virtual ~SkinDelete() = default;
+
+private:
+ AuthSessionPtr m_session;
+ std::shared_ptr<QNetworkReply> m_reply;
+
+protected:
+ virtual void executeTask();
+
+public slots:
+ void downloadError(QNetworkReply::NetworkError);
+ void downloadFinished();
+};
+
diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp
new file mode 100644
index 00000000..4e5a1698
--- /dev/null
+++ b/launcher/minecraft/services/SkinUpload.cpp
@@ -0,0 +1,66 @@
+#include "SkinUpload.h"
+#include <QNetworkRequest>
+#include <QHttpMultiPart>
+#include <Env.h>
+
+QByteArray getVariant(SkinUpload::Model model) {
+ switch (model) {
+ default:
+ qDebug() << "Unknown skin type!";
+ case SkinUpload::STEVE:
+ return "CLASSIC";
+ case SkinUpload::ALEX:
+ return "SLIM";
+ }
+}
+
+SkinUpload::SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, SkinUpload::Model model)
+ : Task(parent), m_model(model), m_skin(skin), m_session(session)
+{
+}
+
+void SkinUpload::executeTask()
+{
+ QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins"));
+ request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit());
+ QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
+
+ QHttpPart skin;
+ skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
+ skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
+ skin.setBody(m_skin);
+
+ QHttpPart model;
+ model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\""));
+ model.setBody(getVariant(m_model));
+
+ multiPart->append(skin);
+ multiPart->append(model);
+
+ QNetworkReply *rep = ENV.qnam().post(request, multiPart);
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+
+ setStatus(tr("Uploading skin"));
+ connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void SkinUpload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "Network error: " << error;
+ emitFailed(m_reply->errorString());
+}
+
+void SkinUpload::downloadFinished()
+{
+ // if the download failed
+ if (m_reply->error() != QNetworkReply::NetworkError::NoError)
+ {
+ emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
+ m_reply.reset();
+ return;
+ }
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h
new file mode 100644
index 00000000..ec859699
--- /dev/null
+++ b/launcher/minecraft/services/SkinUpload.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <QFile>
+#include <QtNetwork/QtNetwork>
+#include <memory>
+#include <minecraft/auth/AuthSession.h>
+#include "tasks/Task.h"
+
+typedef std::shared_ptr<class SkinUpload> SkinUploadPtr;
+
+class SkinUpload : public Task
+{
+ Q_OBJECT
+public:
+ enum Model
+ {
+ STEVE,
+ ALEX
+ };
+
+ // Note this class takes ownership of the file.
+ SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, Model model = STEVE);
+ virtual ~SkinUpload() {}
+
+private:
+ Model m_model;
+ QByteArray m_skin;
+ AuthSessionPtr m_session;
+ std::shared_ptr<QNetworkReply> m_reply;
+protected:
+ virtual void executeTask();
+
+public slots:
+
+ void downloadError(QNetworkReply::NetworkError);
+
+ void downloadFinished();
+};
diff --git a/launcher/minecraft/testdata/1.9-simple.json b/launcher/minecraft/testdata/1.9-simple.json
new file mode 100644
index 00000000..574c5b06
--- /dev/null
+++ b/launcher/minecraft/testdata/1.9-simple.json
@@ -0,0 +1,198 @@
+{
+ "assets": "1.9",
+ "id": "1.9",
+ "libraries": [
+ {
+ "name": "oshi-project:oshi-core:1.1"
+ },
+ {
+ "name": "net.java.dev.jna:jna:3.4.0"
+ },
+ {
+ "name": "net.java.dev.jna:platform:3.4.0"
+ },
+ {
+ "name": "com.ibm.icu:icu4j-core-mojang:51.2"
+ },
+ {
+ "name": "net.sf.jopt-simple:jopt-simple:4.6"
+ },
+ {
+ "name": "com.paulscode:codecjorbis:20101023"
+ },
+ {
+ "name": "com.paulscode:codecwav:20101023"
+ },
+ {
+ "name": "com.paulscode:libraryjavasound:20101123"
+ },
+ {
+ "name": "com.paulscode:librarylwjglopenal:20100824"
+ },
+ {
+ "name": "com.paulscode:soundsystem:20120107"
+ },
+ {
+ "name": "io.netty:netty-all:4.0.23.Final"
+ },
+ {
+ "name": "com.google.guava:guava:17.0"
+ },
+ {
+ "name": "org.apache.commons:commons-lang3:3.3.2"
+ },
+ {
+ "name": "commons-io:commons-io:2.4"
+ },
+ {
+ "name": "commons-codec:commons-codec:1.9"
+ },
+ {
+ "name": "net.java.jinput:jinput:2.0.5"
+ },
+ {
+ "name": "net.java.jutils:jutils:1.0.0"
+ },
+ {
+ "name": "com.google.code.gson:gson:2.2.4"
+ },
+ {
+ "name": "com.mojang:authlib:1.5.22"
+ },
+ {
+ "name": "com.mojang:realms:1.8.4"
+ },
+ {
+ "name": "org.apache.commons:commons-compress:1.8.1"
+ },
+ {
+ "name": "org.apache.httpcomponents:httpclient:4.3.3"
+ },
+ {
+ "name": "commons-logging:commons-logging:1.1.3"
+ },
+ {
+ "name": "org.apache.httpcomponents:httpcore:4.3.2"
+ },
+ {
+ "name": "org.apache.logging.log4j:log4j-api:2.0-beta9"
+ },
+ {
+ "name": "org.apache.logging.log4j:log4j-core:2.0-beta9"
+ },
+ {
+ "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209",
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209",
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "net.java.jinput:jinput-platform:2.0.5",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ }
+ }
+ ],
+ "mainClass": "net.minecraft.client.main.Main",
+ "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}",
+ "minimumLauncherVersion": 18,
+ "releaseTime": "2016-02-29T13:49:54+00:00",
+ "time": "2016-03-01T13:14:53+00:00",
+ "type": "release"
+}
diff --git a/launcher/minecraft/testdata/1.9.json b/launcher/minecraft/testdata/1.9.json
new file mode 100644
index 00000000..697c6059
--- /dev/null
+++ b/launcher/minecraft/testdata/1.9.json
@@ -0,0 +1,529 @@
+{
+ "assetIndex": {
+ "id": "1.9",
+ "sha1": "cde65b47a43f638653ab1da3848b53f8a7477b16",
+ "size": 136916,
+ "totalSize": 119917473,
+ "url": "https://launchermeta.mojang.com/mc-staging/assets/1.9/cde65b47a43f638653ab1da3848b53f8a7477b16/1.9.json"
+ },
+ "assets": "1.9",
+ "downloads": {
+ "client": {
+ "sha1": "2f67dfe8953299440d1902f9124f0f2c3a2c940f",
+ "size": 8697592,
+ "url": "https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar"
+ },
+ "server": {
+ "sha1": "b4d449cf2918e0f3bd8aa18954b916a4d1880f0d",
+ "size": 8848015,
+ "url": "https://launcher.mojang.com/mc/game/1.9/server/b4d449cf2918e0f3bd8aa18954b916a4d1880f0d/server.jar"
+ }
+ },
+ "id": "1.9",
+ "libraries": [
+ {
+ "downloads": {
+ "artifact": {
+ "path": "oshi-project/oshi-core/1.1/oshi-core-1.1.jar",
+ "sha1": "9ddf7b048a8d701be231c0f4f95fd986198fd2d8",
+ "size": 30973,
+ "url": "https://libraries.minecraft.net/oshi-project/oshi-core/1.1/oshi-core-1.1.jar"
+ }
+ },
+ "name": "oshi-project:oshi-core:1.1"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar",
+ "sha1": "803ff252fedbd395baffd43b37341dc4a150a554",
+ "size": 1008730,
+ "url": "https://libraries.minecraft.net/net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar"
+ }
+ },
+ "name": "net.java.dev.jna:jna:3.4.0"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar",
+ "sha1": "e3f70017be8100d3d6923f50b3d2ee17714e9c13",
+ "size": 913436,
+ "url": "https://libraries.minecraft.net/net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar"
+ }
+ },
+ "name": "net.java.dev.jna:platform:3.4.0"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar",
+ "sha1": "63d216a9311cca6be337c1e458e587f99d382b84",
+ "size": 1634692,
+ "url": "https://libraries.minecraft.net/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar"
+ }
+ },
+ "name": "com.ibm.icu:icu4j-core-mojang:51.2"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar",
+ "sha1": "306816fb57cf94f108a43c95731b08934dcae15c",
+ "size": 62477,
+ "url": "https://libraries.minecraft.net/net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar"
+ }
+ },
+ "name": "net.sf.jopt-simple:jopt-simple:4.6"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar",
+ "sha1": "c73b5636faf089d9f00e8732a829577de25237ee",
+ "size": 103871,
+ "url": "https://libraries.minecraft.net/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar"
+ }
+ },
+ "name": "com.paulscode:codecjorbis:20101023"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar",
+ "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da",
+ "size": 5618,
+ "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar"
+ }
+ },
+ "name": "com.paulscode:codecwav:20101023"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar",
+ "sha1": "5c5e304366f75f9eaa2e8cca546a1fb6109348b3",
+ "size": 21679,
+ "url": "https://libraries.minecraft.net/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar"
+ }
+ },
+ "name": "com.paulscode:libraryjavasound:20101123"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar",
+ "sha1": "73e80d0794c39665aec3f62eee88ca91676674ef",
+ "size": 18981,
+ "url": "https://libraries.minecraft.net/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar"
+ }
+ },
+ "name": "com.paulscode:librarylwjglopenal:20100824"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/soundsystem/20120107/soundsystem-20120107.jar",
+ "sha1": "419c05fe9be71f792b2d76cfc9b67f1ed0fec7f6",
+ "size": 65020,
+ "url": "https://libraries.minecraft.net/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar"
+ }
+ },
+ "name": "com.paulscode:soundsystem:20120107"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar",
+ "sha1": "0294104aaf1781d6a56a07d561e792c5d0c95f45",
+ "size": 1779991,
+ "url": "https://libraries.minecraft.net/io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar"
+ }
+ },
+ "name": "io.netty:netty-all:4.0.23.Final"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/google/guava/guava/17.0/guava-17.0.jar",
+ "sha1": "9c6ef172e8de35fd8d4d8783e4821e57cdef7445",
+ "size": 2243036,
+ "url": "https://libraries.minecraft.net/com/google/guava/guava/17.0/guava-17.0.jar"
+ }
+ },
+ "name": "com.google.guava:guava:17.0"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar",
+ "sha1": "90a3822c38ec8c996e84c16a3477ef632cbc87a3",
+ "size": 412739,
+ "url": "https://libraries.minecraft.net/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar"
+ }
+ },
+ "name": "org.apache.commons:commons-lang3:3.3.2"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "commons-io/commons-io/2.4/commons-io-2.4.jar",
+ "sha1": "b1b6ea3b7e4aa4f492509a4952029cd8e48019ad",
+ "size": 185140,
+ "url": "https://libraries.minecraft.net/commons-io/commons-io/2.4/commons-io-2.4.jar"
+ }
+ },
+ "name": "commons-io:commons-io:2.4"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "commons-codec/commons-codec/1.9/commons-codec-1.9.jar",
+ "sha1": "9ce04e34240f674bc72680f8b843b1457383161a",
+ "size": 263965,
+ "url": "https://libraries.minecraft.net/commons-codec/commons-codec/1.9/commons-codec-1.9.jar"
+ }
+ },
+ "name": "commons-codec:commons-codec:1.9"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar",
+ "sha1": "39c7796b469a600f72380316f6b1f11db6c2c7c4",
+ "size": 208338,
+ "url": "https://libraries.minecraft.net/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar"
+ }
+ },
+ "name": "net.java.jinput:jinput:2.0.5"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar",
+ "sha1": "e12fe1fda814bd348c1579329c86943d2cd3c6a6",
+ "size": 7508,
+ "url": "https://libraries.minecraft.net/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar"
+ }
+ },
+ "name": "net.java.jutils:jutils:1.0.0"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/google/code/gson/gson/2.2.4/gson-2.2.4.jar",
+ "sha1": "a60a5e993c98c864010053cb901b7eab25306568",
+ "size": 190432,
+ "url": "https://libraries.minecraft.net/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar"
+ }
+ },
+ "name": "com.google.code.gson:gson:2.2.4"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/mojang/authlib/1.5.22/authlib-1.5.22.jar",
+ "sha1": "afaa8f6df976fcb5520e76ef1d5798c9e6b5c0b2",
+ "size": 64539,
+ "url": "https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar"
+ }
+ },
+ "name": "com.mojang:authlib:1.5.22"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/mojang/realms/1.8.4/realms-1.8.4.jar",
+ "sha1": "15f8dc326c97a96dee6e65392e145ad6d1cb46cb",
+ "size": 1131574,
+ "url": "https://libraries.minecraft.net/com/mojang/realms/1.8.4/realms-1.8.4.jar"
+ }
+ },
+ "name": "com.mojang:realms:1.8.4"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar",
+ "sha1": "a698750c16740fd5b3871425f4cb3bbaa87f529d",
+ "size": 365552,
+ "url": "https://libraries.minecraft.net/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar"
+ }
+ },
+ "name": "org.apache.commons:commons-compress:1.8.1"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar",
+ "sha1": "18f4247ff4572a074444572cee34647c43e7c9c7",
+ "size": 589512,
+ "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar"
+ }
+ },
+ "name": "org.apache.httpcomponents:httpclient:4.3.3"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar",
+ "sha1": "f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f",
+ "size": 62050,
+ "url": "https://libraries.minecraft.net/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar"
+ }
+ },
+ "name": "commons-logging:commons-logging:1.1.3"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar",
+ "sha1": "31fbbff1ddbf98f3aa7377c94d33b0447c646b6e",
+ "size": 282269,
+ "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar"
+ }
+ },
+ "name": "org.apache.httpcomponents:httpcore:4.3.2"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar",
+ "sha1": "1dd66e68cccd907880229f9e2de1314bd13ff785",
+ "size": 108161,
+ "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar"
+ }
+ },
+ "name": "org.apache.logging.log4j:log4j-api:2.0-beta9"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar",
+ "sha1": "678861ba1b2e1fccb594bb0ca03114bb05da9695",
+ "size": 681134,
+ "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar"
+ }
+ },
+ "name": "org.apache.logging.log4j:log4j-core:2.0-beta9"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar",
+ "sha1": "697517568c68e78ae0b4544145af031c81082dfe",
+ "size": 1047168,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar"
+ }
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209",
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar",
+ "sha1": "d51a7c040a721d13efdfbd34f8b257b2df882ad0",
+ "size": 173887,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar"
+ }
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209",
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar",
+ "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33",
+ "size": 22,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar"
+ },
+ "classifiers": {
+ "natives-linux": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar",
+ "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b",
+ "size": 578680,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar"
+ },
+ "natives-osx": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar",
+ "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed",
+ "size": 426822,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"
+ },
+ "natives-windows": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar",
+ "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0",
+ "size": 613748,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar",
+ "sha1": "7707204c9ffa5d91662de95f0a224e2f721b22af",
+ "size": 1045632,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar"
+ }
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar",
+ "sha1": "f0e612c840a7639c1f77f68d72a28dae2f0c8490",
+ "size": 173887,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar"
+ }
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "classifiers": {
+ "natives-linux": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar",
+ "sha1": "d898a33b5d0a6ef3fed3a4ead506566dce6720a5",
+ "size": 578539,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar"
+ },
+ "natives-osx": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar",
+ "sha1": "79f5ce2fea02e77fe47a3c745219167a542121d7",
+ "size": 468116,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar"
+ },
+ "natives-windows": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar",
+ "sha1": "78b2a55ce4dc29c6b3ec4df8ca165eba05f9b341",
+ "size": 613680,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "classifiers": {
+ "natives-linux": {
+ "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar",
+ "sha1": "7ff832a6eb9ab6a767f1ade2b548092d0fa64795",
+ "size": 10362,
+ "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar"
+ },
+ "natives-osx": {
+ "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar",
+ "sha1": "53f9c919f34d2ca9de8c51fc4e1e8282029a9232",
+ "size": 12186,
+ "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar"
+ },
+ "natives-windows": {
+ "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar",
+ "sha1": "385ee093e01f587f30ee1c8a2ee7d408fd732e16",
+ "size": 155179,
+ "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "net.java.jinput:jinput-platform:2.0.5",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ }
+ }
+ ],
+ "mainClass": "net.minecraft.client.main.Main",
+ "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}",
+ "minimumLauncherVersion": 18,
+ "releaseTime": "2016-02-29T13:49:54+00:00",
+ "time": "2016-03-01T13:14:53+00:00",
+ "type": "release"
+}
diff --git a/launcher/minecraft/testdata/codecwav-20101023.jar b/launcher/minecraft/testdata/codecwav-20101023.jar
new file mode 100644
index 00000000..f5236083
--- /dev/null
+++ b/launcher/minecraft/testdata/codecwav-20101023.jar
@@ -0,0 +1 @@
+dummy test file.
diff --git a/launcher/minecraft/testdata/lib-native-arch.json b/launcher/minecraft/testdata/lib-native-arch.json
new file mode 100644
index 00000000..501826ae
--- /dev/null
+++ b/launcher/minecraft/testdata/lib-native-arch.json
@@ -0,0 +1,46 @@
+{
+ "downloads": {
+ "classifiers": {
+ "natives-osx": {
+ "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar",
+ "sha1": "62503ee712766cf77f97252e5902786fd834b8c5",
+ "size": 418331,
+ "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar"
+ },
+ "natives-windows-32": {
+ "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar",
+ "sha1": "7c6affe439099806a4f552da14c42f9d643d8b23",
+ "size": 386792,
+ "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar"
+ },
+ "natives-windows-64": {
+ "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar",
+ "sha1": "39d0c3d363735b4785598e0e7fbf8297c706a9f9",
+ "size": 463390,
+ "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "tv.twitch:twitch-platform:5.16",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows-${arch}"
+ },
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "linux"
+ }
+ }
+ ]
+}
diff --git a/launcher/minecraft/testdata/lib-native.json b/launcher/minecraft/testdata/lib-native.json
new file mode 100644
index 00000000..5b9f3b55
--- /dev/null
+++ b/launcher/minecraft/testdata/lib-native.json
@@ -0,0 +1,52 @@
+{
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar",
+ "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33",
+ "size": 22,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar"
+ },
+ "classifiers": {
+ "natives-linux": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar",
+ "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b",
+ "size": 578680,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar"
+ },
+ "natives-osx": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar",
+ "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed",
+ "size": 426822,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"
+ },
+ "natives-windows": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar",
+ "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0",
+ "size": 613748,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+}
diff --git a/launcher/minecraft/testdata/lib-simple.json b/launcher/minecraft/testdata/lib-simple.json
new file mode 100644
index 00000000..90bbff07
--- /dev/null
+++ b/launcher/minecraft/testdata/lib-simple.json
@@ -0,0 +1,11 @@
+{
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar",
+ "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da",
+ "size": 5618,
+ "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar"
+ }
+ },
+ "name": "com.paulscode:codecwav:20101023"
+}
diff --git a/launcher/minecraft/testdata/testname-testversion-linux-32.jar b/launcher/minecraft/testdata/testname-testversion-linux-32.jar
new file mode 100644
index 00000000..f5236083
--- /dev/null
+++ b/launcher/minecraft/testdata/testname-testversion-linux-32.jar
@@ -0,0 +1 @@
+dummy test file.
diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp
new file mode 100644
index 00000000..e26ab4ef
--- /dev/null
+++ b/launcher/minecraft/update/AssetUpdateTask.cpp
@@ -0,0 +1,107 @@
+#include "Env.h"
+#include "AssetUpdateTask.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "net/ChecksumValidator.h"
+#include "minecraft/AssetsUtils.h"
+
+AssetUpdateTask::AssetUpdateTask(MinecraftInstance * inst)
+{
+ m_inst = inst;
+}
+
+AssetUpdateTask::~AssetUpdateTask()
+{
+}
+
+void AssetUpdateTask::executeTask()
+{
+ setStatus(tr("Updating assets index..."));
+ auto components = m_inst->getPackProfile();
+ auto profile = components->getProfile();
+ auto assets = profile->getMinecraftAssets();
+ QUrl indexUrl = assets->url;
+ QString localPath = assets->id + ".json";
+ auto job = new NetJob(tr("Asset index for %1").arg(m_inst->name()));
+
+ auto metacache = ENV.metacache();
+ auto entry = metacache->resolveEntry("asset_indexes", localPath);
+ entry->setStale(true);
+ auto hexSha1 = assets->sha1.toLatin1();
+ qDebug() << "Asset index SHA1:" << hexSha1;
+ auto dl = Net::Download::makeCached(indexUrl, entry);
+ auto rawSha1 = QByteArray::fromHex(assets->sha1.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ job->addNetAction(dl);
+
+ downloadJob.reset(job);
+
+ connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::assetIndexFinished);
+ connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed);
+ connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress);
+
+ qDebug() << m_inst->name() << ": Starting asset index download";
+ downloadJob->start();
+}
+
+bool AssetUpdateTask::canAbort() const
+{
+ return true;
+}
+
+void AssetUpdateTask::assetIndexFinished()
+{
+ AssetsIndex index;
+ qDebug() << m_inst->name() << ": Finished asset index download";
+
+ auto components = m_inst->getPackProfile();
+ auto profile = components->getProfile();
+ auto assets = profile->getMinecraftAssets();
+
+ QString asset_fname = "assets/indexes/" + assets->id + ".json";
+ // FIXME: this looks like a job for a generic validator based on json schema?
+ if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index))
+ {
+ 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..."));
+ downloadJob = job;
+ connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded);
+ connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed);
+ connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress);
+ downloadJob->start();
+ return;
+ }
+ emitSucceeded();
+}
+
+void AssetUpdateTask::assetIndexFailed(QString reason)
+{
+ qDebug() << m_inst->name() << ": Failed asset index download";
+ emitFailed(tr("Failed to download the assets index:\n%1").arg(reason));
+}
+
+void AssetUpdateTask::assetsFailed(QString reason)
+{
+ emitFailed(tr("Failed to download assets:\n%1").arg(reason));
+}
+
+bool AssetUpdateTask::abort()
+{
+ if(downloadJob)
+ {
+ return downloadJob->abort();
+ }
+ else
+ {
+ qWarning() << "Prematurely aborted AssetUpdateTask";
+ }
+ return true;
+}
diff --git a/launcher/minecraft/update/AssetUpdateTask.h b/launcher/minecraft/update/AssetUpdateTask.h
new file mode 100644
index 00000000..fdfa8f1c
--- /dev/null
+++ b/launcher/minecraft/update/AssetUpdateTask.h
@@ -0,0 +1,28 @@
+#pragma once
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+class MinecraftInstance;
+
+class AssetUpdateTask : public Task
+{
+ Q_OBJECT
+public:
+ AssetUpdateTask(MinecraftInstance * inst);
+ virtual ~AssetUpdateTask();
+
+ void executeTask() override;
+
+ bool canAbort() const override;
+
+private slots:
+ void assetIndexFinished();
+ void assetIndexFailed(QString reason);
+ void assetsFailed(QString reason);
+
+public slots:
+ bool abort() override;
+
+private:
+ MinecraftInstance *m_inst;
+ NetJobPtr downloadJob;
+};
diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp
new file mode 100644
index 00000000..a05a7c2a
--- /dev/null
+++ b/launcher/minecraft/update/FMLLibrariesTask.cpp
@@ -0,0 +1,131 @@
+#include "Env.h"
+#include <FileSystem.h>
+#include <minecraft/VersionFilterData.h>
+#include "FMLLibrariesTask.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "BuildConfig.h"
+
+FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance * inst)
+{
+ m_inst = inst;
+}
+void FMLLibrariesTask::executeTask()
+{
+ // Get the mod list
+ MinecraftInstance *inst = (MinecraftInstance *)m_inst;
+ auto components = inst->getPackProfile();
+ auto profile = components->getProfile();
+
+ if (!profile->hasTrait("legacyFML"))
+ {
+ emitSucceeded();
+ return;
+ }
+
+ QString version = components->getComponentVersion("net.minecraft");
+ auto &fmlLibsMapping = g_VersionFilterData.fmlLibsMapping;
+ if (!fmlLibsMapping.contains(version))
+ {
+ emitSucceeded();
+ return;
+ }
+
+ auto &libList = fmlLibsMapping[version];
+
+ // determine if we need some libs for FML or forge
+ setStatus(tr("Checking for FML libraries..."));
+ if(!components->getComponent("net.minecraftforge"))
+ {
+ emitSucceeded();
+ 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())
+ {
+ emitSucceeded();
+ 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 = BuildConfig.FMLLIBS_BASE_URL + lib.filename;
+ dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry));
+ }
+
+ connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished);
+ connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed);
+ connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress);
+ downloadJob.reset(dljob);
+ downloadJob->start();
+}
+
+bool FMLLibrariesTask::canAbort() const
+{
+ return true;
+}
+
+void FMLLibrariesTask::fmllibsFinished()
+{
+ downloadJob.reset();
+ if (!fmlLibsToProcess.isEmpty())
+ {
+ setStatus(tr("Copying FML libraries into the instance..."));
+ MinecraftInstance *inst = (MinecraftInstance *)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());
+ }
+ emitSucceeded();
+}
+void FMLLibrariesTask::fmllibsFailed(QString reason)
+{
+ QStringList failed = downloadJob->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));
+}
+
+bool FMLLibrariesTask::abort()
+{
+ if(downloadJob)
+ {
+ return downloadJob->abort();
+ }
+ else
+ {
+ qWarning() << "Prematurely aborted FMLLibrariesTask";
+ }
+ return true;
+}
diff --git a/launcher/minecraft/update/FMLLibrariesTask.h b/launcher/minecraft/update/FMLLibrariesTask.h
new file mode 100644
index 00000000..a1e70ed4
--- /dev/null
+++ b/launcher/minecraft/update/FMLLibrariesTask.h
@@ -0,0 +1,31 @@
+#pragma once
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include "minecraft/VersionFilterData.h"
+
+class MinecraftInstance;
+
+class FMLLibrariesTask : public Task
+{
+ Q_OBJECT
+public:
+ FMLLibrariesTask(MinecraftInstance * inst);
+ virtual ~FMLLibrariesTask() {};
+
+ void executeTask() override;
+
+ bool canAbort() const override;
+
+private slots:
+ void fmllibsFinished();
+ void fmllibsFailed(QString reason);
+
+public slots:
+ bool abort() override;
+
+private:
+ MinecraftInstance *m_inst;
+ NetJobPtr downloadJob;
+ QList<FMLlib> fmlLibsToProcess;
+};
+
diff --git a/launcher/minecraft/update/FoldersTask.cpp b/launcher/minecraft/update/FoldersTask.cpp
new file mode 100644
index 00000000..e2b1bb48
--- /dev/null
+++ b/launcher/minecraft/update/FoldersTask.cpp
@@ -0,0 +1,21 @@
+#include "FoldersTask.h"
+#include "minecraft/MinecraftInstance.h"
+#include <QDir>
+
+FoldersTask::FoldersTask(MinecraftInstance * inst)
+ :Task()
+{
+ m_inst = inst;
+}
+
+void FoldersTask::executeTask()
+{
+ // Make directories
+ QDir mcDir(m_inst->gameRoot());
+ if (!mcDir.exists() && !mcDir.mkpath("."))
+ {
+ emitFailed(tr("Failed to create folder for minecraft binaries."));
+ return;
+ }
+ emitSucceeded();
+}
diff --git a/launcher/minecraft/update/FoldersTask.h b/launcher/minecraft/update/FoldersTask.h
new file mode 100644
index 00000000..f6ed5e6d
--- /dev/null
+++ b/launcher/minecraft/update/FoldersTask.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "tasks/Task.h"
+
+class MinecraftInstance;
+class FoldersTask : public Task
+{
+ Q_OBJECT
+public:
+ FoldersTask(MinecraftInstance * inst);
+ virtual ~FoldersTask() {};
+
+ void executeTask() override;
+private:
+ MinecraftInstance *m_inst;
+};
+
diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp
new file mode 100644
index 00000000..7f66a651
--- /dev/null
+++ b/launcher/minecraft/update/LibrariesTask.cpp
@@ -0,0 +1,90 @@
+#include "Env.h"
+#include "LibrariesTask.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+LibrariesTask::LibrariesTask(MinecraftInstance * inst)
+{
+ m_inst = inst;
+}
+
+void LibrariesTask::executeTask()
+{
+ setStatus(tr("Getting the library files from Mojang..."));
+ qDebug() << m_inst->name() << ": downloading libraries";
+ MinecraftInstance *inst = (MinecraftInstance *)m_inst;
+
+ // Build a list of URLs that will need to be downloaded.
+ auto components = inst->getPackProfile();
+ auto profile = components->getProfile();
+
+ auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name()));
+ downloadJob.reset(job);
+
+ auto metacache = ENV.metacache();
+
+ auto processArtifactPool = [&](const QList<LibraryPtr> & pool, QStringList & errors, const QString & localPath)
+ {
+ for (auto lib : pool)
+ {
+ if(!lib)
+ {
+ emitFailed(tr("Null jar is specified in the metadata, aborting."));
+ return false;
+ }
+ auto dls = lib->getDownloads(currentSystem, metacache.get(), errors, localPath);
+ for(auto dl : dls)
+ {
+ downloadJob->addNetAction(dl);
+ }
+ }
+ return true;
+ };
+
+ QStringList failedLocalLibraries;
+ QList<LibraryPtr> libArtifactPool;
+ libArtifactPool.append(profile->getLibraries());
+ libArtifactPool.append(profile->getNativeLibraries());
+ libArtifactPool.append(profile->getMavenFiles());
+ libArtifactPool.append(profile->getMainJar());
+ processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath());
+
+ QStringList failedLocalJarMods;
+ processArtifactPool(profile->getJarMods(), failedLocalJarMods, inst->jarModsDir());
+
+ if (!failedLocalJarMods.empty() || !failedLocalLibraries.empty())
+ {
+ downloadJob.reset();
+ QString failed_all = (failedLocalLibraries + failedLocalJarMods).join("\n");
+ emitFailed(tr("Some artifacts marked as 'local' are missing their files:\n%1\n\nYou need to either add the files, or removed the packages that require them.\nYou'll have to correct this problem manually.").arg(failed_all));
+ return;
+ }
+
+ connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded);
+ connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed);
+ connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress);
+ downloadJob->start();
+}
+
+bool LibrariesTask::canAbort() const
+{
+ return true;
+}
+
+void LibrariesTask::jarlibFailed(QString reason)
+{
+ emitFailed(tr("Game update failed: it was impossible to fetch the required libraries.\nReason:\n%1").arg(reason));
+}
+
+bool LibrariesTask::abort()
+{
+ if(downloadJob)
+ {
+ return downloadJob->abort();
+ }
+ else
+ {
+ qWarning() << "Prematurely aborted LibrariesTask";
+ }
+ return true;
+}
diff --git a/launcher/minecraft/update/LibrariesTask.h b/launcher/minecraft/update/LibrariesTask.h
new file mode 100644
index 00000000..49f76932
--- /dev/null
+++ b/launcher/minecraft/update/LibrariesTask.h
@@ -0,0 +1,26 @@
+#pragma once
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+class MinecraftInstance;
+
+class LibrariesTask : public Task
+{
+ Q_OBJECT
+public:
+ LibrariesTask(MinecraftInstance * inst);
+ virtual ~LibrariesTask() {};
+
+ void executeTask() override;
+
+ bool canAbort() const override;
+
+private slots:
+ void jarlibFailed(QString reason);
+
+public slots:
+ bool abort() override;
+
+private:
+ MinecraftInstance *m_inst;
+ NetJobPtr downloadJob;
+};
diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/launcher/modplatform/atlauncher/ATLPackIndex.cpp
new file mode 100644
index 00000000..35f50b18
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackIndex.cpp
@@ -0,0 +1,33 @@
+#include "ATLPackIndex.h"
+
+#include <QRegularExpression>
+
+#include "Json.h"
+
+static void loadIndexedVersion(ATLauncher::IndexedVersion & v, QJsonObject & obj)
+{
+ v.version = Json::requireString(obj, "version");
+ v.minecraft = Json::requireString(obj, "minecraft");
+}
+
+void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack & m, QJsonObject & obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.position = Json::requireInteger(obj, "position");
+ m.name = Json::requireString(obj, "name");
+ m.type = Json::requireString(obj, "type") == "private" ?
+ ATLauncher::PackType::Private :
+ ATLauncher::PackType::Public;
+ auto versionsArr = Json::requireArray(obj, "versions");
+ for (const auto versionRaw : versionsArr)
+ {
+ auto versionObj = Json::requireObject(versionRaw);
+ ATLauncher::IndexedVersion version;
+ loadIndexedVersion(version, versionObj);
+ m.versions.append(version);
+ }
+ m.system = Json::ensureBoolean(obj, QString("system"), false);
+ m.description = Json::ensureString(obj, "description", "");
+
+ m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "");
+}
diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.h b/launcher/modplatform/atlauncher/ATLPackIndex.h
new file mode 100644
index 00000000..405a3448
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackIndex.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "ATLPackManifest.h"
+
+#include <QString>
+#include <QVector>
+#include <QMetaType>
+
+namespace ATLauncher
+{
+
+struct IndexedVersion
+{
+ QString version;
+ QString minecraft;
+};
+
+struct IndexedPack
+{
+ int id;
+ int position;
+ QString name;
+ PackType type;
+ QVector<IndexedVersion> versions;
+ bool system;
+ QString description;
+
+ QString safeName;
+};
+
+void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
+}
+
+Q_DECLARE_METATYPE(ATLauncher::IndexedPack)
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
new file mode 100644
index 00000000..55087a27
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -0,0 +1,764 @@
+#include <Env.h>
+#include <quazip.h>
+#include <QtConcurrent/QtConcurrent>
+#include <MMCZip.h>
+#include <minecraft/OneSixVersionFormat.h>
+#include <Version.h>
+#include <net/ChecksumValidator.h>
+#include "ATLPackInstallTask.h"
+
+#include "BuildConfig.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "settings/INISettingsObject.h"
+#include "meta/Index.h"
+#include "meta/Version.h"
+#include "meta/VersionList.h"
+
+namespace ATLauncher {
+
+PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version)
+{
+ m_support = support;
+ m_pack = pack;
+ m_version_name = version;
+}
+
+bool PackInstallTask::abort()
+{
+ if(abortable)
+ {
+ return jobPtr->abort();
+ }
+ return false;
+}
+
+void PackInstallTask::executeTask()
+{
+ qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId();
+ auto *netJob = new NetJob("ATLauncher::VersionFetch");
+ auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json")
+ .arg(m_pack).arg(m_version_name);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
+ QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+}
+
+void PackInstallTask::onDownloadSucceeded()
+{
+ qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId();
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ATLauncher::PackVersion version;
+ try
+ {
+ ATLauncher::loadVersion(version, obj);
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ return;
+ }
+ m_version = version;
+
+ auto vlist = ENV.metadataIndex()->get("net.minecraft");
+ if(!vlist)
+ {
+ emitFailed(tr("Failed to get local metadata index for %1").arg("net.minecraft"));
+ return;
+ }
+
+ auto ver = vlist->getVersion(m_version.minecraft);
+ if (!ver) {
+ emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft").arg(m_version.minecraft));
+ return;
+ }
+ ver->load(Net::Mode::Online);
+ minecraftVersion = ver;
+
+ if(m_version.noConfigs) {
+ downloadMods();
+ }
+ else {
+ installConfigs();
+ }
+}
+
+void PackInstallTask::onDownloadFailed(QString reason)
+{
+ qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId();
+ jobPtr.reset();
+ emitFailed(reason);
+}
+
+QString PackInstallTask::getDirForModType(ModType type, QString raw)
+{
+ switch (type) {
+ // Mod types that can either be ignored at this stage, or ignored
+ // completely.
+ case ModType::Root:
+ case ModType::Extract:
+ case ModType::Decomp:
+ case ModType::TexturePackExtract:
+ case ModType::ResourcePackExtract:
+ case ModType::MCPC:
+ return Q_NULLPTR;
+ case ModType::Forge:
+ // Forge detection happens later on, if it cannot be detected it will
+ // install a jarmod component.
+ case ModType::Jar:
+ return "jarmods";
+ case ModType::Mods:
+ return "mods";
+ case ModType::Flan:
+ return "Flan";
+ case ModType::Dependency:
+ return FS::PathCombine("mods", m_version.minecraft);
+ case ModType::Ic2Lib:
+ return FS::PathCombine("mods", "ic2");
+ case ModType::DenLib:
+ return FS::PathCombine("mods", "denlib");
+ case ModType::Coremods:
+ return "coremods";
+ case ModType::Plugins:
+ return "plugins";
+ case ModType::TexturePack:
+ return "texturepacks";
+ case ModType::ResourcePack:
+ return "resourcepacks";
+ case ModType::ShaderPack:
+ return "shaderpacks";
+ case ModType::Millenaire:
+ qWarning() << "Unsupported mod type: " + raw;
+ return Q_NULLPTR;
+ case ModType::Unknown:
+ emitFailed(tr("Unknown mod type: %1").arg(raw));
+ return Q_NULLPTR;
+ }
+
+ return Q_NULLPTR;
+}
+
+QString PackInstallTask::getVersionForLoader(QString uid)
+{
+ if(m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) {
+ auto vlist = ENV.metadataIndex()->get(uid);
+ if(!vlist)
+ {
+ emitFailed(tr("Failed to get local metadata index for %1").arg(uid));
+ return Q_NULLPTR;
+ }
+
+ if(!vlist->isLoaded()) {
+ vlist->load(Net::Mode::Online);
+ }
+
+ if(m_version.loader.recommended || m_version.loader.latest) {
+ for (int i = 0; i < vlist->versions().size(); i++) {
+ auto version = vlist->versions().at(i);
+ auto reqs = version->requires();
+
+ // filter by minecraft version, if the loader depends on a certain version.
+ // not all mod loaders depend on a given Minecraft version, so we won't do this
+ // filtering for those loaders.
+ if (m_version.loader.type != "fabric") {
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) {
+ return req.uid == "net.minecraft";
+ });
+ if (iter == reqs.end()) continue;
+ if (iter->equalsVersion != m_version.minecraft) continue;
+ }
+
+ if (m_version.loader.recommended) {
+ // first recommended build we find, we use.
+ if (!version->isRecommended()) continue;
+ }
+
+ return version->descriptor();
+ }
+
+ emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type));
+ return Q_NULLPTR;
+ }
+ else if(m_version.loader.choose) {
+ // Fabric Loader doesn't depend on a given Minecraft version.
+ if (m_version.loader.type == "fabric") {
+ return m_support->chooseVersion(vlist, Q_NULLPTR);
+ }
+
+ return m_support->chooseVersion(vlist, m_version.minecraft);
+ }
+ }
+
+ if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) {
+ emitFailed(tr("No loader version set for modpack!"));
+ return Q_NULLPTR;
+ }
+
+ return m_version.loader.version;
+}
+
+QString PackInstallTask::detectLibrary(VersionLibrary library)
+{
+ // Try to detect what the library is
+ if (!library.server.isEmpty() && library.server.split("/").length() >= 3) {
+ auto lastSlash = library.server.lastIndexOf("/");
+ auto locationAndVersion = library.server.mid(0, lastSlash);
+ auto fileName = library.server.mid(lastSlash + 1);
+
+ lastSlash = locationAndVersion.lastIndexOf("/");
+ auto location = locationAndVersion.mid(0, lastSlash);
+ auto version = locationAndVersion.mid(lastSlash + 1);
+
+ lastSlash = location.lastIndexOf("/");
+ auto group = location.mid(0, lastSlash).replace("/", ".");
+ auto artefact = location.mid(lastSlash + 1);
+
+ return group + ":" + artefact + ":" + version;
+ }
+
+ if(library.file.contains("-")) {
+ auto lastSlash = library.file.lastIndexOf("-");
+ auto name = library.file.mid(0, lastSlash);
+ auto version = library.file.mid(lastSlash + 1).remove(".jar");
+
+ if(name == QString("guava")) {
+ return "com.google.guava:guava:" + version;
+ }
+ else if(name == QString("commons-lang3")) {
+ return "org.apache.commons:commons-lang3:" + version;
+ }
+ }
+
+ return "org.multimc.atlauncher:" + library.md5 + ":1";
+}
+
+bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile)
+{
+ if(m_version.libraries.isEmpty()) {
+ return true;
+ }
+
+ QList<GradleSpecifier> exempt;
+ for(const auto & componentUid : componentsToInstall.keys()) {
+ auto componentVersion = componentsToInstall.value(componentUid);
+
+ for(const auto & library : componentVersion->data()->libraries) {
+ GradleSpecifier lib(library->rawName());
+ exempt.append(lib);
+ }
+ }
+
+ {
+ for(const auto & library : minecraftVersion->data()->libraries) {
+ GradleSpecifier lib(library->rawName());
+ exempt.append(lib);
+ }
+ }
+
+ auto uuid = QUuid::createUuid();
+ auto id = uuid.toString().remove('{').remove('}');
+ auto target_id = "org.multimc.atlauncher." + id;
+
+ auto patchDir = FS::PathCombine(instanceRoot, "patches");
+ if(!FS::ensureFolderPathExists(patchDir))
+ {
+ return false;
+ }
+ auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+
+ auto f = std::make_shared<VersionFile>();
+ f->name = m_pack + " " + m_version_name + " (libraries)";
+
+ for(const auto & lib : m_version.libraries) {
+ auto libName = detectLibrary(lib);
+ GradleSpecifier libSpecifier(libName);
+
+ bool libExempt = false;
+ for(const auto & existingLib : exempt) {
+ if(libSpecifier.matchName(existingLib)) {
+ // If the pack specifies a newer version of the lib, use that!
+ libExempt = Version(libSpecifier.version()) >= Version(existingLib.version());
+ }
+ }
+ if(libExempt) continue;
+
+ auto library = std::make_shared<Library>();
+ library->setRawName(libName);
+
+ switch(lib.download) {
+ case DownloadType::Server:
+ library->setAbsoluteUrl(BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url);
+ break;
+ case DownloadType::Direct:
+ library->setAbsoluteUrl(lib.url);
+ break;
+ case DownloadType::Browser:
+ case DownloadType::Unknown:
+ emitFailed(tr("Unknown or unsupported download type: %1").arg(lib.download_raw));
+ return false;
+ }
+
+ f->libraries.append(library);
+ }
+
+ if(f->libraries.isEmpty()) {
+ return 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).toJson());
+ file.close();
+
+ profile->appendComponent(new Component(profile.get(), target_id, f));
+ return true;
+}
+
+bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile)
+{
+ if(m_version.mainClass == QString() && m_version.extraArguments == QString()) {
+ return true;
+ }
+
+ auto uuid = QUuid::createUuid();
+ auto id = uuid.toString().remove('{').remove('}');
+ auto target_id = "org.multimc.atlauncher." + id;
+
+ auto patchDir = FS::PathCombine(instanceRoot, "patches");
+ if(!FS::ensureFolderPathExists(patchDir))
+ {
+ return false;
+ }
+ auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+
+ QStringList mainClasses;
+ QStringList tweakers;
+ for(const auto & componentUid : componentsToInstall.keys()) {
+ auto componentVersion = componentsToInstall.value(componentUid);
+
+ if(componentVersion->data()->mainClass != QString("")) {
+ mainClasses.append(componentVersion->data()->mainClass);
+ }
+ tweakers.append(componentVersion->data()->addTweakers);
+ }
+
+ auto f = std::make_shared<VersionFile>();
+ f->name = m_pack + " " + m_version_name;
+ if(m_version.mainClass != QString() && !mainClasses.contains(m_version.mainClass)) {
+ f->mainClass = m_version.mainClass;
+ }
+
+ // Parse out tweakers
+ auto args = m_version.extraArguments.split(" ");
+ QString previous;
+ for(auto arg : args) {
+ if(arg.startsWith("--tweakClass=") || previous == "--tweakClass") {
+ auto tweakClass = arg.remove("--tweakClass=");
+ if(tweakers.contains(tweakClass)) continue;
+
+ f->addTweakers.append(tweakClass);
+ }
+ previous = arg;
+ }
+
+ if(f->mainClass == QString() && f->addTweakers.isEmpty()) {
+ return 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).toJson());
+ file.close();
+
+ profile->appendComponent(new Component(profile.get(), target_id, f));
+ return true;
+}
+
+void PackInstallTask::installConfigs()
+{
+ qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId();
+ setStatus(tr("Downloading configs..."));
+ jobPtr.reset(new NetJob(tr("Config download")));
+
+ auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name);
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip")
+ .arg(m_pack).arg(m_version_name);
+ auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", path);
+ entry->setStale(true);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!m_version.configs.sha1.isEmpty()) {
+ auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ }
+ jobPtr->addNetAction(dl);
+ archivePath = entry->getFullPath();
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
+ {
+ abortable = false;
+ jobPtr.reset();
+ extractConfigs();
+ });
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
+ {
+ abortable = false;
+ jobPtr.reset();
+ emitFailed(reason);
+ });
+ connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
+ {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+}
+
+void PackInstallTask::extractConfigs()
+{
+ qDebug() << "PackInstallTask::extractConfigs: " << QThread::currentThreadId();
+ setStatus(tr("Extracting configs..."));
+
+ QDir extractDir(m_stagingPath);
+
+ QuaZip packZip(archivePath);
+ if(!packZip.open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Failed to open pack configs %1!").arg(archivePath));
+ return;
+ }
+
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft");
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [&]()
+ {
+ downloadMods();
+ });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]()
+ {
+ emitAborted();
+ });
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void PackInstallTask::downloadMods()
+{
+ qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId();
+
+ QVector<ATLauncher::VersionMod> optionalMods;
+ for (const auto& mod : m_version.mods) {
+ if (mod.optional) {
+ optionalMods.push_back(mod);
+ }
+ }
+
+ // Select optional mods, if pack contains any
+ QVector<QString> selectedMods;
+ if (!optionalMods.isEmpty()) {
+ setStatus(tr("Selecting optional mods..."));
+ selectedMods = m_support->chooseOptionalMods(optionalMods);
+ }
+
+ setStatus(tr("Downloading mods..."));
+
+ jarmods.clear();
+ jobPtr.reset(new NetJob(tr("Mod download")));
+ for(const auto& mod : m_version.mods) {
+ // skip non-client mods
+ if(!mod.client) continue;
+
+ // skip optional mods that were not selected
+ if(mod.optional && !selectedMods.contains(mod.name)) continue;
+
+ QString url;
+ switch(mod.download) {
+ case DownloadType::Server:
+ url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url;
+ break;
+ case DownloadType::Browser:
+ emitFailed(tr("Unsupported download type: %1").arg(mod.download_raw));
+ return;
+ case DownloadType::Direct:
+ url = mod.url;
+ break;
+ case DownloadType::Unknown:
+ emitFailed(tr("Unknown download type: %1").arg(mod.download_raw));
+ return;
+ }
+
+ QFileInfo fileName(mod.file);
+ auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix();
+
+ if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) {
+ auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName);
+ entry->setStale(true);
+ modsToExtract.insert(entry->getFullPath(), mod);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+ }
+ else if(mod.type == ModType::Decomp) {
+ auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName);
+ entry->setStale(true);
+ modsToDecomp.insert(entry->getFullPath(), mod);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+ }
+ else {
+ auto relpath = getDirForModType(mod.type, mod.type_raw);
+ if(relpath == Q_NULLPTR) continue;
+
+ auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName);
+ entry->setStale(true);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+
+ auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file);
+ qDebug() << "Will download" << url << "to" << path;
+ modsToCopy[entry->getFullPath()] = path;
+
+ if(mod.type == ModType::Forge) {
+ auto vlist = ENV.metadataIndex()->get("net.minecraftforge");
+ if(vlist)
+ {
+ auto ver = vlist->getVersion(mod.version);
+ if(ver) {
+ ver->load(Net::Mode::Online);
+ componentsToInstall.insert("net.minecraftforge", ver);
+ continue;
+ }
+ }
+
+ qDebug() << "Jarmod: " + path;
+ jarmods.push_back(path);
+ }
+
+ if(mod.type == ModType::Jar) {
+ qDebug() << "Jarmod: " + path;
+ jarmods.push_back(path);
+ }
+ }
+ }
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded);
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
+ {
+ abortable = false;
+ jobPtr.reset();
+ emitFailed(reason);
+ });
+ connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
+ {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+}
+
+void PackInstallTask::onModsDownloaded() {
+ abortable = false;
+
+ qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId();
+ jobPtr.reset();
+
+ if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) {
+ m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy);
+ connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onModsExtracted);
+ connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]()
+ {
+ emitAborted();
+ });
+ m_modExtractFutureWatcher.setFuture(m_modExtractFuture);
+ }
+ else {
+ install();
+ }
+}
+
+void PackInstallTask::onModsExtracted() {
+ qDebug() << "PackInstallTask::onModsExtracted: " << QThread::currentThreadId();
+ if(m_modExtractFuture.result()) {
+ install();
+ }
+ else {
+ emitFailed(tr("Failed to extract mods..."));
+ }
+}
+
+bool PackInstallTask::extractMods(
+ const QMap<QString, VersionMod> &toExtract,
+ const QMap<QString, VersionMod> &toDecomp,
+ const QMap<QString, QString> &toCopy
+) {
+ qDebug() << "PackInstallTask::extractMods: " << QThread::currentThreadId();
+
+ setStatus(tr("Extracting mods..."));
+ for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) {
+ auto &modPath = iter.key();
+ auto &mod = iter.value();
+
+ QString extractToDir;
+ if(mod.type == ModType::Extract) {
+ extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw);
+ }
+ else if(mod.type == ModType::TexturePackExtract) {
+ extractToDir = FS::PathCombine("texturepacks", "extracted");
+ }
+ else if(mod.type == ModType::ResourcePackExtract) {
+ extractToDir = FS::PathCombine("resourcepacks", "extracted");
+ }
+
+ QDir extractDir(m_stagingPath);
+ auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir);
+
+ QString folderToExtract = "";
+ if(mod.type == ModType::Extract) {
+ folderToExtract = mod.extractFolder;
+ folderToExtract.remove(QRegExp("^/"));
+ }
+
+ qDebug() << "Extracting " + mod.file + " to " + extractToDir;
+ if(!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) {
+ // assume error
+ return false;
+ }
+ }
+
+ for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) {
+ auto &modPath = iter.key();
+ auto &mod = iter.value();
+ auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw);
+
+ QDir extractDir(m_stagingPath);
+ auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile);
+
+ qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir;
+ if(!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) {
+ qWarning() << "Failed to extract" << mod.decompFile;
+ return false;
+ }
+ }
+
+ for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) {
+ auto &from = iter.key();
+ auto &to = iter.value();
+ FS::copy fileCopyOperation(from, to);
+ if(!fileCopyOperation()) {
+ qWarning() << "Failed to copy" << from << "to" << to;
+ return false;
+ }
+ }
+ return true;
+}
+
+void PackInstallTask::install()
+{
+ qDebug() << "PackInstallTask::install: " << QThread::currentThreadId();
+ setStatus(tr("Installing modpack"));
+
+ auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+
+ MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ // Use a component to add libraries BEFORE Minecraft
+ if(!createLibrariesComponent(instance.instanceRoot(), components)) {
+ emitFailed(tr("Failed to create libraries component"));
+ return;
+ }
+
+ // Minecraft
+ components->setComponentVersion("net.minecraft", m_version.minecraft, true);
+
+ // Loader
+ if(m_version.loader.type == QString("forge"))
+ {
+ auto version = getVersionForLoader("net.minecraftforge");
+ if(version == Q_NULLPTR) return;
+
+ components->setComponentVersion("net.minecraftforge", version, true);
+ }
+ else if(m_version.loader.type == QString("fabric"))
+ {
+ auto version = getVersionForLoader("net.fabricmc.fabric-loader");
+ if(version == Q_NULLPTR) return;
+
+ components->setComponentVersion("net.fabricmc.fabric-loader", version, true);
+ }
+ else if(m_version.loader.type != QString())
+ {
+ emitFailed(tr("Unknown loader type: ") + m_version.loader.type);
+ return;
+ }
+
+ for(const auto & componentUid : componentsToInstall.keys()) {
+ auto version = componentsToInstall.value(componentUid);
+ components->setComponentVersion(componentUid, version->version());
+ }
+
+ components->installJarMods(jarmods);
+
+ // Use a component to fill in the rest of the data
+ // todo: use more detection
+ if(!createPackComponent(instance.instanceRoot(), components)) {
+ emitFailed(tr("Failed to create pack component"));
+ return;
+ }
+
+ components->saveNow();
+
+ instance.setName(m_instName);
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ jarmods.clear();
+ emitSucceeded();
+}
+
+}
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
new file mode 100644
index 00000000..39e2b013
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -0,0 +1,101 @@
+#pragma once
+
+#include <meta/VersionList.h>
+#include "ATLPackManifest.h"
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+#include "settings/INISettingsObject.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "meta/Version.h"
+
+#include <nonstd/optional>
+
+namespace ATLauncher {
+
+class UserInteractionSupport {
+
+public:
+ /**
+ * Requests a user interaction to select which optional mods should be installed.
+ */
+ virtual QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) = 0;
+
+ /**
+ * Requests a user interaction to select a component version from a given version list
+ * and constrained to a given Minecraft version.
+ */
+ virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0;
+
+};
+
+class PackInstallTask : public InstanceTask
+{
+Q_OBJECT
+
+public:
+ explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version);
+ virtual ~PackInstallTask(){}
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+protected:
+ virtual void executeTask() override;
+
+private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+
+ void onModsDownloaded();
+ void onModsExtracted();
+
+private:
+ QString getDirForModType(ModType type, QString raw);
+ QString getVersionForLoader(QString uid);
+ QString detectLibrary(VersionLibrary library);
+
+ bool createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+ bool createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+
+ void installConfigs();
+ void extractConfigs();
+ void downloadMods();
+ bool extractMods(
+ const QMap<QString, VersionMod> &toExtract,
+ const QMap<QString, VersionMod> &toDecomp,
+ const QMap<QString, QString> &toCopy
+ );
+ void install();
+
+private:
+ UserInteractionSupport *m_support;
+
+ bool abortable = false;
+
+ NetJobPtr jobPtr;
+ QByteArray response;
+
+ QString m_pack;
+ QString m_version_name;
+ PackVersion m_version;
+
+ QMap<QString, VersionMod> modsToExtract;
+ QMap<QString, VersionMod> modsToDecomp;
+ QMap<QString, QString> modsToCopy;
+
+ QString archivePath;
+ QStringList jarmods;
+ Meta::VersionPtr minecraftVersion;
+ QMap<QString, Meta::VersionPtr> componentsToInstall;
+
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+
+ QFuture<bool> m_modExtractFuture;
+ QFutureWatcher<bool> m_modExtractFutureWatcher;
+
+};
+
+}
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
new file mode 100644
index 00000000..e25d8346
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -0,0 +1,218 @@
+#include "ATLPackManifest.h"
+
+#include "Json.h"
+
+static ATLauncher::DownloadType parseDownloadType(QString rawType) {
+ if(rawType == QString("server")) {
+ return ATLauncher::DownloadType::Server;
+ }
+ else if(rawType == QString("browser")) {
+ return ATLauncher::DownloadType::Browser;
+ }
+ else if(rawType == QString("direct")) {
+ return ATLauncher::DownloadType::Direct;
+ }
+
+ return ATLauncher::DownloadType::Unknown;
+}
+
+static ATLauncher::ModType parseModType(QString rawType) {
+ // See https://wiki.atlauncher.com/mod_types
+ if(rawType == QString("root")) {
+ return ATLauncher::ModType::Root;
+ }
+ else if(rawType == QString("forge")) {
+ return ATLauncher::ModType::Forge;
+ }
+ else if(rawType == QString("jar")) {
+ return ATLauncher::ModType::Jar;
+ }
+ else if(rawType == QString("mods")) {
+ return ATLauncher::ModType::Mods;
+ }
+ else if(rawType == QString("flan")) {
+ return ATLauncher::ModType::Flan;
+ }
+ else if(rawType == QString("dependency") || rawType == QString("depandency")) {
+ return ATLauncher::ModType::Dependency;
+ }
+ else if(rawType == QString("ic2lib")) {
+ return ATLauncher::ModType::Ic2Lib;
+ }
+ else if(rawType == QString("denlib")) {
+ return ATLauncher::ModType::DenLib;
+ }
+ else if(rawType == QString("coremods")) {
+ return ATLauncher::ModType::Coremods;
+ }
+ else if(rawType == QString("mcpc")) {
+ return ATLauncher::ModType::MCPC;
+ }
+ else if(rawType == QString("plugins")) {
+ return ATLauncher::ModType::Plugins;
+ }
+ else if(rawType == QString("extract")) {
+ return ATLauncher::ModType::Extract;
+ }
+ else if(rawType == QString("decomp")) {
+ return ATLauncher::ModType::Decomp;
+ }
+ else if(rawType == QString("texturepack")) {
+ return ATLauncher::ModType::TexturePack;
+ }
+ else if(rawType == QString("resourcepack")) {
+ return ATLauncher::ModType::ResourcePack;
+ }
+ else if(rawType == QString("shaderpack")) {
+ return ATLauncher::ModType::ShaderPack;
+ }
+ else if(rawType == QString("texturepackextract")) {
+ return ATLauncher::ModType::TexturePackExtract;
+ }
+ else if(rawType == QString("resourcepackextract")) {
+ return ATLauncher::ModType::ResourcePackExtract;
+ }
+ else if(rawType == QString("millenaire")) {
+ return ATLauncher::ModType::Millenaire;
+ }
+
+ return ATLauncher::ModType::Unknown;
+}
+
+static void loadVersionLoader(ATLauncher::VersionLoader & p, QJsonObject & obj) {
+ p.type = Json::requireString(obj, "type");
+ p.choose = Json::ensureBoolean(obj, QString("choose"), false);
+
+ auto metadata = Json::requireObject(obj, "metadata");
+ p.latest = Json::ensureBoolean(metadata, QString("latest"), false);
+ p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false);
+
+ // Minecraft Forge
+ if (p.type == "forge") {
+ p.version = Json::ensureString(metadata, "version", "");
+ }
+
+ // Fabric Loader
+ if (p.type == "fabric") {
+ p.version = Json::ensureString(metadata, "loader", "");
+ }
+}
+
+static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj) {
+ p.url = Json::requireString(obj, "url");
+ p.file = Json::requireString(obj, "file");
+ p.md5 = Json::requireString(obj, "md5");
+
+ p.download_raw = Json::requireString(obj, "download");
+ p.download = parseDownloadType(p.download_raw);
+
+ p.server = Json::ensureString(obj, "server", "");
+}
+
+static void loadVersionConfigs(ATLauncher::VersionConfigs & p, QJsonObject & obj) {
+ p.filesize = Json::requireInteger(obj, "filesize");
+ p.sha1 = Json::requireString(obj, "sha1");
+}
+
+static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) {
+ p.name = Json::requireString(obj, "name");
+ p.version = Json::requireString(obj, "version");
+ p.url = Json::requireString(obj, "url");
+ p.file = Json::requireString(obj, "file");
+ p.md5 = Json::ensureString(obj, "md5", "");
+
+ p.download_raw = Json::requireString(obj, "download");
+ p.download = parseDownloadType(p.download_raw);
+
+ p.type_raw = Json::requireString(obj, "type");
+ p.type = parseModType(p.type_raw);
+
+ // This contributes to the Minecraft Forge detection, where we rely on mod.type being "Forge"
+ // when the mod represents Forge. As there is little difference between "Jar" and "Forge, some
+ // packs regretfully use "Jar". This will correct the type to "Forge" in these cases (as best
+ // it can).
+ if(p.name == QString("Minecraft Forge") && p.type == ATLauncher::ModType::Jar) {
+ p.type_raw = "forge";
+ p.type = ATLauncher::ModType::Forge;
+ }
+
+ if(obj.contains("extractTo")) {
+ p.extractTo_raw = Json::requireString(obj, "extractTo");
+ p.extractTo = parseModType(p.extractTo_raw);
+ p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/");
+ }
+
+ if(obj.contains("decompType")) {
+ p.decompType_raw = Json::requireString(obj, "decompType");
+ p.decompType = parseModType(p.decompType_raw);
+ p.decompFile = Json::requireString(obj, "decompFile");
+ }
+
+ p.description = Json::ensureString(obj, QString("description"), "");
+ p.optional = Json::ensureBoolean(obj, QString("optional"), false);
+ p.recommended = Json::ensureBoolean(obj, QString("recommended"), false);
+ p.selected = Json::ensureBoolean(obj, QString("selected"), false);
+ p.hidden = Json::ensureBoolean(obj, QString("hidden"), false);
+ p.library = Json::ensureBoolean(obj, QString("library"), false);
+ p.group = Json::ensureString(obj, QString("group"), "");
+ if(obj.contains("depends")) {
+ auto dependsArr = Json::requireArray(obj, "depends");
+ for (const auto depends : dependsArr) {
+ p.depends.append(Json::requireString(depends));
+ }
+ }
+
+ p.client = Json::ensureBoolean(obj, QString("client"), false);
+
+ // computed
+ p.effectively_hidden = p.hidden || p.library;
+}
+
+void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
+{
+ v.version = Json::requireString(obj, "version");
+ v.minecraft = Json::requireString(obj, "minecraft");
+ v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false);
+
+ if(obj.contains("mainClass")) {
+ auto main = Json::requireObject(obj, "mainClass");
+ v.mainClass = Json::ensureString(main, "mainClass", "");
+ }
+
+ if(obj.contains("extraArguments")) {
+ auto arguments = Json::requireObject(obj, "extraArguments");
+ v.extraArguments = Json::ensureString(arguments, "arguments", "");
+ }
+
+ if(obj.contains("loader")) {
+ auto loader = Json::requireObject(obj, "loader");
+ loadVersionLoader(v.loader, loader);
+ }
+
+ if(obj.contains("libraries")) {
+ auto libraries = Json::requireArray(obj, "libraries");
+ for (const auto libraryRaw : libraries)
+ {
+ auto libraryObj = Json::requireObject(libraryRaw);
+ ATLauncher::VersionLibrary target;
+ loadVersionLibrary(target, libraryObj);
+ v.libraries.append(target);
+ }
+ }
+
+ if(obj.contains("mods")) {
+ auto mods = Json::requireArray(obj, "mods");
+ for (const auto modRaw : mods)
+ {
+ auto modObj = Json::requireObject(modRaw);
+ ATLauncher::VersionMod mod;
+ loadVersionMod(mod, modObj);
+ v.mods.append(mod);
+ }
+ }
+
+ if(obj.contains("configs")) {
+ auto configsObj = Json::requireObject(obj, "configs");
+ loadVersionConfigs(v.configs, configsObj);
+ }
+}
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h
new file mode 100644
index 00000000..ead216a5
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include <QString>
+#include <QVector>
+#include <QJsonObject>
+
+namespace ATLauncher
+{
+
+enum class PackType
+{
+ Public,
+ Private
+};
+
+enum class ModType
+{
+ Root,
+ Forge,
+ Jar,
+ Mods,
+ Flan,
+ Dependency,
+ Ic2Lib,
+ DenLib,
+ Coremods,
+ MCPC,
+ Plugins,
+ Extract,
+ Decomp,
+ TexturePack,
+ ResourcePack,
+ ShaderPack,
+ TexturePackExtract,
+ ResourcePackExtract,
+ Millenaire,
+ Unknown
+};
+
+enum class DownloadType
+{
+ Server,
+ Browser,
+ Direct,
+ Unknown
+};
+
+struct VersionLoader
+{
+ QString type;
+ bool latest;
+ bool recommended;
+ bool choose;
+
+ QString version;
+};
+
+struct VersionLibrary
+{
+ QString url;
+ QString file;
+ QString server;
+ QString md5;
+ DownloadType download;
+ QString download_raw;
+};
+
+struct VersionMod
+{
+ QString name;
+ QString version;
+ QString url;
+ QString file;
+ QString md5;
+ DownloadType download;
+ QString download_raw;
+ ModType type;
+ QString type_raw;
+
+ ModType extractTo;
+ QString extractTo_raw;
+ QString extractFolder;
+
+ ModType decompType;
+ QString decompType_raw;
+ QString decompFile;
+
+ QString description;
+ bool optional;
+ bool recommended;
+ bool selected;
+ bool hidden;
+ bool library;
+ QString group;
+ QVector<QString> depends;
+
+ bool client;
+
+ // computed
+ bool effectively_hidden;
+};
+
+struct VersionConfigs
+{
+ int filesize;
+ QString sha1;
+};
+
+struct PackVersion
+{
+ QString version;
+ QString minecraft;
+ bool noConfigs;
+ QString mainClass;
+ QString extraArguments;
+
+ VersionLoader loader;
+ QVector<VersionLibrary> libraries;
+ QVector<VersionMod> mods;
+ VersionConfigs configs;
+};
+
+void loadVersion(PackVersion & v, QJsonObject & obj);
+
+}
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
new file mode 100644
index 00000000..295574f0
--- /dev/null
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -0,0 +1,63 @@
+#include "FileResolvingTask.h"
+#include "Json.h"
+
+namespace {
+ const char * metabase = "https://cursemeta.dries007.net";
+}
+
+Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess)
+ : m_toProcess(toProcess)
+{
+}
+
+void Flame::FileResolvingTask::executeTask()
+{
+ setStatus(tr("Resolving mod IDs..."));
+ setProgress(0, m_toProcess.files.size());
+ m_dljob.reset(new NetJob("Mod id resolver"));
+ results.resize(m_toProcess.files.size());
+ int index = 0;
+ for(auto & file: m_toProcess.files)
+ {
+ auto projectIdStr = QString::number(file.projectId);
+ auto fileIdStr = QString::number(file.fileId);
+ QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr);
+ auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
+ m_dljob->addNetAction(dl);
+ index ++;
+ }
+ connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
+ m_dljob->start();
+}
+
+void Flame::FileResolvingTask::netJobFinished()
+{
+ bool failed = false;
+ int index = 0;
+ for(auto & bytes: results)
+ {
+ auto & out = m_toProcess.files[index];
+ try
+ {
+ failed &= (!out.parseFromBytes(bytes));
+ }
+ catch (const JSONValidationError &e)
+ {
+
+ qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
+ qCritical() << e.cause();
+ qCritical() << "JSON:";
+ qCritical() << bytes;
+ failed = true;
+ }
+ index++;
+ }
+ if(!failed)
+ {
+ emitSucceeded();
+ }
+ else
+ {
+ emitFailed(tr("Some mod ID resolving tasks failed."));
+ }
+}
diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h
new file mode 100644
index 00000000..78a38fcb
--- /dev/null
+++ b/launcher/modplatform/flame/FileResolvingTask.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include "PackManifest.h"
+
+namespace Flame
+{
+class FileResolvingTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit FileResolvingTask(Flame::Manifest &toProcess);
+ virtual ~FileResolvingTask() {};
+
+ const Flame::Manifest &getResults() const
+ {
+ return m_toProcess;
+ }
+
+protected:
+ virtual void executeTask() override;
+
+protected slots:
+ void netJobFinished();
+
+private: /* data */
+ Flame::Manifest m_toProcess;
+ QVector<QByteArray> results;
+ NetJobPtr m_dljob;
+};
+}
diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp
new file mode 100644
index 00000000..3d8ea22a
--- /dev/null
+++ b/launcher/modplatform/flame/FlamePackIndex.cpp
@@ -0,0 +1,92 @@
+#include "FlamePackIndex.h"
+
+#include "Json.h"
+
+void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj)
+{
+ pack.addonId = Json::requireInteger(obj, "id");
+ pack.name = Json::requireString(obj, "name");
+ pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
+ pack.description = Json::ensureString(obj, "summary", "");
+
+ bool thumbnailFound = false;
+ auto attachments = Json::requireArray(obj, "attachments");
+ for(auto attachmentRaw: attachments) {
+ auto attachmentObj = Json::requireObject(attachmentRaw);
+ bool isDefault = attachmentObj.value("isDefault").toBool(false);
+ if(isDefault) {
+ thumbnailFound = true;
+ pack.logoName = Json::requireString(attachmentObj, "title");
+ pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
+ break;
+ }
+ }
+
+ if(!thumbnailFound) {
+ throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name));
+ }
+
+ auto authors = Json::requireArray(obj, "authors");
+ for(auto authorIter: authors) {
+ auto author = Json::requireObject(authorIter);
+ Flame::ModpackAuthor packAuthor;
+ packAuthor.name = Json::requireString(author, "name");
+ packAuthor.url = Json::requireString(author, "url");
+ pack.authors.append(packAuthor);
+ }
+ int defaultFileId = Json::requireInteger(obj, "defaultFileId");
+
+ bool found = false;
+ // check if there are some files before adding the pack
+ auto files = Json::requireArray(obj, "latestFiles");
+ for(auto fileIter: files) {
+ auto file = Json::requireObject(fileIter);
+ int id = Json::requireInteger(file, "id");
+
+ // NOTE: for now, ignore everything that's not the default...
+ if(id != defaultFileId) {
+ continue;
+ }
+
+ auto versionArray = Json::requireArray(file, "gameVersion");
+ if(versionArray.size() < 1) {
+ continue;
+ }
+
+ found = true;
+ break;
+ }
+ if(!found) {
+ throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name));
+ }
+}
+
+void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
+{
+ QVector<Flame::IndexedVersion> unsortedVersions;
+ for(auto versionIter: arr) {
+ auto version = Json::requireObject(versionIter);
+ Flame::IndexedVersion file;
+
+ file.addonId = pack.addonId;
+ file.fileId = Json::requireInteger(version, "id");
+ auto versionArray = Json::requireArray(version, "gameVersion");
+ if(versionArray.size() < 1) {
+ continue;
+ }
+
+ // pick the latest version supported
+ file.mcVersion = versionArray[0].toString();
+ file.version = Json::requireString(version, "displayName");
+ file.downloadUrl = Json::requireString(version, "downloadUrl");
+ unsortedVersions.append(file);
+ }
+
+ auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool
+ {
+ return a.fileId > b.fileId;
+ };
+ std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
+ pack.versions = unsortedVersions;
+ pack.versionsLoaded = true;
+}
diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h
new file mode 100644
index 00000000..7ffa29c3
--- /dev/null
+++ b/launcher/modplatform/flame/FlamePackIndex.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVector>
+
+namespace Flame {
+
+struct ModpackAuthor {
+ QString name;
+ QString url;
+};
+
+struct IndexedVersion {
+ int addonId;
+ int fileId;
+ QString version;
+ QString mcVersion;
+ QString downloadUrl;
+};
+
+struct IndexedPack
+{
+ int addonId;
+ QString name;
+ QString description;
+ QList<ModpackAuthor> authors;
+ QString logoName;
+ QString logoUrl;
+ QString websiteUrl;
+
+ bool versionsLoaded = false;
+ QVector<IndexedVersion> versions;
+};
+
+void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
+void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr);
+}
+
+Q_DECLARE_METATYPE(Flame::IndexedPack)
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
new file mode 100644
index 00000000..b928fd16
--- /dev/null
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -0,0 +1,126 @@
+#include "PackManifest.h"
+#include "Json.h"
+
+static void loadFileV1(Flame::File & f, QJsonObject & file)
+{
+ f.projectId = Json::requireInteger(file, "projectID");
+ f.fileId = Json::requireInteger(file, "fileID");
+ f.required = Json::ensureBoolean(file, QString("required"), true);
+}
+
+static void loadModloaderV1(Flame::Modloader & m, QJsonObject & modLoader)
+{
+ m.id = Json::requireString(modLoader, "id");
+ m.primary = Json::ensureBoolean(modLoader, QString("primary"), false);
+}
+
+static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft)
+{
+ m.version = Json::requireString(minecraft, "version");
+ // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack
+ // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing
+ m.libraries = Json::ensureString(minecraft, QString("libraries"), QString());
+ auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray());
+ for (QJsonValueRef item : arr)
+ {
+ auto obj = Json::requireObject(item);
+ Flame::Modloader loader;
+ loadModloaderV1(loader, obj);
+ m.modLoaders.append(loader);
+ }
+}
+
+static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest)
+{
+ auto mc = Json::requireObject(manifest, "minecraft");
+ loadMinecraftV1(m.minecraft, mc);
+ m.name = Json::ensureString(manifest, QString("name"), "Unnamed");
+ m.version = Json::ensureString(manifest, QString("version"), QString());
+ m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward");
+ auto arr = Json::ensureArray(manifest, "files", QJsonArray());
+ for (QJsonValueRef item : arr)
+ {
+ auto obj = Json::requireObject(item);
+ Flame::File file;
+ loadFileV1(file, obj);
+ m.files.append(file);
+ }
+ m.overrides = Json::ensureString(manifest, "overrides", "overrides");
+}
+
+void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
+{
+ auto doc = Json::requireDocument(filepath);
+ auto obj = Json::requireObject(doc);
+ m.manifestType = Json::requireString(obj, "manifestType");
+ if(m.manifestType != "minecraftModpack")
+ {
+ throw JSONValidationError("Not a modpack manifest!");
+ }
+ m.manifestVersion = Json::requireInteger(obj, "manifestVersion");
+ if(m.manifestVersion != 1)
+ {
+ throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion));
+ }
+ loadManifestV1(m, obj);
+}
+
+bool Flame::File::parseFromBytes(const QByteArray& bytes)
+{
+ auto doc = Json::requireDocument(bytes);
+ auto obj = Json::requireObject(doc);
+ // result code signifies true failure.
+ if(obj.contains("code"))
+ {
+ qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:";
+ qCritical() << bytes;
+ return false;
+ }
+ fileName = Json::requireString(obj, "FileNameOnDisk");
+ QString rawUrl = Json::requireString(obj, "DownloadURL");
+ url = QUrl(rawUrl, QUrl::TolerantMode);
+ if(!url.isValid())
+ {
+ throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
+ }
+ // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
+ // It is also optional
+ QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
+ if(!projObj.isEmpty())
+ {
+ QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
+ if(strType == "singlefile")
+ {
+ type = File::Type::SingleFile;
+ }
+ else if(strType == "ctoc")
+ {
+ type = File::Type::Ctoc;
+ }
+ else if(strType == "cmod2")
+ {
+ type = File::Type::Cmod2;
+ }
+ else if(strType == "mod")
+ {
+ type = File::Type::Mod;
+ }
+ else if(strType == "folder")
+ {
+ type = File::Type::Folder;
+ }
+ else if(strType == "modpack")
+ {
+ type = File::Type::Modpack;
+ }
+ else
+ {
+ qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType;
+ type = File::Type::Unknown;
+ return false;
+ }
+ targetFolder = Json::ensureString(projObj, "Path", "mods");
+ }
+ resolved = true;
+ return true;
+}
diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h
new file mode 100644
index 00000000..02f39f0e
--- /dev/null
+++ b/launcher/modplatform/flame/PackManifest.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <QString>
+#include <QVector>
+#include <QUrl>
+
+namespace Flame
+{
+struct File
+{
+ // NOTE: throws JSONValidationError
+ bool parseFromBytes(const QByteArray &bytes);
+
+ int projectId = 0;
+ int fileId = 0;
+ // NOTE: the opposite to 'optional'. This is at the time of writing unused.
+ bool required = true;
+
+ // our
+ bool resolved = false;
+ QString fileName;
+ QUrl url;
+ QString targetFolder = QLatin1Literal("mods");
+ enum class Type
+ {
+ Unknown,
+ Folder,
+ Ctoc,
+ SingleFile,
+ Cmod2,
+ Modpack,
+ Mod
+ } type = Type::Mod;
+};
+
+struct Modloader
+{
+ QString id;
+ bool primary = false;
+};
+
+struct Minecraft
+{
+ QString version;
+ QString libraries;
+ QVector<Flame::Modloader> modLoaders;
+};
+
+struct Manifest
+{
+ QString manifestType;
+ int manifestVersion = 0;
+ Flame::Minecraft minecraft;
+ QString name;
+ QString version;
+ QString author;
+ QVector<Flame::File> files;
+ QString overrides;
+};
+
+void loadManifest(Flame::Manifest & m, const QString &filepath);
+}
diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
new file mode 100644
index 00000000..c2ef6436
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
@@ -0,0 +1,172 @@
+#include "PackFetchTask.h"
+#include "PrivatePackManager.h"
+
+#include <QDomDocument>
+#include <BuildConfig.h>
+
+namespace LegacyFTB {
+
+void PackFetchTask::fetch()
+{
+ publicPacks.clear();
+ thirdPartyPacks.clear();
+
+ NetJob *netJob = new NetJob("LegacyFTB::ModpackFetch");
+
+ QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml");
+ qDebug() << "Downloading public version info from" << publicPacksUrl.toString();
+ netJob->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData));
+
+ QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml");
+ qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString();
+ netJob->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &PackFetchTask::fileDownloadFailed);
+
+ jobPtr.reset(netJob);
+ netJob->start();
+}
+
+void PackFetchTask::fetchPrivate(const QStringList & toFetch)
+{
+ QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml";
+
+ for (auto &packCode: toFetch)
+ {
+ QByteArray *data = new QByteArray();
+ NetJob *job = new NetJob("Fetching private pack");
+ job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data));
+
+ QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode]
+ {
+ ModpackList packs;
+ parseAndAddPacks(*data, PackType::Private, packs);
+ foreach(Modpack currentPack, packs)
+ {
+ currentPack.packCode = packCode;
+ emit privateFileDownloadFinished(currentPack);
+ }
+
+ job->deleteLater();
+
+ data->clear();
+ delete data;
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason)
+ {
+ emit privateFileDownloadFailed(reason, packCode);
+ job->deleteLater();
+
+ data->clear();
+ delete data;
+ });
+
+ job->start();
+ }
+}
+
+void PackFetchTask::fileDownloadFinished()
+{
+ jobPtr.reset();
+
+ QStringList failedLists;
+
+ if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks))
+ {
+ failedLists.append(tr("Public Packs"));
+ }
+
+ if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks))
+ {
+ failedLists.append(tr("Third Party Packs"));
+ }
+
+ if(failedLists.size() > 0)
+ {
+ emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- ")));
+ }
+ else
+ {
+ emit finished(publicPacks, thirdPartyPacks);
+ }
+}
+
+bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list)
+{
+ QDomDocument doc;
+
+ QString errorMsg = "Unknown error.";
+ int errorLine = -1;
+ int errorCol = -1;
+
+ if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol))
+ {
+ auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:3d!").arg(errorMsg, errorLine, errorCol);
+ qWarning() << fullErrMsg;
+ data.clear();
+ return false;
+ }
+
+ QDomNodeList nodes = doc.elementsByTagName("modpack");
+ for(int i = 0; i < nodes.length(); i++)
+ {
+ QDomElement element = nodes.at(i).toElement();
+
+ Modpack modpack;
+ modpack.name = element.attribute("name");
+ modpack.currentVersion = element.attribute("version");
+ modpack.mcVersion = element.attribute("mcVersion");
+ modpack.description = element.attribute("description");
+ modpack.mods = element.attribute("mods");
+ modpack.logo = element.attribute("logo");
+ modpack.oldVersions = element.attribute("oldVersions").split(";");
+ modpack.broken = false;
+ modpack.bugged = false;
+
+ //remove empty if the xml is bugged
+ for(QString curr : modpack.oldVersions)
+ {
+ if(curr.isNull() || curr.isEmpty())
+ {
+ modpack.oldVersions.removeAll(curr);
+ modpack.bugged = true;
+ qWarning() << "Removed some empty versions from" << modpack.name;
+ }
+ }
+
+ if(modpack.oldVersions.size() < 1)
+ {
+ if(!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty())
+ {
+ modpack.oldVersions.append(modpack.currentVersion);
+ qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")";
+ }
+ else
+ {
+ modpack.broken = true;
+ qWarning() << "Broken pack:" << modpack.name << " => No valid version!";
+ }
+ }
+
+ modpack.author = element.attribute("author");
+
+ modpack.dir = element.attribute("dir");
+ modpack.file = element.attribute("url");
+
+ modpack.type = packType;
+
+ list.append(modpack);
+ }
+
+ return true;
+}
+
+void PackFetchTask::fileDownloadFailed(QString reason)
+{
+ qWarning() << "Fetching FTBPacks failed:" << reason;
+ emit failed(reason);
+}
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h
new file mode 100644
index 00000000..3ab32fab
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "net/NetJob.h"
+#include <QTemporaryDir>
+#include <QByteArray>
+#include <QObject>
+#include "PackHelpers.h"
+
+namespace LegacyFTB {
+
+class PackFetchTask : public QObject {
+
+ Q_OBJECT
+
+public:
+ PackFetchTask() = default;
+ virtual ~PackFetchTask() = default;
+
+ void fetch();
+ void fetchPrivate(const QStringList &toFetch);
+
+private:
+ NetJobPtr jobPtr;
+
+ QByteArray publicModpacksXmlFileData;
+ QByteArray thirdPartyModpacksXmlFileData;
+
+ bool parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list);
+ ModpackList publicPacks;
+ ModpackList thirdPartyPacks;
+
+protected slots:
+ void fileDownloadFinished();
+ void fileDownloadFailed(QString reason);
+
+signals:
+ void finished(ModpackList publicPacks, ModpackList thirdPartyPacks);
+ void failed(QString reason);
+
+ void privateFileDownloadFinished(Modpack modpack);
+ void privateFileDownloadFailed(QString reason, QString packCode);
+};
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PackHelpers.h b/launcher/modplatform/legacy_ftb/PackHelpers.h
new file mode 100644
index 00000000..566210d0
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackHelpers.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <QList>
+#include <QString>
+#include <QStringList>
+#include <QMetaType>
+
+namespace LegacyFTB {
+
+//Header for structs etc...
+enum class PackType
+{
+ Public,
+ ThirdParty,
+ Private
+};
+
+struct Modpack
+{
+ QString name;
+ QString description;
+ QString author;
+ QStringList oldVersions;
+ QString currentVersion;
+ QString mcVersion;
+ QString mods;
+ QString logo;
+
+ //Technical data
+ QString dir;
+ QString file; //<- Url in the xml, but doesn't make much sense
+
+ bool bugged = false;
+ bool broken = false;
+
+ PackType type;
+ QString packCode;
+};
+
+typedef QList<Modpack> ModpackList;
+
+}
+
+//We need it for the proxy model
+Q_DECLARE_METATYPE(LegacyFTB::Modpack)
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
new file mode 100644
index 00000000..c77f3250
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -0,0 +1,214 @@
+#include "PackInstallTask.h"
+
+#include "Env.h"
+#include "MMCZip.h"
+
+#include "BaseInstance.h"
+#include "FileSystem.h"
+#include "settings/INISettingsObject.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/GradleSpecifier.h"
+#include "BuildConfig.h"
+
+#include <QtConcurrent>
+
+namespace LegacyFTB {
+
+PackInstallTask::PackInstallTask(Modpack pack, QString version)
+{
+ m_pack = pack;
+ m_version = version;
+}
+
+void PackInstallTask::executeTask()
+{
+ downloadPack();
+}
+
+void PackInstallTask::downloadPack()
+{
+ setStatus(tr("Downloading zip for %1").arg(m_pack.name));
+
+ auto packoffset = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
+ auto entry = ENV.metacache()->resolveEntry("FTBPacks", packoffset);
+ NetJob *job = new NetJob("Download FTB Pack");
+
+ entry->setStale(true);
+ QString url;
+ if(m_pack.type == PackType::Private)
+ {
+ url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(packoffset);
+ }
+ else
+ {
+ url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(packoffset);
+ }
+ job->addNetAction(Net::Download::makeCached(url, entry));
+ archivePath = entry->getFullPath();
+
+ netJobContainer.reset(job);
+ connect(job, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
+ connect(job, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+ connect(job, &NetJob::progress, this, &PackInstallTask::onDownloadProgress);
+ job->start();
+
+ progress(1, 4);
+}
+
+void PackInstallTask::onDownloadSucceeded()
+{
+ abortable = false;
+ unzip();
+}
+
+void PackInstallTask::onDownloadFailed(QString reason)
+{
+ abortable = false;
+ emitFailed(reason);
+}
+
+void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
+{
+ abortable = true;
+ progress(current, total * 4);
+ setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
+}
+
+void PackInstallTask::unzip()
+{
+ progress(2, 4);
+ setStatus(tr("Extracting modpack"));
+ QDir extractDir(m_stagingPath);
+
+ m_packZip.reset(new QuaZip(archivePath));
+ if(!m_packZip->open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
+ return;
+ }
+
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void PackInstallTask::onUnzipFinished()
+{
+ install();
+}
+
+void PackInstallTask::onUnzipCanceled()
+{
+ emitAborted();
+}
+
+void PackInstallTask::install()
+{
+ progress(3, 4);
+ setStatus(tr("Installing modpack"));
+ QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
+ if(unzipMcDir.exists())
+ {
+ //ok, found minecraft dir, move contents to instance dir
+ if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft"))
+ {
+ emitFailed(tr("Failed to move unzipped minecraft!"));
+ return;
+ }
+ }
+
+ QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+
+ MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", m_pack.mcVersion, true);
+
+ bool fallback = true;
+
+ //handle different versions
+ QFile packJson(m_stagingPath + "/.minecraft/pack.json");
+ QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
+ if(packJson.exists())
+ {
+ packJson.open(QIODevice::ReadOnly | QIODevice::Text);
+ QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
+ packJson.close();
+
+ //we only care about the libs
+ QJsonArray libs = doc.object().value("libraries").toArray();
+
+ foreach (const QJsonValue &value, libs)
+ {
+ QString nameValue = value.toObject().value("name").toString();
+ if(!nameValue.startsWith("net.minecraftforge"))
+ {
+ continue;
+ }
+
+ GradleSpecifier forgeVersion(nameValue);
+
+ components->setComponentVersion("net.minecraftforge", forgeVersion.version().replace(m_pack.mcVersion, "").replace("-", ""));
+ packJson.remove();
+ fallback = false;
+ break;
+ }
+
+ }
+
+ if(jarmodDir.exists())
+ {
+ qDebug() << "Found jarmods, installing...";
+
+ QStringList jarmods;
+ for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
+ {
+ qDebug() << "Jarmod:" << info.fileName();
+ jarmods.push_back(info.absoluteFilePath());
+ }
+
+ components->installJarMods(jarmods);
+ fallback = false;
+ }
+
+ //just nuke unzip directory, it s not needed anymore
+ FS::deletePath(m_stagingPath + "/unzip");
+
+ if(fallback)
+ {
+ //TODO: Some fallback mechanism... or just keep failing!
+ emitFailed(tr("No installation method found!"));
+ return;
+ }
+
+ components->saveNow();
+
+ progress(4, 4);
+
+ instance.setName(m_instName);
+ if(m_instIcon == "default")
+ {
+ m_instIcon = "ftb_logo";
+ }
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ emitSucceeded();
+}
+
+bool PackInstallTask::abort()
+{
+ if(abortable)
+ {
+ return netJobContainer->abort();
+ }
+ return false;
+}
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h
new file mode 100644
index 00000000..600f72e7
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -0,0 +1,55 @@
+#pragma once
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+#include "quazip.h"
+#include "quazipdir.h"
+#include "meta/Index.h"
+#include "meta/Version.h"
+#include "meta/VersionList.h"
+#include "PackHelpers.h"
+
+#include <nonstd/optional>
+
+namespace LegacyFTB {
+
+class PackInstallTask : public InstanceTask
+{
+ Q_OBJECT
+
+public:
+ explicit PackInstallTask(Modpack pack, QString version);
+ virtual ~PackInstallTask(){}
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+private:
+ void downloadPack();
+ void unzip();
+ void install();
+
+private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+ void onDownloadProgress(qint64 current, qint64 total);
+
+ void onUnzipFinished();
+ void onUnzipCanceled();
+
+private: /* data */
+ bool abortable = false;
+ std::unique_ptr<QuaZip> m_packZip;
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ NetJobPtr netJobContainer;
+ QString archivePath;
+
+ Modpack m_pack;
+ QString m_version;
+};
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
new file mode 100644
index 00000000..501e6003
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
@@ -0,0 +1,41 @@
+#include "PrivatePackManager.h"
+
+#include <QDebug>
+
+#include "FileSystem.h"
+
+namespace LegacyFTB {
+
+void PrivatePackManager::load()
+{
+ try
+ {
+ currentPacks = QString::fromUtf8(FS::read(m_filename)).split('\n', QString::SkipEmptyParts).toSet();
+ dirty = false;
+ }
+ catch(...)
+ {
+ currentPacks = {};
+ qWarning() << "Failed to read third party FTB pack codes from" << m_filename;
+ }
+}
+
+void PrivatePackManager::save() const
+{
+ if(!dirty)
+ {
+ return;
+ }
+ try
+ {
+ QStringList list = currentPacks.toList();
+ FS::write(m_filename, list.join('\n').toUtf8());
+ dirty = false;
+ }
+ catch(...)
+ {
+ qWarning() << "Failed to write third party FTB pack codes to" << m_filename;
+ }
+}
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.h b/launcher/modplatform/legacy_ftb/PrivatePackManager.h
new file mode 100644
index 00000000..0e814646
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <QSet>
+#include <QString>
+#include <QFile>
+
+namespace LegacyFTB {
+
+class PrivatePackManager
+{
+public:
+ ~PrivatePackManager()
+ {
+ save();
+ }
+ void load();
+ void save() const;
+ bool empty() const
+ {
+ return currentPacks.empty();
+ }
+ const QSet<QString> &getCurrentPackCodes() const
+ {
+ return currentPacks;
+ }
+ void add(const QString &code)
+ {
+ currentPacks.insert(code);
+ dirty = true;
+ }
+ void remove(const QString &code)
+ {
+ currentPacks.remove(code);
+ dirty = true;
+ }
+
+private:
+ QSet<QString> currentPacks;
+ QString m_filename = "private_packs.txt";
+ mutable bool dirty = false;
+};
+
+}
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
new file mode 100644
index 00000000..f22373bc
--- /dev/null
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -0,0 +1,209 @@
+#include "FTBPackInstallTask.h"
+
+#include "BuildConfig.h"
+#include "Env.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "net/ChecksumValidator.h"
+#include "settings/INISettingsObject.h"
+
+namespace ModpacksCH {
+
+PackInstallTask::PackInstallTask(Modpack pack, QString version)
+{
+ m_pack = pack;
+ m_version_name = version;
+}
+
+bool PackInstallTask::abort()
+{
+ if(abortable)
+ {
+ return jobPtr->abort();
+ }
+ return false;
+}
+
+void PackInstallTask::executeTask()
+{
+ // Find pack version
+ bool found = false;
+ VersionInfo version;
+
+ for(auto vInfo : m_pack.versions) {
+ if (vInfo.name == m_version_name) {
+ found = true;
+ version = vInfo;
+ break;
+ }
+ }
+
+ if(!found) {
+ emitFailed(tr("Failed to find pack version %1").arg(m_version_name));
+ return;
+ }
+
+ auto *netJob = new NetJob("ModpacksCH::VersionFetch");
+ auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2")
+ .arg(m_pack.id).arg(version.id);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
+ QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+}
+
+void PackInstallTask::onDownloadSucceeded()
+{
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ModpacksCH::Version version;
+ try
+ {
+ ModpacksCH::loadVersion(version, obj);
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ return;
+ }
+ m_version = version;
+
+ downloadPack();
+}
+
+void PackInstallTask::onDownloadFailed(QString reason)
+{
+ jobPtr.reset();
+ emitFailed(reason);
+}
+
+void PackInstallTask::downloadPack()
+{
+ setStatus(tr("Downloading mods..."));
+
+ jobPtr.reset(new NetJob(tr("Mod download")));
+ for(auto file : m_version.files) {
+ if(file.serverOnly) continue;
+
+ QFileInfo fileName(file.name);
+ auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix();
+
+ auto entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", cacheName);
+ entry->setStale(true);
+
+ auto relpath = FS::PathCombine("minecraft", file.path, file.name);
+ auto path = FS::PathCombine(m_stagingPath, relpath);
+
+ qDebug() << "Will download" << file.url << "to" << path;
+ filesToCopy[entry->getFullPath()] = path;
+
+ auto dl = Net::Download::makeCached(file.url, entry);
+ if (!file.sha1.isEmpty()) {
+ auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ }
+ jobPtr->addNetAction(dl);
+ }
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
+ {
+ abortable = false;
+ jobPtr.reset();
+ install();
+ });
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
+ {
+ abortable = false;
+ jobPtr.reset();
+ emitFailed(reason);
+ });
+ connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
+ {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+}
+
+void PackInstallTask::install()
+{
+ setStatus(tr("Copying modpack files"));
+
+ for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) {
+ auto &from = iter.key();
+ auto &to = iter.value();
+ FS::copy fileCopyOperation(from, to);
+ if(!fileCopyOperation()) {
+ qWarning() << "Failed to copy" << from << "to" << to;
+ emitFailed(tr("Failed to copy files"));
+ return;
+ }
+ }
+
+ setStatus(tr("Installing modpack"));
+
+ auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+
+ MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ for(auto target : m_version.targets) {
+ if(target.type == "game" && target.name == "minecraft") {
+ components->setComponentVersion("net.minecraft", target.version, true);
+ break;
+ }
+ }
+
+ for(auto target : m_version.targets) {
+ if(target.type != "modloader") continue;
+
+ if(target.name == "forge") {
+ components->setComponentVersion("net.minecraftforge", target.version, true);
+ }
+ else if(target.name == "fabric") {
+ components->setComponentVersion("net.fabricmc.fabric-loader", target.version, true);
+ }
+ }
+
+ // install any jar mods
+ QDir jarModsDir(FS::PathCombine(m_stagingPath, "minecraft", "jarmods"));
+ if (jarModsDir.exists()) {
+ QStringList jarMods;
+
+ for (const auto& info : jarModsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
+ jarMods.push_back(info.absoluteFilePath());
+ }
+
+ components->installJarMods(jarMods);
+ }
+
+ components->saveNow();
+
+ instance.setName(m_instName);
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ emitSucceeded();
+}
+
+}
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
new file mode 100644
index 00000000..fdd84c4e
--- /dev/null
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "FTBPackManifest.h"
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+
+namespace ModpacksCH {
+
+class PackInstallTask : public InstanceTask
+{
+ Q_OBJECT
+
+public:
+ explicit PackInstallTask(Modpack pack, QString version);
+ virtual ~PackInstallTask(){}
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+protected:
+ virtual void executeTask() override;
+
+private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+
+private:
+ void downloadPack();
+ void install();
+
+private:
+ bool abortable = false;
+
+ NetJobPtr jobPtr;
+ QByteArray response;
+
+ Modpack m_pack;
+ QString m_version_name;
+ Version m_version;
+
+ QMap<QString, QString> filesToCopy;
+
+};
+
+}
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
new file mode 100644
index 00000000..fd99d332
--- /dev/null
+++ b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
@@ -0,0 +1,156 @@
+#include "FTBPackManifest.h"
+
+#include "Json.h"
+
+static void loadSpecs(ModpacksCH::Specs & s, QJsonObject & obj)
+{
+ s.id = Json::requireInteger(obj, "id");
+ s.minimum = Json::requireInteger(obj, "minimum");
+ s.recommended = Json::requireInteger(obj, "recommended");
+}
+
+static void loadTag(ModpacksCH::Tag & t, QJsonObject & obj)
+{
+ t.id = Json::requireInteger(obj, "id");
+ t.name = Json::requireString(obj, "name");
+}
+
+static void loadArt(ModpacksCH::Art & a, QJsonObject & obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.url = Json::requireString(obj, "url");
+ a.type = Json::requireString(obj, "type");
+ a.width = Json::requireInteger(obj, "width");
+ a.height = Json::requireInteger(obj, "height");
+ a.compressed = Json::requireBoolean(obj, "compressed");
+ a.sha1 = Json::requireString(obj, "sha1");
+ a.size = Json::requireInteger(obj, "size");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadAuthor(ModpacksCH::Author & a, QJsonObject & obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.name = Json::requireString(obj, "name");
+ a.type = Json::requireString(obj, "type");
+ a.website = Json::requireString(obj, "website");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionInfo(ModpacksCH::VersionInfo & v, QJsonObject & obj)
+{
+ v.id = Json::requireInteger(obj, "id");
+ v.name = Json::requireString(obj, "name");
+ v.type = Json::requireString(obj, "type");
+ v.updated = Json::requireInteger(obj, "updated");
+ auto specs = Json::requireObject(obj, "specs");
+ loadSpecs(v.specs, specs);
+}
+
+void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.name = Json::requireString(obj, "name");
+ m.synopsis = Json::requireString(obj, "synopsis");
+ m.description = Json::requireString(obj, "description");
+ m.type = Json::requireString(obj, "type");
+ m.featured = Json::requireBoolean(obj, "featured");
+ m.installs = Json::requireInteger(obj, "installs");
+ m.plays = Json::requireInteger(obj, "plays");
+ m.updated = Json::requireInteger(obj, "updated");
+ m.refreshed = Json::requireInteger(obj, "refreshed");
+ auto artArr = Json::requireArray(obj, "art");
+ for (QJsonValueRef artRaw : artArr)
+ {
+ auto artObj = Json::requireObject(artRaw);
+ ModpacksCH::Art art;
+ loadArt(art, artObj);
+ m.art.append(art);
+ }
+ auto authorArr = Json::requireArray(obj, "authors");
+ for (QJsonValueRef authorRaw : authorArr)
+ {
+ auto authorObj = Json::requireObject(authorRaw);
+ ModpacksCH::Author author;
+ loadAuthor(author, authorObj);
+ m.authors.append(author);
+ }
+ auto versionArr = Json::requireArray(obj, "versions");
+ for (QJsonValueRef versionRaw : versionArr)
+ {
+ auto versionObj = Json::requireObject(versionRaw);
+ ModpacksCH::VersionInfo version;
+ loadVersionInfo(version, versionObj);
+ m.versions.append(version);
+ }
+ auto tagArr = Json::requireArray(obj, "tags");
+ for (QJsonValueRef tagRaw : tagArr)
+ {
+ auto tagObj = Json::requireObject(tagRaw);
+ ModpacksCH::Tag tag;
+ loadTag(tag, tagObj);
+ m.tags.append(tag);
+ }
+ m.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionTarget(ModpacksCH::VersionTarget & a, QJsonObject & obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.name = Json::requireString(obj, "name");
+ a.type = Json::requireString(obj, "type");
+ a.version = Json::requireString(obj, "version");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.type = Json::requireString(obj, "type");
+ a.path = Json::requireString(obj, "path");
+ a.name = Json::requireString(obj, "name");
+ a.version = Json::requireString(obj, "version");
+ a.url = Json::requireString(obj, "url");
+ a.sha1 = Json::requireString(obj, "sha1");
+ a.size = Json::requireInteger(obj, "size");
+ a.clientOnly = Json::requireBoolean(obj, "clientonly");
+ a.serverOnly = Json::requireBoolean(obj, "serveronly");
+ a.optional = Json::requireBoolean(obj, "optional");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.parent = Json::requireInteger(obj, "parent");
+ m.name = Json::requireString(obj, "name");
+ m.type = Json::requireString(obj, "type");
+ m.installs = Json::requireInteger(obj, "installs");
+ m.plays = Json::requireInteger(obj, "plays");
+ m.updated = Json::requireInteger(obj, "updated");
+ m.refreshed = Json::requireInteger(obj, "refreshed");
+ auto specs = Json::requireObject(obj, "specs");
+ loadSpecs(m.specs, specs);
+ auto targetArr = Json::requireArray(obj, "targets");
+ for (QJsonValueRef targetRaw : targetArr)
+ {
+ auto versionObj = Json::requireObject(targetRaw);
+ ModpacksCH::VersionTarget target;
+ loadVersionTarget(target, versionObj);
+ m.targets.append(target);
+ }
+ auto fileArr = Json::requireArray(obj, "files");
+ for (QJsonValueRef fileRaw : fileArr)
+ {
+ auto fileObj = Json::requireObject(fileRaw);
+ ModpacksCH::VersionFile file;
+ loadVersionFile(file, fileObj);
+ m.files.append(file);
+ }
+}
+
+//static void loadVersionChangelog(ModpacksCH::VersionChangelog & m, QJsonObject & obj)
+//{
+// m.content = Json::requireString(obj, "content");
+// m.updated = Json::requireInteger(obj, "updated");
+//}
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h
new file mode 100644
index 00000000..7818b36d
--- /dev/null
+++ b/launcher/modplatform/modpacksch/FTBPackManifest.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include <QString>
+#include <QVector>
+#include <QUrl>
+#include <QJsonObject>
+#include <QMetaType>
+
+namespace ModpacksCH
+{
+
+struct Specs
+{
+ int id;
+ int minimum;
+ int recommended;
+};
+
+struct Tag
+{
+ int id;
+ QString name;
+};
+
+struct Art
+{
+ int id;
+ QString url;
+ QString type;
+ int width;
+ int height;
+ bool compressed;
+ QString sha1;
+ int size;
+ int64_t updated;
+};
+
+struct Author
+{
+ int id;
+ QString name;
+ QString type;
+ QString website;
+ int64_t updated;
+};
+
+struct VersionInfo
+{
+ int id;
+ QString name;
+ QString type;
+ int64_t updated;
+ Specs specs;
+};
+
+struct Modpack
+{
+ int id;
+ QString name;
+ QString synopsis;
+ QString description;
+ QString type;
+ bool featured;
+ int installs;
+ int plays;
+ int64_t updated;
+ int64_t refreshed;
+ QVector<Art> art;
+ QVector<Author> authors;
+ QVector<VersionInfo> versions;
+ QVector<Tag> tags;
+};
+
+struct VersionTarget
+{
+ int id;
+ QString type;
+ QString name;
+ QString version;
+ int64_t updated;
+};
+
+struct VersionFile
+{
+ int id;
+ QString type;
+ QString path;
+ QString name;
+ QString version;
+ QString url;
+ QString sha1;
+ int size;
+ bool clientOnly;
+ bool serverOnly;
+ bool optional;
+ int64_t updated;
+};
+
+struct Version
+{
+ int id;
+ int parent;
+ QString name;
+ QString type;
+ int installs;
+ int plays;
+ int64_t updated;
+ int64_t refreshed;
+ Specs specs;
+ QVector<VersionTarget> targets;
+ QVector<VersionFile> files;
+};
+
+struct VersionChangelog
+{
+ QString content;
+ int64_t updated;
+};
+
+void loadModpack(Modpack & m, QJsonObject & obj);
+
+void loadVersion(Version & m, QJsonObject & obj);
+}
+
+Q_DECLARE_METATYPE(ModpacksCH::Modpack)
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
new file mode 100644
index 00000000..dbce8e53
--- /dev/null
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
@@ -0,0 +1,141 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "SingleZipPackInstallTask.h"
+
+#include "Env.h"
+#include "MMCZip.h"
+#include "TechnicPackProcessor.h"
+
+#include <QtConcurrent>
+#include <FileSystem.h>
+
+Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
+{
+ m_sourceUrl = sourceUrl;
+ m_minecraftVersion = minecraftVersion;
+}
+
+bool Technic::SingleZipPackInstallTask::abort() {
+ if(m_abortable)
+ {
+ return m_filesNetJob->abort();
+ }
+ return false;
+}
+
+void Technic::SingleZipPackInstallTask::executeTask()
+{
+ setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
+
+ const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
+ auto entry = ENV.metacache()->resolveEntry("general", path);
+ entry->setStale(true);
+ m_filesNetJob.reset(new NetJob(tr("Modpack download")));
+ m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
+ m_archivePath = entry->getFullPath();
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded);
+ connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged);
+ connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SingleZipPackInstallTask::downloadSucceeded()
+{
+ m_abortable = false;
+
+ setStatus(tr("Extracting modpack"));
+ QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft"));
+ qDebug() << "Attempting to create instance from" << m_archivePath;
+
+ // open the zip and find relevant files in it
+ m_packZip.reset(new QuaZip(m_archivePath));
+ if (!m_packZip->open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Unable to open supplied modpack zip file."));
+ return;
+ }
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath());
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+ m_filesNetJob.reset();
+}
+
+void Technic::SingleZipPackInstallTask::downloadFailed(QString reason)
+{
+ m_abortable = false;
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
+{
+ m_abortable = true;
+ setProgress(current / 2, total);
+}
+
+void Technic::SingleZipPackInstallTask::extractFinished()
+{
+ m_packZip.reset();
+ if (!m_extractFuture.result())
+ {
+ emitFailed(tr("Failed to extract modpack"));
+ return;
+ }
+ QDir extractDir(m_stagingPath);
+
+ qDebug() << "Fixing permissions for extracted pack files...";
+ QDirIterator it(extractDir, QDirIterator::Subdirectories);
+ while (it.hasNext())
+ {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ auto origPermissions = permissions;
+ if (file.isDir())
+ {
+ // Folder +rwx for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
+ }
+ else
+ {
+ // File +rw for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
+ }
+ if (origPermissions != permissions)
+ {
+ if (!QFile::setPermissions(filepath, permissions))
+ {
+ logWarning(tr("Could not fix permissions for %1").arg(filepath));
+ }
+ else
+ {
+ qDebug() << "Fixed" << filepath;
+ }
+ }
+ }
+
+ shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded);
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed);
+ packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion);
+}
+
+void Technic::SingleZipPackInstallTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+}
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h
new file mode 100644
index 00000000..80f10a98
--- /dev/null
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h
@@ -0,0 +1,64 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+
+#include "quazip.h"
+
+#include <QFutureWatcher>
+#include <QStringList>
+#include <QUrl>
+
+#include <nonstd/optional>
+
+namespace Technic {
+
+class SingleZipPackInstallTask : public InstanceTask
+{
+ Q_OBJECT
+
+public:
+ SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+protected:
+ void executeTask() override;
+
+
+private slots:
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void extractFinished();
+ void extractAborted();
+
+private:
+ bool m_abortable = false;
+
+ QUrl m_sourceUrl;
+ QString m_minecraftVersion;
+ QString m_archivePath;
+ NetJobPtr m_filesNetJob;
+ std::unique_ptr<QuaZip> m_packZip;
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+};
+
+} // namespace Technic
diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp
new file mode 100644
index 00000000..1b4186d4
--- /dev/null
+++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp
@@ -0,0 +1,207 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "SolderPackInstallTask.h"
+
+#include <FileSystem.h>
+#include <Json.h>
+#include <QtConcurrentRun>
+#include <MMCZip.h>
+#include "TechnicPackProcessor.h"
+
+Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
+{
+ m_sourceUrl = sourceUrl;
+ m_minecraftVersion = minecraftVersion;
+}
+
+bool Technic::SolderPackInstallTask::abort() {
+ if(m_abortable)
+ {
+ return m_filesNetJob->abort();
+ }
+ return false;
+}
+
+void Technic::SolderPackInstallTask::executeTask()
+{
+ setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString()));
+ m_filesNetJob.reset(new NetJob(tr("Finding recommended version")));
+ m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded);
+ connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::versionSucceeded()
+{
+ try
+ {
+ QJsonDocument doc = Json::requireDocument(m_response);
+ QJsonObject obj = Json::requireObject(doc);
+ QString version = Json::requireString(obj, "recommended", "__placeholder__");
+ m_sourceUrl = m_sourceUrl.toString() + '/' + version;
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(e.cause());
+ m_filesNetJob.reset();
+ return;
+ }
+
+ setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString()));
+ m_filesNetJob.reset(new NetJob(tr("Resolving modpack files")));
+ m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
+ connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::fileListSucceeded()
+{
+ setStatus(tr("Downloading modpack:"));
+ QStringList modUrls;
+ try
+ {
+ QJsonDocument doc = Json::requireDocument(m_response);
+ QJsonObject obj = Json::requireObject(doc);
+ QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
+ if (!minecraftVersion.isEmpty())
+ m_minecraftVersion = minecraftVersion;
+ QJsonArray mods = Json::requireArray(obj, "mods", "'mods'");
+ for (auto mod: mods)
+ {
+ QJsonObject modObject = Json::requireObject(mod);
+ modUrls.append(Json::requireString(modObject, "url", "'url'"));
+ }
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(e.cause());
+ m_filesNetJob.reset();
+ return;
+ }
+ m_filesNetJob.reset(new NetJob(tr("Downloading modpack")));
+ int i = 0;
+ for (auto &modUrl: modUrls)
+ {
+ auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
+ m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path));
+ i++;
+ }
+
+ m_modCount = modUrls.size();
+
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
+ connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
+ connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::downloadSucceeded()
+{
+ m_abortable = false;
+
+ setStatus(tr("Extracting modpack"));
+ m_filesNetJob.reset();
+ m_extractFuture = QtConcurrent::run([this]()
+ {
+ int i = 0;
+ QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft");
+ FS::ensureFolderPathExists(extractDir);
+
+ while (m_modCount > i)
+ {
+ auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
+ if (!MMCZip::extractDir(path, extractDir))
+ {
+ return false;
+ }
+ i++;
+ }
+ return true;
+ });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SolderPackInstallTask::extractFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SolderPackInstallTask::extractAborted);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void Technic::SolderPackInstallTask::downloadFailed(QString reason)
+{
+ m_abortable = false;
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
+{
+ m_abortable = true;
+ setProgress(current / 2, total);
+}
+
+void Technic::SolderPackInstallTask::extractFinished()
+{
+ if (!m_extractFuture.result())
+ {
+ emitFailed(tr("Failed to extract modpack"));
+ return;
+ }
+ QDir extractDir(m_stagingPath);
+
+ qDebug() << "Fixing permissions for extracted pack files...";
+ QDirIterator it(extractDir, QDirIterator::Subdirectories);
+ while (it.hasNext())
+ {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ auto origPermissions = permissions;
+ if(file.isDir())
+ {
+ // Folder +rwx for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
+ }
+ else
+ {
+ // File +rw for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
+ }
+ if(origPermissions != permissions)
+ {
+ if(!QFile::setPermissions(filepath, permissions))
+ {
+ logWarning(tr("Could not fix permissions for %1").arg(filepath));
+ }
+ else
+ {
+ qDebug() << "Fixed" << filepath;
+ }
+ }
+ }
+
+ shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded);
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed);
+ packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true);
+}
+
+void Technic::SolderPackInstallTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+ return;
+}
+
diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h
new file mode 100644
index 00000000..6e1057eb
--- /dev/null
+++ b/launcher/modplatform/technic/SolderPackInstallTask.h
@@ -0,0 +1,60 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <InstanceTask.h>
+#include <net/NetJob.h>
+#include <tasks/Task.h>
+
+#include <QUrl>
+
+namespace Technic
+{
+ class SolderPackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+ public:
+ explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+ protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+ private slots:
+ void versionSucceeded();
+ void fileListSucceeded();
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void extractFinished();
+ void extractAborted();
+
+ private:
+ bool m_abortable = false;
+
+ NetJobPtr m_filesNetJob;
+ QUrl m_sourceUrl;
+ QString m_minecraftVersion;
+ QByteArray m_response;
+ QTemporaryDir m_outputDir;
+ int m_modCount;
+ QFuture<bool> m_extractFuture;
+ QFutureWatcher<bool> m_extractFutureWatcher;
+ };
+}
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp
new file mode 100644
index 00000000..52979b7c
--- /dev/null
+++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -0,0 +1,208 @@
+/* Copyright 2020-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "TechnicPackProcessor.h"
+
+#include <FileSystem.h>
+#include <Json.h>
+#include <minecraft/MinecraftInstance.h>
+#include <minecraft/PackProfile.h>
+#include <quazip.h>
+#include <quazipdir.h>
+#include <quazipfile.h>
+#include <settings/INISettingsObject.h>
+
+#include <memory>
+
+void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion, const bool isSolder)
+{
+ QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft");
+ QString configPath = FS::PathCombine(stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ MinecraftInstance instance(globalSettings, instanceSettings, stagingPath);
+
+ instance.setName(instName);
+
+ if (instIcon != "default")
+ {
+ instance.setIconKey(instIcon);
+ }
+
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ QByteArray data;
+
+ QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar");
+ QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json");
+ QString fmlMinecraftVersion;
+ if (QFile::exists(modpackJar))
+ {
+ QuaZip zipFile(modpackJar);
+ if (!zipFile.open(QuaZip::mdUnzip))
+ {
+ emit failed(tr("Unable to open \"bin/modpack.jar\" file!"));
+ return;
+ }
+ QuaZipDir zipFileRoot(&zipFile, "/");
+ if (zipFileRoot.exists("/version.json"))
+ {
+ if (zipFileRoot.exists("/fmlversion.properties"))
+ {
+ zipFile.setCurrentFile("fmlversion.properties");
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ emit failed(tr("Unable to open \"fmlversion.properties\"!"));
+ return;
+ }
+ QByteArray fmlVersionData = file.readAll();
+ file.close();
+ INIFile iniFile;
+ iniFile.loadFile(fmlVersionData);
+ // If not present, this evaluates to a null string
+ fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
+ }
+ zipFile.setCurrentFile("version.json", QuaZip::csSensitive);
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ emit failed(tr("Unable to open \"version.json\"!"));
+ return;
+ }
+ data = file.readAll();
+ file.close();
+ }
+ else
+ {
+ if (minecraftVersion.isEmpty())
+ emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown"));
+ components->setComponentVersion("net.minecraft", minecraftVersion, true);
+ components->installJarMods({modpackJar});
+
+ // Forge for 1.4.7 and for 1.5.2 require extra libraries.
+ // Figure out the forge version and add it as a component
+ // (the code still comes from the jar mod installed above)
+ if (zipFileRoot.exists("/forgeversion.properties"))
+ {
+ zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive);
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ // Really shouldn't happen, but error handling shall not be forgotten
+ emit failed(tr("Unable to open \"forgeversion.properties\""));
+ return;
+ }
+ QByteArray forgeVersionData = file.readAll();
+ file.close();
+ INIFile iniFile;
+ iniFile.loadFile(forgeVersionData);
+ QString major, minor, revision, build;
+ major = iniFile["forge.major.number"].toString();
+ minor = iniFile["forge.minor.number"].toString();
+ revision = iniFile["forge.revision.number"].toString();
+ build = iniFile["forge.build.number"].toString();
+
+ if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty())
+ {
+ emit failed(tr("Invalid \"forgeversion.properties\"!"));
+ return;
+ }
+
+ components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build);
+ }
+
+ components->saveNow();
+ emit succeeded();
+ return;
+ }
+ }
+ else if (QFile::exists(versionJson))
+ {
+ QFile file(versionJson);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ emit failed(tr("Unable to open \"version.json\"!"));
+ return;
+ }
+ data = file.readAll();
+ file.close();
+ }
+ else
+ {
+ // This is the "Vanilla" modpack, excluded by the search code
+ emit failed(tr("Unable to find a \"version.json\"!"));
+ return;
+ }
+
+ try
+ {
+ QJsonDocument doc = Json::requireDocument(data);
+ QJsonObject root = Json::requireObject(doc, "version.json");
+ QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), "");
+ if (minecraftVersion.isEmpty())
+ {
+ if (fmlMinecraftVersion.isEmpty())
+ {
+ emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing"));
+ return;
+ }
+ minecraftVersion = fmlMinecraftVersion;
+ }
+ components->setComponentVersion("net.minecraft", minecraftVersion, true);
+ for (auto library: Json::ensureArray(root, "libraries", {}))
+ {
+ if (!library.isObject())
+ {
+ continue;
+ }
+
+ auto libraryObject = Json::ensureObject(library, {}, "");
+ auto libraryName = Json::ensureString(libraryObject, "name", "", "");
+
+ if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-'))
+ {
+ QString libraryVersion = libraryName.section(':', 2);
+ if (!libraryVersion.startsWith("1.7.10-"))
+ {
+ components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1));
+ }
+ else
+ {
+ // 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part
+ components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1));
+ }
+ }
+ else if (libraryName.startsWith("net.minecraftforge:minecraftforge:"))
+ {
+ components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2));
+ }
+ else if (libraryName.startsWith("net.fabricmc:fabric-loader:"))
+ {
+ components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2));
+ }
+ }
+ }
+ catch (const JSONValidationError &e)
+ {
+ emit failed(tr("Could not understand \"version.json\":\n") + e.cause());
+ return;
+ }
+
+ components->saveNow();
+ emit succeeded();
+}
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.h b/launcher/modplatform/technic/TechnicPackProcessor.h
new file mode 100644
index 00000000..2ad803b3
--- /dev/null
+++ b/launcher/modplatform/technic/TechnicPackProcessor.h
@@ -0,0 +1,35 @@
+/* Copyright 2020-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QString>
+#include "settings/SettingsObject.h"
+
+namespace Technic
+{
+ // not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask
+ class TechnicPackProcessor : public QObject
+ {
+ Q_OBJECT
+
+ signals:
+ void succeeded();
+ void failed(QString reason);
+
+ public:
+ void run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion=QString(), const bool isSolder = false);
+ };
+}
diff --git a/launcher/mojang/PackageManifest.cpp b/launcher/mojang/PackageManifest.cpp
new file mode 100644
index 00000000..b3dfd7fc
--- /dev/null
+++ b/launcher/mojang/PackageManifest.cpp
@@ -0,0 +1,427 @@
+#include "PackageManifest.h"
+#include <Json.h>
+#include <QDir>
+#include <QDirIterator>
+#include <QCryptographicHash>
+#include <QDebug>
+
+#ifndef Q_OS_WIN32
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#endif
+
+namespace mojang_files {
+
+const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
+
+int Path::compare(const Path& rhs) const
+{
+ auto left_cursor = begin();
+ auto left_end = end();
+ auto right_cursor = rhs.begin();
+ auto right_end = rhs.end();
+
+ while (left_cursor != left_end && right_cursor != right_end)
+ {
+ if(*left_cursor < *right_cursor)
+ {
+ return -1;
+ }
+ else if(*left_cursor > *right_cursor)
+ {
+ return 1;
+ }
+ left_cursor++;
+ right_cursor++;
+ }
+
+ if(left_cursor == left_end)
+ {
+ if(right_cursor == right_end)
+ {
+ return 0;
+ }
+ return -1;
+ }
+ return 1;
+}
+
+void Package::addFile(const Path& path, const File& file) {
+ addFolder(path.parent_path());
+ files[path] = file;
+}
+
+void Package::addFolder(Path folder) {
+ if(!folder.has_parent_path()) {
+ return;
+ }
+ do {
+ folders.insert(folder);
+ folder = folder.parent_path();
+ } while(folder.has_parent_path());
+}
+
+void Package::addLink(const Path& path, const Path& target) {
+ addFolder(path.parent_path());
+ symlinks[path] = target;
+}
+
+void Package::addSource(const FileSource& source) {
+ sources[source.hash] = source;
+}
+
+
+namespace {
+void fromJson(QJsonDocument & doc, Package & out) {
+ std::set<Path> seen_paths;
+ if (!doc.isObject())
+ {
+ throw JSONValidationError("file manifest is not an object");
+ }
+ QJsonObject root = doc.object();
+
+ auto filesObj = Json::ensureObject(root, "files");
+ auto iter = filesObj.begin();
+ while (iter != filesObj.end())
+ {
+ Path objectPath = Path(iter.key());
+ auto value = iter.value();
+ iter++;
+ if(seen_paths.count(objectPath)) {
+ throw JSONValidationError("duplicate path inside manifest, the manifest is invalid");
+ }
+ if (!value.isObject())
+ {
+ throw JSONValidationError("file entry inside manifest is not an an object");
+ }
+ seen_paths.insert(objectPath);
+
+ auto fileObject = value.toObject();
+ auto type = Json::requireString(fileObject, "type");
+ if(type == "directory") {
+ out.addFolder(objectPath);
+ continue;
+ }
+ else if(type == "file") {
+ FileSource bestSource;
+ File file;
+ file.executable = Json::ensureBoolean(fileObject, QString("executable"), false);
+ auto downloads = Json::requireObject(fileObject, "downloads");
+ for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) {
+ FileSource source;
+
+ auto downloadObject = Json::requireObject(iter2.value());
+ source.hash = Json::requireString(downloadObject, "sha1");
+ source.size = Json::requireInteger(downloadObject, "size");
+ source.url = Json::requireString(downloadObject, "url");
+
+ auto compression = iter2.key();
+ if(compression == "raw") {
+ file.hash = source.hash;
+ file.size = source.size;
+ source.compression = Compression::Raw;
+ }
+ else if (compression == "lzma") {
+ source.compression = Compression::Lzma;
+ }
+ else {
+ continue;
+ }
+ bestSource.upgrade(source);
+ }
+ if(bestSource.isBad()) {
+ throw JSONValidationError("No valid compression method for file " + iter.key());
+ }
+ out.addFile(objectPath, file);
+ out.addSource(bestSource);
+ }
+ else if(type == "link") {
+ auto target = Json::requireString(fileObject, "target");
+ out.symlinks[objectPath] = target;
+ out.addLink(objectPath, target);
+ }
+ else {
+ throw JSONValidationError("Invalid item type in manifest: " + type);
+ }
+ }
+ // make sure the containing folder exists
+ out.folders.insert(Path());
+}
+}
+
+Package Package::fromManifestContents(const QByteArray& contents)
+{
+ Package out;
+ try
+ {
+ auto doc = Json::requireDocument(contents, "Manifest");
+ fromJson(doc, out);
+ return out;
+ }
+ catch (const Exception &e)
+ {
+ qDebug() << QString("Unable to parse manifest: %1").arg(e.cause());
+ out.valid = false;
+ return out;
+ }
+}
+
+Package Package::fromManifestFile(const QString & filename) {
+ Package out;
+ try
+ {
+ auto doc = Json::requireDocument(filename, filename);
+ fromJson(doc, out);
+ return out;
+ }
+ catch (const Exception &e)
+ {
+ qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause());
+ out.valid = false;
+ return out;
+ }
+}
+
+#ifndef Q_OS_WIN32
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+namespace {
+// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves
+bool actually_read_symlink_target(const QString & filepath, Path & out)
+{
+ struct ::stat st;
+ // FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls.
+ QByteArray nativePath = filepath.toUtf8();
+ const char * filepath_cstr = nativePath.data();
+
+ if (lstat(filepath_cstr, &st) != 0)
+ {
+ return false;
+ }
+
+ auto size = st.st_size ? st.st_size + 1 : PATH_MAX;
+ std::string temp(size, '\0');
+ // because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff
+ do
+ {
+ auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size());
+ if(link_length == -1)
+ {
+ return false;
+ }
+ if(std::string::size_type(link_length) < temp.size())
+ {
+ // buffer was long enough and we managed to read the link target. RETURN here.
+ temp.resize(link_length);
+ out = Path(QString::fromUtf8(temp.c_str()));
+ return true;
+ }
+ temp.resize(temp.size() * 2);
+ } while (true);
+}
+}
+#endif
+
+// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much?
+// FIXME: The error handling is just DEFICIENT
+Package Package::fromInspectedFolder(const QString& folderPath)
+{
+ QDir root(folderPath);
+
+ Package out;
+ QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories);
+ while(iterator.hasNext()) {
+ iterator.next();
+
+ auto fileInfo = iterator.fileInfo();
+ auto relPath = root.relativeFilePath(fileInfo.filePath());
+ // FIXME: this is probably completely busted on Windows anyway, so just disable it.
+ // Qt makes shit up and doesn't understand the platform details
+ // TODO: Actually use a filesystem library that isn't terrible and has decen license.
+ // I only know one, and I wrote it. Sadly, currently proprietary. PAIN.
+#ifndef Q_OS_WIN32
+ if(fileInfo.isSymLink()) {
+ Path targetPath;
+ if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) {
+ qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
+ out.valid = false;
+ }
+ out.addLink(relPath, targetPath);
+ }
+ else
+#endif
+ if(fileInfo.isDir()) {
+ out.addFolder(relPath);
+ }
+ else if(fileInfo.isFile()) {
+ File f;
+ f.executable = fileInfo.isExecutable();
+ f.size = fileInfo.size();
+ // FIXME: async / optimize the hashing
+ QFile input(fileInfo.absoluteFilePath());
+ if(!input.open(QIODevice::ReadOnly)) {
+ qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath();
+ out.valid = false;
+ break;
+ }
+ f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData();
+ out.addFile(relPath, f);
+ }
+ else {
+ // Something else... oh my
+ qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath();
+ out.valid = false;
+ break;
+ }
+ }
+ out.folders.insert(Path("."));
+ out.valid = true;
+ return out;
+}
+
+namespace {
+struct shallow_first_sort
+{
+ bool operator()(const Path &lhs, const Path &rhs) const
+ {
+ auto lhs_depth = lhs.length();
+ auto rhs_depth = rhs.length();
+ if(lhs_depth < rhs_depth)
+ {
+ return true;
+ }
+ else if(lhs_depth == rhs_depth)
+ {
+ if(lhs < rhs)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
+struct deep_first_sort
+{
+ bool operator()(const Path &lhs, const Path &rhs) const
+ {
+ auto lhs_depth = lhs.length();
+ auto rhs_depth = rhs.length();
+ if(lhs_depth > rhs_depth)
+ {
+ return true;
+ }
+ else if(lhs_depth == rhs_depth)
+ {
+ if(lhs < rhs)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+}
+
+UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to)
+{
+ UpdateOperations out;
+
+ if(!from.valid || !to.valid) {
+ out.valid = false;
+ return out;
+ }
+
+ // Files
+ for(auto iter = from.files.begin(); iter != from.files.end(); iter++) {
+ const auto &current_hash = iter->second.hash;
+ const auto &current_executable = iter->second.executable;
+ const auto &path = iter->first;
+
+ auto iter2 = to.files.find(path);
+ if(iter2 == to.files.end()) {
+ // removed
+ out.deletes.push_back(path);
+ continue;
+ }
+ auto new_hash = iter2->second.hash;
+ auto new_executable = iter2->second.executable;
+ if (current_hash != new_hash) {
+ out.deletes.push_back(path);
+ out.downloads.emplace(
+ std::pair<Path, FileDownload>{
+ path,
+ FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable)
+ }
+ );
+ }
+ else if (current_executable != new_executable) {
+ out.executable_fixes[path] = new_executable;
+ }
+ }
+ for(auto iter = to.files.begin(); iter != to.files.end(); iter++) {
+ auto path = iter->first;
+ if(!from.files.count(path)) {
+ out.downloads.emplace(
+ std::pair<Path, FileDownload>{
+ path,
+ FileDownload(to.sources.at(iter->second.hash), iter->second.executable)
+ }
+ );
+ }
+ }
+
+ // Folders
+ std::set<Path, deep_first_sort> remove_folders;
+ std::set<Path, shallow_first_sort> make_folders;
+ for(auto from_path: from.folders) {
+ auto iter = to.folders.find(from_path);
+ if(iter == to.folders.end()) {
+ remove_folders.insert(from_path);
+ }
+ }
+ for(auto & rmdir: remove_folders) {
+ out.rmdirs.push_back(rmdir);
+ }
+ for(auto to_path: to.folders) {
+ auto iter = from.folders.find(to_path);
+ if(iter == from.folders.end()) {
+ make_folders.insert(to_path);
+ }
+ }
+ for(auto & mkdir: make_folders) {
+ out.mkdirs.push_back(mkdir);
+ }
+
+ // Symlinks
+ for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) {
+ const auto &current_target = iter->second;
+ const auto &path = iter->first;
+
+ auto iter2 = to.symlinks.find(path);
+ if(iter2 == to.symlinks.end()) {
+ // removed
+ out.deletes.push_back(path);
+ continue;
+ }
+ const auto &new_target = iter2->second;
+ if (current_target != new_target) {
+ out.deletes.push_back(path);
+ out.mklinks[path] = iter2->second;
+ }
+ }
+ for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) {
+ auto path = iter->first;
+ if(!from.symlinks.count(path)) {
+ out.mklinks[path] = iter->second;
+ }
+ }
+ out.valid = true;
+ return out;
+}
+
+}
diff --git a/launcher/mojang/PackageManifest.h b/launcher/mojang/PackageManifest.h
new file mode 100644
index 00000000..fd7ab0ad
--- /dev/null
+++ b/launcher/mojang/PackageManifest.h
@@ -0,0 +1,171 @@
+#pragma once
+
+#include <QString>
+#include <map>
+#include <set>
+#include <QStringList>
+#include "tasks/Task.h"
+
+namespace mojang_files {
+
+using Hash = QString;
+extern const Hash empty_hash;
+
+// simple-ish path implementation. assumes always relative and does not allow '..' entries
+class Path
+{
+public:
+ using parts_type = QStringList;
+
+ Path() = default;
+ Path(QString string) {
+ auto parts_in = string.split('/');
+ for(auto & part: parts_in) {
+ if(part.isEmpty() || part == ".") {
+ continue;
+ }
+ if(part == "..") {
+ if(parts.size()) {
+ parts.pop_back();
+ }
+ continue;
+ }
+ parts.push_back(part);
+ }
+ }
+
+ bool has_parent_path() const
+ {
+ return parts.size() > 0;
+ }
+
+ Path parent_path() const
+ {
+ if (parts.empty())
+ return Path();
+ return Path(parts.begin(), std::prev(parts.end()));
+ }
+
+ bool empty() const
+ {
+ return parts.empty();
+ }
+
+ int length() const
+ {
+ return parts.length();
+ }
+
+ bool operator==(const Path & rhs) const {
+ return parts == rhs.parts;
+ }
+
+ bool operator!=(const Path & rhs) const {
+ return parts != rhs.parts;
+ }
+
+ inline bool operator<(const Path& rhs) const
+ {
+ return compare(rhs) < 0;
+ }
+
+ parts_type::const_iterator begin() const
+ {
+ return parts.begin();
+ }
+
+ parts_type::const_iterator end() const
+ {
+ return parts.end();
+ }
+
+ QString toString() const {
+ return parts.join("/");
+ }
+
+private:
+ Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) {
+ auto cursor = start;
+ while(cursor != end) {
+ parts.push_back(*cursor);
+ cursor++;
+ }
+ }
+ int compare(const Path& p) const;
+
+ parts_type parts;
+};
+
+
+enum class Compression {
+ Raw,
+ Lzma,
+ Unknown
+};
+
+
+struct FileSource
+{
+ Compression compression = Compression::Unknown;
+ Hash hash;
+ QString url;
+ std::size_t size = 0;
+ void upgrade(const FileSource & other) {
+ if(compression == Compression::Unknown || other.size < size) {
+ *this = other;
+ }
+ }
+ bool isBad() const {
+ return compression == Compression::Unknown;
+ }
+};
+
+struct File
+{
+ Hash hash;
+ bool executable;
+ std::uint64_t size = 0;
+};
+
+struct Package {
+ static Package fromInspectedFolder(const QString &folderPath);
+ static Package fromManifestFile(const QString &path);
+ static Package fromManifestContents(const QByteArray& contents);
+
+ explicit operator bool() const
+ {
+ return valid;
+ }
+ void addFolder(Path folder);
+ void addFile(const Path & path, const File & file);
+ void addLink(const Path & path, const Path & target);
+ void addSource(const FileSource & source);
+
+ std::map<Hash, FileSource> sources;
+ bool valid = true;
+ std::set<Path> folders;
+ std::map<Path, File> files;
+ std::map<Path, Path> symlinks;
+};
+
+struct FileDownload : FileSource
+{
+ FileDownload(const FileSource& source, bool executable) {
+ static_cast<FileSource &> (*this) = source;
+ this->executable = executable;
+ }
+ bool executable = false;
+};
+
+struct UpdateOperations {
+ static UpdateOperations resolve(const Package & from, const Package & to);
+ bool valid = false;
+ std::vector<Path> deletes;
+ std::vector<Path> rmdirs;
+ std::vector<Path> mkdirs;
+ std::map<Path, FileDownload> downloads;
+ std::map<Path, Path> mklinks;
+ std::map<Path, bool> executable_fixes;
+};
+
+}
diff --git a/launcher/mojang/PackageManifest_test.cpp b/launcher/mojang/PackageManifest_test.cpp
new file mode 100644
index 00000000..d4c55c5a
--- /dev/null
+++ b/launcher/mojang/PackageManifest_test.cpp
@@ -0,0 +1,344 @@
+#include <QTest>
+#include <QDebug>
+#include "TestUtil.h"
+
+#include "mojang/PackageManifest.h"
+
+using namespace mojang_files;
+
+QDebug operator<<(QDebug debug, const Path &path)
+{
+ debug << path.toString();
+ return debug;
+}
+
+class PackageManifestTest : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void test_parse();
+ void test_parse_file();
+ void test_inspect();
+#ifndef Q_OS_WIN32
+ void test_inspect_symlinks();
+#endif
+ void mkdir_deep();
+ void rmdir_deep();
+
+ void identical_file();
+ void changed_file();
+ void added_file();
+ void removed_file();
+};
+
+namespace {
+QByteArray basic_manifest = R"END(
+{
+ "files": {
+ "a/b.txt": {
+ "type": "file",
+ "downloads": {
+ "raw": {
+ "url": "http://dethware.org/b.txt",
+ "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "size": 0
+ }
+ },
+ "executable": true
+ },
+ "a/b/c": {
+ "type": "directory"
+ },
+ "a/b/c.txt": {
+ "type": "link",
+ "target": "../b.txt"
+ }
+ }
+}
+)END";
+}
+
+void PackageManifestTest::test_parse()
+{
+ auto manifest = Package::fromManifestContents(basic_manifest);
+ QVERIFY(manifest.valid == true);
+ QVERIFY(manifest.files.size() == 1);
+ QVERIFY(manifest.files.count(Path("a/b.txt")));
+ auto &file = manifest.files[Path("a/b.txt")];
+ QVERIFY(file.executable == true);
+ QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
+ QVERIFY(file.size == 0);
+ QVERIFY(manifest.folders.size() == 4);
+ QVERIFY(manifest.folders.count(Path(".")));
+ QVERIFY(manifest.folders.count(Path("a")));
+ QVERIFY(manifest.folders.count(Path("a/b")));
+ QVERIFY(manifest.folders.count(Path("a/b/c")));
+ QVERIFY(manifest.symlinks.size() == 1);
+ auto symlinkPath = Path("a/b/c.txt");
+ QVERIFY(manifest.symlinks.count(symlinkPath));
+ auto &symlink = manifest.symlinks[symlinkPath];
+ QVERIFY(symlink == Path("../b.txt"));
+ QVERIFY(manifest.sources.size() == 1);
+}
+
+void PackageManifestTest::test_parse_file() {
+ auto path = QFINDTESTDATA("testdata/1.8.0_202-x64.json");
+ auto manifest = Package::fromManifestFile(path);
+ QVERIFY(manifest.valid == true);
+}
+
+
+void PackageManifestTest::test_inspect() {
+ auto path = QFINDTESTDATA("testdata/inspect_win/");
+ auto manifest = Package::fromInspectedFolder(path);
+ QVERIFY(manifest.valid == true);
+ QVERIFY(manifest.files.size() == 2);
+ QVERIFY(manifest.files.count(Path("a/b.txt")));
+ auto &file1 = manifest.files[Path("a/b.txt")];
+ QVERIFY(file1.executable == false);
+ QVERIFY(file1.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
+ QVERIFY(file1.size == 0);
+ QVERIFY(manifest.files.count(Path("a/b/b.txt")));
+ auto &file2 = manifest.files[Path("a/b/b.txt")];
+ QVERIFY(file2.executable == false);
+ QVERIFY(file2.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
+ QVERIFY(file2.size == 0);
+ QVERIFY(manifest.folders.size() == 3);
+ QVERIFY(manifest.folders.count(Path(".")));
+ QVERIFY(manifest.folders.count(Path("a")));
+ QVERIFY(manifest.folders.count(Path("a/b")));
+ QVERIFY(manifest.symlinks.size() == 0);
+}
+
+#ifndef Q_OS_WIN32
+void PackageManifestTest::test_inspect_symlinks() {
+ auto path = QFINDTESTDATA("testdata/inspect/");
+ auto manifest = Package::fromInspectedFolder(path);
+ QVERIFY(manifest.valid == true);
+ QVERIFY(manifest.files.size() == 1);
+ QVERIFY(manifest.files.count(Path("a/b.txt")));
+ auto &file = manifest.files[Path("a/b.txt")];
+ QVERIFY(file.executable == true);
+ QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709");
+ QVERIFY(file.size == 0);
+ QVERIFY(manifest.folders.size() == 3);
+ QVERIFY(manifest.folders.count(Path(".")));
+ QVERIFY(manifest.folders.count(Path("a")));
+ QVERIFY(manifest.folders.count(Path("a/b")));
+ QVERIFY(manifest.symlinks.size() == 1);
+ QVERIFY(manifest.symlinks.count(Path("a/b/b.txt")));
+ qDebug() << manifest.symlinks[Path("a/b/b.txt")];
+ QVERIFY(manifest.symlinks[Path("a/b/b.txt")] == Path("../b.txt"));
+}
+#endif
+
+void PackageManifestTest::mkdir_deep() {
+
+ Package from;
+ auto to = Package::fromManifestContents(R"END(
+{
+ "files": {
+ "a/b/c/d/e": {
+ "type": "directory"
+ }
+ }
+}
+)END");
+ auto operations = UpdateOperations::resolve(from, to);
+ QVERIFY(operations.deletes.size() == 0);
+ QVERIFY(operations.rmdirs.size() == 0);
+
+ QVERIFY(operations.mkdirs.size() == 6);
+ QVERIFY(operations.mkdirs[0] == Path("."));
+ QVERIFY(operations.mkdirs[1] == Path("a"));
+ QVERIFY(operations.mkdirs[2] == Path("a/b"));
+ QVERIFY(operations.mkdirs[3] == Path("a/b/c"));
+ QVERIFY(operations.mkdirs[4] == Path("a/b/c/d"));
+ QVERIFY(operations.mkdirs[5] == Path("a/b/c/d/e"));
+
+ QVERIFY(operations.downloads.size() == 0);
+ QVERIFY(operations.mklinks.size() == 0);
+ QVERIFY(operations.executable_fixes.size() == 0);
+}
+
+void PackageManifestTest::rmdir_deep() {
+
+ Package to;
+ auto from = Package::fromManifestContents(R"END(
+{
+ "files": {
+ "a/b/c/d/e": {
+ "type": "directory"
+ }
+ }
+}
+)END");
+ auto operations = UpdateOperations::resolve(from, to);
+ QVERIFY(operations.deletes.size() == 0);
+
+ QVERIFY(operations.rmdirs.size() == 6);
+ QVERIFY(operations.rmdirs[0] == Path("a/b/c/d/e"));
+ QVERIFY(operations.rmdirs[1] == Path("a/b/c/d"));
+ QVERIFY(operations.rmdirs[2] == Path("a/b/c"));
+ QVERIFY(operations.rmdirs[3] == Path("a/b"));
+ QVERIFY(operations.rmdirs[4] == Path("a"));
+ QVERIFY(operations.rmdirs[5] == Path("."));
+
+ QVERIFY(operations.mkdirs.size() == 0);
+ QVERIFY(operations.downloads.size() == 0);
+ QVERIFY(operations.mklinks.size() == 0);
+ QVERIFY(operations.executable_fixes.size() == 0);
+}
+
+void PackageManifestTest::identical_file() {
+ QByteArray manifest = R"END(
+{
+ "files": {
+ "a/b/c/d/empty.txt": {
+ "type": "file",
+ "downloads": {
+ "raw": {
+ "url": "http://dethware.org/empty.txt",
+ "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "size": 0
+ }
+ },
+ "executable": false
+ }
+ }
+}
+)END";
+ auto from = Package::fromManifestContents(manifest);
+ auto to = Package::fromManifestContents(manifest);
+ auto operations = UpdateOperations::resolve(from, to);
+ QVERIFY(operations.deletes.size() == 0);
+ QVERIFY(operations.rmdirs.size() == 0);
+ QVERIFY(operations.mkdirs.size() == 0);
+ QVERIFY(operations.downloads.size() == 0);
+ QVERIFY(operations.mklinks.size() == 0);
+ QVERIFY(operations.executable_fixes.size() == 0);
+}
+
+void PackageManifestTest::changed_file() {
+ auto from = Package::fromManifestContents(R"END(
+{
+ "files": {
+ "a/b/c/d/file": {
+ "type": "file",
+ "downloads": {
+ "raw": {
+ "url": "http://dethware.org/empty.txt",
+ "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "size": 0
+ }
+ },
+ "executable": false
+ }
+ }
+}
+)END");
+ auto to = Package::fromManifestContents(R"END(
+{
+ "files": {
+ "a/b/c/d/file": {
+ "type": "file",
+ "downloads": {
+ "raw": {
+ "url": "http://dethware.org/space.txt",
+ "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
+ "size": 1
+ }
+ },
+ "executable": false
+ }
+ }
+}
+)END");
+ auto operations = UpdateOperations::resolve(from, to);
+ QVERIFY(operations.deletes.size() == 1);
+ QCOMPARE(operations.deletes[0], Path("a/b/c/d/file"));
+ QVERIFY(operations.rmdirs.size() == 0);
+ QVERIFY(operations.mkdirs.size() == 0);
+ QVERIFY(operations.downloads.size() == 1);
+ QVERIFY(operations.mklinks.size() == 0);
+ QVERIFY(operations.executable_fixes.size() == 0);
+}
+
+void PackageManifestTest::added_file() {
+ auto from = Package::fromManifestContents(R"END(
+{
+ "files": {
+ "a/b/c/d": {
+ "type": "directory"
+ }
+ }
+}
+)END");
+ auto to = Package::fromManifestContents(R"END(
+{
+ "files": {
+ "a/b/c/d/file": {
+ "type": "file",
+ "downloads": {
+ "raw": {
+ "url": "http://dethware.org/space.txt",
+ "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
+ "size": 1
+ }
+ },
+ "executable": false
+ }
+ }
+}
+)END");
+ auto operations = UpdateOperations::resolve(from, to);
+ QVERIFY(operations.deletes.size() == 0);
+ QVERIFY(operations.rmdirs.size() == 0);
+ QVERIFY(operations.mkdirs.size() == 0);
+ QVERIFY(operations.downloads.size() == 1);
+ QVERIFY(operations.mklinks.size() == 0);
+ QVERIFY(operations.executable_fixes.size() == 0);
+}
+
+void PackageManifestTest::removed_file() {
+ auto from = Package::fromManifestContents(R"END(
+{
+ "files": {
+ "a/b/c/d/file": {
+ "type": "file",
+ "downloads": {
+ "raw": {
+ "url": "http://dethware.org/space.txt",
+ "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46",
+ "size": 1
+ }
+ },
+ "executable": false
+ }
+ }
+}
+)END");
+ auto to = Package::fromManifestContents(R"END(
+{
+ "files": {
+ "a/b/c/d": {
+ "type": "directory"
+ }
+ }
+}
+)END");
+ auto operations = UpdateOperations::resolve(from, to);
+ QVERIFY(operations.deletes.size() == 1);
+ QCOMPARE(operations.deletes[0], Path("a/b/c/d/file"));
+ QVERIFY(operations.rmdirs.size() == 0);
+ QVERIFY(operations.mkdirs.size() == 0);
+ QVERIFY(operations.downloads.size() == 0);
+ QVERIFY(operations.mklinks.size() == 0);
+ QVERIFY(operations.executable_fixes.size() == 0);
+}
+
+QTEST_GUILESS_MAIN(PackageManifestTest)
+
+#include "PackageManifest_test.moc"
+
diff --git a/launcher/mojang/testdata/1.8.0_202-x64.json b/launcher/mojang/testdata/1.8.0_202-x64.json
new file mode 100644
index 00000000..3d99d719
--- /dev/null
+++ b/launcher/mojang/testdata/1.8.0_202-x64.json
@@ -0,0 +1 @@
+{"files": {"COPYRIGHT": {"downloads": {"lzma": {"sha1": "dd860e040807f7e53ae89da5f28dd73d57ac605d", "size": 1431, "url": "https://launcher.mojang.com/v1/objects/dd860e040807f7e53ae89da5f28dd73d57ac605d/COPYRIGHT"}, "raw": {"sha1": "c725183c757011e7ba96c83c1e86ee7e8b516a2b", "size": 3244, "url": "https://launcher.mojang.com/v1/objects/c725183c757011e7ba96c83c1e86ee7e8b516a2b/COPYRIGHT"}}, "executable": false, "type": "file"}, "LICENSE": {"downloads": {"raw": {"sha1": "3e86865deec0814c958bcf7fb87f790bccc0e8bd", "size": 40, "url": "https://launcher.mojang.com/v1/objects/3e86865deec0814c958bcf7fb87f790bccc0e8bd/LICENSE"}}, "executable": false, "type": "file"}, "README": {"downloads": {"raw": {"sha1": "f90331df1e5badeadc501d8dd70714c62a920204", "size": 46, "url": "https://launcher.mojang.com/v1/objects/f90331df1e5badeadc501d8dd70714c62a920204/README"}}, "executable": false, "type": "file"}, "THIRDPARTYLICENSEREADME-JAVAFX.txt": {"downloads": {"lzma": {"sha1": "4fee85109d7ff04b982d0576dabd15397f599125", "size": 15455, "url": "https://launcher.mojang.com/v1/objects/4fee85109d7ff04b982d0576dabd15397f599125/THIRDPARTYLICENSEREADME-JAVAFX.txt"}, "raw": {"sha1": "56ff42f87607b997b52ae0ef8bf315e36932e870", "size": 112724, "url": "https://launcher.mojang.com/v1/objects/56ff42f87607b997b52ae0ef8bf315e36932e870/THIRDPARTYLICENSEREADME-JAVAFX.txt"}}, "executable": false, "type": "file"}, "THIRDPARTYLICENSEREADME.txt": {"downloads": {"lzma": {"sha1": "419c1414ba46ae9dbfd38cf4e0601fff61644429", "size": 32266, "url": "https://launcher.mojang.com/v1/objects/419c1414ba46ae9dbfd38cf4e0601fff61644429/THIRDPARTYLICENSEREADME.txt"}, "raw": {"sha1": "b83c3f32261de3e48ccd20614a11e066b1ec9027", "size": 153824, "url": "https://launcher.mojang.com/v1/objects/b83c3f32261de3e48ccd20614a11e066b1ec9027/THIRDPARTYLICENSEREADME.txt"}}, "executable": false, "type": "file"}, "Welcome.html": {"downloads": {"lzma": {"sha1": "01c21a74b4aafb7cbe0388233c43cbdf77dcaaea", "size": 528, "url": "https://launcher.mojang.com/v1/objects/01c21a74b4aafb7cbe0388233c43cbdf77dcaaea/Welcome.html"}, "raw": {"sha1": "d98ae54f03dac87419abc19b97e315830c2da55f", "size": 955, "url": "https://launcher.mojang.com/v1/objects/d98ae54f03dac87419abc19b97e315830c2da55f/Welcome.html"}}, "executable": false, "type": "file"}, "bin": {"type": "directory"}, "bin/ControlPanel": {"target": "jcontrol", "type": "link"}, "bin/java": {"downloads": {"lzma": {"sha1": "3857eea1d59e1bc545c67a753ed2768254807b8a", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/3857eea1d59e1bc545c67a753ed2768254807b8a/java"}, "raw": {"sha1": "3d20560fb5d1a49cb689c2226972e92e06d27ba6", "size": 8464, "url": "https://launcher.mojang.com/v1/objects/3d20560fb5d1a49cb689c2226972e92e06d27ba6/java"}}, "executable": true, "type": "file"}, "bin/javaws": {"downloads": {"lzma": {"sha1": "a6bec5c049e76c4488294a256a2084ea23ddb440", "size": 38173, "url": "https://launcher.mojang.com/v1/objects/a6bec5c049e76c4488294a256a2084ea23ddb440/javaws"}, "raw": {"sha1": "955c0f0066e2f893b0c2b3ccd83e223722e4ab74", "size": 140296, "url": "https://launcher.mojang.com/v1/objects/955c0f0066e2f893b0c2b3ccd83e223722e4ab74/javaws"}}, "executable": true, "type": "file"}, "bin/jcontrol": {"downloads": {"lzma": {"sha1": "40c5e33748f252e1d950b579a4185ab2c23fc908", "size": 2166, "url": "https://launcher.mojang.com/v1/objects/40c5e33748f252e1d950b579a4185ab2c23fc908/jcontrol"}, "raw": {"sha1": "ed541733c8b51e34349c1f8010b277e58ad73f1e", "size": 6264, "url": "https://launcher.mojang.com/v1/objects/ed541733c8b51e34349c1f8010b277e58ad73f1e/jcontrol"}}, "executable": true, "type": "file"}, "bin/jjs": {"downloads": {"lzma": {"sha1": "d44d1ac421979f7671921986214812095a5b0e3b", "size": 2168, "url": "https://launcher.mojang.com/v1/objects/d44d1ac421979f7671921986214812095a5b0e3b/jjs"}, "raw": {"sha1": "f00f944c3dbe556793b5dc686aaeee3e5722e99b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/f00f944c3dbe556793b5dc686aaeee3e5722e99b/jjs"}}, "executable": true, "type": "file"}, "bin/keytool": {"downloads": {"lzma": {"sha1": "93c607dce450976667c382f609a367167bdec05c", "size": 2175, "url": "https://launcher.mojang.com/v1/objects/93c607dce450976667c382f609a367167bdec05c/keytool"}, "raw": {"sha1": "7114b561546270e441e9ed1bcc24e5188c068a42", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/7114b561546270e441e9ed1bcc24e5188c068a42/keytool"}}, "executable": true, "type": "file"}, "bin/orbd": {"downloads": {"lzma": {"sha1": "b27dfded5e2b2f6f02c555971c94e46ca14ac81b", "size": 2254, "url": "https://launcher.mojang.com/v1/objects/b27dfded5e2b2f6f02c555971c94e46ca14ac81b/orbd"}, "raw": {"sha1": "7f31217fecb3dbbd89f1dd3783fca58793a66fd2", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/7f31217fecb3dbbd89f1dd3783fca58793a66fd2/orbd"}}, "executable": true, "type": "file"}, "bin/pack200": {"downloads": {"lzma": {"sha1": "b52da4497b49b1508b6225a5740857ddb8f52e97", "size": 2183, "url": "https://launcher.mojang.com/v1/objects/b52da4497b49b1508b6225a5740857ddb8f52e97/pack200"}, "raw": {"sha1": "16ef3e801efb57e50bc6477a27a9d95d02d0775b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/16ef3e801efb57e50bc6477a27a9d95d02d0775b/pack200"}}, "executable": true, "type": "file"}, "bin/policytool": {"downloads": {"lzma": {"sha1": "87da4c07da45f3d1a1a9d732af197cd39bf69d10", "size": 2182, "url": "https://launcher.mojang.com/v1/objects/87da4c07da45f3d1a1a9d732af197cd39bf69d10/policytool"}, "raw": {"sha1": "a52a29424470cb9b8db5c2fb1751d0b697a7ec8e", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/a52a29424470cb9b8db5c2fb1751d0b697a7ec8e/policytool"}}, "executable": true, "type": "file"}, "bin/rmid": {"downloads": {"lzma": {"sha1": "1494c1174fde0c0a93ea117bc7edf7eb936c0512", "size": 2172, "url": "https://launcher.mojang.com/v1/objects/1494c1174fde0c0a93ea117bc7edf7eb936c0512/rmid"}, "raw": {"sha1": "5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8/rmid"}}, "executable": true, "type": "file"}, "bin/rmiregistry": {"downloads": {"lzma": {"sha1": "7070cf2ec5a5e520a880bae699431edf02083e7e", "size": 2174, "url": "https://launcher.mojang.com/v1/objects/7070cf2ec5a5e520a880bae699431edf02083e7e/rmiregistry"}, "raw": {"sha1": "5f518daa7050028d5d9d849634c73136f2b23a54", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/5f518daa7050028d5d9d849634c73136f2b23a54/rmiregistry"}}, "executable": true, "type": "file"}, "bin/servertool": {"downloads": {"lzma": {"sha1": "1db683a11cc9b7313426c84412f4d95be2fa7ccd", "size": 2185, "url": "https://launcher.mojang.com/v1/objects/1db683a11cc9b7313426c84412f4d95be2fa7ccd/servertool"}, "raw": {"sha1": "49d0ebfeb265ce5a8733e1014541ea2525674a60", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/49d0ebfeb265ce5a8733e1014541ea2525674a60/servertool"}}, "executable": true, "type": "file"}, "bin/tnameserv": {"downloads": {"lzma": {"sha1": "36da9c9a2c5a8b662a3f8d52ca67339bce1c2714", "size": 2291, "url": "https://launcher.mojang.com/v1/objects/36da9c9a2c5a8b662a3f8d52ca67339bce1c2714/tnameserv"}, "raw": {"sha1": "09d998f8efcb6f55d0d87f59e08f8b89662796d9", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/09d998f8efcb6f55d0d87f59e08f8b89662796d9/tnameserv"}}, "executable": true, "type": "file"}, "bin/unpack200": {"downloads": {"lzma": {"sha1": "344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2", "size": 79721, "url": "https://launcher.mojang.com/v1/objects/344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2/unpack200"}, "raw": {"sha1": "5dd933132f1b202e19e0c8e093f7113711cfdfc1", "size": 182616, "url": "https://launcher.mojang.com/v1/objects/5dd933132f1b202e19e0c8e093f7113711cfdfc1/unpack200"}}, "executable": true, "type": "file"}, "lib": {"type": "directory"}, "lib/amd64": {"type": "directory"}, "lib/amd64/jli": {"type": "directory"}, "lib/amd64/jli/libjli.so": {"downloads": {"lzma": {"sha1": "372331ee8e375888f798a2e88180a94493e141b0", "size": 48327, "url": "https://launcher.mojang.com/v1/objects/372331ee8e375888f798a2e88180a94493e141b0/libjli.so"}, "raw": {"sha1": "73b0cf8b7415686bc40c561ff77ff2740ccf7a44", "size": 108616, "url": "https://launcher.mojang.com/v1/objects/73b0cf8b7415686bc40c561ff77ff2740ccf7a44/libjli.so"}}, "executable": true, "type": "file"}, "lib/amd64/jvm.cfg": {"downloads": {"lzma": {"sha1": "86bcfebec37b38415525ffd77d3eaf70d0b1b4ca", "size": 435, "url": "https://launcher.mojang.com/v1/objects/86bcfebec37b38415525ffd77d3eaf70d0b1b4ca/jvm.cfg"}, "raw": {"sha1": "84b38bdc745de446ba0ca0232ea3aaf2efd721da", "size": 627, "url": "https://launcher.mojang.com/v1/objects/84b38bdc745de446ba0ca0232ea3aaf2efd721da/jvm.cfg"}}, "executable": false, "type": "file"}, "lib/amd64/libavplugin-53.so": {"downloads": {"lzma": {"sha1": "a332366762d9efc7b845a682b7edce62db44618c", "size": 14747, "url": "https://launcher.mojang.com/v1/objects/a332366762d9efc7b845a682b7edce62db44618c/libavplugin-53.so"}, "raw": {"sha1": "9bd1473dd8a0dc7950c7af1cc69a45548df26eb5", "size": 51720, "url": "https://launcher.mojang.com/v1/objects/9bd1473dd8a0dc7950c7af1cc69a45548df26eb5/libavplugin-53.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-54.so": {"downloads": {"lzma": {"sha1": "2c615852a0720a275163e00597c1f711f11341da", "size": 15153, "url": "https://launcher.mojang.com/v1/objects/2c615852a0720a275163e00597c1f711f11341da/libavplugin-54.so"}, "raw": {"sha1": "8808050c5949c4800b42d1b19b1f8b0d120bcacb", "size": 51768, "url": "https://launcher.mojang.com/v1/objects/8808050c5949c4800b42d1b19b1f8b0d120bcacb/libavplugin-54.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-55.so": {"downloads": {"lzma": {"sha1": "39ee8e7fe14f0010c78973962800f539c3e4c16b", "size": 15168, "url": "https://launcher.mojang.com/v1/objects/39ee8e7fe14f0010c78973962800f539c3e4c16b/libavplugin-55.so"}, "raw": {"sha1": "f10ea4ea3489e96d8d161a96790133c417ec44e1", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/f10ea4ea3489e96d8d161a96790133c417ec44e1/libavplugin-55.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-56.so": {"downloads": {"lzma": {"sha1": "abe7feced5a559f1bdc868526dc69484e0e591a0", "size": 15169, "url": "https://launcher.mojang.com/v1/objects/abe7feced5a559f1bdc868526dc69484e0e591a0/libavplugin-56.so"}, "raw": {"sha1": "e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed/libavplugin-56.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-57.so": {"downloads": {"lzma": {"sha1": "4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9", "size": 15174, "url": "https://launcher.mojang.com/v1/objects/4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9/libavplugin-57.so"}, "raw": {"sha1": "2949e7ff9b0ac90e8943c211cff141ab12eec3f8", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/2949e7ff9b0ac90e8943c211cff141ab12eec3f8/libavplugin-57.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-ffmpeg-56.so": {"downloads": {"lzma": {"sha1": "c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6", "size": 15231, "url": "https://launcher.mojang.com/v1/objects/c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6/libavplugin-ffmpeg-56.so"}, "raw": {"sha1": "0d36c971a9ad99fc2292092fdec3a4179b1021b9", "size": 51920, "url": "https://launcher.mojang.com/v1/objects/0d36c971a9ad99fc2292092fdec3a4179b1021b9/libavplugin-ffmpeg-56.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-ffmpeg-57.so": {"downloads": {"lzma": {"sha1": "087426bdbffebcfa372a438e863785f4ffbe9a6b", "size": 15180, "url": "https://launcher.mojang.com/v1/objects/087426bdbffebcfa372a438e863785f4ffbe9a6b/libavplugin-ffmpeg-57.so"}, "raw": {"sha1": "5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462/libavplugin-ffmpeg-57.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt.so": {"downloads": {"lzma": {"sha1": "018be58b205b73c842a55df811b70d0e8237216e", "size": 195720, "url": "https://launcher.mojang.com/v1/objects/018be58b205b73c842a55df811b70d0e8237216e/libawt.so"}, "raw": {"sha1": "02632cd326e3161c00a7e784599dd7b9ee053dce", "size": 759184, "url": "https://launcher.mojang.com/v1/objects/02632cd326e3161c00a7e784599dd7b9ee053dce/libawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt_headless.so": {"downloads": {"lzma": {"sha1": "7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa", "size": 11211, "url": "https://launcher.mojang.com/v1/objects/7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa/libawt_headless.so"}, "raw": {"sha1": "862157ec957008d0911c5daedc004b3a202623a4", "size": 39176, "url": "https://launcher.mojang.com/v1/objects/862157ec957008d0911c5daedc004b3a202623a4/libawt_headless.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt_xawt.so": {"downloads": {"lzma": {"sha1": "d536a96af27dfe35de6bb2c8759d51c488cdd8d4", "size": 149598, "url": "https://launcher.mojang.com/v1/objects/d536a96af27dfe35de6bb2c8759d51c488cdd8d4/libawt_xawt.so"}, "raw": {"sha1": "28232b3e01b6f11bfe098bfc6eafc3a513dcebf1", "size": 470232, "url": "https://launcher.mojang.com/v1/objects/28232b3e01b6f11bfe098bfc6eafc3a513dcebf1/libawt_xawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libbci.so": {"downloads": {"lzma": {"sha1": "c36fad091d11e64c815d5ca17c0ef7a55b0776b1", "size": 3509, "url": "https://launcher.mojang.com/v1/objects/c36fad091d11e64c815d5ca17c0ef7a55b0776b1/libbci.so"}, "raw": {"sha1": "33824051db1ccb6332e22c2b63231055240d0af0", "size": 12760, "url": "https://launcher.mojang.com/v1/objects/33824051db1ccb6332e22c2b63231055240d0af0/libbci.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdcpr.so": {"downloads": {"lzma": {"sha1": "70c6b0933a37f2b1124d6e7c131039241fe796ee", "size": 75969, "url": "https://launcher.mojang.com/v1/objects/70c6b0933a37f2b1124d6e7c131039241fe796ee/libdcpr.so"}, "raw": {"sha1": "fa7001bc5d80579e2716590f3eee8027da0beae7", "size": 204456, "url": "https://launcher.mojang.com/v1/objects/fa7001bc5d80579e2716590f3eee8027da0beae7/libdcpr.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdecora_sse.so": {"downloads": {"lzma": {"sha1": "514acc017dfb6cefaf8cc6d18006ce55781cc9bc", "size": 24397, "url": "https://launcher.mojang.com/v1/objects/514acc017dfb6cefaf8cc6d18006ce55781cc9bc/libdecora_sse.so"}, "raw": {"sha1": "d0c84233504c916e548e29f513e25f6a7479abfc", "size": 74912, "url": "https://launcher.mojang.com/v1/objects/d0c84233504c916e548e29f513e25f6a7479abfc/libdecora_sse.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdeploy.so": {"downloads": {"lzma": {"sha1": "6cf31fd98301c749ac0d2c7825f6d925a4409760", "size": 168999, "url": "https://launcher.mojang.com/v1/objects/6cf31fd98301c749ac0d2c7825f6d925a4409760/libdeploy.so"}, "raw": {"sha1": "b3832e97ed8ca794884b56a591b83d02a2c0c06f", "size": 642368, "url": "https://launcher.mojang.com/v1/objects/b3832e97ed8ca794884b56a591b83d02a2c0c06f/libdeploy.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdt_socket.so": {"downloads": {"lzma": {"sha1": "4cc5c880dbb6fa180436d12d60f0abec8ebb59dc", "size": 7784, "url": "https://launcher.mojang.com/v1/objects/4cc5c880dbb6fa180436d12d60f0abec8ebb59dc/libdt_socket.so"}, "raw": {"sha1": "91ce96f252b8139fc12f0f224ed5b1a041767ab7", "size": 24616, "url": "https://launcher.mojang.com/v1/objects/91ce96f252b8139fc12f0f224ed5b1a041767ab7/libdt_socket.so"}}, "executable": true, "type": "file"}, "lib/amd64/libfontmanager.so": {"downloads": {"lzma": {"sha1": "f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d", "size": 146951, "url": "https://launcher.mojang.com/v1/objects/f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d/libfontmanager.so"}, "raw": {"sha1": "2428e805f2c53d1283a033dfd11a86fbb7bd7159", "size": 490672, "url": "https://launcher.mojang.com/v1/objects/2428e805f2c53d1283a033dfd11a86fbb7bd7159/libfontmanager.so"}}, "executable": true, "type": "file"}, "lib/amd64/libfxplugins.so": {"downloads": {"lzma": {"sha1": "a640143365d382a5ad743a784bc2f3706d9d6d67", "size": 50048, "url": "https://launcher.mojang.com/v1/objects/a640143365d382a5ad743a784bc2f3706d9d6d67/libfxplugins.so"}, "raw": {"sha1": "0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee", "size": 151448, "url": "https://launcher.mojang.com/v1/objects/0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee/libfxplugins.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglass.so": {"downloads": {"lzma": {"sha1": "f1ff517714fa5f2c861f33b32db823fe851541f1", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/f1ff517714fa5f2c861f33b32db823fe851541f1/libglass.so"}, "raw": {"sha1": "e7f4fece30ac727be8148d33b8256abd3a41cef9", "size": 13072, "url": "https://launcher.mojang.com/v1/objects/e7f4fece30ac727be8148d33b8256abd3a41cef9/libglass.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglassgtk2.so": {"downloads": {"lzma": {"sha1": "15b90f7a2baacd15e80aa9785d87cf1e4258376d", "size": 220476, "url": "https://launcher.mojang.com/v1/objects/15b90f7a2baacd15e80aa9785d87cf1e4258376d/libglassgtk2.so"}, "raw": {"sha1": "e30a634c2ff2143bdee512360553d6e0304f33b2", "size": 844984, "url": "https://launcher.mojang.com/v1/objects/e30a634c2ff2143bdee512360553d6e0304f33b2/libglassgtk2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglassgtk3.so": {"downloads": {"lzma": {"sha1": "868c231165f8c9043b7f0e7de208ec023f06a6e7", "size": 220560, "url": "https://launcher.mojang.com/v1/objects/868c231165f8c9043b7f0e7de208ec023f06a6e7/libglassgtk3.so"}, "raw": {"sha1": "762a11a2b376b7b5a2a7cad780715524fdd176d5", "size": 845304, "url": "https://launcher.mojang.com/v1/objects/762a11a2b376b7b5a2a7cad780715524fdd176d5/libglassgtk3.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglib-lite.so": {"downloads": {"lzma": {"sha1": "61b8871242febe1be262de167dc20ae94bf964b4", "size": 457046, "url": "https://launcher.mojang.com/v1/objects/61b8871242febe1be262de167dc20ae94bf964b4/libglib-lite.so"}, "raw": {"sha1": "63afa060fc3f120af76128e51d32603fc4336fa8", "size": 1538352, "url": "https://launcher.mojang.com/v1/objects/63afa060fc3f120af76128e51d32603fc4336fa8/libglib-lite.so"}}, "executable": true, "type": "file"}, "lib/amd64/libgstreamer-lite.so": {"downloads": {"lzma": {"sha1": "2447dc368406ba1b989a29937d41924620e01988", "size": 673056, "url": "https://launcher.mojang.com/v1/objects/2447dc368406ba1b989a29937d41924620e01988/libgstreamer-lite.so"}, "raw": {"sha1": "5505e7ca592ac64371d3db8fe53bcb602e9723d3", "size": 2263872, "url": "https://launcher.mojang.com/v1/objects/5505e7ca592ac64371d3db8fe53bcb602e9723d3/libgstreamer-lite.so"}}, "executable": true, "type": "file"}, "lib/amd64/libhprof.so": {"downloads": {"lzma": {"sha1": "94a5589c818db1fb1cf1881e24e217c309fce2e4", "size": 64471, "url": "https://launcher.mojang.com/v1/objects/94a5589c818db1fb1cf1881e24e217c309fce2e4/libhprof.so"}, "raw": {"sha1": "4bb9bdeef6133b6dd558d52d691b077c03e9b0ee", "size": 175504, "url": "https://launcher.mojang.com/v1/objects/4bb9bdeef6133b6dd558d52d691b077c03e9b0ee/libhprof.so"}}, "executable": true, "type": "file"}, "lib/amd64/libinstrument.so": {"downloads": {"lzma": {"sha1": "84ffea356caf725b42c86a8ebc9587f477ddde29", "size": 18603, "url": "https://launcher.mojang.com/v1/objects/84ffea356caf725b42c86a8ebc9587f477ddde29/libinstrument.so"}, "raw": {"sha1": "cb8009769601e3fecd7ea2b36c344f737b1a9da7", "size": 51560, "url": "https://launcher.mojang.com/v1/objects/cb8009769601e3fecd7ea2b36c344f737b1a9da7/libinstrument.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2gss.so": {"downloads": {"lzma": {"sha1": "4b2aa699506b126098b585a9617ce1c05707fa29", "size": 14132, "url": "https://launcher.mojang.com/v1/objects/4b2aa699506b126098b585a9617ce1c05707fa29/libj2gss.so"}, "raw": {"sha1": "cbce4a302b255d4d1924ef7606f038af766c5e86", "size": 47688, "url": "https://launcher.mojang.com/v1/objects/cbce4a302b255d4d1924ef7606f038af766c5e86/libj2gss.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2pcsc.so": {"downloads": {"lzma": {"sha1": "2361d3b2e3da48593c391b29b0d2b5409e4c55e5", "size": 5074, "url": "https://launcher.mojang.com/v1/objects/2361d3b2e3da48593c391b29b0d2b5409e4c55e5/libj2pcsc.so"}, "raw": {"sha1": "1274178492e7a3e997e12f67794616f7c3d8d0b9", "size": 18296, "url": "https://launcher.mojang.com/v1/objects/1274178492e7a3e997e12f67794616f7c3d8d0b9/libj2pcsc.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2pkcs11.so": {"downloads": {"lzma": {"sha1": "ef927e2790ba05931d0f0bdd63da3d275a834946", "size": 21573, "url": "https://launcher.mojang.com/v1/objects/ef927e2790ba05931d0f0bdd63da3d275a834946/libj2pkcs11.so"}, "raw": {"sha1": "bd4f2af9bfdc6168633d1920c1a1415de06bb45a", "size": 79472, "url": "https://launcher.mojang.com/v1/objects/bd4f2af9bfdc6168633d1920c1a1415de06bb45a/libj2pkcs11.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjaas_unix.so": {"downloads": {"lzma": {"sha1": "7f7e843544ee1eb1454a5826bdd4218685b79430", "size": 2404, "url": "https://launcher.mojang.com/v1/objects/7f7e843544ee1eb1454a5826bdd4218685b79430/libjaas_unix.so"}, "raw": {"sha1": "4c517925c7d464a5b719898eb0bea1b04df31f1f", "size": 8192, "url": "https://launcher.mojang.com/v1/objects/4c517925c7d464a5b719898eb0bea1b04df31f1f/libjaas_unix.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjava.so": {"downloads": {"lzma": {"sha1": "5eee7a42600a44a8bb8d6d7f510fd96a29637ac0", "size": 63113, "url": "https://launcher.mojang.com/v1/objects/5eee7a42600a44a8bb8d6d7f510fd96a29637ac0/libjava.so"}, "raw": {"sha1": "e280aeddf3fc0ec664aef7efc0e0e197a54aaf02", "size": 227672, "url": "https://launcher.mojang.com/v1/objects/e280aeddf3fc0ec664aef7efc0e0e197a54aaf02/libjava.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjava_crw_demo.so": {"downloads": {"lzma": {"sha1": "b197cf23ae3556eb0b45c663f0a8cb62408b961e", "size": 10412, "url": "https://launcher.mojang.com/v1/objects/b197cf23ae3556eb0b45c663f0a8cb62408b961e/libjava_crw_demo.so"}, "raw": {"sha1": "18f20f906977c90d0090b41dbda8dd5cfead5a4c", "size": 26144, "url": "https://launcher.mojang.com/v1/objects/18f20f906977c90d0090b41dbda8dd5cfead5a4c/libjava_crw_demo.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font.so": {"downloads": {"lzma": {"sha1": "ffbba0e5022f829412b86063d8a90f95f16709b1", "size": 5608, "url": "https://launcher.mojang.com/v1/objects/ffbba0e5022f829412b86063d8a90f95f16709b1/libjavafx_font.so"}, "raw": {"sha1": "8634a0aca612fc40420a4a7cc8af4cc46cfc6725", "size": 17104, "url": "https://launcher.mojang.com/v1/objects/8634a0aca612fc40420a4a7cc8af4cc46cfc6725/libjavafx_font.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_freetype.so": {"downloads": {"lzma": {"sha1": "310271eda8a2ac264ffc3640a9d847b49438d0bd", "size": 6942, "url": "https://launcher.mojang.com/v1/objects/310271eda8a2ac264ffc3640a9d847b49438d0bd/libjavafx_font_freetype.so"}, "raw": {"sha1": "3e7572d047c12ba2bc43acec7f98a67c20af8042", "size": 27616, "url": "https://launcher.mojang.com/v1/objects/3e7572d047c12ba2bc43acec7f98a67c20af8042/libjavafx_font_freetype.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_pango.so": {"downloads": {"lzma": {"sha1": "a7bcf0669e70b0f43099a99c81e6b6440cb40ac0", "size": 5820, "url": "https://launcher.mojang.com/v1/objects/a7bcf0669e70b0f43099a99c81e6b6440cb40ac0/libjavafx_font_pango.so"}, "raw": {"sha1": "f0b775cc9a514c7ee8b4d6fb300653ce548caf10", "size": 25560, "url": "https://launcher.mojang.com/v1/objects/f0b775cc9a514c7ee8b4d6fb300653ce548caf10/libjavafx_font_pango.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_t2k.so": {"downloads": {"lzma": {"sha1": "551c29dc7c7fc83223aa36a6187f7e0c5d650538", "size": 431450, "url": "https://launcher.mojang.com/v1/objects/551c29dc7c7fc83223aa36a6187f7e0c5d650538/libjavafx_font_t2k.so"}, "raw": {"sha1": "91e5813057c3b852d411540160f8ad05fb9f1ed3", "size": 1486128, "url": "https://launcher.mojang.com/v1/objects/91e5813057c3b852d411540160f8ad05fb9f1ed3/libjavafx_font_t2k.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_iio.so": {"downloads": {"lzma": {"sha1": "c832998fd5e06ed6dcd6428816194c350785420c", "size": 101479, "url": "https://launcher.mojang.com/v1/objects/c832998fd5e06ed6dcd6428816194c350785420c/libjavafx_iio.so"}, "raw": {"sha1": "dcdf68cb25677b76c1cf0bb94294e6e9880a6678", "size": 256336, "url": "https://launcher.mojang.com/v1/objects/dcdf68cb25677b76c1cf0bb94294e6e9880a6678/libjavafx_iio.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjawt.so": {"downloads": {"lzma": {"sha1": "c1ced6aad5c69ff444dc67d0fd7e333558953831", "size": 1872, "url": "https://launcher.mojang.com/v1/objects/c1ced6aad5c69ff444dc67d0fd7e333558953831/libjawt.so"}, "raw": {"sha1": "c5032f2c6fa40bea24e56605cf76b26a27e87b67", "size": 8048, "url": "https://launcher.mojang.com/v1/objects/c5032f2c6fa40bea24e56605cf76b26a27e87b67/libjawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjdwp.so": {"downloads": {"lzma": {"sha1": "c1aabbb3f5a624b9ad10ed871a1d83510a99b646", "size": 94884, "url": "https://launcher.mojang.com/v1/objects/c1aabbb3f5a624b9ad10ed871a1d83510a99b646/libjdwp.so"}, "raw": {"sha1": "a043e97be47937f6f552e94cf79c76c1c57f9594", "size": 272248, "url": "https://launcher.mojang.com/v1/objects/a043e97be47937f6f552e94cf79c76c1c57f9594/libjdwp.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfr.so": {"downloads": {"lzma": {"sha1": "11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7", "size": 8780, "url": "https://launcher.mojang.com/v1/objects/11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7/libjfr.so"}, "raw": {"sha1": "312392dd186b11c418183e818f1928e8685a07e5", "size": 28384, "url": "https://launcher.mojang.com/v1/objects/312392dd186b11c418183e818f1928e8685a07e5/libjfr.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfxmedia.so": {"downloads": {"lzma": {"sha1": "a4e7a126eb648ce6e5e6dc151831da37d8334139", "size": 391897, "url": "https://launcher.mojang.com/v1/objects/a4e7a126eb648ce6e5e6dc151831da37d8334139/libjfxmedia.so"}, "raw": {"sha1": "5fa54944327a6012c3d34cb5c1c4432762178dc8", "size": 1636376, "url": "https://launcher.mojang.com/v1/objects/5fa54944327a6012c3d34cb5c1c4432762178dc8/libjfxmedia.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfxwebkit.so": {"downloads": {"lzma": {"sha1": "b274debd222cdcc2ee84160ebb95144b3880bc97", "size": 20492825, "url": "https://launcher.mojang.com/v1/objects/b274debd222cdcc2ee84160ebb95144b3880bc97/libjfxwebkit.so"}, "raw": {"sha1": "ecee564c3b2f645131b35bb3004abd4caeabd291", "size": 91014584, "url": "https://launcher.mojang.com/v1/objects/ecee564c3b2f645131b35bb3004abd4caeabd291/libjfxwebkit.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjpeg.so": {"downloads": {"lzma": {"sha1": "9ad55e370c5eaaa73c3158339db3c368b1aaf0cb", "size": 113072, "url": "https://launcher.mojang.com/v1/objects/9ad55e370c5eaaa73c3158339db3c368b1aaf0cb/libjpeg.so"}, "raw": {"sha1": "651e6d53ae67db1f0efbf7f104447a9b49b7e333", "size": 292520, "url": "https://launcher.mojang.com/v1/objects/651e6d53ae67db1f0efbf7f104447a9b49b7e333/libjpeg.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsdt.so": {"downloads": {"lzma": {"sha1": "04b6d1361a34c496b5f652b2477784d69b8b6baf", "size": 3964, "url": "https://launcher.mojang.com/v1/objects/04b6d1361a34c496b5f652b2477784d69b8b6baf/libjsdt.so"}, "raw": {"sha1": "82b48a82bf6183d34cf00a0f81661b45c616f31b", "size": 12904, "url": "https://launcher.mojang.com/v1/objects/82b48a82bf6183d34cf00a0f81661b45c616f31b/libjsdt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsig.so": {"downloads": {"lzma": {"sha1": "37d3b89abde397216cc4ecb1339d8543d99b8428", "size": 3536, "url": "https://launcher.mojang.com/v1/objects/37d3b89abde397216cc4ecb1339d8543d99b8428/libjsig.so"}, "raw": {"sha1": "42e52ba1bcbe0362ab24bcf65c93797354db6fb9", "size": 13336, "url": "https://launcher.mojang.com/v1/objects/42e52ba1bcbe0362ab24bcf65c93797354db6fb9/libjsig.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsound.so": {"downloads": {"lzma": {"sha1": "7e3c565d74d8ffae716f32b05544fa4a6f108adc", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7e3c565d74d8ffae716f32b05544fa4a6f108adc/libjsound.so"}, "raw": {"sha1": "0c0fc63b92d7b83c9960fa80d45c80553ea20254", "size": 8232, "url": "https://launcher.mojang.com/v1/objects/0c0fc63b92d7b83c9960fa80d45c80553ea20254/libjsound.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsoundalsa.so": {"downloads": {"lzma": {"sha1": "b06c51858a25ff776519495f1b9b3d9f604b089f", "size": 23097, "url": "https://launcher.mojang.com/v1/objects/b06c51858a25ff776519495f1b9b3d9f604b089f/libjsoundalsa.so"}, "raw": {"sha1": "281d37f0326d4a12dc7ea316ead09c198ff7bdf7", "size": 83256, "url": "https://launcher.mojang.com/v1/objects/281d37f0326d4a12dc7ea316ead09c198ff7bdf7/libjsoundalsa.so"}}, "executable": true, "type": "file"}, "lib/amd64/liblcms.so": {"downloads": {"lzma": {"sha1": "7a239baba2086cae49114b382b74b971da02f08e", "size": 176175, "url": "https://launcher.mojang.com/v1/objects/7a239baba2086cae49114b382b74b971da02f08e/liblcms.so"}, "raw": {"sha1": "c8895cc3c3d023d9e059225969ab67954772c0a1", "size": 526872, "url": "https://launcher.mojang.com/v1/objects/c8895cc3c3d023d9e059225969ab67954772c0a1/liblcms.so"}}, "executable": true, "type": "file"}, "lib/amd64/libmanagement.so": {"downloads": {"lzma": {"sha1": "aed3fdbcefd1716abfc6a306687c8b741cbb318e", "size": 12838, "url": "https://launcher.mojang.com/v1/objects/aed3fdbcefd1716abfc6a306687c8b741cbb318e/libmanagement.so"}, "raw": {"sha1": "eba35f61e0d50e30874b7c7b335edf2d52662423", "size": 51808, "url": "https://launcher.mojang.com/v1/objects/eba35f61e0d50e30874b7c7b335edf2d52662423/libmanagement.so"}}, "executable": true, "type": "file"}, "lib/amd64/libmlib_image.so": {"downloads": {"lzma": {"sha1": "1bb181f079492d55c7a458e96488cd17fe0a7b86", "size": 310272, "url": "https://launcher.mojang.com/v1/objects/1bb181f079492d55c7a458e96488cd17fe0a7b86/libmlib_image.so"}, "raw": {"sha1": "c973c450d33873675945d4694be484e3427f58f1", "size": 1048136, "url": "https://launcher.mojang.com/v1/objects/c973c450d33873675945d4694be484e3427f58f1/libmlib_image.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnet.so": {"downloads": {"lzma": {"sha1": "9dd79703b6deb86e0321afe01c6ac508263c8312", "size": 38123, "url": "https://launcher.mojang.com/v1/objects/9dd79703b6deb86e0321afe01c6ac508263c8312/libnet.so"}, "raw": {"sha1": "b3a17b7d53fcdf1e689e1ec29ce851eee6022ead", "size": 116920, "url": "https://launcher.mojang.com/v1/objects/b3a17b7d53fcdf1e689e1ec29ce851eee6022ead/libnet.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnio.so": {"downloads": {"lzma": {"sha1": "5697c89d5d5d9b74f2e1555fcbba79dd4049e287", "size": 24445, "url": "https://launcher.mojang.com/v1/objects/5697c89d5d5d9b74f2e1555fcbba79dd4049e287/libnio.so"}, "raw": {"sha1": "573bf8f64dbcc397f8abd3e1da28f90ab0679f5b", "size": 93872, "url": "https://launcher.mojang.com/v1/objects/573bf8f64dbcc397f8abd3e1da28f90ab0679f5b/libnio.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnpjp2.so": {"downloads": {"lzma": {"sha1": "6fe53b5951ff740e7f2ef7ffe5975af26da06718", "size": 57892, "url": "https://launcher.mojang.com/v1/objects/6fe53b5951ff740e7f2ef7ffe5975af26da06718/libnpjp2.so"}, "raw": {"sha1": "2bb13c53a4280379253475e51216b97eed1d4ce3", "size": 216592, "url": "https://launcher.mojang.com/v1/objects/2bb13c53a4280379253475e51216b97eed1d4ce3/libnpjp2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnpt.so": {"downloads": {"lzma": {"sha1": "1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876", "size": 5070, "url": "https://launcher.mojang.com/v1/objects/1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876/libnpt.so"}, "raw": {"sha1": "6b1ff6b9b4624f3cc7801f221c82b8046fb76364", "size": 17504, "url": "https://launcher.mojang.com/v1/objects/6b1ff6b9b4624f3cc7801f221c82b8046fb76364/libnpt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_common.so": {"downloads": {"lzma": {"sha1": "f4aca04c90bc7505851c074a08b2c31cae1f94fa", "size": 23315, "url": "https://launcher.mojang.com/v1/objects/f4aca04c90bc7505851c074a08b2c31cae1f94fa/libprism_common.so"}, "raw": {"sha1": "b00866b6ed8646a29a334d46e297267552f27de8", "size": 59008, "url": "https://launcher.mojang.com/v1/objects/b00866b6ed8646a29a334d46e297267552f27de8/libprism_common.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_es2.so": {"downloads": {"lzma": {"sha1": "7ff4173c338c7a6f370f231670055737e032da3e", "size": 18416, "url": "https://launcher.mojang.com/v1/objects/7ff4173c338c7a6f370f231670055737e032da3e/libprism_es2.so"}, "raw": {"sha1": "1390a1dc14345e5a948148e59195d62f3a83863f", "size": 63808, "url": "https://launcher.mojang.com/v1/objects/1390a1dc14345e5a948148e59195d62f3a83863f/libprism_es2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_sw.so": {"downloads": {"lzma": {"sha1": "6728e8bf7b214067d715be6fe0325910d63c2468", "size": 29457, "url": "https://launcher.mojang.com/v1/objects/6728e8bf7b214067d715be6fe0325910d63c2468/libprism_sw.so"}, "raw": {"sha1": "7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4", "size": 71608, "url": "https://launcher.mojang.com/v1/objects/7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4/libprism_sw.so"}}, "executable": true, "type": "file"}, "lib/amd64/libresource.so": {"downloads": {"lzma": {"sha1": "1e35e63f1e74915fba620f1febf420b919d49bc5", "size": 2633, "url": "https://launcher.mojang.com/v1/objects/1e35e63f1e74915fba620f1febf420b919d49bc5/libresource.so"}, "raw": {"sha1": "57490353ad0d83ab1930355213dea269795434fe", "size": 13456, "url": "https://launcher.mojang.com/v1/objects/57490353ad0d83ab1930355213dea269795434fe/libresource.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsctp.so": {"downloads": {"lzma": {"sha1": "4340132ed250d7849a016e071be773eaedd33aa8", "size": 9332, "url": "https://launcher.mojang.com/v1/objects/4340132ed250d7849a016e071be773eaedd33aa8/libsctp.so"}, "raw": {"sha1": "4a80e743750f127c0d5a359f5cd60b97e7ee12ae", "size": 29552, "url": "https://launcher.mojang.com/v1/objects/4a80e743750f127c0d5a359f5cd60b97e7ee12ae/libsctp.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsplashscreen.so": {"downloads": {"lzma": {"sha1": "b226c8dbd73a548fc2b042ee6db6cc80e727597c", "size": 190305, "url": "https://launcher.mojang.com/v1/objects/b226c8dbd73a548fc2b042ee6db6cc80e727597c/libsplashscreen.so"}, "raw": {"sha1": "87d6a491f5ba7e6c4d972264a0c9063afea567a2", "size": 441376, "url": "https://launcher.mojang.com/v1/objects/87d6a491f5ba7e6c4d972264a0c9063afea567a2/libsplashscreen.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsunec.so": {"downloads": {"lzma": {"sha1": "6ebba98fab1e3d872d1363235b76497f6f9babdc", "size": 88829, "url": "https://launcher.mojang.com/v1/objects/6ebba98fab1e3d872d1363235b76497f6f9babdc/libsunec.so"}, "raw": {"sha1": "3b262a0a530f6e4e539aed2cd27b4de1d0ed8859", "size": 283368, "url": "https://launcher.mojang.com/v1/objects/3b262a0a530f6e4e539aed2cd27b4de1d0ed8859/libsunec.so"}}, "executable": true, "type": "file"}, "lib/amd64/libt2k.so": {"downloads": {"lzma": {"sha1": "602cb812ef0b350ccf56ce209a260ddbe3743d92", "size": 190720, "url": "https://launcher.mojang.com/v1/objects/602cb812ef0b350ccf56ce209a260ddbe3743d92/libt2k.so"}, "raw": {"sha1": "b072c56df997f61e15e6b5a43b8907b0d25c2043", "size": 504840, "url": "https://launcher.mojang.com/v1/objects/b072c56df997f61e15e6b5a43b8907b0d25c2043/libt2k.so"}}, "executable": true, "type": "file"}, "lib/amd64/libunpack.so": {"downloads": {"lzma": {"sha1": "7107b615e941074f0b14c31c88fb67200aacd37f", "size": 70308, "url": "https://launcher.mojang.com/v1/objects/7107b615e941074f0b14c31c88fb67200aacd37f/libunpack.so"}, "raw": {"sha1": "b05ff862ed87928ed91e80e5604673c5ea710a53", "size": 197712, "url": "https://launcher.mojang.com/v1/objects/b05ff862ed87928ed91e80e5604673c5ea710a53/libunpack.so"}}, "executable": true, "type": "file"}, "lib/amd64/libverify.so": {"downloads": {"lzma": {"sha1": "ecd98efb8c7da441a8c3580e8f5598f3cb4165b1", "size": 21885, "url": "https://launcher.mojang.com/v1/objects/ecd98efb8c7da441a8c3580e8f5598f3cb4165b1/libverify.so"}, "raw": {"sha1": "e2c8d92531c45ab9be69ffb72c87fa12e9e59827", "size": 66112, "url": "https://launcher.mojang.com/v1/objects/e2c8d92531c45ab9be69ffb72c87fa12e9e59827/libverify.so"}}, "executable": true, "type": "file"}, "lib/amd64/libzip.so": {"downloads": {"lzma": {"sha1": "7c562342e3f7b138dc978495447e3e6a96c2cf45", "size": 54876, "url": "https://launcher.mojang.com/v1/objects/7c562342e3f7b138dc978495447e3e6a96c2cf45/libzip.so"}, "raw": {"sha1": "5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f", "size": 127016, "url": "https://launcher.mojang.com/v1/objects/5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f/libzip.so"}}, "executable": true, "type": "file"}, "lib/amd64/server": {"type": "directory"}, "lib/amd64/server/Xusage.txt": {"downloads": {"lzma": {"sha1": "acb2da24a4c765887df83985e4c26d6be302a0a3", "size": 629, "url": "https://launcher.mojang.com/v1/objects/acb2da24a4c765887df83985e4c26d6be302a0a3/Xusage.txt"}, "raw": {"sha1": "6983727eafe140f9dd793c78aa6f3e007438243a", "size": 1423, "url": "https://launcher.mojang.com/v1/objects/6983727eafe140f9dd793c78aa6f3e007438243a/Xusage.txt"}}, "executable": false, "type": "file"}, "lib/amd64/server/libjsig.so": {"target": "../libjsig.so", "type": "link"}, "lib/amd64/server/libjvm.so": {"downloads": {"lzma": {"sha1": "d5c6f3338aaa6712f79d680ac8c3e31beebaa886", "size": 4154311, "url": "https://launcher.mojang.com/v1/objects/d5c6f3338aaa6712f79d680ac8c3e31beebaa886/libjvm.so"}, "raw": {"sha1": "23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca", "size": 17045016, "url": "https://launcher.mojang.com/v1/objects/23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca/libjvm.so"}}, "executable": true, "type": "file"}, "lib/applet": {"type": "directory"}, "lib/calendars.properties": {"downloads": {"lzma": {"sha1": "4a757c23f2942bd802a4f80235131146d9267750", "size": 558, "url": "https://launcher.mojang.com/v1/objects/4a757c23f2942bd802a4f80235131146d9267750/calendars.properties"}, "raw": {"sha1": "42ebb0988124433b8f2a6e5d9a74ed41240bcfc6", "size": 1378, "url": "https://launcher.mojang.com/v1/objects/42ebb0988124433b8f2a6e5d9a74ed41240bcfc6/calendars.properties"}}, "executable": false, "type": "file"}, "lib/charsets.jar": {"downloads": {"lzma": {"sha1": "2bf44143b2ad9d7d55045a4de4a562330c194dc0", "size": 412367, "url": "https://launcher.mojang.com/v1/objects/2bf44143b2ad9d7d55045a4de4a562330c194dc0/charsets.jar"}, "raw": {"sha1": "d73ab9f8de255a7e112ddd13622bf7f6b18c8447", "size": 3135615, "url": "https://launcher.mojang.com/v1/objects/d73ab9f8de255a7e112ddd13622bf7f6b18c8447/charsets.jar"}}, "executable": false, "type": "file"}, "lib/classlist": {"downloads": {"lzma": {"sha1": "14e7c73d21b8513b0aff8d86e5cb34c52021fbca", "size": 15024, "url": "https://launcher.mojang.com/v1/objects/14e7c73d21b8513b0aff8d86e5cb34c52021fbca/classlist"}, "raw": {"sha1": "9c0404b63c87e2fed35e3a6cd137d6cf876c42bd", "size": 84311, "url": "https://launcher.mojang.com/v1/objects/9c0404b63c87e2fed35e3a6cd137d6cf876c42bd/classlist"}}, "executable": false, "type": "file"}, "lib/cmm": {"type": "directory"}, "lib/cmm/CIEXYZ.pf": {"downloads": {"lzma": {"sha1": "fcc5ca2fd3f45cac3434b480fa3ce00007e96529", "size": 8964, "url": "https://launcher.mojang.com/v1/objects/fcc5ca2fd3f45cac3434b480fa3ce00007e96529/CIEXYZ.pf"}, "raw": {"sha1": "b7779924c70554647b87c2a86159ca7781e929f8", "size": 51236, "url": "https://launcher.mojang.com/v1/objects/b7779924c70554647b87c2a86159ca7781e929f8/CIEXYZ.pf"}}, "executable": false, "type": "file"}, "lib/cmm/GRAY.pf": {"downloads": {"lzma": {"sha1": "5388ccfe67d3131d6d02143d8e8895003ab14ff6", "size": 299, "url": "https://launcher.mojang.com/v1/objects/5388ccfe67d3131d6d02143d8e8895003ab14ff6/GRAY.pf"}, "raw": {"sha1": "27f93961d66b8230d0cdb8b166bc8b4153d5bc2d", "size": 632, "url": "https://launcher.mojang.com/v1/objects/27f93961d66b8230d0cdb8b166bc8b4153d5bc2d/GRAY.pf"}}, "executable": false, "type": "file"}, "lib/cmm/LINEAR_RGB.pf": {"downloads": {"lzma": {"sha1": "2bd90f09c8deb64b1729d6b8173c78f9e9cab27b", "size": 678, "url": "https://launcher.mojang.com/v1/objects/2bd90f09c8deb64b1729d6b8173c78f9e9cab27b/LINEAR_RGB.pf"}, "raw": {"sha1": "7913274c2f73bafcf888f09ff60990b100214ede", "size": 1044, "url": "https://launcher.mojang.com/v1/objects/7913274c2f73bafcf888f09ff60990b100214ede/LINEAR_RGB.pf"}}, "executable": false, "type": "file"}, "lib/cmm/PYCC.pf": {"downloads": {"lzma": {"sha1": "dbb2197ecff3fcdd142e9006490c8cb5c8d19af8", "size": 171521, "url": "https://launcher.mojang.com/v1/objects/dbb2197ecff3fcdd142e9006490c8cb5c8d19af8/PYCC.pf"}, "raw": {"sha1": "4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144", "size": 274474, "url": "https://launcher.mojang.com/v1/objects/4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144/PYCC.pf"}}, "executable": false, "type": "file"}, "lib/cmm/sRGB.pf": {"downloads": {"raw": {"sha1": "9eaea0911d89d63e39e95f2e2116eaec7e0bb91e", "size": 3144, "url": "https://launcher.mojang.com/v1/objects/9eaea0911d89d63e39e95f2e2116eaec7e0bb91e/sRGB.pf"}}, "executable": false, "type": "file"}, "lib/content-types.properties": {"downloads": {"lzma": {"sha1": "43a23d9a6c637c128b14cfa3feced93cbcf85b1a", "size": 1617, "url": "https://launcher.mojang.com/v1/objects/43a23d9a6c637c128b14cfa3feced93cbcf85b1a/content-types.properties"}, "raw": {"sha1": "b21698017c4a2866b5fabe59681b7592e72c83b1", "size": 5916, "url": "https://launcher.mojang.com/v1/objects/b21698017c4a2866b5fabe59681b7592e72c83b1/content-types.properties"}}, "executable": false, "type": "file"}, "lib/currency.data": {"downloads": {"lzma": {"sha1": "451b3f166ea34ef2aefbb01606ea5adcc0d65b42", "size": 1184, "url": "https://launcher.mojang.com/v1/objects/451b3f166ea34ef2aefbb01606ea5adcc0d65b42/currency.data"}, "raw": {"sha1": "bf524381a7a9b9d5bbab48069c583d2936e367a1", "size": 4134, "url": "https://launcher.mojang.com/v1/objects/bf524381a7a9b9d5bbab48069c583d2936e367a1/currency.data"}}, "executable": false, "type": "file"}, "lib/deploy": {"type": "directory"}, "lib/deploy.jar": {"downloads": {"raw": {"sha1": "fbe1de8fcd9a3d482c59414dce9311e4194766c9", "size": 2255881, "url": "https://launcher.mojang.com/v1/objects/fbe1de8fcd9a3d482c59414dce9311e4194766c9/deploy.jar"}}, "executable": false, "type": "file"}, "lib/deploy/MixedCodeMainDialog.ui": {"downloads": {"lzma": {"sha1": "7d812964343d1e978442f5c847c709784fc18fc0", "size": 683, "url": "https://launcher.mojang.com/v1/objects/7d812964343d1e978442f5c847c709784fc18fc0/MixedCodeMainDialog.ui"}, "raw": {"sha1": "c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79", "size": 4507, "url": "https://launcher.mojang.com/v1/objects/c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79/MixedCodeMainDialog.ui"}}, "executable": false, "type": "file"}, "lib/deploy/MixedCodeMainDialogJs.ui": {"downloads": {"lzma": {"sha1": "54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2", "size": 792, "url": "https://launcher.mojang.com/v1/objects/54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2/MixedCodeMainDialogJs.ui"}, "raw": {"sha1": "ad6337fb6d46750e14c12b439a5856f4b6864d0d", "size": 6110, "url": "https://launcher.mojang.com/v1/objects/ad6337fb6d46750e14c12b439a5856f4b6864d0d/MixedCodeMainDialogJs.ui"}}, "executable": false, "type": "file"}, "lib/deploy/cautionshield.icns": {"downloads": {"lzma": {"sha1": "7cea751dc168605054ec38ce8bfa71812be405c1", "size": 2333, "url": "https://launcher.mojang.com/v1/objects/7cea751dc168605054ec38ce8bfa71812be405c1/cautionshield.icns"}, "raw": {"sha1": "1de7ed5d5fc75aa1bcede088c655bee3bde64c38", "size": 3588, "url": "https://launcher.mojang.com/v1/objects/1de7ed5d5fc75aa1bcede088c655bee3bde64c38/cautionshield.icns"}}, "executable": false, "type": "file"}, "lib/deploy/ffjcext.zip": {"downloads": {"lzma": {"sha1": "80bcb9b3794f69d87dba93e90230f288e651e798", "size": 1809, "url": "https://launcher.mojang.com/v1/objects/80bcb9b3794f69d87dba93e90230f288e651e798/ffjcext.zip"}, "raw": {"sha1": "76d051ca7d3666ff25ea8eb9957a05574a45287f", "size": 13454, "url": "https://launcher.mojang.com/v1/objects/76d051ca7d3666ff25ea8eb9957a05574a45287f/ffjcext.zip"}}, "executable": false, "type": "file"}, "lib/deploy/java-icon.ico": {"downloads": {"lzma": {"sha1": "2a24f0207d7ab5976a8b0d92b4b381d49e895c9d", "size": 8468, "url": "https://launcher.mojang.com/v1/objects/2a24f0207d7ab5976a8b0d92b4b381d49e895c9d/java-icon.ico"}, "raw": {"sha1": "2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914", "size": 29926, "url": "https://launcher.mojang.com/v1/objects/2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914/java-icon.ico"}}, "executable": false, "type": "file"}, "lib/deploy/messages.properties": {"downloads": {"lzma": {"sha1": "c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4", "size": 1225, "url": "https://launcher.mojang.com/v1/objects/c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4/messages.properties"}, "raw": {"sha1": "dc52841c708e3c1eb2a044088a43396d1291bb5e", "size": 2860, "url": "https://launcher.mojang.com/v1/objects/dc52841c708e3c1eb2a044088a43396d1291bb5e/messages.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_de.properties": {"downloads": {"lzma": {"sha1": "42b42e6e1d2cb2d781f2226bde612ce519b29bc8", "size": 1394, "url": "https://launcher.mojang.com/v1/objects/42b42e6e1d2cb2d781f2226bde612ce519b29bc8/messages_de.properties"}, "raw": {"sha1": "d989fe1b8f7904888d5102294ebefd28d932ecdb", "size": 3306, "url": "https://launcher.mojang.com/v1/objects/d989fe1b8f7904888d5102294ebefd28d932ecdb/messages_de.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_es.properties": {"downloads": {"lzma": {"sha1": "c4a653e9802ca982e892b45d88c1e259c09e8c8e", "size": 1404, "url": "https://launcher.mojang.com/v1/objects/c4a653e9802ca982e892b45d88c1e259c09e8c8e/messages_es.properties"}, "raw": {"sha1": "1b0334b79db481c3a59be6915d5118d760c97baa", "size": 3600, "url": "https://launcher.mojang.com/v1/objects/1b0334b79db481c3a59be6915d5118d760c97baa/messages_es.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_fr.properties": {"downloads": {"lzma": {"sha1": "2d8dee07e3f5aab7318a22e169810b216ac44f97", "size": 1401, "url": "https://launcher.mojang.com/v1/objects/2d8dee07e3f5aab7318a22e169810b216ac44f97/messages_fr.properties"}, "raw": {"sha1": "69bd2d03c2064f8679de5b4e430ea61b567c69c5", "size": 3409, "url": "https://launcher.mojang.com/v1/objects/69bd2d03c2064f8679de5b4e430ea61b567c69c5/messages_fr.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_it.properties": {"downloads": {"lzma": {"sha1": "7c28cdd8d9e34355ba0fc03004c4f64749cae57e", "size": 1375, "url": "https://launcher.mojang.com/v1/objects/7c28cdd8d9e34355ba0fc03004c4f64749cae57e/messages_it.properties"}, "raw": {"sha1": "dbe49949308f28540a42ae6cd2ad58afbf615592", "size": 3223, "url": "https://launcher.mojang.com/v1/objects/dbe49949308f28540a42ae6cd2ad58afbf615592/messages_it.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_ja.properties": {"downloads": {"lzma": {"sha1": "9a6a4c086e48b9e615b72b6bbebb3c724d178ff4", "size": 1680, "url": "https://launcher.mojang.com/v1/objects/9a6a4c086e48b9e615b72b6bbebb3c724d178ff4/messages_ja.properties"}, "raw": {"sha1": "751170a7cdefcb1226604ac3f8196e06a04fd7ac", "size": 6349, "url": "https://launcher.mojang.com/v1/objects/751170a7cdefcb1226604ac3f8196e06a04fd7ac/messages_ja.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_ko.properties": {"downloads": {"lzma": {"sha1": "0c57c2ebfa0830f816657a384898487fc492efac", "size": 1645, "url": "https://launcher.mojang.com/v1/objects/0c57c2ebfa0830f816657a384898487fc492efac/messages_ko.properties"}, "raw": {"sha1": "bf9e055b5ab138ad6d49769e2b7630b7938848d6", "size": 5712, "url": "https://launcher.mojang.com/v1/objects/bf9e055b5ab138ad6d49769e2b7630b7938848d6/messages_ko.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_pt_BR.properties": {"downloads": {"lzma": {"sha1": "f8364dba0aa0a7e445a1a8d0e7ad66b996f70063", "size": 1388, "url": "https://launcher.mojang.com/v1/objects/f8364dba0aa0a7e445a1a8d0e7ad66b996f70063/messages_pt_BR.properties"}, "raw": {"sha1": "24e4951743521ab9a11381c77bd0cdb1ed30f5b5", "size": 3285, "url": "https://launcher.mojang.com/v1/objects/24e4951743521ab9a11381c77bd0cdb1ed30f5b5/messages_pt_BR.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_sv.properties": {"downloads": {"lzma": {"sha1": "65e5897d552258141aacf02f087c7c9c33ad0727", "size": 1355, "url": "https://launcher.mojang.com/v1/objects/65e5897d552258141aacf02f087c7c9c33ad0727/messages_sv.properties"}, "raw": {"sha1": "bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb", "size": 3384, "url": "https://launcher.mojang.com/v1/objects/bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb/messages_sv.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_CN.properties": {"downloads": {"lzma": {"sha1": "de7d39a6e6748e9f47e842c9da90f515921c222c", "size": 1506, "url": "https://launcher.mojang.com/v1/objects/de7d39a6e6748e9f47e842c9da90f515921c222c/messages_zh_CN.properties"}, "raw": {"sha1": "1c2b96673dddd3596890ef4fc22017d484a1f652", "size": 4072, "url": "https://launcher.mojang.com/v1/objects/1c2b96673dddd3596890ef4fc22017d484a1f652/messages_zh_CN.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_HK.properties": {"downloads": {"lzma": {"sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties"}, "raw": {"sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_TW.properties": {"downloads": {"lzma": {"sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties"}, "raw": {"sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties"}}, "executable": false, "type": "file"}, "lib/deploy/mixcode_s.png": {"downloads": {"raw": {"sha1": "4604e9f265eec97bccd0151c3a81afa9e69499e5", "size": 3115, "url": "https://launcher.mojang.com/v1/objects/4604e9f265eec97bccd0151c3a81afa9e69499e5/mixcode_s.png"}}, "executable": false, "type": "file"}, "lib/deploy/splash.gif": {"downloads": {"raw": {"sha1": "20e7aec75f6d036d504277542e507eb7dc24aae8", "size": 8590, "url": "https://launcher.mojang.com/v1/objects/20e7aec75f6d036d504277542e507eb7dc24aae8/splash.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash@2x.gif": {"downloads": {"raw": {"sha1": "0ae4a5bda2a6d628fac51462390b503c99509fdc", "size": 15276, "url": "https://launcher.mojang.com/v1/objects/0ae4a5bda2a6d628fac51462390b503c99509fdc/splash2x.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash_11-lic.gif": {"downloads": {"raw": {"sha1": "8def364e07f40142822df84b5bb4f50846cb5e4e", "size": 7805, "url": "https://launcher.mojang.com/v1/objects/8def364e07f40142822df84b5bb4f50846cb5e4e/splash_11-lic.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash_11@2x-lic.gif": {"downloads": {"raw": {"sha1": "d2bff9bbf7920ca743b81a0ee23b0719b4d057ca", "size": 12250, "url": "https://launcher.mojang.com/v1/objects/d2bff9bbf7920ca743b81a0ee23b0719b4d057ca/splash_11%402x-lic.gif"}}, "executable": false, "type": "file"}, "lib/desktop": {"type": "directory"}, "lib/desktop/applications": {"type": "directory"}, "lib/desktop/applications/sun-java.desktop": {"downloads": {"lzma": {"sha1": "109d1cdf165f38da92da70b403ca940192a7a9a8", "size": 536, "url": "https://launcher.mojang.com/v1/objects/109d1cdf165f38da92da70b403ca940192a7a9a8/sun-java.desktop"}, "raw": {"sha1": "d346dfe90505603ce5aff5a3c6c2e4a23d5bd990", "size": 777, "url": "https://launcher.mojang.com/v1/objects/d346dfe90505603ce5aff5a3c6c2e4a23d5bd990/sun-java.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/applications/sun-javaws.desktop": {"downloads": {"lzma": {"sha1": "5e1815e7f83515881e6998584dc6bb02c5bef09a", "size": 451, "url": "https://launcher.mojang.com/v1/objects/5e1815e7f83515881e6998584dc6bb02c5bef09a/sun-javaws.desktop"}, "raw": {"sha1": "50ce8e519b836e0f53d58ce1a359d98b6cafdda6", "size": 619, "url": "https://launcher.mojang.com/v1/objects/50ce8e519b836e0f53d58ce1a359d98b6cafdda6/sun-javaws.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/applications/sun_java.desktop": {"downloads": {"lzma": {"sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop"}, "raw": {"sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/icons": {"type": "directory"}, "lib/desktop/icons/HighContrast": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/apps": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/apps": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/apps": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/apps": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/apps": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/apps": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/apps": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/apps": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/mime": {"type": "directory"}, "lib/desktop/mime/packages": {"type": "directory"}, "lib/desktop/mime/packages/x-java-archive.xml": {"downloads": {"lzma": {"sha1": "841230729f0a59de2a1071d155d96358232b2ba1", "size": 591, "url": "https://launcher.mojang.com/v1/objects/841230729f0a59de2a1071d155d96358232b2ba1/x-java-archive.xml"}, "raw": {"sha1": "b6297fd36efa799312961f95ebf0c85c920d5037", "size": 1822, "url": "https://launcher.mojang.com/v1/objects/b6297fd36efa799312961f95ebf0c85c920d5037/x-java-archive.xml"}}, "executable": false, "type": "file"}, "lib/desktop/mime/packages/x-java-jnlp-file.xml": {"downloads": {"lzma": {"sha1": "abf9acbe7c18027c4f036c4e42bb2cf1115525fa", "size": 302, "url": "https://launcher.mojang.com/v1/objects/abf9acbe7c18027c4f036c4e42bb2cf1115525fa/x-java-jnlp-file.xml"}, "raw": {"sha1": "72f03da83ddb76c9105f619fcfa4dbdad70e6b30", "size": 412, "url": "https://launcher.mojang.com/v1/objects/72f03da83ddb76c9105f619fcfa4dbdad70e6b30/x-java-jnlp-file.xml"}}, "executable": false, "type": "file"}, "lib/ext": {"type": "directory"}, "lib/ext/cldrdata.jar": {"downloads": {"raw": {"sha1": "6cacc961942d3f02a88907aa8f2eaae8e20c95a0", "size": 3860502, "url": "https://launcher.mojang.com/v1/objects/6cacc961942d3f02a88907aa8f2eaae8e20c95a0/cldrdata.jar"}}, "executable": false, "type": "file"}, "lib/ext/dnsns.jar": {"downloads": {"raw": {"sha1": "93bebdd7514e53ae31d60c5daba673878c8822ec", "size": 8286, "url": "https://launcher.mojang.com/v1/objects/93bebdd7514e53ae31d60c5daba673878c8822ec/dnsns.jar"}}, "executable": false, "type": "file"}, "lib/ext/jaccess.jar": {"downloads": {"raw": {"sha1": "2f54879df7c29ec67c40d40cfc95c0d4a968bea1", "size": 44516, "url": "https://launcher.mojang.com/v1/objects/2f54879df7c29ec67c40d40cfc95c0d4a968bea1/jaccess.jar"}}, "executable": false, "type": "file"}, "lib/ext/jfxrt.jar": {"downloads": {"lzma": {"sha1": "a6c5b6a782ba360ada6651f5322dcab88c75711c", "size": 3374270, "url": "https://launcher.mojang.com/v1/objects/a6c5b6a782ba360ada6651f5322dcab88c75711c/jfxrt.jar"}, "raw": {"sha1": "1ad7a876f045399c23ee4ab7dee380a04ca2ac08", "size": 18508094, "url": "https://launcher.mojang.com/v1/objects/1ad7a876f045399c23ee4ab7dee380a04ca2ac08/jfxrt.jar"}}, "executable": false, "type": "file"}, "lib/ext/localedata.jar": {"downloads": {"raw": {"sha1": "0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70", "size": 1178926, "url": "https://launcher.mojang.com/v1/objects/0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70/localedata.jar"}}, "executable": false, "type": "file"}, "lib/ext/meta-index": {"downloads": {"lzma": {"sha1": "1359457529f42bacf495afcb68149ae036442dd9", "size": 594, "url": "https://launcher.mojang.com/v1/objects/1359457529f42bacf495afcb68149ae036442dd9/meta-index"}, "raw": {"sha1": "334649c6e2d5d7248211f30855e97cfcb4558851", "size": 1269, "url": "https://launcher.mojang.com/v1/objects/334649c6e2d5d7248211f30855e97cfcb4558851/meta-index"}}, "executable": false, "type": "file"}, "lib/ext/nashorn.jar": {"downloads": {"raw": {"sha1": "dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5", "size": 2023869, "url": "https://launcher.mojang.com/v1/objects/dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5/nashorn.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunec.jar": {"downloads": {"raw": {"sha1": "bf1c817820341a246f7130fe046e8310b03d04f6", "size": 41672, "url": "https://launcher.mojang.com/v1/objects/bf1c817820341a246f7130fe046e8310b03d04f6/sunec.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunjce_provider.jar": {"downloads": {"raw": {"sha1": "bb3494e4b5cb3c3e60da767207731f18b267cb34", "size": 279326, "url": "https://launcher.mojang.com/v1/objects/bb3494e4b5cb3c3e60da767207731f18b267cb34/sunjce_provider.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunpkcs11.jar": {"downloads": {"raw": {"sha1": "5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4", "size": 250218, "url": "https://launcher.mojang.com/v1/objects/5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4/sunpkcs11.jar"}}, "executable": false, "type": "file"}, "lib/ext/zipfs.jar": {"downloads": {"raw": {"sha1": "37b338f0e8e60d6396f51275130e8110816d7b30", "size": 68964, "url": "https://launcher.mojang.com/v1/objects/37b338f0e8e60d6396f51275130e8110816d7b30/zipfs.jar"}}, "executable": false, "type": "file"}, "lib/flavormap.properties": {"downloads": {"lzma": {"sha1": "2d5ef19ee77ccfc95c9413eea155cde59a48fadd", "size": 1541, "url": "https://launcher.mojang.com/v1/objects/2d5ef19ee77ccfc95c9413eea155cde59a48fadd/flavormap.properties"}, "raw": {"sha1": "4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2", "size": 3901, "url": "https://launcher.mojang.com/v1/objects/4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2/flavormap.properties"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.5.bfc": {"downloads": {"lzma": {"sha1": "5197f6e387f16458b7408134e38b06f20f625e4c", "size": 795, "url": "https://launcher.mojang.com/v1/objects/5197f6e387f16458b7408134e38b06f20f625e4c/fontconfig.RedHat.5.bfc"}, "raw": {"sha1": "fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2", "size": 4532, "url": "https://launcher.mojang.com/v1/objects/fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2/fontconfig.RedHat.5.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.5.properties.src": {"downloads": {"lzma": {"sha1": "3897ae198e96e5a687c9c9b218ff5df60c868e0d", "size": 1089, "url": "https://launcher.mojang.com/v1/objects/3897ae198e96e5a687c9c9b218ff5df60c868e0d/fontconfig.RedHat.5.properties.src"}, "raw": {"sha1": "c67d1a06cb37b66e69560c9f5e4be7cf08af0493", "size": 8841, "url": "https://launcher.mojang.com/v1/objects/c67d1a06cb37b66e69560c9f5e4be7cf08af0493/fontconfig.RedHat.5.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.6.bfc": {"downloads": {"lzma": {"sha1": "ef2f5d1f8d620be9927db45d3a28bd75777245cb", "size": 818, "url": "https://launcher.mojang.com/v1/objects/ef2f5d1f8d620be9927db45d3a28bd75777245cb/fontconfig.RedHat.6.bfc"}, "raw": {"sha1": "9ba3b3e2c621c31d0ef1b2053c80f77419a19965", "size": 4250, "url": "https://launcher.mojang.com/v1/objects/9ba3b3e2c621c31d0ef1b2053c80f77419a19965/fontconfig.RedHat.6.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.6.properties.src": {"downloads": {"lzma": {"sha1": "74f4148f9d7ec3d67bbd724834d478a72cfdb0db", "size": 1111, "url": "https://launcher.mojang.com/v1/objects/74f4148f9d7ec3d67bbd724834d478a72cfdb0db/fontconfig.RedHat.6.properties.src"}, "raw": {"sha1": "768e58d4d314621c38daf9fde6d67119f370acd9", "size": 8735, "url": "https://launcher.mojang.com/v1/objects/768e58d4d314621c38daf9fde6d67119f370acd9/fontconfig.RedHat.6.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.10.bfc": {"downloads": {"lzma": {"sha1": "d8fe9b1d8d02368dcd452de93024c6f60670eb87", "size": 1083, "url": "https://launcher.mojang.com/v1/objects/d8fe9b1d8d02368dcd452de93024c6f60670eb87/fontconfig.SuSE.10.bfc"}, "raw": {"sha1": "ffd0dfbd1553e15b11649a73a0b3f48318138e0d", "size": 6702, "url": "https://launcher.mojang.com/v1/objects/ffd0dfbd1553e15b11649a73a0b3f48318138e0d/fontconfig.SuSE.10.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.10.properties.src": {"downloads": {"lzma": {"sha1": "2c382bd741a9e23114be3da82dee8290ebfca8a9", "size": 1555, "url": "https://launcher.mojang.com/v1/objects/2c382bd741a9e23114be3da82dee8290ebfca8a9/fontconfig.SuSE.10.properties.src"}, "raw": {"sha1": "a38dbdbbc514567b8281e1aea726865f37e97894", "size": 16772, "url": "https://launcher.mojang.com/v1/objects/a38dbdbbc514567b8281e1aea726865f37e97894/fontconfig.SuSE.10.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.11.bfc": {"downloads": {"lzma": {"sha1": "2b78cbf11289c9858951fea7180696ba3b7176d6", "size": 1092, "url": "https://launcher.mojang.com/v1/objects/2b78cbf11289c9858951fea7180696ba3b7176d6/fontconfig.SuSE.11.bfc"}, "raw": {"sha1": "a4d8500fcb47f6327460a95851b1368660da8302", "size": 7032, "url": "https://launcher.mojang.com/v1/objects/a4d8500fcb47f6327460a95851b1368660da8302/fontconfig.SuSE.11.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.11.properties.src": {"downloads": {"lzma": {"sha1": "5c1635803906e2c59d36492dec724dd7ae49a5ab", "size": 1589, "url": "https://launcher.mojang.com/v1/objects/5c1635803906e2c59d36492dec724dd7ae49a5ab/fontconfig.SuSE.11.properties.src"}, "raw": {"sha1": "c4b69589e41a7279a71866a9134213be19cdf88d", "size": 16781, "url": "https://launcher.mojang.com/v1/objects/c4b69589e41a7279a71866a9134213be19cdf88d/fontconfig.SuSE.11.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.Turbo.bfc": {"downloads": {"lzma": {"sha1": "1c771325d9ee4af209a3db92294451d58962c7a4", "size": 822, "url": "https://launcher.mojang.com/v1/objects/1c771325d9ee4af209a3db92294451d58962c7a4/fontconfig.Turbo.bfc"}, "raw": {"sha1": "f24368deeb85cc9d0781083bc56e773518d72219", "size": 4668, "url": "https://launcher.mojang.com/v1/objects/f24368deeb85cc9d0781083bc56e773518d72219/fontconfig.Turbo.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.Turbo.properties.src": {"downloads": {"lzma": {"sha1": "7748ffa17e2c8a34754138efa963ba39bd1cbbb3", "size": 1113, "url": "https://launcher.mojang.com/v1/objects/7748ffa17e2c8a34754138efa963ba39bd1cbbb3/fontconfig.Turbo.properties.src"}, "raw": {"sha1": "2bb7258bed7ccd4f117e4e5f892c9b13424b0c82", "size": 9192, "url": "https://launcher.mojang.com/v1/objects/2bb7258bed7ccd4f117e4e5f892c9b13424b0c82/fontconfig.Turbo.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.bfc": {"downloads": {"lzma": {"sha1": "be6d49ee8c64f458c4f0e64254963fec48d25150", "size": 286, "url": "https://launcher.mojang.com/v1/objects/be6d49ee8c64f458c4f0e64254963fec48d25150/fontconfig.bfc"}, "raw": {"sha1": "de39b0e19637f58d92a0188122514aa7247ebb5b", "size": 1678, "url": "https://launcher.mojang.com/v1/objects/de39b0e19637f58d92a0188122514aa7247ebb5b/fontconfig.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.properties.src": {"downloads": {"lzma": {"sha1": "9498d5e00e5401200667687e826e28c60fa60ba4", "size": 417, "url": "https://launcher.mojang.com/v1/objects/9498d5e00e5401200667687e826e28c60fa60ba4/fontconfig.properties.src"}, "raw": {"sha1": "3617ff1424fd204415242565541facf862b16eb4", "size": 1938, "url": "https://launcher.mojang.com/v1/objects/3617ff1424fd204415242565541facf862b16eb4/fontconfig.properties.src"}}, "executable": false, "type": "file"}, "lib/fonts": {"type": "directory"}, "lib/fonts/LucidaBrightDemiBold.ttf": {"downloads": {"lzma": {"sha1": "4f748750831a7719440dff5457f4d207d0f24d21", "size": 42347, "url": "https://launcher.mojang.com/v1/objects/4f748750831a7719440dff5457f4d207d0f24d21/LucidaBrightDemiBold.ttf"}, "raw": {"sha1": "b5c97f985639e19a3b712193ee48b55dda581fd1", "size": 75144, "url": "https://launcher.mojang.com/v1/objects/b5c97f985639e19a3b712193ee48b55dda581fd1/LucidaBrightDemiBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightDemiItalic.ttf": {"downloads": {"lzma": {"sha1": "f82e9a688553c100ecb98412b985807ed56dff5d", "size": 43119, "url": "https://launcher.mojang.com/v1/objects/f82e9a688553c100ecb98412b985807ed56dff5d/LucidaBrightDemiItalic.ttf"}, "raw": {"sha1": "1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7", "size": 75124, "url": "https://launcher.mojang.com/v1/objects/1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7/LucidaBrightDemiItalic.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightItalic.ttf": {"downloads": {"lzma": {"sha1": "6d630df719271319c3d53f90a3d425118b908266", "size": 46206, "url": "https://launcher.mojang.com/v1/objects/6d630df719271319c3d53f90a3d425118b908266/LucidaBrightItalic.ttf"}, "raw": {"sha1": "aa5c037865c563726ecd63d61ca26443589be425", "size": 80856, "url": "https://launcher.mojang.com/v1/objects/aa5c037865c563726ecd63d61ca26443589be425/LucidaBrightItalic.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightRegular.ttf": {"downloads": {"lzma": {"sha1": "4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe", "size": 181085, "url": "https://launcher.mojang.com/v1/objects/4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe/LucidaBrightRegular.ttf"}, "raw": {"sha1": "5d7ed564791c900a8786936930ba99385653139c", "size": 344908, "url": "https://launcher.mojang.com/v1/objects/5d7ed564791c900a8786936930ba99385653139c/LucidaBrightRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaSansDemiBold.ttf": {"downloads": {"lzma": {"sha1": "079b16dc3c4918ab1f4f760b6dc5e6586c219042", "size": 173229, "url": "https://launcher.mojang.com/v1/objects/079b16dc3c4918ab1f4f760b6dc5e6586c219042/LucidaSansDemiBold.ttf"}, "raw": {"sha1": "92b79fefc35e96190250c602a8fed85276b32a95", "size": 317896, "url": "https://launcher.mojang.com/v1/objects/92b79fefc35e96190250c602a8fed85276b32a95/LucidaSansDemiBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaSansRegular.ttf": {"downloads": {"lzma": {"sha1": "64a65d7b94d7153d20957ef6d06bebb4dd0f48e4", "size": 326062, "url": "https://launcher.mojang.com/v1/objects/64a65d7b94d7153d20957ef6d06bebb4dd0f48e4/LucidaSansRegular.ttf"}, "raw": {"sha1": "39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add", "size": 698236, "url": "https://launcher.mojang.com/v1/objects/39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add/LucidaSansRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaTypewriterBold.ttf": {"downloads": {"lzma": {"sha1": "cdb017f7c34bea0802bc5ea5583aef721ed99c49", "size": 130412, "url": "https://launcher.mojang.com/v1/objects/cdb017f7c34bea0802bc5ea5583aef721ed99c49/LucidaTypewriterBold.ttf"}, "raw": {"sha1": "a5da2eb49448f461470387c939f0e69119310e0b", "size": 234068, "url": "https://launcher.mojang.com/v1/objects/a5da2eb49448f461470387c939f0e69119310e0b/LucidaTypewriterBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaTypewriterRegular.ttf": {"downloads": {"lzma": {"sha1": "aeda4a09a53783b0dc97de8e20071bea874cbfe5", "size": 135184, "url": "https://launcher.mojang.com/v1/objects/aeda4a09a53783b0dc97de8e20071bea874cbfe5/LucidaTypewriterRegular.ttf"}, "raw": {"sha1": "c144dcafe4faf2e79cfd74d8134a631f30234db1", "size": 242700, "url": "https://launcher.mojang.com/v1/objects/c144dcafe4faf2e79cfd74d8134a631f30234db1/LucidaTypewriterRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/fonts.dir": {"downloads": {"lzma": {"sha1": "68f2dd93b215ec8b8d9409d2b9c825632c6b907d", "size": 273, "url": "https://launcher.mojang.com/v1/objects/68f2dd93b215ec8b8d9409d2b9c825632c6b907d/fonts.dir"}, "raw": {"sha1": "97f40cca185c954adf5cc582345a7cb8e4c50578", "size": 4041, "url": "https://launcher.mojang.com/v1/objects/97f40cca185c954adf5cc582345a7cb8e4c50578/fonts.dir"}}, "executable": false, "type": "file"}, "lib/hijrah-config-umalqura.properties": {"downloads": {"lzma": {"sha1": "02e8d296e3b18a450f1ed1547cbf2b7275664c9a", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/02e8d296e3b18a450f1ed1547cbf2b7275664c9a/hijrah-config-umalqura.properties"}, "raw": {"sha1": "84aa425100740722e91f4725caf849e7863d12ba", "size": 13962, "url": "https://launcher.mojang.com/v1/objects/84aa425100740722e91f4725caf849e7863d12ba/hijrah-config-umalqura.properties"}}, "executable": false, "type": "file"}, "lib/images": {"type": "directory"}, "lib/images/cursors": {"type": "directory"}, "lib/images/cursors/cursors.properties": {"downloads": {"lzma": {"sha1": "612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f", "size": 385, "url": "https://launcher.mojang.com/v1/objects/612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f/cursors.properties"}, "raw": {"sha1": "f2b9a22ddd0a77869497a64f28f07e89a7d41f48", "size": 1274, "url": "https://launcher.mojang.com/v1/objects/f2b9a22ddd0a77869497a64f28f07e89a7d41f48/cursors.properties"}}, "executable": false, "type": "file"}, "lib/images/cursors/invalid32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_CopyDrop32x32.gif": {"downloads": {"raw": {"sha1": "eb7620fae702172aa663a19d170a0b929d3b11d1", "size": 158, "url": "https://launcher.mojang.com/v1/objects/eb7620fae702172aa663a19d170a0b929d3b11d1/motif_CopyDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_CopyNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_LinkDrop32x32.gif": {"downloads": {"raw": {"sha1": "9699137f990c240e714481563181069c8f6c17bb", "size": 162, "url": "https://launcher.mojang.com/v1/objects/9699137f990c240e714481563181069c8f6c17bb/motif_LinkDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_LinkNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_MoveDrop32x32.gif": {"downloads": {"raw": {"sha1": "03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b", "size": 141, "url": "https://launcher.mojang.com/v1/objects/03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b/motif_MoveDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_MoveNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/icons": {"type": "directory"}, "lib/images/icons/sun-java.png": {"downloads": {"raw": {"sha1": "d101b693aa054f51097eebdfeed8b8a6ca7b55b8", "size": 4707, "url": "https://launcher.mojang.com/v1/objects/d101b693aa054f51097eebdfeed8b8a6ca7b55b8/sun-java.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_HighContrast.png": {"downloads": {"raw": {"sha1": "a6b1e418d6b5d03719b96f61f0c5236a60970151", "size": 3729, "url": "https://launcher.mojang.com/v1/objects/a6b1e418d6b5d03719b96f61f0c5236a60970151/sun-java_HighContrast.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_HighContrastInverse.png": {"downloads": {"raw": {"sha1": "2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887", "size": 3777, "url": "https://launcher.mojang.com/v1/objects/2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887/sun-java_HighContrastInverse.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_LowContrast.png": {"downloads": {"raw": {"sha1": "7714cc4e894c3626c8da6fe742ed22b2829122d9", "size": 4012, "url": "https://launcher.mojang.com/v1/objects/7714cc4e894c3626c8da6fe742ed22b2829122d9/sun-java_LowContrast.png"}}, "executable": false, "type": "file"}, "lib/javafx.properties": {"downloads": {"raw": {"sha1": "49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d", "size": 56, "url": "https://launcher.mojang.com/v1/objects/49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d/javafx.properties"}}, "executable": false, "type": "file"}, "lib/javaws.jar": {"downloads": {"raw": {"sha1": "04fa5ae04ead65b91be5dee575497e49ffd49fe9", "size": 488118, "url": "https://launcher.mojang.com/v1/objects/04fa5ae04ead65b91be5dee575497e49ffd49fe9/javaws.jar"}}, "executable": false, "type": "file"}, "lib/jce.jar": {"downloads": {"raw": {"sha1": "5460adee09cc5fc8829c0acfc46c34670a7d70a0", "size": 115646, "url": "https://launcher.mojang.com/v1/objects/5460adee09cc5fc8829c0acfc46c34670a7d70a0/jce.jar"}}, "executable": false, "type": "file"}, "lib/jexec": {"downloads": {"lzma": {"sha1": "2d4323d4e060f8126d026ca6c03b8972aedd2fab", "size": 3311, "url": "https://launcher.mojang.com/v1/objects/2d4323d4e060f8126d026ca6c03b8972aedd2fab/jexec"}, "raw": {"sha1": "6aa01f1d8d103974164bcfaea03c04eeeefd7d41", "size": 13376, "url": "https://launcher.mojang.com/v1/objects/6aa01f1d8d103974164bcfaea03c04eeeefd7d41/jexec"}}, "executable": true, "type": "file"}, "lib/jfr": {"type": "directory"}, "lib/jfr.jar": {"downloads": {"lzma": {"sha1": "5b9d615c91c72f4fe356d9b4105946679452d1e1", "size": 137982, "url": "https://launcher.mojang.com/v1/objects/5b9d615c91c72f4fe356d9b4105946679452d1e1/jfr.jar"}, "raw": {"sha1": "0f3fd66a336703d935bdc22ad8082bc51d34e534", "size": 560713, "url": "https://launcher.mojang.com/v1/objects/0f3fd66a336703d935bdc22ad8082bc51d34e534/jfr.jar"}}, "executable": false, "type": "file"}, "lib/jfr/default.jfc": {"downloads": {"lzma": {"sha1": "373ddd878146dd8ce8991c2c5115a05a82859bdb", "size": 2207, "url": "https://launcher.mojang.com/v1/objects/373ddd878146dd8ce8991c2c5115a05a82859bdb/default.jfc"}, "raw": {"sha1": "1a64b68d0e7d43f8149faba94440be54f4f24527", "size": 20109, "url": "https://launcher.mojang.com/v1/objects/1a64b68d0e7d43f8149faba94440be54f4f24527/default.jfc"}}, "executable": false, "type": "file"}, "lib/jfr/profile.jfc": {"downloads": {"lzma": {"sha1": "3dcdc5feee3ccfb66bc8726b666944cd4bdadae3", "size": 2199, "url": "https://launcher.mojang.com/v1/objects/3dcdc5feee3ccfb66bc8726b666944cd4bdadae3/profile.jfc"}, "raw": {"sha1": "5d7d08a595f76322c51ae43ea966fbba6b69eebe", "size": 20065, "url": "https://launcher.mojang.com/v1/objects/5d7d08a595f76322c51ae43ea966fbba6b69eebe/profile.jfc"}}, "executable": false, "type": "file"}, "lib/jfxswt.jar": {"downloads": {"raw": {"sha1": "99d9a264c898d84c01e1c42565e7fe1a89dcd72d", "size": 33932, "url": "https://launcher.mojang.com/v1/objects/99d9a264c898d84c01e1c42565e7fe1a89dcd72d/jfxswt.jar"}}, "executable": false, "type": "file"}, "lib/jsse.jar": {"downloads": {"lzma": {"sha1": "94a17dfbc2e76cd12c33970a15341424f875a9ce", "size": 187549, "url": "https://launcher.mojang.com/v1/objects/94a17dfbc2e76cd12c33970a15341424f875a9ce/jsse.jar"}, "raw": {"sha1": "92c5c626e8a2d16f41272c0e404d4f992dd8310a", "size": 675599, "url": "https://launcher.mojang.com/v1/objects/92c5c626e8a2d16f41272c0e404d4f992dd8310a/jsse.jar"}}, "executable": false, "type": "file"}, "lib/jvm.hprof.txt": {"downloads": {"lzma": {"sha1": "eccdb240a815b2a83a502749339b27bb8669965b", "size": 1863, "url": "https://launcher.mojang.com/v1/objects/eccdb240a815b2a83a502749339b27bb8669965b/jvm.hprof.txt"}, "raw": {"sha1": "fbd61d52534cdd0c15df332114d469c65d001e33", "size": 4226, "url": "https://launcher.mojang.com/v1/objects/fbd61d52534cdd0c15df332114d469c65d001e33/jvm.hprof.txt"}}, "executable": false, "type": "file"}, "lib/locale": {"type": "directory"}, "lib/locale/de": {"type": "directory"}, "lib/locale/de/LC_MESSAGES": {"type": "directory"}, "lib/locale/de/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3061d922907cc557208109088fc6ab81d577ff6f", "size": 970, "url": "https://launcher.mojang.com/v1/objects/3061d922907cc557208109088fc6ab81d577ff6f/sunw_java_plugin.mo"}, "raw": {"sha1": "5b223a3d723ac1cfce63623fb109f2868d47d1b7", "size": 2483, "url": "https://launcher.mojang.com/v1/objects/5b223a3d723ac1cfce63623fb109f2868d47d1b7/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/es": {"type": "directory"}, "lib/locale/es/LC_MESSAGES": {"type": "directory"}, "lib/locale/es/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "24338049a89b323e17182b3a3006b50565d4fa0f", "size": 979, "url": "https://launcher.mojang.com/v1/objects/24338049a89b323e17182b3a3006b50565d4fa0f/sunw_java_plugin.mo"}, "raw": {"sha1": "6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1", "size": 2477, "url": "https://launcher.mojang.com/v1/objects/6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/fr": {"type": "directory"}, "lib/locale/fr/LC_MESSAGES": {"type": "directory"}, "lib/locale/fr/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "22796a48ef39f57d2d6fa70f41308e493d7f05c1", "size": 1033, "url": "https://launcher.mojang.com/v1/objects/22796a48ef39f57d2d6fa70f41308e493d7f05c1/sunw_java_plugin.mo"}, "raw": {"sha1": "d9d5b458db6e83fdf85c3526aeee3f57c4929840", "size": 2746, "url": "https://launcher.mojang.com/v1/objects/d9d5b458db6e83fdf85c3526aeee3f57c4929840/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/it": {"type": "directory"}, "lib/locale/it/LC_MESSAGES": {"type": "directory"}, "lib/locale/it/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "59a4cae38bfb8927745674d0efc2f284bc277987", "size": 958, "url": "https://launcher.mojang.com/v1/objects/59a4cae38bfb8927745674d0efc2f284bc277987/sunw_java_plugin.mo"}, "raw": {"sha1": "f6e72e3b2141ccc3dffab10ae14a754e494577ba", "size": 2434, "url": "https://launcher.mojang.com/v1/objects/f6e72e3b2141ccc3dffab10ae14a754e494577ba/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ja": {"type": "directory"}, "lib/locale/ja/LC_MESSAGES": {"type": "directory"}, "lib/locale/ja/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "7d6aeed563e1cefcf0224cf522048468088884a9", "size": 1036, "url": "https://launcher.mojang.com/v1/objects/7d6aeed563e1cefcf0224cf522048468088884a9/sunw_java_plugin.mo"}, "raw": {"sha1": "378881a8cb8dd2aebb43eacd0c68519be4f258b1", "size": 2415, "url": "https://launcher.mojang.com/v1/objects/378881a8cb8dd2aebb43eacd0c68519be4f258b1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ko": {"type": "directory"}, "lib/locale/ko.UTF-8": {"type": "directory"}, "lib/locale/ko.UTF-8/LC_MESSAGES": {"type": "directory"}, "lib/locale/ko.UTF-8/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "12ee3b21511e8497d95ea0ba9d6fe519227d0b16", "size": 1069, "url": "https://launcher.mojang.com/v1/objects/12ee3b21511e8497d95ea0ba9d6fe519227d0b16/sunw_java_plugin.mo"}, "raw": {"sha1": "cb19df01c59662dbe2f4050b1290d374b82fe1fa", "size": 2753, "url": "https://launcher.mojang.com/v1/objects/cb19df01c59662dbe2f4050b1290d374b82fe1fa/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ko/LC_MESSAGES": {"type": "directory"}, "lib/locale/ko/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "6e2e47c64c360517fd436bc79c823b5679a1efe6", "size": 996, "url": "https://launcher.mojang.com/v1/objects/6e2e47c64c360517fd436bc79c823b5679a1efe6/sunw_java_plugin.mo"}, "raw": {"sha1": "12c8a118d150c78f719314df6dec49a967af71e9", "size": 2399, "url": "https://launcher.mojang.com/v1/objects/12c8a118d150c78f719314df6dec49a967af71e9/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/pt_BR": {"type": "directory"}, "lib/locale/pt_BR/LC_MESSAGES": {"type": "directory"}, "lib/locale/pt_BR/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "bcaa7e7916493f071f1bf64bf58c6b038e3569c9", "size": 940, "url": "https://launcher.mojang.com/v1/objects/bcaa7e7916493f071f1bf64bf58c6b038e3569c9/sunw_java_plugin.mo"}, "raw": {"sha1": "a3bc0c43994c53c59bba94982cf95f6d36283dd0", "size": 2420, "url": "https://launcher.mojang.com/v1/objects/a3bc0c43994c53c59bba94982cf95f6d36283dd0/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/sv": {"type": "directory"}, "lib/locale/sv/LC_MESSAGES": {"type": "directory"}, "lib/locale/sv/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "76017835d6261fe2eedbcbe5eb08a7484c3080c5", "size": 946, "url": "https://launcher.mojang.com/v1/objects/76017835d6261fe2eedbcbe5eb08a7484c3080c5/sunw_java_plugin.mo"}, "raw": {"sha1": "09a47686edec4bbb34e82fbd08559f8bb6266544", "size": 2359, "url": "https://launcher.mojang.com/v1/objects/09a47686edec4bbb34e82fbd08559f8bb6266544/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh": {"type": "directory"}, "lib/locale/zh.GBK": {"type": "directory"}, "lib/locale/zh.GBK/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh.GBK/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo"}, "raw": {"sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo"}, "raw": {"sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_HK.BIG5HK": {"type": "directory"}, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo"}, "raw": {"sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_TW": {"type": "directory"}, "lib/locale/zh_TW.BIG5": {"type": "directory"}, "lib/locale/zh_TW.BIG5/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_TW.BIG5/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo"}, "raw": {"sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_TW/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_TW/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "c05e610e75182f0c4e77f3e7a4d9670ed62bf63c", "size": 897, "url": "https://launcher.mojang.com/v1/objects/c05e610e75182f0c4e77f3e7a4d9670ed62bf63c/sunw_java_plugin.mo"}, "raw": {"sha1": "f9b972dd059eae3cd337dfcef6a178e8ed8a7db6", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/f9b972dd059eae3cd337dfcef6a178e8ed8a7db6/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/logging.properties": {"downloads": {"lzma": {"sha1": "642202a58e5216d086ad37c0b5a633be802edc78", "size": 896, "url": "https://launcher.mojang.com/v1/objects/642202a58e5216d086ad37c0b5a633be802edc78/logging.properties"}, "raw": {"sha1": "89da8094484891f9ec1fa40c6c8b61f94c5869d0", "size": 2455, "url": "https://launcher.mojang.com/v1/objects/89da8094484891f9ec1fa40c6c8b61f94c5869d0/logging.properties"}}, "executable": false, "type": "file"}, "lib/management": {"type": "directory"}, "lib/management-agent.jar": {"downloads": {"lzma": {"sha1": "3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd", "size": 243, "url": "https://launcher.mojang.com/v1/objects/3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd/management-agent.jar"}, "raw": {"sha1": "9fbed36522aa3a80bac08a328942cbc5ef39ca8e", "size": 381, "url": "https://launcher.mojang.com/v1/objects/9fbed36522aa3a80bac08a328942cbc5ef39ca8e/management-agent.jar"}}, "executable": false, "type": "file"}, "lib/management/jmxremote.access": {"downloads": {"lzma": {"sha1": "69042ff1b14165db19c9d728614639dec16d6a31", "size": 1419, "url": "https://launcher.mojang.com/v1/objects/69042ff1b14165db19c9d728614639dec16d6a31/jmxremote.access"}, "raw": {"sha1": "21200eaad898ba4a2a8834a032efb6616fabb930", "size": 3998, "url": "https://launcher.mojang.com/v1/objects/21200eaad898ba4a2a8834a032efb6616fabb930/jmxremote.access"}}, "executable": false, "type": "file"}, "lib/management/jmxremote.password.template": {"downloads": {"lzma": {"sha1": "556c64b1e920766f8867be3964de6e49f5b81a60", "size": 1129, "url": "https://launcher.mojang.com/v1/objects/556c64b1e920766f8867be3964de6e49f5b81a60/jmxremote.password.template"}, "raw": {"sha1": "c1e0f01408bf20fbbb8b4810520c725f70050db5", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/c1e0f01408bf20fbbb8b4810520c725f70050db5/jmxremote.password.template"}}, "executable": false, "type": "file"}, "lib/management/management.properties": {"downloads": {"lzma": {"sha1": "3e52f9baa6394ca6956845424c607e5cde5d3c67", "size": 3176, "url": "https://launcher.mojang.com/v1/objects/3e52f9baa6394ca6956845424c607e5cde5d3c67/management.properties"}, "raw": {"sha1": "e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1", "size": 14630, "url": "https://launcher.mojang.com/v1/objects/e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1/management.properties"}}, "executable": false, "type": "file"}, "lib/management/snmp.acl.template": {"downloads": {"lzma": {"sha1": "9a4aa6396c3b488b0663bed5e5ecb762985669c9", "size": 1121, "url": "https://launcher.mojang.com/v1/objects/9a4aa6396c3b488b0663bed5e5ecb762985669c9/snmp.acl.template"}, "raw": {"sha1": "2e9f9ac287274532eb1f0d1afcefd7f3e97cc794", "size": 3376, "url": "https://launcher.mojang.com/v1/objects/2e9f9ac287274532eb1f0d1afcefd7f3e97cc794/snmp.acl.template"}}, "executable": false, "type": "file"}, "lib/meta-index": {"downloads": {"lzma": {"sha1": "1ac60b31362fda4725c665b591c5fbe384cbc8c1", "size": 788, "url": "https://launcher.mojang.com/v1/objects/1ac60b31362fda4725c665b591c5fbe384cbc8c1/meta-index"}, "raw": {"sha1": "bf204f09242203e713c31785158a0792f9edb600", "size": 2034, "url": "https://launcher.mojang.com/v1/objects/bf204f09242203e713c31785158a0792f9edb600/meta-index"}}, "executable": false, "type": "file"}, "lib/net.properties": {"downloads": {"lzma": {"sha1": "e9ec3981a0797bf55bb87b24d9eb651ce7e6916b", "size": 1830, "url": "https://launcher.mojang.com/v1/objects/e9ec3981a0797bf55bb87b24d9eb651ce7e6916b/net.properties"}, "raw": {"sha1": "fd9471742eb759f4478bb1de9a0dc0527265b6ea", "size": 5352, "url": "https://launcher.mojang.com/v1/objects/fd9471742eb759f4478bb1de9a0dc0527265b6ea/net.properties"}}, "executable": false, "type": "file"}, "lib/oblique-fonts": {"type": "directory"}, "lib/oblique-fonts/LucidaSansDemiOblique.ttf": {"downloads": {"lzma": {"sha1": "49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2", "size": 38580, "url": "https://launcher.mojang.com/v1/objects/49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2/LucidaSansDemiOblique.ttf"}, "raw": {"sha1": "53e4e12a675ac222469341c3dbc102464a1be4c7", "size": 91352, "url": "https://launcher.mojang.com/v1/objects/53e4e12a675ac222469341c3dbc102464a1be4c7/LucidaSansDemiOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaSansOblique.ttf": {"downloads": {"lzma": {"sha1": "553123c0edcd08035dede4ffd92b5b81c9a7538a", "size": 116575, "url": "https://launcher.mojang.com/v1/objects/553123c0edcd08035dede4ffd92b5b81c9a7538a/LucidaSansOblique.ttf"}, "raw": {"sha1": "95a195ad4fc520b3e395c85b747fc3024d118dd9", "size": 253724, "url": "https://launcher.mojang.com/v1/objects/95a195ad4fc520b3e395c85b747fc3024d118dd9/LucidaSansOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaTypewriterBoldOblique.ttf": {"downloads": {"lzma": {"sha1": "2475b08151556ad4d89bb1d2b6494c6bee9abd82", "size": 29954, "url": "https://launcher.mojang.com/v1/objects/2475b08151556ad4d89bb1d2b6494c6bee9abd82/LucidaTypewriterBoldOblique.ttf"}, "raw": {"sha1": "f331fc8b0cc494702bc46b690f2b8eed36469a02", "size": 63168, "url": "https://launcher.mojang.com/v1/objects/f331fc8b0cc494702bc46b690f2b8eed36469a02/LucidaTypewriterBoldOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaTypewriterOblique.ttf": {"downloads": {"lzma": {"sha1": "5b970bc3b7abb21dce1aa28ff7f03459d351e552", "size": 60133, "url": "https://launcher.mojang.com/v1/objects/5b970bc3b7abb21dce1aa28ff7f03459d351e552/LucidaTypewriterOblique.ttf"}, "raw": {"sha1": "f8ea00db73f8a89a27674d050edc37c2280930e1", "size": 137484, "url": "https://launcher.mojang.com/v1/objects/f8ea00db73f8a89a27674d050edc37c2280930e1/LucidaTypewriterOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/fonts.dir": {"downloads": {"lzma": {"sha1": "067528c789bd713c7c3f34e779aa6e2e8253dcf6", "size": 188, "url": "https://launcher.mojang.com/v1/objects/067528c789bd713c7c3f34e779aa6e2e8253dcf6/fonts.dir"}, "raw": {"sha1": "5aee54ffba9e33de56fd84ef64fa496b898585bb", "size": 2115, "url": "https://launcher.mojang.com/v1/objects/5aee54ffba9e33de56fd84ef64fa496b898585bb/fonts.dir"}}, "executable": false, "type": "file"}, "lib/plugin.jar": {"downloads": {"raw": {"sha1": "3f250842c79112bae5369e372025b166990820e8", "size": 950772, "url": "https://launcher.mojang.com/v1/objects/3f250842c79112bae5369e372025b166990820e8/plugin.jar"}}, "executable": false, "type": "file"}, "lib/psfont.properties.ja": {"downloads": {"lzma": {"sha1": "7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4", "size": 701, "url": "https://launcher.mojang.com/v1/objects/7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4/psfont.properties.ja"}, "raw": {"sha1": "56ed1c661eeede17b4fae8c9de7b5edbad387abc", "size": 2796, "url": "https://launcher.mojang.com/v1/objects/56ed1c661eeede17b4fae8c9de7b5edbad387abc/psfont.properties.ja"}}, "executable": false, "type": "file"}, "lib/psfontj2d.properties": {"downloads": {"lzma": {"sha1": "4252fa01af8739a3545e2b705e3383892e22ab40", "size": 2278, "url": "https://launcher.mojang.com/v1/objects/4252fa01af8739a3545e2b705e3383892e22ab40/psfontj2d.properties"}, "raw": {"sha1": "aa327a22a49967f4d74afeee6726f505f209692f", "size": 10393, "url": "https://launcher.mojang.com/v1/objects/aa327a22a49967f4d74afeee6726f505f209692f/psfontj2d.properties"}}, "executable": false, "type": "file"}, "lib/resources.jar": {"downloads": {"lzma": {"sha1": "1b0e08441750dc17efe4b527aa146da6cc14e8a6", "size": 579294, "url": "https://launcher.mojang.com/v1/objects/1b0e08441750dc17efe4b527aa146da6cc14e8a6/resources.jar"}, "raw": {"sha1": "daa021906e4648d4c37e798c11733dc2047f2da1", "size": 3505206, "url": "https://launcher.mojang.com/v1/objects/daa021906e4648d4c37e798c11733dc2047f2da1/resources.jar"}}, "executable": false, "type": "file"}, "lib/rt.jar": {"downloads": {"lzma": {"sha1": "fc4a8681aeda29c2a2a3fd11bad7729543283f3d", "size": 14378994, "url": "https://launcher.mojang.com/v1/objects/fc4a8681aeda29c2a2a3fd11bad7729543283f3d/rt.jar"}, "raw": {"sha1": "5396b0954a20f3210f1f4f1886ead30880d6ebfe", "size": 66334986, "url": "https://launcher.mojang.com/v1/objects/5396b0954a20f3210f1f4f1886ead30880d6ebfe/rt.jar"}}, "executable": false, "type": "file"}, "lib/security": {"type": "directory"}, "lib/security/blacklist": {"downloads": {"lzma": {"sha1": "8206fce6c1d91a39fdf78e8e79e953913994a1cd", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/8206fce6c1d91a39fdf78e8e79e953913994a1cd/blacklist"}, "raw": {"sha1": "d4ffb3857eab403955ce9d156e46d056061e6a5a", "size": 4054, "url": "https://launcher.mojang.com/v1/objects/d4ffb3857eab403955ce9d156e46d056061e6a5a/blacklist"}}, "executable": false, "type": "file"}, "lib/security/blacklisted.certs": {"downloads": {"lzma": {"sha1": "8311bead054caf6cfe678d4b7998de4caaabfa53", "size": 806, "url": "https://launcher.mojang.com/v1/objects/8311bead054caf6cfe678d4b7998de4caaabfa53/blacklisted.certs"}, "raw": {"sha1": "c5c005c29a80493f5c31cd7eb629ac1b9c752404", "size": 1273, "url": "https://launcher.mojang.com/v1/objects/c5c005c29a80493f5c31cd7eb629ac1b9c752404/blacklisted.certs"}}, "executable": false, "type": "file"}, "lib/security/cacerts": {"downloads": {"lzma": {"sha1": "654dd94809655d5b28385cbb5eba8d6ad9f2c1aa", "size": 67802, "url": "https://launcher.mojang.com/v1/objects/654dd94809655d5b28385cbb5eba8d6ad9f2c1aa/cacerts"}, "raw": {"sha1": "2917859c443c68e19f93abcd1315c3c2904cbef9", "size": 104430, "url": "https://launcher.mojang.com/v1/objects/2917859c443c68e19f93abcd1315c3c2904cbef9/cacerts"}}, "executable": false, "type": "file"}, "lib/security/java.policy": {"downloads": {"lzma": {"sha1": "b601c420d02ef3dbd8595453d08fdef91134e8b5", "size": 647, "url": "https://launcher.mojang.com/v1/objects/b601c420d02ef3dbd8595453d08fdef91134e8b5/java.policy"}, "raw": {"sha1": "c0112209a567b3b523cfed7041709f9440227968", "size": 2466, "url": "https://launcher.mojang.com/v1/objects/c0112209a567b3b523cfed7041709f9440227968/java.policy"}}, "executable": false, "type": "file"}, "lib/security/java.security": {"downloads": {"lzma": {"sha1": "531620e82ca0365ce8dc97096bb0ac5a7ace5952", "size": 10959, "url": "https://launcher.mojang.com/v1/objects/531620e82ca0365ce8dc97096bb0ac5a7ace5952/java.security"}, "raw": {"sha1": "5dcc17a168c53d0b366784e520bd4d55aa61ac18", "size": 41528, "url": "https://launcher.mojang.com/v1/objects/5dcc17a168c53d0b366784e520bd4d55aa61ac18/java.security"}}, "executable": false, "type": "file"}, "lib/security/javaws.policy": {"downloads": {"raw": {"sha1": "4384ca5e4d32f7dd86d8baddd1e690730d74e694", "size": 98, "url": "https://launcher.mojang.com/v1/objects/4384ca5e4d32f7dd86d8baddd1e690730d74e694/javaws.policy"}}, "executable": false, "type": "file"}, "lib/security/policy": {"type": "directory"}, "lib/security/policy/limited": {"type": "directory"}, "lib/security/policy/limited/US_export_policy.jar": {"downloads": {"raw": {"sha1": "7d69ea3b385bc067738520f1b5c549e1084be285", "size": 3026, "url": "https://launcher.mojang.com/v1/objects/7d69ea3b385bc067738520f1b5c549e1084be285/US_export_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/limited/local_policy.jar": {"downloads": {"raw": {"sha1": "238b8826e110f58acb2e1959773b0a577cd4d569", "size": 3527, "url": "https://launcher.mojang.com/v1/objects/238b8826e110f58acb2e1959773b0a577cd4d569/local_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/unlimited": {"type": "directory"}, "lib/security/policy/unlimited/US_export_policy.jar": {"downloads": {"raw": {"sha1": "f6fb2af1e87fc622cda194a7d6b5f5f069653ff1", "size": 3023, "url": "https://launcher.mojang.com/v1/objects/f6fb2af1e87fc622cda194a7d6b5f5f069653ff1/US_export_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/unlimited/local_policy.jar": {"downloads": {"raw": {"sha1": "517368ab2cbaf6b42ea0b963f98eeedd996e83e3", "size": 3035, "url": "https://launcher.mojang.com/v1/objects/517368ab2cbaf6b42ea0b963f98eeedd996e83e3/local_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/trusted.libraries": {"downloads": {"raw": {"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "size": 0, "url": "https://launcher.mojang.com/v1/objects/da39a3ee5e6b4b0d3255bfef95601890afd80709/trusted.libraries"}}, "executable": false, "type": "file"}, "lib/sound.properties": {"downloads": {"lzma": {"sha1": "3b5f7e4ec437d79048af35094290577f483b3fe1", "size": 473, "url": "https://launcher.mojang.com/v1/objects/3b5f7e4ec437d79048af35094290577f483b3fe1/sound.properties"}, "raw": {"sha1": "9afceb218059d981d0fa9f07aad3c5097cf41b0c", "size": 1210, "url": "https://launcher.mojang.com/v1/objects/9afceb218059d981d0fa9f07aad3c5097cf41b0c/sound.properties"}}, "executable": false, "type": "file"}, "lib/tzdb.dat": {"downloads": {"lzma": {"sha1": "39c69339965484afe89c14111baeeb862fdefd97", "size": 32547, "url": "https://launcher.mojang.com/v1/objects/39c69339965484afe89c14111baeeb862fdefd97/tzdb.dat"}, "raw": {"sha1": "b59c07e3619271a3b9861e999f4b138e971baf69", "size": 105734, "url": "https://launcher.mojang.com/v1/objects/b59c07e3619271a3b9861e999f4b138e971baf69/tzdb.dat"}}, "executable": false, "type": "file"}, "man": {"type": "directory"}, "man/ja": {"target": "ja_JP.UTF-8", "type": "link"}, "man/ja_JP.UTF-8": {"type": "directory"}, "man/ja_JP.UTF-8/man1": {"type": "directory"}, "man/ja_JP.UTF-8/man1/java.1": {"downloads": {"lzma": {"sha1": "f9da09710b6c6df23c256e324a0c4df00a0d6ded", "size": 25461, "url": "https://launcher.mojang.com/v1/objects/f9da09710b6c6df23c256e324a0c4df00a0d6ded/java.1"}, "raw": {"sha1": "b0b12a0bb66e6171771ca4b1dfca32fb759bcaec", "size": 148688, "url": "https://launcher.mojang.com/v1/objects/b0b12a0bb66e6171771ca4b1dfca32fb759bcaec/java.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/javaws.1": {"downloads": {"lzma": {"sha1": "6188fae453ca09ccb19be5c9f4d2059926b36267", "size": 2154, "url": "https://launcher.mojang.com/v1/objects/6188fae453ca09ccb19be5c9f4d2059926b36267/javaws.1"}, "raw": {"sha1": "8f39d928870268ace07bedfebd18db1e1d07fc37", "size": 6641, "url": "https://launcher.mojang.com/v1/objects/8f39d928870268ace07bedfebd18db1e1d07fc37/javaws.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/jjs.1": {"downloads": {"lzma": {"sha1": "6e42b989d28b185dc1aab50c0389834e649a37d4", "size": 3452, "url": "https://launcher.mojang.com/v1/objects/6e42b989d28b185dc1aab50c0389834e649a37d4/jjs.1"}, "raw": {"sha1": "e023322a2013912315a2bd1034e6f829a27c76e0", "size": 11365, "url": "https://launcher.mojang.com/v1/objects/e023322a2013912315a2bd1034e6f829a27c76e0/jjs.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/keytool.1": {"downloads": {"lzma": {"sha1": "a78134a4bddd53d684a70aa677e51a215db1c9cb", "size": 20698, "url": "https://launcher.mojang.com/v1/objects/a78134a4bddd53d684a70aa677e51a215db1c9cb/keytool.1"}, "raw": {"sha1": "148583c837eaaf6333ccfd8c9e8df08574e14b0c", "size": 111033, "url": "https://launcher.mojang.com/v1/objects/148583c837eaaf6333ccfd8c9e8df08574e14b0c/keytool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/orbd.1": {"downloads": {"lzma": {"sha1": "326af0dcbff173ef8aee29163dbe146d7389cc3e", "size": 4225, "url": "https://launcher.mojang.com/v1/objects/326af0dcbff173ef8aee29163dbe146d7389cc3e/orbd.1"}, "raw": {"sha1": "95651622d33c08286858ec337edd3ea72acd93dc", "size": 16092, "url": "https://launcher.mojang.com/v1/objects/95651622d33c08286858ec337edd3ea72acd93dc/orbd.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/pack200.1": {"downloads": {"lzma": {"sha1": "e0eedafa748c61a44e5be4355fe9d44b05048e80", "size": 4293, "url": "https://launcher.mojang.com/v1/objects/e0eedafa748c61a44e5be4355fe9d44b05048e80/pack200.1"}, "raw": {"sha1": "aa21a0ab75707f7fc66e83c7a392e69b37ddf80e", "size": 14482, "url": "https://launcher.mojang.com/v1/objects/aa21a0ab75707f7fc66e83c7a392e69b37ddf80e/pack200.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/policytool.1": {"downloads": {"lzma": {"sha1": "3c766ed12dab58166169d35680c392a6be1814a1", "size": 1380, "url": "https://launcher.mojang.com/v1/objects/3c766ed12dab58166169d35680c392a6be1814a1/policytool.1"}, "raw": {"sha1": "80879c74e072a98fad6f32b3283331aaf9bd002f", "size": 4020, "url": "https://launcher.mojang.com/v1/objects/80879c74e072a98fad6f32b3283331aaf9bd002f/policytool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/rmid.1": {"downloads": {"lzma": {"sha1": "1e20779d990beacc32a48237777d670fcc47ca14", "size": 4836, "url": "https://launcher.mojang.com/v1/objects/1e20779d990beacc32a48237777d670fcc47ca14/rmid.1"}, "raw": {"sha1": "7e40cb8003d098d6e36f45640b26f979ac94b5c5", "size": 19715, "url": "https://launcher.mojang.com/v1/objects/7e40cb8003d098d6e36f45640b26f979ac94b5c5/rmid.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/rmiregistry.1": {"downloads": {"lzma": {"sha1": "aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9", "size": 1627, "url": "https://launcher.mojang.com/v1/objects/aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9/rmiregistry.1"}, "raw": {"sha1": "c53c52f3ae7a011c135894c9fc51b741e729c33d", "size": 4557, "url": "https://launcher.mojang.com/v1/objects/c53c52f3ae7a011c135894c9fc51b741e729c33d/rmiregistry.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/servertool.1": {"downloads": {"lzma": {"sha1": "3b9e624e9d1cf2959b438a35061162e2100ddecd", "size": 2626, "url": "https://launcher.mojang.com/v1/objects/3b9e624e9d1cf2959b438a35061162e2100ddecd/servertool.1"}, "raw": {"sha1": "50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e", "size": 9081, "url": "https://launcher.mojang.com/v1/objects/50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e/servertool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/tnameserv.1": {"downloads": {"lzma": {"sha1": "bb3106ff74c60a76de3d20659b9c2128c70f3bf2", "size": 4478, "url": "https://launcher.mojang.com/v1/objects/bb3106ff74c60a76de3d20659b9c2128c70f3bf2/tnameserv.1"}, "raw": {"sha1": "01e714671ecd1167edcb5310b16a9c59c33c3eaa", "size": 17722, "url": "https://launcher.mojang.com/v1/objects/01e714671ecd1167edcb5310b16a9c59c33c3eaa/tnameserv.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/unpack200.1": {"downloads": {"lzma": {"sha1": "c115a881cf800b08df294df55d9f250ae944e33c", "size": 1973, "url": "https://launcher.mojang.com/v1/objects/c115a881cf800b08df294df55d9f250ae944e33c/unpack200.1"}, "raw": {"sha1": "7c882bba0067367a41ad84868d18793b8a7397a3", "size": 5382, "url": "https://launcher.mojang.com/v1/objects/7c882bba0067367a41ad84868d18793b8a7397a3/unpack200.1"}}, "executable": false, "type": "file"}, "man/man1": {"type": "directory"}, "man/man1/java.1": {"downloads": {"lzma": {"sha1": "06a6b0275c202bf698d73ca71f95618d56d81c15", "size": 25796, "url": "https://launcher.mojang.com/v1/objects/06a6b0275c202bf698d73ca71f95618d56d81c15/java.1"}, "raw": {"sha1": "69fec7a341aa91f18dbdcdb95952dede7e1b689a", "size": 124796, "url": "https://launcher.mojang.com/v1/objects/69fec7a341aa91f18dbdcdb95952dede7e1b689a/java.1"}}, "executable": false, "type": "file"}, "man/man1/javaws.1": {"downloads": {"lzma": {"sha1": "4bae251c6dfb5420f56928815cf80d0b6d517a1f", "size": 1759, "url": "https://launcher.mojang.com/v1/objects/4bae251c6dfb5420f56928815cf80d0b6d517a1f/javaws.1"}, "raw": {"sha1": "e61e44e101b1bc119c2d2d4b10320f38b36a8036", "size": 4897, "url": "https://launcher.mojang.com/v1/objects/e61e44e101b1bc119c2d2d4b10320f38b36a8036/javaws.1"}}, "executable": false, "type": "file"}, "man/man1/jjs.1": {"downloads": {"lzma": {"sha1": "29683cf2bd47015c9461b688749ddffd95f6671d", "size": 1881, "url": "https://launcher.mojang.com/v1/objects/29683cf2bd47015c9461b688749ddffd95f6671d/jjs.1"}, "raw": {"sha1": "78d419bd3a7f3e0802d5220e690429194b5d1beb", "size": 4932, "url": "https://launcher.mojang.com/v1/objects/78d419bd3a7f3e0802d5220e690429194b5d1beb/jjs.1"}}, "executable": false, "type": "file"}, "man/man1/keytool.1": {"downloads": {"lzma": {"sha1": "b67e5126d43713ee3675706724b34061578b42db", "size": 19690, "url": "https://launcher.mojang.com/v1/objects/b67e5126d43713ee3675706724b34061578b42db/keytool.1"}, "raw": {"sha1": "4c976f86057ab779763fcfb98f5702ebef47f629", "size": 86925, "url": "https://launcher.mojang.com/v1/objects/4c976f86057ab779763fcfb98f5702ebef47f629/keytool.1"}}, "executable": false, "type": "file"}, "man/man1/orbd.1": {"downloads": {"lzma": {"sha1": "147064d6f7e027002e296bb246ae572d0ce0495b", "size": 3708, "url": "https://launcher.mojang.com/v1/objects/147064d6f7e027002e296bb246ae572d0ce0495b/orbd.1"}, "raw": {"sha1": "64201e1846fcf1dcc45c786ffeab89426d1c7742", "size": 12180, "url": "https://launcher.mojang.com/v1/objects/64201e1846fcf1dcc45c786ffeab89426d1c7742/orbd.1"}}, "executable": false, "type": "file"}, "man/man1/pack200.1": {"downloads": {"lzma": {"sha1": "fe17486bbe9c58cf4182fa056b9cd124e8295607", "size": 3724, "url": "https://launcher.mojang.com/v1/objects/fe17486bbe9c58cf4182fa056b9cd124e8295607/pack200.1"}, "raw": {"sha1": "26826cf52b89924f2d2a60d6cda798891875eae6", "size": 11623, "url": "https://launcher.mojang.com/v1/objects/26826cf52b89924f2d2a60d6cda798891875eae6/pack200.1"}}, "executable": false, "type": "file"}, "man/man1/policytool.1": {"downloads": {"lzma": {"sha1": "bd154e7c39aca71d15b2098c588866f8d95bc743", "size": 1122, "url": "https://launcher.mojang.com/v1/objects/bd154e7c39aca71d15b2098c588866f8d95bc743/policytool.1"}, "raw": {"sha1": "ab296625155d9a2b25ecc2b4feff2f741b3ad136", "size": 3235, "url": "https://launcher.mojang.com/v1/objects/ab296625155d9a2b25ecc2b4feff2f741b3ad136/policytool.1"}}, "executable": false, "type": "file"}, "man/man1/rmid.1": {"downloads": {"lzma": {"sha1": "6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa", "size": 4255, "url": "https://launcher.mojang.com/v1/objects/6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa/rmid.1"}, "raw": {"sha1": "6f10e214d7950a6a8460524e41dc700f112f89e5", "size": 15979, "url": "https://launcher.mojang.com/v1/objects/6f10e214d7950a6a8460524e41dc700f112f89e5/rmid.1"}}, "executable": false, "type": "file"}, "man/man1/rmiregistry.1": {"downloads": {"lzma": {"sha1": "f40dd17e3a734600ad1828b0c42d3a1685c4c520", "size": 1301, "url": "https://launcher.mojang.com/v1/objects/f40dd17e3a734600ad1828b0c42d3a1685c4c520/rmiregistry.1"}, "raw": {"sha1": "d9a3d23fab689df5bb9a792b88f462f939b49f70", "size": 3449, "url": "https://launcher.mojang.com/v1/objects/d9a3d23fab689df5bb9a792b88f462f939b49f70/rmiregistry.1"}}, "executable": false, "type": "file"}, "man/man1/servertool.1": {"downloads": {"lzma": {"sha1": "74f1e10712202cd3ca0ff5833de05b7ee67092e1", "size": 2307, "url": "https://launcher.mojang.com/v1/objects/74f1e10712202cd3ca0ff5833de05b7ee67092e1/servertool.1"}, "raw": {"sha1": "e6c7b510740ac8681a9bfb5f4ee1f0306125b728", "size": 7237, "url": "https://launcher.mojang.com/v1/objects/e6c7b510740ac8681a9bfb5f4ee1f0306125b728/servertool.1"}}, "executable": false, "type": "file"}, "man/man1/tnameserv.1": {"downloads": {"lzma": {"sha1": "4bec7f4e070d023f124f9352a8971d7acd249a15", "size": 3955, "url": "https://launcher.mojang.com/v1/objects/4bec7f4e070d023f124f9352a8971d7acd249a15/tnameserv.1"}, "raw": {"sha1": "a31dbbe800d49cb371fab9a4b73d22c3bf8799ad", "size": 15747, "url": "https://launcher.mojang.com/v1/objects/a31dbbe800d49cb371fab9a4b73d22c3bf8799ad/tnameserv.1"}}, "executable": false, "type": "file"}, "man/man1/unpack200.1": {"downloads": {"lzma": {"sha1": "f8e73863187929debf2ea6dadefb2995ec7917e7", "size": 1672, "url": "https://launcher.mojang.com/v1/objects/f8e73863187929debf2ea6dadefb2995ec7917e7/unpack200.1"}, "raw": {"sha1": "437f7233d738cb9b822e99003127049005663e0f", "size": 4244, "url": "https://launcher.mojang.com/v1/objects/437f7233d738cb9b822e99003127049005663e0f/unpack200.1"}}, "executable": false, "type": "file"}, "plugin": {"type": "directory"}, "plugin/desktop": {"type": "directory"}, "plugin/desktop/sun_java.desktop": {"downloads": {"lzma": {"sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop"}, "raw": {"sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop"}}, "executable": false, "type": "file"}, "plugin/desktop/sun_java.png": {"downloads": {"raw": {"sha1": "699c41e97a35414e72a80327a54d6e14e874e951", "size": 4351, "url": "https://launcher.mojang.com/v1/objects/699c41e97a35414e72a80327a54d6e14e874e951/sun_java.png"}}, "executable": false, "type": "file"}, "release": {"downloads": {"raw": {"sha1": "cb462682644c0275d94a45b759108815f3112064", "size": 424, "url": "https://launcher.mojang.com/v1/objects/cb462682644c0275d94a45b759108815f3112064/release"}}, "executable": false, "type": "file"}}} \ No newline at end of file
diff --git a/launcher/mojang/testdata/inspect/a/b.txt b/launcher/mojang/testdata/inspect/a/b.txt
new file mode 100755
index 00000000..e69de29b
--- /dev/null
+++ b/launcher/mojang/testdata/inspect/a/b.txt
diff --git a/launcher/mojang/testdata/inspect/a/b/b.txt b/launcher/mojang/testdata/inspect/a/b/b.txt
new file mode 120000
index 00000000..4e19a044
--- /dev/null
+++ b/launcher/mojang/testdata/inspect/a/b/b.txt
@@ -0,0 +1 @@
+../b.txt \ No newline at end of file
diff --git a/launcher/mojang/testdata/inspect_win/a/b.txt b/launcher/mojang/testdata/inspect_win/a/b.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/launcher/mojang/testdata/inspect_win/a/b.txt
diff --git a/launcher/mojang/testdata/inspect_win/a/b/b.txt b/launcher/mojang/testdata/inspect_win/a/b/b.txt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/launcher/mojang/testdata/inspect_win/a/b/b.txt
diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h
new file mode 100644
index 00000000..20e6764c
--- /dev/null
+++ b/launcher/net/ByteArraySink.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "Sink.h"
+
+namespace Net {
+/*
+ * Sink object for downloads that uses an external QByteArray it doesn't own as a target.
+ */
+class ByteArraySink : public Sink
+{
+public:
+ ByteArraySink(QByteArray *output)
+ :m_output(output)
+ {
+ // nil
+ };
+
+ virtual ~ByteArraySink()
+ {
+ // nil
+ }
+
+public:
+ JobStatus init(QNetworkRequest & request) override
+ {
+ m_output->clear();
+ if(initAllValidators(request))
+ return Job_InProgress;
+ return Job_Failed;
+ };
+
+ JobStatus write(QByteArray & data) override
+ {
+ m_output->append(data);
+ if(writeAllValidators(data))
+ return Job_InProgress;
+ return Job_Failed;
+ }
+
+ JobStatus abort() override
+ {
+ m_output->clear();
+ failAllValidators();
+ return Job_Failed;
+ }
+
+ JobStatus finalize(QNetworkReply &reply) override
+ {
+ if(finalizeAllValidators(reply))
+ return Job_Finished;
+ return Job_Failed;
+ }
+
+ bool hasLocalData() override
+ {
+ return false;
+ }
+
+private:
+ QByteArray * m_output;
+};
+}
diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h
new file mode 100644
index 00000000..0d6b19c2
--- /dev/null
+++ b/launcher/net/ChecksumValidator.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "Validator.h"
+#include <QCryptographicHash>
+#include <memory>
+#include <QFile>
+
+namespace Net {
+class ChecksumValidator: public Validator
+{
+public: /* con/des */
+ ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray())
+ :m_checksum(algorithm), m_expected(expected)
+ {
+ };
+ virtual ~ChecksumValidator() {};
+
+public: /* methods */
+ bool init(QNetworkRequest &) override
+ {
+ m_checksum.reset();
+ return true;
+ }
+ bool write(QByteArray & data) override
+ {
+ m_checksum.addData(data);
+ return true;
+ }
+ bool abort() override
+ {
+ return true;
+ }
+ bool validate(QNetworkReply &) override
+ {
+ if(m_expected.size() && m_expected != hash())
+ {
+ qWarning() << "Checksum mismatch, download is bad.";
+ return false;
+ }
+ return true;
+ }
+ QByteArray hash()
+ {
+ return m_checksum.result();
+ }
+ void setExpected(QByteArray expected)
+ {
+ m_expected = expected;
+ }
+
+private: /* data */
+ QCryptographicHash m_checksum;
+ QByteArray m_expected;
+};
+} \ No newline at end of file
diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp
new file mode 100644
index 00000000..3f183b7d
--- /dev/null
+++ b/launcher/net/Download.cpp
@@ -0,0 +1,309 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Download.h"
+
+#include "BuildConfig.h"
+#include <QFileInfo>
+#include <QDateTime>
+#include <QDebug>
+#include "Env.h"
+#include <FileSystem.h>
+#include "ChecksumValidator.h"
+#include "MetaCacheSink.h"
+#include "ByteArraySink.h"
+
+namespace Net {
+
+Download::Download():NetAction()
+{
+ m_status = Job_NotStarted;
+}
+
+Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options)
+{
+ Download * dl = new Download();
+ dl->m_url = url;
+ dl->m_options = options;
+ auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
+ auto cachedNode = new MetaCacheSink(entry, md5Node);
+ dl->m_sink.reset(cachedNode);
+ dl->m_target_path = entry->getFullPath();
+ return std::shared_ptr<Download>(dl);
+}
+
+Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options options)
+{
+ Download * dl = new Download();
+ dl->m_url = url;
+ dl->m_options = options;
+ dl->m_sink.reset(new ByteArraySink(output));
+ return std::shared_ptr<Download>(dl);
+}
+
+Download::Ptr Download::makeFile(QUrl url, QString path, Options options)
+{
+ Download * dl = new Download();
+ dl->m_url = url;
+ dl->m_options = options;
+ dl->m_sink.reset(new FileSink(path));
+ return std::shared_ptr<Download>(dl);
+}
+
+void Download::addValidator(Validator * v)
+{
+ m_sink->addValidator(v);
+}
+
+void Download::start()
+{
+ if(m_status == Job_Aborted)
+ {
+ qWarning() << "Attempt to start an aborted Download:" << m_url.toString();
+ emit aborted(m_index_within_job);
+ return;
+ }
+ QNetworkRequest request(m_url);
+ m_status = m_sink->init(request);
+ switch(m_status)
+ {
+ case Job_Finished:
+ emit succeeded(m_index_within_job);
+ qDebug() << "Download cache hit " << m_url.toString();
+ return;
+ case Job_InProgress:
+ qDebug() << "Downloading " << m_url.toString();
+ break;
+ case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink.
+ case Job_NotStarted:
+ case Job_Failed:
+ emit failed(m_index_within_job);
+ return;
+ case Job_Aborted:
+ return;
+ }
+
+ request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT);
+
+ QNetworkReply *rep = ENV.qnam().get(request);
+
+ m_reply.reset(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, &QNetworkReply::sslErrors, this, &Download::sslErrors);
+ connect(rep, &QNetworkReply::readyRead, this, &Download::downloadReadyRead);
+}
+
+void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void Download::downloadError(QNetworkReply::NetworkError error)
+{
+ if(error == QNetworkReply::OperationCanceledError)
+ {
+ qCritical() << "Aborted " << m_url.toString();
+ m_status = Job_Aborted;
+ }
+ else
+ {
+ if(m_options & Option::AcceptLocalFiles)
+ {
+ if(m_sink->hasLocalData())
+ {
+ m_status = Job_Failed_Proceed;
+ return;
+ }
+ }
+ // error happened during download.
+ qCritical() << "Failed " << m_url.toString() << " with reason " << error;
+ m_status = Job_Failed;
+ }
+}
+
+void Download::sslErrors(const QList<QSslError> & errors)
+{
+ int i = 1;
+ for (auto error : errors)
+ {
+ qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+}
+
+bool Download::handleRedirect()
+{
+ QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
+ if(!redirect.isValid())
+ {
+ if(!m_reply->hasRawHeader("Location"))
+ {
+ // no redirect -> it's fine to continue
+ return false;
+ }
+ // there is a Location header, but it's not correct. we need to apply some workarounds...
+ QByteArray redirectBA = m_reply->rawHeader("Location");
+ if(redirectBA.size() == 0)
+ {
+ // empty, yet present redirect header? WTF?
+ return false;
+ }
+ QString redirectStr = QString::fromUtf8(redirectBA);
+
+ if(redirectStr.startsWith("//"))
+ {
+ /*
+ * IF the URL begins with //, we need to insert the URL scheme.
+ * See: https://bugreports.qt.io/browse/QTBUG-41061
+ * See: http://tools.ietf.org/html/rfc3986#section-4.2
+ */
+ redirectStr = m_reply->url().scheme() + ":" + redirectStr;
+ }
+ else if(redirectStr.startsWith("/"))
+ {
+ /*
+ * IF the URL begins with /, we need to process it as a relative URL
+ */
+ auto url = m_reply->url();
+ url.setPath(redirectStr, QUrl::TolerantMode);
+ redirectStr = url.toString();
+ }
+
+ /*
+ * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues.
+ * FIXME: report Qt bug for this
+ */
+ redirect = QUrl(redirectStr, QUrl::TolerantMode);
+ if(!redirect.isValid())
+ {
+ qWarning() << "Failed to parse redirect URL:" << redirectStr;
+ downloadError(QNetworkReply::ProtocolFailure);
+ return false;
+ }
+ qDebug() << "Fixed location header:" << redirect;
+ }
+ else
+ {
+ qDebug() << "Location header:" << redirect;
+ }
+
+ m_url = QUrl(redirect.toString());
+ qDebug() << "Following redirect to " << m_url.toString();
+ start();
+ return true;
+}
+
+
+void Download::downloadFinished()
+{
+ // handle HTTP redirection first
+ if(handleRedirect())
+ {
+ qDebug() << "Download redirected:" << m_url.toString();
+ return;
+ }
+
+ // if the download failed before this point ...
+ if (m_status == Job_Failed_Proceed)
+ {
+ qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ else if (m_status == Job_Failed)
+ {
+ qDebug() << "Download failed in previous step:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+ else if(m_status == Job_Aborted)
+ {
+ qDebug() << "Download aborted in previous step:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit aborted(m_index_within_job);
+ return;
+ }
+
+ // make sure we got all the remaining data, if any
+ auto data = m_reply->readAll();
+ if(data.size())
+ {
+ qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path;
+ m_status = m_sink->write(data);
+ }
+
+ // otherwise, finalize the whole graph
+ m_status = m_sink->finalize(*m_reply.get());
+ if (m_status != Job_Finished)
+ {
+ qDebug() << "Download failed to finalize:" << m_url.toString();
+ m_sink->abort();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+ m_reply.reset();
+ qDebug() << "Download succeeded:" << m_url.toString();
+ emit succeeded(m_index_within_job);
+}
+
+void Download::downloadReadyRead()
+{
+ if(m_status == Job_InProgress)
+ {
+ auto data = m_reply->readAll();
+ m_status = m_sink->write(data);
+ if(m_status == Job_Failed)
+ {
+ qCritical() << "Failed to process response chunk for " << m_target_path;
+ }
+ // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes";
+ }
+ else
+ {
+ qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status;
+ }
+}
+
+}
+
+bool Net::Download::abort()
+{
+ if(m_reply)
+ {
+ m_reply->abort();
+ }
+ else
+ {
+ m_status = Job_Aborted;
+ }
+ return true;
+}
+
+bool Net::Download::canAbort()
+{
+ return true;
+}
diff --git a/launcher/net/Download.h b/launcher/net/Download.h
new file mode 100644
index 00000000..a224bb86
--- /dev/null
+++ b/launcher/net/Download.h
@@ -0,0 +1,75 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "NetAction.h"
+#include "HttpMetaCache.h"
+#include "Validator.h"
+#include "Sink.h"
+
+namespace Net {
+class Download : public NetAction
+{
+ Q_OBJECT
+
+public: /* types */
+ typedef std::shared_ptr<class Download> Ptr;
+ enum class Option
+ {
+ NoOptions = 0,
+ AcceptLocalFiles = 1
+ };
+ Q_DECLARE_FLAGS(Options, Option)
+
+protected: /* con/des */
+ explicit Download();
+public:
+ virtual ~Download(){};
+ static Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions);
+ static Download::Ptr makeByteArray(QUrl url, QByteArray *output, Options options = Option::NoOptions);
+ static Download::Ptr makeFile(QUrl url, QString path, Options options = Option::NoOptions);
+
+public: /* methods */
+ QString getTargetFilepath()
+ {
+ return m_target_path;
+ }
+ void addValidator(Validator * v);
+ bool abort() override;
+ bool canAbort() override;
+
+private: /* methods */
+ bool handleRedirect();
+
+protected slots:
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
+ void downloadError(QNetworkReply::NetworkError error) override;
+ void sslErrors(const QList<QSslError> & errors);
+ void downloadFinished() override;
+ void downloadReadyRead() override;
+
+public slots:
+ void start() override;
+
+private: /* data */
+ // FIXME: remove this, it has no business being here.
+ QString m_target_path;
+ std::unique_ptr<Sink> m_sink;
+ Options m_options;
+};
+}
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(Net::Download::Options)
diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp
new file mode 100644
index 00000000..8b3e917d
--- /dev/null
+++ b/launcher/net/FileSink.cpp
@@ -0,0 +1,115 @@
+#include "FileSink.h"
+#include <QFile>
+#include <QFileInfo>
+#include "Env.h"
+#include "FileSystem.h"
+
+namespace Net {
+
+FileSink::FileSink(QString filename)
+ :m_filename(filename)
+{
+ // nil
+}
+
+FileSink::~FileSink()
+{
+ // nil
+}
+
+JobStatus FileSink::init(QNetworkRequest& request)
+{
+ auto result = initCache(request);
+ if(result != Job_InProgress)
+ {
+ return result;
+ }
+ // create a new save file and open it for writing
+ if (!FS::ensureFilePathExists(m_filename))
+ {
+ qCritical() << "Could not create folder for " + m_filename;
+ return Job_Failed;
+ }
+ wroteAnyData = false;
+ m_output_file.reset(new QSaveFile(m_filename));
+ if (!m_output_file->open(QIODevice::WriteOnly))
+ {
+ qCritical() << "Could not open " + m_filename + " for writing";
+ return Job_Failed;
+ }
+
+ if(initAllValidators(request))
+ return Job_InProgress;
+ return Job_Failed;
+}
+
+JobStatus FileSink::initCache(QNetworkRequest &)
+{
+ return Job_InProgress;
+}
+
+JobStatus FileSink::write(QByteArray& data)
+{
+ if (!writeAllValidators(data) || m_output_file->write(data) != data.size())
+ {
+ qCritical() << "Failed writing into " + m_filename;
+ m_output_file->cancelWriting();
+ m_output_file.reset();
+ wroteAnyData = false;
+ return Job_Failed;
+ }
+ wroteAnyData = true;
+ return Job_InProgress;
+}
+
+JobStatus FileSink::abort()
+{
+ m_output_file->cancelWriting();
+ failAllValidators();
+ return Job_Failed;
+}
+
+JobStatus FileSink::finalize(QNetworkReply& reply)
+{
+ bool gotFile = false;
+ QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute);
+ bool validStatus = false;
+ int statusCode = statusCodeV.toInt(&validStatus);
+ if(validStatus)
+ {
+ // this leaves out 304 Not Modified
+ gotFile = statusCode == 200 || statusCode == 203;
+ }
+ // if we wrote any data to the save file, we try to commit the data to the real file.
+ // if it actually got a proper file, we write it even if it was empty
+ if (gotFile || wroteAnyData)
+ {
+ // ask validators for data consistency
+ // we only do this for actual downloads, not 'your data is still the same' cache hits
+ if(!finalizeAllValidators(reply))
+ return Job_Failed;
+ // nothing went wrong...
+ if (!m_output_file->commit())
+ {
+ qCritical() << "Failed to commit changes to " << m_filename;
+ m_output_file->cancelWriting();
+ return Job_Failed;
+ }
+ }
+ // then get rid of the save file
+ m_output_file.reset();
+
+ return finalizeCache(reply);
+}
+
+JobStatus FileSink::finalizeCache(QNetworkReply &)
+{
+ return Job_Finished;
+}
+
+bool FileSink::hasLocalData()
+{
+ QFileInfo info(m_filename);
+ return info.exists() && info.size() != 0;
+}
+}
diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h
new file mode 100644
index 00000000..875fe511
--- /dev/null
+++ b/launcher/net/FileSink.h
@@ -0,0 +1,28 @@
+#pragma once
+#include "Sink.h"
+#include <QSaveFile>
+
+namespace Net {
+class FileSink : public Sink
+{
+public: /* con/des */
+ FileSink(QString filename);
+ virtual ~FileSink();
+
+public: /* methods */
+ JobStatus init(QNetworkRequest & request) override;
+ JobStatus write(QByteArray & data) override;
+ JobStatus abort() override;
+ JobStatus finalize(QNetworkReply & reply) override;
+ bool hasLocalData() override;
+
+protected: /* methods */
+ virtual JobStatus initCache(QNetworkRequest &);
+ virtual JobStatus finalizeCache(QNetworkReply &reply);
+
+protected: /* data */
+ QString m_filename;
+ bool wroteAnyData = false;
+ std::unique_ptr<QSaveFile> m_output_file;
+};
+}
diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp
new file mode 100644
index 00000000..4bc8fbc8
--- /dev/null
+++ b/launcher/net/HttpMetaCache.cpp
@@ -0,0 +1,273 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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 (const Exception &e)
+ {
+ qWarning() << e.what();
+ }
+}
diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h
new file mode 100644
index 00000000..1c10e8c7
--- /dev/null
+++ b/launcher/net/HttpMetaCache.h
@@ -0,0 +1,123 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QString>
+#include <QMap>
+#include <qtimer.h>
+#include <memory>
+
+class HttpMetaCache;
+
+class 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 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/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp
new file mode 100644
index 00000000..d7f18533
--- /dev/null
+++ b/launcher/net/MetaCacheSink.cpp
@@ -0,0 +1,65 @@
+#include "MetaCacheSink.h"
+#include <QFile>
+#include <QFileInfo>
+#include "Env.h"
+#include "FileSystem.h"
+
+namespace Net {
+
+MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum)
+ :Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum)
+{
+ addValidator(md5sum);
+}
+
+MetaCacheSink::~MetaCacheSink()
+{
+ // nil
+}
+
+JobStatus MetaCacheSink::initCache(QNetworkRequest& request)
+{
+ if (!m_entry->isStale())
+ {
+ return Job_Finished;
+ }
+ // check if file exists, if it does, use its information for the request
+ QFile current(m_filename);
+ 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());
+ }
+ }
+ return Job_InProgress;
+}
+
+JobStatus MetaCacheSink::finalizeCache(QNetworkReply & reply)
+{
+ QFileInfo output_file_info(m_filename);
+ if(wroteAnyData)
+ {
+ m_entry->setMD5Sum(m_md5Node->hash().toHex().constData());
+ }
+ m_entry->setETag(reply.rawHeader("ETag").constData());
+ if (reply.hasRawHeader("Last-Modified"))
+ {
+ m_entry->setRemoteChangedTimestamp(reply.rawHeader("Last-Modified").constData());
+ }
+ m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch());
+ m_entry->setStale(false);
+ ENV.metacache()->updateEntry(m_entry);
+ return Job_Finished;
+}
+
+bool MetaCacheSink::hasLocalData()
+{
+ QFileInfo info(m_filename);
+ return info.exists() && info.size() != 0;
+}
+}
diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h
new file mode 100644
index 00000000..edcf7ad1
--- /dev/null
+++ b/launcher/net/MetaCacheSink.h
@@ -0,0 +1,22 @@
+#pragma once
+#include "FileSink.h"
+#include "ChecksumValidator.h"
+#include "net/HttpMetaCache.h"
+
+namespace Net {
+class MetaCacheSink : public FileSink
+{
+public: /* con/des */
+ MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum);
+ virtual ~MetaCacheSink();
+ bool hasLocalData() override;
+
+protected: /* methods */
+ JobStatus initCache(QNetworkRequest & request) override;
+ JobStatus finalizeCache(QNetworkReply & reply) override;
+
+private: /* data */
+ MetaEntryPtr m_entry;
+ ChecksumValidator * m_md5Node;
+};
+}
diff --git a/launcher/net/Mode.h b/launcher/net/Mode.h
new file mode 100644
index 00000000..9a95f5ad
--- /dev/null
+++ b/launcher/net/Mode.h
@@ -0,0 +1,10 @@
+#pragma once
+
+namespace Net
+{
+enum class Mode
+{
+ Offline,
+ Online
+};
+}
diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h
new file mode 100644
index 00000000..c13c187f
--- /dev/null
+++ b/launcher/net/NetAction.h
@@ -0,0 +1,113 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QUrl>
+#include <memory>
+#include <QNetworkReply>
+#include <QObjectPtr.h>
+
+enum JobStatus
+{
+ Job_NotStarted,
+ Job_InProgress,
+ Job_Finished,
+ Job_Failed,
+ Job_Aborted,
+ /*
+ * FIXME: @NUKE this confuses the task failing with us having a fallback in the form of local data. Clear up the confusion.
+ * Same could be true for aborted task - the presence of pre-existing result is a separate concern
+ */
+ Job_Failed_Proceed
+};
+
+typedef std::shared_ptr<class NetAction> NetActionPtr;
+class NetAction : public QObject
+{
+ Q_OBJECT
+protected:
+ explicit NetAction() : QObject(0) {};
+
+public:
+ virtual ~NetAction() {};
+
+ bool isRunning() const
+ {
+ return m_status == Job_InProgress;
+ }
+ bool isFinished() const
+ {
+ return m_status >= Job_Finished;
+ }
+ bool wasSuccessful() const
+ {
+ return m_status == Job_Finished || m_status == Job_Failed_Proceed;
+ }
+
+ qint64 totalProgress() const
+ {
+ return m_total_progress;
+ }
+ qint64 currentProgress() const
+ {
+ return m_progress;
+ }
+ virtual bool abort()
+ {
+ return false;
+ }
+ virtual bool canAbort()
+ {
+ return false;
+ }
+ QUrl url()
+ {
+ return m_url;
+ }
+
+signals:
+ void started(int index);
+ void netActionProgress(int index, qint64 current, qint64 total);
+ void succeeded(int index);
+ void failed(int index);
+ void aborted(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;
+
+public:
+ /// index within the parent job, FIXME: nuke
+ int m_index_within_job = 0;
+
+ /// the network reply
+ unique_qobject_ptr<QNetworkReply> m_reply;
+
+ /// source URL
+ QUrl m_url;
+
+ qint64 m_progress = 0;
+ qint64 m_total_progress = 1;
+
+protected:
+ JobStatus m_status = Job_NotStarted;
+};
diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp
new file mode 100644
index 00000000..029d9e34
--- /dev/null
+++ b/launcher/net/NetJob.cpp
@@ -0,0 +1,218 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "NetJob.h"
+#include "Download.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::partAborted(int index)
+{
+ m_aborted = true;
+ m_doing.remove(index);
+ m_failed.insert(index);
+ downloads[index].get()->disconnect(this);
+ startMoreParts();
+}
+
+void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal)
+{
+ auto &slot = parts_progress[index];
+ slot.current_progress = bytesReceived;
+ slot.total_progress = bytesTotal;
+
+ int done = m_done.size();
+ int doing = m_doing.size();
+ int all = parts_progress.size();
+
+ qint64 bytesAll = 0;
+ qint64 bytesTotalAll = 0;
+ for(auto & partIdx: m_doing)
+ {
+ auto part = parts_progress[partIdx];
+ // do not count parts with unknown/nonsensical total size
+ if(part.total_progress <= 0)
+ {
+ continue;
+ }
+ bytesAll += part.current_progress;
+ bytesTotalAll += part.total_progress;
+ }
+
+ qint64 inprogress = (bytesTotalAll == 0) ? 0 : (bytesAll * 1000) / bytesTotalAll;
+ auto current = done * 1000 + doing * inprogress;
+ auto current_total = all * 1000;
+ // HACK: make sure it never jumps backwards.
+ // FAIL: This breaks if the size is not known (or is it something else?) and jumps to 1000, so if it is 1000 reset it to inprogress
+ if(m_current_progress == 1000) {
+ m_current_progress = inprogress;
+ }
+ if(m_current_progress > current)
+ {
+ current = m_current_progress;
+ }
+ m_current_progress = current;
+ setProgress(current, current_total);
+}
+
+void NetJob::executeTask()
+{
+ // hack that delays early failures so they can be caught easier
+ QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection);
+}
+
+void NetJob::startMoreParts()
+{
+ if(!isRunning())
+ {
+ // this actually makes sense. You can put running downloads into a NetJob and then not start it until much later.
+ return;
+ }
+ // OK. We are actively processing tasks, proceed.
+ // Check for final conditions if there's nothing in the queue.
+ if(!m_todo.size())
+ {
+ if(!m_doing.size())
+ {
+ if(!m_failed.size())
+ {
+ emitSucceeded();
+ }
+ else if(m_aborted)
+ {
+ emitAborted();
+ }
+ else
+ {
+ emitFailed(tr("Job '%1' failed to process:\n%2").arg(objectName()).arg(getFailedFiles().join("\n")));
+ }
+ }
+ return;
+ }
+ // There's work to do, 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(aborted(int)), SLOT(partAborted(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]->url().toString());
+ }
+ failed.sort();
+ return failed;
+}
+
+bool NetJob::canAbort() const
+{
+ bool canFullyAbort = true;
+ // can abort the waiting?
+ for(auto index: m_todo)
+ {
+ auto part = downloads[index];
+ canFullyAbort &= part->canAbort();
+ }
+ // can abort the active?
+ for(auto index: m_doing)
+ {
+ auto part = downloads[index];
+ canFullyAbort &= part->canAbort();
+ }
+ return canFullyAbort;
+}
+
+bool NetJob::abort()
+{
+ bool fullyAborted = true;
+ // fail all waiting
+ m_failed.unite(m_todo.toSet());
+ m_todo.clear();
+ // abort active
+ auto toKill = m_doing.toList();
+ for(auto index: toKill)
+ {
+ auto part = downloads[index];
+ fullyAborted &= part->abort();
+ }
+ return fullyAborted;
+}
+
+bool NetJob::addNetAction(NetActionPtr action)
+{
+ action->m_index_within_job = downloads.size();
+ downloads.append(action);
+ part_info pi;
+ parts_progress.append(pi);
+ partProgress(parts_progress.count() - 1, action->currentProgress(), action->totalProgress());
+
+ if(action->isRunning())
+ {
+ 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)));
+ }
+ else
+ {
+ m_todo.append(parts_progress.size() - 1);
+ }
+ return true;
+}
+
+NetJob::~NetJob() = default;
diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h
new file mode 100644
index 00000000..338f8e71
--- /dev/null
+++ b/launcher/net/NetJob.h
@@ -0,0 +1,89 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QtNetwork>
+#include "NetAction.h"
+#include "Download.h"
+#include "HttpMetaCache.h"
+#include "tasks/Task.h"
+#include "QObjectPtr.h"
+
+class NetJob;
+typedef shared_qobject_ptr<NetJob> NetJobPtr;
+
+class NetJob : public Task
+{
+ Q_OBJECT
+public:
+ explicit NetJob(QString job_name) : Task()
+ {
+ setObjectName(job_name);
+ }
+ virtual ~NetJob();
+
+ bool addNetAction(NetActionPtr action);
+
+ 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();
+ }
+ QStringList getFailedFiles();
+
+ bool canAbort() const override;
+
+private slots:
+ void startMoreParts();
+
+public slots:
+ virtual void executeTask() override;
+ virtual bool abort() override;
+
+private slots:
+ void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal);
+ void partSucceeded(int index);
+ void partFailed(int index);
+ void partAborted(int index);
+
+private:
+ struct part_info
+ {
+ qint64 current_progress = 0;
+ qint64 total_progress = 1;
+ int failures = 0;
+ };
+ 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 m_current_progress = 0;
+ bool m_aborted = false;
+};
diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp
new file mode 100644
index 00000000..cb470c49
--- /dev/null
+++ b/launcher/net/PasteUpload.cpp
@@ -0,0 +1,104 @@
+#include "PasteUpload.h"
+#include "Env.h"
+#include <QDebug>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QFile>
+#include <BuildConfig.h>
+
+PasteUpload::PasteUpload(QWidget *window, QString text, QString key) : m_window(window)
+{
+ m_key = key;
+ QByteArray temp;
+ QJsonObject topLevelObj;
+ QJsonObject sectionObject;
+ sectionObject.insert("contents", text);
+ QJsonArray sectionArray;
+ sectionArray.append(sectionObject);
+ topLevelObj.insert("description", "MultiMC Log Upload");
+ topLevelObj.insert("sections", sectionArray);
+ QJsonDocument docOut;
+ docOut.setObject(topLevelObj);
+ m_jsonContent = docOut.toJson();
+}
+
+PasteUpload::~PasteUpload()
+{
+}
+
+bool PasteUpload::validateText()
+{
+ return m_jsonContent.size() <= maxSize();
+}
+
+void PasteUpload::executeTask()
+{
+ QNetworkRequest request(QUrl("https://api.paste.ee/v1/pastes"));
+ request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED);
+
+ request.setRawHeader("Content-Type", "application/json");
+ request.setRawHeader("Content-Length", QByteArray::number(m_jsonContent.size()));
+ request.setRawHeader("X-Auth-Token", m_key.toStdString().c_str());
+
+ QNetworkReply *rep = ENV.qnam().post(request, m_jsonContent);
+
+ 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()
+{
+ QByteArray data = m_reply->readAll();
+ // if the download succeeded
+ if (m_reply->error() == QNetworkReply::NetworkError::NoError)
+ {
+ 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("success").toBool();
+ if (!status)
+ {
+ qCritical() << "paste.ee reported error:" << QString(object.value("error").toString());
+ return false;
+ }
+ m_pasteLink = object.value("link").toString();
+ m_pasteID = object.value("id").toString();
+ qDebug() << m_pasteLink;
+ return true;
+}
+
diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h
new file mode 100644
index 00000000..5514e058
--- /dev/null
+++ b/launcher/net/PasteUpload.h
@@ -0,0 +1,47 @@
+#pragma once
+#include "tasks/Task.h"
+#include <QNetworkReply>
+#include <QBuffer>
+#include <memory>
+
+class 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;
+ }
+ int 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);
+ QString m_error;
+ QWidget *m_window;
+ QString m_pasteID;
+ QString m_pasteLink;
+ QString m_key;
+ QByteArray m_jsonContent;
+ std::shared_ptr<QNetworkReply> m_reply;
+public
+slots:
+ void downloadError(QNetworkReply::NetworkError);
+ void downloadFinished();
+};
diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h
new file mode 100644
index 00000000..d367fb15
--- /dev/null
+++ b/launcher/net/Sink.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "net/NetAction.h"
+
+#include "Validator.h"
+
+namespace Net {
+class Sink
+{
+public: /* con/des */
+ Sink() {};
+ virtual ~Sink() {};
+
+public: /* methods */
+ virtual JobStatus init(QNetworkRequest & request) = 0;
+ virtual JobStatus write(QByteArray & data) = 0;
+ virtual JobStatus abort() = 0;
+ virtual JobStatus finalize(QNetworkReply & reply) = 0;
+ virtual bool hasLocalData() = 0;
+
+ void addValidator(Validator * validator)
+ {
+ if(validator)
+ {
+ validators.push_back(std::shared_ptr<Validator>(validator));
+ }
+ }
+
+protected: /* methods */
+ bool finalizeAllValidators(QNetworkReply & reply)
+ {
+ for(auto & validator: validators)
+ {
+ if(!validator->validate(reply))
+ return false;
+ }
+ return true;
+ }
+ bool failAllValidators()
+ {
+ bool success = true;
+ for(auto & validator: validators)
+ {
+ success &= validator->abort();
+ }
+ return success;
+ }
+ bool initAllValidators(QNetworkRequest & request)
+ {
+ for(auto & validator: validators)
+ {
+ if(!validator->init(request))
+ return false;
+ }
+ return true;
+ }
+ bool writeAllValidators(QByteArray & data)
+ {
+ for(auto & validator: validators)
+ {
+ if(!validator->write(data))
+ return false;
+ }
+ return true;
+ }
+
+protected: /* data */
+ std::vector<std::shared_ptr<Validator>> validators;
+};
+}
diff --git a/launcher/net/Validator.h b/launcher/net/Validator.h
new file mode 100644
index 00000000..59b72a0b
--- /dev/null
+++ b/launcher/net/Validator.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "net/NetAction.h"
+
+namespace Net {
+class Validator
+{
+public: /* con/des */
+ Validator() {};
+ virtual ~Validator() {};
+
+public: /* methods */
+ virtual bool init(QNetworkRequest & request) = 0;
+ virtual bool write(QByteArray & data) = 0;
+ virtual bool abort() = 0;
+ virtual bool validate(QNetworkReply & reply) = 0;
+};
+}
diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp
new file mode 100644
index 00000000..c66f49e1
--- /dev/null
+++ b/launcher/news/NewsChecker.cpp
@@ -0,0 +1,131 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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(Net::Download::makeByteArray(m_feedUrl, &newsData));
+ 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.";
+
+ 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(newsData, false, &errorMsg, &errorLine, &errorCol))
+ {
+ QString fullErrorMsg = QString("Error parsing RSS feed XML. %s at %d:%d.").arg(errorMsg, errorLine, errorCol);
+ fail(fullErrorMsg);
+ newsData.clear();
+ return;
+ }
+ newsData.clear();
+ }
+
+ // 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/launcher/news/NewsChecker.h b/launcher/news/NewsChecker.h
new file mode 100644
index 00000000..84b1f552
--- /dev/null
+++ b/launcher/news/NewsChecker.h
@@ -0,0 +1,103 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QList>
+
+#include <net/NetJob.h>
+
+#include "NewsEntry.h"
+
+class 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: /* data */
+ //! 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;
+
+ QByteArray newsData;
+
+ /*!
+ * 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;
+
+protected slots:
+ /// Emits newsLoaded() and sets m_lastLoadError to empty string.
+ void succeed();
+
+ /// Emits newsLoadingFailed() and sets m_lastLoadError to the given message.
+ void fail(const QString& errorMsg);
+};
+
diff --git a/launcher/news/NewsEntry.cpp b/launcher/news/NewsEntry.cpp
new file mode 100644
index 00000000..7eff657b
--- /dev/null
+++ b/launcher/news/NewsEntry.cpp
@@ -0,0 +1,77 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/news/NewsEntry.h b/launcher/news/NewsEntry.h
new file mode 100644
index 00000000..0dbc70a5
--- /dev/null
+++ b/launcher/news/NewsEntry.h
@@ -0,0 +1,65 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/launcher/notifications/NotificationChecker.cpp b/launcher/notifications/NotificationChecker.cpp
new file mode 100644
index 00000000..8209c28b
--- /dev/null
+++ b/launcher/notifications/NotificationChecker.cpp
@@ -0,0 +1,129 @@
+#include "NotificationChecker.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QDebug>
+
+#include "Env.h"
+#include "net/Download.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 = Net::Download::makeCached(m_notificationsUrl, entry));
+ connect(m_download.get(), &Net::Download::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/launcher/notifications/NotificationChecker.h b/launcher/notifications/NotificationChecker.h
new file mode 100644
index 00000000..eb2b32a2
--- /dev/null
+++ b/launcher/notifications/NotificationChecker.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <QObject>
+
+#include "net/NetJob.h"
+#include "net/Download.h"
+
+class 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;
+ Net::Download::Ptr m_download;
+
+ QString m_appVersionChannel;
+ QString m_appPlatform;
+ QString m_appFullVersion;
+};
diff --git a/launcher/package/linux/MultiMC b/launcher/package/linux/MultiMC
new file mode 100755
index 00000000..da6373bc
--- /dev/null
+++ b/launcher/package/linux/MultiMC
@@ -0,0 +1,93 @@
+#!/bin/bash
+# Basic start script for running MultiMC with the libs packaged with it.
+
+function printerror {
+ printf "$1"
+ if which zenity >/dev/null; then zenity --error --text="$1" &>/dev/null;
+ elif which kdialog >/dev/null; then kdialog --error "$1" &>/dev/null;
+ fi
+}
+
+if [[ $EUID -eq 0 ]]; then
+ printerror "This program should not be run using sudo or as the root user!\n"
+ exit 1
+fi
+
+
+MMC_DIR="$(dirname "$(readlink -f "$0")")"
+echo "MultiMC Dir: ${MMC_DIR}"
+
+# Set up env - filter out input LD_ variables but pass them in under different names
+export GAME_LIBRARY_PATH=${GAME_LIBRARY_PATH-${LD_LIBRARY_PATH}}
+export GAME_PRELOAD=${GAME_PRELOAD-${LD_PRELOAD}}
+export LD_LIBRARY_PATH="${MMC_DIR}/bin":$MMC_LIBRARY_PATH
+export LD_PRELOAD=$MMC_PRELOAD
+export QT_PLUGIN_PATH="${MMC_DIR}/plugins"
+export QT_FONTPATH="${MMC_DIR}/fonts"
+
+# Detect missing dependencies...
+DEPS_LIST=`ldd "${MMC_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | sort -u | awk -vORS=", " '{ print $1 }'`
+if [ "x$DEPS_LIST" = "x" ]; then
+ # We have all our dependencies. Run MultiMC.
+ echo "No missing dependencies found."
+
+ # Just to be sure...
+ chmod +x "${MMC_DIR}/bin/MultiMC"
+
+ # Run MultiMC
+ "${MMC_DIR}/bin/MultiMC" -d "${MMC_DIR}" "$@"
+
+ # Run MultiMC in valgrind
+ # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${MMC_DIR}/bin/MultiMC" -d "${MMC_DIR}" "$@"
+
+ # Run MultiMC with callgrind, delay instrumentation
+ # valgrind --log-file="valgrind.log" --tool=callgrind --instr-atstart=no "${MMC_DIR}/bin/MultiMC" -d "${MMC_DIR}" "$@"
+ # use callgrind_control -i on/off to profile actions
+
+ # Exit with MultiMC's exit code.
+ exit $?
+else
+ # apt
+ if which apt-file &>/dev/null; then
+ LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u`
+ COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do apt-file -l search $LIBRARY; done`
+ COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'`
+ INSTALL_CMD="sudo apt-get install $COMMAND_LIBS"
+ # pacman
+ elif which pkgfile &>/dev/null; then
+ LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u`
+ COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pkgfile $LIBRARY; done`
+ COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'`
+ INSTALL_CMD="sudo pacman -S $COMMAND_LIBS"
+ # dnf
+ elif which dnf &>/dev/null; then
+ LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u`
+ COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do dnf whatprovides -q $LIBRARY; done`
+ COMMAND_LIBS=`echo "$COMMAND_LIBS" | grep -v 'Repo' | sort -u | awk -vORS=" " '{ print $1 }'`
+ INSTALL_CMD="sudo dnf install $COMMAND_LIBS"
+ # yum
+ elif which yum &>/dev/null; then
+ LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u`
+ COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do yum whatprovides $LIBRARY; done`
+ COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'`
+ INSTALL_CMD="sudo yum install $COMMAND_LIBS"
+ # zypper
+ elif which zypper &>/dev/null; then
+ LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u`
+ COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do zypper wp $LIBRARY; done`
+ COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'`
+ INSTALL_CMD="sudo zypper install $COMMAND_LIBS"
+ # emerge
+ elif which pfl &>/dev/null; then
+ LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u`
+ COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pfl $LIBRARY; done`
+ COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'`
+ INSTALL_CMD="sudo emerge $COMMAND_LIBS"
+ fi
+
+ MESSAGE="Error: MultiMC is missing the following libraries that it needs to work correctly:\n\t${DEPS_LIST}\nPlease install them from your distribution's package manager."
+ MESSAGE="$MESSAGE\n\nHint (please apply common sense): $INSTALL_CMD\n"
+
+ printerror "$MESSAGE"
+ exit 1
+fi
diff --git a/launcher/package/linux/multimc.desktop b/launcher/package/linux/multimc.desktop
new file mode 100755
index 00000000..c25be047
--- /dev/null
+++ b/launcher/package/linux/multimc.desktop
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Version=1.0
+Name=MultiMC
+GenericName=Minecraft Launcher
+Comment=Free, open source launcher and instance manager for Minecraft.
+Type=Application
+Terminal=false
+Exec=multimc
+Icon=multimc
+Categories=Game
+Keywords=game;minecraft;
diff --git a/launcher/package/rpm/MultiMC5.spec b/launcher/package/rpm/MultiMC5.spec
new file mode 100644
index 00000000..78b9000e
--- /dev/null
+++ b/launcher/package/rpm/MultiMC5.spec
@@ -0,0 +1,47 @@
+Name: MultiMC5
+Version: 1.4
+Release: 2%{?dist}
+Summary: A local install wrapper for MultiMC
+
+License: ASL 2.0
+URL: https://multimc.org
+BuildArch: x86_64
+
+Requires: zenity qt5-qtbase wget xrandr
+Provides: multimc MultiMC multimc5
+
+%description
+A local install wrapper for MultiMC
+
+%prep
+
+
+%build
+
+
+%install
+mkdir -p %{buildroot}/opt/multimc
+install -m 0644 ../ubuntu/multimc/opt/multimc/icon.svg %{buildroot}/opt/multimc/icon.svg
+install -m 0755 ../ubuntu/multimc/opt/multimc/run.sh %{buildroot}/opt/multimc/run.sh
+mkdir -p %{buildroot}/%{_datadir}/applications
+install -m 0644 ../ubuntu/multimc/usr/share/applications/multimc.desktop %{buildroot}/%{_datadir}/applications/multimc.desktop
+mkdir -p %{buildroot}/%{_metainfodir}
+install -m 0644 ../ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml %{buildroot}/%{_metainfodir}/multimc.metainfo.xml
+
+%files
+%dir /opt/multimc
+/opt/multimc/icon.svg
+/opt/multimc/run.sh
+%{_datadir}/applications/multimc.desktop
+%{_metainfodir}/multimc.metainfo.xml
+
+
+%changelog
+* Tue Jun 01 2021 kb1000 <fedora@kb1000.de> - 1.4-2
+- Add xrandr to the dependencies
+
+* Tue Dec 08 00:34:35 CET 2020 joshua-stone <joshua.gage.stone@gmail.com>
+- Add metainfo.xml for improving package metadata
+
+* Wed Nov 25 22:53:59 CET 2020 kb1000 <fedora@kb1000.de>
+- Initial version of the RPM package, based on the Ubuntu package
diff --git a/launcher/package/rpm/README.md b/launcher/package/rpm/README.md
new file mode 100644
index 00000000..0c2b1e49
--- /dev/null
+++ b/launcher/package/rpm/README.md
@@ -0,0 +1,12 @@
+# What is this?
+A simple RPM package for MultiMC that contains a script that downloads and installs real MultiMC on Red Hat based systems.
+
+It contains a `.desktop` file, a `.metainfo.xml` file, an icon, and a simple script that does the heavy lifting.
+
+# How to build this?
+You need the `rpm-build` package. Switch into this directory, then run:
+```
+rpmbuild --build-in-place -bb MultiMC5.spec
+```
+
+Replace the version with whatever is appropriate.
diff --git a/launcher/package/ubuntu/README.md b/launcher/package/ubuntu/README.md
new file mode 100644
index 00000000..892abd12
--- /dev/null
+++ b/launcher/package/ubuntu/README.md
@@ -0,0 +1,14 @@
+# What is this?
+A simple Ubuntu package for MultiMC that contains a script that downloads and installs real MultiMC on Ubuntu based systems.
+
+It contains a `.desktop` file, an icon, and a simple script that does the heavy lifting.
+
+This is also the source for the files in the [RPM package](../rpm). If you rename, create or delete files here, you'll likely also have to update the RPM spec file there.
+
+# How to build this?
+You need dpkg utils. Rename the `multimc` folder to `multimc_1.5-1` and then run:
+```
+fakeroot dpkg-deb --build multimc_1.5-1
+```
+
+Replace the version with whatever is appropriate.
diff --git a/launcher/package/ubuntu/multimc/DEBIAN/control b/launcher/package/ubuntu/multimc/DEBIAN/control
new file mode 100644
index 00000000..3e0f570c
--- /dev/null
+++ b/launcher/package/ubuntu/multimc/DEBIAN/control
@@ -0,0 +1,12 @@
+Package: multimc
+Version: 1.5-1
+Architecture: all
+Maintainer: Petr Mrázek <peterix@gmail.com>
+Section: games
+Priority: optional
+Installed-Size: 75
+Depends: zenity, desktop-file-utils, libqt5widgets5, libqt5gui5, libqt5network5, libqt5core5a, libqt5xml5, libqt5concurrent5, wget
+Recommends: openjdk-8-jre
+Homepage: http://multimc.org
+Description: A local install wrapper for MultiMC
+
diff --git a/launcher/package/ubuntu/multimc/DEBIAN/postrm b/launcher/package/ubuntu/multimc/DEBIAN/postrm
new file mode 100755
index 00000000..f9bbc8a7
--- /dev/null
+++ b/launcher/package/ubuntu/multimc/DEBIAN/postrm
@@ -0,0 +1,3 @@
+#!/bin/sh
+set -e
+update-desktop-database
diff --git a/launcher/package/ubuntu/multimc/opt/multimc/icon.svg b/launcher/package/ubuntu/multimc/opt/multimc/icon.svg
new file mode 100644
index 00000000..8bb0e289
--- /dev/null
+++ b/launcher/package/ubuntu/multimc/opt/multimc/icon.svg
@@ -0,0 +1,353 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="68.26667"
+ height="68.26667"
+ id="svg4427"
+ version="1.1"
+ inkscape:version="0.92.1 r"
+ sodipodi:docname="multimc-smooth-biginfinity.svg"
+ inkscape:export-filename="/home/peterix/playground/MultiMC-icons/multimc-smooth-biginfinity.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <defs
+ id="defs4429">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4809">
+ <stop
+ style="stop-color:#98c867;stop-opacity:1"
+ offset="0"
+ id="stop4805" />
+ <stop
+ style="stop-color:#5c9a33;stop-opacity:1"
+ offset="1"
+ id="stop4807" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5668"
+ inkscape:collect="always">
+ <stop
+ id="stop5670"
+ offset="0"
+ style="stop-color:#75b54b;stop-opacity:1;" />
+ <stop
+ id="stop5672"
+ offset="1"
+ style="stop-color:#75b54b;stop-opacity:0.6" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5084"
+ inkscape:collect="always">
+ <stop
+ id="stop5086"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:0.8" />
+ <stop
+ id="stop5088"
+ offset="1"
+ style="stop-color:#000000;stop-opacity:0.35" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient5072"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient5082"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3281"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3283"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3286"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2671525,0,0,0.89790119,-0.01941371,-0.00842234)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3288"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3290"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3293"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114"
+ gradientTransform="scale(1.2671525,0.89790119)" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient5580">
+ <stop
+ style="stop-color:#000000;stop-opacity:0.0627451"
+ offset="0"
+ id="stop5576" />
+ <stop
+ style="stop-color:#322217;stop-opacity:0.58823532"
+ offset="1"
+ id="stop5578" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3999"
+ inkscape:collect="always">
+ <stop
+ id="stop3995"
+ offset="0"
+ style="stop-color:#a3704b;stop-opacity:1" />
+ <stop
+ id="stop3997"
+ offset="1"
+ style="stop-color:#6a4a33;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient2727"
+ inkscape:collect="always">
+ <stop
+ id="stop2723"
+ offset="0"
+ style="stop-color:#966c4a;stop-opacity:1" />
+ <stop
+ id="stop2725"
+ offset="1"
+ style="stop-color:#593d29;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2727"
+ id="linearGradient2050"
+ gradientUnits="userSpaceOnUse"
+ x1="36.546478"
+ y1="33.80484"
+ x2="86.415741"
+ y2="97.065842" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3999"
+ id="radialGradient2052"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-9.105292e-4,-0.00104444)"
+ cx="34.133331"
+ cy="34.133335"
+ fx="34.133331"
+ fy="34.133335"
+ r="29.866665" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5580"
+ id="linearGradient2140"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.0010513,-9.083059e-4)"
+ x1="29.866674"
+ y1="29.867579"
+ x2="38.400005"
+ y2="38.400913" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient4790"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2671525,0,0,0.89790119,-0.82864077,-1.0012743)"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4809"
+ id="radialGradient4803"
+ cx="-42.66758"
+ cy="-34.134373"
+ fx="-42.66758"
+ fy="-34.134373"
+ r="34.132812"
+ gradientTransform="matrix(1.7500268,0.1250019,-0.01781176,0.24936465,95.393964,18.110151)"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.6203867"
+ inkscape:cx="52.171166"
+ inkscape:cy="11.292073"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:document-units="px"
+ inkscape:grid-bbox="true"
+ inkscape:window-width="1368"
+ inkscape:window-height="905"
+ inkscape:window-x="2452"
+ inkscape:window-y="723"
+ inkscape:window-maximized="0"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="false"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="false"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-paths="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:snap-center="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4446"
+ empspacing="16"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ spacingx="4.2666667"
+ spacingy="4.2666667"
+ originx="0"
+ originy="0" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata4432">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <g
+ id="g2048"
+ transform="translate(9.113e-4,0.00104183)">
+ <rect
+ rx="8.5333338"
+ ry="8.5333338"
+ style="fill:url(#linearGradient2050);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ id="rect2026"
+ width="68.26667"
+ height="68.26667"
+ x="-1.3322676e-15"
+ y="3.0270508e-06" />
+ <rect
+ rx="4.2666626"
+ y="4.2656283"
+ x="4.2657552"
+ height="59.733334"
+ width="59.73333"
+ id="rect2028"
+ style="fill:url(#radialGradient2052);fill-opacity:1;stroke:none;stroke-width:14.93333435"
+ ry="4.2666669" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4811"
+ d="m 4.2669272,4.2645856 -9.11e-4,8.5333334 h 4.267577 v 4.267579 h 8.5332038 v 4.265625 h 4.265625 V 8.5322946 H 25.6 v 8.5332034 h 4.265625 v -4.267579 h 4.267578 v 8.533204 h 4.265625 v -4.265625 h 4.267578 v 4.265625 h 4.267579 v -4.265625 h 4.265624 v -4.267579 h 4.267579 v 4.267579 h 8.533203 l -1.3e-4,-12.8009124 z"
+ style="opacity:0.6;fill:#593d29;fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccc" />
+ <path
+ style="fill:url(#radialGradient4803);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ d="m 8.5329442,-0.0018207 c -4.7274675,0 -8.5332035,3.805736 -8.5332035,8.533203 v 4.2675787 h 4.265625 V 8.5313823 c 0,-0.521698 0.105433,-1.01339 0.27539,-1.47461 -0.169616,0.460814 -0.27539,0.953462 -0.27539,1.47461 h 4.2675785 v 4.2675787 h 4.2656248 4.267578 v 4.265625 h 4.265625 V 12.798961 8.5313823 4.2657573 h 4.267578 v 4.265625 4.2675787 h 4.265625 V 8.5313823 h 4.267578 v 4.2675787 4.265625 h 4.265625 v -4.265625 h 4.267578 v 4.265625 h 4.267579 v -4.265625 h 4.265624 V 8.5313823 h 4.267579 v 4.2675787 h 4.265625 4.267578 V 8.5313823 h 4.265625 c 0,-4.727467 -3.805737,-8.533203 -8.533203,-8.533203 z m -3.019531,5.513671 c -0.318089,0.317888 -0.570428,0.695824 -0.7753915,1.101563 0.2048795,-0.405231 0.4576385,-0.784012 0.7753915,-1.101563 z"
+ id="path4794"
+ inkscape:connector-curvature="0" />
+ <path
+ style="opacity:1;fill:url(#linearGradient2140);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ d="m 8.5322887,-9.083059e-4 c -4.72747,0 -8.5332,3.8057359059 -8.5332,8.5332029059 V 59.731515 c 0,4.727467 3.80573,8.535156 8.5332,8.535156 H 59.731502 c 4.72747,0 8.5332,-3.807689 8.5332,-8.535156 V 8.5322946 c 0,-4.727467 -3.80573,-8.5332029059 -8.5332,-8.5332029059 z m 0,4.2675779059 H 59.731502 c 2.36373,0 4.26758,1.901892 4.26758,4.265625 V 59.731515 c 0,2.363733 -1.90385,4.267578 -4.26758,4.267578 H 8.5322887 c -2.36373,0 -4.26758,-1.903845 -4.26758,-4.267578 V 8.5322946 c 0,-2.363733 1.90385,-4.265625 4.26758,-4.265625 z"
+ id="path2046"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ id="g1092">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4786"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient4790);fill-opacity:1;stroke:none;stroke-width:1.06666672;opacity:0.5"
+ d="m 38.886673,44.940882 c -0.974277,-0.801673 -2.231353,-2.137814 -3.771231,-4.008427 -2.105641,2.672298 -4.085536,4.598569 -5.939688,5.778816 -2.325625,1.425227 -5.295467,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124578 -10.5594458,-3.37376 C 6.7526311,43.114834 5.275567,39.986037 5.2755773,36.088937 5.275567,32.347763 6.7526311,29.207831 9.7067742,26.669132 12.346618,24.419991 15.897857,23.295407 20.360501,23.295373 c 2.294138,3.4e-5 4.289747,0.334069 5.986829,1.002107 1.979863,0.73491 3.645488,1.737016 4.996881,3.00632 1.257039,1.135751 2.514115,2.471891 3.771231,4.008428 2.105563,-2.672257 4.085457,-4.598527 5.939689,-5.778816 2.325544,-1.425186 5.295385,-2.137794 8.909533,-2.137828 4.242577,3.4e-5 7.762388,1.124618 10.559447,3.37376 2.954063,2.360546 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477134,6.881147 -4.431197,9.419806 -2.639925,2.24918 -6.191163,3.373767 -10.653727,3.373758 -2.294219,9e-6 -4.289826,-0.334026 -5.98683,-1.002106 -1.697101,-0.601255 -3.362726,-1.603361 -4.996881,-3.006321 M 19.747676,44.473233 c 5.185412,1.1e-5 9.333763,-2.672271 12.445062,-8.016856 -3.991253,-5.834464 -8.139602,-8.751705 -12.445062,-8.751733 -3.142715,2.8e-5 -5.515446,0.801713 -7.118198,2.405057 -1.728498,1.71474 -2.592737,3.707818 -2.592722,5.979236 -1.5e-5,2.494152 0.864224,4.509499 2.592722,6.046046 1.759887,1.558846 4.132618,2.338261 7.118198,2.33825 M 50.483209,27.77145 c -4.682663,2.9e-5 -8.831013,2.672312 -12.445062,8.016856 3.959745,5.834503 8.108095,8.751746 12.445062,8.751733 3.142633,1.3e-5 5.515364,-0.801671 7.118198,-2.405056 1.728416,-1.714701 2.592656,-3.707778 2.592722,-5.979238 -6.6e-5,-2.49411 -0.864306,-4.509456 -2.592722,-6.046044 -1.759968,-1.558805 -4.132699,-2.338222 -7.118198,-2.338251" />
+ <path
+ d="m 39.715314,45.942156 c -0.974277,-0.801673 -2.231353,-2.137814 -3.771231,-4.008427 -2.105641,2.672298 -4.085536,4.598569 -5.939688,5.778816 -2.325625,1.425227 -5.295467,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124578 -10.559446,-3.37376 -2.9541431,-2.360505 -4.4312072,-5.489302 -4.4311969,-9.386402 -1.03e-5,-3.741174 1.4770538,-6.881106 4.4311969,-9.419805 2.639844,-2.249141 6.191083,-3.373725 10.653727,-3.373759 2.294138,3.4e-5 4.289747,0.334069 5.986829,1.002107 1.979863,0.73491 3.645488,1.737016 4.996881,3.00632 1.257039,1.135751 2.514115,2.471891 3.771231,4.008428 2.105563,-2.672257 4.085457,-4.598527 5.939689,-5.778816 2.325544,-1.425186 5.295385,-2.137794 8.909533,-2.137828 4.242577,3.4e-5 7.762388,1.124618 10.559447,3.37376 2.954063,2.360546 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477134,6.881147 -4.431197,9.419806 -2.639925,2.24918 -6.191163,3.373767 -10.653727,3.373758 -2.294219,9e-6 -4.289826,-0.334026 -5.98683,-1.002106 -1.697101,-0.601255 -3.362726,-1.603361 -4.996881,-3.006321 M 20.576317,45.474507 c 5.185412,1.1e-5 9.333763,-2.672271 12.445062,-8.016856 -3.991253,-5.834464 -8.139602,-8.751705 -12.445062,-8.751733 -3.142715,2.8e-5 -5.515446,0.801713 -7.118198,2.405057 -1.728498,1.71474 -2.592737,3.707818 -2.592722,5.979236 -1.5e-5,2.494152 0.864224,4.509499 2.592722,6.046046 1.759887,1.558846 4.132618,2.338261 7.118198,2.33825 M 51.31185,28.772724 c -4.682663,2.9e-5 -8.831013,2.672312 -12.445062,8.016856 3.959745,5.834503 8.108095,8.751746 12.445062,8.751733 3.142633,1.3e-5 5.515364,-0.801671 7.118198,-2.405056 1.728416,-1.714701 2.592656,-3.707778 2.592722,-5.979238 -6.6e-5,-2.49411 -0.864306,-4.509456 -2.592722,-6.046044 C 56.67008,29.55217 54.297349,28.772753 51.31185,28.772724"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3293);fill-opacity:1;stroke:none;stroke-width:1.06666672;opacity:0.5"
+ id="path3279"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 37.904564,42.951873 c -0.974278,-0.801672 -2.231352,-2.137814 -3.771231,-4.008428 -2.105642,2.672298 -4.085537,4.598568 -5.939688,5.778817 -2.325625,1.425227 -5.295466,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124577 -10.5594464,-3.37376 -2.9541428,-2.360505 -4.4312068,-5.489302 -4.4311963,-9.386401 -1.05e-5,-3.741175 1.4770535,-6.881107 4.4311963,-9.419805 2.6398444,-2.249142 6.1910824,-3.373727 10.6537284,-3.37376 2.294137,3.3e-5 4.289745,0.334068 5.986829,1.002107 1.979863,0.734909 3.645487,1.737016 4.99688,3.00632 1.257039,1.13575 2.514116,2.471891 3.771231,4.008428 2.105562,-2.672257 4.085456,-4.598528 5.939689,-5.778817 2.325544,-1.425185 5.295387,-2.137795 8.909534,-2.137828 4.242576,3.3e-5 7.762387,1.12462 10.559446,3.373761 2.954062,2.360545 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477135,6.881148 -4.431197,9.419805 -2.639924,2.249182 -6.191164,3.373767 -10.653728,3.37376 -2.294217,7e-6 -4.289826,-0.334028 -5.986828,-1.002107 -1.697101,-0.601254 -3.362727,-1.603361 -4.996882,-3.006321 m -19.138997,-0.46765 c 5.185412,1.3e-5 9.333762,-2.67227 12.445062,-8.016856 -3.991252,-5.834462 -8.139602,-8.751704 -12.445062,-8.751733 -3.142714,2.9e-5 -5.515444,0.801714 -7.118198,2.405056 -1.7284972,1.714743 -2.5927368,3.707819 -2.5927216,5.979239 -1.52e-5,2.49415 0.8642244,4.509496 2.5927216,6.046045 1.759888,1.558845 4.132618,2.338262 7.118198,2.338249 M 49.5011,25.782442 c -4.682663,2.8e-5 -8.831014,2.672311 -12.445063,8.016855 3.959745,5.834504 8.108096,8.751745 12.445063,8.751733 3.142634,1.2e-5 5.515365,-0.801673 7.118198,-2.405056 1.728417,-1.7147 2.592657,-3.707778 2.592721,-5.979238 -6.4e-5,-2.49411 -0.864304,-4.509456 -2.592721,-6.046046 C 54.85933,26.561886 52.486599,25.78247 49.5011,25.782442"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3286);fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ id="path3272"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="ccscsccccccccccccccccccccscscccccsccscccccc"
+ inkscape:connector-curvature="0"
+ id="text5100"
+ d="m 19.4,21.166667 c -4.462644,3.3e-5 -8.026822,1.150858 -10.6666667,3.4 -2.9541428,2.538698 -4.4333436,5.658825 -4.4333333,9.4 -1.03e-5,3.897098 1.4791905,7.039495 4.4333333,9.4 -1.622701,-2.044271 -2.433341,-4.51168 -2.4333333,-7.4 -1.03e-5,-3.741175 1.4791905,-6.861302 4.433333,-9.4 2.639845,-2.249142 6.204023,-3.399967 10.666667,-3.4 2.294138,3.3e-5 4.302916,0.365295 6,1.033333 1.979862,0.73491 3.615274,1.730695 4.966667,3 0.06836,0.06177 0.131637,0.137049 0.2,0.2 -0.731813,-0.797005 -1.468213,-1.538822 -2.2,-2.2 -1.351393,-1.269305 -2.986805,-2.26509 -4.966667,-3 -1.697084,-0.668038 -3.705862,-1.0333 -6,-1.033333 z m 29.6,0.1 c -3.614148,3.3e-5 -6.574457,0.74148 -8.9,2.166666 -1.818222,1.157367 -3.923451,3.291388 -5.983333,5.883334 0.618278,0.658774 1.248369,1.377605 1.866666,2.133333 2.105562,-2.672257 4.262434,-4.836378 6.116667,-6.016667 2.325543,-1.425186 5.285852,-2.166633 8.9,-2.166666 4.242576,3.3e-5 7.769607,1.150858 10.566667,3.4 -0.570388,-0.722129 -1.227721,-1.382884 -2,-2 C 56.769607,22.417525 53.242576,21.2667 49,21.266667 Z m 8.866667,8.1 c 0.9092,1.305235 1.366619,2.857751 1.366666,4.666666 -6.5e-5,2.271461 -0.871584,4.285301 -2.6,6 -1.602834,1.603384 -3.957366,2.400012 -7.1,2.4 -2.653707,8e-6 -5.320858,-1.032242 -7.833333,-3.216666 3.136636,3.509305 6.469807,5.216676 9.833333,5.216666 3.142634,1.2e-5 5.497166,-0.796616 7.1,-2.4 1.728416,-1.714699 2.599935,-3.728539 2.6,-6 -6.5e-5,-2.49411 -0.871584,-4.496744 -2.6,-6.033333 -0.24943,-0.220921 -0.49262,-0.443723 -0.766666,-0.633333 z m -26.633334,4.966666 c -3.1113,5.344585 -7.247921,8.033345 -12.433333,8.033334 -2.58055,1e-5 -4.543473,-0.352086 -6.208333,-1.516667 0.348871,0.50642 0.590094,0.752276 1.075,1.183333 1.759888,1.558846 4.147753,2.333345 7.133333,2.333334 5.185412,1.1e-5 9.322033,-2.688749 12.433333,-8.033334 z m 4.933334,6.5 c -0.04103,0.05207 -0.09239,0.08182 -0.133334,0.133334 0.687326,0.744419 1.306949,1.359747 1.833334,1.8 -0.529404,-0.580895 -1.078447,-1.178283 -1.7,-1.933334 z"
+ style="font-style:normal;font-weight:normal;font-size:76.18933868px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:0.3;fill:#ccff00;fill-opacity:1;stroke:none;stroke-width:1.06666672" />
+ <path
+ sodipodi:nodetypes="ccsccscccccccccccccccccccscscccccsccsccccccc"
+ id="text5058-0"
+ d="m 19.730474,21.54714 c -4.462645,3.3e-5 -8.026823,1.150859 -10.6666669,3.4 -2.9541429,2.538699 -4.433344,5.658826 -4.4333333,9.4 -1.07e-5,3.897099 1.4791904,7.039495 4.4333333,9.4 0.042837,0.03444 0.090155,0.06608 0.1333334,0.1 -2.2392086,-2.228193 -3.3666752,-5.040417 -3.3666667,-8.433333 -1.07e-5,-3.741174 1.4791904,-6.861301 4.4333332,-9.4 2.639844,-2.249141 6.204022,-3.399967 10.666667,-3.4 2.294137,3.3e-5 4.302916,0.365295 6,1.033333 1.870874,0.694455 3.42364,1.628367 4.733333,2.8 -0.314265,-0.308986 -0.652406,-0.582729 -0.966667,-0.866666 -1.351393,-1.269305 -2.986804,-2.265091 -4.966666,-3 -1.697084,-0.668039 -3.705863,-1.033301 -6,-1.033334 z m 29.6,0.1 c -3.614149,3.3e-5 -6.574457,0.741481 -8.9,2.166667 -1.813279,1.154221 -3.963039,3.235656 -6.016667,5.816667 0.355649,0.402628 0.711011,0.798625 1.066667,1.233333 2.105561,-2.672257 4.295767,-4.803044 6.15,-5.983333 2.325543,-1.425187 5.285851,-2.166634 8.9,-2.166667 4.22442,3.3e-5 7.742084,1.136734 10.533333,3.366667 -0.36096,-0.367566 -0.745726,-0.696967 -1.166667,-1.033334 -2.797059,-2.249141 -6.32409,-3.399967 -10.566666,-3.4 z m 8.233333,7.333334 c 1.323326,1.449243 1.999942,3.250987 2,5.433333 -6.5e-5,2.27146 -0.871584,4.2853 -2.6,6 -1.602834,1.603383 -3.957366,2.400012 -7.1,2.4 -2.406328,6e-6 -4.776468,-0.90386 -7.066667,-2.7 2.669147,2.483838 5.436929,3.766674 8.266667,3.766667 3.142634,1.1e-5 5.497166,-0.796617 7.1,-2.4 1.728416,-1.7147 2.599935,-3.72854 2.6,-6 -6.5e-5,-2.49411 -0.871584,-4.496745 -2.6,-6.033334 -0.185641,-0.164422 -0.400724,-0.319587 -0.6,-0.466666 z m -26,5.733333 c -3.1113,5.344584 -7.247921,8.033345 -12.433333,8.033333 -2.612382,1.1e-5 -4.759372,-0.60651 -6.433334,-1.8 0.166027,0.176488 0.313947,0.367942 0.5,0.533334 1.759888,1.558845 4.147754,2.333345 7.133334,2.333333 5.185412,1.2e-5 9.322033,-2.688749 12.433333,-8.033333 z m 4.133333,5.566667 c -0.04657,0.05909 -0.08689,0.108298 -0.133333,0.166666 1.038571,1.18897 1.9748,2.169945 2.7,2.766667 0.06249,0.05364 0.137426,0.08086 0.2,0.133333 -0.792178,-0.781249 -1.706288,-1.778539 -2.766667,-3.066666 z"
+ style="font-style:normal;font-weight:normal;font-size:76.18933868px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:0.6;fill:#ccff00;fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/package/ubuntu/multimc/opt/multimc/run.sh b/launcher/package/ubuntu/multimc/opt/multimc/run.sh
new file mode 100755
index 00000000..c493a513
--- /dev/null
+++ b/launcher/package/ubuntu/multimc/opt/multimc/run.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+INSTDIR="${XDG_DATA_HOME-$HOME/.local/share}/multimc"
+
+if [ `getconf LONG_BIT` = "64" ]
+then
+ PACKAGE="mmc-stable-lin64.tar.gz"
+else
+ PACKAGE="mmc-stable-lin32.tar.gz"
+fi
+
+deploy() {
+ mkdir -p $INSTDIR
+ cd ${INSTDIR}
+
+ wget --progress=dot:force "https://files.multimc.org/downloads/${PACKAGE}" 2>&1 | sed -u 's/.* \([0-9]\+%\)\ \+\([0-9.]\+.\) \(.*\)/\1\n# Downloading at \2\/s, ETA \3/' | zenity --progress --auto-close --auto-kill --title="Downloading MultiMC..."
+
+ tar -xzf ${PACKAGE} --transform='s,MultiMC/,,'
+ rm ${PACKAGE}
+ chmod +x MultiMC
+}
+
+runmmc() {
+ cd ${INSTDIR}
+ ./MultiMC "$@"
+}
+
+if [[ ! -f ${INSTDIR}/MultiMC ]]; then
+ deploy
+ runmmc "$@"
+else
+ runmmc "$@"
+fi
diff --git a/launcher/package/ubuntu/multimc/usr/share/applications/multimc.desktop b/launcher/package/ubuntu/multimc/usr/share/applications/multimc.desktop
new file mode 100755
index 00000000..e0456f89
--- /dev/null
+++ b/launcher/package/ubuntu/multimc/usr/share/applications/multimc.desktop
@@ -0,0 +1,16 @@
+[Desktop Entry]
+Categories=Game;
+Exec=/opt/multimc/run.sh
+Icon=/opt/multimc/icon.svg
+Keywords=game;Minecraft;
+MimeType=
+Name=MultiMC 5
+Path=
+StartupNotify=true
+Terminal=false
+TerminalOptions=
+Type=Application
+X-DBUS-ServiceName=
+X-DBUS-StartupType=
+X-KDE-SubstituteUID=false
+X-KDE-Username=
diff --git a/launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml b/launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml
new file mode 100644
index 00000000..4c6b7450
--- /dev/null
+++ b/launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop">
+ <id>multimc</id>
+ <launchable type="desktop-id">multimc.desktop</launchable>
+ <name>MultiMC</name>
+ <summary>Manage Minecraft instances with ease</summary>
+ <description>
+ <p>Overview</p>
+ <p>MultiMC is a free, open source launcher for Minecraft. It allows you to have multiple, cleanly separated instances of Minecraft (each with their own mods, texture packs, saves, etc) and helps you manage them and their associated options with a simple and powerful interface.</p>
+ <p>Features</p>
+ <ul>
+ <li>Manage multiple instances of Minecraft at once</li>
+ <li>Start Minecraft with a custom resolution</li>
+ <li>Change Java's runtime options (including memory options)</li>
+ <li>Shows Minecraft's console output in a colour coded window</li>
+ <li>Kill Minecraft easily if it crashes / freezes</li>
+ <li>Custom icons and groups for instances</li>
+ <li>Forge integration (automatic installation, version downloads, mod management)</li>
+ <li>Minecraft world management</li>
+ <li>Import and export Minecraft instances to share them with anyone</li>
+ <li>Supports every version of Minecraft that the vanilla launcher does</li>
+ </ul>
+ </description>
+ <screenshots>
+ <screenshot type="default">
+ <image type="source" width="936" height="921">https://multimc.org/images/screenshots/main.png</image>
+ </screenshot>
+ <screenshot>
+ <image type="source" width="936" height="998">https://multimc.org/images/screenshots/editmods.png</image>
+ </screenshot>
+ <screenshot>
+ <image type="source" width="936" height="998">https://multimc.org/images/screenshots/version.png</image>
+ </screenshot>
+ <screenshot>
+ <image type="source" width="936" height="998">https://multimc.org/images/screenshots/console.png</image>
+ </screenshot>
+ <screenshot>
+ <image type="source" width="936" height="921">https://multimc.org/images/screenshots/settings.png</image>
+ </screenshot>
+ </screenshots>
+ <releases>
+ <release date="2021-01-07" version="5"/>
+ </releases>
+ <url type="homepage">https://multimc.org/</url>
+ <url type="help">https://discord.com/invite/0k2zsXGNHs0fE4Wm</url>
+ <url type="faq">https://github.com/MultiMC/MultiMC5/wiki/FAQ</url>
+ <url type="bugtracker">https://github.com/MultiMC/MultiMC5/issues</url>
+ <url type="translate">https://translate.multimc.org/</url>
+ <url type="donation">https://www.patreon.com/multimc</url>
+ <developer_name>The MultiMC Team</developer_name>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>Apache-2.0</project_license>
+ <update_contact>peterix_at_gmail.com</update_contact>
+</component>
diff --git a/launcher/pagedialog/PageDialog.cpp b/launcher/pagedialog/PageDialog.cpp
new file mode 100644
index 00000000..fd5d36d4
--- /dev/null
+++ b/launcher/pagedialog/PageDialog.cpp
@@ -0,0 +1,61 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PageDialog.h"
+
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include <QKeyEvent>
+
+#include "MultiMC.h"
+#include "settings/SettingsObject.h"
+#include "widgets/IconLabel.h"
+#include "widgets/PageContainer.h"
+
+PageDialog::PageDialog(BasePageProvider *pageProvider, QString defaultId, QWidget *parent)
+ : QDialog(parent)
+{
+ setWindowTitle(pageProvider->dialogTitle());
+ m_container = new PageContainer(pageProvider, defaultId, this);
+
+ QVBoxLayout *mainLayout = new QVBoxLayout;
+ mainLayout->addWidget(m_container);
+ mainLayout->setSpacing(0);
+ mainLayout->setContentsMargins(0, 0, 0, 0);
+ setLayout(mainLayout);
+
+ QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close);
+ buttons->button(QDialogButtonBox::Close)->setDefault(true);
+ buttons->setContentsMargins(6, 0, 6, 0);
+ m_container->addButtons(buttons);
+
+ connect(buttons->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(close()));
+ connect(buttons->button(QDialogButtonBox::Help), SIGNAL(clicked()), m_container, SLOT(help()));
+
+ restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("PagedGeometry").toByteArray()));
+}
+
+void PageDialog::closeEvent(QCloseEvent *event)
+{
+ qDebug() << "Paged dialog close requested";
+ if (m_container->prepareToClose())
+ {
+ qDebug() << "Paged dialog close approved";
+ MMC->settings()->set("PagedGeometry", saveGeometry().toBase64());
+ qDebug() << "Paged dialog geometry saved";
+ QDialog::closeEvent(event);
+ }
+}
diff --git a/launcher/pagedialog/PageDialog.h b/launcher/pagedialog/PageDialog.h
new file mode 100644
index 00000000..1029bc30
--- /dev/null
+++ b/launcher/pagedialog/PageDialog.h
@@ -0,0 +1,35 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QDialog>
+#include "pages/BasePageProvider.h"
+
+class PageContainer;
+class PageDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ explicit PageDialog(BasePageProvider *pageProvider, QString defaultId = QString(), QWidget *parent = 0);
+ virtual ~PageDialog() {}
+
+private
+slots:
+ virtual void closeEvent(QCloseEvent *event);
+
+private:
+ PageContainer * m_container;
+};
diff --git a/launcher/pages/BasePage.h b/launcher/pages/BasePage.h
new file mode 100644
index 00000000..408965d0
--- /dev/null
+++ b/launcher/pages/BasePage.h
@@ -0,0 +1,58 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QIcon>
+#include <memory>
+
+#include "BasePageContainer.h"
+
+class BasePage
+{
+public:
+ virtual ~BasePage() {}
+ virtual QString id() const = 0;
+ virtual QString displayName() const = 0;
+ virtual QIcon icon() const = 0;
+ virtual bool apply() { return true; }
+ virtual bool shouldDisplay() const { return true; }
+ virtual QString helpPage() const { return QString(); }
+ void opened()
+ {
+ isOpened = true;
+ openedImpl();
+ }
+ void closed()
+ {
+ isOpened = false;
+ closedImpl();
+ }
+ virtual void openedImpl() {}
+ virtual void closedImpl() {}
+ virtual void setParentContainer(BasePageContainer * container)
+ {
+ m_container = container;
+ };
+public:
+ int stackIndex = -1;
+ int listIndex = -1;
+protected:
+ BasePageContainer * m_container = nullptr;
+ bool isOpened = false;
+};
+
+typedef std::shared_ptr<BasePage> BasePagePtr;
diff --git a/launcher/pages/BasePageContainer.h b/launcher/pages/BasePageContainer.h
new file mode 100644
index 00000000..f8c7adeb
--- /dev/null
+++ b/launcher/pages/BasePageContainer.h
@@ -0,0 +1,10 @@
+#pragma once
+
+class BasePageContainer
+{
+public:
+ virtual ~BasePageContainer(){};
+ virtual bool selectPage(QString pageId) = 0;
+ virtual void refreshContainer() = 0;
+ virtual bool requestClose() = 0;
+};
diff --git a/launcher/pages/BasePageProvider.h b/launcher/pages/BasePageProvider.h
new file mode 100644
index 00000000..7bfaaf3b
--- /dev/null
+++ b/launcher/pages/BasePageProvider.h
@@ -0,0 +1,68 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pages/BasePage.h"
+#include <memory>
+#include <functional>
+
+class BasePageProvider
+{
+public:
+ virtual QList<BasePage *> getPages() = 0;
+ virtual QString dialogTitle() = 0;
+};
+
+class GenericPageProvider : public BasePageProvider
+{
+ typedef std::function<BasePage *()> PageCreator;
+public:
+ explicit GenericPageProvider(const QString &dialogTitle)
+ : m_dialogTitle(dialogTitle)
+ {
+ }
+ virtual ~GenericPageProvider() {}
+
+ QList<BasePage *> getPages() override
+ {
+ QList<BasePage *> pages;
+ for (PageCreator creator : m_creators)
+ {
+ pages.append(creator());
+ }
+ return pages;
+ }
+ QString dialogTitle() override { return m_dialogTitle; }
+
+ void setDialogTitle(const QString &title)
+ {
+ m_dialogTitle = title;
+ }
+ void addPageCreator(PageCreator page)
+ {
+ m_creators.append(page);
+ }
+
+ template<typename PageClass>
+ void addPage()
+ {
+ addPageCreator([](){return new PageClass();});
+ }
+
+private:
+ QList<PageCreator> m_creators;
+ QString m_dialogTitle;
+};
diff --git a/launcher/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp
new file mode 100644
index 00000000..ff3736ed
--- /dev/null
+++ b/launcher/pages/global/AccountListPage.cpp
@@ -0,0 +1,217 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "AccountListPage.h"
+#include "ui_AccountListPage.h"
+
+#include <QItemSelectionModel>
+#include <QMenu>
+
+#include <QDebug>
+
+#include "net/NetJob.h"
+#include "Env.h"
+
+#include "dialogs/ProgressDialog.h"
+#include "dialogs/LoginDialog.h"
+#include "dialogs/CustomMessageBox.h"
+#include "dialogs/SkinUploadDialog.h"
+#include "tasks/Task.h"
+#include "minecraft/auth/YggdrasilTask.h"
+#include "minecraft/services/SkinDelete.h"
+
+#include "MultiMC.h"
+
+#include "BuildConfig.h"
+
+AccountListPage::AccountListPage(QWidget *parent)
+ : QMainWindow(parent), ui(new Ui::AccountListPage)
+{
+ ui->setupUi(this);
+ ui->listView->setEmptyString(tr(
+ "Welcome!\n"
+ "If you're new here, you can click the \"Add\" button to add your Mojang or Minecraft account."
+ ));
+ ui->listView->setEmptyMode(VersionListView::String);
+ ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ m_accounts = MMC->accounts();
+
+ ui->listView->setModel(m_accounts.get());
+ ui->listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ ui->listView->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ // Expand the account column
+ ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
+
+ QItemSelectionModel *selectionModel = ui->listView->selectionModel();
+
+ connect(selectionModel, &QItemSelectionModel::selectionChanged, [this](const QItemSelection &sel, const QItemSelection &dsel) {
+ updateButtonStates();
+ });
+ connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu);
+
+ connect(m_accounts.get(), SIGNAL(listChanged()), SLOT(listChanged()));
+ connect(m_accounts.get(), SIGNAL(activeAccountChanged()), SLOT(listChanged()));
+
+ updateButtonStates();
+}
+
+AccountListPage::~AccountListPage()
+{
+ delete ui;
+}
+
+void AccountListPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->listView->mapToGlobal(pos));
+ delete menu;
+}
+
+void AccountListPage::changeEvent(QEvent* event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ ui->retranslateUi(this);
+ }
+ QMainWindow::changeEvent(event);
+}
+
+QMenu * AccountListPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction(ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+
+void AccountListPage::listChanged()
+{
+ updateButtonStates();
+}
+
+void AccountListPage::on_actionAdd_triggered()
+{
+ addAccount(tr("Please enter your Minecraft account email and password to add your account."));
+}
+
+void AccountListPage::on_actionRemove_triggered()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ m_accounts->removeAccount(selected);
+ }
+}
+
+void AccountListPage::on_actionSetDefault_triggered()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ MojangAccountPtr account =
+ selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
+ m_accounts->setActiveAccount(account->username());
+ }
+}
+
+void AccountListPage::on_actionNoDefault_triggered()
+{
+ m_accounts->setActiveAccount("");
+}
+
+void AccountListPage::updateButtonStates()
+{
+ // If there is no selection, disable buttons that require something selected.
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+
+ ui->actionRemove->setEnabled(selection.size() > 0);
+ ui->actionSetDefault->setEnabled(selection.size() > 0);
+ ui->actionUploadSkin->setEnabled(selection.size() > 0);
+ ui->actionDeleteSkin->setEnabled(selection.size() > 0);
+
+ if(m_accounts->activeAccount().get() == nullptr) {
+ ui->actionNoDefault->setEnabled(false);
+ ui->actionNoDefault->setChecked(true);
+ }
+ else {
+ ui->actionNoDefault->setEnabled(true);
+ ui->actionNoDefault->setChecked(false);
+ }
+
+}
+
+void AccountListPage::addAccount(const QString &errMsg)
+{
+ // TODO: The login dialog isn't quite done yet
+ MojangAccountPtr account = LoginDialog::newAccount(this, errMsg);
+
+ if (account != nullptr)
+ {
+ m_accounts->addAccount(account);
+ if (m_accounts->count() == 1)
+ m_accounts->setActiveAccount(account->username());
+
+ // Grab associated player skins
+ auto job = new NetJob("Player skins: " + account->username());
+
+ for (AccountProfile profile : account->profiles())
+ {
+ auto meta = Env::getInstance().metacache()->resolveEntry("skins", profile.id + ".png");
+ auto action = Net::Download::makeCached(QUrl(BuildConfig.SKINS_BASE + profile.id + ".png"), meta);
+ job->addNetAction(action);
+ meta->setStale(true);
+ }
+
+ job->start();
+ }
+}
+
+void AccountListPage::on_actionUploadSkin_triggered()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() > 0)
+ {
+ QModelIndex selected = selection.first();
+ MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
+ SkinUploadDialog dialog(account, this);
+ dialog.exec();
+ }
+}
+
+void AccountListPage::on_actionDeleteSkin_triggered()
+{
+ QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.size() <= 0)
+ return;
+
+ QModelIndex selected = selection.first();
+ AuthSessionPtr session = std::make_shared<AuthSession>();
+ MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>();
+ auto login = account->login(session);
+ ProgressDialog prog(this);
+ if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) {
+ CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec();
+ return;
+ }
+ auto deleteSkinTask = std::make_shared<SkinDelete>(this, session);
+ if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) {
+ CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec();
+ return;
+ }
+}
diff --git a/launcher/pages/global/AccountListPage.h b/launcher/pages/global/AccountListPage.h
new file mode 100644
index 00000000..fba1833f
--- /dev/null
+++ b/launcher/pages/global/AccountListPage.h
@@ -0,0 +1,84 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMainWindow>
+#include <memory>
+
+#include "pages/BasePage.h"
+
+#include "minecraft/auth/MojangAccountList.h"
+#include "MultiMC.h"
+
+namespace Ui
+{
+class AccountListPage;
+}
+
+class AuthenticateTask;
+
+class AccountListPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+public:
+ explicit AccountListPage(QWidget *parent = 0);
+ ~AccountListPage();
+
+ QString displayName() const override
+ {
+ return tr("Accounts");
+ }
+ QIcon icon() const override
+ {
+ auto icon = MMC->getThemedIcon("accounts");
+ if(icon.isNull())
+ {
+ icon = MMC->getThemedIcon("noaccount");
+ }
+ return icon;
+ }
+ QString id() const override
+ {
+ return "accounts";
+ }
+ QString helpPage() const override
+ {
+ return "Getting-Started#adding-an-account";
+ }
+
+public slots:
+ void on_actionAdd_triggered();
+ void on_actionRemove_triggered();
+ void on_actionSetDefault_triggered();
+ void on_actionNoDefault_triggered();
+ void on_actionUploadSkin_triggered();
+ void on_actionDeleteSkin_triggered();
+
+ void listChanged();
+
+ //! Updates the states of the dialog's buttons.
+ void updateButtonStates();
+
+protected slots:
+ void ShowContextMenu(const QPoint &pos);
+ void addAccount(const QString& errMsg="");
+
+private:
+ void changeEvent(QEvent * event) override;
+ QMenu * createPopupMenu() override;
+ std::shared_ptr<MojangAccountList> m_accounts;
+ Ui::AccountListPage *ui;
+};
diff --git a/launcher/pages/global/AccountListPage.ui b/launcher/pages/global/AccountListPage.ui
new file mode 100644
index 00000000..71647db3
--- /dev/null
+++ b/launcher/pages/global/AccountListPage.ui
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AccountListPage</class>
+ <widget class="QMainWindow" name="AccountListPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="VersionListView" name="listView"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAdd"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionSetDefault"/>
+ <addaction name="actionNoDefault"/>
+ <addaction name="separator"/>
+ <addaction name="actionUploadSkin"/>
+ <addaction name="actionDeleteSkin"/>
+ </widget>
+ <action name="actionAdd">
+ <property name="text">
+ <string>Add</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </action>
+ <action name="actionSetDefault">
+ <property name="text">
+ <string>Set Default</string>
+ </property>
+ </action>
+ <action name="actionNoDefault">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>No Default</string>
+ </property>
+ </action>
+ <action name="actionUploadSkin">
+ <property name="text">
+ <string>Upload Skin</string>
+ </property>
+ </action>
+ <action name="actionDeleteSkin">
+ <property name="text">
+ <string>Delete Skin</string>
+ </property>
+ <property name="toolTip">
+ <string>Delete the currently active skin and go back to the default one</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>VersionListView</class>
+ <extends>QTreeView</extends>
+ <header>widgets/VersionListView.h</header>
+ </customwidget>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/global/CustomCommandsPage.cpp b/launcher/pages/global/CustomCommandsPage.cpp
new file mode 100644
index 00000000..3b182319
--- /dev/null
+++ b/launcher/pages/global/CustomCommandsPage.cpp
@@ -0,0 +1,51 @@
+#include "CustomCommandsPage.h"
+#include <QVBoxLayout>
+#include <QTabWidget>
+#include <QTabBar>
+
+CustomCommandsPage::CustomCommandsPage(QWidget* parent): QWidget(parent)
+{
+
+ auto verticalLayout = new QVBoxLayout(this);
+ verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ verticalLayout->setContentsMargins(0, 0, 0, 0);
+
+ auto tabWidget = new QTabWidget(this);
+ tabWidget->setObjectName(QStringLiteral("tabWidget"));
+ commands = new CustomCommands(this);
+ commands->setContentsMargins(6, 6, 6, 6);
+ tabWidget->addTab(commands, "Foo");
+ tabWidget->tabBar()->hide();
+ verticalLayout->addWidget(tabWidget);
+ loadSettings();
+}
+
+CustomCommandsPage::~CustomCommandsPage()
+{
+}
+
+bool CustomCommandsPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void CustomCommandsPage::applySettings()
+{
+ auto s = MMC->settings();
+ s->set("PreLaunchCommand", commands->prelaunchCommand());
+ s->set("WrapperCommand", commands->wrapperCommand());
+ s->set("PostExitCommand", commands->postexitCommand());
+}
+
+void CustomCommandsPage::loadSettings()
+{
+ auto s = MMC->settings();
+ commands->initialize(
+ false,
+ true,
+ s->get("PreLaunchCommand").toString(),
+ s->get("WrapperCommand").toString(),
+ s->get("PostExitCommand").toString()
+ );
+}
diff --git a/launcher/pages/global/CustomCommandsPage.h b/launcher/pages/global/CustomCommandsPage.h
new file mode 100644
index 00000000..414c3259
--- /dev/null
+++ b/launcher/pages/global/CustomCommandsPage.h
@@ -0,0 +1,55 @@
+/* Copyright 2018-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <QDialog>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "widgets/CustomCommands.h"
+
+class CustomCommandsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit CustomCommandsPage(QWidget *parent = 0);
+ ~CustomCommandsPage();
+
+ QString displayName() const override
+ {
+ return tr("Custom Commands");
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("custom-commands");
+ }
+ QString id() const override
+ {
+ return "custom-commands";
+ }
+ QString helpPage() const override
+ {
+ return "Custom-commands";
+ }
+ bool apply() override;
+
+private:
+ void applySettings();
+ void loadSettings();
+ CustomCommands * commands;
+};
diff --git a/launcher/pages/global/ExternalToolsPage.cpp b/launcher/pages/global/ExternalToolsPage.cpp
new file mode 100644
index 00000000..6a0a38be
--- /dev/null
+++ b/launcher/pages/global/ExternalToolsPage.cpp
@@ -0,0 +1,233 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ExternalToolsPage.h"
+#include "ui_ExternalToolsPage.h"
+
+#include <QMessageBox>
+#include <QFileDialog>
+#include <QStandardPaths>
+#include <QTabBar>
+
+#include "settings/SettingsObject.h"
+#include "tools/BaseProfiler.h"
+#include <FileSystem.h>
+#include "MultiMC.h"
+#include <tools/MCEditTool.h>
+
+ExternalToolsPage::ExternalToolsPage(QWidget *parent) :
+ QWidget(parent),
+ ui(new Ui::ExternalToolsPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+
+ #if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)
+ ui->jsonEditorTextBox->setClearButtonEnabled(true);
+ #endif
+
+ ui->mceditLink->setOpenExternalLinks(true);
+ ui->jvisualvmLink->setOpenExternalLinks(true);
+ ui->jprofilerLink->setOpenExternalLinks(true);
+ loadSettings();
+}
+
+ExternalToolsPage::~ExternalToolsPage()
+{
+ delete ui;
+}
+
+void ExternalToolsPage::loadSettings()
+{
+ auto s = MMC->settings();
+ ui->jprofilerPathEdit->setText(s->get("JProfilerPath").toString());
+ ui->jvisualvmPathEdit->setText(s->get("JVisualVMPath").toString());
+ ui->mceditPathEdit->setText(s->get("MCEditPath").toString());
+
+ // Editors
+ ui->jsonEditorTextBox->setText(s->get("JsonEditor").toString());
+}
+void ExternalToolsPage::applySettings()
+{
+ auto s = MMC->settings();
+
+ s->set("JProfilerPath", ui->jprofilerPathEdit->text());
+ s->set("JVisualVMPath", ui->jvisualvmPathEdit->text());
+ s->set("MCEditPath", ui->mceditPathEdit->text());
+
+ // Editors
+ QString jsonEditor = ui->jsonEditorTextBox->text();
+ if (!jsonEditor.isEmpty() &&
+ (!QFileInfo(jsonEditor).exists() || !QFileInfo(jsonEditor).isExecutable()))
+ {
+ QString found = QStandardPaths::findExecutable(jsonEditor);
+ if (!found.isEmpty())
+ {
+ jsonEditor = found;
+ }
+ }
+ s->set("JsonEditor", jsonEditor);
+}
+
+void ExternalToolsPage::on_jprofilerPathBtn_clicked()
+{
+ QString raw_dir = ui->jprofilerPathEdit->text();
+ QString error;
+ do
+ {
+ raw_dir = QFileDialog::getExistingDirectory(this, tr("JProfiler Folder"), raw_dir);
+ if (raw_dir.isEmpty())
+ {
+ break;
+ }
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ if (!MMC->profilers()["jprofiler"]->check(cooked_dir, &error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error));
+ continue;
+ }
+ else
+ {
+ ui->jprofilerPathEdit->setText(cooked_dir);
+ break;
+ }
+ } while (1);
+}
+void ExternalToolsPage::on_jprofilerCheckBtn_clicked()
+{
+ QString error;
+ if (!MMC->profilers()["jprofiler"]->check(ui->jprofilerPathEdit->text(), &error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error));
+ }
+ else
+ {
+ QMessageBox::information(this, tr("OK"), tr("JProfiler setup seems to be OK"));
+ }
+}
+
+void ExternalToolsPage::on_jvisualvmPathBtn_clicked()
+{
+ QString raw_dir = ui->jvisualvmPathEdit->text();
+ QString error;
+ do
+ {
+ raw_dir = QFileDialog::getOpenFileName(this, tr("JVisualVM Executable"), raw_dir);
+ if (raw_dir.isEmpty())
+ {
+ break;
+ }
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ if (!MMC->profilers()["jvisualvm"]->check(cooked_dir, &error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error));
+ continue;
+ }
+ else
+ {
+ ui->jvisualvmPathEdit->setText(cooked_dir);
+ break;
+ }
+ } while (1);
+}
+void ExternalToolsPage::on_jvisualvmCheckBtn_clicked()
+{
+ QString error;
+ if (!MMC->profilers()["jvisualvm"]->check(ui->jvisualvmPathEdit->text(), &error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error));
+ }
+ else
+ {
+ QMessageBox::information(this, tr("OK"), tr("JVisualVM setup seems to be OK"));
+ }
+}
+
+void ExternalToolsPage::on_mceditPathBtn_clicked()
+{
+ QString raw_dir = ui->mceditPathEdit->text();
+ QString error;
+ do
+ {
+#ifdef Q_OS_OSX
+ raw_dir = QFileDialog::getOpenFileName(this, tr("MCEdit Application"), raw_dir);
+#else
+ raw_dir = QFileDialog::getExistingDirectory(this, tr("MCEdit Folder"), raw_dir);
+#endif
+ if (raw_dir.isEmpty())
+ {
+ break;
+ }
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ if (!MMC->mcedit()->check(cooked_dir, error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error));
+ continue;
+ }
+ else
+ {
+ ui->mceditPathEdit->setText(cooked_dir);
+ break;
+ }
+ } while (1);
+}
+void ExternalToolsPage::on_mceditCheckBtn_clicked()
+{
+ QString error;
+ if (!MMC->mcedit()->check(ui->mceditPathEdit->text(), error))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error));
+ }
+ else
+ {
+ QMessageBox::information(this, tr("OK"), tr("MCEdit setup seems to be OK"));
+ }
+}
+
+void ExternalToolsPage::on_jsonEditorBrowseBtn_clicked()
+{
+ QString raw_file = QFileDialog::getOpenFileName(
+ this, tr("JSON Editor"),
+ ui->jsonEditorTextBox->text().isEmpty()
+#if defined(Q_OS_LINUX)
+ ? QString("/usr/bin")
+#else
+ ? QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).first()
+#endif
+ : ui->jsonEditorTextBox->text());
+
+ if (raw_file.isEmpty())
+ {
+ return;
+ }
+ QString cooked_file = FS::NormalizePath(raw_file);
+
+ // it has to exist and be an executable
+ if (QFileInfo(cooked_file).exists() && QFileInfo(cooked_file).isExecutable())
+ {
+ ui->jsonEditorTextBox->setText(cooked_file);
+ }
+ else
+ {
+ QMessageBox::warning(this, tr("Invalid"),
+ tr("The file chosen does not seem to be an executable"));
+ }
+}
+
+bool ExternalToolsPage::apply()
+{
+ applySettings();
+ return true;
+}
diff --git a/launcher/pages/global/ExternalToolsPage.h b/launcher/pages/global/ExternalToolsPage.h
new file mode 100644
index 00000000..0fc8ebe1
--- /dev/null
+++ b/launcher/pages/global/ExternalToolsPage.h
@@ -0,0 +1,74 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+namespace Ui {
+class ExternalToolsPage;
+}
+
+class ExternalToolsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ExternalToolsPage(QWidget *parent = 0);
+ ~ExternalToolsPage();
+
+ QString displayName() const override
+ {
+ return tr("External Tools");
+ }
+ QIcon icon() const override
+ {
+ auto icon = MMC->getThemedIcon("externaltools");
+ if(icon.isNull())
+ {
+ icon = MMC->getThemedIcon("loadermods");
+ }
+ return icon;
+ }
+ QString id() const override
+ {
+ return "external-tools";
+ }
+ QString helpPage() const override
+ {
+ return "Tools";
+ }
+ virtual bool apply() override;
+
+private:
+ void loadSettings();
+ void applySettings();
+
+private:
+ Ui::ExternalToolsPage *ui;
+
+private
+slots:
+ void on_jprofilerPathBtn_clicked();
+ void on_jprofilerCheckBtn_clicked();
+ void on_jvisualvmPathBtn_clicked();
+ void on_jvisualvmCheckBtn_clicked();
+ void on_mceditPathBtn_clicked();
+ void on_mceditCheckBtn_clicked();
+ void on_jsonEditorBrowseBtn_clicked();
+};
diff --git a/launcher/pages/global/ExternalToolsPage.ui b/launcher/pages/global/ExternalToolsPage.ui
new file mode 100644
index 00000000..e79e9388
--- /dev/null
+++ b/launcher/pages/global/ExternalToolsPage.ui
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExternalToolsPage</class>
+ <widget class="QWidget" name="ExternalToolsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>673</width>
+ <height>751</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string notr="true">JProfiler</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QLineEdit" name="jprofilerPathEdit"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="jprofilerPathBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="jprofilerCheckBtn">
+ <property name="text">
+ <string>Check</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="jprofilerLink">
+ <property name="text">
+ <string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://www.ej-technologies.com/products/jprofiler/overview.html&quot;&gt;https://www.ej-technologies.com/products/jprofiler/overview.html&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string notr="true">JVisualVM</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <widget class="QLineEdit" name="jvisualvmPathEdit"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="jvisualvmPathBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="jvisualvmCheckBtn">
+ <property name="text">
+ <string>Check</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="jvisualvmLink">
+ <property name="text">
+ <string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://visualvm.github.io/&quot;&gt;https://visualvm.github.io/&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string notr="true">MCEdit</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QLineEdit" name="mceditPathEdit"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="mceditPathBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="mceditCheckBtn">
+ <property name="text">
+ <string>Check</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="mceditLink">
+ <property name="text">
+ <string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://www.mcedit.net/&quot;&gt;https://www.mcedit.net/&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="editorsBox">
+ <property name="title">
+ <string>External Editors (leave empty for system default)</string>
+ </property>
+ <layout class="QGridLayout" name="foldersBoxLayout_2">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="jsonEditorTextBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelJsonEditor">
+ <property name="text">
+ <string>Text Editor:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QToolButton" name="jsonEditorBrowseBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>216</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/global/JavaPage.cpp b/launcher/pages/global/JavaPage.cpp
new file mode 100644
index 00000000..cde0e035
--- /dev/null
+++ b/launcher/pages/global/JavaPage.cpp
@@ -0,0 +1,153 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "JavaPage.h"
+#include "JavaCommon.h"
+#include "ui_JavaPage.h"
+
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QDir>
+#include <QTabBar>
+
+#include "dialogs/VersionSelectDialog.h"
+
+#include "java/JavaUtils.h"
+#include "java/JavaInstallList.h"
+
+#include "settings/SettingsObject.h"
+#include <FileSystem.h>
+#include "MultiMC.h"
+#include <sys.h>
+
+JavaPage::JavaPage(QWidget *parent) : QWidget(parent), ui(new Ui::JavaPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+
+ auto sysMiB = Sys::getSystemRam() / Sys::mebibyte;
+ ui->maxMemSpinBox->setMaximum(sysMiB);
+ loadSettings();
+}
+
+JavaPage::~JavaPage()
+{
+ delete ui;
+}
+
+bool JavaPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void JavaPage::applySettings()
+{
+ auto s = MMC->settings();
+
+ // Memory
+ int min = ui->minMemSpinBox->value();
+ int max = ui->maxMemSpinBox->value();
+ if(min < max)
+ {
+ s->set("MinMemAlloc", min);
+ s->set("MaxMemAlloc", max);
+ }
+ else
+ {
+ s->set("MinMemAlloc", max);
+ s->set("MaxMemAlloc", min);
+ }
+ s->set("PermGen", ui->permGenSpinBox->value());
+
+ // Java Settings
+ s->set("JavaPath", ui->javaPathTextBox->text());
+ s->set("JvmArgs", ui->jvmArgsTextBox->text());
+ JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget());
+}
+void JavaPage::loadSettings()
+{
+ auto s = MMC->settings();
+ // Memory
+ int min = s->get("MinMemAlloc").toInt();
+ int max = s->get("MaxMemAlloc").toInt();
+ if(min < max)
+ {
+ ui->minMemSpinBox->setValue(min);
+ ui->maxMemSpinBox->setValue(max);
+ }
+ else
+ {
+ ui->minMemSpinBox->setValue(max);
+ ui->maxMemSpinBox->setValue(min);
+ }
+ ui->permGenSpinBox->setValue(s->get("PermGen").toInt());
+
+ // Java Settings
+ ui->javaPathTextBox->setText(s->get("JavaPath").toString());
+ ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString());
+}
+
+void JavaPage::on_javaDetectBtn_clicked()
+{
+ JavaInstallPtr java;
+
+ VersionSelectDialog vselect(MMC->javalist().get(), tr("Select a Java version"), this, true);
+ vselect.setResizeOn(2);
+ vselect.exec();
+
+ if (vselect.result() == QDialog::Accepted && vselect.selectedVersion())
+ {
+ java = std::dynamic_pointer_cast<JavaInstall>(vselect.selectedVersion());
+ ui->javaPathTextBox->setText(java->path);
+ }
+}
+
+void JavaPage::on_javaBrowseBtn_clicked()
+{
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"));
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if(raw_path.isEmpty())
+ {
+ return;
+ }
+
+ QString cooked_path = FS::NormalizePath(raw_path);
+ QFileInfo javaInfo(cooked_path);;
+ if(!javaInfo.exists() || !javaInfo.isExecutable())
+ {
+ return;
+ }
+ ui->javaPathTextBox->setText(cooked_path);
+}
+
+void JavaPage::on_javaTestBtn_clicked()
+{
+ if(checker)
+ {
+ return;
+ }
+ checker.reset(new JavaCommon::TestCheck(
+ this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(),
+ ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value()));
+ connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished()));
+ checker->run();
+}
+
+void JavaPage::checkerFinished()
+{
+ checker.reset();
+}
diff --git a/launcher/pages/global/JavaPage.h b/launcher/pages/global/JavaPage.h
new file mode 100644
index 00000000..832f460b
--- /dev/null
+++ b/launcher/pages/global/JavaPage.h
@@ -0,0 +1,72 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <QDialog>
+#include "pages/BasePage.h"
+#include "JavaCommon.h"
+#include <MultiMC.h>
+#include <QObjectPtr.h>
+
+class SettingsObject;
+
+namespace Ui
+{
+class JavaPage;
+}
+
+class JavaPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit JavaPage(QWidget *parent = 0);
+ ~JavaPage();
+
+ QString displayName() const override
+ {
+ return tr("Java");
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("java");
+ }
+ QString id() const override
+ {
+ return "java-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Java-settings";
+ }
+ bool apply() override;
+
+private:
+ void applySettings();
+ void loadSettings();
+
+private
+slots:
+ void on_javaDetectBtn_clicked();
+ void on_javaTestBtn_clicked();
+ void on_javaBrowseBtn_clicked();
+ void checkerFinished();
+
+private:
+ Ui::JavaPage *ui;
+ unique_qobject_ptr<JavaCommon::TestCheck> checker;
+};
diff --git a/launcher/pages/global/JavaPage.ui b/launcher/pages/global/JavaPage.ui
new file mode 100644
index 00000000..b67e9994
--- /dev/null
+++ b/launcher/pages/global/JavaPage.ui
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>JavaPage</class>
+ <widget class="QWidget" name="JavaPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>545</width>
+ <height>580</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="memoryGroupBox">
+ <property name="title">
+ <string>Memory</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="maxMemSpinBox">
+ <property name="toolTip">
+ <string>The maximum amount of memory Minecraft is allowed to use.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>128</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>1024</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelMinMem">
+ <property name="text">
+ <string>Minimum memory allocation:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelMaxMem">
+ <property name="text">
+ <string>Maximum memory allocation:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="minMemSpinBox">
+ <property name="toolTip">
+ <string>The amount of memory Minecraft is started with.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>128</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>256</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelPermGen">
+ <property name="text">
+ <string notr="true">PermGen:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="permGenSpinBox">
+ <property name="toolTip">
+ <string>The amount of memory available to store loaded Java classes.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>64</number>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ <property name="singleStep">
+ <number>8</number>
+ </property>
+ <property name="value">
+ <number>64</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="javaSettingsGroupBox">
+ <property name="title">
+ <string>Java Runtime</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelJavaPath">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Java path:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" colspan="2">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLineEdit" name="javaPathTextBox"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="javaBrowseBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>28</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="1" colspan="2">
+ <widget class="QLineEdit" name="jvmArgsTextBox"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelJVMArgs">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>JVM arguments:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QPushButton" name="javaDetectBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Auto-detect...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="2">
+ <widget class="QPushButton" name="javaTestBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Test</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>minMemSpinBox</tabstop>
+ <tabstop>maxMemSpinBox</tabstop>
+ <tabstop>permGenSpinBox</tabstop>
+ <tabstop>javaBrowseBtn</tabstop>
+ <tabstop>javaPathTextBox</tabstop>
+ <tabstop>jvmArgsTextBox</tabstop>
+ <tabstop>javaDetectBtn</tabstop>
+ <tabstop>javaTestBtn</tabstop>
+ <tabstop>tabWidget</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/global/LanguagePage.cpp b/launcher/pages/global/LanguagePage.cpp
new file mode 100644
index 00000000..ae3168cc
--- /dev/null
+++ b/launcher/pages/global/LanguagePage.cpp
@@ -0,0 +1,51 @@
+#include "LanguagePage.h"
+
+#include "widgets/LanguageSelectionWidget.h"
+#include <QVBoxLayout>
+
+LanguagePage::LanguagePage(QWidget* parent) :
+ QWidget(parent)
+{
+ setObjectName(QStringLiteral("languagePage"));
+ auto layout = new QVBoxLayout(this);
+ mainWidget = new LanguageSelectionWidget(this);
+ layout->setContentsMargins(0,0,0,0);
+ layout->addWidget(mainWidget);
+ retranslate();
+}
+
+LanguagePage::~LanguagePage()
+{
+}
+
+bool LanguagePage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void LanguagePage::applySettings()
+{
+ auto settings = MMC->settings();
+ QString key = mainWidget->getSelectedLanguageKey();
+ settings->set("Language", key);
+}
+
+void LanguagePage::loadSettings()
+{
+ // NIL
+}
+
+void LanguagePage::retranslate()
+{
+ mainWidget->retranslate();
+}
+
+void LanguagePage::changeEvent(QEvent* event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslate();
+ }
+ QWidget::changeEvent(event);
+}
diff --git a/launcher/pages/global/LanguagePage.h b/launcher/pages/global/LanguagePage.h
new file mode 100644
index 00000000..ca8eecc6
--- /dev/null
+++ b/launcher/pages/global/LanguagePage.h
@@ -0,0 +1,60 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include <QWidget>
+
+class LanguageSelectionWidget;
+
+class LanguagePage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit LanguagePage(QWidget *parent = 0);
+ virtual ~LanguagePage();
+
+ QString displayName() const override
+ {
+ return tr("Language");
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("language");
+ }
+ QString id() const override
+ {
+ return "language-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Language-settings";
+ }
+ bool apply() override;
+
+ void changeEvent(QEvent * ) override;
+
+private:
+ void applySettings();
+ void loadSettings();
+ void retranslate();
+
+private:
+ LanguageSelectionWidget *mainWidget;
+};
diff --git a/launcher/pages/global/MinecraftPage.cpp b/launcher/pages/global/MinecraftPage.cpp
new file mode 100644
index 00000000..6c9bd307
--- /dev/null
+++ b/launcher/pages/global/MinecraftPage.cpp
@@ -0,0 +1,90 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MinecraftPage.h"
+#include "ui_MinecraftPage.h"
+
+#include <QMessageBox>
+#include <QDir>
+#include <QTabBar>
+
+#include "settings/SettingsObject.h"
+#include "MultiMC.h"
+
+MinecraftPage::MinecraftPage(QWidget *parent) : QWidget(parent), ui(new Ui::MinecraftPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ loadSettings();
+ updateCheckboxStuff();
+}
+
+MinecraftPage::~MinecraftPage()
+{
+ delete ui;
+}
+
+bool MinecraftPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void MinecraftPage::updateCheckboxStuff()
+{
+ ui->windowWidthSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked());
+ ui->windowHeightSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked());
+}
+
+void MinecraftPage::on_maximizedCheckBox_clicked(bool checked)
+{
+ Q_UNUSED(checked);
+ updateCheckboxStuff();
+}
+
+
+void MinecraftPage::applySettings()
+{
+ auto s = MMC->settings();
+
+ // Window Size
+ s->set("LaunchMaximized", ui->maximizedCheckBox->isChecked());
+ s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value());
+ s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value());
+
+ // Native library workarounds
+ s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked());
+ s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked());
+
+ // Game time
+ s->set("ShowGameTime", ui->showGameTime->isChecked());
+ s->set("RecordGameTime", ui->recordGameTime->isChecked());
+}
+
+void MinecraftPage::loadSettings()
+{
+ auto s = MMC->settings();
+
+ // Window Size
+ ui->maximizedCheckBox->setChecked(s->get("LaunchMaximized").toBool());
+ ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt());
+ ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt());
+
+ ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool());
+ ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool());
+
+ ui->showGameTime->setChecked(s->get("ShowGameTime").toBool());
+ ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
+}
diff --git a/launcher/pages/global/MinecraftPage.h b/launcher/pages/global/MinecraftPage.h
new file mode 100644
index 00000000..5e781aed
--- /dev/null
+++ b/launcher/pages/global/MinecraftPage.h
@@ -0,0 +1,70 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <QDialog>
+
+#include "java/JavaChecker.h"
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+class SettingsObject;
+
+namespace Ui
+{
+class MinecraftPage;
+}
+
+class MinecraftPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit MinecraftPage(QWidget *parent = 0);
+ ~MinecraftPage();
+
+ QString displayName() const override
+ {
+ return tr("Minecraft");
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("minecraft");
+ }
+ QString id() const override
+ {
+ return "minecraft-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Minecraft-settings";
+ }
+ bool apply() override;
+
+private:
+ void updateCheckboxStuff();
+ void applySettings();
+ void loadSettings();
+
+private
+slots:
+ void on_maximizedCheckBox_clicked(bool checked);
+
+private:
+ Ui::MinecraftPage *ui;
+
+};
diff --git a/launcher/pages/global/MinecraftPage.ui b/launcher/pages/global/MinecraftPage.ui
new file mode 100644
index 00000000..2abd4bd4
--- /dev/null
+++ b/launcher/pages/global/MinecraftPage.ui
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MinecraftPage</class>
+ <widget class="QWidget" name="MinecraftPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>936</width>
+ <height>1134</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="mainLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="minecraftTab">
+ <attribute name="title">
+ <string notr="true">Minecraft</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QGroupBox" name="windowSizeGroupBox">
+ <property name="title">
+ <string>Window Size</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="maximizedCheckBox">
+ <property name="text">
+ <string>Start Minecraft maximized?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayoutWindowSize">
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelWindowHeight">
+ <property name="text">
+ <string>Window hei&amp;ght:</string>
+ </property>
+ <property name="buddy">
+ <cstring>windowHeightSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelWindowWidth">
+ <property name="text">
+ <string>W&amp;indow width:</string>
+ </property>
+ <property name="buddy">
+ <cstring>windowWidthSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="windowWidthSpinBox">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>1</number>
+ </property>
+ <property name="value">
+ <number>854</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="windowHeightSpinBox">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="value">
+ <number>480</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="nativeLibWorkaroundGroupBox">
+ <property name="title">
+ <string>Native library workarounds</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QCheckBox" name="useNativeGLFWCheck">
+ <property name="text">
+ <string>Use system installation of GLFW</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useNativeOpenALCheck">
+ <property name="text">
+ <string>Use system installation of OpenAL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="gameTimeGroupBox">
+ <property name="title">
+ <string>Game time</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QCheckBox" name="showGameTime">
+ <property name="text">
+ <string>Show time spent playing instances</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="recordGameTime">
+ <property name="text">
+ <string>Record time spent playing instances</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacerMinecraft">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>maximizedCheckBox</tabstop>
+ <tabstop>windowWidthSpinBox</tabstop>
+ <tabstop>windowHeightSpinBox</tabstop>
+ <tabstop>useNativeGLFWCheck</tabstop>
+ <tabstop>useNativeOpenALCheck</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/global/MultiMCPage.cpp b/launcher/pages/global/MultiMCPage.cpp
new file mode 100644
index 00000000..d383e6ed
--- /dev/null
+++ b/launcher/pages/global/MultiMCPage.cpp
@@ -0,0 +1,467 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MultiMCPage.h"
+#include "ui_MultiMCPage.h"
+
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QDir>
+#include <QTextCharFormat>
+
+#include "updater/UpdateChecker.h"
+
+#include "settings/SettingsObject.h"
+#include <FileSystem.h>
+#include "MultiMC.h"
+#include "BuildConfig.h"
+#include "themes/ITheme.h"
+
+#include <QApplication>
+#include <QProcess>
+
+// FIXME: possibly move elsewhere
+enum InstSortMode
+{
+ // Sort alphabetically by name.
+ Sort_Name,
+ // Sort by which instance was launched most recently.
+ Sort_LastLaunch
+};
+
+MultiMCPage::MultiMCPage(QWidget *parent) : QWidget(parent), ui(new Ui::MultiMCPage)
+{
+ ui->setupUi(this);
+ auto origForeground = ui->fontPreview->palette().color(ui->fontPreview->foregroundRole());
+ auto origBackground = ui->fontPreview->palette().color(ui->fontPreview->backgroundRole());
+ m_colors.reset(new LogColorCache(origForeground, origBackground));
+
+ ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name);
+ ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch);
+
+ defaultFormat = new QTextCharFormat(ui->fontPreview->currentCharFormat());
+
+ m_languageModel = MMC->translations();
+ loadSettings();
+
+ if(BuildConfig.UPDATER_ENABLED)
+ {
+ QObject::connect(MMC->updateChecker().get(), &UpdateChecker::channelListLoaded, this,
+ &MultiMCPage::refreshUpdateChannelList);
+
+ if (MMC->updateChecker()->hasChannels())
+ {
+ refreshUpdateChannelList();
+ }
+ else
+ {
+ MMC->updateChecker()->updateChanList(false);
+ }
+ }
+ else
+ {
+ ui->updateSettingsBox->setHidden(true);
+ }
+ // Analytics
+ if(BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->analyticsTab));
+ }
+ connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview()));
+ connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview()));
+
+ //move mac data button
+ QFile file(QDir::current().absolutePath() + "/dontmovemacdata");
+ if (!file.exists())
+ {
+ ui->migrateDataFolderMacBtn->setVisible(false);
+ }
+}
+
+MultiMCPage::~MultiMCPage()
+{
+ delete ui;
+ delete defaultFormat;
+}
+
+bool MultiMCPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void MultiMCPage::on_instDirBrowseBtn_clicked()
+{
+ QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Instance Folder"), ui->instDirTextBox->text());
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if (!raw_dir.isEmpty() && QDir(raw_dir).exists())
+ {
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ if (FS::checkProblemticPathJava(QDir(cooked_dir)))
+ {
+ QMessageBox warning;
+ warning.setText(tr("You're trying to specify an instance folder which\'s path "
+ "contains at least one \'!\'. "
+ "Java is known to cause problems if that is the case, your "
+ "instances (probably) won't start!"));
+ warning.setInformativeText(
+ tr("Do you really want to use this path? "
+ "Selecting \"No\" will close this and not alter your instance path."));
+ warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+ int result = warning.exec();
+ if (result == QMessageBox::Yes)
+ {
+ ui->instDirTextBox->setText(cooked_dir);
+ }
+ }
+ else
+ {
+ ui->instDirTextBox->setText(cooked_dir);
+ }
+ }
+}
+
+void MultiMCPage::on_iconsDirBrowseBtn_clicked()
+{
+ QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Icons Folder"), ui->iconsDirTextBox->text());
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if (!raw_dir.isEmpty() && QDir(raw_dir).exists())
+ {
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ ui->iconsDirTextBox->setText(cooked_dir);
+ }
+}
+void MultiMCPage::on_modsDirBrowseBtn_clicked()
+{
+ QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text());
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if (!raw_dir.isEmpty() && QDir(raw_dir).exists())
+ {
+ QString cooked_dir = FS::NormalizePath(raw_dir);
+ ui->modsDirTextBox->setText(cooked_dir);
+ }
+}
+void MultiMCPage::on_migrateDataFolderMacBtn_clicked()
+{
+ QFile file(QDir::current().absolutePath() + "/dontmovemacdata");
+ file.remove();
+ QProcess::startDetached(qApp->arguments()[0]);
+ qApp->quit();
+}
+
+void MultiMCPage::refreshUpdateChannelList()
+{
+ // Stop listening for selection changes. It's going to change a lot while we update it and
+ // we don't need to update the
+ // description label constantly.
+ QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(updateChannelSelectionChanged(int)));
+
+ QList<UpdateChecker::ChannelListEntry> channelList = MMC->updateChecker()->getChannelList();
+ ui->updateChannelComboBox->clear();
+ int selection = -1;
+ for (int i = 0; i < channelList.count(); i++)
+ {
+ UpdateChecker::ChannelListEntry entry = channelList.at(i);
+
+ // When it comes to selection, we'll rely on the indexes of a channel entry being the
+ // same in the
+ // combo box as it is in the update checker's channel list.
+ // This probably isn't very safe, but the channel list doesn't change often enough (or
+ // at all) for
+ // this to be a big deal. Hope it doesn't break...
+ ui->updateChannelComboBox->addItem(entry.name);
+
+ // If the update channel we just added was the selected one, set the current index in
+ // the combo box to it.
+ if (entry.id == m_currentUpdateChannel)
+ {
+ qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel;
+ selection = i;
+ }
+ }
+
+ ui->updateChannelComboBox->setCurrentIndex(selection);
+
+ // Start listening for selection changes again and update the description label.
+ QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
+ SLOT(updateChannelSelectionChanged(int)));
+ refreshUpdateChannelDesc();
+
+ // Now that we've updated the channel list, we can enable the combo box.
+ // It starts off disabled so that if the channel list hasn't been loaded, it will be
+ // disabled.
+ ui->updateChannelComboBox->setEnabled(true);
+}
+
+void MultiMCPage::updateChannelSelectionChanged(int index)
+{
+ refreshUpdateChannelDesc();
+}
+
+void MultiMCPage::refreshUpdateChannelDesc()
+{
+ // Get the channel list.
+ QList<UpdateChecker::ChannelListEntry> channelList = MMC->updateChecker()->getChannelList();
+ int selectedIndex = ui->updateChannelComboBox->currentIndex();
+ if (selectedIndex < 0)
+ {
+ return;
+ }
+ if (selectedIndex < channelList.count())
+ {
+ // Find the channel list entry with the given index.
+ UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex);
+
+ // Set the description text.
+ ui->updateChannelDescLabel->setText(selected.description);
+
+ // Set the currently selected channel ID.
+ m_currentUpdateChannel = selected.id;
+ }
+}
+
+void MultiMCPage::applySettings()
+{
+ auto s = MMC->settings();
+
+ if (ui->resetNotificationsBtn->isChecked())
+ {
+ s->set("ShownNotifications", QString());
+ }
+
+ // Updates
+ s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked());
+ s->set("UpdateChannel", m_currentUpdateChannel);
+ auto original = s->get("IconTheme").toString();
+ //FIXME: make generic
+ switch (ui->themeComboBox->currentIndex())
+ {
+ case 1:
+ s->set("IconTheme", "pe_dark");
+ break;
+ case 2:
+ s->set("IconTheme", "pe_light");
+ break;
+ case 3:
+ s->set("IconTheme", "pe_blue");
+ break;
+ case 4:
+ s->set("IconTheme", "pe_colored");
+ break;
+ case 5:
+ s->set("IconTheme", "OSX");
+ break;
+ case 6:
+ s->set("IconTheme", "iOS");
+ break;
+ case 7:
+ s->set("IconTheme", "flat");
+ break;
+ case 8:
+ s->set("IconTheme", "custom");
+ break;
+ case 0:
+ default:
+ s->set("IconTheme", "multimc");
+ break;
+ }
+
+ if(original != s->get("IconTheme"))
+ {
+ MMC->setIconTheme(s->get("IconTheme").toString());
+ }
+
+ auto originalAppTheme = s->get("ApplicationTheme").toString();
+ auto newAppTheme = ui->themeComboBoxColors->currentData().toString();
+ if(originalAppTheme != newAppTheme)
+ {
+ s->set("ApplicationTheme", newAppTheme);
+ MMC->setApplicationTheme(newAppTheme, false);
+ }
+
+ // Console settings
+ s->set("ShowConsole", ui->showConsoleCheck->isChecked());
+ s->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked());
+ s->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked());
+ QString consoleFontFamily = ui->consoleFont->currentFont().family();
+ s->set("ConsoleFont", consoleFontFamily);
+ s->set("ConsoleFontSize", ui->fontSizeBox->value());
+ s->set("ConsoleMaxLines", ui->lineLimitSpinBox->value());
+ s->set("ConsoleOverflowStop", ui->checkStopLogging->checkState() != Qt::Unchecked);
+
+ // Folders
+ // TODO: Offer to move instances to new instance folder.
+ s->set("InstanceDir", ui->instDirTextBox->text());
+ s->set("CentralModsDir", ui->modsDirTextBox->text());
+ s->set("IconsDir", ui->iconsDirTextBox->text());
+
+ auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
+ switch (sortMode)
+ {
+ case Sort_LastLaunch:
+ s->set("InstSortMode", "LastLaunch");
+ break;
+ case Sort_Name:
+ default:
+ s->set("InstSortMode", "Name");
+ break;
+ }
+
+ // Analytics
+ if(!BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ s->set("Analytics", ui->analyticsCheck->isChecked());
+ }
+}
+void MultiMCPage::loadSettings()
+{
+ auto s = MMC->settings();
+ // Updates
+ ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool());
+ m_currentUpdateChannel = s->get("UpdateChannel").toString();
+ //FIXME: make generic
+ auto theme = s->get("IconTheme").toString();
+ if (theme == "pe_dark")
+ {
+ ui->themeComboBox->setCurrentIndex(1);
+ }
+ else if (theme == "pe_light")
+ {
+ ui->themeComboBox->setCurrentIndex(2);
+ }
+ else if (theme == "pe_blue")
+ {
+ ui->themeComboBox->setCurrentIndex(3);
+ }
+ else if (theme == "pe_colored")
+ {
+ ui->themeComboBox->setCurrentIndex(4);
+ }
+ else if (theme == "OSX")
+ {
+ ui->themeComboBox->setCurrentIndex(5);
+ }
+ else if (theme == "iOS")
+ {
+ ui->themeComboBox->setCurrentIndex(6);
+ }
+ else if (theme == "flat")
+ {
+ ui->themeComboBox->setCurrentIndex(7);
+ }
+ else if (theme == "custom")
+ {
+ ui->themeComboBox->setCurrentIndex(8);
+ }
+ else
+ {
+ ui->themeComboBox->setCurrentIndex(0);
+ }
+
+ {
+ auto currentTheme = s->get("ApplicationTheme").toString();
+ auto themes = MMC->getValidApplicationThemes();
+ int idx = 0;
+ for(auto &theme: themes)
+ {
+ ui->themeComboBoxColors->addItem(theme->name(), theme->id());
+ if(currentTheme == theme->id())
+ {
+ ui->themeComboBoxColors->setCurrentIndex(idx);
+ }
+ idx++;
+ }
+ }
+
+ // Console settings
+ ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool());
+ ui->autoCloseConsoleCheck->setChecked(s->get("AutoCloseConsole").toBool());
+ ui->showConsoleErrorCheck->setChecked(s->get("ShowConsoleOnError").toBool());
+ QString fontFamily = MMC->settings()->get("ConsoleFont").toString();
+ QFont consoleFont(fontFamily);
+ ui->consoleFont->setCurrentFont(consoleFont);
+
+ bool conversionOk = true;
+ int fontSize = MMC->settings()->get("ConsoleFontSize").toInt(&conversionOk);
+ if(!conversionOk)
+ {
+ fontSize = 11;
+ }
+ ui->fontSizeBox->setValue(fontSize);
+ refreshFontPreview();
+ ui->lineLimitSpinBox->setValue(s->get("ConsoleMaxLines").toInt());
+ ui->checkStopLogging->setChecked(s->get("ConsoleOverflowStop").toBool());
+
+ // Folders
+ ui->instDirTextBox->setText(s->get("InstanceDir").toString());
+ ui->modsDirTextBox->setText(s->get("CentralModsDir").toString());
+ ui->iconsDirTextBox->setText(s->get("IconsDir").toString());
+
+ QString sortMode = s->get("InstSortMode").toString();
+
+ if (sortMode == "LastLaunch")
+ {
+ ui->sortLastLaunchedBtn->setChecked(true);
+ }
+ else
+ {
+ ui->sortByNameBtn->setChecked(true);
+ }
+
+ // Analytics
+ if(!BuildConfig.ANALYTICS_ID.isEmpty())
+ {
+ ui->analyticsCheck->setChecked(s->get("Analytics").toBool());
+ }
+}
+
+void MultiMCPage::refreshFontPreview()
+{
+ int fontSize = ui->fontSizeBox->value();
+ QString fontFamily = ui->consoleFont->currentFont().family();
+ ui->fontPreview->clear();
+ defaultFormat->setFont(QFont(fontFamily, fontSize));
+ {
+ QTextCharFormat format(*defaultFormat);
+ format.setForeground(m_colors->getFront(MessageLevel::Error));
+ // append a paragraph/line
+ auto workCursor = ui->fontPreview->textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(tr("[Something/ERROR] A spooky error!"), format);
+ workCursor.insertBlock();
+ }
+ {
+ QTextCharFormat format(*defaultFormat);
+ format.setForeground(m_colors->getFront(MessageLevel::Message));
+ // append a paragraph/line
+ auto workCursor = ui->fontPreview->textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(tr("[Test/INFO] A harmless message..."), format);
+ workCursor.insertBlock();
+ }
+ {
+ QTextCharFormat format(*defaultFormat);
+ format.setForeground(m_colors->getFront(MessageLevel::Warning));
+ // append a paragraph/line
+ auto workCursor = ui->fontPreview->textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(tr("[Something/WARN] A not so spooky warning."), format);
+ workCursor.insertBlock();
+ }
+}
diff --git a/launcher/pages/global/MultiMCPage.h b/launcher/pages/global/MultiMCPage.h
new file mode 100644
index 00000000..fae75bf2
--- /dev/null
+++ b/launcher/pages/global/MultiMCPage.h
@@ -0,0 +1,103 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <QDialog>
+
+#include "java/JavaChecker.h"
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "ColorCache.h"
+#include <translations/TranslationsModel.h>
+
+class QTextCharFormat;
+class SettingsObject;
+
+namespace Ui
+{
+class MultiMCPage;
+}
+
+class MultiMCPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit MultiMCPage(QWidget *parent = 0);
+ ~MultiMCPage();
+
+ QString displayName() const override
+ {
+ return "MultiMC";
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("multimc");
+ }
+ QString id() const override
+ {
+ return "multimc-settings";
+ }
+ QString helpPage() const override
+ {
+ return "MultiMC-settings";
+ }
+ bool apply() override;
+
+private:
+ void applySettings();
+ void loadSettings();
+
+private
+slots:
+ void on_instDirBrowseBtn_clicked();
+ void on_modsDirBrowseBtn_clicked();
+ void on_iconsDirBrowseBtn_clicked();
+ void on_migrateDataFolderMacBtn_clicked();
+
+ /*!
+ * Updates the list of update channels in the combo box.
+ */
+ void refreshUpdateChannelList();
+
+ /*!
+ * Updates the channel description label.
+ */
+ void refreshUpdateChannelDesc();
+
+ /*!
+ * Updates the font preview
+ */
+ void refreshFontPreview();
+
+ void updateChannelSelectionChanged(int index);
+
+private:
+ Ui::MultiMCPage *ui;
+
+ /*!
+ * Stores the currently selected update channel.
+ */
+ QString m_currentUpdateChannel;
+
+ // default format for the font preview...
+ QTextCharFormat *defaultFormat;
+
+ std::unique_ptr<LogColorCache> m_colors;
+
+ std::shared_ptr<TranslationsModel> m_languageModel;
+};
diff --git a/launcher/pages/global/MultiMCPage.ui b/launcher/pages/global/MultiMCPage.ui
new file mode 100644
index 00000000..4ad20242
--- /dev/null
+++ b/launcher/pages/global/MultiMCPage.ui
@@ -0,0 +1,584 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MultiMCPage</class>
+ <widget class="QWidget" name="MultiMCPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>514</width>
+ <height>629</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="mainLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="featuresTab">
+ <attribute name="title">
+ <string>Features</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <item>
+ <widget class="QGroupBox" name="updateSettingsBox">
+ <property name="title">
+ <string>Update Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QCheckBox" name="autoUpdateCheckBox">
+ <property name="text">
+ <string>Check for updates when MultiMC starts?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="updateChannelLabel">
+ <property name="text">
+ <string>Up&amp;date Channel:</string>
+ </property>
+ <property name="buddy">
+ <cstring>updateChannelComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="updateChannelComboBox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="updateChannelDescLabel">
+ <property name="text">
+ <string>No channel selected.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="foldersBox">
+ <property name="title">
+ <string>Folders</string>
+ </property>
+ <layout class="QGridLayout" name="foldersBoxLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelInstDir">
+ <property name="text">
+ <string>I&amp;nstances:</string>
+ </property>
+ <property name="buddy">
+ <cstring>instDirTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="instDirTextBox"/>
+ </item>
+ <item row="0" column="2">
+ <widget class="QToolButton" name="instDirBrowseBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelModsDir">
+ <property name="text">
+ <string>&amp;Mods:</string>
+ </property>
+ <property name="buddy">
+ <cstring>modsDirTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="modsDirTextBox"/>
+ </item>
+ <item row="1" column="2">
+ <widget class="QToolButton" name="modsDirBrowseBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="iconsDirTextBox"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelIconsDir">
+ <property name="text">
+ <string>&amp;Icons:</string>
+ </property>
+ <property name="buddy">
+ <cstring>iconsDirTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QToolButton" name="iconsDirBrowseBtn">
+ <property name="text">
+ <string notr="true">...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="migrateDataFolderMacBtn">
+ <property name="text">
+ <string>Move MultiMC data to new location (will restart MultiMC)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="generalTab">
+ <attribute name="title">
+ <string>User Interface</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>MultiMC notifications</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QPushButton" name="resetNotificationsBtn">
+ <property name="text">
+ <string>Reset hidden notifications</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="sortingModeBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Instance view sorting mode</string>
+ </property>
+ <layout class="QHBoxLayout" name="sortingModeBoxLayout">
+ <item>
+ <widget class="QRadioButton" name="sortLastLaunchedBtn">
+ <property name="text">
+ <string>By &amp;last launched</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">sortingModeGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="sortByNameBtn">
+ <property name="text">
+ <string>By &amp;name</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">sortingModeGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="themeBox">
+ <property name="title">
+ <string>Theme</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>&amp;Icons</string>
+ </property>
+ <property name="buddy">
+ <cstring>themeComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="themeComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ <item>
+ <property name="text">
+ <string>Default</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Simple (Dark Icons)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Simple (Light Icons)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Simple (Blue Icons)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Simple (Colored Icons)</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">OSX</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">iOS</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">Flat</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Custom</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="themeComboBoxColors">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Colors</string>
+ </property>
+ <property name="buddy">
+ <cstring>themeComboBoxColors</cstring>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="generalTabSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="consoleTab">
+ <attribute name="title">
+ <string>Console</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="consoleSettingsBox">
+ <property name="title">
+ <string>Console Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QCheckBox" name="showConsoleCheck">
+ <property name="text">
+ <string>Show console while the game is running?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="autoCloseConsoleCheck">
+ <property name="text">
+ <string>Automatically close console when the game quits?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showConsoleErrorCheck">
+ <property name="text">
+ <string>Show console when the game crashes?</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string>History limit</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QCheckBox" name="checkStopLogging">
+ <property name="text">
+ <string>Stop logging when log overflows</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QSpinBox" name="lineLimitSpinBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string> lines</string>
+ </property>
+ <property name="minimum">
+ <number>10000</number>
+ </property>
+ <property name="maximum">
+ <number>1000000</number>
+ </property>
+ <property name="singleStep">
+ <number>10000</number>
+ </property>
+ <property name="value">
+ <number>100000</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="themeBox_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Console font</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="0" colspan="2">
+ <widget class="QTextEdit" name="fontPreview">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="undoRedoEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QFontComboBox" name="consoleFont">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="fontSizeBox">
+ <property name="minimum">
+ <number>5</number>
+ </property>
+ <property name="maximum">
+ <number>16</number>
+ </property>
+ <property name="value">
+ <number>11</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="analyticsTab">
+ <attribute name="title">
+ <string>Analytics</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QGroupBox" name="consoleSettingsBox_2">
+ <property name="title">
+ <string>Analytics Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="analyticsCheck">
+ <property name="text">
+ <string>Send anonymous usage statistics?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;
+&lt;body&gt;
+&lt;p&gt;MultiMC sends anonymous usage statistics on every start of the application.&lt;/p&gt;&lt;p&gt;The following data is collected:&lt;/p&gt;
+&lt;ul&gt;
+&lt;li&gt;MultiMC version.&lt;/li&gt;
+&lt;li&gt;Operating system name, version and architecture.&lt;/li&gt;
+&lt;li&gt;CPU architecture (kernel architecture on linux).&lt;/li&gt;
+&lt;li&gt;Size of system memory.&lt;/li&gt;
+&lt;li&gt;Java version, architecture and memory settings.&lt;/li&gt;
+&lt;/ul&gt;
+&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>autoUpdateCheckBox</tabstop>
+ <tabstop>updateChannelComboBox</tabstop>
+ <tabstop>instDirTextBox</tabstop>
+ <tabstop>instDirBrowseBtn</tabstop>
+ <tabstop>modsDirTextBox</tabstop>
+ <tabstop>modsDirBrowseBtn</tabstop>
+ <tabstop>iconsDirTextBox</tabstop>
+ <tabstop>iconsDirBrowseBtn</tabstop>
+ <tabstop>resetNotificationsBtn</tabstop>
+ <tabstop>sortLastLaunchedBtn</tabstop>
+ <tabstop>sortByNameBtn</tabstop>
+ <tabstop>themeComboBox</tabstop>
+ <tabstop>themeComboBoxColors</tabstop>
+ <tabstop>showConsoleCheck</tabstop>
+ <tabstop>autoCloseConsoleCheck</tabstop>
+ <tabstop>showConsoleErrorCheck</tabstop>
+ <tabstop>lineLimitSpinBox</tabstop>
+ <tabstop>checkStopLogging</tabstop>
+ <tabstop>consoleFont</tabstop>
+ <tabstop>fontSizeBox</tabstop>
+ <tabstop>fontPreview</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+ <buttongroups>
+ <buttongroup name="sortingModeGroup"/>
+ </buttongroups>
+</ui>
diff --git a/launcher/pages/global/PasteEEPage.cpp b/launcher/pages/global/PasteEEPage.cpp
new file mode 100644
index 00000000..f932dede
--- /dev/null
+++ b/launcher/pages/global/PasteEEPage.cpp
@@ -0,0 +1,81 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PasteEEPage.h"
+#include "ui_PasteEEPage.h"
+
+#include <QMessageBox>
+#include <QFileDialog>
+#include <QStandardPaths>
+#include <QTabBar>
+
+#include "settings/SettingsObject.h"
+#include "tools/BaseProfiler.h"
+#include "MultiMC.h"
+
+PasteEEPage::PasteEEPage(QWidget *parent) :
+ QWidget(parent),
+ ui(new Ui::PasteEEPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();\
+ connect(ui->customAPIkeyEdit, &QLineEdit::textEdited, this, &PasteEEPage::textEdited);
+ loadSettings();
+}
+
+PasteEEPage::~PasteEEPage()
+{
+ delete ui;
+}
+
+void PasteEEPage::loadSettings()
+{
+ auto s = MMC->settings();
+ QString keyToUse = s->get("PasteEEAPIKey").toString();
+ if(keyToUse == "multimc")
+ {
+ ui->multimcButton->setChecked(true);
+ }
+ else
+ {
+ ui->customButton->setChecked(true);
+ ui->customAPIkeyEdit->setText(keyToUse);
+ }
+}
+
+void PasteEEPage::applySettings()
+{
+ auto s = MMC->settings();
+
+ QString pasteKeyToUse;
+ if (ui->customButton->isChecked())
+ pasteKeyToUse = ui->customAPIkeyEdit->text();
+ else
+ {
+ pasteKeyToUse = "multimc";
+ }
+ s->set("PasteEEAPIKey", pasteKeyToUse);
+}
+
+bool PasteEEPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void PasteEEPage::textEdited(const QString& text)
+{
+ ui->customButton->setChecked(true);
+}
diff --git a/launcher/pages/global/PasteEEPage.h b/launcher/pages/global/PasteEEPage.h
new file mode 100644
index 00000000..001decdb
--- /dev/null
+++ b/launcher/pages/global/PasteEEPage.h
@@ -0,0 +1,62 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+namespace Ui {
+class PasteEEPage;
+}
+
+class PasteEEPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit PasteEEPage(QWidget *parent = 0);
+ ~PasteEEPage();
+
+ QString displayName() const override
+ {
+ return tr("Log Upload");
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("log");
+ }
+ QString id() const override
+ {
+ return "log-upload";
+ }
+ QString helpPage() const override
+ {
+ return "Log-Upload";
+ }
+ virtual bool apply() override;
+
+private:
+ void loadSettings();
+ void applySettings();
+
+private slots:
+ void textEdited(const QString &text);
+
+private:
+ Ui::PasteEEPage *ui;
+};
diff --git a/launcher/pages/global/PasteEEPage.ui b/launcher/pages/global/PasteEEPage.ui
new file mode 100644
index 00000000..10883781
--- /dev/null
+++ b/launcher/pages/global/PasteEEPage.ui
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PasteEEPage</class>
+ <widget class="QWidget" name="PasteEEPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>491</width>
+ <height>474</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>paste.ee API key</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <widget class="QRadioButton" name="multimcButton">
+ <property name="text">
+ <string>MultiMC key - 12MB &amp;upload limit</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">pasteButtonGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="customButton">
+ <property name="text">
+ <string>&amp;Your own key - 12MB upload limit:</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">pasteButtonGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="customAPIkeyEdit">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ <property name="placeholderText">
+ <string>Paste your API key here!</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://paste.ee&quot;&gt;paste.ee&lt;/a&gt; is used by MultiMC for log uploads. If you have a &lt;a href=&quot;https://paste.ee&quot;&gt;paste.ee&lt;/a&gt; account, you can add your API key here and have your uploaded logs paired with your account.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>216</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>multimcButton</tabstop>
+ <tabstop>customButton</tabstop>
+ <tabstop>customAPIkeyEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+ <buttongroups>
+ <buttongroup name="pasteButtonGroup"/>
+ </buttongroups>
+</ui>
diff --git a/launcher/pages/global/ProxyPage.cpp b/launcher/pages/global/ProxyPage.cpp
new file mode 100644
index 00000000..809059ff
--- /dev/null
+++ b/launcher/pages/global/ProxyPage.cpp
@@ -0,0 +1,101 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ProxyPage.h"
+#include "ui_ProxyPage.h"
+
+#include <QTabBar>
+
+#include "settings/SettingsObject.h"
+#include "MultiMC.h"
+#include "Env.h"
+
+ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ loadSettings();
+ updateCheckboxStuff();
+
+ connect(ui->proxyGroup, SIGNAL(buttonClicked(int)), SLOT(proxyChanged(int)));
+}
+
+ProxyPage::~ProxyPage()
+{
+ delete ui;
+}
+
+bool ProxyPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void ProxyPage::updateCheckboxStuff()
+{
+ ui->proxyAddrBox->setEnabled(!ui->proxyNoneBtn->isChecked() &&
+ !ui->proxyDefaultBtn->isChecked());
+ ui->proxyAuthBox->setEnabled(!ui->proxyNoneBtn->isChecked() &&
+ !ui->proxyDefaultBtn->isChecked());
+}
+
+void ProxyPage::proxyChanged(int)
+{
+ updateCheckboxStuff();
+}
+
+void ProxyPage::applySettings()
+{
+ auto s = MMC->settings();
+
+ // Proxy
+ QString proxyType = "None";
+ if (ui->proxyDefaultBtn->isChecked())
+ proxyType = "Default";
+ else if (ui->proxyNoneBtn->isChecked())
+ proxyType = "None";
+ else if (ui->proxySOCKS5Btn->isChecked())
+ proxyType = "SOCKS5";
+ else if (ui->proxyHTTPBtn->isChecked())
+ proxyType = "HTTP";
+
+ s->set("ProxyType", proxyType);
+ s->set("ProxyAddr", ui->proxyAddrEdit->text());
+ s->set("ProxyPort", ui->proxyPortEdit->value());
+ s->set("ProxyUser", ui->proxyUserEdit->text());
+ s->set("ProxyPass", ui->proxyPassEdit->text());
+
+ ENV.updateProxySettings(proxyType, ui->proxyAddrEdit->text(), ui->proxyPortEdit->value(),
+ ui->proxyUserEdit->text(), ui->proxyPassEdit->text());
+}
+void ProxyPage::loadSettings()
+{
+ auto s = MMC->settings();
+ // Proxy
+ QString proxyType = s->get("ProxyType").toString();
+ if (proxyType == "Default")
+ ui->proxyDefaultBtn->setChecked(true);
+ else if (proxyType == "None")
+ ui->proxyNoneBtn->setChecked(true);
+ else if (proxyType == "SOCKS5")
+ ui->proxySOCKS5Btn->setChecked(true);
+ else if (proxyType == "HTTP")
+ ui->proxyHTTPBtn->setChecked(true);
+
+ ui->proxyAddrEdit->setText(s->get("ProxyAddr").toString());
+ ui->proxyPortEdit->setValue(s->get("ProxyPort").value<uint16_t>());
+ ui->proxyUserEdit->setText(s->get("ProxyUser").toString());
+ ui->proxyPassEdit->setText(s->get("ProxyPass").toString());
+}
diff --git a/launcher/pages/global/ProxyPage.h b/launcher/pages/global/ProxyPage.h
new file mode 100644
index 00000000..ff94ec49
--- /dev/null
+++ b/launcher/pages/global/ProxyPage.h
@@ -0,0 +1,66 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <QDialog>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+namespace Ui
+{
+class ProxyPage;
+}
+
+class ProxyPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ProxyPage(QWidget *parent = 0);
+ ~ProxyPage();
+
+ QString displayName() const override
+ {
+ return tr("Proxy");
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("proxy");
+ }
+ QString id() const override
+ {
+ return "proxy-settings";
+ }
+ QString helpPage() const override
+ {
+ return "Proxy-settings";
+ }
+ bool apply() override;
+
+private:
+ void updateCheckboxStuff();
+ void applySettings();
+ void loadSettings();
+
+private
+slots:
+ void proxyChanged(int);
+
+private:
+ Ui::ProxyPage *ui;
+};
diff --git a/launcher/pages/global/ProxyPage.ui b/launcher/pages/global/ProxyPage.ui
new file mode 100644
index 00000000..69fcef1e
--- /dev/null
+++ b/launcher/pages/global/ProxyPage.ui
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ProxyPage</class>
+ <widget class="QWidget" name="ProxyPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>598</width>
+ <height>617</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <widget class="QWidget" name="tabWidgetPage1">
+ <attribute name="title">
+ <string notr="true"/>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="proxyPlainTextWarningLabel_2">
+ <property name="text">
+ <string>This only applies to MultiMC. Minecraft does not accept proxy settings.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="proxyTypeBox">
+ <property name="title">
+ <string>Type</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QRadioButton" name="proxyDefaultBtn">
+ <property name="toolTip">
+ <string>Uses your system's default proxy settings.</string>
+ </property>
+ <property name="text">
+ <string>&amp;Default</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">proxyGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="proxyNoneBtn">
+ <property name="text">
+ <string>&amp;None</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">proxyGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="proxySOCKS5Btn">
+ <property name="text">
+ <string>SOC&amp;KS5</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">proxyGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="proxyHTTPBtn">
+ <property name="text">
+ <string>H&amp;TTP</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">proxyGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="proxyAddrBox">
+ <property name="title">
+ <string>Address and Port</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLineEdit" name="proxyAddrEdit">
+ <property name="placeholderText">
+ <string notr="true">127.0.0.1</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="proxyPortEdit">
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buttonSymbols">
+ <enum>QAbstractSpinBox::PlusMinus</enum>
+ </property>
+ <property name="maximum">
+ <number>65535</number>
+ </property>
+ <property name="value">
+ <number>8080</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="proxyAuthBox">
+ <property name="title">
+ <string>Authentication</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_5">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="proxyUserEdit"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="proxyUsernameLabel">
+ <property name="text">
+ <string>Username:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="proxyPasswordLabel">
+ <property name="text">
+ <string>Password:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="proxyPassEdit">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QLabel" name="proxyPlainTextWarningLabel">
+ <property name="text">
+ <string>Note: Proxy username and password are stored in plain text inside MultiMC's configuration file!</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+ <buttongroups>
+ <buttongroup name="proxyGroup"/>
+ </buttongroups>
+</ui>
diff --git a/launcher/pages/instance/GameOptionsPage.cpp b/launcher/pages/instance/GameOptionsPage.cpp
new file mode 100644
index 00000000..782f2ab3
--- /dev/null
+++ b/launcher/pages/instance/GameOptionsPage.cpp
@@ -0,0 +1,37 @@
+#include "GameOptionsPage.h"
+#include "ui_GameOptionsPage.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/gameoptions/GameOptions.h"
+
+GameOptionsPage::GameOptionsPage(MinecraftInstance * inst, QWidget* parent)
+ : QWidget(parent), ui(new Ui::GameOptionsPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ m_model = inst->gameOptionsModel();
+ ui->optionsView->setModel(m_model.get());
+ auto head = ui->optionsView->header();
+ if(head->count())
+ {
+ head->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ for(int i = 1; i < head->count(); i++)
+ {
+ head->setSectionResizeMode(i, QHeaderView::Stretch);
+ }
+ }
+}
+
+GameOptionsPage::~GameOptionsPage()
+{
+ // m_model->save();
+}
+
+void GameOptionsPage::openedImpl()
+{
+ // m_model->observe();
+}
+
+void GameOptionsPage::closedImpl()
+{
+ // m_model->unobserve();
+}
diff --git a/launcher/pages/instance/GameOptionsPage.h b/launcher/pages/instance/GameOptionsPage.h
new file mode 100644
index 00000000..0fd2fbff
--- /dev/null
+++ b/launcher/pages/instance/GameOptionsPage.h
@@ -0,0 +1,63 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+#include <QString>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+namespace Ui
+{
+class GameOptionsPage;
+}
+
+class GameOptions;
+class MinecraftInstance;
+
+class GameOptionsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit GameOptionsPage(MinecraftInstance *inst, QWidget *parent = 0);
+ virtual ~GameOptionsPage();
+
+ void openedImpl() override;
+ void closedImpl() override;
+
+ virtual QString displayName() const override
+ {
+ return tr("Game Options");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("settings");
+ }
+ virtual QString id() const override
+ {
+ return "gameoptions";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Game-Options-management";
+ }
+
+private: // data
+ Ui::GameOptionsPage *ui = nullptr;
+ std::shared_ptr<GameOptions> m_model;
+};
diff --git a/launcher/pages/instance/GameOptionsPage.ui b/launcher/pages/instance/GameOptionsPage.ui
new file mode 100644
index 00000000..f0a5ce0e
--- /dev/null
+++ b/launcher/pages/instance/GameOptionsPage.ui
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>GameOptionsPage</class>
+ <widget class="QWidget" name="GameOptionsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>706</width>
+ <height>575</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0" colspan="2">
+ <widget class="QTreeView" name="optionsView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>optionsView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/InstanceSettingsPage.cpp b/launcher/pages/instance/InstanceSettingsPage.cpp
new file mode 100644
index 00000000..7bd424c0
--- /dev/null
+++ b/launcher/pages/instance/InstanceSettingsPage.cpp
@@ -0,0 +1,338 @@
+#include "InstanceSettingsPage.h"
+#include "ui_InstanceSettingsPage.h"
+
+#include <QFileDialog>
+#include <QDialog>
+#include <QMessageBox>
+
+#include "dialogs/VersionSelectDialog.h"
+#include "JavaCommon.h"
+#include "MultiMC.h"
+
+#include <java/JavaInstallList.h>
+#include <FileSystem.h>
+#include <sys.h>
+#include <widgets/CustomCommands.h>
+
+InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent)
+ : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst)
+{
+ m_settings = inst->settings();
+ ui->setupUi(this);
+ auto sysMB = Sys::getSystemRam() / Sys::mebibyte;
+ ui->maxMemSpinBox->setMaximum(sysMB);
+ connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked);
+ connect(MMC, &MultiMC::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings);
+ connect(MMC, &MultiMC::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings);
+ loadSettings();
+}
+
+bool InstanceSettingsPage::shouldDisplay() const
+{
+ return !m_instance->isRunning();
+}
+
+InstanceSettingsPage::~InstanceSettingsPage()
+{
+ delete ui;
+}
+
+void InstanceSettingsPage::globalSettingsButtonClicked(bool)
+{
+ switch(ui->settingsTabs->currentIndex()) {
+ case 0:
+ MMC->ShowGlobalSettings(this, "java-settings");
+ return;
+ case 1:
+ MMC->ShowGlobalSettings(this, "minecraft-settings");
+ return;
+ case 2:
+ MMC->ShowGlobalSettings(this, "custom-commands");
+ return;
+ }
+}
+
+bool InstanceSettingsPage::apply()
+{
+ applySettings();
+ return true;
+}
+
+void InstanceSettingsPage::applySettings()
+{
+ SettingsObject::Lock lock(m_settings);
+
+ // Console
+ bool console = ui->consoleSettingsBox->isChecked();
+ m_settings->set("OverrideConsole", console);
+ if (console)
+ {
+ m_settings->set("ShowConsole", ui->showConsoleCheck->isChecked());
+ m_settings->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked());
+ m_settings->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked());
+ }
+ else
+ {
+ m_settings->reset("ShowConsole");
+ m_settings->reset("AutoCloseConsole");
+ m_settings->reset("ShowConsoleOnError");
+ }
+
+ // Window Size
+ bool window = ui->windowSizeGroupBox->isChecked();
+ m_settings->set("OverrideWindow", window);
+ if (window)
+ {
+ m_settings->set("LaunchMaximized", ui->maximizedCheckBox->isChecked());
+ m_settings->set("MinecraftWinWidth", ui->windowWidthSpinBox->value());
+ m_settings->set("MinecraftWinHeight", ui->windowHeightSpinBox->value());
+ }
+ else
+ {
+ m_settings->reset("LaunchMaximized");
+ m_settings->reset("MinecraftWinWidth");
+ m_settings->reset("MinecraftWinHeight");
+ }
+
+ // Memory
+ bool memory = ui->memoryGroupBox->isChecked();
+ m_settings->set("OverrideMemory", memory);
+ if (memory)
+ {
+ int min = ui->minMemSpinBox->value();
+ int max = ui->maxMemSpinBox->value();
+ if(min < max)
+ {
+ m_settings->set("MinMemAlloc", min);
+ m_settings->set("MaxMemAlloc", max);
+ }
+ else
+ {
+ m_settings->set("MinMemAlloc", max);
+ m_settings->set("MaxMemAlloc", min);
+ }
+ m_settings->set("PermGen", ui->permGenSpinBox->value());
+ }
+ else
+ {
+ m_settings->reset("MinMemAlloc");
+ m_settings->reset("MaxMemAlloc");
+ m_settings->reset("PermGen");
+ }
+
+ // Java Install Settings
+ bool javaInstall = ui->javaSettingsGroupBox->isChecked();
+ m_settings->set("OverrideJavaLocation", javaInstall);
+ if (javaInstall)
+ {
+ m_settings->set("JavaPath", ui->javaPathTextBox->text());
+ }
+ else
+ {
+ m_settings->reset("JavaPath");
+ }
+
+ // Java arguments
+ bool javaArgs = ui->javaArgumentsGroupBox->isChecked();
+ m_settings->set("OverrideJavaArgs", javaArgs);
+ if(javaArgs)
+ {
+ m_settings->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " "));
+ JavaCommon::checkJVMArgs(m_settings->get("JvmArgs").toString(), this->parentWidget());
+ }
+ else
+ {
+ m_settings->reset("JvmArgs");
+ }
+
+ // old generic 'override both' is removed.
+ m_settings->reset("OverrideJava");
+
+ // Custom Commands
+ bool custcmd = ui->customCommands->checked();
+ m_settings->set("OverrideCommands", custcmd);
+ if (custcmd)
+ {
+ m_settings->set("PreLaunchCommand", ui->customCommands->prelaunchCommand());
+ m_settings->set("WrapperCommand", ui->customCommands->wrapperCommand());
+ m_settings->set("PostExitCommand", ui->customCommands->postexitCommand());
+ }
+ else
+ {
+ m_settings->reset("PreLaunchCommand");
+ m_settings->reset("WrapperCommand");
+ m_settings->reset("PostExitCommand");
+ }
+
+ // Workarounds
+ bool workarounds = ui->nativeWorkaroundsGroupBox->isChecked();
+ m_settings->set("OverrideNativeWorkarounds", workarounds);
+ if(workarounds)
+ {
+ m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked());
+ m_settings->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked());
+ }
+ else
+ {
+ m_settings->reset("UseNativeOpenAL");
+ m_settings->reset("UseNativeGLFW");
+ }
+
+ // Game time
+ bool gameTime = ui->gameTimeGroupBox->isChecked();
+ m_settings->set("OverrideGameTime", gameTime);
+ if (gameTime)
+ {
+ m_settings->set("ShowGameTime", ui->showGameTime->isChecked());
+ m_settings->set("RecordGameTime", ui->recordGameTime->isChecked());
+ }
+ else
+ {
+ m_settings->reset("ShowGameTime");
+ m_settings->reset("RecordGameTime");
+ }
+
+ // Join server on launch
+ bool joinServerOnLaunch = ui->serverJoinGroupBox->isChecked();
+ m_settings->set("JoinServerOnLaunch", joinServerOnLaunch);
+ if (joinServerOnLaunch)
+ {
+ m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text());
+ }
+ else
+ {
+ m_settings->reset("JoinServerOnLaunchAddress");
+ }
+}
+
+void InstanceSettingsPage::loadSettings()
+{
+ // Console
+ ui->consoleSettingsBox->setChecked(m_settings->get("OverrideConsole").toBool());
+ ui->showConsoleCheck->setChecked(m_settings->get("ShowConsole").toBool());
+ ui->autoCloseConsoleCheck->setChecked(m_settings->get("AutoCloseConsole").toBool());
+ ui->showConsoleErrorCheck->setChecked(m_settings->get("ShowConsoleOnError").toBool());
+
+ // Window Size
+ ui->windowSizeGroupBox->setChecked(m_settings->get("OverrideWindow").toBool());
+ ui->maximizedCheckBox->setChecked(m_settings->get("LaunchMaximized").toBool());
+ ui->windowWidthSpinBox->setValue(m_settings->get("MinecraftWinWidth").toInt());
+ ui->windowHeightSpinBox->setValue(m_settings->get("MinecraftWinHeight").toInt());
+
+ // Memory
+ ui->memoryGroupBox->setChecked(m_settings->get("OverrideMemory").toBool());
+ int min = m_settings->get("MinMemAlloc").toInt();
+ int max = m_settings->get("MaxMemAlloc").toInt();
+ if(min < max)
+ {
+ ui->minMemSpinBox->setValue(min);
+ ui->maxMemSpinBox->setValue(max);
+ }
+ else
+ {
+ ui->minMemSpinBox->setValue(max);
+ ui->maxMemSpinBox->setValue(min);
+ }
+ ui->permGenSpinBox->setValue(m_settings->get("PermGen").toInt());
+ bool permGenVisible = m_settings->get("PermGenVisible").toBool();
+ ui->permGenSpinBox->setVisible(permGenVisible);
+ ui->labelPermGen->setVisible(permGenVisible);
+ ui->labelPermgenNote->setVisible(permGenVisible);
+
+
+ // Java Settings
+ bool overrideJava = m_settings->get("OverrideJava").toBool();
+ bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool() || overrideJava;
+ bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool() || overrideJava;
+
+ ui->javaSettingsGroupBox->setChecked(overrideLocation);
+ ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString());
+
+ ui->javaArgumentsGroupBox->setChecked(overrideArgs);
+ ui->jvmArgsTextBox->setPlainText(m_settings->get("JvmArgs").toString());
+
+ // Custom commands
+ ui->customCommands->initialize(
+ true,
+ m_settings->get("OverrideCommands").toBool(),
+ m_settings->get("PreLaunchCommand").toString(),
+ m_settings->get("WrapperCommand").toString(),
+ m_settings->get("PostExitCommand").toString()
+ );
+
+ // Workarounds
+ ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool());
+ ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool());
+ ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool());
+
+ // Miscellanous
+ ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool());
+ ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool());
+ ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool());
+
+ ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool());
+ ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString());
+}
+
+void InstanceSettingsPage::on_javaDetectBtn_clicked()
+{
+ JavaInstallPtr java;
+
+ VersionSelectDialog vselect(MMC->javalist().get(), tr("Select a Java version"), this, true);
+ vselect.setResizeOn(2);
+ vselect.exec();
+
+ if (vselect.result() == QDialog::Accepted && vselect.selectedVersion())
+ {
+ java = std::dynamic_pointer_cast<JavaInstall>(vselect.selectedVersion());
+ ui->javaPathTextBox->setText(java->path);
+ bool visible = java->id.requiresPermGen() && m_settings->get("OverrideMemory").toBool();
+ ui->permGenSpinBox->setVisible(visible);
+ ui->labelPermGen->setVisible(visible);
+ ui->labelPermgenNote->setVisible(visible);
+ m_settings->set("PermGenVisible", visible);
+ }
+}
+
+void InstanceSettingsPage::on_javaBrowseBtn_clicked()
+{
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"));
+
+ // do not allow current dir - it's dirty. Do not allow dirs that don't exist
+ if(raw_path.isEmpty())
+ {
+ return;
+ }
+ QString cooked_path = FS::NormalizePath(raw_path);
+
+ QFileInfo javaInfo(cooked_path);
+ if(!javaInfo.exists() || !javaInfo.isExecutable())
+ {
+ return;
+ }
+ ui->javaPathTextBox->setText(cooked_path);
+
+ // custom Java could be anything... enable perm gen option
+ ui->permGenSpinBox->setVisible(true);
+ ui->labelPermGen->setVisible(true);
+ ui->labelPermgenNote->setVisible(true);
+ m_settings->set("PermGenVisible", true);
+}
+
+void InstanceSettingsPage::on_javaTestBtn_clicked()
+{
+ if(checker)
+ {
+ return;
+ }
+ checker.reset(new JavaCommon::TestCheck(
+ this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "),
+ ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value()));
+ connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished()));
+ checker->run();
+}
+
+void InstanceSettingsPage::checkerFinished()
+{
+ checker.reset();
+}
diff --git a/launcher/pages/instance/InstanceSettingsPage.h b/launcher/pages/instance/InstanceSettingsPage.h
new file mode 100644
index 00000000..068213a8
--- /dev/null
+++ b/launcher/pages/instance/InstanceSettingsPage.h
@@ -0,0 +1,76 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "java/JavaChecker.h"
+#include "BaseInstance.h"
+#include <QObjectPtr.h>
+#include "pages/BasePage.h"
+#include "JavaCommon.h"
+#include "MultiMC.h"
+
+class JavaChecker;
+namespace Ui
+{
+class InstanceSettingsPage;
+}
+
+class InstanceSettingsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit InstanceSettingsPage(BaseInstance *inst, QWidget *parent = 0);
+ virtual ~InstanceSettingsPage();
+ virtual QString displayName() const override
+ {
+ return tr("Settings");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("instance-settings");
+ }
+ virtual QString id() const override
+ {
+ return "settings";
+ }
+ virtual bool apply() override;
+ virtual QString helpPage() const override
+ {
+ return "Instance-settings";
+ }
+ virtual bool shouldDisplay() const override;
+
+private slots:
+ void on_javaDetectBtn_clicked();
+ void on_javaTestBtn_clicked();
+ void on_javaBrowseBtn_clicked();
+
+ void applySettings();
+ void loadSettings();
+
+ void checkerFinished();
+
+ void globalSettingsButtonClicked(bool checked);
+
+private:
+ Ui::InstanceSettingsPage *ui;
+ BaseInstance *m_instance;
+ SettingsObjectPtr m_settings;
+ unique_qobject_ptr<JavaCommon::TestCheck> checker;
+};
diff --git a/launcher/pages/instance/InstanceSettingsPage.ui b/launcher/pages/instance/InstanceSettingsPage.ui
new file mode 100644
index 00000000..e569ce56
--- /dev/null
+++ b/launcher/pages/instance/InstanceSettingsPage.ui
@@ -0,0 +1,548 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>InstanceSettingsPage</class>
+ <widget class="QWidget" name="InstanceSettingsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>691</width>
+ <height>581</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCommandLinkButton" name="openGlobalJavaSettingsButton">
+ <property name="text">
+ <string>Open Global Settings</string>
+ </property>
+ <property name="description">
+ <string>The settings here are overrides for global settings.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="settingsTabs">
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="minecraftTab">
+ <attribute name="title">
+ <string notr="true">Java</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QGroupBox" name="javaSettingsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Java insta&amp;llation</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" colspan="3">
+ <widget class="QLineEdit" name="javaPathTextBox"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QPushButton" name="javaDetectBtn">
+ <property name="text">
+ <string>Auto-detect...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QPushButton" name="javaBrowseBtn">
+ <property name="text">
+ <string>Browse...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QPushButton" name="javaTestBtn">
+ <property name="text">
+ <string>Test</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="memoryGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Memor&amp;y</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelMinMem">
+ <property name="text">
+ <string>Minimum memory allocation:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="maxMemSpinBox">
+ <property name="toolTip">
+ <string>The maximum amount of memory Minecraft is allowed to use.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>128</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>1024</number>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="minMemSpinBox">
+ <property name="toolTip">
+ <string>The amount of memory Minecraft is started with.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>128</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>256</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QSpinBox" name="permGenSpinBox">
+ <property name="toolTip">
+ <string>The amount of memory available to store loaded Java classes.</string>
+ </property>
+ <property name="suffix">
+ <string notr="true"> MiB</string>
+ </property>
+ <property name="minimum">
+ <number>64</number>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ <property name="singleStep">
+ <number>8</number>
+ </property>
+ <property name="value">
+ <number>64</number>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelPermGen">
+ <property name="text">
+ <string notr="true">PermGen:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelMaxMem">
+ <property name="text">
+ <string>Maximum memory allocation:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0" colspan="2">
+ <widget class="QLabel" name="labelPermgenNote">
+ <property name="text">
+ <string>Note: Permgen is set automatically by Java 8 and later</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="javaArgumentsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Java argumen&amp;ts</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_5">
+ <item row="1" column="1">
+ <widget class="QPlainTextEdit" name="jvmArgsTextBox"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="javaTab">
+ <attribute name="title">
+ <string>Game windows</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QGroupBox" name="windowSizeGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Game Window</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="maximizedCheckBox">
+ <property name="text">
+ <string>Start Minecraft maximized?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayoutWindowSize">
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelWindowHeight">
+ <property name="text">
+ <string>Window height:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelWindowWidth">
+ <property name="text">
+ <string>Window width:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QSpinBox" name="windowWidthSpinBox">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="singleStep">
+ <number>1</number>
+ </property>
+ <property name="value">
+ <number>854</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QSpinBox" name="windowHeightSpinBox">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>65536</number>
+ </property>
+ <property name="value">
+ <number>480</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="consoleSettingsBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Conso&amp;le Settings</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QCheckBox" name="showConsoleCheck">
+ <property name="text">
+ <string>Show console while the game is running?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="autoCloseConsoleCheck">
+ <property name="text">
+ <string>Automatically close console when the game quits?</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showConsoleErrorCheck">
+ <property name="text">
+ <string>Show console when the game crashes?</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacerMinecraft_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>88</width>
+ <height>125</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Custom commands</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="CustomCommands" name="customCommands" native="true"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="workaroundsPage">
+ <attribute name="title">
+ <string>Workarounds</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QGroupBox" name="nativeWorkaroundsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Native libraries</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QCheckBox" name="useNativeGLFWCheck">
+ <property name="text">
+ <string>Use system installation of GLFW</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useNativeOpenALCheck">
+ <property name="text">
+ <string>Use system installation of OpenAL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="miscellanousPage">
+ <attribute name="title">
+ <string>Miscellanous</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <item>
+ <widget class="QGroupBox" name="gameTimeGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Override global game time settings</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <widget class="QCheckBox" name="showGameTime">
+ <property name="text">
+ <string>Show time spent playing this instance</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="recordGameTime">
+ <property name="text">
+ <string>Record time spent playing this instance</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="serverJoinGroupBox">
+ <property name="title">
+ <string>Set a server to join on launch</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <item>
+ <layout class="QGridLayout" name="serverJoinLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="serverJoinAddressLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Server address:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="serverJoinAddress"/>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacerMiscellanous">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>CustomCommands</class>
+ <extends>QWidget</extends>
+ <header>widgets/CustomCommands.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>openGlobalJavaSettingsButton</tabstop>
+ <tabstop>settingsTabs</tabstop>
+ <tabstop>javaSettingsGroupBox</tabstop>
+ <tabstop>javaPathTextBox</tabstop>
+ <tabstop>javaDetectBtn</tabstop>
+ <tabstop>javaBrowseBtn</tabstop>
+ <tabstop>javaTestBtn</tabstop>
+ <tabstop>memoryGroupBox</tabstop>
+ <tabstop>minMemSpinBox</tabstop>
+ <tabstop>maxMemSpinBox</tabstop>
+ <tabstop>permGenSpinBox</tabstop>
+ <tabstop>javaArgumentsGroupBox</tabstop>
+ <tabstop>jvmArgsTextBox</tabstop>
+ <tabstop>windowSizeGroupBox</tabstop>
+ <tabstop>maximizedCheckBox</tabstop>
+ <tabstop>windowWidthSpinBox</tabstop>
+ <tabstop>windowHeightSpinBox</tabstop>
+ <tabstop>consoleSettingsBox</tabstop>
+ <tabstop>showConsoleCheck</tabstop>
+ <tabstop>autoCloseConsoleCheck</tabstop>
+ <tabstop>showConsoleErrorCheck</tabstop>
+ <tabstop>nativeWorkaroundsGroupBox</tabstop>
+ <tabstop>useNativeGLFWCheck</tabstop>
+ <tabstop>useNativeOpenALCheck</tabstop>
+ <tabstop>showGameTime</tabstop>
+ <tabstop>recordGameTime</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/LegacyUpgradePage.cpp b/launcher/pages/instance/LegacyUpgradePage.cpp
new file mode 100644
index 00000000..af800b03
--- /dev/null
+++ b/launcher/pages/instance/LegacyUpgradePage.cpp
@@ -0,0 +1,50 @@
+#include "LegacyUpgradePage.h"
+#include "ui_LegacyUpgradePage.h"
+
+#include "InstanceList.h"
+#include "minecraft/legacy/LegacyInstance.h"
+#include "minecraft/legacy/LegacyUpgradeTask.h"
+#include "MultiMC.h"
+#include "dialogs/CustomMessageBox.h"
+#include "dialogs/ProgressDialog.h"
+
+LegacyUpgradePage::LegacyUpgradePage(InstancePtr inst, QWidget *parent)
+ : QWidget(parent), ui(new Ui::LegacyUpgradePage), m_inst(inst)
+{
+ ui->setupUi(this);
+}
+
+LegacyUpgradePage::~LegacyUpgradePage()
+{
+ delete ui;
+}
+
+void LegacyUpgradePage::runModalTask(Task *task)
+{
+ connect(task, &Task::failed, [this](QString reason)
+ {
+ CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Warning)->show();
+ });
+ ProgressDialog loadDialog(this);
+ loadDialog.setSkipButton(true, tr("Abort"));
+ if(loadDialog.execWithTask(task) == QDialog::Accepted)
+ {
+ m_container->requestClose();
+ }
+}
+
+void LegacyUpgradePage::on_upgradeButton_clicked()
+{
+ QString newName = tr("%1 (Migrated)").arg(m_inst->name());
+ auto upgradeTask = new LegacyUpgradeTask(m_inst);
+ upgradeTask->setName(newName);
+ upgradeTask->setGroup(MMC->instances()->getInstanceGroup(m_inst->id()));
+ upgradeTask->setIcon(m_inst->iconKey());
+ unique_qobject_ptr<Task> task(MMC->instances()->wrapInstanceTask(upgradeTask));
+ runModalTask(task.get());
+}
+
+bool LegacyUpgradePage::shouldDisplay() const
+{
+ return !m_inst->isRunning();
+}
diff --git a/launcher/pages/instance/LegacyUpgradePage.h b/launcher/pages/instance/LegacyUpgradePage.h
new file mode 100644
index 00000000..df34e33a
--- /dev/null
+++ b/launcher/pages/instance/LegacyUpgradePage.h
@@ -0,0 +1,64 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "minecraft/legacy/LegacyInstance.h"
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class LegacyUpgradePage;
+}
+
+class LegacyUpgradePage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit LegacyUpgradePage(InstancePtr inst, QWidget *parent = 0);
+ virtual ~LegacyUpgradePage();
+ virtual QString displayName() const override
+ {
+ return tr("Upgrade");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("checkupdate");
+ }
+ virtual QString id() const override
+ {
+ return "upgrade";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Legacy-upgrade";
+ }
+ virtual bool shouldDisplay() const override;
+
+private slots:
+ void on_upgradeButton_clicked();
+
+private:
+ void runModalTask(Task *task);
+
+private:
+ Ui::LegacyUpgradePage *ui;
+ InstancePtr m_inst;
+};
diff --git a/launcher/pages/instance/LegacyUpgradePage.ui b/launcher/pages/instance/LegacyUpgradePage.ui
new file mode 100644
index 00000000..a94ee039
--- /dev/null
+++ b/launcher/pages/instance/LegacyUpgradePage.ui
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LegacyUpgradePage</class>
+ <widget class="QWidget" name="LegacyUpgradePage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>546</width>
+ <height>405</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTextBrowser" name="textBrowser">
+ <property name="html">
+ <string>&lt;html&gt;&lt;body&gt;&lt;h1&gt;Upgrade is required&lt;/h1&gt;&lt;p&gt;MultiMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.&lt;/p&gt;&lt;p&gt;The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.&lt;/p&gt;&lt;p&gt;Please report any issues on our &lt;a href=&quot;https://github.com/MultiMC/MultiMC5/issues&quot;&gt;github issues page&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There is also a &lt;a href=&quot;https://discord.gg/GtPmv93&quot;&gt;discord channel for testing here&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCommandLinkButton" name="upgradeButton">
+ <property name="text">
+ <string>Upgrade the instance</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/LogPage.cpp b/launcher/pages/instance/LogPage.cpp
new file mode 100644
index 00000000..3d2085c6
--- /dev/null
+++ b/launcher/pages/instance/LogPage.cpp
@@ -0,0 +1,312 @@
+#include "LogPage.h"
+#include "ui_LogPage.h"
+
+#include "MultiMC.h"
+
+#include <QIcon>
+#include <QScrollBar>
+#include <QShortcut>
+
+#include "launch/LaunchTask.h"
+#include <settings/Setting.h>
+#include "GuiUtil.h"
+#include <ColorCache.h>
+
+class LogFormatProxyModel : public QIdentityProxyModel
+{
+public:
+ LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent)
+ {
+ }
+ QVariant data(const QModelIndex &index, int role) const override
+ {
+ switch(role)
+ {
+ case Qt::FontRole:
+ return m_font;
+ case Qt::TextColorRole:
+ {
+ MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt();
+ return m_colors->getFront(level);
+ }
+ case Qt::BackgroundRole:
+ {
+ MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt();
+ return m_colors->getBack(level);
+ }
+ default:
+ return QIdentityProxyModel::data(index, role);
+ }
+ }
+
+ void setFont(QFont font)
+ {
+ m_font = font;
+ }
+
+ void setColors(LogColorCache* colors)
+ {
+ m_colors.reset(colors);
+ }
+
+ QModelIndex find(const QModelIndex &start, const QString &value, bool reverse) const
+ {
+ QModelIndex parentIndex = parent(start);
+ auto compare = [&](int r) -> QModelIndex
+ {
+ QModelIndex idx = index(r, start.column(), parentIndex);
+ if (!idx.isValid() || idx == start)
+ {
+ return QModelIndex();
+ }
+ QVariant v = data(idx, Qt::DisplayRole);
+ QString t = v.toString();
+ if (t.contains(value, Qt::CaseInsensitive))
+ return idx;
+ return QModelIndex();
+ };
+ if(reverse)
+ {
+ int from = start.row();
+ int to = 0;
+
+ for (int i = 0; i < 2; ++i)
+ {
+ for (int r = from; (r >= to); --r)
+ {
+ auto idx = compare(r);
+ if(idx.isValid())
+ return idx;
+ }
+ // prepare for the next iteration
+ from = rowCount() - 1;
+ to = start.row();
+ }
+ }
+ else
+ {
+ int from = start.row();
+ int to = rowCount(parentIndex);
+
+ for (int i = 0; i < 2; ++i)
+ {
+ for (int r = from; (r < to); ++r)
+ {
+ auto idx = compare(r);
+ if(idx.isValid())
+ return idx;
+ }
+ // prepare for the next iteration
+ from = 0;
+ to = start.row();
+ }
+ }
+ return QModelIndex();
+ }
+private:
+ QFont m_font;
+ std::unique_ptr<LogColorCache> m_colors;
+};
+
+LogPage::LogPage(InstancePtr instance, QWidget *parent)
+ : QWidget(parent), ui(new Ui::LogPage), m_instance(instance)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+
+ m_proxy = new LogFormatProxyModel(this);
+ // set up text colors in the log proxy and adapt them to the current theme foreground and background
+ {
+ auto origForeground = ui->text->palette().color(ui->text->foregroundRole());
+ auto origBackground = ui->text->palette().color(ui->text->backgroundRole());
+ m_proxy->setColors(new LogColorCache(origForeground, origBackground));
+ }
+
+ // set up fonts in the log proxy
+ {
+ QString fontFamily = MMC->settings()->get("ConsoleFont").toString();
+ bool conversionOk = false;
+ int fontSize = MMC->settings()->get("ConsoleFontSize").toInt(&conversionOk);
+ if(!conversionOk)
+ {
+ fontSize = 11;
+ }
+ m_proxy->setFont(QFont(fontFamily, fontSize));
+ }
+
+ ui->text->setModel(m_proxy);
+
+ // set up instance and launch process recognition
+ {
+ auto launchTask = m_instance->getLaunchTask();
+ if(launchTask)
+ {
+ setInstanceLaunchTaskChanged(launchTask, true);
+ }
+ connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &LogPage::onInstanceLaunchTaskChanged);
+ }
+
+ auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this);
+ connect(findShortcut, SIGNAL(activated()), SLOT(findActivated()));
+ auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this);
+ connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated()));
+ connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked()));
+ auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this);
+ connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated()));
+}
+
+LogPage::~LogPage()
+{
+ delete ui;
+}
+
+void LogPage::modelStateToUI()
+{
+ if(m_model->wrapLines())
+ {
+ ui->text->setWordWrap(true);
+ ui->wrapCheckbox->setCheckState(Qt::Checked);
+ }
+ else
+ {
+ ui->text->setWordWrap(false);
+ ui->wrapCheckbox->setCheckState(Qt::Unchecked);
+ }
+ if(m_model->suspended())
+ {
+ ui->trackLogCheckbox->setCheckState(Qt::Unchecked);
+ }
+ else
+ {
+ ui->trackLogCheckbox->setCheckState(Qt::Checked);
+ }
+}
+
+void LogPage::UIToModelState()
+{
+ if(!m_model)
+ {
+ return;
+ }
+ m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked);
+ m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked);
+}
+
+void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc, bool initial)
+{
+ m_process = proc;
+ if(m_process)
+ {
+ m_model = proc->getLogModel();
+ m_proxy->setSourceModel(m_model.get());
+ if(initial)
+ {
+ modelStateToUI();
+ }
+ else
+ {
+ UIToModelState();
+ }
+ }
+ else
+ {
+ m_proxy->setSourceModel(nullptr);
+ m_model.reset();
+ }
+}
+
+void LogPage::onInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc)
+{
+ setInstanceLaunchTaskChanged(proc, false);
+}
+
+bool LogPage::apply()
+{
+ return true;
+}
+
+bool LogPage::shouldDisplay() const
+{
+ return m_instance->isRunning() || m_proxy->rowCount() > 0;
+}
+
+void LogPage::on_btnPaste_clicked()
+{
+ if(!m_model)
+ return;
+
+ //FIXME: turn this into a proper task and move the upload logic out of GuiUtil!
+ m_model->append(MessageLevel::MultiMC, QString("MultiMC: Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date)));
+ auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this);
+ if(!url.isEmpty())
+ {
+ m_model->append(MessageLevel::MultiMC, QString("MultiMC: Log uploaded to: %1").arg(url));
+ }
+ else
+ {
+ m_model->append(MessageLevel::Error, "MultiMC: Log upload failed!");
+ }
+}
+
+void LogPage::on_btnCopy_clicked()
+{
+ if(!m_model)
+ return;
+ m_model->append(MessageLevel::MultiMC, QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date)));
+ GuiUtil::setClipboardText(m_model->toPlainText());
+}
+
+void LogPage::on_btnClear_clicked()
+{
+ if(!m_model)
+ return;
+ m_model->clear();
+ m_container->refreshContainer();
+}
+
+void LogPage::on_btnBottom_clicked()
+{
+ ui->text->scrollToBottom();
+}
+
+void LogPage::on_trackLogCheckbox_clicked(bool checked)
+{
+ if(!m_model)
+ return;
+ m_model->suspend(!checked);
+}
+
+void LogPage::on_wrapCheckbox_clicked(bool checked)
+{
+ ui->text->setWordWrap(checked);
+ if(!m_model)
+ return;
+ m_model->setLineWrap(checked);
+}
+
+void LogPage::on_findButton_clicked()
+{
+ auto modifiers = QApplication::keyboardModifiers();
+ bool reverse = modifiers & Qt::ShiftModifier;
+ ui->text->findNext(ui->searchBar->text(), reverse);
+}
+
+void LogPage::findNextActivated()
+{
+ ui->text->findNext(ui->searchBar->text(), false);
+}
+
+void LogPage::findPreviousActivated()
+{
+ ui->text->findNext(ui->searchBar->text(), true);
+}
+
+void LogPage::findActivated()
+{
+ // focus the search bar if it doesn't have focus
+ if (!ui->searchBar->hasFocus())
+ {
+ ui->searchBar->setFocus();
+ ui->searchBar->selectAll();
+ }
+}
diff --git a/launcher/pages/instance/LogPage.h b/launcher/pages/instance/LogPage.h
new file mode 100644
index 00000000..b0b0e04b
--- /dev/null
+++ b/launcher/pages/instance/LogPage.h
@@ -0,0 +1,86 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "BaseInstance.h"
+#include "launch/LaunchTask.h"
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+namespace Ui
+{
+class LogPage;
+}
+class QTextCharFormat;
+class LogFormatProxyModel;
+
+class LogPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit LogPage(InstancePtr instance, QWidget *parent = 0);
+ virtual ~LogPage();
+ virtual QString displayName() const override
+ {
+ return tr("Minecraft Log");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("log");
+ }
+ virtual QString id() const override
+ {
+ return "console";
+ }
+ virtual bool apply() override;
+ virtual QString helpPage() const override
+ {
+ return "Minecraft-Logs";
+ }
+ virtual bool shouldDisplay() const override;
+
+private slots:
+ void on_btnPaste_clicked();
+ void on_btnCopy_clicked();
+ void on_btnClear_clicked();
+ void on_btnBottom_clicked();
+
+ void on_trackLogCheckbox_clicked(bool checked);
+ void on_wrapCheckbox_clicked(bool checked);
+
+ void on_findButton_clicked();
+ void findActivated();
+ void findNextActivated();
+ void findPreviousActivated();
+
+ void onInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc);
+
+private:
+ void modelStateToUI();
+ void UIToModelState();
+ void setInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc, bool initial);
+
+private:
+ Ui::LogPage *ui;
+ InstancePtr m_instance;
+ shared_qobject_ptr<LaunchTask> m_process;
+
+ LogFormatProxyModel * m_proxy;
+ shared_qobject_ptr <LogModel> m_model;
+};
diff --git a/launcher/pages/instance/LogPage.ui b/launcher/pages/instance/LogPage.ui
new file mode 100644
index 00000000..4843d7c3
--- /dev/null
+++ b/launcher/pages/instance/LogPage.ui
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LogPage</class>
+ <widget class="QWidget" name="LogPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>825</width>
+ <height>782</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="5">
+ <widget class="LogView" name="text">
+ <property name="undoRedoEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="plainText">
+ <string notr="true"/>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ <property name="centerOnScroll">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="5">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QCheckBox" name="trackLogCheckbox">
+ <property name="text">
+ <string>Keep updating</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="wrapCheckbox">
+ <property name="text">
+ <string>Wrap lines</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnCopy">
+ <property name="toolTip">
+ <string>Copy the whole log into the clipboard</string>
+ </property>
+ <property name="text">
+ <string>&amp;Copy</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnPaste">
+ <property name="toolTip">
+ <string>Upload the log to paste.ee - it will stay online for a month</string>
+ </property>
+ <property name="text">
+ <string>Upload</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btnClear">
+ <property name="toolTip">
+ <string>Clear the log</string>
+ </property>
+ <property name="text">
+ <string>Clear</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Search:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QPushButton" name="findButton">
+ <property name="text">
+ <string>Find</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="searchBar"/>
+ </item>
+ <item row="2" column="4">
+ <widget class="QPushButton" name="btnBottom">
+ <property name="toolTip">
+ <string>Scroll all the way to bottom</string>
+ </property>
+ <property name="text">
+ <string>Bottom</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>LogView</class>
+ <extends>QPlainTextEdit</extends>
+ <header>widgets/LogView.h</header>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>trackLogCheckbox</tabstop>
+ <tabstop>wrapCheckbox</tabstop>
+ <tabstop>btnCopy</tabstop>
+ <tabstop>btnPaste</tabstop>
+ <tabstop>btnClear</tabstop>
+ <tabstop>text</tabstop>
+ <tabstop>searchBar</tabstop>
+ <tabstop>findButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/ModFolderPage.cpp b/launcher/pages/instance/ModFolderPage.cpp
new file mode 100644
index 00000000..98f20e77
--- /dev/null
+++ b/launcher/pages/instance/ModFolderPage.cpp
@@ -0,0 +1,363 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ModFolderPage.h"
+#include "ui_ModFolderPage.h"
+
+#include <QMessageBox>
+#include <QEvent>
+#include <QKeyEvent>
+#include <QAbstractItemModel>
+#include <QMenu>
+
+#include "MultiMC.h"
+#include "dialogs/CustomMessageBox.h"
+#include <GuiUtil.h>
+#include "minecraft/mod/ModFolderModel.h"
+#include "minecraft/mod/Mod.h"
+#include "minecraft/VersionFilterData.h"
+#include "minecraft/PackProfile.h"
+#include <DesktopServices.h>
+
+#include <QSortFilterProxyModel>
+#include "Version.h"
+
+namespace {
+ // FIXME: wasteful
+ void RemoveThePrefix(QString & string) {
+ QRegularExpression regex(QStringLiteral("^(([Tt][Hh][eE])|([Tt][eE][Hh])) +"));
+ string.remove(regex);
+ string = string.trimmed();
+ }
+}
+
+class ModSortProxy : public QSortFilterProxyModel
+{
+public:
+ explicit ModSortProxy(QObject *parent = 0) : QSortFilterProxyModel(parent)
+ {
+ }
+
+protected:
+ bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override {
+ ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
+ if(!model) {
+ return false;
+ }
+ const auto &mod = model->at(source_row);
+ if(mod.name().contains(filterRegExp())) {
+ return true;
+ }
+ if(mod.description().contains(filterRegExp())) {
+ return true;
+ }
+ for(auto & author: mod.authors()) {
+ if (author.contains(filterRegExp())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const override
+ {
+ ModFolderModel *model = qobject_cast<ModFolderModel *>(sourceModel());
+ if(
+ !model ||
+ !source_left.isValid() ||
+ !source_right.isValid() ||
+ source_left.column() != source_right.column()
+ ) {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+
+ // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and proceed.
+
+ auto column = (ModFolderModel::Columns) source_left.column();
+ bool invert = false;
+ switch(column) {
+ // GH-2550 - sort by enabled/disabled
+ case ModFolderModel::ActiveColumn: {
+ auto dataL = source_left.data(Qt::CheckStateRole).toBool();
+ auto dataR = source_right.data(Qt::CheckStateRole).toBool();
+ if(dataL != dataR) {
+ return dataL > dataR;
+ }
+ // fallthrough
+ invert = sortOrder() == Qt::DescendingOrder;
+ }
+ // GH-2722 - sort mod names in a way that discards "The" prefixes
+ case ModFolderModel::NameColumn: {
+ auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
+ RemoveThePrefix(dataL);
+ auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
+ RemoveThePrefix(dataR);
+
+ auto less = dataL.compare(dataR, sortCaseSensitivity());
+ if(less != 0) {
+ return invert ? (less > 0) : (less < 0);
+ }
+ // fallthrough
+ invert = sortOrder() == Qt::DescendingOrder;
+ }
+ // GH-2762 - sort versions by parsing them as versions
+ case ModFolderModel::VersionColumn: {
+ auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
+ auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
+ return invert ? (dataL > dataR) : (dataL < dataR);
+ }
+ default: {
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+ }
+ }
+ }
+};
+
+ModFolderPage::ModFolderPage(
+ BaseInstance *inst,
+ std::shared_ptr<ModFolderModel> mods,
+ QString id,
+ QString iconName,
+ QString displayName,
+ QString helpPage,
+ QWidget *parent
+) :
+ QMainWindow(parent),
+ ui(new Ui::ModFolderPage)
+{
+ ui->setupUi(this);
+ ui->actionsToolbar->insertSpacer(ui->actionView_configs);
+
+ m_inst = inst;
+ on_RunningState_changed(m_inst && m_inst->isRunning());
+ m_mods = mods;
+ m_id = id;
+ m_displayName = displayName;
+ m_iconName = iconName;
+ m_helpName = helpPage;
+ m_fileSelectionFilter = "%1 (*.zip *.jar)";
+ m_filterModel = new ModSortProxy(this);
+ m_filterModel->setDynamicSortFilter(true);
+ m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSourceModel(m_mods.get());
+ m_filterModel->setFilterKeyColumn(-1);
+ ui->modTreeView->setModel(m_filterModel);
+ ui->modTreeView->installEventFilter(this);
+ ui->modTreeView->sortByColumn(1, Qt::AscendingOrder);
+ ui->modTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->modTreeView, &ModListView::customContextMenuRequested, this, &ModFolderPage::ShowContextMenu);
+ connect(ui->modTreeView, &ModListView::activated, this, &ModFolderPage::modItemActivated);
+
+ auto smodel = ui->modTreeView->selectionModel();
+ connect(smodel, &QItemSelectionModel::currentChanged, this, &ModFolderPage::modCurrent);
+ connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged);
+ connect(m_inst, &BaseInstance::runningStatusChanged, this, &ModFolderPage::on_RunningState_changed);
+}
+
+void ModFolderPage::modItemActivated(const QModelIndex&)
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
+ m_mods->setModStatus(selection.indexes(), ModFolderModel::Toggle);
+}
+
+QMenu * ModFolderPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction() );
+ return filteredMenu;
+}
+
+void ModFolderPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->modTreeView->mapToGlobal(pos));
+ delete menu;
+}
+
+void ModFolderPage::openedImpl()
+{
+ m_mods->startWatching();
+}
+
+void ModFolderPage::closedImpl()
+{
+ m_mods->stopWatching();
+}
+
+void ModFolderPage::on_filterTextChanged(const QString& newContents)
+{
+ m_viewFilter = newContents;
+ m_filterModel->setFilterFixedString(m_viewFilter);
+}
+
+
+CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods,
+ QString id, QString iconName, QString displayName,
+ QString helpPage, QWidget *parent)
+ : ModFolderPage(inst, mods, id, iconName, displayName, helpPage, parent)
+{
+}
+
+ModFolderPage::~ModFolderPage()
+{
+ m_mods->stopWatching();
+ delete ui;
+}
+
+void ModFolderPage::on_RunningState_changed(bool running)
+{
+ if(m_controlsEnabled == !running) {
+ return;
+ }
+ m_controlsEnabled = !running;
+ ui->actionAdd->setEnabled(m_controlsEnabled);
+ ui->actionDisable->setEnabled(m_controlsEnabled);
+ ui->actionEnable->setEnabled(m_controlsEnabled);
+ ui->actionRemove->setEnabled(m_controlsEnabled);
+}
+
+bool ModFolderPage::shouldDisplay() const
+{
+ return true;
+}
+
+bool CoreModFolderPage::shouldDisplay() const
+{
+ if (ModFolderPage::shouldDisplay())
+ {
+ auto inst = dynamic_cast<MinecraftInstance *>(m_inst);
+ if (!inst)
+ return true;
+ auto version = inst->getPackProfile();
+ if (!version)
+ return true;
+ if(!version->getComponent("net.minecraftforge"))
+ {
+ return false;
+ }
+ if(!version->getComponent("net.minecraft"))
+ {
+ return false;
+ }
+ if(version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool ModFolderPage::modListFilter(QKeyEvent *keyEvent)
+{
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete:
+ on_actionRemove_triggered();
+ return true;
+ case Qt::Key_Plus:
+ on_actionAdd_triggered();
+ return true;
+ default:
+ break;
+ }
+ return QWidget::eventFilter(ui->modTreeView, keyEvent);
+}
+
+bool ModFolderPage::eventFilter(QObject *obj, QEvent *ev)
+{
+ if (ev->type() != QEvent::KeyPress)
+ {
+ return QWidget::eventFilter(obj, ev);
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
+ if (obj == ui->modTreeView)
+ return modListFilter(keyEvent);
+ return QWidget::eventFilter(obj, ev);
+}
+
+void ModFolderPage::on_actionAdd_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto list = GuiUtil::BrowseForFiles(
+ m_helpName,
+ tr("Select %1",
+ "Select whatever type of files the page contains. Example: 'Loader Mods'")
+ .arg(m_displayName),
+ m_fileSelectionFilter.arg(m_displayName), MMC->settings()->get("CentralModsDir").toString(),
+ this->parentWidget());
+ if (!list.empty())
+ {
+ for (auto filename : list)
+ {
+ m_mods->installMod(filename);
+ }
+ }
+}
+
+void ModFolderPage::on_actionEnable_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
+ m_mods->setModStatus(selection.indexes(), ModFolderModel::Enable);
+}
+
+void ModFolderPage::on_actionDisable_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
+ m_mods->setModStatus(selection.indexes(), ModFolderModel::Disable);
+}
+
+void ModFolderPage::on_actionRemove_triggered()
+{
+ if(!m_controlsEnabled) {
+ return;
+ }
+ auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection());
+ m_mods->deleteMods(selection.indexes());
+}
+
+void ModFolderPage::on_actionView_configs_triggered()
+{
+ DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true);
+}
+
+void ModFolderPage::on_actionView_Folder_triggered()
+{
+ DesktopServices::openDirectory(m_mods->dir().absolutePath(), true);
+}
+
+void ModFolderPage::modCurrent(const QModelIndex &current, const QModelIndex &previous)
+{
+ if (!current.isValid())
+ {
+ ui->frame->clear();
+ return;
+ }
+ auto sourceCurrent = m_filterModel->mapToSource(current);
+ int row = sourceCurrent.row();
+ Mod &m = m_mods->operator[](row);
+ ui->frame->updateWithMod(m);
+}
diff --git a/launcher/pages/instance/ModFolderPage.h b/launcher/pages/instance/ModFolderPage.h
new file mode 100644
index 00000000..f653a8c0
--- /dev/null
+++ b/launcher/pages/instance/ModFolderPage.h
@@ -0,0 +1,119 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMainWindow>
+
+#include "minecraft/MinecraftInstance.h"
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+class ModFolderModel;
+namespace Ui
+{
+class ModFolderPage;
+}
+
+class ModFolderPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ModFolderPage(
+ BaseInstance *inst,
+ std::shared_ptr<ModFolderModel> mods,
+ QString id,
+ QString iconName,
+ QString displayName,
+ QString helpPage = "",
+ QWidget *parent = 0
+ );
+ virtual ~ModFolderPage();
+
+ void setFilter(const QString & filter)
+ {
+ m_fileSelectionFilter = filter;
+ }
+
+ virtual QString displayName() const override
+ {
+ return m_displayName;
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon(m_iconName);
+ }
+ virtual QString id() const override
+ {
+ return m_id;
+ }
+ virtual QString helpPage() const override
+ {
+ return m_helpName;
+ }
+ virtual bool shouldDisplay() const override;
+
+ virtual void openedImpl() override;
+ virtual void closedImpl() override;
+protected:
+ bool eventFilter(QObject *obj, QEvent *ev) override;
+ bool modListFilter(QKeyEvent *ev);
+ QMenu * createPopupMenu() override;
+
+protected:
+ BaseInstance *m_inst = nullptr;
+
+protected:
+ Ui::ModFolderPage *ui = nullptr;
+ std::shared_ptr<ModFolderModel> m_mods;
+ QSortFilterProxyModel *m_filterModel = nullptr;
+ QString m_iconName;
+ QString m_id;
+ QString m_displayName;
+ QString m_helpName;
+ QString m_fileSelectionFilter;
+ QString m_viewFilter;
+ bool m_controlsEnabled = true;
+
+public
+slots:
+ void modCurrent(const QModelIndex &current, const QModelIndex &previous);
+
+private
+slots:
+ void modItemActivated(const QModelIndex &index);
+ void on_filterTextChanged(const QString & newContents);
+ void on_RunningState_changed(bool running);
+ void on_actionAdd_triggered();
+ void on_actionRemove_triggered();
+ void on_actionEnable_triggered();
+ void on_actionDisable_triggered();
+ void on_actionView_Folder_triggered();
+ void on_actionView_configs_triggered();
+ void ShowContextMenu(const QPoint &pos);
+};
+
+class CoreModFolderPage : public ModFolderPage
+{
+public:
+ explicit CoreModFolderPage(BaseInstance *inst, std::shared_ptr<ModFolderModel> mods, QString id,
+ QString iconName, QString displayName, QString helpPage = "",
+ QWidget *parent = 0);
+ virtual ~CoreModFolderPage()
+ {
+ }
+ virtual bool shouldDisplay() const;
+};
diff --git a/launcher/pages/instance/ModFolderPage.ui b/launcher/pages/instance/ModFolderPage.ui
new file mode 100644
index 00000000..954a0167
--- /dev/null
+++ b/launcher/pages/instance/ModFolderPage.ui
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ModFolderPage</class>
+ <widget class="QMainWindow" name="ModFolderPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1042</width>
+ <height>501</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="4" column="1" colspan="3">
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="filterEdit">
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="filterLabel">
+ <property name="text">
+ <string>Filter:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="1" colspan="3">
+ <widget class="MCModInfoFrame" name="frame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1" colspan="3">
+ <widget class="ModListView" name="modTreeView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DropOnly</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="actionsToolbar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAdd"/>
+ <addaction name="separator"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionEnable"/>
+ <addaction name="actionDisable"/>
+ <addaction name="actionView_configs"/>
+ <addaction name="actionView_Folder"/>
+ </widget>
+ <action name="actionAdd">
+ <property name="text">
+ <string>&amp;Add</string>
+ </property>
+ <property name="toolTip">
+ <string>Add mods</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>&amp;Remove</string>
+ </property>
+ <property name="toolTip">
+ <string>Remove selected mods</string>
+ </property>
+ </action>
+ <action name="actionEnable">
+ <property name="text">
+ <string>&amp;Enable</string>
+ </property>
+ <property name="toolTip">
+ <string>Enable selected mods</string>
+ </property>
+ </action>
+ <action name="actionDisable">
+ <property name="text">
+ <string>&amp;Disable</string>
+ </property>
+ <property name="toolTip">
+ <string>Disable selected mods</string>
+ </property>
+ </action>
+ <action name="actionView_configs">
+ <property name="text">
+ <string>View &amp;Configs</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the 'config' folder in the system file manager.</string>
+ </property>
+ </action>
+ <action name="actionView_Folder">
+ <property name="text">
+ <string>View &amp;Folder</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ModListView</class>
+ <extends>QTreeView</extends>
+ <header>widgets/ModListView.h</header>
+ </customwidget>
+ <customwidget>
+ <class>MCModInfoFrame</class>
+ <extends>QFrame</extends>
+ <header>widgets/MCModInfoFrame.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>modTreeView</tabstop>
+ <tabstop>filterEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/NotesPage.cpp b/launcher/pages/instance/NotesPage.cpp
new file mode 100644
index 00000000..fa966c91
--- /dev/null
+++ b/launcher/pages/instance/NotesPage.cpp
@@ -0,0 +1,21 @@
+#include "NotesPage.h"
+#include "ui_NotesPage.h"
+#include <QTabBar>
+
+NotesPage::NotesPage(BaseInstance *inst, QWidget *parent)
+ : QWidget(parent), ui(new Ui::NotesPage), m_inst(inst)
+{
+ ui->setupUi(this);
+ ui->noteEditor->setText(m_inst->notes());
+}
+
+NotesPage::~NotesPage()
+{
+ delete ui;
+}
+
+bool NotesPage::apply()
+{
+ m_inst->setNotes(ui->noteEditor->toPlainText());
+ return true;
+}
diff --git a/launcher/pages/instance/NotesPage.h b/launcher/pages/instance/NotesPage.h
new file mode 100644
index 00000000..d0c00ac1
--- /dev/null
+++ b/launcher/pages/instance/NotesPage.h
@@ -0,0 +1,60 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "BaseInstance.h"
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+namespace Ui
+{
+class NotesPage;
+}
+
+class NotesPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit NotesPage(BaseInstance *inst, QWidget *parent = 0);
+ virtual ~NotesPage();
+ virtual QString displayName() const override
+ {
+ return tr("Notes");
+ }
+ virtual QIcon icon() const override
+ {
+ auto icon = MMC->getThemedIcon("notes");
+ if(icon.isNull())
+ icon = MMC->getThemedIcon("news");
+ return icon;
+ }
+ virtual QString id() const override
+ {
+ return "notes";
+ }
+ virtual bool apply() override;
+ virtual QString helpPage() const override
+ {
+ return "Notes";
+ }
+
+private:
+ Ui::NotesPage *ui;
+ BaseInstance *m_inst;
+};
diff --git a/launcher/pages/instance/NotesPage.ui b/launcher/pages/instance/NotesPage.ui
new file mode 100644
index 00000000..67cb261c
--- /dev/null
+++ b/launcher/pages/instance/NotesPage.ui
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NotesPage</class>
+ <widget class="QWidget" name="NotesPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>731</width>
+ <height>538</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTextEdit" name="noteEditor">
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="tabChangesFocus">
+ <bool>true</bool>
+ </property>
+ <property name="acceptRichText">
+ <bool>false</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>noteEditor</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/OtherLogsPage.cpp b/launcher/pages/instance/OtherLogsPage.cpp
new file mode 100644
index 00000000..b67b84bd
--- /dev/null
+++ b/launcher/pages/instance/OtherLogsPage.cpp
@@ -0,0 +1,313 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "OtherLogsPage.h"
+#include "ui_OtherLogsPage.h"
+
+#include <QMessageBox>
+
+#include "GuiUtil.h"
+#include "RecursiveFileSystemWatcher.h"
+#include <GZip.h>
+#include <FileSystem.h>
+#include <QShortcut>
+
+OtherLogsPage::OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent)
+ : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), m_fileFilter(fileFilter),
+ m_watcher(new RecursiveFileSystemWatcher(this))
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+
+ m_watcher->setMatcher(fileFilter);
+ m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path));
+
+ connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, &OtherLogsPage::populateSelectLogBox);
+ populateSelectLogBox();
+
+ auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this);
+ connect(findShortcut, &QShortcut::activated, this, &OtherLogsPage::findActivated);
+
+ auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this);
+ connect(findNextShortcut, &QShortcut::activated, this, &OtherLogsPage::findNextActivated);
+
+ auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this);
+ connect(findPreviousShortcut, &QShortcut::activated, this, &OtherLogsPage::findPreviousActivated);
+
+ connect(ui->searchBar, &QLineEdit::returnPressed, this, &OtherLogsPage::on_findButton_clicked);
+}
+
+OtherLogsPage::~OtherLogsPage()
+{
+ delete ui;
+}
+
+void OtherLogsPage::openedImpl()
+{
+ m_watcher->enable();
+}
+void OtherLogsPage::closedImpl()
+{
+ m_watcher->disable();
+}
+
+void OtherLogsPage::populateSelectLogBox()
+{
+ ui->selectLogBox->clear();
+ ui->selectLogBox->addItems(m_watcher->files());
+ if (m_currentFile.isEmpty())
+ {
+ setControlsEnabled(false);
+ ui->selectLogBox->setCurrentIndex(-1);
+ }
+ else
+ {
+ const int index = ui->selectLogBox->findText(m_currentFile);
+ if (index != -1)
+ {
+ ui->selectLogBox->setCurrentIndex(index);
+ setControlsEnabled(true);
+ }
+ else
+ {
+ setControlsEnabled(false);
+ }
+ }
+}
+
+void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index)
+{
+ QString file;
+ if (index != -1)
+ {
+ file = ui->selectLogBox->itemText(index);
+ }
+
+ if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_path, file)))
+ {
+ m_currentFile = QString();
+ ui->text->clear();
+ setControlsEnabled(false);
+ }
+ else
+ {
+ m_currentFile = file;
+ on_btnReload_clicked();
+ setControlsEnabled(true);
+ }
+}
+
+void OtherLogsPage::on_btnReload_clicked()
+{
+ if(m_currentFile.isEmpty())
+ {
+ setControlsEnabled(false);
+ return;
+ }
+ QFile file(FS::PathCombine(m_path, m_currentFile));
+ if (!file.open(QFile::ReadOnly))
+ {
+ setControlsEnabled(false);
+ ui->btnReload->setEnabled(true); // allow reload
+ m_currentFile = QString();
+ QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2")
+ .arg(m_currentFile, file.errorString()));
+ }
+ else
+ {
+ auto setPlainText = [&](const QString & text)
+ {
+ QString fontFamily = MMC->settings()->get("ConsoleFont").toString();
+ bool conversionOk = false;
+ int fontSize = MMC->settings()->get("ConsoleFontSize").toInt(&conversionOk);
+ if(!conversionOk)
+ {
+ fontSize = 11;
+ }
+ QTextDocument *doc = ui->text->document();
+ doc->setDefaultFont(QFont(fontFamily, fontSize));
+ ui->text->setPlainText(text);
+ };
+ auto showTooBig = [&]()
+ {
+ setPlainText(
+ tr("The file (%1) is too big. You may want to open it in a viewer optimized "
+ "for large files.").arg(file.fileName()));
+ };
+ if(file.size() > (1024ll * 1024ll * 12ll))
+ {
+ showTooBig();
+ return;
+ }
+ QString content;
+ if(file.fileName().endsWith(".gz"))
+ {
+ QByteArray temp;
+ if(!GZip::unzip(file.readAll(), temp))
+ {
+ setPlainText(
+ tr("The file (%1) is not readable.").arg(file.fileName()));
+ return;
+ }
+ content = QString::fromUtf8(temp);
+ }
+ else
+ {
+ content = QString::fromUtf8(file.readAll());
+ }
+ if (content.size() >= 50000000ll)
+ {
+ showTooBig();
+ return;
+ }
+ setPlainText(content);
+ }
+}
+
+void OtherLogsPage::on_btnPaste_clicked()
+{
+ GuiUtil::uploadPaste(ui->text->toPlainText(), this);
+}
+
+void OtherLogsPage::on_btnCopy_clicked()
+{
+ GuiUtil::setClipboardText(ui->text->toPlainText());
+}
+
+void OtherLogsPage::on_btnDelete_clicked()
+{
+ if(m_currentFile.isEmpty())
+ {
+ setControlsEnabled(false);
+ return;
+ }
+ if (QMessageBox::question(this, tr("Delete"),
+ tr("Do you really want to delete %1?").arg(m_currentFile),
+ QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
+ {
+ return;
+ }
+ QFile file(FS::PathCombine(m_path, m_currentFile));
+ if (!file.remove())
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2")
+ .arg(m_currentFile, file.errorString()));
+ }
+}
+
+
+
+void OtherLogsPage::on_btnClean_clicked()
+{
+ auto toDelete = m_watcher->files();
+ if(toDelete.isEmpty())
+ {
+ return;
+ }
+ QMessageBox *messageBox = new QMessageBox(this);
+ messageBox->setWindowTitle(tr("Clean up"));
+ if(toDelete.size() > 5)
+ {
+ messageBox->setText(tr("Do you really want to delete all log files?"));
+ messageBox->setDetailedText(toDelete.join('\n'));
+ }
+ else
+ {
+ messageBox->setText(tr("Do you really want to delete these files?\n%1").arg(toDelete.join('\n')));
+ }
+ messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
+ messageBox->setDefaultButton(QMessageBox::Ok);
+ messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ messageBox->setIcon(QMessageBox::Question);
+ messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
+
+ if (messageBox->exec() != QMessageBox::Ok)
+ {
+ return;
+ }
+ QStringList failed;
+ for(auto item: toDelete)
+ {
+ QFile file(FS::PathCombine(m_path, item));
+ if (!file.remove())
+ {
+ failed.push_back(item);
+ }
+ }
+ if(!failed.empty())
+ {
+ QMessageBox *messageBox = new QMessageBox(this);
+ messageBox->setWindowTitle(tr("Error"));
+ if(failed.size() > 5)
+ {
+ messageBox->setText(tr("Couldn't delete some files!"));
+ messageBox->setDetailedText(failed.join('\n'));
+ }
+ else
+ {
+ messageBox->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n')));
+ }
+ messageBox->setStandardButtons(QMessageBox::Ok);
+ messageBox->setDefaultButton(QMessageBox::Ok);
+ messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ messageBox->setIcon(QMessageBox::Critical);
+ messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction);
+ messageBox->exec();
+ }
+}
+
+
+void OtherLogsPage::setControlsEnabled(const bool enabled)
+{
+ ui->btnReload->setEnabled(enabled);
+ ui->btnDelete->setEnabled(enabled);
+ ui->btnCopy->setEnabled(enabled);
+ ui->btnPaste->setEnabled(enabled);
+ ui->text->setEnabled(enabled);
+ ui->btnClean->setEnabled(enabled);
+}
+
+// FIXME: HACK, use LogView instead?
+static void findNext(QPlainTextEdit * _this, const QString& what, bool reverse)
+{
+ _this->find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0));
+}
+
+void OtherLogsPage::on_findButton_clicked()
+{
+ auto modifiers = QApplication::keyboardModifiers();
+ bool reverse = modifiers & Qt::ShiftModifier;
+ findNext(ui->text, ui->searchBar->text(), reverse);
+}
+
+void OtherLogsPage::findNextActivated()
+{
+ findNext(ui->text, ui->searchBar->text(), false);
+}
+
+void OtherLogsPage::findPreviousActivated()
+{
+ findNext(ui->text, ui->searchBar->text(), true);
+}
+
+void OtherLogsPage::findActivated()
+{
+ // focus the search bar if it doesn't have focus
+ if (!ui->searchBar->hasFocus())
+ {
+ ui->searchBar->setFocus();
+ ui->searchBar->selectAll();
+ }
+}
diff --git a/launcher/pages/instance/OtherLogsPage.h b/launcher/pages/instance/OtherLogsPage.h
new file mode 100644
index 00000000..7f21c0fa
--- /dev/null
+++ b/launcher/pages/instance/OtherLogsPage.h
@@ -0,0 +1,81 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include <pathmatcher/IPathMatcher.h>
+
+namespace Ui
+{
+class OtherLogsPage;
+}
+
+class RecursiveFileSystemWatcher;
+
+class OtherLogsPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent = 0);
+ ~OtherLogsPage();
+
+ QString id() const override
+ {
+ return "logs";
+ }
+ QString displayName() const override
+ {
+ return tr("Other logs");
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("log");
+ }
+ QString helpPage() const override
+ {
+ return "Minecraft-Logs";
+ }
+ void openedImpl() override;
+ void closedImpl() override;
+
+private slots:
+ void populateSelectLogBox();
+ void on_selectLogBox_currentIndexChanged(const int index);
+ void on_btnReload_clicked();
+ void on_btnPaste_clicked();
+ void on_btnCopy_clicked();
+ void on_btnDelete_clicked();
+ void on_btnClean_clicked();
+
+ void on_findButton_clicked();
+ void findActivated();
+ void findNextActivated();
+ void findPreviousActivated();
+
+private:
+ void setControlsEnabled(const bool enabled);
+
+private:
+ Ui::OtherLogsPage *ui;
+ QString m_path;
+ QString m_currentFile;
+ IPathMatcher::Ptr m_fileFilter;
+ RecursiveFileSystemWatcher *m_watcher;
+};
diff --git a/launcher/pages/instance/OtherLogsPage.ui b/launcher/pages/instance/OtherLogsPage.ui
new file mode 100644
index 00000000..56ff3b62
--- /dev/null
+++ b/launcher/pages/instance/OtherLogsPage.ui
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OtherLogsPage</class>
+ <widget class="QWidget" name="OtherLogsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>657</width>
+ <height>538</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="searchBar"/>
+ </item>
+ <item row="2" column="2">
+ <widget class="QPushButton" name="findButton">
+ <property name="text">
+ <string>Find</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="4">
+ <widget class="QPlainTextEdit" name="text">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="4">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="3" column="1">
+ <widget class="QPushButton" name="btnCopy">
+ <property name="toolTip">
+ <string>Copy the whole log into the clipboard</string>
+ </property>
+ <property name="text">
+ <string>&amp;Copy</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="3">
+ <widget class="QPushButton" name="btnDelete">
+ <property name="toolTip">
+ <string>Clear the log</string>
+ </property>
+ <property name="text">
+ <string>Delete</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="2">
+ <widget class="QPushButton" name="btnPaste">
+ <property name="toolTip">
+ <string>Upload the log to paste.ee - it will stay online for a month</string>
+ </property>
+ <property name="text">
+ <string>Upload</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="4">
+ <widget class="QPushButton" name="btnClean">
+ <property name="toolTip">
+ <string>Clear the log</string>
+ </property>
+ <property name="text">
+ <string>Clean</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QPushButton" name="btnReload">
+ <property name="text">
+ <string>Reload</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="5">
+ <widget class="QComboBox" name="selectLogBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Search:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>selectLogBox</tabstop>
+ <tabstop>btnReload</tabstop>
+ <tabstop>btnCopy</tabstop>
+ <tabstop>btnPaste</tabstop>
+ <tabstop>btnDelete</tabstop>
+ <tabstop>btnClean</tabstop>
+ <tabstop>text</tabstop>
+ <tabstop>searchBar</tabstop>
+ <tabstop>findButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/ResourcePackPage.h b/launcher/pages/instance/ResourcePackPage.h
new file mode 100644
index 00000000..1486bf52
--- /dev/null
+++ b/launcher/pages/instance/ResourcePackPage.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "ModFolderPage.h"
+#include "ui_ModFolderPage.h"
+
+class ResourcePackPage : public ModFolderPage
+{
+ Q_OBJECT
+public:
+ explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0)
+ : ModFolderPage(instance, instance->resourcePackList(), "resourcepacks",
+ "resourcepacks", tr("Resource packs"), "Resource-packs", parent)
+ {
+ ui->actionView_configs->setVisible(false);
+ }
+ virtual ~ResourcePackPage() {}
+
+ virtual bool shouldDisplay() const override
+ {
+ return !m_inst->traits().contains("no-texturepacks") &&
+ !m_inst->traits().contains("texturepacks");
+ }
+};
diff --git a/launcher/pages/instance/ScreenshotsPage.cpp b/launcher/pages/instance/ScreenshotsPage.cpp
new file mode 100644
index 00000000..efa0f9f2
--- /dev/null
+++ b/launcher/pages/instance/ScreenshotsPage.cpp
@@ -0,0 +1,422 @@
+#include "ScreenshotsPage.h"
+#include "ui_ScreenshotsPage.h"
+
+#include <QModelIndex>
+#include <QMutableListIterator>
+#include <QMap>
+#include <QSet>
+#include <QFileIconProvider>
+#include <QFileSystemModel>
+#include <QStyledItemDelegate>
+#include <QLineEdit>
+#include <QEvent>
+#include <QPainter>
+#include <QClipboard>
+#include <QKeyEvent>
+#include <QMenu>
+
+#include <MultiMC.h>
+
+#include "dialogs/ProgressDialog.h"
+#include "dialogs/CustomMessageBox.h"
+#include "net/NetJob.h"
+#include "screenshots/ImgurUpload.h"
+#include "screenshots/ImgurAlbumCreation.h"
+#include "tasks/SequentialTask.h"
+
+#include "RWStorage.h"
+#include <FileSystem.h>
+#include <DesktopServices.h>
+
+typedef RWStorage<QString, QIcon> SharedIconCache;
+typedef std::shared_ptr<SharedIconCache> SharedIconCachePtr;
+
+class ThumbnailingResult : public QObject
+{
+ Q_OBJECT
+public slots:
+ inline void emitResultsReady(const QString &path) { emit resultsReady(path); }
+ inline void emitResultsFailed(const QString &path) { emit resultsFailed(path); }
+signals:
+ void resultsReady(const QString &path);
+ void resultsFailed(const QString &path);
+};
+
+class ThumbnailRunnable : public QRunnable
+{
+public:
+ ThumbnailRunnable(QString path, SharedIconCachePtr cache)
+ {
+ m_path = path;
+ m_cache = cache;
+ }
+ void run()
+ {
+ QFileInfo info(m_path);
+ if (info.isDir())
+ return;
+ if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0))
+ return;
+ int tries = 5;
+ while (tries)
+ {
+ if (!m_cache->stale(m_path))
+ return;
+ QImage image(m_path);
+ if (image.isNull())
+ {
+ QThread::msleep(500);
+ tries--;
+ continue;
+ }
+ QImage small;
+ if (image.width() > image.height())
+ small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
+ else
+ small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
+ QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
+ QImage square(QSize(256, 256), QImage::Format_ARGB32);
+ square.fill(Qt::transparent);
+
+ QPainter painter(&square);
+ painter.drawImage(offset, small);
+ painter.end();
+
+ QIcon icon(QPixmap::fromImage(square));
+ m_cache->add(m_path, icon);
+ m_resultEmitter.emitResultsReady(m_path);
+ return;
+ }
+ m_resultEmitter.emitResultsFailed(m_path);
+ }
+ QString m_path;
+ SharedIconCachePtr m_cache;
+ ThumbnailingResult m_resultEmitter;
+};
+
+// this is about as elegant and well written as a bag of bricks with scribbles done by insane
+// asylum patients.
+class FilterModel : public QIdentityProxyModel
+{
+ Q_OBJECT
+public:
+ explicit FilterModel(QObject *parent = 0) : QIdentityProxyModel(parent)
+ {
+ m_thumbnailingPool.setMaxThreadCount(4);
+ m_thumbnailCache = std::make_shared<SharedIconCache>();
+ m_thumbnailCache->add("placeholder", MMC->getThemedIcon("screenshot-placeholder"));
+ connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
+ // FIXME: the watched file set is not updated when files are removed
+ }
+ virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); }
+ virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const
+ {
+ auto model = sourceModel();
+ if (!model)
+ return QVariant();
+ if (role == Qt::DisplayRole || role == Qt::EditRole)
+ {
+ QVariant result = sourceModel()->data(mapToSource(proxyIndex), role);
+ return result.toString().remove(QRegExp("\\.png$"));
+ }
+ if (role == Qt::DecorationRole)
+ {
+ QVariant result =
+ sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole);
+ QString filePath = result.toString();
+ QIcon temp;
+ if (!watched.contains(filePath))
+ {
+ ((QFileSystemWatcher &)watcher).addPath(filePath);
+ ((QSet<QString> &)watched).insert(filePath);
+ }
+ if (m_thumbnailCache->get(filePath, temp))
+ {
+ return temp;
+ }
+ if (!m_failed.contains(filePath))
+ {
+ ((FilterModel *)this)->thumbnailImage(filePath);
+ }
+ return (m_thumbnailCache->get("placeholder"));
+ }
+ return sourceModel()->data(mapToSource(proxyIndex), role);
+ }
+ virtual bool setData(const QModelIndex &index, const QVariant &value,
+ int role = Qt::EditRole)
+ {
+ auto model = sourceModel();
+ if (!model)
+ return false;
+ if (role != Qt::EditRole)
+ return false;
+ // FIXME: this is a workaround for a bug in QFileSystemModel, where it doesn't
+ // sort after renames
+ {
+ ((QFileSystemModel *)model)->setNameFilterDisables(true);
+ ((QFileSystemModel *)model)->setNameFilterDisables(false);
+ }
+ return model->setData(mapToSource(index), value.toString() + ".png", role);
+ }
+
+private:
+ void thumbnailImage(QString path)
+ {
+ auto runnable = new ThumbnailRunnable(path, m_thumbnailCache);
+ connect(&(runnable->m_resultEmitter), SIGNAL(resultsReady(QString)),
+ SLOT(thumbnailReady(QString)));
+ connect(&(runnable->m_resultEmitter), SIGNAL(resultsFailed(QString)),
+ SLOT(thumbnailFailed(QString)));
+ ((QThreadPool &)m_thumbnailingPool).start(runnable);
+ }
+private slots:
+ void thumbnailReady(QString path) { emit layoutChanged(); }
+ void thumbnailFailed(QString path) { m_failed.insert(path); }
+ void fileChanged(QString filepath)
+ {
+ m_thumbnailCache->setStale(filepath);
+ thumbnailImage(filepath);
+ // reinsert the path...
+ watcher.removePath(filepath);
+ watcher.addPath(filepath);
+ }
+
+private:
+ SharedIconCachePtr m_thumbnailCache;
+ QThreadPool m_thumbnailingPool;
+ QSet<QString> m_failed;
+ QSet<QString> watched;
+ QFileSystemWatcher watcher;
+};
+
+class CenteredEditingDelegate : public QStyledItemDelegate
+{
+public:
+ explicit CenteredEditingDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {}
+ virtual ~CenteredEditingDelegate() {}
+ virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+ {
+ auto widget = QStyledItemDelegate::createEditor(parent, option, index);
+ auto foo = dynamic_cast<QLineEdit *>(widget);
+ if (foo)
+ {
+ foo->setAlignment(Qt::AlignHCenter);
+ foo->setFrame(true);
+ foo->setMaximumWidth(192);
+ }
+ return widget;
+ }
+};
+
+ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent)
+ : QMainWindow(parent), ui(new Ui::ScreenshotsPage)
+{
+ m_model.reset(new QFileSystemModel());
+ m_filterModel.reset(new FilterModel());
+ m_filterModel->setSourceModel(m_model.get());
+ m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable);
+ m_model->setReadOnly(false);
+ m_model->setNameFilters({"*.png"});
+ m_model->setNameFilterDisables(false);
+ m_folder = path;
+ m_valid = FS::ensureFolderPathExists(m_folder);
+
+ ui->setupUi(this);
+ ui->toolBar->insertSpacer(ui->actionView_Folder);
+
+ ui->listView->setIconSize(QSize(128, 128));
+ ui->listView->setGridSize(QSize(192, 160));
+ ui->listView->setSpacing(9);
+ // ui->listView->setUniformItemSizes(true);
+ ui->listView->setLayoutMode(QListView::Batched);
+ ui->listView->setViewMode(QListView::IconMode);
+ ui->listView->setResizeMode(QListView::Adjust);
+ ui->listView->installEventFilter(this);
+ ui->listView->setEditTriggers(0);
+ ui->listView->setItemDelegate(new CenteredEditingDelegate(this));
+ ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu);
+ connect(ui->listView, SIGNAL(activated(QModelIndex)), SLOT(onItemActivated(QModelIndex)));
+}
+
+bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt)
+{
+ if (obj != ui->listView)
+ return QWidget::eventFilter(obj, evt);
+ if (evt->type() != QEvent::KeyPress)
+ {
+ return QWidget::eventFilter(obj, evt);
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt);
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete:
+ on_actionDelete_triggered();
+ return true;
+ case Qt::Key_F2:
+ on_actionRename_triggered();
+ return true;
+ default:
+ break;
+ }
+ return QWidget::eventFilter(obj, evt);
+}
+
+ScreenshotsPage::~ScreenshotsPage()
+{
+ delete ui;
+}
+
+void ScreenshotsPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->listView->mapToGlobal(pos));
+ delete menu;
+}
+
+QMenu * ScreenshotsPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+void ScreenshotsPage::onItemActivated(QModelIndex index)
+{
+ if (!index.isValid())
+ return;
+ auto info = m_model->fileInfo(index);
+ QString fileName = info.absoluteFilePath();
+ DesktopServices::openFile(info.absoluteFilePath());
+}
+
+void ScreenshotsPage::on_actionView_Folder_triggered()
+{
+ DesktopServices::openDirectory(m_folder, true);
+}
+
+void ScreenshotsPage::on_actionUpload_triggered()
+{
+ auto selection = ui->listView->selectionModel()->selectedRows();
+ if (selection.isEmpty())
+ return;
+
+ QList<ScreenshotPtr> uploaded;
+ auto job = NetJobPtr(new NetJob("Screenshot Upload"));
+ if(selection.size() < 2)
+ {
+ auto item = selection.at(0);
+ auto info = m_model->fileInfo(item);
+ auto screenshot = std::make_shared<ScreenShot>(info);
+ job->addNetAction(ImgurUpload::make(screenshot));
+
+ m_uploadActive = true;
+ ProgressDialog dialog(this);
+ if(dialog.execWithTask(job.get()) != QDialog::Accepted)
+ {
+ CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"),
+ tr("Unknown error"), QMessageBox::Warning)->exec();
+ }
+ else
+ {
+ auto link = screenshot->m_url;
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText(link);
+ CustomMessageBox::selectable(
+ this,
+ tr("Upload finished"),
+ tr("The <a href=\"%1\">link to the uploaded screenshot</a> has been placed in your clipboard.")
+ .arg(link),
+ QMessageBox::Information
+ )->exec();
+ }
+
+ m_uploadActive = false;
+ return;
+ }
+
+ for (auto item : selection)
+ {
+ auto info = m_model->fileInfo(item);
+ auto screenshot = std::make_shared<ScreenShot>(info);
+ uploaded.push_back(screenshot);
+ job->addNetAction(ImgurUpload::make(screenshot));
+ }
+ SequentialTask task;
+ auto albumTask = NetJobPtr(new NetJob("Imgur Album Creation"));
+ auto imgurAlbum = ImgurAlbumCreation::make(uploaded);
+ albumTask->addNetAction(imgurAlbum);
+ task.addTask(job.unwrap());
+ task.addTask(albumTask.unwrap());
+ m_uploadActive = true;
+ ProgressDialog prog(this);
+ if (prog.execWithTask(&task) != QDialog::Accepted)
+ {
+ CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"),
+ tr("Unknown error"), QMessageBox::Warning)->exec();
+ }
+ else
+ {
+ auto link = QString("https://imgur.com/a/%1").arg(imgurAlbum->id());
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText(link);
+ CustomMessageBox::selectable(
+ this,
+ tr("Upload finished"),
+ tr("The <a href=\"%1\">link to the uploaded album</a> has been placed in your clipboard.") .arg(link),
+ QMessageBox::Information
+ )->exec();
+ }
+ m_uploadActive = false;
+}
+
+void ScreenshotsPage::on_actionDelete_triggered()
+{
+ auto mbox = CustomMessageBox::selectable(
+ this, tr("Are you sure?"), tr("This will delete all selected screenshots."),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No);
+ std::unique_ptr<QMessageBox> box(mbox);
+
+ if (box->exec() != QMessageBox::Yes)
+ return;
+
+ auto selected = ui->listView->selectionModel()->selectedIndexes();
+ for (auto item : selected)
+ {
+ m_model->remove(item);
+ }
+}
+
+void ScreenshotsPage::on_actionRename_triggered()
+{
+ auto selection = ui->listView->selectionModel()->selectedIndexes();
+ if (selection.isEmpty())
+ return;
+ ui->listView->edit(selection[0]);
+ // TODO: mass renaming
+}
+
+void ScreenshotsPage::openedImpl()
+{
+ if(!m_valid)
+ {
+ m_valid = FS::ensureFolderPathExists(m_folder);
+ }
+ if (m_valid)
+ {
+ QString path = QDir(m_folder).absolutePath();
+ auto idx = m_model->setRootPath(path);
+ if(idx.isValid())
+ {
+ ui->listView->setModel(m_filterModel.get());
+ ui->listView->setRootIndex(m_filterModel->mapFromSource(idx));
+ }
+ else
+ {
+ ui->listView->setModel(nullptr);
+ }
+ }
+}
+
+#include "ScreenshotsPage.moc"
diff --git a/launcher/pages/instance/ScreenshotsPage.h b/launcher/pages/instance/ScreenshotsPage.h
new file mode 100644
index 00000000..03a809de
--- /dev/null
+++ b/launcher/pages/instance/ScreenshotsPage.h
@@ -0,0 +1,89 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMainWindow>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+class QFileSystemModel;
+class QIdentityProxyModel;
+namespace Ui
+{
+class ScreenshotsPage;
+}
+
+struct ScreenShot;
+class ScreenshotList;
+class ImgurAlbumCreation;
+
+class ScreenshotsPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ScreenshotsPage(QString path, QWidget *parent = 0);
+ virtual ~ScreenshotsPage();
+
+ virtual void openedImpl() override;
+
+ enum
+ {
+ NothingDone = 0x42
+ };
+
+ virtual bool eventFilter(QObject *, QEvent *) override;
+ virtual QString displayName() const override
+ {
+ return tr("Screenshots");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("screenshots");
+ }
+ virtual QString id() const override
+ {
+ return "screenshots";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Screenshots-management";
+ }
+ virtual bool apply() override
+ {
+ return !m_uploadActive;
+ }
+
+protected:
+ QMenu * createPopupMenu() override;
+
+private slots:
+ void on_actionUpload_triggered();
+ void on_actionDelete_triggered();
+ void on_actionRename_triggered();
+ void on_actionView_Folder_triggered();
+ void onItemActivated(QModelIndex);
+ void ShowContextMenu(const QPoint &pos);
+
+private:
+ Ui::ScreenshotsPage *ui;
+ std::shared_ptr<QFileSystemModel> m_model;
+ std::shared_ptr<QIdentityProxyModel> m_filterModel;
+ QString m_folder;
+ bool m_valid = false;
+ bool m_uploadActive = false;
+};
diff --git a/launcher/pages/instance/ScreenshotsPage.ui b/launcher/pages/instance/ScreenshotsPage.ui
new file mode 100644
index 00000000..f11f4cd4
--- /dev/null
+++ b/launcher/pages/instance/ScreenshotsPage.ui
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ScreenshotsPage</class>
+ <widget class="QMainWindow" name="ScreenshotsPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QListView" name="listView">
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionUpload"/>
+ <addaction name="actionDelete"/>
+ <addaction name="actionRename"/>
+ <addaction name="actionView_Folder"/>
+ </widget>
+ <action name="actionUpload">
+ <property name="text">
+ <string>Upload</string>
+ </property>
+ </action>
+ <action name="actionDelete">
+ <property name="text">
+ <string>Delete</string>
+ </property>
+ </action>
+ <action name="actionRename">
+ <property name="text">
+ <string>Rename</string>
+ </property>
+ </action>
+ <action name="actionView_Folder">
+ <property name="text">
+ <string>View Folder</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/ServersPage.cpp b/launcher/pages/instance/ServersPage.cpp
new file mode 100644
index 00000000..d63c6e70
--- /dev/null
+++ b/launcher/pages/instance/ServersPage.cpp
@@ -0,0 +1,768 @@
+#include "ServersPage.h"
+#include "ui_ServersPage.h"
+
+#include <FileSystem.h>
+#include <sstream>
+#include <io/stream_reader.h>
+#include <tag_string.h>
+#include <tag_primitive.h>
+#include <tag_list.h>
+#include <tag_compound.h>
+#include <minecraft/MinecraftInstance.h>
+
+#include <QFileSystemWatcher>
+#include <QMenu>
+
+static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things.
+
+struct Server
+{
+ // Types
+ enum class AcceptsTextures : int
+ {
+ ASK = 0,
+ ALWAYS = 1,
+ NEVER = 2
+ };
+
+ // Methods
+ Server()
+ {
+ m_name = QObject::tr("Minecraft Server");
+ }
+ Server(const QString & name, const QString & address)
+ {
+ m_name = name;
+ m_address = address;
+ }
+ Server(nbt::tag_compound& server)
+ {
+ std::string addressStr(server["ip"]);
+ m_address = QString::fromUtf8(addressStr.c_str());
+
+ std::string nameStr(server["name"]);
+ m_name = QString::fromUtf8(nameStr.c_str());
+
+ if(server["icon"])
+ {
+ std::string base64str(server["icon"]);
+ m_icon = QByteArray::fromBase64(base64str.c_str());
+ }
+
+ if(server.has_key("acceptTextures", nbt::tag_type::Byte))
+ {
+ bool value = server["acceptTextures"].as<nbt::tag_byte>().get();
+ if(value)
+ {
+ m_acceptsTextures = AcceptsTextures::ALWAYS;
+ }
+ else
+ {
+ m_acceptsTextures = AcceptsTextures::NEVER;
+ }
+ }
+ }
+
+ void serialize(nbt::tag_compound& server)
+ {
+ server.insert("name", m_name.trimmed().toUtf8().toStdString());
+ server.insert("ip", m_address.trimmed().toUtf8().toStdString());
+ if(m_icon.size())
+ {
+ server.insert("icon", m_icon.toBase64().toStdString());
+ }
+ if(m_acceptsTextures != AcceptsTextures::ASK)
+ {
+ server.insert("acceptTextures", nbt::tag_byte(m_acceptsTextures == AcceptsTextures::ALWAYS));
+ }
+ }
+
+ // Data - persistent and user changeable
+ QString m_name;
+ QString m_address;
+ AcceptsTextures m_acceptsTextures = AcceptsTextures::ASK;
+
+ // Data - persistent and automatically updated
+ QByteArray m_icon;
+
+ // Data - temporary
+ bool m_checked = false;
+ bool m_up = false;
+ QString m_motd; // https://mctools.org/motd-creator
+ int m_ping = 0;
+ int m_currentPlayers = 0;
+ int m_maxPlayers = 0;
+};
+
+static std::unique_ptr <nbt::tag_compound> parseServersDat(const QString& filename)
+{
+ try
+ {
+ QByteArray input = FS::read(filename);
+ std::istringstream foo(std::string(input.constData(), input.size()));
+ auto pair = nbt::io::read_compound(foo);
+
+ if(pair.first != "")
+ return nullptr;
+
+ if(pair.second == nullptr)
+ return nullptr;
+
+ return std::move(pair.second);
+ }
+ catch (...)
+ {
+ return nullptr;
+ }
+}
+
+static bool serializeServerDat(const QString& filename, nbt::tag_compound * levelInfo)
+{
+ try
+ {
+ if(!FS::ensureFilePathExists(filename))
+ {
+ return false;
+ }
+ std::ostringstream s;
+ nbt::io::write_tag("", *levelInfo, s);
+ QByteArray val(s.str().data(), (int) s.str().size() );
+ FS::write(filename, val);
+ return true;
+ }
+ catch (...)
+ {
+ return false;
+ }
+}
+
+class ServersModel: public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Roles
+ {
+ ServerPtrRole = Qt::UserRole,
+ };
+ explicit ServersModel(const QString &path, QObject *parent = 0)
+ : QAbstractListModel(parent)
+ {
+ m_path = path;
+ m_watcher = new QFileSystemWatcher(this);
+ connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ServersModel::fileChanged);
+ connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &ServersModel::dirChanged);
+ m_saveTimer.setSingleShot(true);
+ m_saveTimer.setInterval(5000);
+ connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal);
+ }
+ virtual ~ServersModel() {};
+
+ void observe()
+ {
+ if(m_observed)
+ {
+ return;
+ }
+ m_observed = true;
+
+ if(!m_loaded)
+ {
+ load();
+ }
+
+ updateFSObserver();
+ }
+
+ void unobserve()
+ {
+ if(!m_observed)
+ {
+ return;
+ }
+ m_observed = false;
+
+ updateFSObserver();
+ }
+
+ void lock()
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ saveNow();
+
+ m_locked = true;
+ updateFSObserver();
+ }
+
+ void unlock()
+ {
+ if(!m_locked)
+ {
+ return;
+ }
+ m_locked = false;
+
+ updateFSObserver();
+ }
+
+ int addEmptyRow(int position)
+ {
+ if(m_locked)
+ {
+ return -1;
+ }
+ if(position < 0 || position >= rowCount())
+ {
+ position = rowCount();
+ }
+ beginInsertRows(QModelIndex(), position, position);
+ m_servers.insert(position, Server());
+ endInsertRows();
+ scheduleSave();
+ return position;
+ }
+
+ bool removeRow(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ if(row < 0 || row >= rowCount())
+ {
+ return false;
+ }
+ beginRemoveRows(QModelIndex(), row, row);
+ m_servers.removeAt(row);
+ endRemoveRows(); // does absolutely nothing, the selected server stays as the next line...
+ scheduleSave();
+ return true;
+ }
+
+ bool moveUp(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ if(row <= 0)
+ {
+ return false;
+ }
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1);
+ m_servers.swap(row-1, row);
+ endMoveRows();
+ scheduleSave();
+ return true;
+ }
+
+ bool moveDown(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ int count = rowCount();
+ if(row + 1 >= count)
+ {
+ return false;
+ }
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2);
+ m_servers.swap(row+1, row);
+ endMoveRows();
+ scheduleSave();
+ return true;
+ }
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override
+ {
+ if (section < 0 || section >= COLUMN_COUNT)
+ return QVariant();
+
+ if(role == Qt::DisplayRole)
+ {
+ switch(section)
+ {
+ case 0:
+ return tr("Name");
+ case 1:
+ return tr("Address");
+ case 2:
+ return tr("Latency");
+ }
+ }
+
+ return QAbstractListModel::headerData(section, orientation, role);
+ }
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
+ {
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+ if(column < 0 || column >= COLUMN_COUNT)
+ return QVariant();
+
+ if (row < 0 || row >= m_servers.size())
+ return QVariant();
+
+ switch(column)
+ {
+ case 0:
+ switch (role)
+ {
+ case Qt::DecorationRole:
+ {
+ auto & bytes = m_servers[row].m_icon;
+ if(bytes.size())
+ {
+ QPixmap px;
+ if(px.loadFromData(bytes))
+ return QIcon(px);
+ }
+ return MMC->getThemedIcon("unknown_server");
+ }
+ case Qt::DisplayRole:
+ return m_servers[row].m_name;
+ case ServerPtrRole:
+ return QVariant::fromValue<void *>((void *)&m_servers[row]);
+ default:
+ return QVariant();
+ }
+ case 1:
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return m_servers[row].m_address;
+ default:
+ return QVariant();
+ }
+ case 2:
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return m_servers[row].m_ping;
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ return m_servers.size();
+ }
+ int columnCount(const QModelIndex & parent) const override
+ {
+ return COLUMN_COUNT;
+ }
+
+ Server * at(int index)
+ {
+ if(index < 0 || index >= rowCount())
+ {
+ return nullptr;
+ }
+ return &m_servers[index];
+ }
+
+ void setName(int row, const QString & name)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_name == name)
+ {
+ return;
+ }
+ server->m_name = name;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void setAddress(int row, const QString & address)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_address == address)
+ {
+ return;
+ }
+ server->m_address = address;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void setAcceptsTextures(int row, Server::AcceptsTextures textures)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_acceptsTextures == textures)
+ {
+ return;
+ }
+ server->m_acceptsTextures = textures;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void load()
+ {
+ cancelSave();
+ beginResetModel();
+ QList<Server> servers;
+ auto serversDat = parseServersDat(serversPath());
+ if(serversDat)
+ {
+ auto &serversList = serversDat->at("servers").as<nbt::tag_list>();
+ for(auto iter = serversList.begin(); iter != serversList.end(); iter++)
+ {
+ auto & serverTag = (*iter).as<nbt::tag_compound>();
+ Server s(serverTag);
+ servers.append(s);
+ }
+ }
+ m_servers.swap(servers);
+ m_loaded = true;
+ endResetModel();
+ }
+
+ void saveNow()
+ {
+ if(saveIsScheduled())
+ {
+ save_internal();
+ }
+ }
+
+
+public slots:
+ void dirChanged(const QString& path)
+ {
+ qDebug() << "Changed:" << path;
+ load();
+ }
+ void fileChanged(const QString& path)
+ {
+ qDebug() << "Changed:" << path;
+ }
+
+private slots:
+ void save_internal()
+ {
+ cancelSave();
+ QString path = serversPath();
+ qDebug() << "Server list about to be saved to" << path;
+
+ nbt::tag_compound out;
+ nbt::tag_list list;
+ for(auto & server: m_servers)
+ {
+ nbt::tag_compound serverNbt;
+ server.serialize(serverNbt);
+ list.push_back(std::move(serverNbt));
+ }
+ out.insert("servers", nbt::value(std::move(list)));
+
+ if(!serializeServerDat(path, &out))
+ {
+ qDebug() << "Failed to save server list:" << path << "Will try again.";
+ scheduleSave();
+ }
+ }
+
+private:
+ void scheduleSave()
+ {
+ if(!m_loaded)
+ {
+ qDebug() << "Server list should never save if it didn't successfully load, path:" << m_path;
+ return;
+ }
+ if(!m_dirty)
+ {
+ m_dirty = true;
+ qDebug() << "Server list save is scheduled for" << m_path;
+ }
+ m_saveTimer.start();
+ }
+
+ void cancelSave()
+ {
+ m_dirty = false;
+ m_saveTimer.stop();
+ }
+
+ bool saveIsScheduled() const
+ {
+ return m_dirty;
+ }
+
+ void updateFSObserver()
+ {
+ bool observingFS = m_watcher->directories().contains(m_path);
+ if(m_observed && m_locked)
+ {
+ if(!observingFS)
+ {
+ qWarning() << "Will watch" << m_path;
+ if(!m_watcher->addPath(m_path))
+ {
+ qWarning() << "Failed to start watching" << m_path;
+ }
+ }
+ }
+ else
+ {
+ if(observingFS)
+ {
+ qWarning() << "Will stop watching" << m_path;
+ if(!m_watcher->removePath(m_path))
+ {
+ qWarning() << "Failed to stop watching" << m_path;
+ }
+ }
+ }
+ }
+
+ QString serversPath()
+ {
+ QFileInfo foo(FS::PathCombine(m_path, "servers.dat"));
+ return foo.filePath();
+ }
+
+private:
+ bool m_loaded = false;
+ bool m_locked = false;
+ bool m_observed = false;
+ bool m_dirty = false;
+ QString m_path;
+ QList<Server> m_servers;
+ QFileSystemWatcher *m_watcher = nullptr;
+ QTimer m_saveTimer;
+};
+
+ServersPage::ServersPage(InstancePtr inst, QWidget* parent)
+ : QMainWindow(parent), ui(new Ui::ServersPage)
+{
+ ui->setupUi(this);
+ m_inst = inst;
+ m_model = new ServersModel(inst->gameRoot(), this);
+ ui->serversView->setIconSize(QSize(64,64));
+ ui->serversView->setModel(m_model);
+ ui->serversView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->serversView, &QTreeView::customContextMenuRequested, this, &ServersPage::ShowContextMenu);
+
+ auto head = ui->serversView->header();
+ if(head->count())
+ {
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ for(int i = 1; i < head->count(); i++)
+ {
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+ }
+
+ auto selectionModel = ui->serversView->selectionModel();
+ connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged);
+ connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed);
+ connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited);
+ connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited);
+ connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int)));
+ connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved);
+
+ m_locked = m_inst->isRunning();
+ if(m_locked)
+ {
+ m_model->lock();
+ }
+
+ updateState();
+}
+
+ServersPage::~ServersPage()
+{
+ m_model->saveNow();
+ delete ui;
+}
+
+void ServersPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->serversView->mapToGlobal(pos));
+ delete menu;
+}
+
+QMenu * ServersPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+void ServersPage::on_RunningState_changed(bool running)
+{
+ if(m_locked == running)
+ {
+ return;
+ }
+ m_locked = running;
+ if(m_locked)
+ {
+ m_model->lock();
+ }
+ else
+ {
+ m_model->unlock();
+ }
+ updateState();
+}
+
+void ServersPage::currentChanged(const QModelIndex &current, const QModelIndex &previous)
+{
+ int nextServer = -1;
+ if (!current.isValid())
+ {
+ nextServer = -1;
+ }
+ else
+ {
+ nextServer = current.row();
+ }
+ currentServer = nextServer;
+ updateState();
+}
+
+// WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal.
+void ServersPage::rowsRemoved(const QModelIndex& parent, int first, int last)
+{
+ if(currentServer < first)
+ {
+ // current was before the removal
+ return;
+ }
+ else if(currentServer >= first && currentServer <= last)
+ {
+ // current got removed...
+ return;
+ }
+ else
+ {
+ // current was past the removal
+ int count = last - first + 1;
+ currentServer -= count;
+ }
+}
+
+void ServersPage::nameEdited(const QString& name)
+{
+ m_model->setName(currentServer, name);
+}
+
+void ServersPage::addressEdited(const QString& address)
+{
+ m_model->setAddress(currentServer, address);
+}
+
+void ServersPage::resourceIndexChanged(int index)
+{
+ auto acceptsTextures = Server::AcceptsTextures(index);
+ m_model->setAcceptsTextures(currentServer, acceptsTextures);
+}
+
+void ServersPage::updateState()
+{
+ auto server = m_model->at(currentServer);
+
+ bool serverEditEnabled = server && !m_locked;
+ ui->addressLine->setEnabled(serverEditEnabled);
+ ui->nameLine->setEnabled(serverEditEnabled);
+ ui->resourceComboBox->setEnabled(serverEditEnabled);
+ ui->actionMove_Down->setEnabled(serverEditEnabled);
+ ui->actionMove_Up->setEnabled(serverEditEnabled);
+ ui->actionRemove->setEnabled(serverEditEnabled);
+ ui->actionJoin->setEnabled(serverEditEnabled);
+
+ if(server)
+ {
+ ui->addressLine->setText(server->m_address);
+ ui->nameLine->setText(server->m_name);
+ ui->resourceComboBox->setCurrentIndex(int(server->m_acceptsTextures));
+ }
+ else
+ {
+ ui->addressLine->setText(QString());
+ ui->nameLine->setText(QString());
+ ui->resourceComboBox->setCurrentIndex(0);
+ }
+
+ ui->actionAdd->setDisabled(m_locked);
+}
+
+void ServersPage::openedImpl()
+{
+ m_model->observe();
+}
+
+void ServersPage::closedImpl()
+{
+ m_model->unobserve();
+}
+
+void ServersPage::on_actionAdd_triggered()
+{
+ int position = m_model->addEmptyRow(currentServer + 1);
+ if(position < 0)
+ {
+ return;
+ }
+ // select the new row
+ ui->serversView->selectionModel()->setCurrentIndex(
+ m_model->index(position),
+ QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear | QItemSelectionModel::Rows
+ );
+ currentServer = position;
+}
+
+void ServersPage::on_actionRemove_triggered()
+{
+ m_model->removeRow(currentServer);
+}
+
+void ServersPage::on_actionMove_Up_triggered()
+{
+ if(m_model->moveUp(currentServer))
+ {
+ currentServer --;
+ }
+}
+
+void ServersPage::on_actionMove_Down_triggered()
+{
+ if(m_model->moveDown(currentServer))
+ {
+ currentServer ++;
+ }
+}
+
+void ServersPage::on_actionJoin_triggered()
+{
+ const auto &address = m_model->at(currentServer)->m_address;
+ MMC->launch(m_inst, true, nullptr, std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(address)));
+}
+
+#include "ServersPage.moc"
diff --git a/launcher/pages/instance/ServersPage.h b/launcher/pages/instance/ServersPage.h
new file mode 100644
index 00000000..8c5b7eb8
--- /dev/null
+++ b/launcher/pages/instance/ServersPage.h
@@ -0,0 +1,94 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMainWindow>
+#include <QString>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+namespace Ui
+{
+class ServersPage;
+}
+
+struct Server;
+class ServersModel;
+class MinecraftInstance;
+
+class ServersPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ServersPage(InstancePtr inst, QWidget *parent = 0);
+ virtual ~ServersPage();
+
+ void openedImpl() override;
+ void closedImpl() override;
+
+ virtual QString displayName() const override
+ {
+ return tr("Servers");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("unknown_server");
+ }
+ virtual QString id() const override
+ {
+ return "servers";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Servers-management";
+ }
+
+protected:
+ QMenu * createPopupMenu() override;
+
+private:
+ void updateState();
+ void scheduleSave();
+ bool saveIsScheduled() const;
+
+private slots:
+ void currentChanged(const QModelIndex &current, const QModelIndex &previous);
+ void rowsRemoved(const QModelIndex &parent, int first, int last);
+
+ void on_actionAdd_triggered();
+ void on_actionRemove_triggered();
+ void on_actionMove_Up_triggered();
+ void on_actionMove_Down_triggered();
+ void on_actionJoin_triggered();
+
+ void on_RunningState_changed(bool running);
+
+ void nameEdited(const QString & name);
+ void addressEdited(const QString & address);
+ void resourceIndexChanged(int index);\
+
+ void ShowContextMenu(const QPoint &pos);
+
+private: // data
+ int currentServer = -1;
+ bool m_locked = true;
+ Ui::ServersPage *ui = nullptr;
+ ServersModel * m_model = nullptr;
+ InstancePtr m_inst = nullptr;
+};
+
diff --git a/launcher/pages/instance/ServersPage.ui b/launcher/pages/instance/ServersPage.ui
new file mode 100644
index 00000000..d89b7cba
--- /dev/null
+++ b/launcher/pages/instance/ServersPage.ui
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ServersPage</class>
+ <widget class="QMainWindow" name="ServersPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1318</width>
+ <height>879</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTreeView" name="serversView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name</string>
+ </property>
+ <property name="buddy">
+ <cstring>nameLine</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="nameLine"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="addressLabel">
+ <property name="text">
+ <string>Address</string>
+ </property>
+ <property name="buddy">
+ <cstring>addressLine</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="addressLine"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="resourcesLabel">
+ <property name="text">
+ <string>Reso&amp;urces</string>
+ </property>
+ <property name="buddy">
+ <cstring>resourceComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="resourceComboBox">
+ <item>
+ <property name="text">
+ <string>Ask to download</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Always download</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Never download</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAdd"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionMove_Up"/>
+ <addaction name="actionMove_Down"/>
+ <addaction name="actionJoin"/>
+ </widget>
+ <action name="actionAdd">
+ <property name="text">
+ <string>Add</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </action>
+ <action name="actionMove_Up">
+ <property name="text">
+ <string>Move Up</string>
+ </property>
+ </action>
+ <action name="actionMove_Down">
+ <property name="text">
+ <string>Move Down</string>
+ </property>
+ </action>
+ <action name="actionJoin">
+ <property name="text">
+ <string>Join</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>serversView</tabstop>
+ <tabstop>nameLine</tabstop>
+ <tabstop>addressLine</tabstop>
+ <tabstop>resourceComboBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/TexturePackPage.h b/launcher/pages/instance/TexturePackPage.h
new file mode 100644
index 00000000..3f04997d
--- /dev/null
+++ b/launcher/pages/instance/TexturePackPage.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "ModFolderPage.h"
+#include "ui_ModFolderPage.h"
+
+class TexturePackPage : public ModFolderPage
+{
+ Q_OBJECT
+public:
+ explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0)
+ : ModFolderPage(instance, instance->texturePackList(), "texturepacks", "resourcepacks",
+ tr("Texture packs"), "Texture-packs", parent)
+ {
+ ui->actionView_configs->setVisible(false);
+ }
+ virtual ~TexturePackPage() {}
+
+ virtual bool shouldDisplay() const override
+ {
+ return m_inst->traits().contains("texturepacks");
+ }
+};
diff --git a/launcher/pages/instance/VersionPage.cpp b/launcher/pages/instance/VersionPage.cpp
new file mode 100644
index 00000000..a98bfb7d
--- /dev/null
+++ b/launcher/pages/instance/VersionPage.cpp
@@ -0,0 +1,642 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MultiMC.h"
+
+#include <QMessageBox>
+#include <QLabel>
+#include <QEvent>
+#include <QKeyEvent>
+#include <QMenu>
+
+#include "VersionPage.h"
+#include "ui_VersionPage.h"
+
+#include "dialogs/CustomMessageBox.h"
+#include "dialogs/VersionSelectDialog.h"
+#include "dialogs/NewComponentDialog.h"
+
+#include "dialogs/ProgressDialog.h"
+#include <GuiUtil.h>
+
+#include <QAbstractItemModel>
+#include <QMessageBox>
+#include <QListView>
+#include <QString>
+#include <QUrl>
+
+#include "minecraft/PackProfile.h"
+#include "minecraft/auth/MojangAccountList.h"
+#include "minecraft/mod/Mod.h"
+#include "icons/IconList.h"
+#include "Exception.h"
+#include "Version.h"
+#include "DesktopServices.h"
+
+#include <meta/Index.h>
+#include <meta/VersionList.h>
+
+class IconProxy : public QIdentityProxyModel
+{
+ Q_OBJECT
+public:
+
+ IconProxy(QWidget *parentWidget) : QIdentityProxyModel(parentWidget)
+ {
+ connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone);
+ m_parentWidget = parentWidget;
+ }
+
+ virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override
+ {
+ QVariant var = QIdentityProxyModel::data(proxyIndex, role);
+ int column = proxyIndex.column();
+ if(column == 0 && role == Qt::DecorationRole && m_parentWidget)
+ {
+ if(!var.isNull())
+ {
+ auto string = var.toString();
+ if(string == "warning")
+ {
+ return MMC->getThemedIcon("status-yellow");
+ }
+ else if(string == "error")
+ {
+ return MMC->getThemedIcon("status-bad");
+ }
+ }
+ return MMC->getThemedIcon("status-good");
+ }
+ return var;
+ }
+private slots:
+ void widgetGone()
+ {
+ m_parentWidget = nullptr;
+ }
+
+private:
+ QWidget *m_parentWidget = nullptr;
+};
+
+QIcon VersionPage::icon() const
+{
+ return MMC->icons()->getIcon(m_inst->iconKey());
+}
+bool VersionPage::shouldDisplay() const
+{
+ return true;
+}
+
+QMenu * VersionPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent)
+ : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst)
+{
+ ui->setupUi(this);
+
+ ui->toolBar->insertSpacer(ui->actionReload);
+
+ m_profile = m_inst->getPackProfile();
+
+ reloadPackProfile();
+
+ auto proxy = new IconProxy(ui->packageView);
+ proxy->setSourceModel(m_profile.get());
+
+ m_filterModel = new QSortFilterProxyModel();
+ m_filterModel->setDynamicSortFilter(true);
+ m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
+ m_filterModel->setSourceModel(proxy);
+ m_filterModel->setFilterKeyColumn(-1);
+
+ ui->packageView->setModel(m_filterModel);
+ ui->packageView->installEventFilter(this);
+ ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection);
+ ui->packageView->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ connect(ui->packageView->selectionModel(), &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent);
+ auto smodel = ui->packageView->selectionModel();
+ connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent);
+
+ connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls);
+ controlsEnabled = !m_inst->isRunning();
+ updateVersionControls();
+ preselect(0);
+ connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus);
+ connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu);
+ connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged);
+}
+
+VersionPage::~VersionPage()
+{
+ delete ui;
+}
+
+void VersionPage::showContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->packageView->mapToGlobal(pos));
+ delete menu;
+}
+
+void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &previous)
+{
+ if (!current.isValid())
+ {
+ ui->frame->clear();
+ return;
+ }
+ int row = current.row();
+ auto patch = m_profile->getComponent(row);
+ auto severity = patch->getProblemSeverity();
+ switch(severity)
+ {
+ case ProblemSeverity::Warning:
+ ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName()));
+ break;
+ case ProblemSeverity::Error:
+ ui->frame->setModText(tr("%1 has issues!").arg(patch->getName()));
+ break;
+ default:
+ case ProblemSeverity::None:
+ ui->frame->clear();
+ return;
+ }
+
+ auto &problems = patch->getProblems();
+ QString problemOut;
+ for (auto &problem: problems)
+ {
+ if(problem.m_severity == ProblemSeverity::Error)
+ {
+ problemOut += tr("Error: ");
+ }
+ else if(problem.m_severity == ProblemSeverity::Warning)
+ {
+ problemOut += tr("Warning: ");
+ }
+ problemOut += problem.m_description;
+ problemOut += "\n";
+ }
+ ui->frame->setModDescription(problemOut);
+}
+
+void VersionPage::updateRunningStatus(bool running)
+{
+ if(controlsEnabled == running) {
+ controlsEnabled = !running;
+ updateVersionControls();
+ }
+}
+
+void VersionPage::updateVersionControls()
+{
+ // FIXME: this is a dirty hack
+ auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft"));
+
+ bool supportsFabric = minecraftVersion >= Version("1.14");
+ ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric);
+
+ bool supportsForge = minecraftVersion <= Version("1.16.5");
+ ui->actionInstall_Forge->setEnabled(controlsEnabled && supportsForge);
+
+ bool supportsLiteLoader = minecraftVersion <= Version("1.12.2");
+ ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader);
+
+ updateButtons();
+}
+
+void VersionPage::updateButtons(int row)
+{
+ if(row == -1)
+ row = currentRow();
+ auto patch = m_profile->getComponent(row);
+ ui->actionRemove->setEnabled(controlsEnabled && patch && patch->isRemovable());
+ ui->actionMove_down->setEnabled(controlsEnabled && patch && patch->isMoveable());
+ ui->actionMove_up->setEnabled(controlsEnabled && patch && patch->isMoveable());
+ ui->actionChange_version->setEnabled(controlsEnabled && patch && patch->isVersionChangeable());
+ ui->actionEdit->setEnabled(controlsEnabled && patch && patch->isCustom());
+ ui->actionCustomize->setEnabled(controlsEnabled && patch && patch->isCustomizable());
+ ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible());
+ ui->actionDownload_All->setEnabled(controlsEnabled);
+ ui->actionAdd_Empty->setEnabled(controlsEnabled);
+ ui->actionReload->setEnabled(controlsEnabled);
+ ui->actionInstall_mods->setEnabled(controlsEnabled);
+ ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled);
+ ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled);
+}
+
+bool VersionPage::reloadPackProfile()
+{
+ try
+ {
+ m_profile->reload(Net::Mode::Online);
+ return true;
+ }
+ catch (const Exception &e)
+ {
+ QMessageBox::critical(this, tr("Error"), e.cause());
+ return false;
+ }
+ catch (...)
+ {
+ QMessageBox::critical(
+ this, tr("Error"),
+ tr("Couldn't load the instance profile."));
+ return false;
+ }
+}
+
+void VersionPage::on_actionReload_triggered()
+{
+ reloadPackProfile();
+ m_container->refreshContainer();
+}
+
+void VersionPage::on_actionRemove_triggered()
+{
+ if (ui->packageView->currentIndex().isValid())
+ {
+ // FIXME: use actual model, not reloading.
+ if (!m_profile->remove(ui->packageView->currentIndex().row()))
+ {
+ QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file"));
+ }
+ }
+ updateButtons();
+ reloadPackProfile();
+ m_container->refreshContainer();
+}
+
+void VersionPage::on_actionInstall_mods_triggered()
+{
+ if(m_container)
+ {
+ m_container->selectPage("mods");
+ }
+}
+
+void VersionPage::on_actionAdd_to_Minecraft_jar_triggered()
+{
+ auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), MMC->settings()->get("CentralModsDir").toString(), this->parentWidget());
+ if(!list.empty())
+ {
+ m_profile->installJarMods(list);
+ }
+ updateButtons();
+}
+
+void VersionPage::on_actionReplace_Minecraft_jar_triggered()
+{
+ auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), MMC->settings()->get("CentralModsDir").toString(), this->parentWidget());
+ if(!jarPath.isEmpty())
+ {
+ m_profile->installCustomJar(jarPath);
+ }
+ updateButtons();
+}
+
+void VersionPage::on_actionMove_up_triggered()
+{
+ try
+ {
+ m_profile->move(currentRow(), PackProfile::MoveUp);
+ }
+ catch (const Exception &e)
+ {
+ QMessageBox::critical(this, tr("Error"), e.cause());
+ }
+ updateButtons();
+}
+
+void VersionPage::on_actionMove_down_triggered()
+{
+ try
+ {
+ m_profile->move(currentRow(), PackProfile::MoveDown);
+ }
+ catch (const Exception &e)
+ {
+ QMessageBox::critical(this, tr("Error"), e.cause());
+ }
+ updateButtons();
+}
+
+void VersionPage::on_actionChange_version_triggered()
+{
+ auto versionRow = currentRow();
+ if(versionRow == -1)
+ {
+ return;
+ }
+ auto patch = m_profile->getComponent(versionRow);
+ auto name = patch->getName();
+ auto list = patch->getVersionList();
+ if(!list)
+ {
+ return;
+ }
+ auto uid = list->uid();
+ // FIXME: this is a horrible HACK. Get version filtering information from the actual metadata...
+ if(uid == "net.minecraftforge")
+ {
+ on_actionInstall_Forge_triggered();
+ return;
+ }
+ else if (uid == "com.mumfrey.liteloader")
+ {
+ on_actionInstall_LiteLoader_triggered();
+ return;
+ }
+ VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this);
+ if (uid == "net.fabricmc.intermediary")
+ {
+ vselect.setEmptyString(tr("No intermediary mappings versions are currently available."));
+ vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!"));
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
+ }
+ auto currentVersion = patch->getVersion();
+ if(!currentVersion.isEmpty())
+ {
+ vselect.setCurrentVersion(currentVersion);
+ }
+ if (!vselect.exec() || !vselect.selectedVersion())
+ return;
+
+ qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor();
+ bool important = false;
+ if(uid == "net.minecraft")
+ {
+ important = true;
+ }
+ m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important);
+ m_profile->resolve(Net::Mode::Online);
+ m_container->refreshContainer();
+}
+
+void VersionPage::on_actionDownload_All_triggered()
+{
+ if (!MMC->accounts()->anyAccountIsValid())
+ {
+ CustomMessageBox::selectable(
+ this, tr("Error"),
+ tr("MultiMC cannot download Minecraft or update instances unless you have at least "
+ "one account added.\nPlease add your Mojang or Minecraft account."),
+ QMessageBox::Warning)->show();
+ return;
+ }
+
+ auto updateTask = m_inst->createUpdateTask(Net::Mode::Online);
+ if (!updateTask)
+ {
+ return;
+ }
+ ProgressDialog tDialog(this);
+ connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString)));
+ // FIXME: unused return value
+ tDialog.execWithTask(updateTask.get());
+ updateButtons();
+ m_container->refreshContainer();
+}
+
+void VersionPage::on_actionInstall_Forge_triggered()
+{
+ auto vlist = ENV.metadataIndex()->get("net.minecraftforge");
+ if(!vlist)
+ {
+ return;
+ }
+ VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this);
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!"));
+
+ auto currentVersion = m_profile->getComponentVersion("net.minecraftforge");
+ if(!currentVersion.isEmpty())
+ {
+ vselect.setCurrentVersion(currentVersion);
+ }
+
+ if (vselect.exec() && vselect.selectedVersion())
+ {
+ auto vsn = vselect.selectedVersion();
+ m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor());
+ m_profile->resolve(Net::Mode::Online);
+ // m_profile->installVersion();
+ preselect(m_profile->rowCount(QModelIndex())-1);
+ m_container->refreshContainer();
+ }
+}
+
+void VersionPage::on_actionInstall_Fabric_triggered()
+{
+ auto vlist = ENV.metadataIndex()->get("net.fabricmc.fabric-loader");
+ if(!vlist)
+ {
+ return;
+ }
+ VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this);
+ vselect.setEmptyString(tr("No Fabric Loader versions are currently available."));
+ vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!"));
+
+ auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader");
+ if(!currentVersion.isEmpty())
+ {
+ vselect.setCurrentVersion(currentVersion);
+ }
+
+ if (vselect.exec() && vselect.selectedVersion())
+ {
+ auto vsn = vselect.selectedVersion();
+ m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor());
+ m_profile->resolve(Net::Mode::Online);
+ preselect(m_profile->rowCount(QModelIndex())-1);
+ m_container->refreshContainer();
+ }
+}
+
+void VersionPage::on_actionAdd_Empty_triggered()
+{
+ NewComponentDialog compdialog(QString(), QString(), this);
+ QStringList blacklist;
+ for(int i = 0; i < m_profile->rowCount(); i++)
+ {
+ auto comp = m_profile->getComponent(i);
+ blacklist.push_back(comp->getID());
+ }
+ compdialog.setBlacklist(blacklist);
+ if (compdialog.exec())
+ {
+ qDebug() << "name:" << compdialog.name();
+ qDebug() << "uid:" << compdialog.uid();
+ m_profile->installEmpty(compdialog.uid(), compdialog.name());
+ }
+}
+
+void VersionPage::on_actionInstall_LiteLoader_triggered()
+{
+ auto vlist = ENV.metadataIndex()->get("com.mumfrey.liteloader");
+ if(!vlist)
+ {
+ return;
+ }
+ VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this);
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft"));
+ vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!"));
+
+ auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader");
+ if(!currentVersion.isEmpty())
+ {
+ vselect.setCurrentVersion(currentVersion);
+ }
+
+ if (vselect.exec() && vselect.selectedVersion())
+ {
+ auto vsn = vselect.selectedVersion();
+ m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor());
+ m_profile->resolve(Net::Mode::Online);
+ // m_profile->installVersion(vselect.selectedVersion());
+ preselect(m_profile->rowCount(QModelIndex())-1);
+ m_container->refreshContainer();
+ }
+}
+
+void VersionPage::on_actionLibrariesFolder_triggered()
+{
+ DesktopServices::openDirectory(m_inst->getLocalLibraryPath(), true);
+}
+
+void VersionPage::on_actionMinecraftFolder_triggered()
+{
+ DesktopServices::openDirectory(m_inst->gameRoot(), true);
+}
+
+void VersionPage::versionCurrent(const QModelIndex &current, const QModelIndex &previous)
+{
+ currentIdx = current.row();
+ updateButtons(currentIdx);
+}
+
+void VersionPage::preselect(int row)
+{
+ if(row < 0)
+ {
+ row = 0;
+ }
+ if(row >= m_profile->rowCount(QModelIndex()))
+ {
+ row = m_profile->rowCount(QModelIndex()) - 1;
+ }
+ if(row < 0)
+ {
+ return;
+ }
+ auto model_index = m_profile->index(row);
+ ui->packageView->selectionModel()->select(model_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ updateButtons(row);
+}
+
+void VersionPage::onGameUpdateError(QString error)
+{
+ CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show();
+}
+
+Component * VersionPage::current()
+{
+ auto row = currentRow();
+ if(row < 0)
+ {
+ return nullptr;
+ }
+ return m_profile->getComponent(row);
+}
+
+int VersionPage::currentRow()
+{
+ if (ui->packageView->selectionModel()->selectedRows().isEmpty())
+ {
+ return -1;
+ }
+ return ui->packageView->selectionModel()->selectedRows().first().row();
+}
+
+void VersionPage::on_actionCustomize_triggered()
+{
+ auto version = currentRow();
+ if(version == -1)
+ {
+ return;
+ }
+ auto patch = m_profile->getComponent(version);
+ if(!patch->getVersionFile())
+ {
+ // TODO: wait for the update task to finish here...
+ return;
+ }
+ if(!m_profile->customize(version))
+ {
+ // TODO: some error box here
+ }
+ updateButtons();
+ preselect(currentIdx);
+}
+
+void VersionPage::on_actionEdit_triggered()
+{
+ auto version = current();
+ if(!version)
+ {
+ return;
+ }
+ auto filename = version->getFilename();
+ if(!QFileInfo::exists(filename))
+ {
+ qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!";
+ return;
+ }
+ MMC->openJsonEditor(filename);
+}
+
+void VersionPage::on_actionRevert_triggered()
+{
+ auto version = currentRow();
+ if(version == -1)
+ {
+ return;
+ }
+ if(!m_profile->revertToBase(version))
+ {
+ // TODO: some error box here
+ }
+ updateButtons();
+ preselect(currentIdx);
+ m_container->refreshContainer();
+}
+
+void VersionPage::onFilterTextChanged(const QString &newContents)
+{
+ m_filterModel->setFilterFixedString(newContents);
+}
+
+#include "VersionPage.moc"
+
diff --git a/launcher/pages/instance/VersionPage.h b/launcher/pages/instance/VersionPage.h
new file mode 100644
index 00000000..b5b4a6f5
--- /dev/null
+++ b/launcher/pages/instance/VersionPage.h
@@ -0,0 +1,104 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QMainWindow>
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "pages/BasePage.h"
+
+namespace Ui
+{
+class VersionPage;
+}
+
+class VersionPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit VersionPage(MinecraftInstance *inst, QWidget *parent = 0);
+ virtual ~VersionPage();
+ virtual QString displayName() const override
+ {
+ return tr("Version");
+ }
+ virtual QIcon icon() const override;
+ virtual QString id() const override
+ {
+ return "version";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Instance-Version";
+ }
+ virtual bool shouldDisplay() const override;
+
+private slots:
+ void on_actionChange_version_triggered();
+ void on_actionInstall_Forge_triggered();
+ void on_actionInstall_Fabric_triggered();
+ void on_actionAdd_Empty_triggered();
+ void on_actionInstall_LiteLoader_triggered();
+ void on_actionReload_triggered();
+ void on_actionRemove_triggered();
+ void on_actionMove_up_triggered();
+ void on_actionMove_down_triggered();
+ void on_actionAdd_to_Minecraft_jar_triggered();
+ void on_actionReplace_Minecraft_jar_triggered();
+ void on_actionRevert_triggered();
+ void on_actionEdit_triggered();
+ void on_actionInstall_mods_triggered();
+ void on_actionCustomize_triggered();
+ void on_actionDownload_All_triggered();
+
+ void on_actionMinecraftFolder_triggered();
+ void on_actionLibrariesFolder_triggered();
+
+ void updateVersionControls();
+
+private:
+ Component * current();
+ int currentRow();
+ void updateButtons(int row = -1);
+ void preselect(int row = 0);
+ int doUpdate();
+
+protected:
+ QMenu * createPopupMenu() override;
+
+ /// FIXME: this shouldn't be necessary!
+ bool reloadPackProfile();
+
+private:
+ Ui::VersionPage *ui;
+ QSortFilterProxyModel *m_filterModel;
+ std::shared_ptr<PackProfile> m_profile;
+ MinecraftInstance *m_inst;
+ int currentIdx = 0;
+ bool controlsEnabled = false;
+
+public slots:
+ void versionCurrent(const QModelIndex &current, const QModelIndex &previous);
+
+private slots:
+ void updateRunningStatus(bool running);
+ void onGameUpdateError(QString error);
+ void packageCurrent(const QModelIndex &current, const QModelIndex &previous);
+ void showContextMenu(const QPoint &pos);
+ void onFilterTextChanged(const QString & newContents);
+};
diff --git a/launcher/pages/instance/VersionPage.ui b/launcher/pages/instance/VersionPage.ui
new file mode 100644
index 00000000..84d06e2e
--- /dev/null
+++ b/launcher/pages/instance/VersionPage.ui
@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>VersionPage</class>
+ <widget class="QMainWindow" name="VersionPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>961</width>
+ <height>1091</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="ModListView" name="packageView">
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="headerHidden">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerVisible">
+ <bool>true</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="filterEdit">
+ <property name="clearButtonEnabled">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="filterLabel">
+ <property name="text">
+ <string>Filter:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="MCModInfoFrame" name="frame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionChange_version"/>
+ <addaction name="actionMove_up"/>
+ <addaction name="actionMove_down"/>
+ <addaction name="actionRemove"/>
+ <addaction name="separator"/>
+ <addaction name="actionCustomize"/>
+ <addaction name="actionEdit"/>
+ <addaction name="actionRevert"/>
+ <addaction name="separator"/>
+ <addaction name="actionInstall_Forge"/>
+ <addaction name="actionInstall_Fabric"/>
+ <addaction name="actionInstall_LiteLoader"/>
+ <addaction name="actionInstall_mods"/>
+ <addaction name="separator"/>
+ <addaction name="actionAdd_to_Minecraft_jar"/>
+ <addaction name="actionReplace_Minecraft_jar"/>
+ <addaction name="actionAdd_Empty"/>
+ <addaction name="separator"/>
+ <addaction name="actionMinecraftFolder"/>
+ <addaction name="actionLibrariesFolder"/>
+ <addaction name="separator"/>
+ <addaction name="actionReload"/>
+ <addaction name="actionDownload_All"/>
+ </widget>
+ <action name="actionChange_version">
+ <property name="text">
+ <string>Change version</string>
+ </property>
+ <property name="toolTip">
+ <string>Change version of the selected package.</string>
+ </property>
+ </action>
+ <action name="actionMove_up">
+ <property name="text">
+ <string>Move up</string>
+ </property>
+ <property name="toolTip">
+ <string>Make the selected package apply sooner.</string>
+ </property>
+ </action>
+ <action name="actionMove_down">
+ <property name="text">
+ <string>Move down</string>
+ </property>
+ <property name="toolTip">
+ <string>Make the selected package apply later.</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ <property name="toolTip">
+ <string>Remove selected package from the instance.</string>
+ </property>
+ </action>
+ <action name="actionCustomize">
+ <property name="text">
+ <string>Customize</string>
+ </property>
+ <property name="toolTip">
+ <string>Customize selected package.</string>
+ </property>
+ </action>
+ <action name="actionEdit">
+ <property name="text">
+ <string>Edit</string>
+ </property>
+ <property name="toolTip">
+ <string>Edit selected package.</string>
+ </property>
+ </action>
+ <action name="actionRevert">
+ <property name="text">
+ <string>Revert</string>
+ </property>
+ <property name="toolTip">
+ <string>Revert the selected package to default.</string>
+ </property>
+ </action>
+ <action name="actionInstall_Forge">
+ <property name="text">
+ <string>Install Forge</string>
+ </property>
+ <property name="toolTip">
+ <string>Install the Minecraft Forge package.</string>
+ </property>
+ </action>
+ <action name="actionInstall_Fabric">
+ <property name="text">
+ <string>Install Fabric</string>
+ </property>
+ <property name="toolTip">
+ <string>Install the Fabric Loader package.</string>
+ </property>
+ </action>
+ <action name="actionInstall_LiteLoader">
+ <property name="text">
+ <string>Install LiteLoader</string>
+ </property>
+ <property name="toolTip">
+ <string>Install the LiteLoader package.</string>
+ </property>
+ </action>
+ <action name="actionInstall_mods">
+ <property name="text">
+ <string>Install mods</string>
+ </property>
+ <property name="toolTip">
+ <string>Install normal mods.</string>
+ </property>
+ </action>
+ <action name="actionAdd_to_Minecraft_jar">
+ <property name="text">
+ <string>Add to Minecraft.jar</string>
+ </property>
+ <property name="toolTip">
+ <string>Add a mod into the Minecraft jar file.</string>
+ </property>
+ </action>
+ <action name="actionReplace_Minecraft_jar">
+ <property name="text">
+ <string>Replace Minecraft.jar</string>
+ </property>
+ </action>
+ <action name="actionAdd_Empty">
+ <property name="text">
+ <string>Add Empty</string>
+ </property>
+ <property name="toolTip">
+ <string>Add an empty custom package.</string>
+ </property>
+ </action>
+ <action name="actionReload">
+ <property name="text">
+ <string>Reload</string>
+ </property>
+ <property name="toolTip">
+ <string>Reload all packages.</string>
+ </property>
+ </action>
+ <action name="actionDownload_All">
+ <property name="text">
+ <string>Download All</string>
+ </property>
+ <property name="toolTip">
+ <string>Download the files needed to launch the instance now.</string>
+ </property>
+ </action>
+ <action name="actionMinecraftFolder">
+ <property name="text">
+ <string>Open .minecraft</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the instance's .minecraft folder.</string>
+ </property>
+ </action>
+ <action name="actionLibrariesFolder">
+ <property name="text">
+ <string>Open libraries</string>
+ </property>
+ <property name="toolTip">
+ <string>Open the instance's local libraries folder.</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ModListView</class>
+ <extends>QTreeView</extends>
+ <header>widgets/ModListView.h</header>
+ </customwidget>
+ <customwidget>
+ <class>MCModInfoFrame</class>
+ <extends>QFrame</extends>
+ <header>widgets/MCModInfoFrame.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/instance/WorldListPage.cpp b/launcher/pages/instance/WorldListPage.cpp
new file mode 100644
index 00000000..119cff3e
--- /dev/null
+++ b/launcher/pages/instance/WorldListPage.cpp
@@ -0,0 +1,408 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 "WorldListPage.h"
+#include "ui_WorldListPage.h"
+#include "minecraft/WorldList.h"
+#include <DesktopServices.h>
+#include <QEvent>
+#include <QMenu>
+#include <QKeyEvent>
+#include <QClipboard>
+#include <QMessageBox>
+#include <QTreeView>
+#include <QInputDialog>
+#include <tools/MCEditTool.h>
+
+#include "MultiMC.h"
+#include <GuiUtil.h>
+#include <QProcess>
+#include <FileSystem.h>
+
+class WorldListProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ WorldListProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {}
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
+ {
+ QModelIndex sourceIndex = mapToSource(index);
+
+ if (index.column() == 0 && role == Qt::DecorationRole)
+ {
+ WorldList *worlds = qobject_cast<WorldList *>(sourceModel());
+ auto iconFile = worlds->data(sourceIndex, WorldList::IconFileRole).toString();
+ if(iconFile.isNull()) {
+ // NOTE: Minecraft uses the same placeholder for servers AND worlds
+ return MMC->getThemedIcon("unknown_server");
+ }
+ return QIcon(iconFile);
+ }
+
+ return sourceIndex.data(role);
+ }
+};
+
+
+WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worlds, QWidget *parent)
+ : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds)
+{
+ ui->setupUi(this);
+
+ ui->toolBar->insertSpacer(ui->actionRefresh);
+
+ WorldListProxyModel * proxy = new WorldListProxyModel(this);
+ proxy->setSortCaseSensitivity(Qt::CaseInsensitive);
+ proxy->setSourceModel(m_worlds.get());
+ ui->worldTreeView->setSortingEnabled(true);
+ ui->worldTreeView->setModel(proxy);
+ ui->worldTreeView->installEventFilter(this);
+ ui->worldTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
+ ui->worldTreeView->setIconSize(QSize(64,64));
+ connect(ui->worldTreeView, &QTreeView::customContextMenuRequested, this, &WorldListPage::ShowContextMenu);
+
+ auto head = ui->worldTreeView->header();
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ head->setSectionResizeMode(1, QHeaderView::ResizeToContents);
+
+ connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged);
+ worldChanged(QModelIndex(), QModelIndex());
+}
+
+void WorldListPage::openedImpl()
+{
+ m_worlds->startWatching();
+}
+
+void WorldListPage::closedImpl()
+{
+ m_worlds->stopWatching();
+}
+
+WorldListPage::~WorldListPage()
+{
+ m_worlds->stopWatching();
+ delete ui;
+}
+
+void WorldListPage::ShowContextMenu(const QPoint& pos)
+{
+ auto menu = ui->toolBar->createContextMenu(this, tr("Context menu"));
+ menu->exec(ui->worldTreeView->mapToGlobal(pos));
+ delete menu;
+}
+
+QMenu * WorldListPage::createPopupMenu()
+{
+ QMenu* filteredMenu = QMainWindow::createPopupMenu();
+ filteredMenu->removeAction( ui->toolBar->toggleViewAction() );
+ return filteredMenu;
+}
+
+bool WorldListPage::shouldDisplay() const
+{
+ return true;
+}
+
+bool WorldListPage::worldListFilter(QKeyEvent *keyEvent)
+{
+ switch (keyEvent->key())
+ {
+ case Qt::Key_Delete:
+ on_actionRemove_triggered();
+ return true;
+ default:
+ break;
+ }
+ return QWidget::eventFilter(ui->worldTreeView, keyEvent);
+}
+
+bool WorldListPage::eventFilter(QObject *obj, QEvent *ev)
+{
+ if (ev->type() != QEvent::KeyPress)
+ {
+ return QWidget::eventFilter(obj, ev);
+ }
+ QKeyEvent *keyEvent = static_cast<QKeyEvent *>(ev);
+ if (obj == ui->worldTreeView)
+ return worldListFilter(keyEvent);
+ return QWidget::eventFilter(obj, ev);
+}
+
+void WorldListPage::on_actionRemove_triggered()
+{
+ auto proxiedIndex = getSelectedWorld();
+
+ if(!proxiedIndex.isValid())
+ return;
+
+ auto result = QMessageBox::question(this,
+ tr("Are you sure?"),
+ tr("This will remove the selected world permenantly.\n"
+ "The world will be gone forever (A LONG TIME).\n"
+ "\n"
+ "Do you want to continue?"));
+ if(result != QMessageBox::Yes)
+ {
+ return;
+ }
+ m_worlds->stopWatching();
+ m_worlds->deleteWorld(proxiedIndex.row());
+ m_worlds->startWatching();
+}
+
+void WorldListPage::on_actionView_Folder_triggered()
+{
+ DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true);
+}
+
+void WorldListPage::on_actionDatapacks_triggered()
+{
+ QModelIndex index = getSelectedWorld();
+
+ if (!index.isValid())
+ {
+ return;
+ }
+
+ if(!worldSafetyNagQuestion())
+ return;
+
+ auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
+
+ DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true);
+}
+
+
+void WorldListPage::on_actionReset_Icon_triggered()
+{
+ auto proxiedIndex = getSelectedWorld();
+
+ if(!proxiedIndex.isValid())
+ return;
+
+ if(m_worlds->resetIcon(proxiedIndex.row())) {
+ ui->actionReset_Icon->setEnabled(false);
+ }
+}
+
+
+QModelIndex WorldListPage::getSelectedWorld()
+{
+ auto index = ui->worldTreeView->selectionModel()->currentIndex();
+
+ auto proxy = (QSortFilterProxyModel *) ui->worldTreeView->model();
+ return proxy->mapToSource(index);
+}
+
+void WorldListPage::on_actionCopy_Seed_triggered()
+{
+ QModelIndex index = getSelectedWorld();
+
+ if (!index.isValid())
+ {
+ return;
+ }
+ int64_t seed = m_worlds->data(index, WorldList::SeedRole).toLongLong();
+ MMC->clipboard()->setText(QString::number(seed));
+}
+
+void WorldListPage::on_actionMCEdit_triggered()
+{
+ if(m_mceditStarting)
+ return;
+
+ auto mcedit = MMC->mcedit();
+
+ const QString mceditPath = mcedit->path();
+
+ QModelIndex index = getSelectedWorld();
+
+ if (!index.isValid())
+ {
+ return;
+ }
+
+ if(!worldSafetyNagQuestion())
+ return;
+
+ auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
+
+ auto program = mcedit->getProgramPath();
+ if(program.size())
+ {
+#ifdef Q_OS_WIN32
+ if(!QProcess::startDetached(program, {fullPath}, mceditPath))
+ {
+ mceditError();
+ }
+#else
+ m_mceditProcess.reset(new LoggedProcess());
+ m_mceditProcess->setDetachable(true);
+ connect(m_mceditProcess.get(), &LoggedProcess::stateChanged, this, &WorldListPage::mceditState);
+ m_mceditProcess->start(program, {fullPath});
+ m_mceditProcess->setWorkingDirectory(mceditPath);
+ m_mceditStarting = true;
+#endif
+ }
+ else
+ {
+ QMessageBox::warning(
+ this->parentWidget(),
+ tr("No MCEdit found or set up!"),
+ tr("You do not have MCEdit set up or it was moved.\nYou can set it up in the global settings.")
+ );
+ }
+}
+
+void WorldListPage::mceditError()
+{
+ QMessageBox::warning(
+ this->parentWidget(),
+ tr("MCEdit failed to start!"),
+ tr("MCEdit failed to start.\nIt may be necessary to reinstall it.")
+ );
+}
+
+void WorldListPage::mceditState(LoggedProcess::State state)
+{
+ bool failed = false;
+ switch(state)
+ {
+ case LoggedProcess::NotRunning:
+ case LoggedProcess::Starting:
+ return;
+ case LoggedProcess::FailedToStart:
+ case LoggedProcess::Crashed:
+ case LoggedProcess::Aborted:
+ {
+ failed = true;
+ }
+ case LoggedProcess::Running:
+ case LoggedProcess::Finished:
+ {
+ m_mceditStarting = false;
+ break;
+ }
+ }
+ if(failed)
+ {
+ mceditError();
+ }
+}
+
+void WorldListPage::worldChanged(const QModelIndex &current, const QModelIndex &previous)
+{
+ QModelIndex index = getSelectedWorld();
+ bool enable = index.isValid();
+ ui->actionCopy_Seed->setEnabled(enable);
+ ui->actionMCEdit->setEnabled(enable);
+ ui->actionRemove->setEnabled(enable);
+ ui->actionCopy->setEnabled(enable);
+ ui->actionRename->setEnabled(enable);
+ ui->actionDatapacks->setEnabled(enable);
+ bool hasIcon = !index.data(WorldList::IconFileRole).isNull();
+ ui->actionReset_Icon->setEnabled(enable && hasIcon);
+}
+
+void WorldListPage::on_actionAdd_triggered()
+{
+ auto list = GuiUtil::BrowseForFiles(
+ displayName(),
+ tr("Select a Minecraft world zip"),
+ tr("Minecraft World Zip File (*.zip)"), QString(), this->parentWidget());
+ if (!list.empty())
+ {
+ m_worlds->stopWatching();
+ for (auto filename : list)
+ {
+ m_worlds->installWorld(QFileInfo(filename));
+ }
+ m_worlds->startWatching();
+ }
+}
+
+bool WorldListPage::isWorldSafe(QModelIndex)
+{
+ return !m_inst->isRunning();
+}
+
+bool WorldListPage::worldSafetyNagQuestion()
+{
+ if(!isWorldSafe(getSelectedWorld()))
+ {
+ auto result = QMessageBox::question(this, tr("Copy World"), tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?"));
+ if(result == QMessageBox::No)
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+
+void WorldListPage::on_actionCopy_triggered()
+{
+ QModelIndex index = getSelectedWorld();
+ if (!index.isValid())
+ {
+ return;
+ }
+
+ if(!worldSafetyNagQuestion())
+ return;
+
+ auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
+ auto world = (World *) worldVariant.value<void *>();
+ bool ok = false;
+ QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world->name(), &ok);
+
+ if (ok && name.length() > 0)
+ {
+ world->install(m_worlds->dir().absolutePath(), name);
+ }
+}
+
+void WorldListPage::on_actionRename_triggered()
+{
+ QModelIndex index = getSelectedWorld();
+ if (!index.isValid())
+ {
+ return;
+ }
+
+ if(!worldSafetyNagQuestion())
+ return;
+
+ auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
+ auto world = (World *) worldVariant.value<void *>();
+
+ bool ok = false;
+ QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new world name."), QLineEdit::Normal, world->name(), &ok);
+
+ if (ok && name.length() > 0)
+ {
+ world->rename(name);
+ }
+}
+
+void WorldListPage::on_actionRefresh_triggered()
+{
+ m_worlds->update();
+}
+
+#include "WorldListPage.moc"
diff --git a/launcher/pages/instance/WorldListPage.h b/launcher/pages/instance/WorldListPage.h
new file mode 100644
index 00000000..4fc9aa09
--- /dev/null
+++ b/launcher/pages/instance/WorldListPage.h
@@ -0,0 +1,99 @@
+/* Copyright 2015-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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 <QMainWindow>
+
+#include "minecraft/MinecraftInstance.h"
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include <LoggedProcess.h>
+
+class WorldList;
+namespace Ui
+{
+class WorldListPage;
+}
+
+class WorldListPage : public QMainWindow, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit WorldListPage(
+ BaseInstance *inst,
+ std::shared_ptr<WorldList> worlds,
+ QWidget *parent = 0
+ );
+ virtual ~WorldListPage();
+
+ virtual QString displayName() const override
+ {
+ return tr("Worlds");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("worlds");
+ }
+ virtual QString id() const override
+ {
+ return "worlds";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Worlds";
+ }
+ virtual bool shouldDisplay() const override;
+
+ virtual void openedImpl() override;
+ virtual void closedImpl() override;
+
+protected:
+ bool eventFilter(QObject *obj, QEvent *ev) override;
+ bool worldListFilter(QKeyEvent *ev);
+ QMenu * createPopupMenu() override;
+
+protected:
+ BaseInstance *m_inst;
+
+private:
+ QModelIndex getSelectedWorld();
+ bool isWorldSafe(QModelIndex index);
+ bool worldSafetyNagQuestion();
+ void mceditError();
+
+private:
+ Ui::WorldListPage *ui;
+ std::shared_ptr<WorldList> m_worlds;
+ unique_qobject_ptr<LoggedProcess> m_mceditProcess;
+ bool m_mceditStarting = false;
+
+private slots:
+ void on_actionCopy_Seed_triggered();
+ void on_actionMCEdit_triggered();
+ void on_actionRemove_triggered();
+ void on_actionAdd_triggered();
+ void on_actionCopy_triggered();
+ void on_actionRename_triggered();
+ void on_actionRefresh_triggered();
+ void on_actionView_Folder_triggered();
+ void on_actionDatapacks_triggered();
+ void on_actionReset_Icon_triggered();
+ void worldChanged(const QModelIndex &current, const QModelIndex &previous);
+ void mceditState(LoggedProcess::State state);
+
+ void ShowContextMenu(const QPoint &pos);
+};
diff --git a/launcher/pages/instance/WorldListPage.ui b/launcher/pages/instance/WorldListPage.ui
new file mode 100644
index 00000000..ed078d94
--- /dev/null
+++ b/launcher/pages/instance/WorldListPage.ui
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>WorldListPage</class>
+ <widget class="QMainWindow" name="WorldListPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTreeView" name="worldTreeView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="allColumnsShowFocus">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="WideBar" name="toolBar">
+ <property name="windowTitle">
+ <string>Actions</string>
+ </property>
+ <property name="allowedAreas">
+ <set>Qt::LeftToolBarArea|Qt::RightToolBarArea</set>
+ </property>
+ <property name="toolButtonStyle">
+ <enum>Qt::ToolButtonTextOnly</enum>
+ </property>
+ <property name="floatable">
+ <bool>false</bool>
+ </property>
+ <attribute name="toolBarArea">
+ <enum>RightToolBarArea</enum>
+ </attribute>
+ <attribute name="toolBarBreak">
+ <bool>false</bool>
+ </attribute>
+ <addaction name="actionAdd"/>
+ <addaction name="separator"/>
+ <addaction name="actionRename"/>
+ <addaction name="actionCopy"/>
+ <addaction name="actionRemove"/>
+ <addaction name="actionMCEdit"/>
+ <addaction name="actionDatapacks"/>
+ <addaction name="actionReset_Icon"/>
+ <addaction name="separator"/>
+ <addaction name="actionCopy_Seed"/>
+ <addaction name="actionRefresh"/>
+ <addaction name="actionView_Folder"/>
+ </widget>
+ <action name="actionAdd">
+ <property name="text">
+ <string>Add</string>
+ </property>
+ </action>
+ <action name="actionRename">
+ <property name="text">
+ <string>Rename</string>
+ </property>
+ </action>
+ <action name="actionCopy">
+ <property name="text">
+ <string>Copy</string>
+ </property>
+ </action>
+ <action name="actionRemove">
+ <property name="text">
+ <string>Remove</string>
+ </property>
+ </action>
+ <action name="actionMCEdit">
+ <property name="text">
+ <string>MCEdit</string>
+ </property>
+ </action>
+ <action name="actionCopy_Seed">
+ <property name="text">
+ <string>Copy Seed</string>
+ </property>
+ </action>
+ <action name="actionRefresh">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </action>
+ <action name="actionView_Folder">
+ <property name="text">
+ <string>View Folder</string>
+ </property>
+ </action>
+ <action name="actionReset_Icon">
+ <property name="text">
+ <string>Reset Icon</string>
+ </property>
+ <property name="toolTip">
+ <string>Remove world icon to make the game re-generate it on next load.</string>
+ </property>
+ </action>
+ <action name="actionDatapacks">
+ <property name="text">
+ <string>Datapacks</string>
+ </property>
+ <property name="toolTip">
+ <string>Manage datapacks inside the world.</string>
+ </property>
+ </action>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>WideBar</class>
+ <extends>QToolBar</extends>
+ <header>widgets/WideBar.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/modplatform/ImportPage.cpp b/launcher/pages/modplatform/ImportPage.cpp
new file mode 100644
index 00000000..c2369bdc
--- /dev/null
+++ b/launcher/pages/modplatform/ImportPage.cpp
@@ -0,0 +1,130 @@
+#include "ImportPage.h"
+#include "ui_ImportPage.h"
+
+#include "MultiMC.h"
+#include "dialogs/NewInstanceDialog.h"
+#include <QFileDialog>
+#include <QValidator>
+#include <InstanceImportTask.h>
+
+class UrlValidator : public QValidator
+{
+public:
+ using QValidator::QValidator;
+
+ State validate(QString &in, int &pos) const
+ {
+ const QUrl url(in);
+ if (url.isValid() && !url.isRelative() && !url.isEmpty())
+ {
+ return Acceptable;
+ }
+ else if (QFile::exists(in))
+ {
+ return Acceptable;
+ }
+ else
+ {
+ return Intermediate;
+ }
+ }
+};
+
+ImportPage::ImportPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::ImportPage), dialog(dialog)
+{
+ ui->setupUi(this);
+ ui->modpackEdit->setValidator(new UrlValidator(ui->modpackEdit));
+ connect(ui->modpackEdit, &QLineEdit::textChanged, this, &ImportPage::updateState);
+}
+
+ImportPage::~ImportPage()
+{
+ delete ui;
+}
+
+bool ImportPage::shouldDisplay() const
+{
+ return true;
+}
+
+void ImportPage::openedImpl()
+{
+ updateState();
+}
+
+void ImportPage::updateState()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+ if(ui->modpackEdit->hasAcceptableInput())
+ {
+ QString input = ui->modpackEdit->text();
+ auto url = QUrl::fromUserInput(input);
+ if(url.isLocalFile())
+ {
+ // FIXME: actually do some validation of what's inside here... this is fake AF
+ QFileInfo fi(input);
+ if(fi.exists() && fi.suffix() == "zip")
+ {
+ QFileInfo fi(url.fileName());
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedIcon("default");
+ }
+ }
+ else
+ {
+ if(input.endsWith("?client=y")) {
+ input.chop(9);
+ input.append("/file");
+ url = QUrl::fromUserInput(input);
+ }
+ // hook, line and sinker.
+ QFileInfo fi(url.fileName());
+ dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url));
+ dialog->setSuggestedIcon("default");
+ }
+ }
+ else
+ {
+ dialog->setSuggestedPack();
+ }
+}
+
+void ImportPage::setUrl(const QString& url)
+{
+ ui->modpackEdit->setText(url);
+ updateState();
+}
+
+void ImportPage::on_modpackBtn_clicked()
+{
+ const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), tr("Zip (*.zip)"));
+ if (url.isValid())
+ {
+ if (url.isLocalFile())
+ {
+ ui->modpackEdit->setText(url.toLocalFile());
+ }
+ else
+ {
+ ui->modpackEdit->setText(url.toString());
+ }
+ }
+}
+
+
+QUrl ImportPage::modpackUrl() const
+{
+ const QUrl url(ui->modpackEdit->text());
+ if (url.isValid() && !url.isRelative() && !url.host().isEmpty())
+ {
+ return url;
+ }
+ else
+ {
+ return QUrl::fromLocalFile(ui->modpackEdit->text());
+ }
+}
diff --git a/launcher/pages/modplatform/ImportPage.h b/launcher/pages/modplatform/ImportPage.h
new file mode 100644
index 00000000..67e3c201
--- /dev/null
+++ b/launcher/pages/modplatform/ImportPage.h
@@ -0,0 +1,70 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class ImportPage;
+}
+
+class NewInstanceDialog;
+
+class ImportPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ImportPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~ImportPage();
+ virtual QString displayName() const override
+ {
+ return tr("Import from zip");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("viewfolder");
+ }
+ virtual QString id() const override
+ {
+ return "import";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Zip-import";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void setUrl(const QString & url);
+ void openedImpl() override;
+
+private slots:
+ void on_modpackBtn_clicked();
+ void updateState();
+
+private:
+ QUrl modpackUrl() const;
+
+private:
+ Ui::ImportPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+};
+
diff --git a/launcher/pages/modplatform/ImportPage.ui b/launcher/pages/modplatform/ImportPage.ui
new file mode 100644
index 00000000..eb63cbe9
--- /dev/null
+++ b/launcher/pages/modplatform/ImportPage.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ImportPage</class>
+ <widget class="QWidget" name="ImportPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>546</width>
+ <height>405</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="1">
+ <widget class="QPushButton" name="modpackBtn">
+ <property name="text">
+ <string>Browse</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLineEdit" name="modpackEdit">
+ <property name="placeholderText">
+ <string notr="true">http://</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="modpackLabel">
+ <property name="text">
+ <string>Local file or link to a direct download:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/modplatform/VanillaPage.cpp b/launcher/pages/modplatform/VanillaPage.cpp
new file mode 100644
index 00000000..02638315
--- /dev/null
+++ b/launcher/pages/modplatform/VanillaPage.cpp
@@ -0,0 +1,104 @@
+#include "VanillaPage.h"
+#include "ui_VanillaPage.h"
+
+#include "MultiMC.h"
+
+#include <meta/Index.h>
+#include <meta/VersionList.h>
+#include <dialogs/NewInstanceDialog.h>
+#include <Filter.h>
+#include <Env.h>
+#include <InstanceCreationTask.h>
+#include <QTabBar>
+
+VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent)
+ : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion);
+ filterChanged();
+ connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged);
+ connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh);
+}
+
+void VanillaPage::openedImpl()
+{
+ if(!initialized)
+ {
+ auto vlist = ENV.metadataIndex()->get("net.minecraft");
+ ui->versionList->initialize(vlist.get());
+ initialized = true;
+ }
+ else
+ {
+ suggestCurrent();
+ }
+}
+
+void VanillaPage::refresh()
+{
+ ui->versionList->loadList();
+}
+
+void VanillaPage::filterChanged()
+{
+ QStringList out;
+ if(ui->alphaFilter->isChecked())
+ out << "(old_alpha)";
+ if(ui->betaFilter->isChecked())
+ out << "(old_beta)";
+ if(ui->snapshotFilter->isChecked())
+ out << "(snapshot)";
+ if(ui->oldSnapshotFilter->isChecked())
+ out << "(old_snapshot)";
+ if(ui->releaseFilter->isChecked())
+ out << "(release)";
+ if(ui->experimentsFilter->isChecked())
+ out << "(experiment)";
+ auto regexp = out.join('|');
+ ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false));
+}
+
+VanillaPage::~VanillaPage()
+{
+ delete ui;
+}
+
+bool VanillaPage::shouldDisplay() const
+{
+ return true;
+}
+
+BaseVersionPtr VanillaPage::selectedVersion() const
+{
+ return m_selectedVersion;
+}
+
+void VanillaPage::suggestCurrent()
+{
+ if (!isOpened)
+ {
+ return;
+ }
+
+ if(!m_selectedVersion)
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion));
+ dialog->setSuggestedIcon("default");
+}
+
+void VanillaPage::setSelectedVersion(BaseVersionPtr version)
+{
+ m_selectedVersion = version;
+ suggestCurrent();
+}
diff --git a/launcher/pages/modplatform/VanillaPage.h b/launcher/pages/modplatform/VanillaPage.h
new file mode 100644
index 00000000..af6fd392
--- /dev/null
+++ b/launcher/pages/modplatform/VanillaPage.h
@@ -0,0 +1,75 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "tasks/Task.h"
+
+namespace Ui
+{
+class VanillaPage;
+}
+
+class NewInstanceDialog;
+
+class VanillaPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0);
+ virtual ~VanillaPage();
+ virtual QString displayName() const override
+ {
+ return tr("Vanilla");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("minecraft");
+ }
+ virtual QString id() const override
+ {
+ return "vanilla";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Vanilla-platform";
+ }
+ virtual bool shouldDisplay() const override;
+ void openedImpl() override;
+
+ BaseVersionPtr selectedVersion() const;
+
+public slots:
+ void setSelectedVersion(BaseVersionPtr version);
+
+private slots:
+ void filterChanged();
+
+private:
+ void refresh();
+ void suggestCurrent();
+
+private:
+ bool initialized = false;
+ NewInstanceDialog *dialog = nullptr;
+ Ui::VanillaPage *ui = nullptr;
+ bool m_versionSetByUser = false;
+ BaseVersionPtr m_selectedVersion;
+};
diff --git a/launcher/pages/modplatform/VanillaPage.ui b/launcher/pages/modplatform/VanillaPage.ui
new file mode 100644
index 00000000..47effc86
--- /dev/null
+++ b/launcher/pages/modplatform/VanillaPage.ui
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>VanillaPage</class>
+ <widget class="QWidget" name="VanillaPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>815</width>
+ <height>607</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string notr="true"/>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="1">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Filter</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="releaseFilter">
+ <property name="text">
+ <string>Releases</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="snapshotFilter">
+ <property name="text">
+ <string>Snapshots</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="oldSnapshotFilter">
+ <property name="text">
+ <string>Old Snapshots</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="betaFilter">
+ <property name="text">
+ <string>Betas</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="alphaFilter">
+ <property name="text">
+ <string>Alphas</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="experimentsFilter">
+ <property name="text">
+ <string>Experiments</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="refreshBtn">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="VersionSelectWidget" name="versionList" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>VersionSelectWidget</class>
+ <extends>QWidget</extends>
+ <header>widgets/VersionSelectWidget.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>releaseFilter</tabstop>
+ <tabstop>snapshotFilter</tabstop>
+ <tabstop>oldSnapshotFilter</tabstop>
+ <tabstop>betaFilter</tabstop>
+ <tabstop>alphaFilter</tabstop>
+ <tabstop>experimentsFilter</tabstop>
+ <tabstop>refreshBtn</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp
new file mode 100644
index 00000000..b5d8f22b
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp
@@ -0,0 +1,81 @@
+#include "AtlFilterModel.h"
+
+#include <QDebug>
+
+#include <modplatform/atlauncher/ATLPackIndex.h>
+#include <Version.h>
+#include <MMCStrings.h>
+
+namespace Atl {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByPopularity;
+ sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity);
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+ sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion);
+
+ searchTerm = "";
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting sorting)
+{
+ currentSorting = sorting;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+void FilterModel::setSearchTerm(const QString term)
+{
+ searchTerm = term.trimmed();
+ invalidate();
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ if (searchTerm.isEmpty()) {
+ return true;
+ }
+
+ QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+ ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value<ATLauncher::IndexedPack>();
+ return pack.name.contains(searchTerm, Qt::CaseInsensitive);
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value<ATLauncher::IndexedPack>();
+ ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value<ATLauncher::IndexedPack>();
+
+ if (currentSorting == ByPopularity) {
+ return leftPack.position > rightPack.position;
+ }
+ else if (currentSorting == ByGameVersion) {
+ Version lv(leftPack.versions.at(0).minecraft);
+ Version rv(rightPack.versions.at(0).minecraft);
+ return lv < rv;
+ }
+ else if (currentSorting == ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ // Invalid sorting set, somehow...
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+}
diff --git a/launcher/pages/modplatform/atlauncher/AtlFilterModel.h b/launcher/pages/modplatform/atlauncher/AtlFilterModel.h
new file mode 100644
index 00000000..bd72ad91
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlFilterModel.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <QtCore/QSortFilterProxyModel>
+
+namespace Atl {
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByPopularity,
+ ByGameVersion,
+ ByName,
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+ void setSearchTerm(QString term);
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+private:
+ QMap<QString, Sorting> sortings;
+ Sorting currentSorting;
+ QString searchTerm;
+
+};
+
+}
diff --git a/launcher/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/pages/modplatform/atlauncher/AtlListModel.cpp
new file mode 100644
index 00000000..f3be6198
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlListModel.cpp
@@ -0,0 +1,194 @@
+#include "AtlListModel.h"
+
+#include <BuildConfig.h>
+#include <MultiMC.h>
+#include <Env.h>
+#include <Json.h>
+
+namespace Atl {
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ ATLauncher::IndexedPack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ return pack.name;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.safeName))
+ {
+ return (m_logoMap.value(pack.safeName));
+ }
+ auto icon = MMC->getThemedIcon("atlauncher-placeholder");
+
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower());
+ ((ListModel *)this)->requestLogo(pack.safeName, url);
+
+ return icon;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::request()
+{
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ auto *netJob = new NetJob("Atl::Request");
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json");
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed);
+}
+
+void ListModel::requestFinished()
+{
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<ATLauncher::IndexedPack> newList;
+
+ auto packs = doc.array();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ ATLauncher::IndexedPack pack;
+
+ try {
+ ATLauncher::loadIndexedPack(pack, packObj);
+ }
+ catch (const JSONValidationError &e) {
+ qDebug() << QString::fromUtf8(response);
+ qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause();
+ return;
+ }
+
+ // ignore packs without a published version
+ if(pack.versions.length() == 0) continue;
+ // only display public packs (for now)
+ if(pack.type != ATLauncher::PackType::Public) continue;
+ // ignore "system" packs (Vanilla, Vanilla with Forge, etc)
+ if(pack.system) continue;
+
+ newList.append(pack);
+ }
+
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void ListModel::requestFailed(QString reason)
+{
+ jobPtr.reset();
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void ListModel::logoLoaded(QString logo, QIcon out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, out);
+
+ for(int i = 0; i < modpacks.size(); i++) {
+ if(modpacks[i].safeName == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::requestLogo(QString file, QString url)
+{
+ if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath]
+ {
+ emit logoLoaded(file, QIcon(fullPath));
+ if(waitingCallbacks.contains(file))
+ {
+ waitingCallbacks.value(file)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, file]
+ {
+ emit logoFailed(file);
+ });
+
+ job->start();
+
+ m_loadingLogos.append(file);
+}
+
+}
diff --git a/launcher/pages/modplatform/atlauncher/AtlListModel.h b/launcher/pages/modplatform/atlauncher/AtlListModel.h
new file mode 100644
index 00000000..2d30a64e
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlListModel.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "net/NetJob.h"
+#include <QIcon>
+#include <modplatform/atlauncher/ATLPackIndex.h>
+
+namespace Atl {
+
+typedef QMap<QString, QIcon> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ void request();
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+
+private slots:
+ void requestFinished();
+ void requestFailed(QString reason);
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<ATLauncher::IndexedPack> modpacks;
+
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ NetJobPtr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
new file mode 100644
index 00000000..14bbd18b
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp
@@ -0,0 +1,209 @@
+#include "AtlOptionalModDialog.h"
+#include "ui_AtlOptionalModDialog.h"
+
+AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
+ : QAbstractListModel(parent), m_mods(mods) {
+
+ // fill mod index
+ for (int i = 0; i < m_mods.size(); i++) {
+ auto mod = m_mods.at(i);
+ m_index[mod.name] = i;
+ }
+ // set initial state
+ for (int i = 0; i < m_mods.size(); i++) {
+ auto mod = m_mods.at(i);
+ m_selection[mod.name] = false;
+ setMod(mod, i, mod.selected, false);
+ }
+}
+
+QVector<QString> AtlOptionalModListModel::getResult() {
+ QVector<QString> result;
+
+ for (const auto& mod : m_mods) {
+ if (m_selection[mod.name]) {
+ result.push_back(mod.name);
+ }
+ }
+
+ return result;
+}
+
+int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const {
+ return m_mods.size();
+}
+
+int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const {
+ // Enabled, Name, Description
+ return 3;
+}
+
+QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const {
+ auto row = index.row();
+ auto mod = m_mods.at(row);
+
+ if (role == Qt::DisplayRole) {
+ if (index.column() == NameColumn) {
+ return mod.name;
+ }
+ if (index.column() == DescriptionColumn) {
+ return mod.description;
+ }
+ }
+ else if (role == Qt::ToolTipRole) {
+ if (index.column() == DescriptionColumn) {
+ return mod.description;
+ }
+ }
+ else if (role == Qt::CheckStateRole) {
+ if (index.column() == EnabledColumn) {
+ return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked;
+ }
+ }
+
+ return QVariant();
+}
+
+bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) {
+ if (role == Qt::CheckStateRole) {
+ auto row = index.row();
+ auto mod = m_mods.at(row);
+
+ toggleMod(mod, row);
+ return true;
+ }
+
+ return false;
+}
+
+QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const {
+ if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
+ switch (section) {
+ case EnabledColumn:
+ return QString();
+ case NameColumn:
+ return QString("Name");
+ case DescriptionColumn:
+ return QString("Description");
+ }
+ }
+
+ return QVariant();
+}
+
+Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const {
+ auto flags = QAbstractListModel::flags(index);
+ if (index.isValid() && index.column() == EnabledColumn) {
+ flags |= Qt::ItemIsUserCheckable;
+ }
+ return flags;
+}
+
+void AtlOptionalModListModel::selectRecommended() {
+ for (const auto& mod : m_mods) {
+ m_selection[mod.name] = mod.recommended;
+ }
+
+ emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn),
+ AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn));
+}
+
+void AtlOptionalModListModel::clearAll() {
+ for (const auto& mod : m_mods) {
+ m_selection[mod.name] = false;
+ }
+
+ emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn),
+ AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn));
+}
+
+void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) {
+ setMod(mod, index, !m_selection[mod.name]);
+}
+
+void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) {
+ if (m_selection[mod.name] == enable) return;
+
+ m_selection[mod.name] = enable;
+
+ // disable other mods in the group, if applicable
+ if (enable && !mod.group.isEmpty()) {
+ for (int i = 0; i < m_mods.size(); i++) {
+ if (index == i) continue;
+ auto other = m_mods.at(i);
+
+ if (mod.group == other.group) {
+ setMod(other, i, false, shouldEmit);
+ }
+ }
+ }
+
+ for (const auto& dependencyName : mod.depends) {
+ auto dependencyIndex = m_index[dependencyName];
+ auto dependencyMod = m_mods.at(dependencyIndex);
+
+ // enable/disable dependencies
+ if (enable) {
+ setMod(dependencyMod, dependencyIndex, true, shouldEmit);
+ }
+
+ // if the dependency is 'effectively hidden', then track which mods
+ // depend on it - so we can efficiently disable it when no more dependents
+ // depend on it.
+ auto dependants = m_dependants[dependencyName];
+
+ if (enable) {
+ dependants.append(mod.name);
+ }
+ else {
+ dependants.removeAll(mod.name);
+
+ // if there are no longer any dependents, let's disable the mod
+ if (dependencyMod.effectively_hidden && dependants.isEmpty()) {
+ setMod(dependencyMod, dependencyIndex, false, shouldEmit);
+ }
+ }
+ }
+
+ // disable mods that depend on this one, if disabling
+ if (!enable) {
+ auto dependants = m_dependants[mod.name];
+ for (const auto& dependencyName : dependants) {
+ auto dependencyIndex = m_index[dependencyName];
+ auto dependencyMod = m_mods.at(dependencyIndex);
+
+ setMod(dependencyMod, dependencyIndex, false, shouldEmit);
+ }
+ }
+
+ if (shouldEmit) {
+ emit dataChanged(AtlOptionalModListModel::index(index, EnabledColumn),
+ AtlOptionalModListModel::index(index, EnabledColumn));
+ }
+}
+
+
+AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
+ : QDialog(parent), ui(new Ui::AtlOptionalModDialog) {
+ ui->setupUi(this);
+
+ listModel = new AtlOptionalModListModel(this, mods);
+ ui->treeView->setModel(listModel);
+
+ ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ ui->treeView->header()->setSectionResizeMode(
+ AtlOptionalModListModel::NameColumn, QHeaderView::ResizeToContents);
+ ui->treeView->header()->setSectionResizeMode(
+ AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch);
+
+ connect(ui->selectRecommendedButton, &QPushButton::pressed,
+ listModel, &AtlOptionalModListModel::selectRecommended);
+ connect(ui->clearAllButton, &QPushButton::pressed,
+ listModel, &AtlOptionalModListModel::clearAll);
+ connect(ui->installButton, &QPushButton::pressed,
+ this, &QDialog::close);
+}
+
+AtlOptionalModDialog::~AtlOptionalModDialog() {
+ delete ui;
+}
diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h
new file mode 100644
index 00000000..a1df43f6
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <QDialog>
+#include <QAbstractListModel>
+
+#include "modplatform/atlauncher/ATLPackIndex.h"
+
+namespace Ui {
+class AtlOptionalModDialog;
+}
+
+class AtlOptionalModListModel : public QAbstractListModel {
+ Q_OBJECT
+
+public:
+ enum Columns
+ {
+ EnabledColumn = 0,
+ NameColumn,
+ DescriptionColumn,
+ };
+
+ AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
+
+ QVector<QString> getResult();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+public slots:
+ void selectRecommended();
+ void clearAll();
+
+private:
+ void toggleMod(ATLauncher::VersionMod mod, int index);
+ void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true);
+
+private:
+ QVector<ATLauncher::VersionMod> m_mods;
+ QMap<QString, bool> m_selection;
+ QMap<QString, int> m_index;
+ QMap<QString, QVector<QString>> m_dependants;
+};
+
+class AtlOptionalModDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
+ ~AtlOptionalModDialog() override;
+
+ QVector<QString> getResult() {
+ return listModel->getResult();
+ }
+
+private:
+ Ui::AtlOptionalModDialog *ui;
+
+ AtlOptionalModListModel *listModel;
+};
diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
new file mode 100644
index 00000000..5d3193a4
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AtlOptionalModDialog</class>
+ <widget class="QDialog" name="AtlOptionalModDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>550</width>
+ <height>310</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Select Mods To Install</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="3">
+ <widget class="QPushButton" name="installButton">
+ <property name="text">
+ <string>Install</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QPushButton" name="selectRecommendedButton">
+ <property name="text">
+ <string>Select Recommended</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QPushButton" name="shareCodeButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Use Share Code</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QPushButton" name="clearAllButton">
+ <property name="text">
+ <string>Clear All</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="4">
+ <widget class="ModListView" name="treeView"/>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>ModListView</class>
+ <extends>QTreeView</extends>
+ <header>widgets/ModListView.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/pages/modplatform/atlauncher/AtlPage.cpp
new file mode 100644
index 00000000..9fdf111f
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlPage.cpp
@@ -0,0 +1,175 @@
+#include "AtlPage.h"
+#include "ui_AtlPage.h"
+
+#include "dialogs/NewInstanceDialog.h"
+#include "AtlOptionalModDialog.h"
+#include <modplatform/atlauncher/ATLPackInstallTask.h>
+#include <BuildConfig.h>
+#include <dialogs/VersionSelectDialog.h>
+
+AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog)
+{
+ ui->setupUi(this);
+
+ filterModel = new Atl::FilterModel(this);
+ listModel = new Atl::ListModel(this);
+ filterModel->setSourceModel(listModel);
+ ui->packView->setModel(filterModel);
+ ui->packView->setSortingEnabled(true);
+
+ ui->packView->header()->hide();
+ ui->packView->setIndentation(0);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
+ }
+ ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
+
+ connect(ui->searchEdit, &QLineEdit::textChanged, this, &AtlPage::triggerSearch);
+ connect(ui->resetButton, &QPushButton::clicked, this, &AtlPage::resetSearch);
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged);
+}
+
+AtlPage::~AtlPage()
+{
+ delete ui;
+}
+
+bool AtlPage::shouldDisplay() const
+{
+ return true;
+}
+
+void AtlPage::openedImpl()
+{
+ if(!initialized)
+ {
+ listModel->request();
+ initialized = true;
+ }
+
+ suggestCurrent();
+}
+
+void AtlPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion));
+ auto editedLogoName = selected.safeName;
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower());
+ listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+}
+
+void AtlPage::triggerSearch()
+{
+ filterModel->setSearchTerm(ui->searchEdit->text());
+}
+
+void AtlPage::resetSearch()
+{
+ ui->searchEdit->setText("");
+}
+
+void AtlPage::onSortingSelectionChanged(QString data)
+{
+ auto toSet = filterModel->getAvailableSortings().value(data);
+ filterModel->setSorting(toSet);
+}
+
+void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ selected = filterModel->data(first, Qt::UserRole).value<ATLauncher::IndexedPack>();
+
+ ui->packDescription->setHtml(selected.description.replace("\n", "<br>"));
+
+ for(const auto& version : selected.versions) {
+ ui->versionSelectionBox->addItem(version.version);
+ }
+
+ suggestCurrent();
+}
+
+void AtlPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
+
+QVector<QString> AtlPage::chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) {
+ AtlOptionalModDialog optionalModDialog(this, mods);
+ optionalModDialog.exec();
+ return optionalModDialog.getResult();
+}
+
+QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) {
+ VersionSelectDialog vselect(vlist.get(), "Choose Version", MMC->activeWindow(), false);
+ if (minecraftVersion != Q_NULLPTR) {
+ vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion);
+ vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
+ }
+ else {
+ vselect.setEmptyString(tr("No versions are currently available"));
+ }
+ vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!"));
+
+ // select recommended build
+ for (int i = 0; i < vlist->versions().size(); i++) {
+ auto version = vlist->versions().at(i);
+ auto reqs = version->requires();
+
+ // filter by minecraft version, if the loader depends on a certain version.
+ if (minecraftVersion != Q_NULLPTR) {
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) {
+ return req.uid == "net.minecraft";
+ });
+ if (iter == reqs.end()) continue;
+ if (iter->equalsVersion != minecraftVersion) continue;
+ }
+
+ // first recommended build we find, we use.
+ if (version->isRecommended()) {
+ vselect.setCurrentVersion(version->descriptor());
+ break;
+ }
+ }
+
+ vselect.exec();
+ return vselect.selectedVersion()->descriptor();
+}
diff --git a/launcher/pages/modplatform/atlauncher/AtlPage.h b/launcher/pages/modplatform/atlauncher/AtlPage.h
new file mode 100644
index 00000000..932ec6a6
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlPage.h
@@ -0,0 +1,87 @@
+/* Copyright 2013-2019 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 "AtlFilterModel.h"
+#include "AtlListModel.h"
+
+#include <QWidget>
+#include <modplatform/atlauncher/ATLPackInstallTask.h>
+
+#include "MultiMC.h"
+#include "pages/BasePage.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class AtlPage;
+}
+
+class NewInstanceDialog;
+
+class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport
+{
+Q_OBJECT
+
+public:
+ explicit AtlPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~AtlPage();
+ virtual QString displayName() const override
+ {
+ return tr("ATLauncher");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("atlauncher");
+ }
+ virtual QString id() const override
+ {
+ return "atl";
+ }
+ virtual QString helpPage() const override
+ {
+ return "ATL-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+private:
+ void suggestCurrent();
+
+ QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
+ QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) override;
+
+private slots:
+ void triggerSearch();
+ void resetSearch();
+
+ void onSortingSelectionChanged(QString data);
+
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::AtlPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Atl::ListModel* listModel = nullptr;
+ Atl::FilterModel* filterModel = nullptr;
+
+ ATLauncher::IndexedPack selected;
+ QString selectedVersion;
+
+ bool initialized = false;
+};
diff --git a/launcher/pages/modplatform/atlauncher/AtlPage.ui b/launcher/pages/modplatform/atlauncher/AtlPage.ui
new file mode 100644
index 00000000..f16c24b8
--- /dev/null
+++ b/launcher/pages/modplatform/atlauncher/AtlPage.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AtlPage</class>
+ <widget class="QWidget" name="AtlPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>837</width>
+ <height>685</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QTreeView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>96</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Version selected:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="resetButton">
+ <property name="text">
+ <string>Reset</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>resetButton</tabstop>
+ <tabstop>packView</tabstop>
+ <tabstop>packDescription</tabstop>
+ <tabstop>sortByBox</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/modplatform/flame/FlameModel.cpp b/launcher/pages/modplatform/flame/FlameModel.cpp
new file mode 100644
index 00000000..228a88c5
--- /dev/null
+++ b/launcher/pages/modplatform/flame/FlameModel.cpp
@@ -0,0 +1,259 @@
+#include "FlameModel.h"
+#include "MultiMC.h"
+#include <Json.h>
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+#include <QLabel>
+
+#include <RWStorage.h>
+#include <Env.h>
+
+namespace Flame {
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ IndexedPack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ if(pack.description.length() > 100)
+ {
+ //some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+
+ }
+ return pack.description;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.logoName))
+ {
+ return (m_logoMap.value(pack.logoName));
+ }
+ QIcon icon = MMC->getThemedIcon("screenshot-placeholder");
+ ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
+ return icon;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::logoLoaded(QString logo, QIcon out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, out);
+ for(int i = 0; i < modpacks.size(); i++) {
+ if(modpacks[i].logoName == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo))
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
+ {
+ emit logoLoaded(logo, QIcon(fullPath));
+ if(waitingCallbacks.contains(logo))
+ {
+ waitingCallbacks.value(logo)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ emit logoFailed(logo);
+ });
+
+ job->start();
+
+ m_loadingLogos.append(logo);
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
+{
+ return QAbstractListModel::flags(index);
+}
+
+bool ListModel::canFetchMore(const QModelIndex& parent) const
+{
+ return searchState == CanPossiblyFetchMore;
+}
+
+void ListModel::fetchMore(const QModelIndex& parent)
+{
+ if (parent.isValid())
+ return;
+ if(nextSearchOffset == 0) {
+ qWarning() << "fetchMore with 0 offset is wrong...";
+ return;
+ }
+ performPaginatedSearch();
+}
+
+void ListModel::performPaginatedSearch()
+{
+ NetJob *netJob = new NetJob("Flame::Search");
+ auto searchUrl = QString(
+ "https://addons-ecs.forgesvc.net/api/v2/addon/search?"
+ "categoryId=0&"
+ "gameId=432&"
+ "index=%1&"
+ "pageSize=25&"
+ "searchFilter=%2&"
+ "sectionId=4471&"
+ "sort=%3"
+ ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
+}
+
+void ListModel::searchWithTerm(const QString& term, int sort)
+{
+ if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
+ return;
+ }
+ currentSearchTerm = term;
+ currentSort = sort;
+ if(jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ }
+ else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+}
+
+void Flame::ListModel::searchRequestFinished()
+{
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<Flame::IndexedPack> newList;
+ auto packs = doc.array();
+ for(auto packRaw : packs) {
+ auto packObj = packRaw.toObject();
+
+ Flame::IndexedPack pack;
+ try
+ {
+ Flame::loadIndexedPack(pack, packObj);
+ newList.append(pack);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qWarning() << "Error while loading pack from CurseForge: " << e.cause();
+ continue;
+ }
+ }
+ if(packs.size() < 25) {
+ searchState = Finished;
+ } else {
+ nextSearchOffset += 25;
+ searchState = CanPossiblyFetchMore;
+ }
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void Flame::ListModel::searchRequestFailed(QString reason)
+{
+ jobPtr.reset();
+
+ if(searchState == ResetRequested) {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ nextSearchOffset = 0;
+ performPaginatedSearch();
+ } else {
+ searchState = Finished;
+ }
+}
+
+}
+
diff --git a/launcher/pages/modplatform/flame/FlameModel.h b/launcher/pages/modplatform/flame/FlameModel.h
new file mode 100644
index 00000000..24383db0
--- /dev/null
+++ b/launcher/pages/modplatform/flame/FlameModel.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <RWStorage.h>
+
+#include <QAbstractListModel>
+#include <QSortFilterProxyModel>
+#include <QThreadPool>
+#include <QIcon>
+#include <QStyledItemDelegate>
+#include <QList>
+#include <QString>
+#include <QStringList>
+#include <QMetaType>
+
+#include <functional>
+#include <net/NetJob.h>
+
+#include <modplatform/flame/FlamePackIndex.h>
+
+namespace Flame {
+
+
+typedef QMap<QString, QIcon> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ bool canFetchMore(const QModelIndex & parent) const override;
+ void fetchMore(const QModelIndex & parent) override;
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString & term, const int sort);
+
+private slots:
+ void performPaginatedSearch();
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+ void searchRequestFinished();
+ void searchRequestFailed(QString reason);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<IndexedPack> modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ LogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ QString currentSearchTerm;
+ int currentSort = 0;
+ int nextSearchOffset = 0;
+ enum SearchState {
+ None,
+ CanPossiblyFetchMore,
+ ResetRequested,
+ Finished
+ } searchState = None;
+ NetJobPtr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/pages/modplatform/flame/FlamePage.cpp b/launcher/pages/modplatform/flame/FlamePage.cpp
new file mode 100644
index 00000000..ade58431
--- /dev/null
+++ b/launcher/pages/modplatform/flame/FlamePage.cpp
@@ -0,0 +1,185 @@
+#include "FlamePage.h"
+#include "ui_FlamePage.h"
+
+#include "MultiMC.h"
+#include <Json.h>
+#include "dialogs/NewInstanceDialog.h"
+#include <InstanceImportTask.h>
+#include "FlameModel.h"
+#include <QKeyEvent>
+
+FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ listModel = new Flame::ListModel(this);
+ ui->packView->setModel(listModel);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ // index is used to set the sorting with the curseforge api
+ ui->sortByBox->addItem(tr("Sort by featured"));
+ ui->sortByBox->addItem(tr("Sort by popularity"));
+ ui->sortByBox->addItem(tr("Sort by last updated"));
+ ui->sortByBox->addItem(tr("Sort by name"));
+ ui->sortByBox->addItem(tr("Sort by author"));
+ ui->sortByBox->addItem(tr("Sort by total downloads"));
+
+ connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged);
+}
+
+FlamePage::~FlamePage()
+{
+ delete ui;
+}
+
+bool FlamePage::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
+
+bool FlamePage::shouldDisplay() const
+{
+ return true;
+}
+
+void FlamePage::openedImpl()
+{
+ suggestCurrent();
+ triggerSearch();
+}
+
+void FlamePage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex());
+}
+
+void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ current = listModel->data(first, Qt::UserRole).value<Flame::IndexedPack>();
+ QString text = "";
+ QString name = current.name;
+
+ if (current.websiteUrl.isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
+ if (!current.authors.empty()) {
+ auto authorToStr = [](Flame::ModpackAuthor & author) {
+ if(author.url.isEmpty()) {
+ return author.name;
+ }
+ return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
+ };
+ QStringList authorStrs;
+ for(auto & author: current.authors) {
+ authorStrs.push_back(authorToStr(author));
+ }
+ text += "<br>" + tr(" by ") + authorStrs.join(", ");
+ }
+ text += "<br><br>";
+
+ ui->packDescription->setHtml(text + current.description);
+
+ if (current.versionsLoaded == false)
+ {
+ qDebug() << "Loading flame modpack versions";
+ NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name));
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ int addonId = current.addonId;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get()));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response]
+ {
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ QJsonArray arr = doc.array();
+ try
+ {
+ Flame::loadIndexedPackVersions(current, arr);
+ }
+ catch(const JSONValidationError &e)
+ {
+ qDebug() << *response;
+ qWarning() << "Error while reading flame modpack version: " << e.cause();
+ }
+
+ for(auto version : current.versions) {
+ ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
+ }
+
+ suggestCurrent();
+ });
+ netJob->start();
+ }
+ else
+ {
+ for(auto version : current.versions) {
+ ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
+ }
+
+ suggestCurrent();
+ }
+}
+
+void FlamePage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
+ QString editedLogoName;
+ editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
+ listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+}
+
+void FlamePage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+ selectedVersion = ui->versionSelectionBox->currentData().toString();
+ suggestCurrent();
+}
diff --git a/launcher/pages/modplatform/flame/FlamePage.h b/launcher/pages/modplatform/flame/FlamePage.h
new file mode 100644
index 00000000..467bb44b
--- /dev/null
+++ b/launcher/pages/modplatform/flame/FlamePage.h
@@ -0,0 +1,80 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "tasks/Task.h"
+#include <modplatform/flame/FlamePackIndex.h>
+
+namespace Ui
+{
+class FlamePage;
+}
+
+class NewInstanceDialog;
+
+namespace Flame {
+ class ListModel;
+}
+
+class FlamePage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit FlamePage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~FlamePage();
+ virtual QString displayName() const override
+ {
+ return tr("CurseForge");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("flame");
+ }
+ virtual QString id() const override
+ {
+ return "flame";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Flame-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::FlamePage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Flame::ListModel* listModel = nullptr;
+ Flame::IndexedPack current;
+
+ QString selectedVersion;
+};
diff --git a/launcher/pages/modplatform/flame/FlamePage.ui b/launcher/pages/modplatform/flame/FlamePage.ui
new file mode 100644
index 00000000..9723815a
--- /dev/null
+++ b/launcher/pages/modplatform/flame/FlamePage.ui
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FlamePage</class>
+ <widget class="QWidget" name="FlamePage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>837</width>
+ <height>685</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="1" column="0">
+ <widget class="QListView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Version selected:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>packView</tabstop>
+ <tabstop>packDescription</tabstop>
+ <tabstop>sortByBox</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/pages/modplatform/ftb/FtbFilterModel.cpp
new file mode 100644
index 00000000..dec3a017
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbFilterModel.cpp
@@ -0,0 +1,64 @@
+#include "FtbFilterModel.h"
+
+#include <QDebug>
+
+#include "modplatform/modpacksch/FTBPackManifest.h"
+#include <MMCStrings.h>
+
+namespace Ftb {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByPlays;
+ sortings.insert(tr("Sort by plays"), Sorting::ByPlays);
+ sortings.insert(tr("Sort by installs"), Sorting::ByInstalls);
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting sorting)
+{
+ currentSorting = sorting;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ return true;
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ ModpacksCH::Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<ModpacksCH::Modpack>();
+ ModpacksCH::Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<ModpacksCH::Modpack>();
+
+ if (currentSorting == ByPlays) {
+ return leftPack.plays < rightPack.plays;
+ }
+ else if (currentSorting == ByInstalls) {
+ return leftPack.installs < rightPack.installs;
+ }
+ else if (currentSorting == ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ // Invalid sorting set, somehow...
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+}
diff --git a/launcher/pages/modplatform/ftb/FtbFilterModel.h b/launcher/pages/modplatform/ftb/FtbFilterModel.h
new file mode 100644
index 00000000..4fe2a274
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbFilterModel.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <QtCore/QSortFilterProxyModel>
+
+namespace Ftb {
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByPlays,
+ ByInstalls,
+ ByName,
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+private:
+ QMap<QString, Sorting> sortings;
+ Sorting currentSorting;
+
+};
+
+}
diff --git a/launcher/pages/modplatform/ftb/FtbListModel.cpp b/launcher/pages/modplatform/ftb/FtbListModel.cpp
new file mode 100644
index 00000000..98973f2e
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbListModel.cpp
@@ -0,0 +1,304 @@
+#include "FtbListModel.h"
+
+#include "BuildConfig.h"
+#include "Env.h"
+#include "MultiMC.h"
+#include "Json.h"
+
+#include <QPainter>
+
+namespace Ftb {
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ ModpacksCH::Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ return pack.synopsis;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ QIcon placeholder = MMC->getThemedIcon("screenshot-placeholder");
+
+ auto iter = m_logoMap.find(pack.name);
+ if (iter != m_logoMap.end()) {
+ auto & logo = *iter;
+ if(!logo.result.isNull()) {
+ return logo.result;
+ }
+ return placeholder;
+ }
+
+ for(auto art : pack.art) {
+ if(art.type == "square") {
+ ((ListModel *)this)->requestLogo(pack.name, art.url);
+ }
+ }
+ return placeholder;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::performSearch()
+{
+ auto *netJob = new NetJob("Ftb::Search");
+ QString searchUrl;
+ if(currentSearchTerm.isEmpty()) {
+ searchUrl = BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/all";
+ }
+ else {
+ searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/search/25?term=%1")
+ .arg(currentSearchTerm);
+ }
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
+}
+
+void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void ListModel::searchWithTerm(const QString &term)
+{
+ if(searchState != Failed && currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) {
+ // unless the search has failed, then there is no need to perform an identical search.
+ return;
+ }
+ currentSearchTerm = term;
+
+ if(jobPtr) {
+ jobPtr->abort();
+ jobPtr.reset();
+ }
+
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+
+ performSearch();
+}
+
+void ListModel::searchRequestFinished()
+{
+ jobPtr.reset();
+ remainingPacks.clear();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto packs = doc.object().value("packs").toArray();
+ for(auto pack : packs) {
+ auto packId = pack.toInt();
+ remainingPacks.append(packId);
+ }
+
+ if(!remainingPacks.isEmpty()) {
+ currentPack = remainingPacks.at(0);
+ requestPack();
+ }
+}
+
+void ListModel::searchRequestFailed(QString reason)
+{
+ jobPtr.reset();
+ remainingPacks.clear();
+
+ searchState = Failed;
+}
+
+void ListModel::requestPack()
+{
+ auto *netJob = new NetJob("Ftb::Search");
+ auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1")
+ .arg(currentPack);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed);
+}
+
+void ListModel::packRequestFinished()
+{
+ jobPtr.reset();
+ remainingPacks.removeOne(currentPack);
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ModpacksCH::Modpack pack;
+ try
+ {
+ ModpacksCH::loadModpack(pack, obj);
+ }
+ catch (const JSONValidationError &e)
+ {
+ qDebug() << QString::fromUtf8(response);
+ qWarning() << "Error while reading pack manifest from FTB: " << e.cause();
+ return;
+ }
+
+ // Since there is no guarantee that packs have a version, this will just
+ // ignore those "dud" packs.
+ if (pack.versions.empty())
+ {
+ qWarning() << "FTB Pack " << pack.id << " ignored. reason: lacking any versions";
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size());
+ modpacks.append(pack);
+ endInsertRows();
+ }
+
+ if(!remainingPacks.isEmpty()) {
+ currentPack = remainingPacks.at(0);
+ requestPack();
+ }
+}
+
+void ListModel::packRequestFailed(QString reason)
+{
+ jobPtr.reset();
+ remainingPacks.removeOne(currentPack);
+}
+
+void ListModel::logoLoaded(QString logo, bool stale)
+{
+ auto & logoObj = m_logoMap[logo];
+ logoObj.downloadJob.reset();
+ QString smallPath = logoObj.fullpath + ".small";
+
+ QFileInfo smallInfo(smallPath);
+
+ if(stale || !smallInfo.exists()) {
+ QImage image(logoObj.fullpath);
+ if (image.isNull())
+ {
+ logoObj.failed = true;
+ return;
+ }
+ QImage small;
+ if (image.width() > image.height()) {
+ small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation);
+ }
+ else {
+ small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation);
+ }
+ QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2);
+ QImage square(QSize(256, 256), QImage::Format_ARGB32);
+ square.fill(Qt::transparent);
+
+ QPainter painter(&square);
+ painter.drawImage(offset, small);
+ painter.end();
+
+ square.save(logoObj.fullpath + ".small", "PNG");
+ }
+
+ logoObj.result = QIcon(logoObj.fullpath + ".small");
+ for(int i = 0; i < modpacks.size(); i++) {
+ if(modpacks[i].name == logo) {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_logoMap[logo].failed = true;
+ m_logoMap[logo].downloadJob.reset();
+}
+
+void ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_logoMap.contains(logo)) {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)));
+
+ bool stale = entry->isStale();
+
+ NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale]
+ {
+ logoLoaded(logo, stale);
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ logoFailed(logo);
+ });
+
+ auto &newLogoEntry = m_logoMap[logo];
+ newLogoEntry.downloadJob = job;
+ newLogoEntry.fullpath = fullPath;
+ job->start();
+}
+
+}
diff --git a/launcher/pages/modplatform/ftb/FtbListModel.h b/launcher/pages/modplatform/ftb/FtbListModel.h
new file mode 100644
index 00000000..de94e6ba
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbListModel.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <QAbstractListModel>
+
+#include "modplatform/modpacksch/FTBPackManifest.h"
+#include "net/NetJob.h"
+#include <QIcon>
+
+namespace Ftb {
+
+struct Logo {
+ QString fullpath;
+ NetJobPtr downloadJob;
+ QIcon result;
+ bool failed = false;
+};
+
+typedef QMap<QString, Logo> LogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString & term);
+
+private slots:
+ void performSearch();
+ void searchRequestFinished();
+ void searchRequestFailed(QString reason);
+
+ void requestPack();
+ void packRequestFinished();
+ void packRequestFailed(QString reason);
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, bool stale);
+
+private:
+ void requestLogo(QString file, QString url);
+
+private:
+ QList<ModpacksCH::Modpack> modpacks;
+ LogoMap m_logoMap;
+
+ QString currentSearchTerm;
+ enum SearchState {
+ None,
+ CanPossiblyFetchMore,
+ ResetRequested,
+ Finished,
+ Failed,
+ } searchState = None;
+ NetJobPtr jobPtr;
+ int currentPack;
+ QList<int> remainingPacks;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/pages/modplatform/ftb/FtbPage.cpp b/launcher/pages/modplatform/ftb/FtbPage.cpp
new file mode 100644
index 00000000..b7f35c5d
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbPage.cpp
@@ -0,0 +1,145 @@
+#include "FtbPage.h"
+#include "ui_FtbPage.h"
+
+#include <QKeyEvent>
+
+#include "dialogs/NewInstanceDialog.h"
+#include "modplatform/modpacksch/FTBPackInstallTask.h"
+
+#include "HoeDown.h"
+
+FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog)
+{
+ ui->setupUi(this);
+
+ filterModel = new Ftb::FilterModel(this);
+ listModel = new Ftb::ListModel(this);
+ filterModel->setSourceModel(listModel);
+ ui->packView->setModel(filterModel);
+ ui->packView->setSortingEnabled(true);
+ ui->packView->header()->hide();
+ ui->packView->setIndentation(0);
+
+ ui->searchEdit->installEventFilter(this);
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ for(int i = 0; i < filterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i));
+ }
+ ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting());
+
+ connect(ui->searchButton, &QPushButton::clicked, this, &FtbPage::triggerSearch);
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &FtbPage::onSortingSelectionChanged);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged);
+}
+
+FtbPage::~FtbPage()
+{
+ delete ui;
+}
+
+bool FtbPage::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
+
+bool FtbPage::shouldDisplay() const
+{
+ return true;
+}
+
+void FtbPage::openedImpl()
+{
+ triggerSearch();
+ suggestCurrent();
+}
+
+void FtbPage::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if (selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name, new ModpacksCH::PackInstallTask(selected, selectedVersion));
+ for(auto art : selected.art) {
+ if(art.type == "square") {
+ QString editedLogoName;
+ editedLogoName = selected.name;
+
+ listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName);
+ });
+ }
+ }
+}
+
+void FtbPage::triggerSearch()
+{
+ listModel->searchWithTerm(ui->searchEdit->text());
+}
+
+void FtbPage::onSortingSelectionChanged(QString data)
+{
+ auto toSet = filterModel->getAvailableSortings().value(data);
+ filterModel->setSorting(toSet);
+}
+
+void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ ui->versionSelectionBox->clear();
+
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+
+ selected = filterModel->data(first, Qt::UserRole).value<ModpacksCH::Modpack>();
+
+ HoeDown hoedown;
+ QString output = hoedown.process(selected.description.toUtf8());
+ ui->packDescription->setHtml(output);
+
+ // reverse foreach, so that the newest versions are first
+ for (auto i = selected.versions.size(); i--;) {
+ ui->versionSelectionBox->addItem(selected.versions.at(i).name);
+ }
+
+ suggestCurrent();
+}
+
+void FtbPage::onVersionSelectionChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
diff --git a/launcher/pages/modplatform/ftb/FtbPage.h b/launcher/pages/modplatform/ftb/FtbPage.h
new file mode 100644
index 00000000..c9c93897
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbPage.h
@@ -0,0 +1,80 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "FtbFilterModel.h"
+#include "FtbListModel.h"
+
+#include <QWidget>
+
+#include "MultiMC.h"
+#include "pages/BasePage.h"
+#include "tasks/Task.h"
+
+namespace Ui
+{
+ class FtbPage;
+}
+
+class NewInstanceDialog;
+
+class FtbPage : public QWidget, public BasePage
+{
+Q_OBJECT
+
+public:
+ explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~FtbPage();
+ virtual QString displayName() const override
+ {
+ return tr("FTB");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("ftb_logo");
+ }
+ virtual QString id() const override
+ {
+ return "ftb";
+ }
+ virtual QString helpPage() const override
+ {
+ return "FTB-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject * watched, QEvent * event) override;
+
+private:
+ void suggestCurrent();
+
+private slots:
+ void triggerSearch();
+ void onSortingSelectionChanged(QString data);
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+ void onVersionSelectionChanged(QString data);
+
+private:
+ Ui::FtbPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Ftb::ListModel* listModel = nullptr;
+ Ftb::FilterModel* filterModel = nullptr;
+
+ ModpacksCH::Modpack selected;
+ QString selectedVersion;
+};
diff --git a/launcher/pages/modplatform/ftb/FtbPage.ui b/launcher/pages/modplatform/ftb/FtbPage.ui
new file mode 100644
index 00000000..135afc6d
--- /dev/null
+++ b/launcher/pages/modplatform/ftb/FtbPage.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FtbPage</class>
+ <widget class="QWidget" name="FtbPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>875</width>
+ <height>745</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0" rowminimumheight="0" columnminimumwidth="0,0,0">
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Version selected:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox"/>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="0">
+ <widget class="QTreeView" name="packView">
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QTextBrowser" name="packDescription">
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="openLinks">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>versionSelectionBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/pages/modplatform/legacy_ftb/ListModel.cpp
new file mode 100644
index 00000000..32596fb3
--- /dev/null
+++ b/launcher/pages/modplatform/legacy_ftb/ListModel.cpp
@@ -0,0 +1,260 @@
+#include "ListModel.h"
+#include "MultiMC.h"
+
+#include <MMCStrings.h>
+#include <Version.h>
+
+#include <QtMath>
+#include <QLabel>
+
+#include <RWStorage.h>
+#include <Env.h>
+
+#include <BuildConfig.h>
+
+namespace LegacyFTB {
+
+FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
+{
+ currentSorting = Sorting::ByGameVersion;
+ sortings.insert(tr("Sort by name"), Sorting::ByName);
+ sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion);
+}
+
+bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value<Modpack>();
+ Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value<Modpack>();
+
+ if(currentSorting == Sorting::ByGameVersion) {
+ Version lv(leftPack.mcVersion);
+ Version rv(rightPack.mcVersion);
+ return lv < rv;
+
+ } else if(currentSorting == Sorting::ByName) {
+ return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0;
+ }
+
+ //UHM, some inavlid value set?!
+ qWarning() << "Invalid sorting set!";
+ return true;
+}
+
+bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+ return true;
+}
+
+const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()
+{
+ return sortings;
+}
+
+QString FilterModel::translateCurrentSorting()
+{
+ return sortings.key(currentSorting);
+}
+
+void FilterModel::setSorting(Sorting s)
+{
+ currentSorting = s;
+ invalidate();
+}
+
+FilterModel::Sorting FilterModel::getCurrentSorting()
+{
+ return currentSorting;
+}
+
+ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+ListModel::~ListModel()
+{
+}
+
+QString ListModel::translatePackType(PackType type) const
+{
+ switch(type)
+ {
+ case PackType::Public:
+ return tr("Public Modpack");
+ case PackType::ThirdParty:
+ return tr("Third Party Modpack");
+ case PackType::Private:
+ return tr("Private Modpack");
+ }
+ qWarning() << "Unknown FTB modpack type:" << int(type);
+ return QString();
+}
+
+int ListModel::rowCount(const QModelIndex &parent) const
+{
+ return modpacks.size();
+}
+
+int ListModel::columnCount(const QModelIndex &parent) const
+{
+ return 1;
+}
+
+QVariant ListModel::data(const QModelIndex &index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name + "\n" + translatePackType(pack.type);
+ }
+ else if (role == Qt::ToolTipRole)
+ {
+ if(pack.description.length() > 100)
+ {
+ //some magic to prevent to long tooltips and replace html linebreaks
+ QString edit = pack.description.left(97);
+ edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
+ return edit;
+
+ }
+ return pack.description;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.logo))
+ {
+ return (m_logoMap.value(pack.logo));
+ }
+ QIcon icon = MMC->getThemedIcon("screenshot-placeholder");
+ ((ListModel *)this)->requestLogo(pack.logo);
+ return icon;
+ }
+ else if(role == Qt::TextColorRole)
+ {
+ if(pack.broken)
+ {
+ //FIXME: Hardcoded color
+ return QColor(255, 0, 50);
+ }
+ else if(pack.bugged)
+ {
+ //FIXME: Hardcoded color
+ //bugged pack, currently only indicates bugged xml
+ return QColor(244, 229, 66);
+ }
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+
+ return QVariant();
+}
+
+void ListModel::fill(ModpackList modpacks)
+{
+ beginResetModel();
+ this->modpacks = modpacks;
+ endResetModel();
+}
+
+void ListModel::addPack(Modpack modpack)
+{
+ beginResetModel();
+ this->modpacks.append(modpack);
+ endResetModel();
+}
+
+void ListModel::clear()
+{
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+}
+
+Modpack ListModel::at(int row)
+{
+ return modpacks.at(row);
+}
+
+void ListModel::remove(int row)
+{
+ if(row < 0 || row >= modpacks.size())
+ {
+ qWarning() << "Attempt to remove FTB modpacks with invalid row" << row;
+ return;
+ }
+ beginRemoveRows(QModelIndex(), row, row);
+ modpacks.removeAt(row);
+ endRemoveRows();
+}
+
+void ListModel::logoLoaded(QString logo, QIcon out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, out);
+ emit dataChanged(createIndex(0, 0), createIndex(1, 0));
+}
+
+void ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void ListModel::requestLogo(QString file)
+{
+ if(m_loadingLogos.contains(file) || m_failedLogos.contains(file))
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0)));
+ NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file));
+ job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry));
+
+ auto fullPath = entry->getFullPath();
+ QObject::connect(job, &NetJob::finished, this, [this, file, fullPath]
+ {
+ emit logoLoaded(file, QIcon(fullPath));
+ if(waitingCallbacks.contains(file))
+ {
+ waitingCallbacks.value(file)(fullPath);
+ }
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, file]
+ {
+ emit logoFailed(file);
+ });
+
+ job->start();
+
+ m_loadingLogos.append(file);
+}
+
+void ListModel::getLogo(const QString &logo, LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo);
+ }
+}
+
+Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
+{
+ return QAbstractListModel::flags(index);
+}
+
+}
diff --git a/launcher/pages/modplatform/legacy_ftb/ListModel.h b/launcher/pages/modplatform/legacy_ftb/ListModel.h
new file mode 100644
index 00000000..c55df000
--- /dev/null
+++ b/launcher/pages/modplatform/legacy_ftb/ListModel.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include <modplatform/legacy_ftb/PackHelpers.h>
+#include <RWStorage.h>
+
+#include <QAbstractListModel>
+#include <QSortFilterProxyModel>
+#include <QThreadPool>
+#include <QIcon>
+#include <QStyledItemDelegate>
+
+#include <functional>
+
+namespace LegacyFTB {
+
+typedef QMap<QString, QIcon> FTBLogoMap;
+typedef std::function<void(QString)> LogoCallback;
+
+class FilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+public:
+ FilterModel(QObject* parent = Q_NULLPTR);
+ enum Sorting {
+ ByName,
+ ByGameVersion
+ };
+ const QMap<QString, Sorting> getAvailableSortings();
+ QString translateCurrentSorting();
+ void setSorting(Sorting sorting);
+ Sorting getCurrentSorting();
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+
+private:
+ QMap<QString, Sorting> sortings;
+ Sorting currentSorting;
+
+};
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+private:
+ ModpackList modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ FTBLogoMap m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ void requestLogo(QString file);
+ QString translatePackType(PackType type) const;
+
+
+private slots:
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QIcon out);
+
+public:
+ ListModel(QObject *parent);
+ ~ListModel();
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ void fill(ModpackList modpacks);
+ void addPack(Modpack modpack);
+ void clear();
+ void remove(int row);
+
+ Modpack at(int row);
+ void getLogo(const QString &logo, LogoCallback callback);
+};
+
+}
diff --git a/launcher/pages/modplatform/legacy_ftb/Page.cpp b/launcher/pages/modplatform/legacy_ftb/Page.cpp
new file mode 100644
index 00000000..a438f76c
--- /dev/null
+++ b/launcher/pages/modplatform/legacy_ftb/Page.cpp
@@ -0,0 +1,369 @@
+#include "Page.h"
+#include "ui_Page.h"
+
+#include <QInputDialog>
+
+#include "MultiMC.h"
+#include "dialogs/CustomMessageBox.h"
+#include "dialogs/NewInstanceDialog.h"
+#include "modplatform/legacy_ftb/PackFetchTask.h"
+#include "modplatform/legacy_ftb/PackInstallTask.h"
+#include "modplatform/legacy_ftb/PrivatePackManager.h"
+#include "ListModel.h"
+
+namespace LegacyFTB {
+
+Page::Page(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), dialog(dialog), ui(new Ui::Page)
+{
+ ftbFetchTask.reset(new PackFetchTask());
+ ftbPrivatePacks.reset(new PrivatePackManager());
+
+ ui->setupUi(this);
+
+ {
+ publicFilterModel = new FilterModel(this);
+ publicListModel = new ListModel(this);
+ publicFilterModel->setSourceModel(publicListModel);
+
+ ui->publicPackList->setModel(publicFilterModel);
+ ui->publicPackList->setSortingEnabled(true);
+ ui->publicPackList->header()->hide();
+ ui->publicPackList->setIndentation(0);
+ ui->publicPackList->setIconSize(QSize(42, 42));
+
+ for(int i = 0; i < publicFilterModel->getAvailableSortings().size(); i++)
+ {
+ ui->sortByBox->addItem(publicFilterModel->getAvailableSortings().keys().at(i));
+ }
+
+ ui->sortByBox->setCurrentText(publicFilterModel->translateCurrentSorting());
+ }
+
+ {
+ thirdPartyFilterModel = new FilterModel(this);
+ thirdPartyModel = new ListModel(this);
+ thirdPartyFilterModel->setSourceModel(thirdPartyModel);
+
+ ui->thirdPartyPackList->setModel(thirdPartyFilterModel);
+ ui->thirdPartyPackList->setSortingEnabled(true);
+ ui->thirdPartyPackList->header()->hide();
+ ui->thirdPartyPackList->setIndentation(0);
+ ui->thirdPartyPackList->setIconSize(QSize(42, 42));
+
+ thirdPartyFilterModel->setSorting(publicFilterModel->getCurrentSorting());
+ }
+
+ {
+ privateFilterModel = new FilterModel(this);
+ privateListModel = new ListModel(this);
+ privateFilterModel->setSourceModel(privateListModel);
+
+ ui->privatePackList->setModel(privateFilterModel);
+ ui->privatePackList->setSortingEnabled(true);
+ ui->privatePackList->header()->hide();
+ ui->privatePackList->setIndentation(0);
+ ui->privatePackList->setIconSize(QSize(42, 42));
+
+ privateFilterModel->setSorting(publicFilterModel->getCurrentSorting());
+ }
+
+ ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
+
+ connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged);
+ connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged);
+
+ connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged);
+ connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged);
+ connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged);
+
+ connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked);
+ connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked);
+
+ connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged);
+
+ // ui->modpackInfo->setOpenExternalLinks(true);
+
+ ui->publicPackList->selectionModel()->reset();
+ ui->thirdPartyPackList->selectionModel()->reset();
+ ui->privatePackList->selectionModel()->reset();
+
+ onTabChanged(ui->tabWidget->currentIndex());
+}
+
+Page::~Page()
+{
+ delete ui;
+}
+
+bool Page::shouldDisplay() const
+{
+ return true;
+}
+
+void Page::openedImpl()
+{
+ if(!initialized)
+ {
+ connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully);
+ connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed);
+
+ connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully);
+ connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed);
+
+ ftbFetchTask->fetch();
+ ftbPrivatePacks->load();
+ ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().toList());
+ initialized = true;
+ }
+ suggestCurrent();
+}
+
+void Page::suggestCurrent()
+{
+ if(!isOpened)
+ {
+ return;
+ }
+
+ if(selected.broken || selectedVersion.isEmpty())
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ dialog->setSuggestedPack(selected.name, new PackInstallTask(selected, selectedVersion));
+ QString editedLogoName;
+ if(selected.logo.toLower().startsWith("ftb"))
+ {
+ editedLogoName = selected.logo;
+ }
+ else
+ {
+ editedLogoName = "ftb_" + selected.logo;
+ }
+
+ editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png"));
+
+ if(selected.type == PackType::Public)
+ {
+ publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+ }
+ else if (selected.type == PackType::ThirdParty)
+ {
+ thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+ }
+ else if (selected.type == PackType::Private)
+ {
+ privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+ }
+}
+
+void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks)
+{
+ publicListModel->fill(publicPacks);
+ thirdPartyModel->fill(thirdPartyPacks);
+}
+
+void Page::ftbPackDataDownloadFailed(QString reason)
+{
+ //TODO: Display the error
+}
+
+void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack)
+{
+ privateListModel->addPack(pack);
+}
+
+void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode)
+{
+ auto reply = QMessageBox::question(
+ this,
+ tr("FTB private packs"),
+ tr("Failed to download pack information for code %1.\nShould it be removed now?").arg(packCode)
+ );
+ if(reply == QMessageBox::Yes)
+ {
+ ftbPrivatePacks->remove(packCode);
+ }
+}
+
+void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev)
+{
+ if(!now.isValid())
+ {
+ onPackSelectionChanged();
+ return;
+ }
+ Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value<Modpack>();
+ onPackSelectionChanged(&selectedPack);
+}
+
+void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev)
+{
+ if(!now.isValid())
+ {
+ onPackSelectionChanged();
+ return;
+ }
+ Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value<Modpack>();
+ onPackSelectionChanged(&selectedPack);
+}
+
+void Page::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev)
+{
+ if(!now.isValid())
+ {
+ onPackSelectionChanged();
+ return;
+ }
+ Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value<Modpack>();
+ onPackSelectionChanged(&selectedPack);
+}
+
+void Page::onPackSelectionChanged(Modpack* pack)
+{
+ ui->versionSelectionBox->clear();
+ if(pack)
+ {
+ currentModpackInfo->setHtml("Pack by <b>" + pack->author + "</b>" +
+ "<br>Minecraft " + pack->mcVersion + "<br>" + "<br>" + pack->description + "<ul><li>" + pack->mods.replace(";", "</li><li>")
+ + "</li></ul>");
+ bool currentAdded = false;
+
+ for(int i = 0; i < pack->oldVersions.size(); i++)
+ {
+ if(pack->currentVersion == pack->oldVersions.at(i))
+ {
+ currentAdded = true;
+ }
+ ui->versionSelectionBox->addItem(pack->oldVersions.at(i));
+ }
+
+ if(!currentAdded)
+ {
+ ui->versionSelectionBox->addItem(pack->currentVersion);
+ }
+ selected = *pack;
+ }
+ else
+ {
+ currentModpackInfo->setHtml("");
+ ui->versionSelectionBox->clear();
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ return;
+ }
+ suggestCurrent();
+}
+
+void Page::onVersionSelectionItemChanged(QString data)
+{
+ if(data.isNull() || data.isEmpty())
+ {
+ selectedVersion = "";
+ return;
+ }
+
+ selectedVersion = data;
+ suggestCurrent();
+}
+
+void Page::onSortingSelectionChanged(QString data)
+{
+ FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data);
+ publicFilterModel->setSorting(toSet);
+ thirdPartyFilterModel->setSorting(toSet);
+ privateFilterModel->setSorting(toSet);
+}
+
+void Page::onTabChanged(int tab)
+{
+ if(tab == 1)
+ {
+ currentModel = thirdPartyFilterModel;
+ currentList = ui->thirdPartyPackList;
+ currentModpackInfo = ui->thirdPartyPackDescription;
+ }
+ else if(tab == 2)
+ {
+ currentModel = privateFilterModel;
+ currentList = ui->privatePackList;
+ currentModpackInfo = ui->privatePackDescription;
+ }
+ else
+ {
+ currentModel = publicFilterModel;
+ currentList = ui->publicPackList;
+ currentModpackInfo = ui->publicPackDescription;
+ }
+
+ currentList->selectionModel()->reset();
+ QModelIndex idx = currentList->currentIndex();
+ if(idx.isValid())
+ {
+ auto pack = currentModel->data(idx, Qt::UserRole).value<Modpack>();
+ onPackSelectionChanged(&pack);
+ }
+ else
+ {
+ onPackSelectionChanged();
+ }
+}
+
+void Page::onAddPackClicked()
+{
+ bool ok;
+ QString text = QInputDialog::getText(
+ this,
+ tr("Add FTB pack"),
+ tr("Enter pack code:"),
+ QLineEdit::Normal,
+ QString(),
+ &ok
+ );
+ if(ok && !text.isEmpty())
+ {
+ ftbPrivatePacks->add(text);
+ ftbFetchTask->fetchPrivate({text});
+ }
+}
+
+void Page::onRemovePackClicked()
+{
+ auto index = ui->privatePackList->currentIndex();
+ if(!index.isValid())
+ {
+ return;
+ }
+ auto row = index.row();
+ Modpack pack = privateListModel->at(row);
+ auto answer = QMessageBox::question(
+ this,
+ tr("Remove pack"),
+ tr("Are you sure you want to remove pack %1?").arg(pack.name),
+ QMessageBox::Yes | QMessageBox::No
+ );
+ if(answer != QMessageBox::Yes)
+ {
+ return;
+ }
+
+ ftbPrivatePacks->remove(pack.packCode);
+ privateListModel->remove(row);
+ onPackSelectionChanged();
+}
+
+}
diff --git a/launcher/pages/modplatform/legacy_ftb/Page.h b/launcher/pages/modplatform/legacy_ftb/Page.h
new file mode 100644
index 00000000..e840216e
--- /dev/null
+++ b/launcher/pages/modplatform/legacy_ftb/Page.h
@@ -0,0 +1,119 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+#include <QTreeView>
+#include <QTextBrowser>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "tasks/Task.h"
+#include "modplatform/legacy_ftb/PackHelpers.h"
+#include "modplatform/legacy_ftb/PackFetchTask.h"
+#include "QObjectPtr.h"
+
+class NewInstanceDialog;
+
+namespace LegacyFTB {
+
+namespace Ui
+{
+class Page;
+}
+
+class ListModel;
+class FilterModel;
+class PrivatePackListModel;
+class PrivatePackFilterModel;
+class PrivatePackManager;
+
+class Page : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit Page(NewInstanceDialog * dialog, QWidget *parent = 0);
+ virtual ~Page();
+ QString displayName() const override
+ {
+ return tr("FTB Legacy");
+ }
+ QIcon icon() const override
+ {
+ return MMC->getThemedIcon("ftb_logo");
+ }
+ QString id() const override
+ {
+ return "legacy_ftb";
+ }
+ QString helpPage() const override
+ {
+ return "FTB-platform";
+ }
+ bool shouldDisplay() const override;
+ void openedImpl() override;
+
+private:
+ void suggestCurrent();
+ void onPackSelectionChanged(Modpack *pack = nullptr);
+
+private slots:
+ void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks);
+ void ftbPackDataDownloadFailed(QString reason);
+
+ void ftbPrivatePackDataDownloadSuccessfully(Modpack pack);
+ void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode);
+
+ void onSortingSelectionChanged(QString data);
+ void onVersionSelectionItemChanged(QString data);
+
+ void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second);
+ void onThirdPartyPackSelectionChanged(QModelIndex first, QModelIndex second);
+ void onPrivatePackSelectionChanged(QModelIndex first, QModelIndex second);
+
+ void onTabChanged(int tab);
+
+ void onAddPackClicked();
+ void onRemovePackClicked();
+
+private:
+ FilterModel* currentModel = nullptr;
+ QTreeView* currentList = nullptr;
+ QTextBrowser* currentModpackInfo = nullptr;
+
+ bool initialized = false;
+ Modpack selected;
+ QString selectedVersion;
+
+ ListModel* publicListModel = nullptr;
+ FilterModel* publicFilterModel = nullptr;
+
+ ListModel *thirdPartyModel = nullptr;
+ FilterModel *thirdPartyFilterModel = nullptr;
+
+ ListModel *privateListModel = nullptr;
+ FilterModel *privateFilterModel = nullptr;
+
+ unique_qobject_ptr<PackFetchTask> ftbFetchTask;
+ std::unique_ptr<PrivatePackManager> ftbPrivatePacks;
+
+ NewInstanceDialog* dialog = nullptr;
+
+ Ui::Page *ui = nullptr;
+};
+
+}
diff --git a/launcher/pages/modplatform/legacy_ftb/Page.ui b/launcher/pages/modplatform/legacy_ftb/Page.ui
new file mode 100644
index 00000000..15e5d432
--- /dev/null
+++ b/launcher/pages/modplatform/legacy_ftb/Page.ui
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LegacyFTB::Page</class>
+ <widget class="QWidget" name="LegacyFTB::Page">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>709</width>
+ <height>602</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Public</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QTreeView" name="publicPackList">
+ <property name="maximumSize">
+ <size>
+ <width>250</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QTextBrowser" name="publicPackDescription"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_2">
+ <attribute name="title">
+ <string>3rd Party</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="0" column="1">
+ <widget class="QTextBrowser" name="thirdPartyPackDescription"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QTreeView" name="thirdPartyPackList">
+ <property name="maximumSize">
+ <size>
+ <width>250</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_3">
+ <attribute name="title">
+ <string>Private</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="0" column="0">
+ <widget class="QTreeView" name="privatePackList">
+ <property name="maximumSize">
+ <size>
+ <width>250</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QPushButton" name="addPackBtn">
+ <property name="text">
+ <string>Add pack</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QPushButton" name="removePackBtn">
+ <property name="text">
+ <string>Remove selected pack</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" rowspan="3">
+ <widget class="QTextBrowser" name="privatePackDescription"/>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="0" column="1">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Version selected:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QComboBox" name="versionSelectionBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QComboBox" name="sortByBox">
+ <property name="minimumSize">
+ <size>
+ <width>265</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pages/modplatform/technic/TechnicData.h b/launcher/pages/modplatform/technic/TechnicData.h
new file mode 100644
index 00000000..50fd75e8
--- /dev/null
+++ b/launcher/pages/modplatform/technic/TechnicData.h
@@ -0,0 +1,42 @@
+/* Copyright 2020-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QList>
+#include <QString>
+
+namespace Technic {
+struct Modpack {
+ QString slug;
+
+ QString name;
+ QString logoUrl;
+ QString logoName;
+
+ bool broken = true;
+
+ QString url;
+ bool isSolder = false;
+ QString minecraftVersion;
+
+ bool metadataLoaded = false;
+ QString websiteUrl;
+ QString author;
+ QString description;
+};
+}
+
+Q_DECLARE_METATYPE(Technic::Modpack)
diff --git a/launcher/pages/modplatform/technic/TechnicModel.cpp b/launcher/pages/modplatform/technic/TechnicModel.cpp
new file mode 100644
index 00000000..def30783
--- /dev/null
+++ b/launcher/pages/modplatform/technic/TechnicModel.cpp
@@ -0,0 +1,238 @@
+/* Copyright 2020-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "TechnicModel.h"
+#include "Env.h"
+#include "MultiMC.h"
+#include "Json.h"
+
+#include <QIcon>
+
+Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+Technic::ListModel::~ListModel()
+{
+}
+
+QVariant Technic::ListModel::data(const QModelIndex& index, int role) const
+{
+ int pos = index.row();
+ if(pos >= modpacks.size() || pos < 0 || !index.isValid())
+ {
+ return QString("INVALID INDEX %1").arg(pos);
+ }
+
+ Modpack pack = modpacks.at(pos);
+ if(role == Qt::DisplayRole)
+ {
+ return pack.name;
+ }
+ else if(role == Qt::DecorationRole)
+ {
+ if(m_logoMap.contains(pack.logoName))
+ {
+ return (m_logoMap.value(pack.logoName));
+ }
+ QIcon icon = MMC->getThemedIcon("screenshot-placeholder");
+ ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
+ return icon;
+ }
+ else if(role == Qt::UserRole)
+ {
+ QVariant v;
+ v.setValue(pack);
+ return v;
+ }
+ return QVariant();
+}
+
+int Technic::ListModel::columnCount(const QModelIndex&) const
+{
+ return 1;
+}
+
+int Technic::ListModel::rowCount(const QModelIndex&) const
+{
+ return modpacks.size();
+}
+
+void Technic::ListModel::searchWithTerm(const QString& term)
+{
+ if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) {
+ return;
+ }
+ currentSearchTerm = term;
+ if(jobPtr) {
+ jobPtr->abort();
+ searchState = ResetRequested;
+ return;
+ }
+ else {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+ searchState = None;
+ }
+ performSearch();
+}
+
+void Technic::ListModel::performSearch()
+{
+ NetJob *netJob = new NetJob("Technic::Search");
+ QString searchUrl = "";
+ if (currentSearchTerm.isEmpty()) {
+ searchUrl = "https://api.technicpack.net/trending?build=multimc";
+ }
+ else
+ {
+ searchUrl = QString(
+ "https://api.technicpack.net/search?build=multimc&q=%1"
+ ).arg(currentSearchTerm);
+ }
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+ QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed);
+}
+
+void Technic::ListModel::searchRequestFinished()
+{
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ QList<Modpack> newList;
+ try {
+ auto root = Json::requireObject(doc);
+ auto objs = Json::requireArray(root, "modpacks");
+ for (auto technicPack: objs) {
+ Modpack pack;
+ auto technicPackObject = Json::requireObject(technicPack);
+ pack.name = Json::requireString(technicPackObject, "name");
+ pack.slug = Json::requireString(technicPackObject, "slug");
+ if (pack.slug == "vanilla")
+ continue;
+
+ auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null");
+ if(rawURL == "null") {
+ pack.logoUrl = "null";
+ pack.logoName = "null";
+ }
+ else {
+ pack.logoUrl = rawURL;
+ pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
+ }
+ pack.broken = false;
+ newList.append(pack);
+ }
+ }
+ catch (const JSONValidationError &err)
+ {
+ qCritical() << "Couldn't parse technic search results:" << err.cause() ;
+ return;
+ }
+ searchState = Finished;
+ beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1);
+ modpacks.append(newList);
+ endInsertRows();
+}
+
+void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback)
+{
+ if(m_logoMap.contains(logo))
+ {
+ callback(ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath());
+ }
+ else
+ {
+ requestLogo(logo, logoUrl);
+ }
+}
+
+void Technic::ListModel::searchRequestFailed()
+{
+ jobPtr.reset();
+
+ if(searchState == ResetRequested)
+ {
+ beginResetModel();
+ modpacks.clear();
+ endResetModel();
+
+ performSearch();
+ }
+ else
+ {
+ searchState = Finished;
+ }
+}
+
+
+void Technic::ListModel::logoLoaded(QString logo, QString out)
+{
+ m_loadingLogos.removeAll(logo);
+ m_logoMap.insert(logo, QIcon(out));
+ for(int i = 0; i < modpacks.size(); i++)
+ {
+ if(modpacks[i].logoName == logo)
+ {
+ emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
+ }
+ }
+}
+
+void Technic::ListModel::logoFailed(QString logo)
+{
+ m_failedLogos.append(logo);
+ m_loadingLogos.removeAll(logo);
+}
+
+void Technic::ListModel::requestLogo(QString logo, QString url)
+{
+ if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null")
+ {
+ return;
+ }
+
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo));
+ NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo));
+ job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
+
+ auto fullPath = entry->getFullPath();
+
+ QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath]
+ {
+ logoLoaded(logo, fullPath);
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, logo]
+ {
+ logoFailed(logo);
+ });
+
+ job->start();
+
+ m_loadingLogos.append(logo);
+}
diff --git a/launcher/pages/modplatform/technic/TechnicModel.h b/launcher/pages/modplatform/technic/TechnicModel.h
new file mode 100644
index 00000000..82a03842
--- /dev/null
+++ b/launcher/pages/modplatform/technic/TechnicModel.h
@@ -0,0 +1,70 @@
+/* Copyright 2020-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QModelIndex>
+
+#include "TechnicData.h"
+#include "net/NetJob.h"
+
+namespace Technic {
+
+typedef std::function<void(QString)> LogoCallback;
+
+class ListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ ListModel(QObject *parent);
+ virtual ~ListModel();
+
+ virtual QVariant data(const QModelIndex& index, int role) const;
+ virtual int columnCount(const QModelIndex& parent) const;
+ virtual int rowCount(const QModelIndex& parent) const;
+
+ void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback);
+ void searchWithTerm(const QString & term);
+
+private slots:
+ void searchRequestFinished();
+ void searchRequestFailed();
+
+ void logoFailed(QString logo);
+ void logoLoaded(QString logo, QString out);
+
+private:
+ void performSearch();
+ void requestLogo(QString logo, QString url);
+
+private:
+ QList<Modpack> modpacks;
+ QStringList m_failedLogos;
+ QStringList m_loadingLogos;
+ QMap<QString, QIcon> m_logoMap;
+ QMap<QString, LogoCallback> waitingCallbacks;
+
+ QString currentSearchTerm;
+ enum SearchState {
+ None,
+ ResetRequested,
+ Finished
+ } searchState = None;
+ NetJobPtr jobPtr;
+ QByteArray response;
+};
+
+}
diff --git a/launcher/pages/modplatform/technic/TechnicPage.cpp b/launcher/pages/modplatform/technic/TechnicPage.cpp
new file mode 100644
index 00000000..e836f767
--- /dev/null
+++ b/launcher/pages/modplatform/technic/TechnicPage.cpp
@@ -0,0 +1,198 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "TechnicPage.h"
+#include "ui_TechnicPage.h"
+
+#include "MultiMC.h"
+#include "dialogs/NewInstanceDialog.h"
+#include "TechnicModel.h"
+#include <QKeyEvent>
+#include "modplatform/technic/SingleZipPackInstallTask.h"
+#include "modplatform/technic/SolderPackInstallTask.h"
+#include "Json.h"
+
+TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
+ : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog)
+{
+ ui->setupUi(this);
+ connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch);
+ ui->searchEdit->installEventFilter(this);
+ model = new Technic::ListModel(this);
+ ui->packView->setModel(model);
+ connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged);
+}
+
+bool TechnicPage::eventFilter(QObject* watched, QEvent* event)
+{
+ if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Return) {
+ triggerSearch();
+ keyEvent->accept();
+ return true;
+ }
+ }
+ return QWidget::eventFilter(watched, event);
+}
+
+TechnicPage::~TechnicPage()
+{
+ delete ui;
+}
+
+bool TechnicPage::shouldDisplay() const
+{
+ return true;
+}
+
+void TechnicPage::openedImpl()
+{
+ suggestCurrent();
+ triggerSearch();
+}
+
+void TechnicPage::triggerSearch() {
+ model->searchWithTerm(ui->searchEdit->text());
+}
+
+void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+{
+ if(!first.isValid())
+ {
+ if(isOpened)
+ {
+ dialog->setSuggestedPack();
+ }
+ //ui->frame->clear();
+ return;
+ }
+
+ current = model->data(first, Qt::UserRole).value<Technic::Modpack>();
+ suggestCurrent();
+}
+
+void TechnicPage::suggestCurrent()
+{
+ if (!isOpened)
+ {
+ return;
+ }
+ if (current.broken)
+ {
+ dialog->setSuggestedPack();
+ return;
+ }
+
+ QString editedLogoName = "technic_" + current.logoName.section(".", 0, 0);
+ model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
+ {
+ dialog->setSuggestedIconFromFile(logo, editedLogoName);
+ });
+
+ if (current.metadataLoaded)
+ {
+ metadataLoaded();
+ return;
+ }
+
+ NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name));
+ std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
+ QString slug = current.slug;
+ netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get()));
+ QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug]
+ {
+ if (current.slug != slug)
+ {
+ return;
+ }
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ QJsonObject obj = doc.object();
+ if(parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ if (!obj.contains("url"))
+ {
+ qWarning() << "Json doesn't contain an url key";
+ return;
+ }
+ QJsonValueRef url = obj["url"];
+ if (url.isString())
+ {
+ current.url = url.toString();
+ }
+ else
+ {
+ if (!obj.contains("solder"))
+ {
+ qWarning() << "Json doesn't contain a valid url or solder key";
+ return;
+ }
+ QJsonValueRef solderUrl = obj["solder"];
+ if (solderUrl.isString())
+ {
+ current.url = solderUrl.toString();
+ current.isSolder = true;
+ }
+ else
+ {
+ qWarning() << "Json doesn't contain a valid url or solder key";
+ return;
+ }
+ }
+
+ current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
+ current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__");
+ current.author = Json::ensureString(obj, "user", QString(), "__placeholder__");
+ current.description = Json::ensureString(obj, "description", QString(), "__placeholder__");
+ current.metadataLoaded = true;
+ metadataLoaded();
+ });
+ netJob->start();
+}
+
+// expects current.metadataLoaded to be true
+void TechnicPage::metadataLoaded()
+{
+ QString text = "";
+ QString name = current.name;
+
+ if (current.websiteUrl.isEmpty())
+ // This allows injecting HTML here.
+ text = name;
+ else
+ // URL not properly escaped for inclusion in HTML. The name allows for injecting HTML.
+ text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
+ if (!current.author.isEmpty()) {
+ // This allows injecting HTML here
+ text += tr(" by ") + current.author;
+ }
+
+ ui->frame->setModText(text);
+ ui->frame->setModDescription(current.description);
+ if (!current.isSolder)
+ {
+ dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion));
+ }
+ else
+ {
+ while (current.url.endsWith('/')) current.url.chop(1);
+ dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(current.url + "/modpack/" + current.slug, current.minecraftVersion));
+ }
+}
diff --git a/launcher/pages/modplatform/technic/TechnicPage.h b/launcher/pages/modplatform/technic/TechnicPage.h
new file mode 100644
index 00000000..27e1258a
--- /dev/null
+++ b/launcher/pages/modplatform/technic/TechnicPage.h
@@ -0,0 +1,78 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+#include "tasks/Task.h"
+#include "TechnicData.h"
+
+namespace Ui
+{
+class TechnicPage;
+}
+
+class NewInstanceDialog;
+
+namespace Technic {
+ class ListModel;
+}
+
+class TechnicPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit TechnicPage(NewInstanceDialog* dialog, QWidget *parent = 0);
+ virtual ~TechnicPage();
+ virtual QString displayName() const override
+ {
+ return tr("Technic");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("technic");
+ }
+ virtual QString id() const override
+ {
+ return "technic";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Technic-platform";
+ }
+ virtual bool shouldDisplay() const override;
+
+ void openedImpl() override;
+
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+private:
+ void suggestCurrent();
+ void metadataLoaded();
+
+private slots:
+ void triggerSearch();
+ void onSelectionChanged(QModelIndex first, QModelIndex second);
+
+private:
+ Ui::TechnicPage *ui = nullptr;
+ NewInstanceDialog* dialog = nullptr;
+ Technic::ListModel* model = nullptr;
+ Technic::Modpack current;
+};
diff --git a/launcher/pages/modplatform/technic/TechnicPage.ui b/launcher/pages/modplatform/technic/TechnicPage.ui
new file mode 100644
index 00000000..2ca45dd2
--- /dev/null
+++ b/launcher/pages/modplatform/technic/TechnicPage.ui
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TechnicPage</class>
+ <widget class="QWidget" name="TechnicPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>546</width>
+ <height>405</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLineEdit" name="searchEdit">
+ <property name="placeholderText">
+ <string>Search and filter ...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="searchButton">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QListView" name="packView">
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>48</width>
+ <height>48</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="MCModInfoFrame" name="frame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>MCModInfoFrame</class>
+ <extends>QFrame</extends>
+ <header>widgets/MCModInfoFrame.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>searchEdit</tabstop>
+ <tabstop>searchButton</tabstop>
+ <tabstop>packView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/pathmatcher/FSTreeMatcher.h b/launcher/pathmatcher/FSTreeMatcher.h
new file mode 100644
index 00000000..361924af
--- /dev/null
+++ b/launcher/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/launcher/pathmatcher/IPathMatcher.h b/launcher/pathmatcher/IPathMatcher.h
new file mode 100644
index 00000000..b60621c9
--- /dev/null
+++ b/launcher/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/launcher/pathmatcher/MultiMatcher.h b/launcher/pathmatcher/MultiMatcher.h
new file mode 100644
index 00000000..8bc1b6ee
--- /dev/null
+++ b/launcher/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/launcher/pathmatcher/RegexpMatcher.h b/launcher/pathmatcher/RegexpMatcher.h
new file mode 100644
index 00000000..825d488c
--- /dev/null
+++ b/launcher/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/launcher/resources/MultiMC.icns b/launcher/resources/MultiMC.icns
new file mode 100644
index 00000000..c4eb59d5
--- /dev/null
+++ b/launcher/resources/MultiMC.icns
Binary files differ
diff --git a/launcher/resources/MultiMC.ico b/launcher/resources/MultiMC.ico
new file mode 100644
index 00000000..a86a1f0d
--- /dev/null
+++ b/launcher/resources/MultiMC.ico
Binary files differ
diff --git a/launcher/resources/MultiMC.manifest b/launcher/resources/MultiMC.manifest
new file mode 100644
index 00000000..9278f6e4
--- /dev/null
+++ b/launcher/resources/MultiMC.manifest
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+ <assemblyIdentity name="MultiMC.Application.5" type="win32" version="5.0.0.0" />
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
+ <security>
+ <requestedPrivileges>
+ <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
+ </requestedPrivileges>
+ </security>
+ </trustInfo>
+ <dependency>
+ <dependentAssembly>
+ <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="x86" publicKeyToken="6595b64144ccf1df" language="*"/>
+ </dependentAssembly>
+ </dependency>
+ <description>Custom Minecraft launcher for managing multiple installs.</description>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!--The ID below indicates app support for Windows Vista -->
+ <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+ <!--The ID below indicates app support for Windows 7 -->
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+ <!--The ID below indicates app support for Windows 8 -->
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+ <!--The ID below indicates app support for Windows 8.1 -->
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+ <!--The ID below indicates app support for Windows 10 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ </application>
+ </compatibility>
+</assembly> \ No newline at end of file
diff --git a/launcher/resources/OSX/OSX.qrc b/launcher/resources/OSX/OSX.qrc
new file mode 100644
index 00000000..19fd4b6a
--- /dev/null
+++ b/launcher/resources/OSX/OSX.qrc
@@ -0,0 +1,38 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/icons/OSX">
+ <file>index.theme</file>
+ <file>scalable/about.svg</file>
+ <file>scalable/accounts.svg</file>
+ <file>scalable/bug.svg</file>
+ <file>scalable/centralmods.svg</file>
+ <file>scalable/checkupdate.svg</file>
+ <file>scalable/copy.svg</file>
+ <file>scalable/coremods.svg</file>
+ <file>scalable/externaltools.svg</file>
+ <file>scalable/help.svg</file>
+ <file>scalable/instance-settings.svg</file>
+ <file>scalable/jarmods.svg</file>
+ <file>scalable/java.svg</file>
+ <file>scalable/language.svg</file>
+ <file>scalable/loadermods.svg</file>
+ <file>scalable/log.svg</file>
+ <file>scalable/minecraft.svg</file>
+ <file>scalable/multimc.svg</file>
+ <file>scalable/new.svg</file>
+ <file>scalable/news.svg</file>
+ <file>scalable/notes.svg</file>
+ <file>scalable/patreon.svg</file>
+ <file>scalable/proxy.svg</file>
+ <file>scalable/quickmods.svg</file>
+ <file>scalable/refresh.svg</file>
+ <file>scalable/resourcepacks.svg</file>
+ <file>scalable/screenshots.svg</file>
+ <file>scalable/settings.svg</file>
+ <file>scalable/status-bad.svg</file>
+ <file>scalable/status-good.svg</file>
+ <file>scalable/status-yellow.svg</file>
+ <file>scalable/viewfolder.svg</file>
+ <file>scalable/worlds.svg</file>
+ </qresource>
+</RCC>
diff --git a/launcher/resources/OSX/index.theme b/launcher/resources/OSX/index.theme
new file mode 100644
index 00000000..7f90a32e
--- /dev/null
+++ b/launcher/resources/OSX/index.theme
@@ -0,0 +1,11 @@
+[Icon Theme]
+Name=OSX
+Comment=OSX theme by pexner
+Inherits=multimc
+Directories=scalable
+
+[scalable]
+Size=48
+Type=Scalable
+MinSize=16
+MaxSize=256
diff --git a/launcher/resources/OSX/scalable/about.svg b/launcher/resources/OSX/scalable/about.svg
new file mode 100644
index 00000000..eb87ccf1
--- /dev/null
+++ b/launcher/resources/OSX/scalable/about.svg
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M30,28H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.1,31.1,28,30,28z"/>
+<path fill="#FBFBFB" d="M30,27H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.1,31.1,27,30,27z"/>
+<g>
+ <polygon fill="#585858" points="17.2,20.2 17.2,15.2 14,15.2 14,16.5 15.2,16.5 15.2,20.2 14,20.2 14,21.5 18.5,21.5 18.5,20.2
+ "/>
+ <g id="_x38__12_">
+ <g>
+ <path fill="#585858" d="M16,9c-4.4,0-8,3.6-8,8c0,4.4,3.6,8,8,8s8-3.6,8-8C24,12.6,20.4,9,16,9z M16,24c-3.9,0-7-3.1-7-7
+ c0-3.9,3.1-7,7-7s7,3.1,7,7C23,20.9,19.9,24,16,24z"/>
+ </g>
+ </g>
+ <circle fill="#585858" cx="16.2" cy="13.2" r="1"/>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/accounts.svg b/launcher/resources/OSX/scalable/accounts.svg
new file mode 100644
index 00000000..163bcee0
--- /dev/null
+++ b/launcher/resources/OSX/scalable/accounts.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<g id="_x36__10_">
+ <g>
+ <path fill="#585858" d="M21,12.2c0.6-0.9,1-2,1-3.2c0-2.8-2-5-4.5-5S13,6.2,13,9c0,1.2,0.4,2.3,1,3.2c-1.2,0.4-2,1.5-2,2.8v2
+ c0,1.7,1.3,3,3,3h5c1.7,0,3-1.3,3-3v-2C23,13.7,22.2,12.6,21,12.2z"/>
+ </g>
+</g>
+<ellipse fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" cx="17.5" cy="9" rx="3.5" ry="4"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#CCCCCC" d="M22,15.2c0-1.1-0.8-1.9-1.9-2.2c-0.7,0.6-1.6,0.9-2.6,0.9
+ c-1,0-1.9-0.3-2.6-0.9c-1.1,0.2-1.9,1.1-1.9,2.2v1.5c0,1.2,1.1,2.2,2.5,2.2h4.1c1.4,0,2.5-1,2.5-2.2V15.2z"/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/bug.svg b/launcher/resources/OSX/scalable/bug.svg
new file mode 100644
index 00000000..00565bb6
--- /dev/null
+++ b/launcher/resources/OSX/scalable/bug.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M30,28H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.1,31.1,28,30,28z"/>
+<path fill="#FBFBFB" d="M30,27H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.1,31.1,27,30,27z"/>
+<path fill="#585858" d="M23.5,18.9c0.3,0,0.5-0.2,0.5-0.5c0-0.3-0.2-0.5-0.5-0.5H21c0-0.1,0-0.2,0-0.3c0-0.4,0-0.7,0-1
+ c0.7-0.1,1.2-0.4,1.6-0.9c1.3-1.5,0.9-4.4,0.9-4.5c0-0.3-0.3-0.4-0.5-0.4c-0.3,0-0.4,0.3-0.4,0.5c0,0,0.4,2.6-0.7,3.8
+ c-0.3,0.3-0.6,0.5-1.1,0.6c-0.1-0.2-0.2-0.3-0.3-0.5c-0.2-0.2-0.4-0.3-0.7-0.4c0.2-0.3,0.4-0.6,0.4-1.1c0-1.2-0.8-2.2-2.1-2.8l0,0
+ C17.9,9.8,17,9,16,9c-1,0-1.9,0.8-2,1.9l0,0v0c-1.3,0.6-2.1,1.6-2.1,2.8c0,0.5,0.1,0.8,0.4,1.1c-0.3,0.1-0.5,0.2-0.7,0.4
+ c-0.1,0.1-0.2,0.3-0.3,0.5c-0.4-0.1-0.8-0.2-1.1-0.6c-1-1.2-0.7-3.7-0.7-3.8c0-0.3-0.1-0.5-0.4-0.5c-0.3,0-0.5,0.1-0.5,0.4
+ c0,0.1-0.4,3,0.9,4.5c0.4,0.5,1,0.8,1.6,0.9c0,0.3,0,0.6,0,1c0,0.1,0,0.2,0,0.3H8.5c-0.3,0-0.5,0.2-0.5,0.5c0,0.3,0.2,0.5,0.5,0.5
+ h2.6c0,0.6,0.1,1.2,0.3,1.7c-0.8,0.1-1.5,0.4-2,1c-1.1,1.2-0.9,3-0.9,3c0,0.2,0.2,0.4,0.5,0.4c0,0,0,0,0,0c0.3,0,0.5-0.3,0.4-0.5
+ c0,0-0.1-1.4,0.7-2.3c0.4-0.4,0.9-0.6,1.6-0.7c0.9,1.8,2.5,3.1,4.3,3.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c1.9,0,3.5-1.2,4.3-3.1
+ c0.7,0,1.2,0.3,1.6,0.7c0.8,0.9,0.7,2.3,0.7,2.3c0,0.3,0.2,0.5,0.4,0.5c0,0,0,0,0,0c0.2,0,0.4-0.2,0.5-0.4c0-0.1,0.1-1.9-0.9-3
+ c-0.5-0.5-1.1-0.9-1.9-1c0.2-0.5,0.3-1.1,0.3-1.7H23.5z M16,9.9c0.4,0,0.8,0.2,1,0.6c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.2,0
+ c-0.2,0-0.4,0-0.5,0c-0.2,0-0.4,0-0.5,0c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.3,0C15.2,10.2,15.6,9.9,16,9.9z M12.8,13.7
+ c0-0.9,0.7-1.7,1.8-2.1c0.9-0.3,2-0.3,2.9,0c1.1,0.4,1.8,1.2,1.8,2.1c0,0.5-0.1,0.8-1.6,0.8c-0.5,0-1,0-1.6,0c0,0,0,0,0,0
+ c0,0,0,0,0,0c0,0,0,0,0,0c-0.6,0-1.1,0-1.6,0C13,14.5,12.8,14.2,12.8,13.7z M16.5,23.6v-5.1c0-0.3-0.2-0.5-0.5-0.5s-0.5,0.2-0.5,0.5
+ v5.1c-2-0.3-3.5-2.5-3.5-5.1c0-0.3,0-0.6,0-0.9c0-0.9,0-1.5,0.2-1.7c0.3-0.3,1.1-0.4,2.3-0.5c0.2,0,0.4,0,0.7,0c0.6,0,1.2,0,1.7,0
+ c0.3,0,0.5,0,0.8,0c1.2,0,1.9,0.2,2.2,0.5c0.3,0.3,0.2,0.8,0.2,1.7c0,0.2,0,0.5,0,0.8c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1
+ C20,21.1,18.5,23.3,16.5,23.6z"/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/centralmods.svg b/launcher/resources/OSX/scalable/centralmods.svg
new file mode 100644
index 00000000..37b821e4
--- /dev/null
+++ b/launcher/resources/OSX/scalable/centralmods.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M30,28H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.1,31.1,28,30,28z"/>
+<path fill="#FBFBFB" d="M30,27H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.1,31.1,27,30,27z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#585858" d="M22,25H10c-1.1,0-2-0.9-2-2V11c0-1.1,0.9-2,2-2h3c1.1,0,2,0.9,2,2
+ h7c1.1,0,2,0.9,2,2v10C24,24.1,23.1,25,22,25z M9,23c0,0.6,0.4,1,1,1h12c0.6,0,1-0.4,1-1V13c0-0.6-0.4-1-1-1h-8v-1c0-0.6-0.4-1-1-1
+ h-3c-0.6,0-1,0.4-1,1V23z"/>
+<g>
+ <path fill="#585858" d="M19,23l-3-1.6L13,23l0.6-3.5l-2.4-2.4l3.3-0.5l1.5-3.2l1.5,3.2l3.3,0.5l-2.4,2.4L19,23z M13.3,17.8l1.4,1.4
+ l-0.3,1.9l1.7-0.9l1.7,0.9l-0.3-1.9l1.4-1.4l-1.9-0.3L16,15.8l-0.8,1.7L13.3,17.8z"/>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/checkupdate.svg b/launcher/resources/OSX/scalable/checkupdate.svg
new file mode 100644
index 00000000..30cec51f
--- /dev/null
+++ b/launcher/resources/OSX/scalable/checkupdate.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M30,28H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.1,31.1,28,30,28z"/>
+<path fill="#FBFBFB" d="M30,27H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.1,31.1,27,30,27z"/>
+<g>
+ <g id="_x37__9_">
+ <g>
+ <path fill="#585858" d="M18.9,21.7c-0.2-0.2-0.5-0.2-0.7,0l-1.6,1.6v-6.8c0-0.3-0.2-0.5-0.5-0.5c-0.3,0-0.5,0.2-0.5,0.5v6.8
+ l-1.6-1.6c-0.2-0.2-0.5-0.2-0.7,0c-0.2,0.2-0.2,0.5,0,0.7l2.5,2.5C15.7,25,15.9,25,16,25c0.1,0,0.3,0,0.4-0.1l2.5-2.5
+ C19.1,22.2,19.1,21.9,18.9,21.7z"/>
+ </g>
+ </g>
+ <path fill="#585858" d="M18,19.5h2.5c1.3-0.8,2.6-2.1,2.5-3.5c-0.1-1.6-1.8-3.6-4.2-3.5C18.4,11,17.2,10,15.5,10
+ c-2.2,0-3.5,1.9-3.5,4c-1.7,0.1-3,1.5-3,3c0,2.7,3,2.5,3,2.5s2,0,2,0c0.3,0,0.5,0.2,0.5,0.5s-0.2,0.5-0.5,0.5l-2,0
+ c-2.2-0.1-4-1.4-4-3.5c0-1.8,1.3-3.3,3-3.8c0.2-2.3,2.1-4.2,4.5-4.2c1.8,0,3.3,1,4,2.5c2.5,0.1,4.5,2.2,4.5,4.7
+ c0,1.9-1.8,3.5-3.5,4.2H18c-0.3,0-0.5-0.2-0.5-0.5S17.7,19.5,18,19.5z"/>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/copy.svg b/launcher/resources/OSX/scalable/copy.svg
new file mode 100644
index 00000000..7382d6e2
--- /dev/null
+++ b/launcher/resources/OSX/scalable/copy.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M29.9,28.4h-28c-1.1,0-2-0.9-2-2v-18c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C31.9,27.5,31,28.4,29.9,28.4z"
+ />
+<path fill="#FBFBFB" d="M29.9,27.4h-28c-1.1,0-2-0.9-2-2v-17c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C31.9,26.5,31,27.4,29.9,27.4z"
+ />
+<g id="_x33__11_">
+ <g>
+ <path fill="#585858" d="M17.9,13.4h-8c-1.1,0-2,0.9-2,2v8c0,1.1,0.9,2,2,2h8c1.1,0,2-0.9,2-2v-8C19.9,14.3,19,13.4,17.9,13.4z
+ M18.9,23.4c0,0.6-0.4,1-1,1h-8c-0.6,0-1-0.4-1-1v-8c0-0.6,0.4-1,1-1h8c0.6,0,1,0.4,1,1V23.4z M21.9,9.4h-8c-1.1,0-2,0.9-2,2v1h1
+ v-1c0-0.6,0.4-1,1-1h8c0.6,0,1,0.4,1,1v8c0,0.6-0.4,1-1,1h-1v1h1c1.1,0,2-0.9,2-2v-8C23.9,10.3,23,9.4,21.9,9.4z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/coremods.svg b/launcher/resources/OSX/scalable/coremods.svg
new file mode 100644
index 00000000..b0df6052
--- /dev/null
+++ b/launcher/resources/OSX/scalable/coremods.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#E6E6E6" cx="14.9" cy="12.4" r="7"/>
+<polygon fill="#FFFFFF" points="12.2,11.5 13.5,13 13.2,14.9 14.9,14 16.5,14.9 16.2,13 17.6,11.5 15.7,11.3 14.9,9.5 14.1,11.3 "/>
+<rect fill="none" width="24" height="24"/>
+<g>
+ <g id="_x38__4_">
+ <g>
+ <path fill="#585858" d="M14.9,4.4c-4.4,0-8,3.6-8,8c0,4.4,3.6,8,8,8s8-3.6,8-8C22.9,8,19.3,4.4,14.9,4.4z M14.9,19.4
+ c-3.9,0-7-3.1-7-7c0-3.9,3.1-7,7-7s7,3.1,7,7C21.9,16.2,18.7,19.4,14.9,19.4z"/>
+ </g>
+ </g>
+ <g>
+ <path fill="#585858" d="M17.8,16.8l-3-1.6l-3,1.6l0.6-3.5l-2.4-2.4l3.3-0.5l1.5-3.2l1.5,3.2l3.3,0.5l-2.4,2.4L17.8,16.8z
+ M12.2,11.5l1.4,1.4l-0.3,1.9l1.7-0.9l1.7,0.9L16.2,13l1.4-1.4l-1.9-0.3l-0.8-1.7l-0.8,1.7L12.2,11.5z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/externaltools.svg b/launcher/resources/OSX/scalable/externaltools.svg
new file mode 100644
index 00000000..a2b7488e
--- /dev/null
+++ b/launcher/resources/OSX/scalable/externaltools.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<g id="_x35__1_">
+ <g>
+ <path fill="#585858" d="M9.5,9.5C9.8,9.5,10,9.2,10,9l0-2.4l7.6,7.3c0.2,0.2,0.5,0.2,0.7,0c0.2-0.2,0.2-0.5,0-0.7L10.8,6L13,6
+ c0.3,0,0.5-0.2,0.5-0.5S13.3,5,13,5H9.5C9.2,5,9,5.2,9,5.5V9C9,9.2,9.2,9.5,9.5,9.5z M21,5h-5.5v1H21c0.5,0,1,0.5,1,1l0,10
+ c0,0.5-0.4,1-1,1l-10,0c-0.5,0-1-0.5-1-1v-5.5H9V17c0,1.1,1.1,2,2.2,2H21c1.1,0,2-0.9,2-2V7.2C23,6.1,22.1,5,21,5z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/help.svg b/launcher/resources/OSX/scalable/help.svg
new file mode 100644
index 00000000..9d1b367c
--- /dev/null
+++ b/launcher/resources/OSX/scalable/help.svg
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata24"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs22" /><rect
+ id="rect2"
+ height="32"
+ width="32"
+ fill="none"
+ clip-rule="evenodd"
+ fill-rule="evenodd" /><path
+ id="path4"
+ d="M30,28H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.1,31.1,28,30,28z"
+ fill="#B6B5B6" /><path
+ id="path6"
+ d="M30,27H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.1,31.1,27,30,27z"
+ fill="#FBFBFB" /><g
+ id="g854"><g
+ id="g861"
+ transform="matrix(0.51850737,0,0,0.51850737,29.410453,7.662064)"
+ style="fill:#585858;fill-opacity:1"><g
+ id="text832"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:27.34714317px;line-height:85.45981598px;font-family:'Nimbus Sans L';-inkscape-font-specification:'Nimbus Sans L';letter-spacing:0px;word-spacing:0px;fill:#585858;fill-opacity:1;stroke:none;stroke-width:3.4183929px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ aria-label="?"><path
+ id="path855"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Monofonto;-inkscape-font-specification:Monofonto;fill:#585858;fill-opacity:1;stroke-width:3.4183929px"
+ d="m -20.968435,13.920532 c 0,0.6381 -0.07293,1.239737 -0.218777,1.804911 -0.145852,0.565174 -0.401092,1.066538 -0.76572,1.504093 -0.401092,0.492249 -0.774836,0.902456 -1.121233,1.230621 -0.346397,0.328166 -0.656332,0.6381 -0.929803,0.929803 -0.25524,0.273471 -0.464902,0.556059 -0.628985,0.847762 -0.145851,0.291703 -0.218777,0.656331 -0.218777,1.093885 v 0.38286 h -3.144921 V 20.72997 c 0,-0.601637 0.136736,-1.221505 0.410207,-1.859605 0.273471,-0.656332 0.756604,-1.285316 1.449399,-1.886953 0.346397,-0.309935 0.638099,-0.583406 0.875108,-0.820415 0.237009,-0.25524 0.428439,-0.492248 0.57429,-0.711025 0.145851,-0.237009 0.246124,-0.483133 0.300819,-0.738373 0.05469,-0.273471 0.08204,-0.57429 0.08204,-0.902456 0,-0.528711 -0.12762,-0.984497 -0.38286,-1.367357 -0.237009,-0.401091 -0.656331,-0.601637 -1.257968,-0.601637 -1.057423,0 -1.640829,0.692794 -1.750218,2.078383 h -3.06288 c 0.01823,-0.747489 0.145853,-1.431167 0.38286,-2.051036 0.237009,-0.6381 0.565175,-1.185043 0.984498,-1.640829 0.419323,-0.474017 0.920687,-0.8386455 1.504092,-1.0938855 0.601637,-0.25524 1.2762,-0.38286 2.023689,-0.38286 0.838646,0 1.567903,0.1367357 2.187771,0.4102072 0.619869,0.25524 1.130349,0.6198684 1.53144,1.0938853 0.401092,0.455786 0.692795,1.002729 0.875109,1.640829 0.200546,0.619869 0.300819,1.294432 0.30082,2.023689 z" /></g><circle
+ id="circle6-6"
+ r="1.8"
+ cy="25.466606"
+ cx="-26.38899"
+ style="fill:#585858;fill-opacity:1" /></g><g
+ id="g17"><g
+ id="_x38__12_"><g
+ id="g12"><path
+ d="M16,9c-4.4,0-8,3.6-8,8c0,4.4,3.6,8,8,8s8-3.6,8-8C24,12.6,20.4,9,16,9z M16,24c-3.9,0-7-3.1-7-7 c0-3.9,3.1-7,7-7s7,3.1,7,7C23,20.9,19.9,24,16,24z"
+ id="path10"
+ fill="#585858" /></g></g></g></g></svg> \ No newline at end of file
diff --git a/launcher/resources/OSX/scalable/instance-settings.svg b/launcher/resources/OSX/scalable/instance-settings.svg
new file mode 100644
index 00000000..394877f8
--- /dev/null
+++ b/launcher/resources/OSX/scalable/instance-settings.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M22,6c0-0.6-0.4-1-1-1H9C8.4,5,8,5.4,8,6v12c0,0.6,0.4,1,1,1h12
+ c0.6,0,1-0.4,1-1V6z"/>
+<g id="_x32__10_">
+ <g>
+ <path fill="#585858" d="M20,16h-5.6c-0.2-0.9-1-1.5-1.9-1.5s-1.7,0.6-1.9,1.5H10c-0.3,0-0.5,0.2-0.5,0.5c0,0.3,0.2,0.5,0.5,0.5
+ h0.6c0.2,0.9,1,1.5,1.9,1.5s1.7-0.6,1.9-1.5H20c0.3,0,0.5-0.2,0.5-0.5C20.5,16.2,20.3,16,20,16z M12.5,17.7
+ c-0.7,0-1.2-0.5-1.2-1.2s0.5-1.2,1.2-1.2s1.2,0.5,1.2,1.2S13.2,17.7,12.5,17.7z M21,4H9C7.9,4,7,4.9,7,6v12c0,1.1,0.9,2,2,2h12
+ c1.1,0,2-0.9,2-2V6C23,4.9,22.1,4,21,4z M22,18c0,0.6-0.4,1-1,1H9c-0.6,0-1-0.4-1-1V6c0-0.6,0.4-1,1-1h12c0.6,0,1,0.4,1,1V18z
+ M20,8h-5.6c-0.2-0.9-1-1.5-1.9-1.5S10.8,7.1,10.6,8H10C9.7,8,9.5,8.2,9.5,8.5C9.5,8.8,9.7,9,10,9h0.6c0.2,0.9,1,1.5,1.9,1.5
+ s1.7-0.6,1.9-1.5H20c0.3,0,0.5-0.2,0.5-0.5C20.5,8.2,20.3,8,20,8z M12.5,9.7c-0.7,0-1.2-0.5-1.2-1.2c0-0.7,0.5-1.2,1.2-1.2
+ s1.2,0.5,1.2,1.2C13.7,9.2,13.2,9.7,12.5,9.7z M20,12h-1.1c-0.2-0.9-1-1.5-1.9-1.5s-1.7,0.6-1.9,1.5H10c-0.3,0-0.5,0.2-0.5,0.5
+ c0,0.3,0.2,0.5,0.5,0.5h5.1c0.2,0.9,1,1.5,1.9,1.5s1.7-0.6,1.9-1.5H20c0.3,0,0.5-0.2,0.5-0.5C20.5,12.2,20.3,12,20,12z M17,13.7
+ c-0.7,0-1.2-0.5-1.2-1.2c0-0.7,0.5-1.2,1.2-1.2s1.2,0.5,1.2,1.2C18.2,13.2,17.7,13.7,17,13.7z"/>
+ </g>
+</g>
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" cx="12.5" cy="16.5" r="1.2"/>
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" cx="17" cy="12.5" r="1.2"/>
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" cx="12.5" cy="8.5" r="1.2"/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/jarmods.svg b/launcher/resources/OSX/scalable/jarmods.svg
new file mode 100644
index 00000000..213ec833
--- /dev/null
+++ b/launcher/resources/OSX/scalable/jarmods.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<g>
+ <g>
+ <g>
+ <path fill="#585858" d="M17.2,18.4h-5.4l-2.8-6.1L9.4,12c0,0,1-0.8,2.2-0.9c0.3,0,0.5,0.2,0.5,0.5c0,0.3-0.2,0.5-0.5,0.5
+ c-0.6,0-1,0.3-1.3,0.5l2.2,4.8h4.1l2.4-5.1c0.3-0.8,1.1-1.2,2-1.1c0.9,0.2,1.4,0.9,1.4,1.7c-0.1,1.8-1.4,2.4-1.4,2.4
+ c-0.3,0.1-0.6,0-0.7-0.3c-0.1-0.3,0-0.6,0.3-0.7c0,0,0.7-0.4,0.8-1.6c0-0.4-0.3-0.6-0.6-0.6c-0.1,0-0.6-0.1-0.8,0.4l0,0
+ L17.2,18.4z"/>
+ </g>
+ <path fill="#585858" d="M21.6,17.4c-0.3,0-0.5,0.2-0.5,0.5c0,0.4-2.2,1.5-6.6,1.5S8,18.3,8,17.9c0-0.3,0.7-0.7,2.2-1l-0.4-0.9
+ C8.1,16.3,7,17,7,17.9c0,1.7,3.9,2.5,7.6,2.5s7.6-0.8,7.6-2.5C22.1,17.6,21.9,17.4,21.6,17.4z"/>
+ <g>
+ <rect x="10.4" y="13.3" fill="#585858" width="8.2" height="1"/>
+ </g>
+ <g>
+ <rect x="11.3" y="15.3" fill="#585858" width="6.6" height="1"/>
+ </g>
+ </g>
+ <g>
+ <g>
+ <polygon fill="#999999" points="18,12.4 15.5,11 13,12.4 13.5,9.5 11.5,7.4 14.2,7 15.5,4.4 16.7,7 19.5,7.4 17.5,9.5 "/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/java.svg b/launcher/resources/OSX/scalable/java.svg
new file mode 100644
index 00000000..e1aee159
--- /dev/null
+++ b/launcher/resources/OSX/scalable/java.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<g>
+ <g>
+ <path fill="#585858" d="M18,18.2h-5.3l-2.8-6.1l0.3-0.2c0,0,1-0.8,2.2-0.9c0.3,0,0.5,0.2,0.5,0.5c0,0.3-0.2,0.5-0.5,0.5
+ c-0.6,0-1,0.3-1.3,0.5l2.2,4.8h4l2.3-5.1c0.3-0.8,1.1-1.2,1.9-1.1c0.9,0.2,1.4,0.9,1.4,1.7c-0.1,1.8-1.3,2.4-1.4,2.4
+ c-0.3,0.1-0.6,0-0.7-0.3c-0.1-0.3,0-0.6,0.3-0.7c0,0,0.7-0.4,0.8-1.5c0-0.4-0.3-0.6-0.6-0.6c-0.1,0-0.6-0.1-0.8,0.4l0,0L18,18.2z"
+ />
+ </g>
+ <g>
+ <path fill="#999999" d="M14.9,11.4c-0.1,0-0.3-0.1-0.4-0.2c-1-1.1-1.5-2.4-1.4-3.3c0.1-0.9,1.1-2.6,3-3.6c0.2-0.1,0.6,0,0.7,0.2
+ c0.1,0.2,0,0.6-0.2,0.7C15,5.9,14.1,7.4,14.1,8c-0.1,0.5,0.3,1.5,1.2,2.6c0.2,0.2,0.2,0.5,0,0.7C15.1,11.4,15,11.4,14.9,11.4z"/>
+ </g>
+ <g>
+ <path fill="#999999" d="M16.5,12.5c-0.1,0-0.2,0-0.3-0.1c-0.2-0.2-0.3-0.5-0.1-0.7c0.1-0.1,0.2-0.2,0.3-0.3
+ c0.5-0.4,0.5-0.6,0.3-0.9c0-0.1-0.2-0.2-0.3-0.3c-0.4-0.5-1.1-1.1-1.1-2c0-1,1.1-2,3.7-3.4c0.2-0.1,0.6,0,0.7,0.2
+ c0.1,0.2,0,0.6-0.2,0.7c-2.8,1.6-3.1,2.3-3.1,2.5c0,0.4,0.5,0.9,0.8,1.3c0.2,0.2,0.3,0.3,0.4,0.4c0.7,1.1-0.1,1.8-0.4,2.2
+ c-0.1,0.1-0.1,0.1-0.2,0.2C16.8,12.4,16.7,12.5,16.5,12.5z"/>
+ </g>
+ <path fill="#585858" d="M22.3,17.2c-0.3,0-0.5,0.2-0.5,0.5c0,0.4-2.2,1.5-6.5,1.5s-6.5-1-6.5-1.5c0-0.2,0.7-0.7,2.1-1l-0.4-0.9
+ c-1.6,0.4-2.7,1-2.7,2c0,1.7,3.9,2.5,7.5,2.5s7.5-0.8,7.5-2.5C22.8,17.4,22.6,17.2,22.3,17.2z"/>
+ <g>
+ <rect x="11.2" y="13.2" fill="#585858" width="8.2" height="1"/>
+ </g>
+ <g>
+ <rect x="12" y="15.2" fill="#585858" width="6.6" height="1"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/language.svg b/launcher/resources/OSX/scalable/language.svg
new file mode 100644
index 00000000..4f7d002a
--- /dev/null
+++ b/launcher/resources/OSX/scalable/language.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ id="Calque_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 24 24"
+ enable-background="new 0 0 24 24"
+ xml:space="preserve"><metadata
+ id="metadata22"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs20" /><path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ fill="#E6E6E6"
+ d="M21,20H9c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h12c1.1,0,2,0.9,2,2 v12C23,19.1,22.1,20,21,20z"
+ id="path2" /><rect
+ fill="none"
+ width="24"
+ height="24"
+ id="rect4" /><g
+ id="_x36__8_"><g
+ id="g8"><path
+ d="M 21,4 H 9 C 7.9,4 7,4.9 7,6 v 12 c 0,1.1 0.9,2 2,2 h 12 c 1.1,0 2,-0.9 2,-2 V 6 C 23,4.9 22.1,4 21,4 Z m 1,14 -1,1 H 9 L 8,18 V 6 C 8,5.4 8.4,5 9,5 h 12 c 0.6,0 1,0.4 1,1 z"
+ id="path6"
+ style="fill:#585858" /></g></g><circle
+ cx="-3.8492424"
+ cy="11.559504"
+ r="1"
+ id="circle11"
+ style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd" /><path
+ d="m 10.985086,8.5301236 c -0.285244,0 -0.531238,0.2015787 -0.587348,0.480998 L 9.2012867,14.998205 c -0.064883,0.324431 0.1450781,0.638482 0.4689122,0.703366 0.3279037,0.0672 0.6390781,-0.145078 0.7033681,-0.468911 l 0.384313,-1.91432 h 1.69195 l 0.381898,1.91432 c 0.06544,0.328662 0.388374,0.53424 0.705784,0.468911 0.323873,-0.06488 0.533795,-0.378935 0.468912,-0.703366 L 12.809973,9.0111216 c -0.05611,-0.2794193 -0.302101,-0.480998 -0.587347,-0.480998 z m 0.362559,1.3487256 h 0.256212 l 0.478579,2.3953168 h -1.21337 z m 6.429409,-1.5928481 c -0.330857,0 -0.599434,0.2698964 -0.599434,0.6018494 v 0.5994329 h -1.795884 c -0.330855,0 -0.597015,0.269898 -0.597015,0.6018506 0,0.331951 0.266159,0.599434 0.597015,0.599434 h 0.161945 c 0.340555,1.096391 0.840589,1.953495 1.401899,2.629772 -0.439717,0.403341 -0.871563,0.734012 -1.339055,1.104601 -0.25779,0.207035 -0.298799,0.58673 -0.09185,0.845975 0.205993,0.258844 0.58387,0.299404 0.841141,0.09185 0.507959,-0.402258 0.944446,-0.738141 1.421237,-1.177114 0.476792,0.438973 0.95437,0.774856 1.462328,1.177114 0.257273,0.207554 0.635147,0.166994 0.841141,-0.09185 0.206952,-0.259245 0.163525,-0.63894 -0.09426,-0.845975 -0.467495,-0.370589 -0.938012,-0.70126 -1.377731,-1.104601 0.561312,-0.676317 1.102396,-1.53342 1.442991,-2.629772 h 0.159526 c 0.330857,0 0.599433,-0.267483 0.599433,-0.599434 0,-0.3319526 -0.268576,-0.6018506 -0.599433,-0.6018506 H 18.376487 V 8.8878505 c 0,-0.331953 -0.268576,-0.6018494 -0.599433,-0.6018494 z m -0.983747,2.4025669 h 2.006168 c -0.25847,0.696576 -0.641961,1.260479 -1.022421,1.74029 -0.380459,-0.479811 -0.725279,-1.043674 -0.983747,-1.74029 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#585858;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.7126205;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect977" /></svg> \ No newline at end of file
diff --git a/launcher/resources/OSX/scalable/loadermods.svg b/launcher/resources/OSX/scalable/loadermods.svg
new file mode 100644
index 00000000..76951ebd
--- /dev/null
+++ b/launcher/resources/OSX/scalable/loadermods.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#585858" d="M22,6c0-0.6-0.4-1-1-1H9C8.4,5,8,5.4,8,6v12c0,0.6,0.4,1,1,1h12
+ c0.6,0,1-0.4,1-1V6z M21,20H9c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h12c1.1,0,2,0.9,2,2v12C23,19.1,22.1,20,21,20z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#E6E6E6" d="M22,6c0-0.6-0.4-1-1-1H9C8.4,5,8,5.4,8,6v12c0,0.6,0.4,1,1,1h12
+ c0.6,0,1-0.4,1-1V6z"/>
+<polygon fill="#585858" points="18,16.3 15,14.7 12,16.3 12.6,12.8 10.2,10.4 13.5,9.9 15,6.7 16.5,9.9 19.8,10.4 17.4,12.8 "/>
+<polygon fill="#FFFFFF" points="12.3,11.1 13.7,12.5 13.3,14.4 15,13.5 16.7,14.4 16.3,12.5 17.7,11.1 15.8,10.8 15,9.1 14.2,10.8
+ "/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/log.svg b/launcher/resources/OSX/scalable/log.svg
new file mode 100644
index 00000000..0ac45d54
--- /dev/null
+++ b/launcher/resources/OSX/scalable/log.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<g id="_x37__11_">
+ <g>
+ <path fill="#585858" d="M21,4c0,0-2.9,0.8-5.5,1.5c-0.3,0-0.7,0-1,0C12,4.8,9,4,9,4C7.9,4,7,4.9,7,6v10.5c0,1.1,0.9,1.7,2,2
+ c0,0,2.7,0.8,5.5,1.5h1c2.8-0.8,5.5-1.5,5.5-1.5c1-0.3,2-0.9,2-2V6C23,4.9,22.1,4,21,4z M14.5,19C11.8,18.3,9,17.5,9,17.5
+ c-0.6-0.2-1-0.4-1-1V6c0-0.6,0.4-1,1-1l5.5,1.5V19z M22,16.5c0,0.6-0.5,0.8-1,1c0,0-2.7,0.8-5.5,1.5V6.5L21,5c0.6,0,1,0.4,1,1
+ V16.5z"/>
+ </g>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#E6E6E6" d="M9,5C8.4,5,8,5.4,8,6v10.5c0,0.6,0.4,0.8,1,1c0,0,2.8,0.8,5.5,1.5
+ V6.5L9,5z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M22,6c0-0.6-0.4-1-1-1l-5.5,1.5V19c2.8-0.7,5.5-1.5,5.5-1.5
+ c0.5-0.2,1-0.4,1-1V6z"/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/minecraft.svg b/launcher/resources/OSX/scalable/minecraft.svg
new file mode 100644
index 00000000..86c915bc
--- /dev/null
+++ b/launcher/resources/OSX/scalable/minecraft.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<path fill="#585858" d="M15.9,4.4l-7,3.6v8.9l7,3.6l7-3.6V7.9L15.9,4.4z M21.2,8.2l-5.3,2.7l-5.3-2.7l5.3-2.7L21.2,8.2z M9.9,9
+ l5.5,2.8V19l-5.5-2.8V9z M16.4,19v-7.2L21.9,9v7.2L16.4,19z"/>
+<polygon fill="#FFFFFF" points="21.2,8.2 15.9,10.9 10.5,8.2 15.9,5.5 "/>
+<polygon fill="#F2F2F2" points="9.9,9 15.4,11.8 15.4,19 9.9,16.2 "/>
+<polygon fill="#CCCCCC" points="16.4,19 16.4,11.8 21.9,9 21.9,16.2 "/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/multimc.svg b/launcher/resources/OSX/scalable/multimc.svg
new file mode 100644
index 00000000..caad21b5
--- /dev/null
+++ b/launcher/resources/OSX/scalable/multimc.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<path fill="#585858" d="M6.9,4.4v16h16v-16H6.9z M21.9,5.4l0,14h-14v-14H21.9z"/>
+<rect fill="none" width="24" height="24"/>
+<rect x="7.9" y="5.4" fill="#F2F2F2" width="14" height="14"/>
+<polygon fill="#999999" points="7.9,7.4 7.9,6.9 8.9,6.9 8.9,7.9 9.9,7.9 9.9,7.4 10.9,7.4 10.9,8.9 11.9,8.9 11.9,6.4 12.9,6.4
+ 12.9,7.4 13.9,7.4 13.9,6.9 14.9,6.9 14.9,8.9 15.9,8.9 15.9,7.4 16.9,7.4 16.9,8.4 17.9,8.4 17.9,7.4 18.9,7.4 18.9,6.9 19.9,6.9
+ 19.9,7.4 21.4,7.4 21.4,6.9 21.9,6.9 21.9,7.4 22.9,7.4 22.9,4.4 6.9,4.4 6.9,7.4 "/>
+<g>
+ <path fill="#585858" d="M14.9,13.6c-0.9,0.9-1.9,2-3.4,2c-1.5,0-2.6-1.1-2.6-2.7c0-1.4,1.2-2.7,2.8-2.7c1.3,0,2.4,1.1,3.2,2
+ c0.9-0.9,1.9-2,3.4-2c1.6,0,2.6,1.1,2.6,2.7c0,1.4-1.2,2.7-2.8,2.7C16.7,15.6,15.7,14.5,14.9,13.6z M14.1,12.9
+ c-0.7-0.7-1.5-1.6-2.5-1.6c-0.9,0-1.6,0.7-1.6,1.6c0,0.9,0.7,1.6,1.6,1.6C12.6,14.5,13.4,13.6,14.1,12.9z M19.7,12.9
+ c0-0.9-0.7-1.6-1.5-1.6c-1,0-1.9,0.9-2.6,1.6c0.7,0.7,1.5,1.6,2.5,1.6C19,14.5,19.7,13.7,19.7,12.9z"/>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/new.svg b/launcher/resources/OSX/scalable/new.svg
new file mode 100644
index 00000000..79ee87ba
--- /dev/null
+++ b/launcher/resources/OSX/scalable/new.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M29.9,28.4h-28c-1.1,0-2-0.9-2-2v-18c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C31.9,27.5,31,28.4,29.9,28.4z"
+ />
+<path fill="#FBFBFB" d="M29.9,27.4h-28c-1.1,0-2-0.9-2-2v-17c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C31.9,26.5,31,27.4,29.9,27.4z"
+ />
+<g id="_x31__5_">
+ <g>
+ <path fill="#585858" d="M18.9,16.9h-2.5v-2.5c0-0.3-0.2-0.5-0.5-0.5s-0.5,0.2-0.5,0.5v2.5h-2.5c-0.3,0-0.5,0.2-0.5,0.5
+ c0,0.3,0.2,0.5,0.5,0.5h2.5v2.5c0,0.3,0.2,0.5,0.5,0.5s0.5-0.2,0.5-0.5v-2.5h2.5c0.3,0,0.5-0.2,0.5-0.5
+ C19.4,17.1,19.1,16.9,18.9,16.9z M21.9,9.4h-12c-1.1,0-2,0.9-2,2v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-12
+ C23.9,10.3,23,9.4,21.9,9.4z M22.9,23.4c0,0.6-0.4,1-1,1h-12c-0.6,0-1-0.4-1-1v-12c0-0.6,0.4-1,1-1h12c0.6,0,1,0.4,1,1V23.4z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/news.svg b/launcher/resources/OSX/scalable/news.svg
new file mode 100644
index 00000000..b8ce3cd1
--- /dev/null
+++ b/launcher/resources/OSX/scalable/news.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v14c0,1.1,0.9,2,2,2h1.3
+ l2.7,4l2.7-4H26c1.1,0,2-0.9,2-2V6z"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#585858" d="M26,26H14l-4,6l-4-6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20
+ c3.3,0,6,2.7,6,6v14C32,23.3,29.3,26,26,26z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v14c0,1.1,0.9,2,2,2h1.3l2.7,4l2.7-4H26
+ c1.1,0,2-0.9,2-2V6z"/>
+ <rect x="8" y="12" fill-rule="evenodd" clip-rule="evenodd" fill="#585858" width="16" height="2"/>
+ <rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" fill="#585858" width="16" height="2"/>
+ <rect x="8" y="16" fill-rule="evenodd" clip-rule="evenodd" fill="#585858" width="8" height="2"/>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/notes.svg b/launcher/resources/OSX/scalable/notes.svg
new file mode 100644
index 00000000..c2e95cfd
--- /dev/null
+++ b/launcher/resources/OSX/scalable/notes.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#E6E6E6" d="M22,7c0-0.6-0.4-1-1-1h-1.5v2h-7V6H11c-0.6,0-1,0.4-1,1v10
+ c0,0.6,0.4,1,1,1h10c0.6,0,1-0.4,1-1V7z"/>
+<g id="_x38__6_">
+ <g>
+ <path fill="#585858" d="M19.5,15h-7c-0.3,0-0.5,0.2-0.5,0.5c0,0.3,0.2,0.5,0.5,0.5h7c0.3,0,0.5-0.2,0.5-0.5
+ C20,15.2,19.8,15,19.5,15z M21,5h-1.5V4h-1.3c-0.4-0.6-1.2-1-2.2-1s-1.7,0.4-2.2,1h-1.3v1H11C9.9,5,9,5.9,9,7v10c0,1.1,0.9,2,2,2
+ h10c1.1,0,2-0.9,2-2V7C23,5.9,22.1,5,21,5z M13.5,5h1.1c0-0.6,0.6-1,1.4-1s1.4,0.4,1.4,1h1.1v2h-5V5z M22,17c0,0.6-0.4,1-1,1H11
+ c-0.6,0-1-0.4-1-1V7c0-0.6,0.4-1,1-1h1.5v2h7V6H21c0.6,0,1,0.4,1,1V17z M19.5,12.5h-7c-0.3,0-0.5,0.2-0.5,0.5
+ c0,0.3,0.2,0.5,0.5,0.5h7c0.3,0,0.5-0.2,0.5-0.5C20,12.7,19.8,12.5,19.5,12.5z M19.5,10h-7c-0.3,0-0.5,0.2-0.5,0.5
+ c0,0.3,0.2,0.5,0.5,0.5h7c0.3,0,0.5-0.2,0.5-0.5C20,10.2,19.8,10,19.5,10z"/>
+ </g>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M18.5,5h-1.1c0-0.6-0.6-1-1.4-1c-0.8,0-1.4,0.4-1.4,1h-1.1v2h5V5z"
+ />
+</svg>
diff --git a/launcher/resources/OSX/scalable/patreon.svg b/launcher/resources/OSX/scalable/patreon.svg
new file mode 100644
index 00000000..4f0da3e5
--- /dev/null
+++ b/launcher/resources/OSX/scalable/patreon.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M30,28H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.1,31.1,28,30,28z"/>
+<path fill="#FBFBFB" d="M30,27H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.1,31.1,27,30,27z"/>
+<g>
+ <rect x="8" y="9" fill="none" width="16" height="16"/>
+ <path fill="#585858" d="M16,8.3c-4.4,0-8,3.6-8,8v8h1.9v-1.9V20v-3.7c0-3.4,2.8-6.1,6.1-6.1c3.4,0,6.1,2.8,6.1,6.1
+ c0,3.3-2.6,6-5.9,6.1c-1,0-1.9-0.2-2.3-0.4v-2.6c0.6,0.4,1.3,0.6,2,0.6c2,0,3.7-1.6,3.7-3.7c0-2-1.6-3.7-3.7-3.7
+ c-2,0-3.7,1.6-3.7,3.7v8h3.3c0.1,0,0.2,0,0.3,0c4.4,0,8-3.6,8-8S20.4,8.3,16,8.3z"/>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/proxy.svg b/launcher/resources/OSX/scalable/proxy.svg
new file mode 100644
index 00000000..99acaa2b
--- /dev/null
+++ b/launcher/resources/OSX/scalable/proxy.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<path fill="#585858" d="M20.1,10.1c-0.8,0-1.5,0.4-1.9,0.9l-4.7-3c0.1-0.3,0.2-0.6,0.2-0.9c0-1.4-1.1-2.5-2.5-2.5S8.6,5.7,8.6,7.1
+ s1.1,2.5,2.5,2.5c0.7,0,1.3-0.3,1.8-0.8l4.8,3.1c-0.1,0.2-0.1,0.4-0.1,0.7c0,0.2,0,0.5,0.1,0.7l-4.8,3.1c-0.5-0.5-1.1-0.8-1.8-0.8
+ c-1.4,0-2.5,1.1-2.5,2.5s1.1,2.5,2.5,2.5s2.5-1.1,2.5-2.5c0-0.3-0.1-0.6-0.2-0.9l4.7-3c0.5,0.6,1.1,0.9,1.9,0.9
+ c1.4,0,2.5-1.1,2.5-2.5S21.5,10.1,20.1,10.1z M11.1,8.6c-0.8,0-1.5-0.7-1.5-1.5s0.7-1.5,1.5-1.5s1.5,0.7,1.5,1.5S11.9,8.6,11.1,8.6z
+ M11.1,19.6c-0.8,0-1.5-0.7-1.5-1.5s0.7-1.5,1.5-1.5s1.5,0.7,1.5,1.5S11.9,19.6,11.1,19.6z M20.1,14.1c-0.8,0-1.5-0.7-1.5-1.5
+ s0.7-1.5,1.5-1.5s1.5,0.7,1.5,1.5S20.9,14.1,20.1,14.1z"/>
+<circle fill="#FFFFFF" cx="11.1" cy="7.1" r="1.5"/>
+<circle fill="#FFFFFF" cx="11.1" cy="18.1" r="1.5"/>
+<circle fill="#FFFFFF" cx="20.1" cy="12.6" r="1.5"/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/quickmods.svg b/launcher/resources/OSX/scalable/quickmods.svg
new file mode 100644
index 00000000..e0aaad87
--- /dev/null
+++ b/launcher/resources/OSX/scalable/quickmods.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#E6E6E6" d="M22,6c0-0.6-0.4-1-1-1H9C8.4,5,8,5.4,8,6v12c0,0.6,0.4,1,1,1h12
+ c0.6,0,1-0.4,1-1V6z"/>
+<polygon fill="#FFFFFF" points="12.5,8.1 12.5,15.9 18.6,12 "/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#585858" d="M21,20H9c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h12c1.1,0,2,0.9,2,2
+ v12C23,19.1,22.1,20,21,20z M22,6c0-0.6-0.4-1-1-1H9C8.4,5,8,5.4,8,6v12c0,0.6,0.4,1,1,1h12c0.6,0,1-0.4,1-1V6z"/>
+ <g>
+ <path fill="#585858" d="M12.3,17c-0.4,0-0.8-0.3-0.8-0.9V7.9c0-0.6,0.4-0.9,0.8-0.9c0.2,0,0.4,0.1,0.5,0.2l6.5,4.1
+ c0.3,0.2,0.4,0.4,0.4,0.7s-0.2,0.6-0.4,0.7l-6.5,4.1C12.7,16.9,12.5,17,12.3,17z M12.5,8.1l0,7.7l6.1-3.9L12.5,8.1z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/refresh.svg b/launcher/resources/OSX/scalable/refresh.svg
new file mode 100644
index 00000000..c97489c1
--- /dev/null
+++ b/launcher/resources/OSX/scalable/refresh.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M30,28H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.1,31.1,28,30,28z"/>
+<path fill="#FBFBFB" d="M30,27H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.1,31.1,27,30,27z"/>
+<g id="_x32__12_">
+ <g>
+ <path fill="#585858" d="M19.9,10h3.6c0.3,0,0.5-0.2,0.5-0.5C24,9.2,23.8,9,23.5,9h-5C18.4,9,18,9,18,9.5v5c0,0.3,0.2,0.5,0.5,0.5
+ s0.5-0.2,0.5-0.5v-3.8c2.4,1.1,4,3.5,4,6.3c0,3.9-3.1,7-7,7s-7-3.1-7-7c0-3.5,2.6-6.4,6-6.9v-1c-3.9,0.5-7,3.9-7,7.9
+ c0,4.4,3.6,8,8,8s8-3.6,8-8C24,14,22.3,11.4,19.9,10z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/resourcepacks.svg b/launcher/resources/OSX/scalable/resourcepacks.svg
new file mode 100644
index 00000000..c85d4e3c
--- /dev/null
+++ b/launcher/resources/OSX/scalable/resourcepacks.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<rect fill="none" width="24" height="24"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#E6E6E6" d="M22,17c0,0.6-0.4,1-1,1H9c-0.6,0-1-0.4-1-1v-5.5h5.1
+ c0,0.2-0.1,0.3-0.1,0.5c0,1.1,0.9,2,2,2s2-0.9,2-2c0-0.2,0-0.3-0.1-0.5H22V17z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#E6E6E6" d="M22,10.5H8V8c0-0.6,0.4-1,1-1h12c0.6,0,1,0.4,1,1V10.5z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#585858" d="M21,6h-3V5c0-0.6-0.4-1-1-1h-4c-0.6,0-1,0.4-1,1v1H9
+ C7.9,6,7,6.9,7,8v9c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V8C23,6.9,22.1,6,21,6z M13,5.5C13,5.2,13.2,5,13.5,5h3C16.8,5,17,5.2,17,5.5
+ V6c-0.3,0-3.7,0-4,0V5.5z M22,17c0,0.6-0.4,1-1,1H9c-0.6,0-1-0.4-1-1v-5.5h5.1c0,0.2-0.1,0.3-0.1,0.5c0,1.1,0.9,2,2,2s2-0.9,2-2
+ c0-0.2,0-0.3-0.1-0.5H22V17z M14,12c0-0.2,0.1-0.4,0.1-0.5h1.7c0.1,0.1,0.1,0.3,0.1,0.5c0,0.6-0.4,1-1,1S14,12.6,14,12z M22,10.5H8
+ V8c0-0.6,0.4-1,1-1h12c0.6,0,1,0.4,1,1V10.5z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M14,12c0-0.2,0.1-0.4,0.1-0.5h1.7c0.1,0.1,0.1,0.3,0.1,0.5
+ c0,0.6-0.4,1-1,1S14,12.6,14,12z"/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/screenshots.svg b/launcher/resources/OSX/scalable/screenshots.svg
new file mode 100644
index 00000000..12df0c88
--- /dev/null
+++ b/launcher/resources/OSX/scalable/screenshots.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#E6E6E6" d="M21,20H9c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h12c1.1,0,2,0.9,2,2
+ v12C23,19.1,22.1,20,21,20z"/>
+<rect fill="none" width="24" height="24"/>
+<g id="_x36__8_">
+ <g>
+ <path fill="#585858" d="M21,4H9C7.9,4,7,4.9,7,6v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V6C23,4.9,22.1,4,21,4z M22,12.6l-3-3.1
+ l-5,5.1l-2-2.1l-4,3.7V6c0-0.6,0.4-1,1-1h12c0.6,0,1,0.4,1,1V12.6z M11,6C9.9,6,9,6.9,9,8c0,1.1,0.9,2,2,2s2-0.9,2-2
+ C13,6.9,12.1,6,11,6z M11,9c-0.6,0-1-0.4-1-1c0-0.6,0.4-1,1-1s1,0.4,1,1C12,8.6,11.6,9,11,9z"/>
+ </g>
+</g>
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" cx="11" cy="8" r="1"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#999999" d="M21,19c0.6,0,1-0.4,1-1v-4l-3-3l-4.3,4.3l3.7,3.7H21z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#999999" d="M8,18c0,0.6,0.4,1,1,1h8l-5-5l-4,3.6V18z"/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/settings.svg b/launcher/resources/OSX/scalable/settings.svg
new file mode 100644
index 00000000..dcdd9f1c
--- /dev/null
+++ b/launcher/resources/OSX/scalable/settings.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M30,28H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.1,31.1,28,30,28z"/>
+<path fill="#FBFBFB" d="M30,27H2c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.1,31.1,27,30,27z"/>
+<g id="_x31__16_">
+ <g>
+ <path fill="#585858" d="M23,18.3l-1.2-0.7c0.1-0.4,0.1-0.7,0.1-1.1c0-0.4,0-0.7-0.1-1.1l1.2-0.7c0.5-0.3,0.6-0.9,0.4-1.4l-1-1.7
+ c-0.3-0.5-0.9-0.6-1.4-0.4L19.9,12c-0.5-0.5-1.2-0.9-1.9-1.1V10c0-0.6-0.4-1-1-1h-2c-0.5,0-1,0.4-1,1v0.8
+ c-0.7,0.2-1.3,0.6-1.9,1.1L11,11.3c-0.5-0.3-1.1-0.1-1.4,0.4l-1,1.7c-0.3,0.5-0.1,1.1,0.4,1.4l1.2,0.7c-0.1,0.4-0.1,0.7-0.1,1.1
+ c0,0.4,0,0.7,0.1,1.1L9,18.3c-0.5,0.3-0.6,0.9-0.4,1.4l1,1.7c0.3,0.5,0.9,0.6,1.4,0.4l1.2-0.7c0.5,0.5,1.2,0.9,1.9,1.1V23
+ c0,0.6,0.4,1,1,1h2c0.5,0,1-0.4,1-1v-0.8c0.7-0.2,1.3-0.6,1.9-1.1l1.2,0.7c0.5,0.3,1.1,0.1,1.4-0.4l1-1.7
+ C23.6,19.2,23.5,18.5,23,18.3z M22.3,19.6l-0.5,0.9c-0.1,0.2-0.4,0.3-0.7,0.2l-1.4-0.8c-0.7,0.8-1.6,1.4-2.7,1.6v1.1
+ c0,0.3-0.2,0.5-0.5,0.5h-1c-0.3,0-0.5-0.2-0.5-0.5v-1.1c-1.1-0.2-2-0.8-2.7-1.6l-1.4,0.8c-0.2,0.1-0.5,0.1-0.7-0.2l-0.5-0.9
+ c-0.1-0.2-0.1-0.5,0.2-0.7l1.4-0.8c-0.2-0.5-0.3-1-0.3-1.6s0.1-1.1,0.3-1.6l-1.4-0.8c-0.2-0.1-0.3-0.4-0.2-0.7l0.5-0.9
+ c0.1-0.2,0.4-0.3,0.7-0.2l1.4,0.8c0.7-0.8,1.6-1.4,2.7-1.6v-1.1c0-0.3,0.2-0.5,0.5-0.5h1c0.3,0,0.5,0.2,0.5,0.5v1.1
+ c1.1,0.2,2,0.8,2.7,1.6l1.4-0.8c0.2-0.1,0.5-0.1,0.7,0.2l0.5,0.9c0.1,0.2,0.1,0.5-0.2,0.7l-1.4,0.8c0.2,0.5,0.3,1,0.3,1.6
+ s-0.1,1.1-0.3,1.6l1.4,0.8C22.3,19,22.4,19.3,22.3,19.6z M16,14c-1.4,0-2.5,1.1-2.5,2.5c0,1.4,1.1,2.5,2.5,2.5s2.5-1.1,2.5-2.5
+ C18.5,15.1,17.4,14,16,14z M16,18c-0.8,0-1.5-0.7-1.5-1.5c0-0.8,0.7-1.5,1.5-1.5s1.5,0.7,1.5,1.5C17.5,17.3,16.8,18,16,18z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/status-bad.svg b/launcher/resources/OSX/scalable/status-bad.svg
new file mode 100644
index 00000000..add7a6f7
--- /dev/null
+++ b/launcher/resources/OSX/scalable/status-bad.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#F2F2F2" d="M28,6c0-1.2-0.8-2-2-2H6C4.8,4,4,4.8,4,6v20c0,1.2,0.8,2,2,2h20c1.2,0,2-0.8,2-2V6z"/>
+<path fill="#585858" d="M26,32H6c-3.4,0-6-2.6-6-6V6c0-3.4,2.6-6,6-6h20c3.4,0,6,2.6,6,6v20C32,29.4,29.4,32,26,32z M28,6
+ c0-1.2-0.8-2-2-2H6C4.8,4,4,4.8,4,6v20c0,1.2,0.8,2,2,2h20c1.2,0,2-0.8,2-2V6z M21.6,21.6c-0.8,0.8-2,0.8-2.8,0L16,18.8l-2.8,2.8
+ c-0.8,0.8-2,0.8-2.8,0c-0.8-0.8-0.8-2,0-2.8l2.8-2.8l-2.8-2.8c-0.8-0.8-0.8-2,0-2.8c0.8-0.8,2-0.8,2.8,0l2.8,2.8l2.8-2.8
+ c0.8-0.8,2-0.8,2.8,0s0.8,2,0,2.8L18.8,16l2.8,2.8C22.4,19.6,22.4,20.8,21.6,21.6z"/>
+</svg>
diff --git a/launcher/resources/OSX/scalable/status-good.svg b/launcher/resources/OSX/scalable/status-good.svg
new file mode 100644
index 00000000..f10da757
--- /dev/null
+++ b/launcher/resources/OSX/scalable/status-good.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#F2F2F2" d="M28,26c0,1.2-0.8,2-2,2H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z"/>
+<g>
+ <path fill="none" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z M28,26c0,1.2-0.8,2-2,2
+ H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2
+ c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+</g>
+<g>
+ <g>
+ <path fill="#585858" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z M28,26
+ c0,1.2-0.8,2-2,2H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0
+ s-0.8,2,0,2.8l4.2,4.2c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/status-yellow.svg b/launcher/resources/OSX/scalable/status-yellow.svg
new file mode 100644
index 00000000..fba697bc
--- /dev/null
+++ b/launcher/resources/OSX/scalable/status-yellow.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="#E3A333" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z M28,26
+ c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+</g>
+<path fill="#585858" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z"/>
+<path fill="#F2F2F2" d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+<g>
+ <path fill="#585858" d="M6,20c0-5.8,2.4-7.9,5.8-7.9c2,0,3.4,0.7,5.3,1.9c1.6,1,2.5,1.5,3.3,1.5c0.7,0,1.2-0.7,1.3-3.1H26
+ c0.2,6.1-2.3,8-5.5,8c-2,0-3.5-0.8-5.3-1.9c-1.7-1-2.6-1.6-3.3-1.6c-0.8,0-1.4,0.5-1.6,3.2H6z"/>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/viewfolder.svg b/launcher/resources/OSX/scalable/viewfolder.svg
new file mode 100644
index 00000000..682c72c7
--- /dev/null
+++ b/launcher/resources/OSX/scalable/viewfolder.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<path fill="#B6B5B6" d="M30,28.4H2c-1.1,0-2-0.9-2-2v-18c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v18C32,27.5,31.1,28.4,30,28.4z"/>
+<path fill="#FBFBFB" d="M30,27.4H2c-1.1,0-2-0.9-2-2v-17c0-1.1,0.9-2,2-2h28c1.1,0,2,0.9,2,2v17C32,26.5,31.1,27.4,30,27.4z"/>
+<g id="_x36__6_">
+ <g>
+ <path fill="#585858" d="M22,11.4h-7c0-1.1-0.9-2-2-2h-3c-1.1,0-2,0.9-2,2v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-10
+ C24,12.3,23.1,11.4,22,11.4z M23,23.4c0,0.6-0.4,1-1,1H10c-0.6,0-1-0.4-1-1v-8h14V23.4z M23,14.4H9v-3c0-0.6,0.4-1,1-1h3
+ c0.6,0,1,0.4,1,1v1h8c0.6,0,1,0.4,1,1V14.4z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/OSX/scalable/worlds.svg b/launcher/resources/OSX/scalable/worlds.svg
new file mode 100644
index 00000000..b1491272
--- /dev/null
+++ b/launcher/resources/OSX/scalable/worlds.svg
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 24 24"
+ viewBox="0 0 24 24"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata22"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs20" /><path
+ id="path2"
+ d="M21,20H9c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h12c1.1,0,2,0.9,2,2 v12C23,19.1,22.1,20,21,20z"
+ fill="#E6E6E6"
+ clip-rule="evenodd"
+ fill-rule="evenodd" /><rect
+ id="rect4"
+ height="24"
+ width="24"
+ fill="none" /><g
+ id="_x36__8_"><g
+ id="g8"><path
+ style="fill:#585858"
+ id="path6"
+ d="M 21,4 H 9 C 7.9,4 7,4.9 7,6 v 12 c 0,1.1 0.9,2 2,2 h 12 c 1.1,0 2,-0.9 2,-2 V 6 C 23,4.9 22.1,4 21,4 Z m 1,14 -1,1 H 9 L 8,18 V 6 C 8,5.4 8.4,5 9,5 h 12 c 0.6,0 1,0.4 1,1 z" /></g></g><circle
+ style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd"
+ id="circle11"
+ r="1"
+ cy="11.559504"
+ cx="-3.8492424" /><g
+ style="stroke:#585858;stroke-width:1.55746377;stroke-opacity:1"
+ transform="matrix(0.55621145,0.32075232,-0.32075232,0.55621145,11.232654,-2.0314203)"
+ id="g881"><ellipse
+ ry="8.9473686"
+ rx="8.9473696"
+ cy="16"
+ cx="16"
+ id="path845"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#585858;stroke-width:1.55746377;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><ellipse
+ ry="8.9473686"
+ rx="3.4915254"
+ cy="15.947369"
+ cx="16"
+ id="path845-3"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#585858;stroke-width:1.55746377;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><path
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#585858;stroke-width:1.55746377;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 8.1848294,12.430327 C 12.570183,14.790801 20.662272,13.548526 23.355,11.76263"
+ id="path870" /><path
+ id="path872"
+ d="M 8.2166335,20.492522 C 12.601987,18.132048 21.148735,17.776739 23.841463,19.562635"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#585858;stroke-width:1.55746377;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/assets/underconstruction.png b/launcher/resources/assets/underconstruction.png
new file mode 100644
index 00000000..6ae06476
--- /dev/null
+++ b/launcher/resources/assets/underconstruction.png
Binary files differ
diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc
new file mode 100644
index 00000000..83505635
--- /dev/null
+++ b/launcher/resources/backgrounds/backgrounds.qrc
@@ -0,0 +1,7 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/backgrounds">
+ <file alias="kitteh">catbgrnd2.png</file>
+ <file alias="catmas">catmas.png</file>
+ </qresource>
+</RCC>
diff --git a/launcher/resources/backgrounds/catbgrnd2.png b/launcher/resources/backgrounds/catbgrnd2.png
new file mode 100644
index 00000000..e9de7f27
--- /dev/null
+++ b/launcher/resources/backgrounds/catbgrnd2.png
Binary files differ
diff --git a/launcher/resources/backgrounds/catmas.png b/launcher/resources/backgrounds/catmas.png
new file mode 100644
index 00000000..8bdb1d5c
--- /dev/null
+++ b/launcher/resources/backgrounds/catmas.png
Binary files differ
diff --git a/launcher/resources/documents/documents.qrc b/launcher/resources/documents/documents.qrc
new file mode 100644
index 00000000..007efcde
--- /dev/null
+++ b/launcher/resources/documents/documents.qrc
@@ -0,0 +1,7 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/documents">
+ <file>../../../COPYING.md</file>
+ </qresource>
+</RCC>
+
diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc
new file mode 100644
index 00000000..b6e2ee38
--- /dev/null
+++ b/launcher/resources/flat/flat.qrc
@@ -0,0 +1,45 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/icons/flat">
+ <file>index.theme</file>
+ <file>scalable/about.svg</file>
+ <file>scalable/accounts.svg</file>
+ <file>scalable/bug.svg</file>
+ <file>scalable/cat.svg</file>
+ <file>scalable/centralmods.svg</file>
+ <file>scalable/checkupdate.svg</file>
+ <file>scalable/copy.svg</file>
+ <file>scalable/coremods.svg</file>
+ <file>scalable/discord.svg</file>
+ <file>scalable/externaltools.svg</file>
+ <file>scalable/help.svg</file>
+ <file>scalable/instance-settings.svg</file>
+ <file>scalable/jarmods.svg</file>
+ <file>scalable/java.svg</file>
+ <file>scalable/language.svg</file>
+ <file>scalable/loadermods.svg</file>
+ <file>scalable/log.svg</file>
+ <file>scalable/minecraft.svg</file>
+ <file>scalable/multimc.svg</file>
+ <file>scalable/new.svg</file>
+ <file>scalable/news.svg</file>
+ <file>scalable/notes.svg</file>
+ <file>scalable/packages.svg</file>
+ <file>scalable/patreon.svg</file>
+ <file>scalable/proxy.svg</file>
+ <file>scalable/quickmods.svg</file>
+ <file>scalable/reddit-alien.svg</file>
+ <file>scalable/refresh.svg</file>
+ <file>scalable/resourcepacks.svg</file>
+ <file>scalable/screenshot-placeholder.svg</file>
+ <file>scalable/screenshots.svg</file>
+ <file>scalable/settings.svg</file>
+ <file>scalable/star.svg</file>
+ <file>scalable/status-bad.svg</file>
+ <file>scalable/status-good.svg</file>
+ <file>scalable/status-running.svg</file>
+ <file>scalable/status-yellow.svg</file>
+ <file>scalable/viewfolder.svg</file>
+ <file>scalable/worlds.svg</file>
+ </qresource>
+</RCC>
diff --git a/launcher/resources/flat/index.theme b/launcher/resources/flat/index.theme
new file mode 100644
index 00000000..34e27aa0
--- /dev/null
+++ b/launcher/resources/flat/index.theme
@@ -0,0 +1,11 @@
+[Icon Theme]
+Name=Flat
+Comment=Flat icons
+Inherits=multimc
+Directories=scalable
+
+[scalable]
+Size=48
+Type=Scalable
+MinSize=16
+MaxSize=256
diff --git a/launcher/resources/flat/scalable/about.svg b/launcher/resources/flat/scalable/about.svg
new file mode 100644
index 00000000..4f85045d
--- /dev/null
+++ b/launcher/resources/flat/scalable/about.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/accounts.svg b/launcher/resources/flat/scalable/accounts.svg
new file mode 100644
index 00000000..e6a1328d
--- /dev/null
+++ b/launcher/resources/flat/scalable/accounts.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M16,13C15.71,13 15.38,13 15.03,13.05C16.19,13.89 17,15 17,16.5V19H23V16.5C23,14.17 18.33,13 16,13M8,13C5.67,13 1,14.17 1,16.5V19H15V16.5C15,14.17 10.33,13 8,13M8,11A3,3 0 0,0 11,8A3,3 0 0,0 8,5A3,3 0 0,0 5,8A3,3 0 0,0 8,11M16,11A3,3 0 0,0 19,8A3,3 0 0,0 16,5A3,3 0 0,0 13,8A3,3 0 0,0 16,11Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/bug.svg b/launcher/resources/flat/scalable/bug.svg
new file mode 100644
index 00000000..ea370faa
--- /dev/null
+++ b/launcher/resources/flat/scalable/bug.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/cat.svg b/launcher/resources/flat/scalable/cat.svg
new file mode 100644
index 00000000..e90763b5
--- /dev/null
+++ b/launcher/resources/flat/scalable/cat.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12,8L10.67,8.09C9.81,7.07 7.4,4.5 5,4.5C5,4.5 3.03,7.46 4.96,11.41C4.41,12.24 4.07,12.67 4,13.66L2.07,13.95L2.28,14.93L4.04,14.67L4.18,15.38L2.61,16.32L3.08,17.21L4.53,16.32C5.68,18.76 8.59,20 12,20C15.41,20 18.32,18.76 19.47,16.32L20.92,17.21L21.39,16.32L19.82,15.38L19.96,14.67L21.72,14.93L21.93,13.95L20,13.66C19.93,12.67 19.59,12.24 19.04,11.41C20.97,7.46 19,4.5 19,4.5C16.6,4.5 14.19,7.07 13.33,8.09L12,8M9,11A1,1 0 0,1 10,12A1,1 0 0,1 9,13A1,1 0 0,1 8,12A1,1 0 0,1 9,11M15,11A1,1 0 0,1 16,12A1,1 0 0,1 15,13A1,1 0 0,1 14,12A1,1 0 0,1 15,11M11,14H13L12.3,15.39C12.5,16.03 13.06,16.5 13.75,16.5A1.5,1.5 0 0,0 15.25,15H15.75A2,2 0 0,1 13.75,17C13,17 12.35,16.59 12,16V16H12C11.65,16.59 11,17 10.25,17A2,2 0 0,1 8.25,15H8.75A1.5,1.5 0 0,0 10.25,16.5C10.94,16.5 11.5,16.03 11.7,15.39L11,14Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/centralmods.svg b/launcher/resources/flat/scalable/centralmods.svg
new file mode 100644
index 00000000..c694662a
--- /dev/null
+++ b/launcher/resources/flat/scalable/centralmods.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-2.06 11L15 15.28 12.06 17l.78-3.33-2.59-2.24 3.41-.29L15 8l1.34 3.14 3.41.29-2.59 2.24.78 3.33z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/checkupdate.svg b/launcher/resources/flat/scalable/checkupdate.svg
new file mode 100644
index 00000000..e6525a08
--- /dev/null
+++ b/launcher/resources/flat/scalable/checkupdate.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M21 10.12h-6.78l2.74-2.82c-2.73-2.7-7.15-2.8-9.88-.1-2.73 2.71-2.73 7.08 0 9.79 2.73 2.71 7.15 2.71 9.88 0C18.32 15.65 19 14.08 19 12.1h2c0 1.98-.88 4.55-2.64 6.29-3.51 3.48-9.21 3.48-12.72 0-3.5-3.47-3.53-9.11-.02-12.58 3.51-3.47 9.14-3.47 12.65 0L21 3v7.12zM12.5 8v4.25l3.5 2.08-.72 1.21L11 13V8h1.5z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/copy.svg b/launcher/resources/flat/scalable/copy.svg
new file mode 100644
index 00000000..36986e0d
--- /dev/null
+++ b/launcher/resources/flat/scalable/copy.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9h-4v4h-2v-4H9V9h4V5h2v4h4v2z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/coremods.svg b/launcher/resources/flat/scalable/coremods.svg
new file mode 100644
index 00000000..21a3450e
--- /dev/null
+++ b/launcher/resources/flat/scalable/coremods.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M22,12A10,10,0,1,1,12,2,10,10,0,0,1,22,12ZM12,4a8,8,0,1,0,8,8A8,8,0,0,0,12,4Zm4.23,14-1.12-4.82,3.73-3.23-4.92-.42L12,5,10.08,9.54,5.16,10l3.73,3.23L7.77,18,12,15.45,16.23,18"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/discord.svg b/launcher/resources/flat/scalable/discord.svg
new file mode 100644
index 00000000..ad63180f
--- /dev/null
+++ b/launcher/resources/flat/scalable/discord.svg
@@ -0,0 +1,4 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M10.14,11.63a1.15,1.15,0,1,0,1,1.14A1.1,1.1,0,0,0,10.14,11.63Zm3.75,0a1.15,1.15,0,1,0,1,1.14A1.1,1.1,0,0,0,13.89,11.63Z"/>
+ <path d="M18.89,3H5.11A2.11,2.11,0,0,0,3,5.12V19a2.11,2.11,0,0,0,2.11,2.12H16.77l-.55-1.9,1.32,1.22,1.24,1.15,2.21,2V5.12A2.11,2.11,0,0,0,18.89,3Zm-4,13.43s-.37-.44-.68-.83a3.25,3.25,0,0,0,1.86-1.22,5.89,5.89,0,0,1-1.18.61,6.77,6.77,0,0,1-1.49.44,7.21,7.21,0,0,1-2.66,0A8.63,8.63,0,0,1,9.25,15a6,6,0,0,1-.75-.35l-.09-.05,0,0-.29-.17a3.2,3.2,0,0,0,1.8,1.21l-.69.85a3.73,3.73,0,0,1-3.14-1.56,13.77,13.77,0,0,1,1.48-6,5.09,5.09,0,0,1,2.89-1.08l.1.12A6.94,6.94,0,0,0,7.82,9.26s.23-.12.61-.3a7.72,7.72,0,0,1,2.33-.65l.17,0a8.7,8.7,0,0,1,2.08,0,8.38,8.38,0,0,1,3.1,1A6.85,6.85,0,0,0,13.55,8l.14-.16a5.09,5.09,0,0,1,2.89,1.08,13.77,13.77,0,0,1,1.48,6A3.76,3.76,0,0,1,14.92,16.43Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/externaltools.svg b/launcher/resources/flat/scalable/externaltools.svg
new file mode 100644
index 00000000..55820dfc
--- /dev/null
+++ b/launcher/resources/flat/scalable/externaltools.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/help.svg b/launcher/resources/flat/scalable/help.svg
new file mode 100644
index 00000000..26d5d7f4
--- /dev/null
+++ b/launcher/resources/flat/scalable/help.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ id="svg4"
+ version="1.1"
+ width="24"
+ viewBox="0 0 24 24"
+ height="24"
+ fill="#757575">
+ <path
+ d="m 15.07,11.25 -0.9,0.92 C 13.45,12.89 13,13.5 13,15 h -2 v -0.5 c 0,-1.11 0.45,-2.11 1.17,-2.83 l 1.24,-1.26 C 13.78,10.049999 14,9.549999 14,9 14,7.89 13.100001,7 12,7 A 2,2 0 0 0 10,9 H 7.9999995 A 4,4 0 0 1 12,5 4,4 0 0 1 16,9 c 0,0.879999 -0.36,1.67 -0.93,2.25 M 13,19 h -2 v -2 h 2 M 12,2 A 10,10 0 0 0 1.9999995,12 10,10 0 0 0 12,22 10,10 0 0 0 22,12 C 22,6.47 17.5,2 12,2 Z"
+ id="path817" />
+</svg>
diff --git a/launcher/resources/flat/scalable/instance-settings.svg b/launcher/resources/flat/scalable/instance-settings.svg
new file mode 100644
index 00000000..dd9d86ed
--- /dev/null
+++ b/launcher/resources/flat/scalable/instance-settings.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/jarmods.svg b/launcher/resources/flat/scalable/jarmods.svg
new file mode 100644
index 00000000..db90fa34
--- /dev/null
+++ b/launcher/resources/flat/scalable/jarmods.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M2,21H20V19H2M20,8H18V5h2m0-2H4V13a4,4,0,0,0,4,4h6a4,4,0,0,0,4-4V10h2a2,2,0,0,0,2-2V5A2,2,0,0,0,20,3ZM11,4.43l1.62,3.29,3.63.53-2.63,2.56.62,3.62L11,12.72,7.75,14.43l.62-3.62L5.74,8.25l3.63-.53Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/java.svg b/launcher/resources/flat/scalable/java.svg
new file mode 100644
index 00000000..dc19ee23
--- /dev/null
+++ b/launcher/resources/flat/scalable/java.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M2,21H20V19H2M20,8H18V5H20M20,3H4V13A4,4 0 0,0 8,17H14A4,4 0 0,0 18,13V10H20A2,2 0 0,0 22,8V5C22,3.89 21.1,3 20,3Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/language.svg b/launcher/resources/flat/scalable/language.svg
new file mode 100644
index 00000000..f4d3f2f4
--- /dev/null
+++ b/launcher/resources/flat/scalable/language.svg
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ fill="#757575"
+ height="24"
+ viewBox="0 0 24 24"
+ width="24"
+ version="1.1"
+ id="svg4"
+ sodipodi:docname="language.svg"
+ inkscape:version="0.92.2 2405546, 2018-03-11">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="3840"
+ inkscape:window-height="2123"
+ id="namedview6"
+ showgrid="true"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:zoom="6.9532167"
+ inkscape:cx="-18.49351"
+ inkscape:cy="-12.477971"
+ inkscape:window-x="1200"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg4">
+ <inkscape:grid
+ type="xygrid"
+ id="grid981" />
+ <sodipodi:guide
+ position="-8,11.440678"
+ orientation="1,0"
+ id="guide1060"
+ inkscape:locked="false" />
+ <sodipodi:guide
+ position="-28.34375,24"
+ orientation="0,1"
+ id="guide1062"
+ inkscape:locked="false" />
+ </sodipodi:namedview>
+ <path
+ d="M 5,3 C 3.89,3 3,3.89 3,5 v 14 c 0,1.104569 0.895431,2 2,2 h 14 c 1.104569,0 2,-0.895431 2,-2 V 5 C 21,3.89 20.1,3 19,3 Z m 10.359375,4.505859 c 0.400344,0 0.726563,0.326845 0.726563,0.728516 v 0.724609 h 2.21875 c 0.400344,0 0.726562,0.326845 0.726562,0.728516 0,0.401669 -0.326217,0.726562 -0.726562,0.726562 h -0.191407 c -0.412128,1.326612 -1.066893,2.363281 -1.746093,3.181641 0.53207,0.488052 1.100334,0.887517 1.666015,1.335938 0.311924,0.250518 0.365651,0.709744 0.115235,1.023437 -0.249259,0.313205 -0.708226,0.362473 -1.019532,0.111328 -0.614642,-0.486742 -1.192601,-0.892661 -1.769531,-1.423828 -0.576929,0.531167 -1.104107,0.937086 -1.71875,1.423828 -0.311303,0.251148 -0.768322,0.201879 -1.017578,-0.111328 -0.250416,-0.313693 -0.200604,-0.772919 0.111328,-1.023437 0.565677,-0.448421 1.087072,-0.847886 1.619141,-1.335938 -0.679199,-0.818312 -1.283234,-1.854981 -1.695313,-3.181641 h -0.197265 c -0.400344,0 -0.720704,-0.324893 -0.720704,-0.726562 0,-0.401671 0.320361,-0.728516 0.720704,-0.728516 h 2.173828 V 8.234375 c 0,-0.401671 0.324264,-0.728516 0.724609,-0.728516 z M 7.142578,7.800781 h 1.496094 c 0.345155,0 0.643047,0.243927 0.710937,0.582031 l 1.447266,7.244141 c 0.07851,0.39257 -0.174512,0.773053 -0.566406,0.851563 -0.384074,0.07905 -0.774336,-0.168718 -0.853516,-0.566407 L 8.914062,13.595703 H 6.867188 L 6.402344,15.912109 C 6.324551,16.303955 5.947553,16.559826 5.550781,16.478516 5.158934,16.400005 4.905865,16.019523 4.984375,15.626953 L 6.431641,8.382812 C 6.499536,8.044708 6.797425,7.800781 7.142578,7.800781 Z m 0.4375,1.632813 -0.578125,2.898437 H 8.46875 L 7.890625,9.433594 Z m 6.589844,0.980468 c 0.312752,0.842923 0.729088,1.524886 1.189453,2.105469 0.460366,-0.580583 0.925527,-1.262594 1.238281,-2.105469 z"
+ id="path1072"
+ inkscape:connector-curvature="0" />
+ <g
+ style="fill:#000000"
+ id="g821"
+ transform="matrix(0.0322459,0,0,0.0322459,-17.878956,30.647558)">
+ <g
+ style="fill:#000000"
+ id="g819" />
+ </g>
+</svg>
diff --git a/launcher/resources/flat/scalable/loadermods.svg b/launcher/resources/flat/scalable/loadermods.svg
new file mode 100644
index 00000000..8a2fd12c
--- /dev/null
+++ b/launcher/resources/flat/scalable/loadermods.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M16.23,18l-1.12-4.82,3.73-3.23-4.92-.42L12,5,10.08,9.54,5.16,10l3.73,3.23L7.77,18,12,15.45,16.23,18M20,4H4V20H20Zm0,18H4a2,2,0,0,1-2-2V4A2,2,0,0,1,4,2H20a2,2,0,0,1,2,2V20A2,2,0,0,1,20,22Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/log.svg b/launcher/resources/flat/scalable/log.svg
new file mode 100644
index 00000000..e8caa08a
--- /dev/null
+++ b/launcher/resources/flat/scalable/log.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/minecraft.svg b/launcher/resources/flat/scalable/minecraft.svg
new file mode 100644
index 00000000..c17c44cd
--- /dev/null
+++ b/launcher/resources/flat/scalable/minecraft.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/multimc.svg b/launcher/resources/flat/scalable/multimc.svg
new file mode 100644
index 00000000..1c1f2359
--- /dev/null
+++ b/launcher/resources/flat/scalable/multimc.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M0,2A2,2,0,0,1,2,0H22a2,2,0,0,1,2,2V8L22,8V6H20V8H18v2H16V8H14V6H12V8H10V6H8v4H6V8H4V6H2V8H0ZM0,22a2,2,0,0,0,2,2H22a2,2,0,0,0,2-2V10H22V22H2V10H0Zm18.71-3.29a3.83,3.83,0,0,0-5.41-5.41L12,14.59l-1.29-1.29a3.83,3.83,0,1,0,0,5.41L12,17.41l1.29,1.29a3.83,3.83,0,0,0,5.41,0Zm-4-4a1.83,1.83,0,1,1,0,2.59L13.41,16Zm-5.41,0L10.59,16,9.29,17.29a1.83,1.83,0,1,1,0-2.59Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/new.svg b/launcher/resources/flat/scalable/new.svg
new file mode 100644
index 00000000..01f19d7c
--- /dev/null
+++ b/launcher/resources/flat/scalable/new.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/news.svg b/launcher/resources/flat/scalable/news.svg
new file mode 100644
index 00000000..8868414e
--- /dev/null
+++ b/launcher/resources/flat/scalable/news.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M20,11H4V8H20M20,15H13V13H20M20,19H13V17H20M11,19H4V13H11M20.33,4.67L18.67,3L17,4.67L15.33,3L13.67,4.67L12,3L10.33,4.67L8.67,3L7,4.67L5.33,3L3.67,4.67L2,3V19A2,2 0 0,0 4,21H20A2,2 0 0,0 22,19V3L20.33,4.67Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/notes.svg b/launcher/resources/flat/scalable/notes.svg
new file mode 100644
index 00000000..ebe0cb5a
--- /dev/null
+++ b/launcher/resources/flat/scalable/notes.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M14,17H7V15H14M17,13H7V11H17M17,9H7V7H17M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/packages.svg b/launcher/resources/flat/scalable/packages.svg
new file mode 100644
index 00000000..fe576a43
--- /dev/null
+++ b/launcher/resources/flat/scalable/packages.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M5.12,5l.81-1h12l.94,1m1.67.23L19.15,3.55A1.45,1.45,0,0,0,18,3H6a1.49,1.49,0,0,0-1.16.55L3.46,5.23A1.92,1.92,0,0,0,3,6.5V19a2,2,0,0,0,2,2H19a2,2,0,0,0,2-2V6.5A1.92,1.92,0,0,0,20.54,5.23Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/patreon.svg b/launcher/resources/flat/scalable/patreon.svg
new file mode 100644
index 00000000..ad561f57
--- /dev/null
+++ b/launcher/resources/flat/scalable/patreon.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12,3h0a9,9,0,0,0-9,9v9H5.09V12a6.91,6.91,0,1,1,7.23,6.9,5.9,5.9,0,0,1-2.59-.47v-3A4.13,4.13,0,1,0,7.85,12v9H12A9,9,0,1,0,12,3Zm0,15.91h0Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/proxy.svg b/launcher/resources/flat/scalable/proxy.svg
new file mode 100644
index 00000000..4956fec8
--- /dev/null
+++ b/launcher/resources/flat/scalable/proxy.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M22 4v-.5C22 2.12 20.88 1 19.5 1S17 2.12 17 3.5V4c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h5c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-.8 0h-3.4v-.5c0-.94.76-1.7 1.7-1.7s1.7.76 1.7 1.7V4zm-2.28 8c.04.33.08.66.08 1 0 2.08-.8 3.97-2.1 5.39-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H7v-2h2c.55 0 1-.45 1-1V8h2c1.1 0 2-.9 2-2V3.46c-.95-.3-1.95-.46-3-.46C5.48 3 1 7.48 1 13s4.48 10 10 10 10-4.48 10-10c0-.34-.02-.67-.05-1h-2.03zM10 20.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L8 16v1c0 1.1.9 2 2 2v1.93z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/quickmods.svg b/launcher/resources/flat/scalable/quickmods.svg
new file mode 100644
index 00000000..952d1e0e
--- /dev/null
+++ b/launcher/resources/flat/scalable/quickmods.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M8.75,5.5v7.15H10.7V18.5l4.55-7.8h-2.6l2.6-5.2ZM20,4H4V20H20Zm0,18H4a2,2,0,0,1-2-2V4A2,2,0,0,1,4,2H20a2,2,0,0,1,2,2V20A2,2,0,0,1,20,22Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/reddit-alien.svg b/launcher/resources/flat/scalable/reddit-alien.svg
new file mode 100644
index 00000000..9bcfbedc
--- /dev/null
+++ b/launcher/resources/flat/scalable/reddit-alien.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M20,11.86a1.76,1.76,0,0,0-1.75-1.75,1.73,1.73,0,0,0-1.22.51,9,9,0,0,0-4.67-1.38l1-3.16L16,6.71s0,0,0,0a1.46,1.46,0,1,0,.1-.53l-2.89-.68a.25.25,0,0,0-.29.17L11.83,9.23a9.16,9.16,0,0,0-4.88,1.36,1.75,1.75,0,1,0-2.07,2.79,3,3,0,0,0-.06.58C4.82,16.58,8,18.7,12,18.7s7.14-2.13,7.14-4.74a2.94,2.94,0,0,0-.05-.55A1.74,1.74,0,0,0,20,11.86ZM8.51,13.08a1.06,1.06,0,1,1,1.06,1.06A1.06,1.06,0,0,1,8.51,13.08Zm6.06,3.14A3.48,3.48,0,0,1,12,17h0a3.48,3.48,0,0,1-2.56-.79.25.25,0,0,1,.35-.35,3,3,0,0,0,2.2.65h0a3,3,0,0,0,2.2-.65.25.25,0,1,1,.35.35Zm-.13-2.08a1.06,1.06,0,1,1,1.06-1.06A1.06,1.06,0,0,1,14.44,14.14Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/refresh.svg b/launcher/resources/flat/scalable/refresh.svg
new file mode 100644
index 00000000..94be1e27
--- /dev/null
+++ b/launcher/resources/flat/scalable/refresh.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/resourcepacks.svg b/launcher/resources/flat/scalable/resourcepacks.svg
new file mode 100644
index 00000000..b6054baf
--- /dev/null
+++ b/launcher/resources/flat/scalable/resourcepacks.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M19.51 3.08L3.08 19.51c.09.34.27.65.51.9.25.24.56.42.9.51L20.93 4.49c-.19-.69-.73-1.23-1.42-1.41zM11.88 3L3 11.88v2.83L14.71 3h-2.83zM5 3c-1.1 0-2 .9-2 2v2l4-4H5zm14 18c.55 0 1.05-.22 1.41-.59.37-.36.59-.86.59-1.41v-2l-4 4h2zm-9.71 0h2.83L21 12.12V9.29L9.29 21z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/screenshot-placeholder.svg b/launcher/resources/flat/scalable/screenshot-placeholder.svg
new file mode 100644
index 00000000..99e0c17a
--- /dev/null
+++ b/launcher/resources/flat/scalable/screenshot-placeholder.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M21 15h2v2h-2v-2zm0-4h2v2h-2v-2zm2 8h-2v2c1 0 2-1 2-2zM13 3h2v2h-2V3zm8 4h2v2h-2V7zm0-4v2h2c0-1-1-2-2-2zM1 7h2v2H1V7zm16-4h2v2h-2V3zm0 16h2v2h-2v-2zM3 3C2 3 1 4 1 5h2V3zm6 0h2v2H9V3zM5 3h2v2H5V3zm-4 8v8c0 1.1.9 2 2 2h12V11H1zm2 8l2.5-3.21 1.79 2.15 2.5-3.22L13 19H3z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/screenshots.svg b/launcher/resources/flat/scalable/screenshots.svg
new file mode 100644
index 00000000..208bb104
--- /dev/null
+++ b/launcher/resources/flat/scalable/screenshots.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M21 3H3C2 3 1 4 1 5v14c0 1.1.9 2 2 2h18c1 0 2-1 2-2V5c0-1-1-2-2-2zM5 17l3.5-4.5 2.5 3.01L14.5 11l4.5 6H5z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/settings.svg b/launcher/resources/flat/scalable/settings.svg
new file mode 100644
index 00000000..dd9d86ed
--- /dev/null
+++ b/launcher/resources/flat/scalable/settings.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/star.svg b/launcher/resources/flat/scalable/star.svg
new file mode 100644
index 00000000..878bdca8
--- /dev/null
+++ b/launcher/resources/flat/scalable/star.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/status-bad.svg b/launcher/resources/flat/scalable/status-bad.svg
new file mode 100644
index 00000000..3f8e0116
--- /dev/null
+++ b/launcher/resources/flat/scalable/status-bad.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm4.24,7.17L13.41,12l2.83,2.83-1.41,1.41L12,13.41,9.17,16.24,7.76,14.83,10.59,12,7.76,9.17,9.17,7.76,12,10.59l2.83-2.83Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/status-good.svg b/launcher/resources/flat/scalable/status-good.svg
new file mode 100644
index 00000000..3503d6ba
--- /dev/null
+++ b/launcher/resources/flat/scalable/status-good.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/status-running.svg b/launcher/resources/flat/scalable/status-running.svg
new file mode 100644
index 00000000..7c750319
--- /dev/null
+++ b/launcher/resources/flat/scalable/status-running.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/status-yellow.svg b/launcher/resources/flat/scalable/status-yellow.svg
new file mode 100644
index 00000000..ac2d2349
--- /dev/null
+++ b/launcher/resources/flat/scalable/status-yellow.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/viewfolder.svg b/launcher/resources/flat/scalable/viewfolder.svg
new file mode 100644
index 00000000..2f5e29c9
--- /dev/null
+++ b/launcher/resources/flat/scalable/viewfolder.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/flat/scalable/worlds.svg b/launcher/resources/flat/scalable/worlds.svg
new file mode 100644
index 00000000..95a59bd4
--- /dev/null
+++ b/launcher/resources/flat/scalable/worlds.svg
@@ -0,0 +1,3 @@
+<svg fill="#757575" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M18.2,13a3.18,3.18,0,0,1-.84,2.16.8.8,0,0,0-.76-.56h-.4V13.4a.4.4,0,0,0-.4-.4H13.4v-.8h.8a.4.4,0,0,0,.4-.4V11h.8a.8.8,0,0,0,.8-.8V10A3.19,3.19,0,0,1,18.2,13Zm-4.4,1.6v-.4l-1.92-1.92a3.18,3.18,0,0,0,2.72,3.89V15.4A.8.8,0,0,1,13.8,14.6ZM22,8V18a2,2,0,0,1-2,2H4a2,2,0,0,1-2-2V6A2,2,0,0,1,4,4h6l2,2h8A2,2,0,0,1,22,8Zm-3,5a4,4,0,1,0-4,4A4,4,0,0,0,19,13Z"/>
+</svg> \ No newline at end of file
diff --git a/launcher/resources/iOS/iOS.qrc b/launcher/resources/iOS/iOS.qrc
new file mode 100644
index 00000000..511e390b
--- /dev/null
+++ b/launcher/resources/iOS/iOS.qrc
@@ -0,0 +1,38 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/icons/iOS">
+ <file>index.theme</file>
+ <file>scalable/about.svg</file>
+ <file>scalable/accounts.svg</file>
+ <file>scalable/bug.svg</file>
+ <file>scalable/centralmods.svg</file>
+ <file>scalable/checkupdate.svg</file>
+ <file>scalable/copy.svg</file>
+ <file>scalable/coremods.svg</file>
+ <file>scalable/externaltools.svg</file>
+ <file>scalable/help.svg</file>
+ <file>scalable/instance-settings.svg</file>
+ <file>scalable/jarmods.svg</file>
+ <file>scalable/java.svg</file>
+ <file>scalable/language.svg</file>
+ <file>scalable/loadermods.svg</file>
+ <file>scalable/log.svg</file>
+ <file>scalable/minecraft.svg</file>
+ <file>scalable/multimc.svg</file>
+ <file>scalable/new.svg</file>
+ <file>scalable/news.svg</file>
+ <file>scalable/notes.svg</file>
+ <file>scalable/patreon.svg</file>
+ <file>scalable/proxy.svg</file>
+ <file>scalable/quickmods.svg</file>
+ <file>scalable/refresh.svg</file>
+ <file>scalable/resourcepacks.svg</file>
+ <file>scalable/screenshots.svg</file>
+ <file>scalable/settings.svg</file>
+ <file>scalable/status-bad.svg</file>
+ <file>scalable/status-good.svg</file>
+ <file>scalable/status-yellow.svg</file>
+ <file>scalable/viewfolder.svg</file>
+ <file>scalable/worlds.svg</file>
+ </qresource>
+</RCC>
diff --git a/launcher/resources/iOS/index.theme b/launcher/resources/iOS/index.theme
new file mode 100644
index 00000000..b0f2f6ba
--- /dev/null
+++ b/launcher/resources/iOS/index.theme
@@ -0,0 +1,11 @@
+[Icon Theme]
+Name=iOS
+Comment=iOS theme by pexner
+Inherits=multimc
+Directories=scalable
+
+[scalable]
+Size=48
+Type=Scalable
+MinSize=16
+MaxSize=256
diff --git a/launcher/resources/iOS/scalable/about.svg b/launcher/resources/iOS/scalable/about.svg
new file mode 100644
index 00000000..c4d35471
--- /dev/null
+++ b/launcher/resources/iOS/scalable/about.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <polygon fill="#3366CC" points="18.5,22.5 18.5,12.5 12,12.5 12,15 14.5,15 14.5,22.5 12,22.5 12,25 21,25 21,22.5 "/>
+ <g id="_x38__8_">
+ <g>
+ <path fill="#3366CC" d="M16,0C7.2,0,0,7.2,0,16c0,8.8,7.2,16,16,16c8.8,0,16-7.2,16-16C32,7.2,24.8,0,16,0z M16,30
+ C8.3,30,2,23.7,2,16C2,8.3,8.3,2,16,2c7.7,0,14,6.3,14,14C30,23.7,23.7,30,16,30z"/>
+ </g>
+ </g>
+ <circle fill="#3366CC" cx="16.5" cy="8.5" r="2"/>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/accounts.svg b/launcher/resources/iOS/scalable/accounts.svg
new file mode 100644
index 00000000..65f76c3f
--- /dev/null
+++ b/launcher/resources/iOS/scalable/accounts.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x36__17_">
+ <g>
+ <path fill="#3366CC" d="M23,16.4c1.3-1.7,2-3.9,2-6.4c0-5.5-4-10-9-10S7,4.5,7,10c0,2.4,0.8,4.6,2,6.4c-2.3,0.8-4,3-4,5.6v4
+ c0,3.3,2.7,6,6,6h10c3.3,0,6-2.7,6-6v-4C27,19.4,25.3,17.2,23,16.4z M9,10c0-4.4,3.1-8,7-8s7,3.6,7,8c0,4.4-3.1,8-7,8S9,14.4,9,10
+ z M25,25.5c0,2.5-2.2,4.5-4.9,4.5h-8.2C9.2,30,7,28,7,25.5v-3c0-2.1,1.6-3.9,3.8-4.4C12.3,19.3,14,20,16,20s3.8-0.7,5.2-1.9
+ c2.2,0.5,3.8,2.2,3.8,4.4V25.5z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/bug.svg b/launcher/resources/iOS/scalable/bug.svg
new file mode 100644
index 00000000..fc4a3d69
--- /dev/null
+++ b/launcher/resources/iOS/scalable/bug.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#3366CC" d="M31.1,19.8c0.5,0,0.9-0.4,0.9-0.9c0-0.5-0.4-0.9-0.9-0.9H26c0-0.2,0-0.4,0-0.6c0-0.7,0-1.4,0-2
+ c1.3-0.2,2.4-0.8,3.3-1.8c2.6-3,1.8-8.8,1.8-9C31,4,30.5,3.6,30,3.7c-0.5,0.1-0.9,0.6-0.8,1.1c0,0.1,0.7,5.1-1.4,7.5
+ c-0.6,0.6-1.3,1-2.2,1.1c-0.1-0.4-0.3-0.7-0.6-1c-0.4-0.4-0.8-0.6-1.4-0.9c0.4-0.5,0.7-1.2,0.7-2.2c0-2.4-1.6-4.5-4.2-5.6l-0.1,0
+ C19.8,1.6,18.1,0,16,0C14,0,12.3,1.6,12,3.7l-0.1,0v0C9.3,4.9,7.7,7,7.7,9.4c0,1,0.3,1.7,0.7,2.2c-0.6,0.2-1,0.5-1.4,0.9
+ c-0.3,0.3-0.5,0.6-0.6,1c-0.9-0.1-1.6-0.5-2.2-1.1C2.1,9.9,2.8,4.8,2.8,4.7c0.1-0.5-0.3-1-0.8-1.1C1.5,3.6,1,4,1,4.5
+ c0,0.2-0.8,6,1.8,9c0.9,1,2,1.6,3.3,1.8c0,0.6,0,1.2,0,2c0,0.2,0,0.4,0,0.6H0.9c-0.5,0-0.9,0.4-0.9,0.9c0,0.5,0.4,0.9,0.9,0.9h5.2
+ c0.1,1.2,0.3,2.3,0.6,3.4c-1.6,0.2-3,0.8-3.9,1.9c-2.2,2.4-1.9,5.9-1.9,6.1c0,0.5,0.5,0.9,0.9,0.9c0,0,0.1,0,0.1,0
+ c0.5,0,0.9-0.5,0.9-1c0,0-0.2-2.9,1.4-4.6C4.9,25.5,6,25.1,7.4,25c1.7,3.7,4.9,6.2,8.6,6.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
+ c3.7,0,6.9-2.5,8.6-6.2c1.4,0.1,2.4,0.5,3.2,1.4c1.6,1.7,1.4,4.6,1.4,4.6c0,0.5,0.3,1,0.9,1c0,0,0.1,0,0.1,0c0.5,0,0.9-0.4,0.9-0.9
+ c0-0.2,0.3-3.7-1.9-6.1c-1-1.1-2.3-1.7-3.9-1.9c0.3-1.1,0.5-2.2,0.6-3.4H31.1z M16,1.9c0.8,0,1.5,0.5,1.9,1.2
+ c-0.2,0-0.4-0.1-0.5-0.1c-0.1,0-0.2,0-0.3,0c-0.4,0-0.7-0.1-1.1-0.1c-0.4,0-0.7,0-1.1,0.1c-0.1,0-0.2,0-0.3,0c-0.2,0-0.4,0-0.5,0.1
+ C14.5,2.4,15.2,1.9,16,1.9z M9.6,9.4c0-1.7,1.4-3.3,3.5-4.1c1.8-0.7,3.9-0.7,5.8,0c2.1,0.8,3.5,2.4,3.5,4.1c0,1-0.2,1.5-3.1,1.6
+ c-1,0-2-0.1-3.3-0.1c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c-1.2,0-2.3,0-3.2,0.1C9.9,11,9.6,10.5,9.6,9.4z M16.9,29.2V18.9
+ c0-0.5-0.4-0.9-0.9-0.9c-0.5,0-0.9,0.4-0.9,0.9v10.3C11.1,28.6,8,24.3,8,19c0-0.7,0-1.3,0-1.8c0-1.8-0.1-2.9,0.4-3.4
+ c0.6-0.6,2.1-0.8,4.5-0.9c0.4,0,0.9,0,1.3,0c1.1,0,2.4,0,3.5,0c0.5,0,1,0,1.5,0c2.4,0.1,3.9,0.3,4.5,0.9c0.5,0.5,0.5,1.7,0.4,3.4
+ c0,0.5,0,1,0,1.6c0,0,0,0,0,0.1c0,0,0,0,0,0.1c0,0,0,0.1,0,0.1C24.1,24.3,21,28.6,16.9,29.2z"/>
+</svg>
diff --git a/launcher/resources/iOS/scalable/centralmods.svg b/launcher/resources/iOS/scalable/centralmods.svg
new file mode 100644
index 00000000..1b4c4741
--- /dev/null
+++ b/launcher/resources/iOS/scalable/centralmods.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M28,32H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h6c2.2,0,4,1.8,4,4h14
+ c2.2,0,4,1.8,4,4v20C32,30.2,30.2,32,28,32z M2,28c0,1.1,0.9,2,2,2h24c1.1,0,2-0.9,2-2V8c0-1.1-0.9-2-2-2H12V4c0-1.1-0.9-2-2-2H4
+ C2.9,2,2,2.9,2,4V28z"/>
+<g>
+ <path fill="#3366CC" d="M21.9,26.9L16,23.7l-5.9,3.3l1.1-6.9l-4.8-4.9l6.6-1l3-6.4l3,6.4l6.6,1L20.8,20L21.9,26.9z M10.6,16.5
+ l2.8,2.8l-0.6,3.9l3.3-1.8l3.3,1.8l-0.6-3.9l2.8-2.8l-3.8-0.6L16,12.5l-1.6,3.5L10.6,16.5z"/>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/checkupdate.svg b/launcher/resources/iOS/scalable/checkupdate.svg
new file mode 100644
index 00000000..9fc983d1
--- /dev/null
+++ b/launcher/resources/iOS/scalable/checkupdate.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x37__7_">
+ <g>
+ <path fill="#3366CC" d="M21.7,25.4c-0.4-0.4-1-0.4-1.4,0L17,28.7V15c0-0.6-0.4-1-1-1c-0.6,0-1,0.4-1,1v13.6l-3.3-3.2
+ c-0.4-0.4-1-0.4-1.4,0c-0.4,0.4-0.4,1,0,1.4l4.9,4.9c0.2,0.2,0.5,0.3,0.8,0.3c0.3,0,0.6-0.1,0.8-0.3l4.9-4.9
+ C22.1,26.4,22.1,25.8,21.7,25.4z"/>
+ </g>
+</g>
+<path fill="#3366CC" d="M20,21h5c2.7-1.5,5.1-4.2,5-7c-0.2-3.3-3.5-7.2-8.5-7c-0.8-2.9-3.1-5-6.5-5c-4.3,0-7,3.9-7,8
+ c-3.3,0.2-6,3-6,6c0,5.4,6,5,6,5s4,0,4,0c0.6,0,1,0.4,1,1s-0.4,1-1,1l-4,0c-4.5-0.2-8-2.7-8-7c0-3.6,2.5-6.7,6-7.7
+ C6.4,3.7,10.3,0,15,0c3.5,0,6.6,2.1,8.1,5c5,0.3,8.9,4.4,8.9,9.5c0,3.9-3.6,7-7,8.5h-5c-0.6,0-1-0.4-1-1S19.4,21,20,21z"/>
+</svg>
diff --git a/launcher/resources/iOS/scalable/copy.svg b/launcher/resources/iOS/scalable/copy.svg
new file mode 100644
index 00000000..3ccc2f06
--- /dev/null
+++ b/launcher/resources/iOS/scalable/copy.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x33__7_">
+ <g>
+ <path fill="#3366CC" d="M20,8H4c-2.2,0-4,1.8-4,4v16c0,2.2,1.8,4,4,4h16c2.2,0,4-1.8,4-4V12C24,9.8,22.2,8,20,8z M22,28
+ c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V12c0-1.1,0.9-2,2-2h16c1.1,0,2,0.9,2,2V28z M28,0H12C9.8,0,8,1.8,8,4v2h2V4c0-1.1,0.9-2,2-2
+ h16c1.1,0,2,0.9,2,2v16c0,1.1-0.9,2-2,2h-2v2h2c2.2,0,4-1.8,4-4V4C32,1.8,30.2,0,28,0z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/coremods.svg b/launcher/resources/iOS/scalable/coremods.svg
new file mode 100644
index 00000000..ea47872c
--- /dev/null
+++ b/launcher/resources/iOS/scalable/coremods.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <g id="_x38__2_">
+ <g>
+ <path fill="#3366CC" d="M16,0C7.2,0,0,7.2,0,16c0,8.8,7.2,16,16,16s16-7.2,16-16C32,7.2,24.8,0,16,0z M16,30C8.3,30,2,23.7,2,16
+ C2,8.3,8.3,2,16,2s14,6.3,14,14C30,23.7,23.7,30,16,30z"/>
+ </g>
+ </g>
+ <g>
+ <path fill="#3366CC" d="M21.9,24.8L16,21.5l-5.9,3.3l1.1-6.9l-4.8-4.9l6.6-1l3-6.4l3,6.4l6.6,1l-4.8,4.9L21.9,24.8z M10.6,14.3
+ l2.8,2.8L12.7,21l3.3-1.8l3.3,1.8l-0.6-3.9l2.8-2.8l-3.8-0.6L16,10.3l-1.6,3.5L10.6,14.3z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/externaltools.svg b/launcher/resources/iOS/scalable/externaltools.svg
new file mode 100644
index 00000000..16e9fa48
--- /dev/null
+++ b/launcher/resources/iOS/scalable/externaltools.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x35__5_">
+ <g>
+ <path fill="#3366CC" d="M3,11c0.6,0,1-0.5,1-1l0-4.8l15.2,14.5c0.4,0.4,1,0.4,1.4,0c0.4-0.4,0.4-1,0-1.4L5.6,4L10,4
+ c0.6,0,1-0.5,1-1s-0.4-1-1-1H3C2.5,2,2,2.4,2,3v7C2,10.5,2.4,11,3,11z M26,2H15v2h11c1.1,0,2,0.9,2,2l0,20.1c0,1.1-0.9,2-2,2L6,28
+ c-1.1,0-2-0.9-2-2V15H2v11c0,2.2,2.2,4,4.4,4h19.7c2.2,0,3.9-1.8,3.9-3.9V6.4C30,4.2,28.2,2,26,2z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/help.svg b/launcher/resources/iOS/scalable/help.svg
new file mode 100644
index 00000000..9c2d2e93
--- /dev/null
+++ b/launcher/resources/iOS/scalable/help.svg
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs16" /><g
+ id="g11"><g
+ id="_x38__8_"><g
+ id="g6"><path
+ id="path4"
+ d="M16,0C7.2,0,0,7.2,0,16c0,8.8,7.2,16,16,16c8.8,0,16-7.2,16-16C32,7.2,24.8,0,16,0z M16,30 C8.3,30,2,23.7,2,16C2,8.3,8.3,2,16,2c7.7,0,14,6.3,14,14C30,23.7,23.7,30,16,30z"
+ fill="#3366CC" /></g></g></g><g
+ style="fill:#3366cc;fill-opacity:1"
+ transform="matrix(1.0878826,0,0,1.0878826,44.136533,-3.5919647)"
+ id="g861"><g
+ aria-label="?"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:27.34714317px;line-height:85.45981598px;font-family:'Nimbus Sans L';-inkscape-font-specification:'Nimbus Sans L';letter-spacing:0px;word-spacing:0px;fill:#3366cc;fill-opacity:1;stroke:none;stroke-width:3.4183929px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text832"><path
+ d="m -20.968435,13.920532 c 0,0.6381 -0.07293,1.239737 -0.218777,1.804911 -0.145852,0.565174 -0.401092,1.066538 -0.76572,1.504093 -0.401092,0.492249 -0.774836,0.902456 -1.121233,1.230621 -0.346397,0.328166 -0.656332,0.6381 -0.929803,0.929803 -0.25524,0.273471 -0.464902,0.556059 -0.628985,0.847762 -0.145851,0.291703 -0.218777,0.656331 -0.218777,1.093885 v 0.38286 h -3.144921 V 20.72997 c 0,-0.601637 0.136736,-1.221505 0.410207,-1.859605 0.273471,-0.656332 0.756604,-1.285316 1.449399,-1.886953 0.346397,-0.309935 0.638099,-0.583406 0.875108,-0.820415 0.237009,-0.25524 0.428439,-0.492248 0.57429,-0.711025 0.145851,-0.237009 0.246124,-0.483133 0.300819,-0.738373 0.05469,-0.273471 0.08204,-0.57429 0.08204,-0.902456 0,-0.528711 -0.12762,-0.984497 -0.38286,-1.367357 -0.237009,-0.401091 -0.656331,-0.601637 -1.257968,-0.601637 -1.057423,0 -1.640829,0.692794 -1.750218,2.078383 h -3.06288 c 0.01823,-0.747489 0.145853,-1.431167 0.38286,-2.051036 0.237009,-0.6381 0.565175,-1.185043 0.984498,-1.640829 0.419323,-0.474017 0.920687,-0.8386455 1.504092,-1.0938855 0.601637,-0.25524 1.2762,-0.38286 2.023689,-0.38286 0.838646,0 1.567903,0.1367357 2.187771,0.4102072 0.619869,0.25524 1.130349,0.6198684 1.53144,1.0938853 0.401092,0.455786 0.692795,1.002729 0.875109,1.640829 0.200546,0.619869 0.300819,1.294432 0.30082,2.023689 z"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Monofonto;-inkscape-font-specification:Monofonto;fill:#3366cc;fill-opacity:1;stroke-width:3.4183929px"
+ id="path855" /></g><circle
+ style="fill:#3366cc;fill-opacity:1"
+ cx="-26.38899"
+ cy="25.466606"
+ r="1.8"
+ id="circle6-6" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/iOS/scalable/instance-settings.svg b/launcher/resources/iOS/scalable/instance-settings.svg
new file mode 100644
index 00000000..95b8a508
--- /dev/null
+++ b/launcher/resources/iOS/scalable/instance-settings.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x32__5_">
+ <g>
+ <path fill="#3366CC" d="M26,24H14.9c-0.4-1.7-2-3-3.9-3s-3.4,1.3-3.9,3H6c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h1.1c0.4,1.7,2,3,3.9,3
+ s3.4-1.3,3.9-3H26c0.6,0,1-0.4,1-1C27,24.4,26.6,24,26,24z M11,27.4c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4
+ s2.4,1.1,2.4,2.4C13.4,26.3,12.3,27.4,11,27.4z M26,16h-2.1c-0.4-1.7-2-3-3.9-3s-3.4,1.3-3.9,3H6c-0.6,0-1,0.4-1,1
+ c0,0.6,0.4,1,1,1h10.1c0.4,1.7,2,3,3.9,3s3.4-1.3,3.9-3H26c0.6,0,1-0.4,1-1C27,16.4,26.6,16,26,16z M20,19.4
+ c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4s2.4,1.1,2.4,2.4C22.4,18.3,21.3,19.4,20,19.4z M26,8H14.9c-0.4-1.7-2-3-3.9-3
+ S7.6,6.3,7.1,8H6C5.4,8,5,8.4,5,9c0,0.6,0.4,1,1,1h1.1c0.4,1.7,2,3,3.9,3s3.4-1.3,3.9-3H26c0.6,0,1-0.4,1-1C27,8.4,26.6,8,26,8z
+ M11,11.4c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4s2.4,1.1,2.4,2.4C13.4,10.3,12.3,11.4,11,11.4z M28,0H4C1.8,0,0,1.8,0,4
+ v24c0,2.2,1.8,4,4,4h24c2.2,0,4-1.8,4-4V4C32,1.8,30.2,0,28,0z M30,28c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V4c0-1.1,0.9-2,2-2h24
+ c1.1,0,2,0.9,2,2V28z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/jarmods.svg b/launcher/resources/iOS/scalable/jarmods.svg
new file mode 100644
index 00000000..c4c5ca8c
--- /dev/null
+++ b/launcher/resources/iOS/scalable/jarmods.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <g>
+ <g>
+ <path fill="#3366CC" d="M20.5,28H9.8L4.2,15.9l0.6-0.5c0.1-0.1,1.9-1.7,4.5-1.8c0.6,0,1,0.4,1.1,0.9c0,0.6-0.4,1-0.9,1.1
+ c-1.1,0.1-2.1,0.6-2.6,0.9l4.4,9.6h8.1l4.7-10.3c0.6-1.6,2.2-2.5,3.9-2.2c1.7,0.3,2.9,1.7,2.8,3.4c-0.2,3.7-2.7,4.8-2.8,4.8
+ c-0.5,0.2-1.1,0-1.3-0.5c-0.2-0.5,0-1.1,0.5-1.3c0,0,1.5-0.7,1.6-3.1c0-0.8-0.7-1.2-1.1-1.2c-0.2,0-1.3-0.2-1.6,0.9l0,0.1
+ L20.5,28z"/>
+ </g>
+ <path fill="#3366CC" d="M29.3,26c-0.6,0-1,0.5-1,1c0,0.9-4.5,2.9-13.1,2.9S2,27.9,2,27c0-0.5,1.5-1.4,4.3-2.1l-0.9-1.9
+ C2.3,23.9,0,25.2,0,27c0,3.4,7.9,5,15.2,5s15.2-1.6,15.2-5C30.3,26.5,29.9,26,29.3,26z"/>
+ <g>
+ <rect x="6.8" y="17.9" fill="#3366CC" width="16.4" height="2"/>
+ </g>
+ <g>
+ <rect x="8.6" y="22" fill="#3366CC" width="13.3" height="2"/>
+ </g>
+ </g>
+ <g>
+ <g>
+ <path fill="#3366CC" d="M17,4.7l0.7,1.5l0.5,1l1.1,0.2l1.7,0.3l-1.3,1.3l-0.7,0.7l0.2,1l0.3,1.8L18,11.6L17,11l-1,0.5l-1.4,0.8
+ l0.3-1.8l0.2-1l-0.7-0.7l-1.3-1.3l1.7-0.3l1.1-0.2l0.5-1L17,4.7 M17,0l-2.5,5.3L9,6.2l4,4.1L12,16.1l5-2.7l5,2.7L21,10.2l4-4.1
+ l-5.5-0.8L17,0L17,0z"/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/java.svg b/launcher/resources/iOS/scalable/java.svg
new file mode 100644
index 00000000..8d7c2798
--- /dev/null
+++ b/launcher/resources/iOS/scalable/java.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <g>
+ <path fill="#3366CC" d="M20.4,28.1H9.8L4.2,15.9l0.6-0.5c0.1-0.1,1.9-1.7,4.4-1.8c0.6,0,1,0.4,1.1,0.9c0,0.6-0.4,1-0.9,1.1
+ c-1.1,0.1-2.1,0.6-2.6,0.9L11,26h8.1l4.7-10.2c0.6-1.6,2.2-2.5,3.9-2.1c1.7,0.3,2.9,1.7,2.8,3.3c-0.2,3.7-2.7,4.8-2.8,4.8
+ c-0.5,0.2-1.1,0-1.3-0.5c-0.2-0.5,0-1.1,0.5-1.3c0,0,1.5-0.7,1.6-3.1c0-0.8-0.7-1.2-1.1-1.2c-0.2,0-1.3-0.2-1.6,0.9l0,0.1
+ L20.4,28.1z"/>
+ </g>
+ <g>
+ <path fill="#3366CC" d="M14.2,14.4c-0.3,0-0.6-0.1-0.8-0.3c-1.9-2.2-3.1-4.8-2.9-6.7c0.2-1.8,2.2-5.3,5.9-7.2
+ C17-0.1,17.6,0,17.9,0.5C18.1,1,18,1.6,17.5,1.9c-3,1.6-4.7,4.5-4.8,5.7c-0.1,1,0.6,3.1,2.4,5.2c0.4,0.4,0.3,1.1-0.1,1.4
+ C14.7,14.3,14.5,14.4,14.2,14.4z"/>
+ </g>
+ <g>
+ <path fill="#3366CC" d="M17.5,16.5c-0.2,0-0.4-0.1-0.6-0.2c-0.4-0.3-0.5-1-0.2-1.4c0.1-0.2,0.3-0.4,0.5-0.6c0.9-0.9,1-1.2,0.6-1.8
+ c-0.1-0.1-0.3-0.4-0.6-0.6c-0.9-0.9-2.2-2.3-2.2-4c0-1.9,2.2-4,7.3-6.8c0.5-0.3,1.1-0.1,1.4,0.4c0.3,0.5,0.1,1.1-0.4,1.4
+ c-5.7,3.1-6.3,4.5-6.3,5c0,0.9,1,1.9,1.6,2.6c0.3,0.3,0.6,0.6,0.8,0.9c1.4,2.1-0.2,3.7-0.9,4.4c-0.1,0.1-0.3,0.2-0.3,0.3
+ C18.2,16.4,17.9,16.5,17.5,16.5z"/>
+ </g>
+ <path fill="#3366CC" d="M29.2,26c-0.6,0-1,0.5-1,1c0,0.9-4.4,2.9-13.1,2.9S2,27.9,2,27c0-0.5,1.4-1.4,4.3-2l-0.9-1.9
+ C2.3,23.9,0,25.2,0,27c0,3.4,7.8,5,15.1,5s15.1-1.6,15.1-5C30.2,26.5,29.7,26,29.2,26z"/>
+ <g>
+ <rect x="6.8" y="18" fill="#3366CC" width="16.4" height="2"/>
+ </g>
+ <g>
+ <rect x="8.6" y="22" fill="#3366CC" width="13.2" height="2"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/language.svg b/launcher/resources/iOS/scalable/language.svg
new file mode 100644
index 00000000..fcc3436e
--- /dev/null
+++ b/launcher/resources/iOS/scalable/language.svg
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata15"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs13" />
+<g
+ id="g8">
+
+ <path
+ id="path6"
+ d="M28,32H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h24c2.2,0,4,1.8,4,4 v24C32,30.2,30.2,32,28,32z M30,4c0-1.1-0.9-2-2-2H4C2.9,2,2,2.9,2,4v24c0,1.1,0.9,2,2,2h24c1.1,0,2-0.9,2-2V4z"
+ fill="#3366CC"
+ clip-rule="evenodd"
+ fill-rule="evenodd" />
+</g>
+<path
+ d="m 8.8837922,9.8498439 c -0.5055801,0 -0.9415895,0.3572871 -1.0410421,0.8525411 L 5.7221093,21.314148 c -0.1150014,0.575036 0.2571428,1.131673 0.8311203,1.246676 0.5811915,0.119103 1.1327283,-0.257142 1.2466794,-0.831119 l 0.6811731,-3.393021 h 2.9988849 l 0.676892,3.393021 c 0.115982,0.582535 0.688371,0.946911 1.250962,0.831119 0.574046,-0.115001 0.94612,-0.67164 0.831119,-1.246676 L 12.118299,10.702385 C 12.018854,10.207131 11.582843,9.8498439 11.07726,9.8498439 Z m 0.642615,2.3905391 h 0.4541203 l 0.8482535,4.245562 H 8.6781534 Z M 20.922167,9.4171507 c -0.586426,0 -1.062464,0.4783758 -1.062464,1.0667433 v 1.062461 h -3.183102 c -0.58642,0 -1.058175,0.478378 -1.058175,1.066747 0,0.588364 0.471753,1.062461 1.058175,1.062461 h 0.28704 c 0.603613,1.943292 1.489894,3.462457 2.484785,4.661121 -0.779374,0.714899 -1.544795,1.300994 -2.373399,1.957842 -0.456918,0.366958 -0.529604,1.039945 -0.162795,1.499442 0.36511,0.458786 1.034877,0.530676 1.490873,0.162795 0.900329,-0.71298 1.673977,-1.308312 2.519064,-2.086367 0.845087,0.778055 1.691565,1.373387 2.591893,2.086367 0.456001,0.367877 1.125761,0.295987 1.490873,-0.162795 0.366811,-0.459497 0.289838,-1.132484 -0.167067,-1.499442 -0.82861,-0.656848 -1.662574,-1.242943 -2.441951,-1.957842 0.994894,-1.198734 1.953935,-2.7179 2.55762,-4.661121 h 0.28275 c 0.586426,0 1.062461,-0.474097 1.062461,-1.062461 0,-0.588369 -0.476035,-1.066747 -1.062461,-1.066747 h -3.251659 v -1.062461 c 0,-0.5883675 -0.476037,-1.0667433 -1.062461,-1.0667433 z m -1.743636,4.2584123 h 3.555818 c -0.458122,1.234642 -1.137838,2.234128 -1.812182,3.084565 -0.674344,-0.850437 -1.285517,-1.849853 -1.743636,-3.084565 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#3366cc;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.58040667;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect977" /></svg> \ No newline at end of file
diff --git a/launcher/resources/iOS/scalable/loadermods.svg b/launcher/resources/iOS/scalable/loadermods.svg
new file mode 100644
index 00000000..010efa11
--- /dev/null
+++ b/launcher/resources/iOS/scalable/loadermods.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <g>
+ <path fill="#3366CC" d="M21.9,24.6L16,21.3l-5.9,3.3l1.1-6.9l-4.8-4.9l6.6-1l3-6.4l3,6.4l6.6,1l-4.8,4.9L21.9,24.6z M10.6,14.2
+ l2.8,2.8l-0.6,3.9L16,19l3.3,1.8L18.7,17l2.8-2.8l-3.8-0.6L16,10.1l-1.6,3.5L10.6,14.2z"/>
+ </g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M28,32H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h24c2.2,0,4,1.8,4,4
+ v24C32,30.2,30.2,32,28,32z M30,4c0-1.1-0.9-2-2-2H4C2.9,2,2,2.9,2,4v24c0,1.1,0.9,2,2,2h24c1.1,0,2-0.9,2-2V4z"/>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/log.svg b/launcher/resources/iOS/scalable/log.svg
new file mode 100644
index 00000000..5d1c7f06
--- /dev/null
+++ b/launcher/resources/iOS/scalable/log.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x37__21_">
+ <g>
+ <path fill="#3366CC" d="M28,0c0,0-5.8,1.6-11,3c-0.7,0-1.3,0-2,0C10,1.7,4,0,4,0C1.8,0,0,1.8,0,4v21c0,2.2,1.9,3.3,4,4
+ c0,0,5.4,1.5,11,3h2c5.6-1.5,11-3,11-3c2.1-0.5,4-1.8,4-4V4C32,1.8,30.2,0,28,0z M15,30c-5.5-1.5-11-3-11-3c-1.1-0.3-2-0.9-2-2V4
+ c0-1.1,0.9-2,2-2l11,3V30z M30,25c0,1.1-0.9,1.7-2,2c0,0-5.4,1.5-11,3V5l11-3c1.1,0,2,0.9,2,2V25z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/minecraft.svg b/launcher/resources/iOS/scalable/minecraft.svg
new file mode 100644
index 00000000..069b4e71
--- /dev/null
+++ b/launcher/resources/iOS/scalable/minecraft.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#3366CC" d="M16,0L2,7.1v17.8L16,32l14-7.1V7.1L16,0z M26.7,7.7L16,13.1L5.3,7.7L16,2.2L26.7,7.7z M4,9.2l11,5.6v14.4
+ L4,23.7V9.2z M17,29.2V14.8l11-5.6v14.4L17,29.2z"/>
+</svg>
diff --git a/launcher/resources/iOS/scalable/multimc.svg b/launcher/resources/iOS/scalable/multimc.svg
new file mode 100644
index 00000000..bc819433
--- /dev/null
+++ b/launcher/resources/iOS/scalable/multimc.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#3366CC" d="M0,0v32h32V0H0z M30,30H2V5h2v2h2V6h2v3h2V4h2v2h2V5h2v4h2V6h2v2h2V6h2V5h2v1h3V5h1L30,30z"/>
+<g>
+ <path fill="#3366CC" d="M16,18.5c-1.9,1.9-3.8,3.9-6.7,3.9C6.2,22.4,4,20.1,4,17c0-2.9,2.3-5.4,5.5-5.4c2.7,0,4.7,2.2,6.5,3.9
+ c1.9-1.9,3.8-3.9,6.7-3.9c3.1,0,5.3,2.3,5.3,5.4c0,2.9-2.3,5.4-5.5,5.4C19.8,22.4,17.8,20.2,16,18.5z M14.5,17
+ c-1.4-1.3-2.9-3.2-5-3.2c-1.8,0-3.2,1.5-3.2,3.2c0,1.9,1.3,3.2,3.1,3.2C11.5,20.2,13.2,18.4,14.5,17z M25.7,17
+ c0-1.9-1.3-3.2-3.1-3.2c-2.1,0-3.8,1.8-5.1,3.2c1.4,1.3,2.9,3.2,5,3.2C24.3,20.2,25.7,18.7,25.7,17z"/>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/new.svg b/launcher/resources/iOS/scalable/new.svg
new file mode 100644
index 00000000..9f221580
--- /dev/null
+++ b/launcher/resources/iOS/scalable/new.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x31__2_">
+ <g>
+ <path fill="#3366CC" d="M22,15h-5v-5c0-0.6-0.4-1-1-1s-1,0.4-1,1v5h-5c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h5v5c0,0.6,0.4,1,1,1
+ s1-0.4,1-1v-5h5c0.6,0,1-0.4,1-1C23,15.4,22.6,15,22,15z M28,0H4C1.8,0,0,1.8,0,4v24c0,2.2,1.8,4,4,4h24c2.2,0,4-1.8,4-4V4
+ C32,1.8,30.2,0,28,0z M30,28c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V4c0-1.1,0.9-2,2-2h24c1.1,0,2,0.9,2,2V28z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/news.svg b/launcher/resources/iOS/scalable/news.svg
new file mode 100644
index 00000000..d3c010bb
--- /dev/null
+++ b/launcher/resources/iOS/scalable/news.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M26,26H14l-4,6l-4-6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20
+ c3.3,0,6,2.7,6,6v14C32,23.3,29.3,26,26,26z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v14c0,1.1,0.9,2,2,2h1.3l2.7,4l2.7-4H26
+ c1.1,0,2-0.9,2-2V6z"/>
+ <rect x="8" y="12" fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" width="16" height="2"/>
+ <rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" width="16" height="2"/>
+ <rect x="8" y="16" fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" width="8" height="2"/>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/notes.svg b/launcher/resources/iOS/scalable/notes.svg
new file mode 100644
index 00000000..b42ebeef
--- /dev/null
+++ b/launcher/resources/iOS/scalable/notes.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x38__21_">
+ <g>
+ <path fill="#3366CC" d="M23,24H9c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1C24,24.4,23.6,24,23,24z M26,4h-3V2h-2.7
+ c-0.9-1.2-2.5-2-4.3-2s-3.4,0.8-4.3,2H9v2H6C3.8,4,2,5.8,2,8v20c0,2.2,1.8,4,4,4h20c2.2,0,4-1.8,4-4V8C30,5.8,28.2,4,26,4z M11,4
+ h2.2c0-1.1,1.2-2,2.8-2c1.5,0,2.8,0.9,2.8,2H21v4H11V4z M28,28c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V8c0-1.1,0.9-2,2-2h3v4h14V6h3
+ c1.1,0,2,0.9,2,2V28z M23,19H9c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1C24,19.4,23.6,19,23,19z M23,14H9
+ c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1C24,14.4,23.6,14,23,14z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/patreon.svg b/launcher/resources/iOS/scalable/patreon.svg
new file mode 100644
index 00000000..1bd06f4a
--- /dev/null
+++ b/launcher/resources/iOS/scalable/patreon.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <rect y="0" fill="none" width="32" height="32"/>
+ <path fill="#3366CC" d="M16,0C7.2,0,0,7.2,0,16v16h3.7v-3.7v-5V16C3.7,9.2,9.2,3.7,16,3.7c6.8,0,12.3,5.5,12.3,12.3
+ c0,6.6-5.2,12-11.7,12.3c-2.1,0-3.8-0.4-4.6-0.8v-5.3c1.2,0.8,2.5,1.2,4,1.2c4.1,0,7.3-3.3,7.3-7.3c0-4.1-3.3-7.3-7.3-7.3
+ c-4.1,0-7.3,3.3-7.3,7.3v16h6.7c0.2,0,0.4,0,0.7,0c8.8,0,16-7.2,16-16S24.8,0,16,0z"/>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/proxy.svg b/launcher/resources/iOS/scalable/proxy.svg
new file mode 100644
index 00000000..f6552281
--- /dev/null
+++ b/launcher/resources/iOS/scalable/proxy.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#3366CC" d="M25,11c-1.6,0-3,0.7-3.9,1.9l-9.5-6.1C11.9,6.2,12,5.6,12,5c0-2.8-2.2-5-5-5S2,2.2,2,5s2.2,5,5,5
+ c1.4,0,2.7-0.6,3.6-1.5l9.6,6.2C20.1,15.1,20,15.5,20,16c0,0.5,0.1,0.9,0.2,1.4l-9.6,6.2C9.7,22.6,8.4,22,7,22c-2.8,0-5,2.2-5,5
+ s2.2,5,5,5s5-2.2,5-5c0-0.6-0.1-1.2-0.3-1.8l9.5-6.1c0.9,1.1,2.3,1.8,3.8,1.8c2.8,0,5-2.2,5-5S27.8,11,25,11z M7,8C5.3,8,4,6.7,4,5
+ s1.3-3,3-3s3,1.3,3,3S8.7,8,7,8z M7,30c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3S8.7,30,7,30z M25,19c-1.7,0-3-1.3-3-3s1.3-3,3-3
+ s3,1.3,3,3S26.7,19,25,19z"/>
+</svg>
diff --git a/launcher/resources/iOS/scalable/quickmods.svg b/launcher/resources/iOS/scalable/quickmods.svg
new file mode 100644
index 00000000..cd63ba71
--- /dev/null
+++ b/launcher/resources/iOS/scalable/quickmods.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M28,32H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h24c2.2,0,4,1.8,4,4
+ v24C32,30.2,30.2,32,28,32z M30,4c0-1.1-0.9-2-2-2H4C2.9,2,2,2.9,2,4v24c0,1.1,0.9,2,2,2h24c1.1,0,2-0.9,2-2V4z"/>
+ <g>
+ <path fill="#3366CC" d="M10.6,26C9.8,26,9,25.4,9,24.2V7.8C9,6.6,9.8,6,10.6,6c0.4,0,0.7,0.1,1.1,0.3l13,8.2
+ c0.6,0.3,0.9,0.9,0.9,1.5s-0.3,1.1-0.9,1.5l-13,8.2C11.3,25.9,10.9,26,10.6,26z M11,8.3l0,15.4L23.2,16L11,8.3z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/refresh.svg b/launcher/resources/iOS/scalable/refresh.svg
new file mode 100644
index 00000000..297b79c9
--- /dev/null
+++ b/launcher/resources/iOS/scalable/refresh.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x32__8_">
+ <g>
+ <path fill="#3366CC" d="M23.7,2H31c0.6,0,1-0.4,1-1c0-0.6-0.4-1-1-1H21c-0.1,0-1,0-1,1v10c0,0.6,0.4,1,1,1c0.6,0,1-0.4,1-1V3.4
+ c4.7,2.2,8,7.1,8,12.6c0,7.7-6.3,14-14,14C8.3,30,2,23.7,2,16C2,8.9,7.2,3.1,14,2.2v-2C6.1,1.1,0,7.8,0,16c0,8.8,7.2,16,16,16
+ c8.8,0,16-7.2,16-16C32,10,28.7,4.7,23.7,2z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/resourcepacks.svg b/launcher/resources/iOS/scalable/resourcepacks.svg
new file mode 100644
index 00000000..5b359d63
--- /dev/null
+++ b/launcher/resources/iOS/scalable/resourcepacks.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x33__19_">
+ <g>
+ <path fill="#3366CC" d="M28,5h-6V3c0-1.1-0.9-2-2-2h-8c-1.1,0-2,0.9-2,2v2H4C1.8,5,0,6.8,0,9v18c0,2.2,1.8,4,4,4h24
+ c2.2,0,4-1.8,4-4V9C32,6.8,30.2,5,28,5z M12,4c0-0.6,0.4-1,1-1h6c0.6,0,1,0.4,1,1v1h-8V4z M30,27c0,1.1-0.9,2-2,2H4
+ c-1.1,0-2-0.9-2-2V16h10.1c-0.1,0.3-0.1,0.7-0.1,1c0,2.2,1.8,4,4,4s4-1.8,4-4c0-0.3-0.1-0.7-0.1-1H30V27z M14,17
+ c0-0.4,0.1-0.7,0.3-1h3.4c0.2,0.3,0.3,0.6,0.3,1c0,1.1-0.9,2-2,2S14,18.1,14,17z M30,14H2V9c0-1.1,0.9-2,2-2h24c1.1,0,2,0.9,2,2
+ V14z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/screenshots.svg b/launcher/resources/iOS/scalable/screenshots.svg
new file mode 100644
index 00000000..39ce7b82
--- /dev/null
+++ b/launcher/resources/iOS/scalable/screenshots.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x36__21_">
+ <g>
+ <path fill="#3366CC" d="M28,0H4C1.8,0,0,1.8,0,4v24c0,2.2,1.8,4,4,4h24c2.2,0,4-1.8,4-4V4C32,1.8,30.2,0,28,0z M4,30
+ c-1.1,0-2-0.9-2-2v-0.9l7.9-7.1L20,30H4z M30,28c0,1.1-0.9,2-2,2h-5.2l-7.4-7.5L24,14l6,6V28z M30,17.1L24,11l-9.9,10.1L10,17
+ l-8,7.3V4c0-1.1,0.9-2,2-2h24c1.1,0,2,0.9,2,2V17.1z M8,4C5.8,4,4,5.8,4,8s1.8,4,4,4c2.2,0,4-1.8,4-4S10.2,4,8,4z M8,10
+ c-1.1,0-2-0.9-2-2c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2C10,9.1,9.1,10,8,10z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/settings.svg b/launcher/resources/iOS/scalable/settings.svg
new file mode 100644
index 00000000..95b8a508
--- /dev/null
+++ b/launcher/resources/iOS/scalable/settings.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x32__5_">
+ <g>
+ <path fill="#3366CC" d="M26,24H14.9c-0.4-1.7-2-3-3.9-3s-3.4,1.3-3.9,3H6c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h1.1c0.4,1.7,2,3,3.9,3
+ s3.4-1.3,3.9-3H26c0.6,0,1-0.4,1-1C27,24.4,26.6,24,26,24z M11,27.4c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4
+ s2.4,1.1,2.4,2.4C13.4,26.3,12.3,27.4,11,27.4z M26,16h-2.1c-0.4-1.7-2-3-3.9-3s-3.4,1.3-3.9,3H6c-0.6,0-1,0.4-1,1
+ c0,0.6,0.4,1,1,1h10.1c0.4,1.7,2,3,3.9,3s3.4-1.3,3.9-3H26c0.6,0,1-0.4,1-1C27,16.4,26.6,16,26,16z M20,19.4
+ c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4s2.4,1.1,2.4,2.4C22.4,18.3,21.3,19.4,20,19.4z M26,8H14.9c-0.4-1.7-2-3-3.9-3
+ S7.6,6.3,7.1,8H6C5.4,8,5,8.4,5,9c0,0.6,0.4,1,1,1h1.1c0.4,1.7,2,3,3.9,3s3.4-1.3,3.9-3H26c0.6,0,1-0.4,1-1C27,8.4,26.6,8,26,8z
+ M11,11.4c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4s2.4,1.1,2.4,2.4C13.4,10.3,12.3,11.4,11,11.4z M28,0H4C1.8,0,0,1.8,0,4
+ v24c0,2.2,1.8,4,4,4h24c2.2,0,4-1.8,4-4V4C32,1.8,30.2,0,28,0z M30,28c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V4c0-1.1,0.9-2,2-2h24
+ c1.1,0,2,0.9,2,2V28z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/status-bad.svg b/launcher/resources/iOS/scalable/status-bad.svg
new file mode 100644
index 00000000..4019c8da
--- /dev/null
+++ b/launcher/resources/iOS/scalable/status-bad.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#3366CC" d="M26,32H6c-3.4,0-6-2.6-6-6V6c0-3.4,2.6-6,6-6h20c3.4,0,6,2.6,6,6v20C32,29.4,29.4,32,26,32z M28,6
+ c0-1.2-0.8-2-2-2H6C4.8,4,4,4.8,4,6v20c0,1.2,0.8,2,2,2h20c1.2,0,2-0.8,2-2V6z M21.6,21.6c-0.8,0.8-2,0.8-2.8,0L16,18.8l-2.8,2.8
+ c-0.8,0.8-2,0.8-2.8,0c-0.8-0.8-0.8-2,0-2.8l2.8-2.8l-2.8-2.8c-0.8-0.8-0.8-2,0-2.8c0.8-0.8,2-0.8,2.8,0l2.8,2.8l2.8-2.8
+ c0.8-0.8,2-0.8,2.8,0s0.8,2,0,2.8L18.8,16l2.8,2.8C22.4,19.6,22.4,20.8,21.6,21.6z"/>
+</svg>
diff --git a/launcher/resources/iOS/scalable/status-good.svg b/launcher/resources/iOS/scalable/status-good.svg
new file mode 100644
index 00000000..e1859113
--- /dev/null
+++ b/launcher/resources/iOS/scalable/status-good.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="none" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z M28,26c0,1.2-0.8,2-2,2
+ H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2
+ c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+</g>
+<g>
+ <g>
+ <path fill="#3366CC" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z M28,26
+ c0,1.2-0.8,2-2,2H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0
+ s-0.8,2,0,2.8l4.2,4.2c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/status-yellow.svg b/launcher/resources/iOS/scalable/status-yellow.svg
new file mode 100644
index 00000000..d8a28e23
--- /dev/null
+++ b/launcher/resources/iOS/scalable/status-yellow.svg
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="Calque_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 32 32"
+ enable-background="new 0 0 32 32"
+ xml:space="preserve"
+ inkscape:version="0.48.5 r10040"
+ width="100%"
+ height="100%"
+ sodipodi:docname="status-yellow.svg"><metadata
+ id="metadata19"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs17" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1680"
+ inkscape:window-height="1030"
+ id="namedview15"
+ showgrid="false"
+ inkscape:zoom="7.375"
+ inkscape:cx="25.957106"
+ inkscape:cy="16"
+ inkscape:window-x="-4"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="Calque_1" /><g
+ id="g3"><path
+ fill="#E3A333"
+ d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z M28,26 c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"
+ id="path5" /></g><path
+ d="M 6 0 C 2.7 0 0 2.7 0 6 L 0 26 C 0 29.3 2.7 32 6 32 L 26 32 C 29.3 32 32 29.3 32 26 L 32 6 C 32 2.7 29.3 0 26 0 L 6 0 z M 6 4 L 26 4 C 27.1 4 28 4.9 28 6 L 28 26 C 28 27.1 27.1 28 26 28 L 6 28 C 4.9 28 4 27.1 4 26 L 4 6 C 4 4.9 4.9 4 6 4 z "
+ id="path7"
+ style="fill:#3366cc;fill-opacity:1" /><g
+ id="g11"><path
+ fill="#3366CC"
+ d="M6,20c0-5.8,2.4-7.9,5.8-7.9c2,0,3.4,0.7,5.3,1.9c1.6,1,2.5,1.5,3.3,1.5c0.7,0,1.2-0.7,1.3-3.1H26 c0.2,6.1-2.3,8-5.5,8c-2,0-3.5-0.8-5.3-1.9c-1.7-1-2.6-1.6-3.3-1.6c-0.8,0-1.4,0.5-1.6,3.2H6z"
+ id="path13" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/iOS/scalable/viewfolder.svg b/launcher/resources/iOS/scalable/viewfolder.svg
new file mode 100644
index 00000000..0ae0c0b5
--- /dev/null
+++ b/launcher/resources/iOS/scalable/viewfolder.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g id="_x36__4_">
+ <g>
+ <path fill="#3366CC" d="M28,4H14c0-2.2-1.8-4-4-4H4C1.8,0,0,1.8,0,4v24c0,2.2,1.8,4,4,4h24c2.2,0,4-1.8,4-4V8C32,5.8,30.2,4,28,4z
+ M30,28c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V12h28V28z M30,10H2V4c0-1.1,0.9-2,2-2h6c1.1,0,2,0.9,2,2v2h16c1.1,0,2,0.9,2,2V10z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/iOS/scalable/worlds.svg b/launcher/resources/iOS/scalable/worlds.svg
new file mode 100644
index 00000000..1596fd76
--- /dev/null
+++ b/launcher/resources/iOS/scalable/worlds.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata12"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs10" /><g
+ id="_x36__21_"><g
+ id="g4"><path
+ style="fill:#3366cc;fill-opacity:1"
+ id="path2"
+ d="M 4,0 C 1.8,0 0,1.8 0,4 v 24 c 0,2.2 1.8,4 4,4 h 24 c 2.2,0 4,-1.8 4,-4 V 4 C 32,1.8 30.2,0 28,0 Z m 0,2 h 24 c 0.1375,0 0.272461,0.014258 0.402344,0.041016 6.24e-4,1.264e-4 0.0013,-1.27e-4 0.002,0 0.782578,0.1593282 1.395359,0.7721094 1.554687,1.5546875 C 29.98599,3.7261594 30,3.8618476 30,4 v 24 c 0,0.1375 -0.01426,0.272461 -0.04102,0.402344 -1.26e-4,6.24e-4 1.27e-4,0.0013 0,0.002 -0.159328,0.782578 -0.772109,1.395359 -1.554687,1.554687 C 28.273841,29.98599 28.138152,30 28,30 H 4 c -0.1378377,0 -0.2724931,-0.01471 -0.4023438,-0.04102 -6.162e-4,-1.27e-4 -0.00134,1.28e-4 -0.00195,0 -0.7825781,-0.159328 -1.3953593,-0.772109 -1.5546875,-1.554687 -1.27e-4,-6.24e-4 1.264e-4,-0.0013 0,-0.002 C 2.0142578,28.272461 2,28.1375 2,28 V 4 c 0,-0.1378377 0.014708,-0.2724931 0.041016,-0.4023438 1.27e-4,-6.162e-4 -1.275e-4,-0.00134 0,-0.00195 C 2.2003438,2.813125 2.813125,2.2003438 3.5957031,2.0410156 3.7261328,2.0144609 3.8615,2 4,2 Z" /></g></g><g
+ style="stroke:#3366cc;stroke-width:1.63695813;stroke-opacity:1"
+ transform="matrix(1.0584012,0.61035173,-0.61035173,1.0584012,8.831209,-10.700046)"
+ id="g881"><ellipse
+ ry="8.9473686"
+ rx="8.9473696"
+ cy="16"
+ cx="16"
+ id="path845"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#3366cc;stroke-width:1.63695813;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><ellipse
+ ry="8.9473686"
+ rx="3.4915254"
+ cy="15.947369"
+ cx="16"
+ id="path845-3"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#3366cc;stroke-width:1.63695813;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><path
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#3366cc;stroke-width:1.63695813;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 8.1848294,12.430327 C 12.570183,14.790801 20.662272,13.548526 23.355,11.76263"
+ id="path870" /><path
+ id="path872"
+ d="M 8.2166335,20.492522 C 12.601987,18.132048 21.148735,17.776739 23.841463,19.562635"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#3366cc;stroke-width:1.63695813;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/multimc.rc b/launcher/resources/multimc.rc
new file mode 100644
index 00000000..e7340f2a
--- /dev/null
+++ b/launcher/resources/multimc.rc
@@ -0,0 +1,29 @@
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include <windows.h>
+
+IDI_ICON1 ICON DISCARDABLE "MultiMC.ico"
+1 RT_MANIFEST "MultiMC.manifest"
+
+VS_VERSION_INFO VERSIONINFO
+FILEVERSION 1,0,0,0
+FILEOS VOS_NT_WINDOWS32
+FILETYPE VFT_APP
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "000004b0"
+ BEGIN
+ VALUE "CompanyName", "MultiMC Contributors"
+ VALUE "FileDescription", "MultiMC Launcher"
+ VALUE "FileVersion", "1.0.0.0"
+ VALUE "ProductName", "MultiMC"
+ VALUE "ProductVersion", "5"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0000, 0x04b0 // Unicode
+ END
+END
diff --git a/launcher/resources/multimc/128x128/instances/chicken.png b/launcher/resources/multimc/128x128/instances/chicken.png
new file mode 100644
index 00000000..71f6dedc
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/chicken.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/creeper.png b/launcher/resources/multimc/128x128/instances/creeper.png
new file mode 100644
index 00000000..41b7d07d
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/creeper.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/enderpearl.png b/launcher/resources/multimc/128x128/instances/enderpearl.png
new file mode 100644
index 00000000..0a5bf91a
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/enderpearl.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/flame.png b/launcher/resources/multimc/128x128/instances/flame.png
new file mode 100644
index 00000000..8a50a0b4
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/flame.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/ftb_glow.png b/launcher/resources/multimc/128x128/instances/ftb_glow.png
new file mode 100644
index 00000000..86632b21
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/ftb_glow.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/ftb_logo.png b/launcher/resources/multimc/128x128/instances/ftb_logo.png
new file mode 100644
index 00000000..e725b7fe
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/ftb_logo.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/gear.png b/launcher/resources/multimc/128x128/instances/gear.png
new file mode 100644
index 00000000..75c68a66
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/gear.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/herobrine.png b/launcher/resources/multimc/128x128/instances/herobrine.png
new file mode 100644
index 00000000..13f1494c
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/herobrine.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/infinity.png b/launcher/resources/multimc/128x128/instances/infinity.png
new file mode 100644
index 00000000..cc7ca7bc
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/infinity.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/magitech.png b/launcher/resources/multimc/128x128/instances/magitech.png
new file mode 100644
index 00000000..0f81a199
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/magitech.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/meat.png b/launcher/resources/multimc/128x128/instances/meat.png
new file mode 100644
index 00000000..fefc9bf1
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/meat.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/netherstar.png b/launcher/resources/multimc/128x128/instances/netherstar.png
new file mode 100644
index 00000000..132085f0
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/netherstar.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/skeleton.png b/launcher/resources/multimc/128x128/instances/skeleton.png
new file mode 100644
index 00000000..55fcf5a9
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/skeleton.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/squarecreeper.png b/launcher/resources/multimc/128x128/instances/squarecreeper.png
new file mode 100644
index 00000000..c82d8406
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/squarecreeper.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/instances/steve.png b/launcher/resources/multimc/128x128/instances/steve.png
new file mode 100644
index 00000000..a07cbd2f
--- /dev/null
+++ b/launcher/resources/multimc/128x128/instances/steve.png
Binary files differ
diff --git a/launcher/resources/multimc/128x128/unknown_server.png b/launcher/resources/multimc/128x128/unknown_server.png
new file mode 100644
index 00000000..ec98382d
--- /dev/null
+++ b/launcher/resources/multimc/128x128/unknown_server.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/about.png b/launcher/resources/multimc/16x16/about.png
new file mode 100644
index 00000000..a6a986e1
--- /dev/null
+++ b/launcher/resources/multimc/16x16/about.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/bug.png b/launcher/resources/multimc/16x16/bug.png
new file mode 100644
index 00000000..0c5b78b4
--- /dev/null
+++ b/launcher/resources/multimc/16x16/bug.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/cat.png b/launcher/resources/multimc/16x16/cat.png
new file mode 100644
index 00000000..e6e31b44
--- /dev/null
+++ b/launcher/resources/multimc/16x16/cat.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/centralmods.png b/launcher/resources/multimc/16x16/centralmods.png
new file mode 100644
index 00000000..c1b91c76
--- /dev/null
+++ b/launcher/resources/multimc/16x16/centralmods.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/checkupdate.png b/launcher/resources/multimc/16x16/checkupdate.png
new file mode 100644
index 00000000..f3742058
--- /dev/null
+++ b/launcher/resources/multimc/16x16/checkupdate.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/copy.png b/launcher/resources/multimc/16x16/copy.png
new file mode 100644
index 00000000..ccaed9e1
--- /dev/null
+++ b/launcher/resources/multimc/16x16/copy.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/coremods.png b/launcher/resources/multimc/16x16/coremods.png
new file mode 100644
index 00000000..af0f1166
--- /dev/null
+++ b/launcher/resources/multimc/16x16/coremods.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/help.png b/launcher/resources/multimc/16x16/help.png
new file mode 100644
index 00000000..e6edf6ba
--- /dev/null
+++ b/launcher/resources/multimc/16x16/help.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/instance-settings.png b/launcher/resources/multimc/16x16/instance-settings.png
new file mode 100644
index 00000000..b916cd24
--- /dev/null
+++ b/launcher/resources/multimc/16x16/instance-settings.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/jarmods.png b/launcher/resources/multimc/16x16/jarmods.png
new file mode 100644
index 00000000..1a97c9c0
--- /dev/null
+++ b/launcher/resources/multimc/16x16/jarmods.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/loadermods.png b/launcher/resources/multimc/16x16/loadermods.png
new file mode 100644
index 00000000..b5ab3fce
--- /dev/null
+++ b/launcher/resources/multimc/16x16/loadermods.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/log.png b/launcher/resources/multimc/16x16/log.png
new file mode 100644
index 00000000..efa2a0b5
--- /dev/null
+++ b/launcher/resources/multimc/16x16/log.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/minecraft.png b/launcher/resources/multimc/16x16/minecraft.png
new file mode 100644
index 00000000..e9f2f2a5
--- /dev/null
+++ b/launcher/resources/multimc/16x16/minecraft.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/new.png b/launcher/resources/multimc/16x16/new.png
new file mode 100644
index 00000000..2e56f589
--- /dev/null
+++ b/launcher/resources/multimc/16x16/new.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/news.png b/launcher/resources/multimc/16x16/news.png
new file mode 100644
index 00000000..872e85db
--- /dev/null
+++ b/launcher/resources/multimc/16x16/news.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/noaccount.png b/launcher/resources/multimc/16x16/noaccount.png
new file mode 100644
index 00000000..b49bcf36
--- /dev/null
+++ b/launcher/resources/multimc/16x16/noaccount.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/patreon.png b/launcher/resources/multimc/16x16/patreon.png
new file mode 100644
index 00000000..9150c478
--- /dev/null
+++ b/launcher/resources/multimc/16x16/patreon.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/refresh.png b/launcher/resources/multimc/16x16/refresh.png
new file mode 100644
index 00000000..86b6f82c
--- /dev/null
+++ b/launcher/resources/multimc/16x16/refresh.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/resourcepacks.png b/launcher/resources/multimc/16x16/resourcepacks.png
new file mode 100644
index 00000000..d862f5ca
--- /dev/null
+++ b/launcher/resources/multimc/16x16/resourcepacks.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/screenshots.png b/launcher/resources/multimc/16x16/screenshots.png
new file mode 100644
index 00000000..460000d4
--- /dev/null
+++ b/launcher/resources/multimc/16x16/screenshots.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/settings.png b/launcher/resources/multimc/16x16/settings.png
new file mode 100644
index 00000000..b916cd24
--- /dev/null
+++ b/launcher/resources/multimc/16x16/settings.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/star.png b/launcher/resources/multimc/16x16/star.png
new file mode 100644
index 00000000..4963e6ec
--- /dev/null
+++ b/launcher/resources/multimc/16x16/star.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/status-bad.png b/launcher/resources/multimc/16x16/status-bad.png
new file mode 100644
index 00000000..5b3f2051
--- /dev/null
+++ b/launcher/resources/multimc/16x16/status-bad.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/status-good.png b/launcher/resources/multimc/16x16/status-good.png
new file mode 100644
index 00000000..5cbdee81
--- /dev/null
+++ b/launcher/resources/multimc/16x16/status-good.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/status-running.png b/launcher/resources/multimc/16x16/status-running.png
new file mode 100644
index 00000000..a4c42e39
--- /dev/null
+++ b/launcher/resources/multimc/16x16/status-running.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/status-yellow.png b/launcher/resources/multimc/16x16/status-yellow.png
new file mode 100644
index 00000000..b25375d1
--- /dev/null
+++ b/launcher/resources/multimc/16x16/status-yellow.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/viewfolder.png b/launcher/resources/multimc/16x16/viewfolder.png
new file mode 100644
index 00000000..98b8a944
--- /dev/null
+++ b/launcher/resources/multimc/16x16/viewfolder.png
Binary files differ
diff --git a/launcher/resources/multimc/16x16/worlds.png b/launcher/resources/multimc/16x16/worlds.png
new file mode 100644
index 00000000..1a38f38e
--- /dev/null
+++ b/launcher/resources/multimc/16x16/worlds.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/about.png b/launcher/resources/multimc/22x22/about.png
new file mode 100644
index 00000000..57775e25
--- /dev/null
+++ b/launcher/resources/multimc/22x22/about.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/bug.png b/launcher/resources/multimc/22x22/bug.png
new file mode 100644
index 00000000..90481bba
--- /dev/null
+++ b/launcher/resources/multimc/22x22/bug.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/cat.png b/launcher/resources/multimc/22x22/cat.png
new file mode 100644
index 00000000..3ea7ba69
--- /dev/null
+++ b/launcher/resources/multimc/22x22/cat.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/centralmods.png b/launcher/resources/multimc/22x22/centralmods.png
new file mode 100644
index 00000000..a10f9a2b
--- /dev/null
+++ b/launcher/resources/multimc/22x22/centralmods.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/checkupdate.png b/launcher/resources/multimc/22x22/checkupdate.png
new file mode 100644
index 00000000..badb200c
--- /dev/null
+++ b/launcher/resources/multimc/22x22/checkupdate.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/copy.png b/launcher/resources/multimc/22x22/copy.png
new file mode 100644
index 00000000..ea236a24
--- /dev/null
+++ b/launcher/resources/multimc/22x22/copy.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/help.png b/launcher/resources/multimc/22x22/help.png
new file mode 100644
index 00000000..da79b3e3
--- /dev/null
+++ b/launcher/resources/multimc/22x22/help.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/instance-settings.png b/launcher/resources/multimc/22x22/instance-settings.png
new file mode 100644
index 00000000..daf56aad
--- /dev/null
+++ b/launcher/resources/multimc/22x22/instance-settings.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/new.png b/launcher/resources/multimc/22x22/new.png
new file mode 100644
index 00000000..c707fbbf
--- /dev/null
+++ b/launcher/resources/multimc/22x22/new.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/news.png b/launcher/resources/multimc/22x22/news.png
new file mode 100644
index 00000000..1953bf7b
--- /dev/null
+++ b/launcher/resources/multimc/22x22/news.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/patreon.png b/launcher/resources/multimc/22x22/patreon.png
new file mode 100644
index 00000000..f2c2076c
--- /dev/null
+++ b/launcher/resources/multimc/22x22/patreon.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/refresh.png b/launcher/resources/multimc/22x22/refresh.png
new file mode 100644
index 00000000..45b5535c
--- /dev/null
+++ b/launcher/resources/multimc/22x22/refresh.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/screenshots.png b/launcher/resources/multimc/22x22/screenshots.png
new file mode 100644
index 00000000..6fb42bbd
--- /dev/null
+++ b/launcher/resources/multimc/22x22/screenshots.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/settings.png b/launcher/resources/multimc/22x22/settings.png
new file mode 100644
index 00000000..daf56aad
--- /dev/null
+++ b/launcher/resources/multimc/22x22/settings.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/status-bad.png b/launcher/resources/multimc/22x22/status-bad.png
new file mode 100644
index 00000000..2707539e
--- /dev/null
+++ b/launcher/resources/multimc/22x22/status-bad.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/status-good.png b/launcher/resources/multimc/22x22/status-good.png
new file mode 100644
index 00000000..f55debc3
--- /dev/null
+++ b/launcher/resources/multimc/22x22/status-good.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/status-running.png b/launcher/resources/multimc/22x22/status-running.png
new file mode 100644
index 00000000..0dffba18
--- /dev/null
+++ b/launcher/resources/multimc/22x22/status-running.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/status-yellow.png b/launcher/resources/multimc/22x22/status-yellow.png
new file mode 100644
index 00000000..481eb7f3
--- /dev/null
+++ b/launcher/resources/multimc/22x22/status-yellow.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/viewfolder.png b/launcher/resources/multimc/22x22/viewfolder.png
new file mode 100644
index 00000000..b645167f
--- /dev/null
+++ b/launcher/resources/multimc/22x22/viewfolder.png
Binary files differ
diff --git a/launcher/resources/multimc/22x22/worlds.png b/launcher/resources/multimc/22x22/worlds.png
new file mode 100644
index 00000000..e8825bab
--- /dev/null
+++ b/launcher/resources/multimc/22x22/worlds.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/cat.png b/launcher/resources/multimc/24x24/cat.png
new file mode 100644
index 00000000..c93245f6
--- /dev/null
+++ b/launcher/resources/multimc/24x24/cat.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/coremods.png b/launcher/resources/multimc/24x24/coremods.png
new file mode 100644
index 00000000..90603d24
--- /dev/null
+++ b/launcher/resources/multimc/24x24/coremods.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/jarmods.png b/launcher/resources/multimc/24x24/jarmods.png
new file mode 100644
index 00000000..68cb8e9d
--- /dev/null
+++ b/launcher/resources/multimc/24x24/jarmods.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/loadermods.png b/launcher/resources/multimc/24x24/loadermods.png
new file mode 100644
index 00000000..250a6260
--- /dev/null
+++ b/launcher/resources/multimc/24x24/loadermods.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/log.png b/launcher/resources/multimc/24x24/log.png
new file mode 100644
index 00000000..fe302053
--- /dev/null
+++ b/launcher/resources/multimc/24x24/log.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/minecraft.png b/launcher/resources/multimc/24x24/minecraft.png
new file mode 100644
index 00000000..b31177c9
--- /dev/null
+++ b/launcher/resources/multimc/24x24/minecraft.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/noaccount.png b/launcher/resources/multimc/24x24/noaccount.png
new file mode 100644
index 00000000..ac12437c
--- /dev/null
+++ b/launcher/resources/multimc/24x24/noaccount.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/patreon.png b/launcher/resources/multimc/24x24/patreon.png
new file mode 100644
index 00000000..add80668
--- /dev/null
+++ b/launcher/resources/multimc/24x24/patreon.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/resourcepacks.png b/launcher/resources/multimc/24x24/resourcepacks.png
new file mode 100644
index 00000000..68359d39
--- /dev/null
+++ b/launcher/resources/multimc/24x24/resourcepacks.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/star.png b/launcher/resources/multimc/24x24/star.png
new file mode 100644
index 00000000..7f16618a
--- /dev/null
+++ b/launcher/resources/multimc/24x24/star.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/status-bad.png b/launcher/resources/multimc/24x24/status-bad.png
new file mode 100644
index 00000000..d1547a47
--- /dev/null
+++ b/launcher/resources/multimc/24x24/status-bad.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/status-good.png b/launcher/resources/multimc/24x24/status-good.png
new file mode 100644
index 00000000..3545bc4c
--- /dev/null
+++ b/launcher/resources/multimc/24x24/status-good.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/status-running.png b/launcher/resources/multimc/24x24/status-running.png
new file mode 100644
index 00000000..ecd64451
--- /dev/null
+++ b/launcher/resources/multimc/24x24/status-running.png
Binary files differ
diff --git a/launcher/resources/multimc/24x24/status-yellow.png b/launcher/resources/multimc/24x24/status-yellow.png
new file mode 100644
index 00000000..dd5fde67
--- /dev/null
+++ b/launcher/resources/multimc/24x24/status-yellow.png
Binary files differ
diff --git a/launcher/resources/multimc/256x256/minecraft.png b/launcher/resources/multimc/256x256/minecraft.png
new file mode 100644
index 00000000..77e3f03e
--- /dev/null
+++ b/launcher/resources/multimc/256x256/minecraft.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/about.png b/launcher/resources/multimc/32x32/about.png
new file mode 100644
index 00000000..5174c4f1
--- /dev/null
+++ b/launcher/resources/multimc/32x32/about.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/bug.png b/launcher/resources/multimc/32x32/bug.png
new file mode 100644
index 00000000..ada46653
--- /dev/null
+++ b/launcher/resources/multimc/32x32/bug.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/cat.png b/launcher/resources/multimc/32x32/cat.png
new file mode 100644
index 00000000..78ff98e9
--- /dev/null
+++ b/launcher/resources/multimc/32x32/cat.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/centralmods.png b/launcher/resources/multimc/32x32/centralmods.png
new file mode 100644
index 00000000..cd2b8208
--- /dev/null
+++ b/launcher/resources/multimc/32x32/centralmods.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/checkupdate.png b/launcher/resources/multimc/32x32/checkupdate.png
new file mode 100644
index 00000000..754005f9
--- /dev/null
+++ b/launcher/resources/multimc/32x32/checkupdate.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/copy.png b/launcher/resources/multimc/32x32/copy.png
new file mode 100644
index 00000000..c137b0f1
--- /dev/null
+++ b/launcher/resources/multimc/32x32/copy.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/coremods.png b/launcher/resources/multimc/32x32/coremods.png
new file mode 100644
index 00000000..770d695e
--- /dev/null
+++ b/launcher/resources/multimc/32x32/coremods.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/help.png b/launcher/resources/multimc/32x32/help.png
new file mode 100644
index 00000000..b3854278
--- /dev/null
+++ b/launcher/resources/multimc/32x32/help.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instance-settings.png b/launcher/resources/multimc/32x32/instance-settings.png
new file mode 100644
index 00000000..a9c0817c
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instance-settings.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/brick.png b/launcher/resources/multimc/32x32/instances/brick.png
new file mode 100644
index 00000000..c324fda0
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/brick.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/chicken.png b/launcher/resources/multimc/32x32/instances/chicken.png
new file mode 100644
index 00000000..f870467a
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/chicken.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/creeper.png b/launcher/resources/multimc/32x32/instances/creeper.png
new file mode 100644
index 00000000..a67ecfc3
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/creeper.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/diamond.png b/launcher/resources/multimc/32x32/instances/diamond.png
new file mode 100644
index 00000000..1eb26469
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/diamond.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/dirt.png b/launcher/resources/multimc/32x32/instances/dirt.png
new file mode 100644
index 00000000..9e19eb8f
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/dirt.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/enderpearl.png b/launcher/resources/multimc/32x32/instances/enderpearl.png
new file mode 100644
index 00000000..a818eb8e
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/enderpearl.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/flame.png b/launcher/resources/multimc/32x32/instances/flame.png
new file mode 100644
index 00000000..d8987338
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/flame.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/ftb_glow.png b/launcher/resources/multimc/32x32/instances/ftb_glow.png
new file mode 100644
index 00000000..c4e6fd5d
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/ftb_glow.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/ftb_logo.png b/launcher/resources/multimc/32x32/instances/ftb_logo.png
new file mode 100644
index 00000000..20df7171
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/ftb_logo.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/gear.png b/launcher/resources/multimc/32x32/instances/gear.png
new file mode 100644
index 00000000..da9ba2f9
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/gear.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/gold.png b/launcher/resources/multimc/32x32/instances/gold.png
new file mode 100644
index 00000000..593410fa
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/gold.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/grass.png b/launcher/resources/multimc/32x32/instances/grass.png
new file mode 100644
index 00000000..f1694547
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/grass.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/herobrine.png b/launcher/resources/multimc/32x32/instances/herobrine.png
new file mode 100644
index 00000000..e5460da3
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/herobrine.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/infinity.png b/launcher/resources/multimc/32x32/instances/infinity.png
new file mode 100644
index 00000000..bd94a3dc
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/infinity.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/iron.png b/launcher/resources/multimc/32x32/instances/iron.png
new file mode 100644
index 00000000..3e811bd6
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/iron.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/magitech.png b/launcher/resources/multimc/32x32/instances/magitech.png
new file mode 100644
index 00000000..6fd8ff60
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/magitech.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/meat.png b/launcher/resources/multimc/32x32/instances/meat.png
new file mode 100644
index 00000000..6694859d
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/meat.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/netherstar.png b/launcher/resources/multimc/32x32/instances/netherstar.png
new file mode 100644
index 00000000..43cb5113
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/netherstar.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/planks.png b/launcher/resources/multimc/32x32/instances/planks.png
new file mode 100644
index 00000000..a94b7502
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/planks.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/skeleton.png b/launcher/resources/multimc/32x32/instances/skeleton.png
new file mode 100644
index 00000000..0c8d3505
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/skeleton.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/squarecreeper.png b/launcher/resources/multimc/32x32/instances/squarecreeper.png
new file mode 100644
index 00000000..b78c4ae0
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/squarecreeper.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/steve.png b/launcher/resources/multimc/32x32/instances/steve.png
new file mode 100644
index 00000000..07c6acde
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/steve.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/stone.png b/launcher/resources/multimc/32x32/instances/stone.png
new file mode 100644
index 00000000..1b6ef7a4
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/stone.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/instances/tnt.png b/launcher/resources/multimc/32x32/instances/tnt.png
new file mode 100644
index 00000000..e40d404d
--- /dev/null
+++ b/launcher/resources/multimc/32x32/instances/tnt.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/jarmods.png b/launcher/resources/multimc/32x32/jarmods.png
new file mode 100644
index 00000000..5cda173a
--- /dev/null
+++ b/launcher/resources/multimc/32x32/jarmods.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/loadermods.png b/launcher/resources/multimc/32x32/loadermods.png
new file mode 100644
index 00000000..c4ca12e2
--- /dev/null
+++ b/launcher/resources/multimc/32x32/loadermods.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/log.png b/launcher/resources/multimc/32x32/log.png
new file mode 100644
index 00000000..d620da12
--- /dev/null
+++ b/launcher/resources/multimc/32x32/log.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/minecraft.png b/launcher/resources/multimc/32x32/minecraft.png
new file mode 100644
index 00000000..816bec98
--- /dev/null
+++ b/launcher/resources/multimc/32x32/minecraft.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/new.png b/launcher/resources/multimc/32x32/new.png
new file mode 100644
index 00000000..a3555ba4
--- /dev/null
+++ b/launcher/resources/multimc/32x32/new.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/news.png b/launcher/resources/multimc/32x32/news.png
new file mode 100644
index 00000000..c579fd44
--- /dev/null
+++ b/launcher/resources/multimc/32x32/news.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/noaccount.png b/launcher/resources/multimc/32x32/noaccount.png
new file mode 100644
index 00000000..a73afc94
--- /dev/null
+++ b/launcher/resources/multimc/32x32/noaccount.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/patreon.png b/launcher/resources/multimc/32x32/patreon.png
new file mode 100644
index 00000000..70085aa1
--- /dev/null
+++ b/launcher/resources/multimc/32x32/patreon.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/refresh.png b/launcher/resources/multimc/32x32/refresh.png
new file mode 100644
index 00000000..afa2a9d7
--- /dev/null
+++ b/launcher/resources/multimc/32x32/refresh.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/resourcepacks.png b/launcher/resources/multimc/32x32/resourcepacks.png
new file mode 100644
index 00000000..c14759ef
--- /dev/null
+++ b/launcher/resources/multimc/32x32/resourcepacks.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/screenshots.png b/launcher/resources/multimc/32x32/screenshots.png
new file mode 100644
index 00000000..4fcd6224
--- /dev/null
+++ b/launcher/resources/multimc/32x32/screenshots.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/settings.png b/launcher/resources/multimc/32x32/settings.png
new file mode 100644
index 00000000..a9c0817c
--- /dev/null
+++ b/launcher/resources/multimc/32x32/settings.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/star.png b/launcher/resources/multimc/32x32/star.png
new file mode 100644
index 00000000..b271f0d1
--- /dev/null
+++ b/launcher/resources/multimc/32x32/star.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/status-bad.png b/launcher/resources/multimc/32x32/status-bad.png
new file mode 100644
index 00000000..8c2c9d4f
--- /dev/null
+++ b/launcher/resources/multimc/32x32/status-bad.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/status-good.png b/launcher/resources/multimc/32x32/status-good.png
new file mode 100644
index 00000000..1a805e68
--- /dev/null
+++ b/launcher/resources/multimc/32x32/status-good.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/status-running.png b/launcher/resources/multimc/32x32/status-running.png
new file mode 100644
index 00000000..f561f01a
--- /dev/null
+++ b/launcher/resources/multimc/32x32/status-running.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/status-yellow.png b/launcher/resources/multimc/32x32/status-yellow.png
new file mode 100644
index 00000000..42c68552
--- /dev/null
+++ b/launcher/resources/multimc/32x32/status-yellow.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/viewfolder.png b/launcher/resources/multimc/32x32/viewfolder.png
new file mode 100644
index 00000000..74ab8fa6
--- /dev/null
+++ b/launcher/resources/multimc/32x32/viewfolder.png
Binary files differ
diff --git a/launcher/resources/multimc/32x32/worlds.png b/launcher/resources/multimc/32x32/worlds.png
new file mode 100644
index 00000000..c986596c
--- /dev/null
+++ b/launcher/resources/multimc/32x32/worlds.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/about.png b/launcher/resources/multimc/48x48/about.png
new file mode 100644
index 00000000..b4ac71b8
--- /dev/null
+++ b/launcher/resources/multimc/48x48/about.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/bug.png b/launcher/resources/multimc/48x48/bug.png
new file mode 100644
index 00000000..298f9397
--- /dev/null
+++ b/launcher/resources/multimc/48x48/bug.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/cat.png b/launcher/resources/multimc/48x48/cat.png
new file mode 100644
index 00000000..25912a3c
--- /dev/null
+++ b/launcher/resources/multimc/48x48/cat.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/centralmods.png b/launcher/resources/multimc/48x48/centralmods.png
new file mode 100644
index 00000000..d927e39b
--- /dev/null
+++ b/launcher/resources/multimc/48x48/centralmods.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/checkupdate.png b/launcher/resources/multimc/48x48/checkupdate.png
new file mode 100644
index 00000000..2e2c7d6b
--- /dev/null
+++ b/launcher/resources/multimc/48x48/checkupdate.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/copy.png b/launcher/resources/multimc/48x48/copy.png
new file mode 100644
index 00000000..ea40e34b
--- /dev/null
+++ b/launcher/resources/multimc/48x48/copy.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/help.png b/launcher/resources/multimc/48x48/help.png
new file mode 100644
index 00000000..82d828fa
--- /dev/null
+++ b/launcher/resources/multimc/48x48/help.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/instance-settings.png b/launcher/resources/multimc/48x48/instance-settings.png
new file mode 100644
index 00000000..6674eb23
--- /dev/null
+++ b/launcher/resources/multimc/48x48/instance-settings.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/log.png b/launcher/resources/multimc/48x48/log.png
new file mode 100644
index 00000000..45f60e6b
--- /dev/null
+++ b/launcher/resources/multimc/48x48/log.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/minecraft.png b/launcher/resources/multimc/48x48/minecraft.png
new file mode 100644
index 00000000..38fc9f6c
--- /dev/null
+++ b/launcher/resources/multimc/48x48/minecraft.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/new.png b/launcher/resources/multimc/48x48/new.png
new file mode 100644
index 00000000..a81753b3
--- /dev/null
+++ b/launcher/resources/multimc/48x48/new.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/news.png b/launcher/resources/multimc/48x48/news.png
new file mode 100644
index 00000000..0f82d857
--- /dev/null
+++ b/launcher/resources/multimc/48x48/news.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/noaccount.png b/launcher/resources/multimc/48x48/noaccount.png
new file mode 100644
index 00000000..4703796c
--- /dev/null
+++ b/launcher/resources/multimc/48x48/noaccount.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/patreon.png b/launcher/resources/multimc/48x48/patreon.png
new file mode 100644
index 00000000..7aec4d7d
--- /dev/null
+++ b/launcher/resources/multimc/48x48/patreon.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/refresh.png b/launcher/resources/multimc/48x48/refresh.png
new file mode 100644
index 00000000..0b08b238
--- /dev/null
+++ b/launcher/resources/multimc/48x48/refresh.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/screenshots.png b/launcher/resources/multimc/48x48/screenshots.png
new file mode 100644
index 00000000..03c0059f
--- /dev/null
+++ b/launcher/resources/multimc/48x48/screenshots.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/settings.png b/launcher/resources/multimc/48x48/settings.png
new file mode 100644
index 00000000..6674eb23
--- /dev/null
+++ b/launcher/resources/multimc/48x48/settings.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/star.png b/launcher/resources/multimc/48x48/star.png
new file mode 100644
index 00000000..d9468e7e
--- /dev/null
+++ b/launcher/resources/multimc/48x48/star.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/status-bad.png b/launcher/resources/multimc/48x48/status-bad.png
new file mode 100644
index 00000000..41c9cf22
--- /dev/null
+++ b/launcher/resources/multimc/48x48/status-bad.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/status-good.png b/launcher/resources/multimc/48x48/status-good.png
new file mode 100644
index 00000000..df7cb59b
--- /dev/null
+++ b/launcher/resources/multimc/48x48/status-good.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/status-running.png b/launcher/resources/multimc/48x48/status-running.png
new file mode 100644
index 00000000..b8c0bf7c
--- /dev/null
+++ b/launcher/resources/multimc/48x48/status-running.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/status-yellow.png b/launcher/resources/multimc/48x48/status-yellow.png
new file mode 100644
index 00000000..4f3b11d6
--- /dev/null
+++ b/launcher/resources/multimc/48x48/status-yellow.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/viewfolder.png b/launcher/resources/multimc/48x48/viewfolder.png
new file mode 100644
index 00000000..0492a736
--- /dev/null
+++ b/launcher/resources/multimc/48x48/viewfolder.png
Binary files differ
diff --git a/launcher/resources/multimc/48x48/worlds.png b/launcher/resources/multimc/48x48/worlds.png
new file mode 100644
index 00000000..4fc33751
--- /dev/null
+++ b/launcher/resources/multimc/48x48/worlds.png
Binary files differ
diff --git a/launcher/resources/multimc/50x50/instances/enderman.png b/launcher/resources/multimc/50x50/instances/enderman.png
new file mode 100644
index 00000000..9f3a72b3
--- /dev/null
+++ b/launcher/resources/multimc/50x50/instances/enderman.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/about.png b/launcher/resources/multimc/64x64/about.png
new file mode 100644
index 00000000..b83e9269
--- /dev/null
+++ b/launcher/resources/multimc/64x64/about.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/bug.png b/launcher/resources/multimc/64x64/bug.png
new file mode 100644
index 00000000..156b0315
--- /dev/null
+++ b/launcher/resources/multimc/64x64/bug.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/cat.png b/launcher/resources/multimc/64x64/cat.png
new file mode 100644
index 00000000..2cc21f80
--- /dev/null
+++ b/launcher/resources/multimc/64x64/cat.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/centralmods.png b/launcher/resources/multimc/64x64/centralmods.png
new file mode 100644
index 00000000..8831f437
--- /dev/null
+++ b/launcher/resources/multimc/64x64/centralmods.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/checkupdate.png b/launcher/resources/multimc/64x64/checkupdate.png
new file mode 100644
index 00000000..dd1e29ac
--- /dev/null
+++ b/launcher/resources/multimc/64x64/checkupdate.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/copy.png b/launcher/resources/multimc/64x64/copy.png
new file mode 100644
index 00000000..d12cf9c8
--- /dev/null
+++ b/launcher/resources/multimc/64x64/copy.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/coremods.png b/launcher/resources/multimc/64x64/coremods.png
new file mode 100644
index 00000000..668be334
--- /dev/null
+++ b/launcher/resources/multimc/64x64/coremods.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/help.png b/launcher/resources/multimc/64x64/help.png
new file mode 100644
index 00000000..0f3948c2
--- /dev/null
+++ b/launcher/resources/multimc/64x64/help.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/instance-settings.png b/launcher/resources/multimc/64x64/instance-settings.png
new file mode 100644
index 00000000..e3ff58fa
--- /dev/null
+++ b/launcher/resources/multimc/64x64/instance-settings.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/jarmods.png b/launcher/resources/multimc/64x64/jarmods.png
new file mode 100644
index 00000000..55d1a42a
--- /dev/null
+++ b/launcher/resources/multimc/64x64/jarmods.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/loadermods.png b/launcher/resources/multimc/64x64/loadermods.png
new file mode 100644
index 00000000..24618fd0
--- /dev/null
+++ b/launcher/resources/multimc/64x64/loadermods.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/log.png b/launcher/resources/multimc/64x64/log.png
new file mode 100644
index 00000000..0f531cdf
--- /dev/null
+++ b/launcher/resources/multimc/64x64/log.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/new.png b/launcher/resources/multimc/64x64/new.png
new file mode 100644
index 00000000..c3c6796c
--- /dev/null
+++ b/launcher/resources/multimc/64x64/new.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/news.png b/launcher/resources/multimc/64x64/news.png
new file mode 100644
index 00000000..e306eed3
--- /dev/null
+++ b/launcher/resources/multimc/64x64/news.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/patreon.png b/launcher/resources/multimc/64x64/patreon.png
new file mode 100644
index 00000000..ef5d690e
--- /dev/null
+++ b/launcher/resources/multimc/64x64/patreon.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/refresh.png b/launcher/resources/multimc/64x64/refresh.png
new file mode 100644
index 00000000..8373d819
--- /dev/null
+++ b/launcher/resources/multimc/64x64/refresh.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/resourcepacks.png b/launcher/resources/multimc/64x64/resourcepacks.png
new file mode 100644
index 00000000..fb874e7d
--- /dev/null
+++ b/launcher/resources/multimc/64x64/resourcepacks.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/screenshots.png b/launcher/resources/multimc/64x64/screenshots.png
new file mode 100644
index 00000000..af18e39c
--- /dev/null
+++ b/launcher/resources/multimc/64x64/screenshots.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/settings.png b/launcher/resources/multimc/64x64/settings.png
new file mode 100644
index 00000000..e3ff58fa
--- /dev/null
+++ b/launcher/resources/multimc/64x64/settings.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/star.png b/launcher/resources/multimc/64x64/star.png
new file mode 100644
index 00000000..4ed5d978
--- /dev/null
+++ b/launcher/resources/multimc/64x64/star.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/status-bad.png b/launcher/resources/multimc/64x64/status-bad.png
new file mode 100644
index 00000000..64060ba0
--- /dev/null
+++ b/launcher/resources/multimc/64x64/status-bad.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/status-good.png b/launcher/resources/multimc/64x64/status-good.png
new file mode 100644
index 00000000..e862ddcd
--- /dev/null
+++ b/launcher/resources/multimc/64x64/status-good.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/status-running.png b/launcher/resources/multimc/64x64/status-running.png
new file mode 100644
index 00000000..38afda0f
--- /dev/null
+++ b/launcher/resources/multimc/64x64/status-running.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/status-yellow.png b/launcher/resources/multimc/64x64/status-yellow.png
new file mode 100644
index 00000000..3d54d320
--- /dev/null
+++ b/launcher/resources/multimc/64x64/status-yellow.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/viewfolder.png b/launcher/resources/multimc/64x64/viewfolder.png
new file mode 100644
index 00000000..7d531f9c
--- /dev/null
+++ b/launcher/resources/multimc/64x64/viewfolder.png
Binary files differ
diff --git a/launcher/resources/multimc/64x64/worlds.png b/launcher/resources/multimc/64x64/worlds.png
new file mode 100644
index 00000000..1d40f1df
--- /dev/null
+++ b/launcher/resources/multimc/64x64/worlds.png
Binary files differ
diff --git a/launcher/resources/multimc/8x8/noaccount.png b/launcher/resources/multimc/8x8/noaccount.png
new file mode 100644
index 00000000..466e4c07
--- /dev/null
+++ b/launcher/resources/multimc/8x8/noaccount.png
Binary files differ
diff --git a/launcher/resources/multimc/index.theme b/launcher/resources/multimc/index.theme
new file mode 100644
index 00000000..6061b7f8
--- /dev/null
+++ b/launcher/resources/multimc/index.theme
@@ -0,0 +1,58 @@
+[Icon Theme]
+Name=multimc
+Comment=MultiMC Default Icons
+Inherits=default
+Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances
+
+[8x8]
+Size=8
+
+[16x16]
+Size=16
+
+[22x22]
+Size=22
+
+[24x24]
+Size=24
+
+[32x32]
+Size=32
+
+[32x32/instances]
+Size=32
+MinSize=1
+MaxSize=32
+
+[48x48]
+Size=48
+
+[50x50/instances]
+Size=50
+
+[64x64]
+Size=64
+
+[128x128]
+Size=128
+MinSize=33
+MaxSize=128
+
+[128x128/instances]
+Size=128
+MinSize=33
+MaxSize=128
+
+[256x256]
+Size=256
+
+[scalable]
+Size=48
+Type=Scalable
+MinSize=16
+MaxSize=256
+
+[scalable/instances]
+Size=128
+MinSize=16
+MaxSize=256
diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc
new file mode 100644
index 00000000..249e8e28
--- /dev/null
+++ b/launcher/resources/multimc/multimc.qrc
@@ -0,0 +1,320 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/icons/multimc">
+ <file>index.theme</file>
+ <!-- Logo. Our own. For use in branding. -->
+ <file>scalable/logo.svg</file>
+
+ <!-- Logo. Our own. For use within the application (Settings pages and similar). -->
+ <file>scalable/multimc.svg</file>
+
+ <!-- REDDIT logo icon, needs reddit license! -->
+ <file>scalable/reddit-alien.svg</file>
+
+ <!-- Icon for CurseForge. Unknown license? -->
+ <file alias="32x32/flame.png">32x32/instances/flame.png</file>
+ <file alias="128x128/flame.png">128x128/instances/flame.png</file>
+
+ <!-- technic logo icon -->
+ <file>scalable/technic.svg</file>
+
+ <!-- ATLauncher logo icon (and related bits) -->
+ <file>scalable/atlauncher.svg</file>
+ <file>scalable/atlauncher-placeholder.png</file>
+
+ <!-- A proxy icon. Our own. SSSsss -->
+ <file>scalable/proxy.svg</file>
+
+ <!-- A free language icon. http://www.languageicon.org/ -->
+ <file>scalable/language.svg</file>
+
+ <!-- Java icon. From Oracle, fixed because it was derpy. -->
+ <file>scalable/java.svg</file>
+
+ <!-- Star, CC-BY-SA 3.0, Oxygen icons.-->
+ <file>16x16/star.png</file>
+ <file>24x24/star.png</file>
+ <file>32x32/star.png</file>
+ <file>48x48/star.png</file>
+ <file>64x64/star.png</file>
+
+ <!-- "folder-remote", CC-BY-SA 3.0, Oxygen icons. Used for the worlds folder-->
+ <file>16x16/worlds.png</file>
+ <file>22x22/worlds.png</file>
+ <file>32x32/worlds.png</file>
+ <file>48x48/worlds.png</file>
+ <file>64x64/worlds.png</file>
+
+ <!-- Minecraft icon. Source: http://www.minecraftforum.net/forums/show-your-creation/fan-art/1574882-icon-better-minecraft-icon -->
+ <file>16x16/minecraft.png</file>
+ <file>24x24/minecraft.png</file>
+ <file>32x32/minecraft.png</file>
+ <file>48x48/minecraft.png</file>
+ <file>256x256/minecraft.png</file>
+
+ <!-- About dialog. GPLv2, http://openiconlibrary.sourceforge.net/gallery2/?./Icons/actions/help-contents.png -->
+ <file>16x16/about.png</file>
+ <file>22x22/about.png</file>
+ <file>32x32/about.png</file>
+ <file>48x48/about.png</file>
+ <file>64x64/about.png</file>
+
+ <!-- Report bug. Our own. -->
+ <file>scalable/bug.svg</file>
+ <file>16x16/bug.png</file>
+ <file>22x22/bug.png</file>
+ <file>32x32/bug.png</file>
+ <file>48x48/bug.png</file>
+ <file>64x64/bug.png</file>
+
+ <!-- Screenshots. Our own. -->
+ <!-- frame is adapted and simplified from http://www.wpclipart.com/page_frames/picture_frames/golden_picture_frame.png.html -->
+ <file>16x16/screenshots.png</file>
+ <file>22x22/screenshots.png</file>
+ <file>32x32/screenshots.png</file>
+ <file>48x48/screenshots.png</file>
+ <file>64x64/screenshots.png</file>
+ <file>scalable/screenshots.svg</file>
+
+ <!-- Custom commands. -->
+ <file>scalable/custom-commands.svg</file>
+
+ <!-- Patron logo. (C) 2014 Patreon, Inc., http://www.patreon.com/toolbox?ftyp=media -->
+ <file>16x16/patreon.png</file>
+ <file>22x22/patreon.png</file>
+ <file>24x24/patreon.png</file>
+ <file>32x32/patreon.png</file>
+ <file>48x48/patreon.png</file>
+ <file>64x64/patreon.png</file>
+
+ <!-- The cat button. Freeware, http://findicons.com/icon/73096/black_cat -->
+ <file>16x16/cat.png</file>
+ <file>22x22/cat.png</file>
+ <file>24x24/cat.png</file>
+ <file>32x32/cat.png</file>
+ <file>48x48/cat.png</file>
+ <file>64x64/cat.png</file>
+
+ <!-- Show mods folder. CC-BY-SA 3.0 http://openiconlibrary.sourceforge.net/gallery2/?./Icons/places/oxygen-style/folder-favorites.png -->
+ <file>scalable/centralmods.svg</file>
+ <file>16x16/centralmods.png</file>
+ <file>22x22/centralmods.png</file>
+ <file>32x32/centralmods.png</file>
+ <file>48x48/centralmods.png</file>
+ <file>64x64/centralmods.png</file>
+
+ <!-- Update. GPLv2, https://code.google.com/p/gnome-colors/ -->
+ <file>scalable/checkupdate.svg</file>
+ <file>16x16/checkupdate.png</file>
+ <file>22x22/checkupdate.png</file>
+ <file>32x32/checkupdate.png</file>
+ <file>48x48/checkupdate.png</file>
+ <file>64x64/checkupdate.png</file>
+
+ <!-- copy instance. CC-BY-SA 3.0, http://openiconlibrary.sourceforge.net/gallery2/?./Icons/actions/edit-copy-6.png -->
+ <file>16x16/copy.png</file>
+ <file>22x22/copy.png</file>
+ <file>32x32/copy.png</file>
+ <file>48x48/copy.png</file>
+ <file>64x64/copy.png</file>
+
+ <!-- Help. CC-BY-SA 3.0, http://openiconlibrary.sourceforge.net/gallery2/?./Icons/actions/help.png -->
+ <file>16x16/help.png</file>
+ <file>22x22/help.png</file>
+ <file>32x32/help.png</file>
+ <file>48x48/help.png</file>
+ <file>64x64/help.png</file>
+
+ <!-- New instance. GPLv2, http://openiconlibrary.sourceforge.net/gallery2/?./Icons/actions/document-new-3.png -->
+ <file>16x16/new.png</file>
+ <file>22x22/new.png</file>
+ <file>32x32/new.png</file>
+ <file>48x48/new.png</file>
+ <file>64x64/new.png</file>
+
+ <!-- Open news. Our own. -->
+ <file>scalable/news.svg</file>
+ <file>16x16/news.png</file>
+ <file>22x22/news.png</file>
+ <file>32x32/news.png</file>
+ <file>48x48/news.png</file>
+ <file>64x64/news.png</file>
+
+ <!-- Bad status. Our own. -->
+ <file>16x16/status-bad.png</file>
+ <file>24x24/status-bad.png</file>
+ <file>22x22/status-bad.png</file>
+ <file>32x32/status-bad.png</file>
+ <file>48x48/status-bad.png</file>
+ <file>64x64/status-bad.png</file>
+
+ <!-- Good status. Our own. -->
+ <file>16x16/status-good.png</file>
+ <file>24x24/status-good.png</file>
+ <file>22x22/status-good.png</file>
+ <file>32x32/status-good.png</file>
+ <file>48x48/status-good.png</file>
+ <file>64x64/status-good.png</file>
+
+ <!-- Yellow status. Whatever that means... Our own. -->
+ <file>16x16/status-yellow.png</file>
+ <file>24x24/status-yellow.png</file>
+ <file>22x22/status-yellow.png</file>
+ <file>32x32/status-yellow.png</file>
+ <file>48x48/status-yellow.png</file>
+ <file>64x64/status-yellow.png</file>
+
+ <!-- A status icon for things that are in progress/running... Our own. -->
+ <file>16x16/status-running.png</file>
+ <file>24x24/status-running.png</file>
+ <file>22x22/status-running.png</file>
+ <file>32x32/status-running.png</file>
+ <file>48x48/status-running.png</file>
+ <file>64x64/status-running.png</file>
+ <file>scalable/status-running.svg</file>
+
+ <!-- Plugin (blue recolor), CC-BY-SA 3.0, Oxygen icons. -->
+ <file>16x16/loadermods.png</file>
+ <file>24x24/loadermods.png</file>
+ <file>32x32/loadermods.png</file>
+ <file>64x64/loadermods.png</file>
+
+ <!-- Plugin (red recolor), CC-BY-SA 3.0, Oxygen icons. -->
+ <file>16x16/jarmods.png</file>
+ <file>24x24/jarmods.png</file>
+ <file>32x32/jarmods.png</file>
+ <file>64x64/jarmods.png</file>
+
+ <!-- Plugin (green original), CC-BY-SA 3.0, Oxygen icons. -->
+ <file>16x16/coremods.png</file>
+ <file>24x24/coremods.png</file>
+ <file>32x32/coremods.png</file>
+ <file>64x64/coremods.png</file>
+
+ <!-- Resource packs, CC-BY-SA 3.0, Oxygen icons. -->
+ <file>16x16/resourcepacks.png</file>
+ <file>24x24/resourcepacks.png</file>
+ <file>32x32/resourcepacks.png</file>
+ <file>64x64/resourcepacks.png</file>
+
+ <!-- Refresh, CC-BY-SA 3.0, Oxygen icons. -->
+ <file>16x16/refresh.png</file>
+ <file>22x22/refresh.png</file>
+ <file>32x32/refresh.png</file>
+ <file>48x48/refresh.png</file>
+ <file>64x64/refresh.png</file>
+
+ <!-- Settings, LGPL-2.1, http://openiconlibrary.sourceforge.net/gallery2/?./Icons/apps/system-settings-3.png -->
+ <file>16x16/settings.png</file>
+ <file>22x22/settings.png</file>
+ <file>32x32/settings.png</file>
+ <file>48x48/settings.png</file>
+ <file>64x64/settings.png</file>
+
+ <!-- Same, used for instance settings -->
+ <file>16x16/instance-settings.png</file>
+ <file>22x22/instance-settings.png</file>
+ <file>32x32/instance-settings.png</file>
+ <file>48x48/instance-settings.png</file>
+ <file>64x64/instance-settings.png</file>
+
+ <!-- View folder. CC-BY-SA 3.0, http://openiconlibrary.sourceforge.net/gallery2/?./Icons/actions/document-open-folder.png -->
+ <file>scalable/viewfolder.svg</file>
+ <file>16x16/viewfolder.png</file>
+ <file>22x22/viewfolder.png</file>
+ <file>32x32/viewfolder.png</file>
+ <file>48x48/viewfolder.png</file>
+ <file>64x64/viewfolder.png</file>
+
+ <!-- Default minecraft skin, desaturated, cutout of head, Mojang -->
+ <file>8x8/noaccount.png</file>
+ <file>16x16/noaccount.png</file>
+ <file>24x24/noaccount.png</file>
+ <file>32x32/noaccount.png</file>
+ <file>48x48/noaccount.png</file>
+
+ <!-- and the same again. TODO: make a nice accounts icon -->
+ <file alias="8x8/accounts.png">8x8/noaccount.png</file>
+ <file alias="16x16/accounts.png">16x16/noaccount.png</file>
+ <file alias="24x24/accounts.png">24x24/noaccount.png</file>
+ <file alias="32x32/accounts.png">32x32/noaccount.png</file>
+ <file alias="48x48/accounts.png">48x48/noaccount.png</file>
+
+ <!-- Log file, LGPL, http://www.iconarchive.com/show/crystal-clear-icons-by-everaldo/Mimetype-text-icon.html -->
+ <file>16x16/log.png</file>
+ <file>24x24/log.png</file>
+ <file>32x32/log.png</file>
+ <file>48x48/log.png</file>
+ <file>64x64/log.png</file>
+
+ <!-- placeholder for minecraft servers -->
+ <file>128x128/unknown_server.png</file>
+
+ <!-- placeholder when loading screenshot images -->
+ <file>scalable/screenshot-placeholder.svg</file>
+
+ <!-- discord logo icon thing. from discord. traced from bitmap -->
+ <file>scalable/discord.svg</file>
+
+ <!-- instance icons -->
+ <file>32x32/instances/chicken.png</file>
+ <file>128x128/instances/chicken.png</file>
+
+ <file>32x32/instances/creeper.png</file>
+ <file>128x128/instances/creeper.png</file>
+
+ <file>32x32/instances/enderpearl.png</file>
+ <file>128x128/instances/enderpearl.png</file>
+
+ <file>32x32/instances/ftb_glow.png</file>
+ <file>128x128/instances/ftb_glow.png</file>
+
+ <file>32x32/instances/ftb_logo.png</file>
+ <file>128x128/instances/ftb_logo.png</file>
+
+ <file>32x32/instances/flame.png</file>
+ <file>128x128/instances/flame.png</file>
+
+ <file>32x32/instances/gear.png</file>
+ <file>128x128/instances/gear.png</file>
+
+ <file>32x32/instances/herobrine.png</file>
+ <file>128x128/instances/herobrine.png</file>
+
+ <file>32x32/instances/infinity.png</file>
+ <file>128x128/instances/infinity.png</file>
+
+ <file>32x32/instances/magitech.png</file>
+ <file>128x128/instances/magitech.png</file>
+
+ <file>32x32/instances/meat.png</file>
+ <file>128x128/instances/meat.png</file>
+
+ <file>32x32/instances/netherstar.png</file>
+ <file>128x128/instances/netherstar.png</file>
+
+ <file>32x32/instances/skeleton.png</file>
+ <file>128x128/instances/skeleton.png</file>
+
+ <file>32x32/instances/squarecreeper.png</file>
+ <file>128x128/instances/squarecreeper.png</file>
+
+ <file>32x32/instances/steve.png</file>
+ <file>128x128/instances/steve.png</file>
+
+ <file>32x32/instances/brick.png</file>
+ <file>32x32/instances/diamond.png</file>
+ <file>32x32/instances/dirt.png</file>
+ <file>32x32/instances/gold.png</file>
+ <file>32x32/instances/grass.png</file>
+ <file>32x32/instances/iron.png</file>
+ <file>32x32/instances/planks.png</file>
+ <file>32x32/instances/stone.png</file>
+ <file>32x32/instances/tnt.png</file>
+
+ <file>50x50/instances/enderman.png</file>
+
+ <file>scalable/instances/fox.svg</file>
+ <file>scalable/instances/bee.svg</file>
+ </qresource>
+</RCC>
diff --git a/launcher/resources/multimc/scalable/atlauncher-placeholder.png b/launcher/resources/multimc/scalable/atlauncher-placeholder.png
new file mode 100644
index 00000000..f4314c43
--- /dev/null
+++ b/launcher/resources/multimc/scalable/atlauncher-placeholder.png
Binary files differ
diff --git a/launcher/resources/multimc/scalable/atlauncher.svg b/launcher/resources/multimc/scalable/atlauncher.svg
new file mode 100644
index 00000000..1bb5f359
--- /dev/null
+++ b/launcher/resources/multimc/scalable/atlauncher.svg
@@ -0,0 +1,15 @@
+<svg viewBox="0 0 2084 2084" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd"
+ stroke-linejoin="round" stroke-miterlimit="2">
+ <g fill-rule="nonzero">
+ <path d="M1041.67 81.38l272.437 159.032-825.246 478.685-272.438-157.971L1041.67 81.38zm87.28 371.074l274.024-159.032 463.937 271.945-276.14 153.73-461.821-266.643z"
+ fill="#3b3b3b"/>
+ <path d="M216.42 561.126v961.081l825.247 479.746V1684.95l-551.222-321.774-1.587-644.079L216.42 561.126z"
+ fill="#2e2e2e"/>
+ <path d="M1866.91 1517.97l-825.246 483.986v-317.003l550.164-320.714-1.058-645.139 276.14-153.73v952.6z"
+ fill="#333"/>
+ <path d="M1590.77 719.097l-549.106 310.112v165.393l214.246-122.984v488.757l138.599-81.106V989.451l196.261-115.563V719.097z"
+ fill="#89c236"/>
+ <path d="M488.858 719.097l1.587 644.079 152.353 90.118v-198.79l230.645 132.527v199.319l168.753 98.6v-655.741L488.858 719.097zm383.527 531.166l-227.471-131.466v-150.02l227.471 127.225v154.261z"
+ fill="#7baf31"/>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/bug.svg b/launcher/resources/multimc/scalable/bug.svg
new file mode 100644
index 00000000..178e3c23
--- /dev/null
+++ b/launcher/resources/multimc/scalable/bug.svg
@@ -0,0 +1,387 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="bug.svg"
+ inkscape:export-filename="/home/peterix/minecraft/src/MultiMC5/resources/multimc/64x64/bug.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4218">
+ <stop
+ id="stop4220"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:0.18222222" />
+ <stop
+ id="stop4222"
+ offset="1"
+ style="stop-color:#ffffff;stop-opacity:0" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4204">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0.2;"
+ offset="0"
+ id="stop4206" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop4208" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3928">
+ <stop
+ style="stop-color:#ee2200;stop-opacity:1;"
+ offset="0"
+ id="stop3930" />
+ <stop
+ style="stop-color:#5d0000;stop-opacity:1"
+ offset="1"
+ id="stop3932" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3914">
+ <stop
+ style="stop-color:#d41e00;stop-opacity:1;"
+ offset="0"
+ id="stop3916" />
+ <stop
+ style="stop-color:#560000;stop-opacity:1"
+ offset="1"
+ id="stop3918" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3982"
+ x="-0.1382716"
+ width="1.2765432"
+ y="-0.1382716"
+ height="1.2765432">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.17283951"
+ id="feGaussianBlur3984" />
+ </filter>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3928"
+ id="radialGradient4007"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1999988,1.6000096,-0.64514251,0.48385298,660.86724,457.21535)"
+ cx="50"
+ cy="1039.8744"
+ fx="50"
+ fy="1039.8744"
+ r="5" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3914"
+ id="radialGradient4009"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.9840484,3.4721317,-1.0766621,0.61522625,1063.4143,213.8808)"
+ cx="53.469341"
+ cy="1036.8372"
+ fx="53.469341"
+ fy="1036.8372"
+ r="4.0321208" />
+ <filter
+ inkscape:collect="always"
+ id="filter4097">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.55555556"
+ id="feGaussianBlur4099" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4161">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.12345679"
+ id="feGaussianBlur4163" />
+ </filter>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3928"
+ id="radialGradient4174"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-1.195069,-1.6036951,1.3789506,-1.0275877,-1295.4553,2174.0912)"
+ cx="49.742828"
+ cy="1033.5878"
+ fx="49.742828"
+ fy="1033.5878"
+ r="5" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4204"
+ id="linearGradient4212"
+ x1="70.5"
+ y1="1035.3622"
+ x2="69"
+ y2="1036.8622"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4218"
+ id="linearGradient4216"
+ x1="84"
+ y1="1035.3622"
+ x2="85"
+ y2="1034.3622"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-15,-1.5)" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3928"
+ id="radialGradient4237"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-1.195069,-1.6036951,1.3789506,-1.0275877,-1295.4553,2174.0912)"
+ cx="49.742828"
+ cy="1033.5878"
+ fx="49.742828"
+ fy="1033.5878"
+ r="5" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4204"
+ id="linearGradient4239"
+ gradientUnits="userSpaceOnUse"
+ x1="70.5"
+ y1="1035.3622"
+ x2="69"
+ y2="1036.8622" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4218"
+ id="linearGradient4241"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-15,-1.5)"
+ x1="84"
+ y1="1035.3622"
+ x2="85"
+ y2="1034.3622" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="-143.08597"
+ inkscape:cy="-12.323554"
+ inkscape:document-units="px"
+ inkscape:current-layer="g3986"
+ showgrid="false"
+ inkscape:window-width="1680"
+ inkscape:window-height="1026"
+ inkscape:window-x="-3"
+ inkscape:window-y="-4"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1020.3622)">
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4013"
+ d="m 10.25,1022.0497 0,4 -4,4 -4,0 0,2 4,0 0,2 c 2e-6,1.4503 0.117423,2.7742 0.3125,4 l -4.3125,0 0,2 4.75,0 c 0.379006,1.4252 0.851671,2.6914 1.46875,3.7813 l -2.21875,2.2187 0,2 2,0 1.875,-1.875 c 1.218087,1.3481 2.651451,2.2896 4.125,2.9375 l 0,2.9375 2,0 0,-2.25 c 5.000011,1.25 10.000061,-0.7499 10,-3.75 2.599309,0 4.428024,-3.7452 4.0625,-8 l 1.9375,0 0,-2 -2.25,0 c -0.338089,-1.3524 -0.923138,-2.7153 -1.78125,-3.9687 l 2.03125,-2.0313 0,-2 -2,0 -1.875,1.875 c -1.104498,-0.9979 -2.47625,-1.8385 -4.125,-2.5 l 0,-3.375 -2,0 0,2.75 c -1.736548,-0.4618 -3.716809,-0.75 -6,-0.75 l -2,0 0,-4 -2,0 z"
+ style="opacity:0.69037655;fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter4097)" />
+ <g
+ id="g3986"
+ transform="matrix(2,0,0,2,-89,-1043.3622)">
+ <rect
+ transform="translate(0,1020.3622)"
+ y="24"
+ x="51"
+ height="3"
+ width="1"
+ id="rect3876"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ <rect
+ y="1040.3622"
+ x="45.5"
+ height="1"
+ width="2.5"
+ id="rect3874"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ <path
+ transform="translate(0,1020.3622)"
+ inkscape:connector-curvature="0"
+ id="path3878"
+ d="m 46.5,24.5 1,0.5 2,-2 0.5,0 -1,-1 z"
+ style="fill:#000000;stroke:none"
+ sodipodi:nodetypes="cccccc" />
+ <rect
+ transform="translate(0,1020.3622)"
+ y="19"
+ x="58"
+ height="1"
+ width="2"
+ id="rect3870"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ <path
+ transform="translate(0,1020.3622)"
+ inkscape:connector-curvature="0"
+ id="path3868"
+ d="M 57,16.5 58.5,15 59,16 57.5,17.5 C 56.533856,16.56406 55.788428,17.105786 57,16.5 z"
+ style="fill:#000000;stroke:none"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ transform="translate(0,1020.3622)"
+ inkscape:connector-curvature="0"
+ id="path3866"
+ d="m 47,18 0,-2 2,-2 2,0 c 0.815212,1.242754 1.053351,2.549628 0,4 -1.239551,0.522502 -2.476067,1.061913 -4,0 z"
+ style="fill:#333333;stroke:none"
+ sodipodi:nodetypes="cccccc" />
+ <rect
+ transform="translate(0,1020.3622)"
+ y="13"
+ x="54"
+ height="2"
+ width="1"
+ id="rect3872"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ <rect
+ transform="translate(0,1020.3622)"
+ y="12"
+ x="49"
+ height="3"
+ width="1"
+ id="rect3880"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ <rect
+ transform="translate(0,1020.3622)"
+ y="16"
+ x="45"
+ height="1"
+ width="3"
+ id="rect3882"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccccc"
+ style="opacity:0.80753138;fill:#1a1a1a;stroke:none;filter:url(#filter3982)"
+ d="m 47.75,1039.1747 0.140625,-2.1562 0.640625,-1.2032 1.125,-0.8125 2.03125,-0.062 -0.21875,3.9375 z"
+ id="path3948"
+ inkscape:connector-curvature="0" />
+ <g
+ id="g4232"
+ transform="translate(-17,5.5)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4165"
+ d="m 68,1028.8622 c 0.803076,1.5119 0.470137,2.7762 0,4 -1.461682,0.7487 -2.788281,0.7094 -4,0 1.5e-5,10 10.000061,9.0001 10,6 3.000056,0 4.000023,-10 -6,-10 z"
+ style="fill:url(#radialGradient4237);fill-opacity:1;stroke:none" />
+ <path
+ style="fill:url(#linearGradient4239);fill-opacity:1;stroke:none"
+ d="m 68,1032.8622 6,6 c 6.1e-5,3.0001 -9.999985,4 -10,-6 1.211719,0.7094 2.538318,0.7487 4,0 z"
+ id="path4194"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path4196"
+ d="m 68,1032.8622 5.999942,6 c 3.000056,0 4.000029,-10 -5.999994,-10 0.803076,1.5119 0.470189,2.7762 5.2e-5,4 z"
+ style="fill:url(#linearGradient4241);fill-opacity:1;stroke:none" />
+ </g>
+ <path
+ transform="matrix(1.5,0,0,1.5,-27.5,1011.8622)"
+ d="M 54,16.5 C 54,16.776142 53.776142,17 53.5,17 53.223858,17 53,16.776142 53,16.5 53,16.223858 53.223858,16 53.5,16 c 0.276142,0 0.5,0.223858 0.5,0.5 z"
+ sodipodi:ry="0.5"
+ sodipodi:rx="0.5"
+ sodipodi:cy="16.5"
+ sodipodi:cx="53.5"
+ id="path3936"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ sodipodi:type="arc" />
+ <path
+ transform="matrix(1.5,0,0,1.5,-28,1010.8622)"
+ d="M 56,18.5 C 56,18.776142 55.776142,19 55.5,19 55.223858,19 55,18.776142 55,18.5 55,18.223858 55.223858,18 55.5,18 c 0.276142,0 0.5,0.223858 0.5,0.5 z"
+ sodipodi:ry="0.5"
+ sodipodi:rx="0.5"
+ sodipodi:cy="18.5"
+ sodipodi:cx="55.5"
+ id="path3938"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ sodipodi:type="arc" />
+ <path
+ transform="matrix(1.5,0,0,1.5,-25,1009.3622)"
+ d="M 50,20.5 C 50,20.776142 49.776142,21 49.5,21 49.223858,21 49,20.776142 49,20.5 49,20.223858 49.223858,20 49.5,20 c 0.276142,0 0.5,0.223858 0.5,0.5 z"
+ sodipodi:ry="0.5"
+ sodipodi:rx="0.5"
+ sodipodi:cy="20.5"
+ sodipodi:cx="49.5"
+ id="path3940"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ sodipodi:type="arc" />
+ <path
+ transform="matrix(1.5,0,0,1.5,-26,1008.8622)"
+ d="M 52,22.5 C 52,22.776142 51.776142,23 51.5,23 51.223858,23 51,22.776142 51,22.5 51,22.223858 51.223858,22 51.5,22 c 0.276142,0 0.5,0.223858 0.5,0.5 z"
+ sodipodi:ry="0.5"
+ sodipodi:rx="0.5"
+ sodipodi:cy="22.5"
+ sodipodi:cx="51.5"
+ id="path3942"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ sodipodi:type="arc" />
+ <path
+ transform="matrix(1.5,0,0,1.5,-26.5,1009.3622)"
+ d="M 54,23.5 C 54,23.776142 53.776142,24 53.5,24 53.223858,24 53,23.776142 53,23.5 53,23.223858 53.223858,23 53.5,23 c 0.276142,0 0.5,0.223858 0.5,0.5 z"
+ sodipodi:ry="0.5"
+ sodipodi:rx="0.5"
+ sodipodi:cy="23.5"
+ sodipodi:cx="53.5"
+ id="path3944"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ sodipodi:type="arc" />
+ <path
+ transform="matrix(1.5,0,0,1.5,-27.5,1010.3622)"
+ d="M 57,20.5 C 57,20.776142 56.776142,21 56.5,21 56.223858,21 56,20.776142 56,20.5 56,20.223858 56.223858,20 56.5,20 c 0.276142,0 0.5,0.223858 0.5,0.5 z"
+ sodipodi:ry="0.5"
+ sodipodi:rx="0.5"
+ sodipodi:cy="20.5"
+ sodipodi:cx="56.5"
+ id="path3946"
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ sodipodi:type="arc" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/centralmods.svg b/launcher/resources/multimc/scalable/centralmods.svg
new file mode 100644
index 00000000..a8b123d0
--- /dev/null
+++ b/launcher/resources/multimc/scalable/centralmods.svg
@@ -0,0 +1,346 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) --><svg height="128" id="svg2811" inkscape:export-filename="/home/pinheiro/pics/oxygen/scalable/places/folder-bookmarks.png" inkscape:export-xdpi="360" inkscape:export-ydpi="360" inkscape:output_extension="org.inkscape.output.svgz.inkscape" inkscape:version="0.46" sodipodi:docbase="/home/david/oxygen/trunk/scalable/places" sodipodi:docname="folder-bookmarks.svgz" sodipodi:version="0.32" version="1.0" width="128" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <metadata>
+ <rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <cc:Work rdf:about="">
+ <dc:title></dc:title>
+ <dc:description></dc:description>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>unsorted</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <dc:publisher>
+ <cc:Agent rdf:about="http://www.openclipart.org/">
+ <dc:title>Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title></dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title></dc:title>
+ </cc:Agent>
+ </dc:rights>
+ <dc:date></dc:date>
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/ or http://creativecommons.org/licenses/LGPL/2.1/"/>
+ <dc:language>en</dc:language>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs id="defs2813">
+ <linearGradient id="linearGradient3629" inkscape:collect="always">
+ <stop id="stop3631" offset="0" style="stop-color:#ffda36;stop-opacity:1;"/>
+ <stop id="stop3633" offset="1" style="stop-color:#ffda36;stop-opacity:0;"/>
+ </linearGradient>
+ <inkscape:perspective id="perspective99" inkscape:persp3d-origin="64 : 42.666667 : 1" inkscape:vp_x="0 : 64 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="128 : 64 : 1" sodipodi:type="inkscape:persp3d"/>
+ <linearGradient gradientTransform="matrix(1.0033808,0,0,1,-8.2378002,8)" gradientUnits="userSpaceOnUse" id="linearGradient3067" x1="122.74438" x2="122.39215" y1="96.721588" y2="20.043535">
+ <stop id="stop3069" offset="0" style="stop-color:#88c4ff;stop-opacity:1;"/>
+ <stop id="stop3077" offset="0.13053299" style="stop-color:#b3d9ff;stop-opacity:1;"/>
+ <stop id="stop3071" offset="0.72006166" style="stop-color:#71A8F5"/>
+ <stop id="stop3073" offset="1" style="stop-color:#508ed9;stop-opacity:1;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient2822">
+ <stop id="stop2824" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop2826" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(1.0033404,0,0,1,-8.2374684,8)" gradientUnits="userSpaceOnUse" id="XMLID_9_" x1="71.9995" x2="71.9995" y1="14.2578" y2="19.9583">
+ <stop id="stop46" offset="0.25" style="stop-color:#71a8f5;stop-opacity:0;"/>
+ <stop id="stop48" offset="1" style="stop-color:#0057ae;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(1.0033876,0,0,1,1.7561237,8)" gradientUnits="userSpaceOnUse" id="XMLID_8_" x1="72.0005" x2="72.0005" y1="96" y2="20.0005">
+ <stop id="stop35" offset="0" style="stop-color:#cfe7ff;stop-opacity:1;"/>
+ <stop id="stop37" offset="0.5917" style="stop-color:#71A8F5"/>
+ <stop id="stop39" offset="1" style="stop-color:#2C72C7"/>
+ </linearGradient>
+ <filter height="1.768" id="filter2807" inkscape:collect="always" width="1.0512" x="-0.0256" y="-0.384">
+ <feGaussianBlur id="feGaussianBlur2809" inkscape:collect="always" stdDeviation="1.28"/>
+ </filter>
+ <linearGradient gradientTransform="translate(-7.999995,8)" gradientUnits="userSpaceOnUse" id="XMLID_6_" x1="72.0005" x2="72.0005" y1="96" y2="4.882812e-04">
+ <stop id="stop7" offset="0" style="stop-color:#00479E"/>
+ <stop id="stop9" offset="0.0769" style="stop-color:#2C72C7"/>
+ <stop id="stop11" offset="0.58579999" style="stop-color:#6ea1df;stop-opacity:1;"/>
+ <stop id="stop13" offset="0.96450001" style="stop-color:#adcbee;stop-opacity:1;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="translate(10,0)" gradientUnits="userSpaceOnUse" id="linearGradient2828" inkscape:collect="always" x1="22.413761" x2="22.413761" xlink:href="#linearGradient2822" y1="28.5" y2="34.472866"/>
+ <linearGradient gradientTransform="matrix(1.0033876,0,0,1,-8.2438763,8)" gradientUnits="userSpaceOnUse" id="linearGradient2838" inkscape:collect="always" x1="72.0005" x2="72.0005" xlink:href="#XMLID_8_" y1="96" y2="20.0005"/>
+ <clipPath clipPathUnits="userSpaceOnUse" id="clipPath2844">
+ <path d="M 133.86613,100.978 C 133.79288,102.369 132.59585,103.5 131.19913,103.5 L 16.812952,103.5 C 15.415236,103.5 14.2192,102.368 14.145947,100.979 C 14.145947,100.979 10.508666,30.967 10.508666,30.957 C 10.461513,30.301 10.673223,29.685 11.105684,29.22 C 11.539144,28.756 12.140172,28.501 12.799403,28.501 L 135.21268,28.501 C 135.8719,28.501 136.47293,28.756 136.90339,29.219 C 137.33384,29.682 137.54254,30.298 137.49137,30.966 L 133.86613,100.978 z" id="path2846" style="fill:#ff00bf"/>
+ </clipPath>
+ <filter height="1.8302754" id="filter2848" inkscape:collect="always" width="1.0247144" x="-0.012357198" y="-0.41513768">
+ <feGaussianBlur id="feGaussianBlur2850" inkscape:collect="always" stdDeviation="0.65388509"/>
+ </filter>
+ <linearGradient gradientTransform="matrix(1.0033808,0,0,1,-8.2378,8)" gradientUnits="userSpaceOnUse" id="linearGradient3109" inkscape:collect="always" x1="122.74438" x2="122.74438" xlink:href="#linearGradient3067" y1="96.721588" y2="20"/>
+ <linearGradient id="linearGradient3290">
+ <stop id="stop3292" offset="0" style="stop-color:yellow;stop-opacity:1;"/>
+ <stop id="stop3294" offset="1" style="stop-color:#ffb66d;stop-opacity:1;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3273">
+ <stop id="stop3275" offset="0" style="stop-color:#ffffff;stop-opacity:0.55035973;"/>
+ <stop id="stop3277" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3291">
+ <stop id="stop3293" offset="0" style="stop-color:black;stop-opacity:1"/>
+ <stop id="stop3295" offset="1" style="stop-color:black;stop-opacity:0"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3638">
+ <stop id="stop3640" offset="0" style="stop-color:#ffffff;stop-opacity:0;"/>
+ <stop id="stop3661" offset="0.06868132" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3659" offset="0.5" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3642" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient12948">
+ <stop id="stop12950" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop12952" offset="1" style="stop-color:#c0c0c0;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient1563">
+ <stop id="stop1565" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop1567" offset="1" style="stop-color:white;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="41.233166" cy="39.832623" fx="41.409943" fy="44.369892" gradientTransform="matrix(1,0,0,0.75,0,9.9581557)" gradientUnits="userSpaceOnUse" id="radialGradient3673" inkscape:collect="always" r="8.1317282" xlink:href="#linearGradient3309"/>
+ <radialGradient cx="41.233166" cy="39.832623" fx="41.409943" fy="44.369892" gradientTransform="matrix(1,0,0,0.75,0,9.9581557)" gradientUnits="userSpaceOnUse" id="radialGradient3669" inkscape:collect="always" r="8.1317282" xlink:href="#linearGradient3309"/>
+ <filter id="filter3663" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur3665" inkscape:collect="always" stdDeviation="0.24178075"/>
+ </filter>
+ <radialGradient cx="41.233166" cy="39.832623" fx="41.409943" fy="44.369892" gradientTransform="matrix(1,0,0,0.75,0,9.9581557)" gradientUnits="userSpaceOnUse" id="radialGradient3638" inkscape:collect="always" r="8.1317282" xlink:href="#linearGradient3640"/>
+ <radialGradient cx="122.69361" cy="52.672272" fx="122.55822" fy="51.026066" gradientTransform="matrix(1.2538745,-0.4598801,0.2575547,0.7022291,-43.321636,71.69735)" gradientUnits="userSpaceOnUse" id="radialGradient3620" inkscape:collect="always" r="3.1883843" xlink:href="#linearGradient1563"/>
+ <radialGradient cx="121.58587" cy="52.85474" fx="121.58587" fy="52.85474" gradientTransform="matrix(2.2248115,-0.5961364,0.2773621,1.0351295,-163.57967,70.62501)" gradientUnits="userSpaceOnUse" id="radialGradient3618" inkscape:collect="always" r="3.1883843" xlink:href="#linearGradient1563"/>
+ <filter height="1.2126913" id="filter3604" inkscape:collect="always" width="1.2307636" x="-0.11538182" y="-0.10634566">
+ <feGaussianBlur id="feGaussianBlur3606" inkscape:collect="always" stdDeviation="0.25849222"/>
+ </filter>
+ <radialGradient cx="122.69361" cy="52.672272" fx="122.55822" fy="51.026066" gradientTransform="matrix(1.2538745,-0.4598801,0.2575547,0.7022291,-43.321636,71.69735)" gradientUnits="userSpaceOnUse" id="radialGradient3570" inkscape:collect="always" r="3.1883843" xlink:href="#linearGradient1563"/>
+ <filter height="1.3358284" id="filter3564" inkscape:collect="always" width="1.3643636" x="-0.18218182" y="-0.1679142">
+ <feGaussianBlur id="feGaussianBlur3566" inkscape:collect="always" stdDeviation="0.40814561"/>
+ </filter>
+ <radialGradient cx="121.58587" cy="52.85474" fx="121.58587" fy="52.85474" gradientTransform="matrix(2.2248115,-0.5961364,0.2773621,1.0351295,-163.57967,70.62501)" gradientUnits="userSpaceOnUse" id="radialGradient3382" inkscape:collect="always" r="3.1883843" xlink:href="#linearGradient1563"/>
+ <radialGradient cx="64.101562" cy="48.703125" fx="64.101562" fy="48.703125" gradientTransform="matrix(1,0,0,0.6476898,0,17.158608)" gradientUnits="userSpaceOnUse" id="radialGradient3372" inkscape:collect="always" r="56.812496" xlink:href="#linearGradient3309"/>
+ <filter id="filter3362" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur3364" inkscape:collect="always" stdDeviation="0.3785028"/>
+ </filter>
+ <radialGradient cx="26.573795" cy="73.493042" fx="35.587811" fy="102.79941" gradientTransform="matrix(4.6812453,-5.2700969e-7,3.571426e-8,0.3172375,-85.242554,44.725131)" gradientUnits="userSpaceOnUse" id="radialGradient3300" inkscape:collect="always" r="60.700562" xlink:href="#linearGradient3946"/>
+ <filter id="filter3285" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur3287" inkscape:collect="always" stdDeviation="0.26585998"/>
+ </filter>
+ <radialGradient cx="64.35347" cy="106.71302" fx="64.35347" fy="106.71302" gradientTransform="matrix(0.4443044,1.2598841e-8,0,0.1825067,35.563425,89.646593)" gradientUnits="userSpaceOnUse" id="radialGradient3267" inkscape:collect="always" r="60.700505" xlink:href="#linearGradient3946"/>
+ <radialGradient cx="64.07962" cy="66.197433" fx="64.07962" fy="66.197433" gradientTransform="matrix(1,0,0,0.9554688,0,2.9478533)" gradientUnits="userSpaceOnUse" id="radialGradient3263" inkscape:collect="always" r="60.700504" xlink:href="#XMLID_1_"/>
+ <filter id="filter3259" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur3261" inkscape:collect="always" stdDeviation="0.23739589"/>
+ </filter>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient2475" inkscape:collect="always" x1="64.07962" x2="64.07962" xlink:href="#XMLID_1_" y1="-14.227339" y2="120.44466"/>
+ <radialGradient cx="64.35347" cy="98.207405" fx="64.35347" fy="98.207405" gradientTransform="matrix(9.1358439e-2,-2.9656957e-8,4.5207376e-8,0.1392616,58.276716,94.261412)" gradientUnits="userSpaceOnUse" id="radialGradient3956" inkscape:collect="always" r="60.700505" xlink:href="#linearGradient3946"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient3934" inkscape:collect="always" spreadMethod="reflect" x1="28.637825" x2="31.289474" xlink:href="#linearGradient3844" y1="120.84999" y2="122.08743"/>
+ <filter id="filter3928" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur3930" inkscape:collect="always" stdDeviation="0.18346262"/>
+ </filter>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient2728" inkscape:collect="always" spreadMethod="reflect" x1="28.637825" x2="31.289474" xlink:href="#linearGradient3844" y1="120.84999" y2="122.08743"/>
+ <filter height="1.3101371" id="filter3838" inkscape:collect="always" width="1.3563383" x="-0.17816916" y="-0.15506857">
+ <feGaussianBlur id="feGaussianBlur3840" inkscape:collect="always" stdDeviation="0.46259975"/>
+ </filter>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient3800" inkscape:collect="always" spreadMethod="reflect" x1="63.948792" x2="67.219337" xlink:href="#linearGradient3363" y1="12.034382" y2="12.034382"/>
+ <filter id="filter3401" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur3403" inkscape:collect="always" stdDeviation="0.11157909"/>
+ </filter>
+ <filter id="filter3391" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur3393" inkscape:collect="always" stdDeviation="0.55939545"/>
+ </filter>
+ <linearGradient gradientTransform="translate(-152,0)" gradientUnits="userSpaceOnUse" id="linearGradient3385" inkscape:collect="always" x1="216.88614" x2="216.88614" xlink:href="#linearGradient3379" y1="122.5867" y2="37.969955"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient3369" inkscape:collect="always" x1="177.42397" x2="177.60074" xlink:href="#linearGradient3363" y1="22.377773" y2="93.022789"/>
+ <linearGradient gradientTransform="translate(-37.771032,-0.1213203)" gradientUnits="userSpaceOnUse" id="linearGradient3345" inkscape:collect="always" x1="261.50107" x2="200.17728" xlink:href="#linearGradient1563" y1="77.652245" y2="31.10997"/>
+ <linearGradient gradientTransform="translate(-170.08594,0)" gradientUnits="userSpaceOnUse" id="linearGradient3315" inkscape:collect="always" x1="219.22163" x2="219.22163" xlink:href="#linearGradient3363" y1="28.149843" y2="116.41813"/>
+ <radialGradient cx="69.526619" cy="60.115833" fx="69.526619" fy="60.115833" gradientTransform="matrix(0.5227399,0,-1.554444e-8,0.5266221,349.81061,60.575712)" gradientUnits="userSpaceOnUse" id="radialGradient3304" inkscape:collect="always" r="111.65377" xlink:href="#linearGradient3290"/>
+ <radialGradient cx="69.526619" cy="60.115833" fx="69.526619" fy="89.655701" gradientTransform="matrix(0.9439139,-0.3301918,0.332644,0.9509241,-16.097695,27.249949)" gradientUnits="userSpaceOnUse" id="radialGradient2906" inkscape:collect="always" r="111.65377" xlink:href="#linearGradient3290"/>
+ <linearGradient gradientTransform="matrix(0,1.022977,-1.022977,0,111.9686,137.8125)" gradientUnits="userSpaceOnUse" id="linearGradient3230" inkscape:collect="always" x1="-88.058083" x2="-45.096584" xlink:href="#linearGradient3711" y1="-131.93112" y2="-131.93112"/>
+ <radialGradient cx="343.99899" cy="92" fx="343.99899" fy="92" gradientUnits="userSpaceOnUse" id="radialGradient3228" inkscape:collect="always" r="36" xlink:href="#linearGradient3711"/>
+ <linearGradient gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)" gradientUnits="userSpaceOnUse" id="linearGradient3226" inkscape:collect="always" x1="-70.002899" x2="-11.91648" xlink:href="#linearGradient26907" y1="-383.9971" y2="-383.9971"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient2149" inkscape:collect="always" x1="62.112335" x2="67.887672" xlink:href="#linearGradient3081" y1="90.513916" y2="39.095695"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="XMLID_4_" x1="64.000504" x2="64.000504" y1="108.8652" y2="92.865196">
+ <stop id="stop7270" offset="0" style="stop-color:#EEEEEC"/>
+ <stop id="stop7272" offset="1" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <linearGradient gradientTransform="translate(175.0067,11.74752)" gradientUnits="userSpaceOnUse" id="XMLID_3_" x1="63.9995" x2="63.9995" y1="92.865196" y2="120.8652">
+ <stop id="stop7261" offset="0" style="stop-color:#888A85"/>
+ <stop id="stop7263" offset="0.3226" style="stop-color:#A6A7A3"/>
+ <stop id="stop7265" offset="1" style="stop-color:#EEEEEC"/>
+ </linearGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" id="XMLID_1_" x1="95.693398" x2="32.308601" y1="141.1738" y2="77.789001">
+ <stop id="stop7227" offset="0" style="stop-color:#ffc60a;stop-opacity:1;"/>
+ <stop id="stop7233" offset="1" style="stop-color:#b03b00;stop-opacity:1;"/>
+ </linearGradient>
+ <foreignObject height="1" id="foreignObject7221" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/" width="1" x="0" y="0">
+ <i:pgfRef xlink:href="#adobe_illustrator_pgf"/>
+ </foreignObject>
+ <linearGradient id="linearGradient5073" inkscape:collect="always">
+ <stop id="stop5075" offset="0" style="stop-color:#000000;stop-opacity:1;"/>
+ <stop id="stop5077" offset="1" style="stop-color:#000000;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="80.738739" cy="155.37218" fx="80.738739" fy="177.29686" gradientTransform="matrix(1,0,0,0.111111,0,138.1081)" gradientUnits="userSpaceOnUse" id="radialGradient5079" inkscape:collect="always" r="64.796692" xlink:href="#linearGradient5073"/>
+ <linearGradient id="linearGradient2690">
+ <stop id="stop2692" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop2694" offset="1" style="stop-color:#c0c0c0;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient2684">
+ <stop id="stop2686" offset="0" style="stop-color:#ffffff;stop-opacity:0.55035973;"/>
+ <stop id="stop2688" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient2678">
+ <stop id="stop2680" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop2682" offset="1" style="stop-color:white;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient2668">
+ <stop id="stop2670" offset="0" style="stop-color:#ffffff;stop-opacity:0;"/>
+ <stop id="stop2672" offset="0.06868132" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop2674" offset="0.5" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop2676" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient2662">
+ <stop id="stop2664" offset="0" style="stop-color:yellow;stop-opacity:1;"/>
+ <stop id="stop2666" offset="1" style="stop-color:#f07800;stop-opacity:1;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3081">
+ <stop id="stop3083" offset="0" style="stop-color:#28691f;stop-opacity:1;"/>
+ <stop id="stop3085" offset="1" style="stop-color:#00bf00;stop-opacity:1;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)" gradientUnits="userSpaceOnUse" id="linearGradient3711" x1="-84.002403" x2="-23.516129" y1="-383.9971" y2="-383.9975">
+ <stop id="stop3713" offset="0" style="stop-color:white;stop-opacity:1;"/>
+ <stop id="stop3715" offset="1" style="stop-color:white;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)" gradientUnits="userSpaceOnUse" id="linearGradient26907" x1="-84.002403" x2="-12.0029" y1="-383.9971" y2="-383.9971">
+ <stop id="stop26909" offset="0" style="stop-color:#888a85;stop-opacity:1;"/>
+ <stop id="stop26911" offset="1" style="stop-color:#2e3436;stop-opacity:1;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3309">
+ <stop id="stop3311" offset="0" style="stop-color:#f4ff3f;stop-opacity:1;"/>
+ <stop id="stop3313" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3363" inkscape:collect="always">
+ <stop id="stop3365" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3367" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3379" inkscape:collect="always">
+ <stop id="stop3381" offset="0" style="stop-color:#fffc07;stop-opacity:1;"/>
+ <stop id="stop3383" offset="1" style="stop-color:#fffc07;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3844" inkscape:collect="always">
+ <stop id="stop3846" offset="0" style="stop-color:#faff64;stop-opacity:1;"/>
+ <stop id="stop3848" offset="1" style="stop-color:#faff64;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient id="linearGradient3946">
+ <stop id="stop3948" offset="0" style="stop-color:#7e0000;stop-opacity:1;"/>
+ <stop id="stop3950" offset="1" style="stop-color:#673400;stop-opacity:0;"/>
+ </linearGradient>
+ <inkscape:perspective id="perspective102" inkscape:persp3d-origin="64 : 42.666667 : 1" inkscape:vp_x="0 : 64 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="128 : 64 : 1" sodipodi:type="inkscape:persp3d"/>
+ <linearGradient id="linearGradient3640">
+ <stop id="stop3643" offset="0" style="stop-color:#7e0000;stop-opacity:0.50990099"/>
+ <stop id="stop3645" offset="1" style="stop-color:#673400;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="64.07962" cy="66.197433" fx="64.07962" fy="66.197433" gradientTransform="matrix(1,0,0,0.9554688,0,2.9478533)" gradientUnits="userSpaceOnUse" id="radialGradient2810" inkscape:collect="always" r="60.700504" xlink:href="#XMLID_1_"/>
+ <radialGradient cx="69.526619" cy="60.115833" fx="69.526619" fy="89.655701" gradientTransform="matrix(0.9439139,-0.3301918,0.332644,0.9509241,-16.097695,27.249949)" gradientUnits="userSpaceOnUse" id="radialGradient2812" inkscape:collect="always" r="111.65377" xlink:href="#linearGradient3290"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient2814" inkscape:collect="always" x1="64.07962" x2="64.07962" xlink:href="#XMLID_1_" y1="-14.227339" y2="120.44466"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient2816" inkscape:collect="always" spreadMethod="reflect" x1="63.948792" x2="67.219337" xlink:href="#linearGradient3363" y1="12.034382" y2="12.034382"/>
+ <radialGradient cx="64.101562" cy="48.703125" fx="64.101562" fy="48.703125" gradientTransform="matrix(1,0,0,0.6476898,0,17.158608)" gradientUnits="userSpaceOnUse" id="radialGradient2818" inkscape:collect="always" r="56.812496" xlink:href="#linearGradient3309"/>
+ <radialGradient cx="41.233166" cy="39.832623" fx="41.409943" fy="44.369892" gradientTransform="matrix(1,0,0,0.75,0,9.9581557)" gradientUnits="userSpaceOnUse" id="radialGradient2820" inkscape:collect="always" r="8.1317282" xlink:href="#linearGradient3309"/>
+ <linearGradient gradientTransform="translate(-170.08594,0)" gradientUnits="userSpaceOnUse" id="linearGradient2823" inkscape:collect="always" x1="219.22163" x2="219.22163" xlink:href="#linearGradient3363" y1="28.149843" y2="116.41813"/>
+ <radialGradient cx="41.233166" cy="39.832623" fx="41.409943" fy="44.369892" gradientTransform="matrix(1,0,0,0.75,0,9.9581557)" gradientUnits="userSpaceOnUse" id="radialGradient2825" inkscape:collect="always" r="8.1317282" xlink:href="#linearGradient3309"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient2827" inkscape:collect="always" x1="177.42397" x2="177.60074" xlink:href="#linearGradient3363" y1="22.377773" y2="93.022789"/>
+ <linearGradient gradientTransform="translate(-37.771032,-0.1213203)" gradientUnits="userSpaceOnUse" id="linearGradient2829" inkscape:collect="always" x1="261.50107" x2="200.17728" xlink:href="#linearGradient1563" y1="77.652245" y2="31.10997"/>
+ <linearGradient gradientTransform="matrix(0.479981,0,0,0.479981,33.177757,62.637707)" gradientUnits="userSpaceOnUse" id="linearGradient2831" inkscape:collect="always" spreadMethod="reflect" x1="80.100487" x2="77.714729" xlink:href="#linearGradient3273" y1="44.807674" y2="101.4734"/>
+ <radialGradient cx="64.35347" cy="98.207405" fx="64.35347" fy="98.207405" gradientTransform="matrix(9.1358439e-2,-2.9656957e-8,4.5207376e-8,0.1392616,58.276716,94.261412)" gradientUnits="userSpaceOnUse" id="radialGradient2833" inkscape:collect="always" r="60.700505" xlink:href="#linearGradient3946"/>
+ <linearGradient gradientTransform="translate(-152,0)" gradientUnits="userSpaceOnUse" id="linearGradient2835" inkscape:collect="always" x1="216.88614" x2="216.88614" xlink:href="#linearGradient3379" y1="122.5867" y2="37.969955"/>
+ <radialGradient cx="121.58587" cy="52.85474" fx="121.58587" fy="52.85474" gradientTransform="matrix(2.2248115,-0.5961364,0.2773621,1.0351295,-163.57967,70.62501)" gradientUnits="userSpaceOnUse" id="radialGradient2837" inkscape:collect="always" r="3.1883843" xlink:href="#linearGradient1563"/>
+ <radialGradient cx="122.69361" cy="52.672272" fx="122.55822" fy="51.026066" gradientTransform="matrix(1.2538745,-0.4598801,0.2575547,0.7022291,-43.321636,71.69735)" gradientUnits="userSpaceOnUse" id="radialGradient2839" inkscape:collect="always" r="3.1883843" xlink:href="#linearGradient1563"/>
+ <radialGradient cx="121.58587" cy="52.85474" fx="121.58587" fy="52.85474" gradientTransform="matrix(2.2248115,-0.5961364,0.2773621,1.0351295,-163.57967,70.62501)" gradientUnits="userSpaceOnUse" id="radialGradient2841" inkscape:collect="always" r="3.1883843" xlink:href="#linearGradient1563"/>
+ <radialGradient cx="122.69361" cy="52.672272" fx="122.55822" fy="51.026066" gradientTransform="matrix(1.2538745,-0.4598801,0.2575547,0.7022291,-43.321636,71.69735)" gradientUnits="userSpaceOnUse" id="radialGradient2843" inkscape:collect="always" r="3.1883843" xlink:href="#linearGradient1563"/>
+ <linearGradient gradientTransform="translate(-152,0)" gradientUnits="userSpaceOnUse" id="linearGradient2845" inkscape:collect="always" x1="216.88614" x2="216.88614" xlink:href="#linearGradient3379" y1="122.5867" y2="37.969955"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient2847" inkscape:collect="always" spreadMethod="reflect" x1="28.637825" x2="31.289474" xlink:href="#linearGradient3844" y1="120.84999" y2="122.08743"/>
+ <radialGradient cx="64.35347" cy="106.71302" fx="64.35347" fy="106.71302" gradientTransform="matrix(0.4443044,1.2598841e-8,0,0.1825067,35.563425,89.646593)" gradientUnits="userSpaceOnUse" id="radialGradient2849" inkscape:collect="always" r="60.700505" xlink:href="#linearGradient3946"/>
+ <radialGradient cx="26.573795" cy="73.493042" fx="35.587811" fy="102.79941" gradientTransform="matrix(4.6812453,-5.2700969e-7,3.571426e-8,0.3172375,-85.242554,44.725131)" gradientUnits="userSpaceOnUse" id="radialGradient2851" inkscape:collect="always" r="60.700562" xlink:href="#linearGradient3946"/>
+ <linearGradient gradientTransform="matrix(0.479981,0,0,0.479981,33.177757,62.637707)" gradientUnits="userSpaceOnUse" id="linearGradient2853" inkscape:collect="always" spreadMethod="reflect" x1="80.100487" x2="77.714729" xlink:href="#linearGradient3273" y1="44.807674" y2="101.4734"/>
+ <linearGradient gradientTransform="translate(-152,0)" gradientUnits="userSpaceOnUse" id="linearGradient2855" inkscape:collect="always" x1="216.88614" x2="216.88614" xlink:href="#linearGradient3379" y1="122.5867" y2="37.969955"/>
+ <radialGradient cx="66.977158" cy="54.133701" fx="66.977158" fy="54.133701" gradientTransform="matrix(1.6011045,0,0,1.0874708,-40.260269,-12.584491)" gradientUnits="userSpaceOnUse" id="radialGradient3635" inkscape:collect="always" r="41.380203" xlink:href="#linearGradient3629"/>
+ <filter id="filter3653" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur3655" inkscape:collect="always" stdDeviation="2.160955"/>
+ </filter>
+ <radialGradient cx="64.111018" cy="72.034855" fx="64.111018" fy="72.034855" gradientTransform="matrix(1,0,0,0.6792004,0,23.108756)" gradientUnits="userSpaceOnUse" id="radialGradient3659" inkscape:collect="always" r="41.380203" xlink:href="#linearGradient3629"/>
+ <radialGradient cx="66.890625" cy="94.021507" fx="66.890625" fy="94.021507" gradientTransform="matrix(1.3950344,0,0,0.9475079,-26.4241,-2.1179212)" gradientUnits="userSpaceOnUse" id="radialGradient3663" inkscape:collect="always" r="41.380203" xlink:href="#linearGradient3629"/>
+ </defs>
+ <sodipodi:namedview bordercolor="#666666" borderlayer="true" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="1.7230397" inkscape:cy="67.535605" inkscape:document-units="px" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:showpageshadow="true" inkscape:window-height="750" inkscape:window-width="991" inkscape:window-x="45" inkscape:window-y="0" inkscape:zoom="2.34375" pagecolor="#ffffff" showborder="true" showgrid="true">
+ <inkscape:grid enabled="true" id="grid3665" spacingx="4px" spacingy="4px" type="xygrid" visible="true"/>
+ </sodipodi:namedview>
+ <g id="layer1" inkscape:groupmode="layer" inkscape:label="Livello 1">
+ <path d="M 117.983,19 C 117.992,17.35 116.64999,16 114.99999,16 L 44.961007,16 L 38.375006,9.414 C 37.597005,8.636 36.061006,8 34.961,8 L 13.999998,8 C 11.799996,8 9.999999,9.8 9.999999,12 L 9.999999,19 C 9.999999,19 10.999999,104 7,104 L 121,104 C 116.99999,104 117.983,19 117.983,19 z" id="path15" style="fill:url(#XMLID_6_)"/>
+ <g id="g17" style="opacity:0.6;filter:url(#filter2807)" transform="matrix(1.0033404,0,0,1,-8.2374684,8)">
+ <path d="M 132,95.146667 C 132,96.877333 128.4,98.293333 124,98.293333 L 20,98.293333 C 15.6,98.293333 12,96.877333 12,95.146667 C 12,93.416 15.6,92 20,92 L 124,92 C 128.4,92 132,93.416 132,95.146667 z" id="path19"/>
+ </g>
+ <path d="M 124.36598,101.004 C 124.27969,102.652 122.85389,104 121.19831,104 L 6.812906,104 C 5.157329,104 3.731522,102.652 3.644228,101.004 L 0.007982,30.992 C -0.112423,29.347 1.143808,28 2.799384,28 L 125.21183,28 C 126.86741,28 128.11762,29.346 127.9912,30.991 L 124.36598,101.004 z" id="path30" style="opacity:0.9;fill:url(#linearGradient3109);fill-opacity:1"/>
+ <path d="M 10.580094,28 C 10.551086,23.609 10.516087,20.392 10.500078,19 L 10.500078,12 C 10.500078,10.07 12.07033,8.5 14.000627,8.5 L 34.964921,8.5 C 35.937077,8.5 37.339294,9.081 38.026407,9.768 L 44.759457,16.5 L 115.01648,16.5 C 115.68059,16.5 116.30469,16.76 116.77576,17.233 C 117.24683,17.706 117.50487,18.333 117.50087,18.997 C 117.50087,18.997 117.57288,18.925 117.60989,18.888 C 117.48787,19.495 117.47487,21.062 117.44687,25.161 L 117.43687,28 L 117.92894,28 C 117.95994,22.468 117.99995,19 117.99995,19 C 118.00896,17.35 116.66674,16 115.01648,16 L 44.966498,16 L 38.379463,9.414 C 37.60134,8.636 36.065099,8 34.964921,8 L 14.000627,8 C 11.80028,8 10,9.8 10,12 L 10,19 C 10,19 10.041009,22.468 10.073018,28 L 10.580094,28 z" id="path32" style="fill:#5293ee;fill-opacity:1"/>
+ <path d="M 9.8848616,22 C 9.8938916,23.75 9.9029216,25.755 9.9109516,28 L 118.09511,28 C 118.10414,25.755 118.11317,23.75 118.1212,22 L 9.8848616,22 z" id="path50" style="fill:url(#XMLID_9_);opacity:0.5"/>
+ <path clip-path="url(#clipPath2844)" d="M 12.8125,28.5 C 12.153269,28.5 11.52721,28.754751 11.09375,29.21875 C 10.661289,29.68375 10.452847,30.312749 10.5,30.96875 C 10.5,30.968937 10.559992,32.232788 10.5625,32.28125 C 10.647737,31.886257 10.815287,31.518165 11.09375,31.21875 C 11.52721,30.754751 12.153269,30.5 12.8125,30.5 L 135.21875,30.5 C 135.87797,30.500001 136.47579,30.75575 136.90625,31.21875 C 137.18342,31.516877 137.35439,31.883216 137.4375,32.28125 L 137.5,30.96875 C 137.55117,30.30075 137.3367,29.68175 136.90625,29.21875 C 136.47579,28.75575 135.87797,28.500001 135.21875,28.5 L 12.8125,28.5 z" id="path2788" style="fill:url(#linearGradient2828);fill-opacity:1;filter:url(#filter2848)" transform="translate(-10,0)"/>
+ <path d="M 125.21268,28 L 2.799403,28 C 1.143815,28 -0.1124237,29.347 0.007982,30.992 L 3.645263,101.004 C 3.731547,102.652 5.157364,104 6.812952,104 L 121.19913,104 C 122.85472,104 124.28053,102.652 124.36682,101.004 L 127.99106,30.991 C 128.11849,29.346 126.86827,28 125.21268,28 z M 123.86613,100.978 C 123.79288,102.369 122.59585,103.5 121.19913,103.5 L 6.812952,103.5 C 5.415236,103.5 4.2192,102.368 4.145947,100.979 C 4.145947,100.979 0.508666,30.967 0.508666,30.957 C 0.461513,30.301 0.673223,29.685 1.105684,29.22 C 1.539144,28.756 2.140172,28.501 2.799403,28.501 L 125.21268,28.501 C 125.8719,28.501 126.47293,28.756 126.90339,29.219 C 127.33384,29.682 127.54254,30.298 127.49137,30.966 L 123.86613,100.978 z" id="path2836" style="fill:url(#linearGradient2838)"/>
+ <path d="M 65.931929,44.786008 C 55.175741,44.634267 51.043165,70.978907 35.902168,70.978907 L 97.750451,73.3925 C 81.772038,73.3925 79.337655,44.975127 65.931929,44.786008 z" id="path2857" sodipodi:nodetypes="cccs" style="fill:url(#radialGradient3635);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3653)" transform="matrix(1.0562371,0,0,1.0562371,-3.6968591,-4.2252662)"/>
+ <g id="g2758" inkscape:label="Layer 1" transform="matrix(0.6943204,0,0,0.6943204,21.245436,37.651516)">
+ <g id="g3675">
+ <path d="M 64.03125,8.2 C 56.162818,8.2100117 46.828561,34.754451 40.46875,39.3875 C 34.10894,44.020548 5.9844574,44.776082 3.5625,52.2625 C 1.1405426,59.748917 23.465249,76.813524 25.90625,84.29375 C 28.347251,91.77398 20.40967,118.7394 26.78125,123.35625 C 33.15283,127.9731 56.287818,112.02251 64.15625,112.0125 C 72.024682,112.00249 95.202691,127.89555 101.5625,123.2625 C 107.92231,118.62945 99.890544,91.686414 102.3125,84.2 C 104.73446,76.713583 127.03475,59.58648 124.59375,52.10625 C 122.15275,44.626021 94.027829,43.941849 87.65625,39.325 C 81.28467,34.708152 71.899685,8.1899879 64.03125,8.2 z M 64.03125,12.10625 C 64.208046,12.245423 65.56776,12.912264 67.15625,14.85625 C 68.97167,17.077947 71.031426,20.410059 73.0625,23.95 C 75.093573,27.48994 77.113982,31.248819 79.09375,34.5125 C 81.073519,37.776182 82.75512,40.528991 85.40625,42.45 C 88.057376,44.371009 91.18831,45.11637 94.90625,45.98125 C 98.624192,46.846129 102.81606,47.591152 106.8125,48.41875 C 110.80894,49.246347 114.60465,50.166787 117.28125,51.2 C 119.62327,52.104061 120.71845,53.200764 120.90625,53.325 C 120.82618,53.533062 120.57672,54.994782 119.21875,57.10625 C 117.66679,59.519356 115.1453,62.518181 112.40625,65.54375 C 109.66721,68.569316 106.71091,71.652346 104.21875,74.54375 C 101.72659,77.435155 99.632744,79.897501 98.625,83.0125 C 97.617256,86.127495 97.892393,89.334266 98.21875,93.1375 C 98.545107,96.940738 99.114622,101.17466 99.5625,105.23125 C 100.01038,109.28783 100.31178,113.17888 100.15625,116.04375 C 100.02016,118.55052 99.34151,119.89095 99.28125,120.10625 C 99.057443,120.09786 97.552762,120.37027 95.125,119.73125 C 92.350417,119.00093 88.723899,117.49504 85,115.825 C 81.276103,114.15497 77.426259,112.30169 73.90625,110.825 C 70.386242,109.3483 67.4302,108.13334 64.15625,108.1375 C 60.882303,108.14167 57.891241,109.3706 54.375,110.85625 C 50.858761,112.3419 47.032137,114.20799 43.3125,115.8875 C 39.592862,117.567 35.960216,119.05638 33.1875,119.79375 C 30.761373,120.43895 29.286908,120.19088 29.0625,120.2 C 29.004012,119.9864 28.29872,118.6439 28.15625,116.1375 C 27.993428,113.27303 28.281199,109.38271 28.71875,105.325 C 29.156299,101.2673 29.714573,97.035302 30.03125,93.23125 C 30.347928,89.427198 30.609418,86.187425 29.59375,83.075 C 28.578082,79.962573 26.468263,77.522553 23.96875,74.6375 C 21.469238,71.752452 18.527988,68.687339 15.78125,65.66875 C 13.034512,62.650158 10.495601,59.671649 8.9375,57.2625 C 7.5741618,55.154496 7.3592053,53.657399 7.28125,53.45 C 7.2962039,53.537785 8.2681026,52.326785 10.84375,51.325 C 13.517705,50.284977 17.34943,49.350265 21.34375,48.5125 C 25.33807,47.674737 29.534272,46.949339 33.25,46.075 C 36.96573,45.200663 40.103767,44.44025 42.75,42.5125 C 45.396234,40.584748 47.059794,37.812458 49.03125,34.54375 C 51.002705,31.275042 53.009191,27.526347 55.03125,23.98125 C 57.053308,20.436153 59.096493,17.08256 60.90625,14.85625 C 62.489787,12.908229 63.857465,12.244552 64.03125,12.10625 z" id="path3961" style="opacity:1;fill:url(#radialGradient2810);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3259)"/>
+ <path d="M 153.09403,94.713757 C 144.53658,107.09689 92.616372,93.013297 78.414631,98.001518 C 64.21289,102.98974 32.50348,146.4474 18.082028,142.13539 C 3.6605746,137.82337 1.0106378,84.092245 -8.1220219,72.127031 C -17.254681,60.161818 -68.384124,43.433534 -68.739625,28.385431 C -69.095125,13.337327 -18.812666,-5.7867426 -10.255219,-18.169872 C -1.697772,-30.553002 -1.5880954,-84.349316 12.613645,-89.337536 C 26.815387,-94.325757 60.541592,-52.41396 74.963045,-48.101941 C 89.384498,-43.789923 140.58172,-60.30959 149.71438,-48.344376 C 158.84704,-36.379162 129.40853,8.6478227 129.76403,23.695927 C 130.11953,38.74403 161.65148,82.330628 153.09403,94.713757 z" id="path3574" inkscape:flatsided="false" inkscape:randomized="0" inkscape:rounded="0.20136392" sodipodi:arg1="0.60469864" sodipodi:arg2="1.2330172" sodipodi:cx="52.952892" sodipodi:cy="25.510532" sodipodi:r1="121.72647" sodipodi:r2="76.832565" sodipodi:sides="5" sodipodi:type="star" style="opacity:1;fill:url(#radialGradient2812);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" transform="matrix(0.4934214,0.1726044,-0.1726044,0.4934214,42.377875,49.908537)"/>
+ <path d="M 64.03125,8 C 56.162818,8.0100117 46.828561,34.554451 40.46875,39.1875 C 34.10894,43.820548 5.984457,44.576082 3.5625,52.0625 C 1.140543,59.548917 23.465249,76.613524 25.90625,84.09375 C 28.347251,91.57398 20.40967,118.5394 26.78125,123.15625 C 33.15283,127.7731 56.287818,111.82251 64.15625,111.8125 C 72.024682,111.80249 95.202691,127.69555 101.5625,123.0625 C 107.92231,118.42945 99.890544,91.486414 102.3125,84 C 104.73446,76.513583 127.03475,59.38648 124.59375,51.90625 C 122.15275,44.426021 94.027829,43.741849 87.65625,39.125 C 81.28467,34.508152 71.899685,7.9899879 64.03125,8 z M 64.03125,11.90625 C 64.208046,12.045423 65.56776,12.712264 67.15625,14.65625 C 68.97167,16.877947 71.031426,20.210059 73.0625,23.75 C 75.093573,27.28994 77.113982,31.048819 79.09375,34.3125 C 81.073519,37.576182 82.75512,40.328991 85.40625,42.25 C 88.057376,44.171009 91.18831,44.91637 94.90625,45.78125 C 98.624192,46.646129 102.81606,47.391152 106.8125,48.21875 C 110.80894,49.046347 114.60465,49.966787 117.28125,51 C 119.62327,51.904061 120.71845,53.000764 120.90625,53.125 C 120.82618,53.333062 120.57672,54.794782 119.21875,56.90625 C 117.66679,59.319356 115.1453,62.318181 112.40625,65.34375 C 109.66721,68.369316 106.71091,71.452346 104.21875,74.34375 C 101.72659,77.235155 99.632744,79.697501 98.625,82.8125 C 97.617256,85.927495 97.892393,89.134266 98.21875,92.9375 C 98.545107,96.740738 99.114622,100.97466 99.5625,105.03125 C 100.01038,109.08783 100.31178,112.97888 100.15625,115.84375 C 100.02016,118.35052 99.34151,119.69095 99.28125,119.90625 C 99.057443,119.89786 97.552762,120.17027 95.125,119.53125 C 92.350417,118.80093 88.723899,117.29504 85,115.625 C 81.276103,113.95497 77.426259,112.10169 73.90625,110.625 C 70.386242,109.1483 67.4302,107.93334 64.15625,107.9375 C 60.882303,107.94167 57.891241,109.1706 54.375,110.65625 C 50.858761,112.1419 47.032137,114.00799 43.3125,115.6875 C 39.592862,117.367 35.960216,118.85638 33.1875,119.59375 C 30.761373,120.23895 29.286908,119.99088 29.0625,120 C 29.004012,119.7864 28.29872,118.4439 28.15625,115.9375 C 27.993428,113.07303 28.281199,109.18271 28.71875,105.125 C 29.156299,101.0673 29.714573,96.835302 30.03125,93.03125 C 30.347928,89.227198 30.609418,85.987425 29.59375,82.875 C 28.578082,79.762573 26.468263,77.322553 23.96875,74.4375 C 21.469238,71.552452 18.527988,68.487339 15.78125,65.46875 C 13.034512,62.450158 10.495601,59.471649 8.9375,57.0625 C 7.574162,54.954496 7.359205,53.457399 7.28125,53.25 C 7.296204,53.337785 8.268103,52.126785 10.84375,51.125 C 13.517705,50.084977 17.34943,49.150265 21.34375,48.3125 C 25.33807,47.474737 29.534272,46.749339 33.25,45.875 C 36.96573,45.000663 40.103767,44.24025 42.75,42.3125 C 45.396234,40.384748 47.059794,37.612458 49.03125,34.34375 C 51.002705,31.075042 53.009191,27.326347 55.03125,23.78125 C 57.053308,20.236153 59.096493,16.88256 60.90625,14.65625 C 62.489787,12.708229 63.857465,12.044552 64.03125,11.90625 z" id="path2304" style="opacity:1;fill:url(#linearGradient2814);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 60.98796,9.471226 C 62.846491,8.2143022 64.889907,8.0204702 67.219338,9.471226 L 64.037358,15.614216 L 60.98796,9.471226 z" id="path3409" sodipodi:nodetypes="cccc" style="fill:url(#linearGradient2816);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3838)"/>
+ <path d="M 64.039064,11.90625 C 63.865274,12.044552 62.497594,12.708229 60.914064,14.65625 C 59.104304,16.88256 57.061124,20.236153 55.039064,23.78125 C 53.017004,27.326347 51.010514,31.075042 49.039064,34.34375 C 47.067604,37.612458 45.404044,40.384748 42.757814,42.3125 C 40.111574,44.24025 36.973544,45.000663 33.257814,45.875 C 29.542084,46.749339 25.345884,47.474737 21.351564,48.3125 C 17.357244,49.150265 13.525514,50.084977 10.851564,51.125 C 8.2759131,52.126785 7.3040131,53.337785 7.2890631,53.25 C 7.3670131,53.457399 7.5819731,54.954496 8.9453131,57.0625 C 10.503414,59.471649 13.042324,62.450158 15.789064,65.46875 C 18.535804,68.487339 21.477054,71.552452 23.976564,74.4375 C 26.476074,77.322553 28.585894,79.762573 29.601564,82.875 C 29.865144,83.682722 30.019904,84.511238 30.132814,85.34375 C 32.540654,85.431079 34.961934,85.5 37.414064,85.5 C 64.456484,85.5 88.974124,80.107134 106.91406,71.34375 C 108.71383,69.370041 110.60784,67.338911 112.41406,65.34375 C 115.15311,62.318181 117.67459,59.319356 119.22656,56.90625 C 120.58453,54.794782 120.83398,53.333062 120.91406,53.125 C 120.72626,53.000764 119.63107,51.904061 117.28906,51 C 114.61246,49.966787 110.81674,49.046347 106.82031,48.21875 C 102.82387,47.391152 98.631994,46.646129 94.914064,45.78125 C 91.196124,44.91637 88.065184,44.171009 85.414064,42.25 C 82.762934,40.328991 81.081334,37.576182 79.101564,34.3125 C 77.121794,31.048819 75.101384,27.28994 73.070314,23.75 C 71.039234,20.210059 68.979484,16.877947 67.164064,14.65625 C 65.575574,12.712264 64.215854,12.045423 64.039064,11.90625 z" id="path3370" style="opacity:1;fill:url(#radialGradient2818);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 44.96875,33.9375 L 44.1875,35.21875 C 44.177097,35.219516 44.166653,35.219516 44.15625,35.21875 L 42.8125,36.9375 L 41.53125,38.46875 C 41.521947,38.480223 41.511473,38.490697 41.5,38.5 L 40.03125,39.71875 C 40.021947,39.730223 40.011473,39.740697 40,39.75 L 37.125,41.03125 C 37.115697,41.042723 37.105223,41.053197 37.09375,41.0625 L 33.875,41.96875 L 33.375,45.625 L 35.125,45.21875 L 35.125,45.25 L 39.71875,43.75 L 39.75,43.75 L 42.8125,42.0625 L 42.875,42.03125 L 45.78125,39.09375 L 45.8125,39.0625 L 48.96875,34.03125 L 44.96875,33.9375 z" id="path3667" inkscape:original="M 44.84375 33.71875 L 44 35.09375 L 42.65625 36.8125 L 41.375 38.34375 L 39.90625 39.5625 L 37.03125 40.84375 L 33.6875 41.78125 L 33.09375 45.9375 L 35.1875 45.4375 L 39.8125 43.9375 L 42.96875 42.21875 L 45.96875 39.21875 L 49.375 33.8125 L 44.84375 33.71875 z " inkscape:radius="-0.21269609" sodipodi:type="inkscape:offset" style="opacity:0.64356432;fill:url(#radialGradient2820);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3663)" transform="translate(0,0.3)"/>
+ <path d="M 64.039064,11.90625 C 63.865274,12.044552 62.497594,12.708229 60.914064,14.65625 C 59.104304,16.88256 57.061124,20.236153 55.039064,23.78125 C 53.017004,27.326347 51.010514,31.075042 49.039064,34.34375 C 47.067604,37.612458 45.404044,40.384748 42.757814,42.3125 C 40.111574,44.24025 36.973544,45.000663 33.257814,45.875 C 29.542084,46.749339 25.345884,47.474737 21.351564,48.3125 C 17.357244,49.150265 13.525514,50.084977 10.851564,51.125 C 8.2759131,52.126785 7.3040131,53.337785 7.2890631,53.25 C 7.3670131,53.457399 7.5819731,54.954496 8.9453131,57.0625 C 10.503414,59.471649 13.042324,62.450158 15.789064,65.46875 C 18.535804,68.487339 21.477054,71.552452 23.976564,74.4375 C 26.476074,77.322553 28.585894,79.762573 29.601564,82.875 C 29.865144,83.682722 30.019904,84.511238 30.132814,85.34375 C 32.540654,85.431079 34.961934,85.5 37.414064,85.5 C 64.456484,85.5 88.974124,80.107134 106.91406,71.34375 C 108.71383,69.370041 110.60784,67.338911 112.41406,65.34375 C 115.15311,62.318181 117.67459,59.319356 119.22656,56.90625 C 120.58453,54.794782 120.83398,53.333062 120.91406,53.125 C 120.72626,53.000764 119.63107,51.904061 117.28906,51 C 114.61246,49.966787 110.81674,49.046347 106.82031,48.21875 C 102.82387,47.391152 98.631994,46.646129 94.914064,45.78125 C 91.196124,44.91637 88.065184,44.171009 85.414064,42.25 C 82.762934,40.328991 81.081334,37.576182 79.101564,34.3125 C 77.121794,31.048819 75.101384,27.28994 73.070314,23.75 C 71.039234,20.210059 68.979484,16.877947 67.164064,14.65625 C 65.575574,12.712264 64.215854,12.045423 64.039064,11.90625 z" id="path2910" style="opacity:1;fill:url(#linearGradient2823);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 44.96875,33.9375 L 44.1875,35.21875 C 44.177097,35.219516 44.166653,35.219516 44.15625,35.21875 L 42.8125,36.9375 L 41.53125,38.46875 C 41.521947,38.480223 41.511473,38.490697 41.5,38.5 L 40.03125,39.71875 C 40.021947,39.730223 40.011473,39.740697 40,39.75 L 37.125,41.03125 C 37.115697,41.042723 37.105223,41.053197 37.09375,41.0625 L 33.875,41.96875 L 33.375,45.625 L 35.125,45.21875 L 35.125,45.25 L 39.71875,43.75 L 39.75,43.75 L 42.8125,42.0625 L 42.875,42.03125 L 45.78125,39.09375 L 45.8125,39.0625 L 48.96875,34.03125 L 44.96875,33.9375 z" id="path3671" inkscape:original="M 44.84375 33.71875 L 44 35.09375 L 42.65625 36.8125 L 41.375 38.34375 L 39.90625 39.5625 L 37.03125 40.84375 L 33.6875 41.78125 L 33.09375 45.9375 L 35.1875 45.4375 L 39.8125 43.9375 L 42.96875 42.21875 L 45.96875 39.21875 L 49.375 33.8125 L 44.84375 33.71875 z " inkscape:radius="-0.21269609" sodipodi:type="inkscape:offset" style="opacity:0.64356432;fill:url(#radialGradient2825);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3663)" transform="matrix(-1,0,0,1,128.125,0.3)"/>
+ <g id="g3339" transform="translate(-132.29928,0)">
+ <path d="M 196.34375,11.90625 C 196.16996,12.044552 194.80228,12.708229 193.21875,14.65625 C 191.40899,16.88256 189.36581,20.236153 187.34375,23.78125 C 185.32169,27.326347 183.3152,31.075042 181.34375,34.34375 C 179.37229,37.612458 177.70873,40.384748 175.0625,42.3125 C 172.41626,44.24025 169.27823,45.000663 165.5625,45.875 C 161.84677,46.749339 157.65057,47.474737 153.65625,48.3125 C 149.66193,49.150265 145.8302,50.084977 143.15625,51.125 C 140.5806,52.126785 139.6087,53.337785 139.59375,53.25 C 139.62377,53.329884 139.71528,53.638731 139.84375,54.0625 C 140.2595,53.69998 141.25985,52.862595 143.15625,52.125 C 145.8302,51.084977 149.66193,50.150265 153.65625,49.3125 C 157.65057,48.474737 161.84677,47.749339 165.5625,46.875 C 169.27823,46.000663 172.41626,45.24025 175.0625,43.3125 C 177.70873,41.384748 179.37229,38.612458 181.34375,35.34375 C 183.3152,32.075042 185.32169,28.326347 187.34375,24.78125 C 189.36581,21.236153 191.40899,17.88256 193.21875,15.65625 C 194.80228,13.708229 196.16996,13.044552 196.34375,12.90625 C 196.52054,13.045423 197.88026,13.712264 199.46875,15.65625 C 201.28417,17.877947 203.34392,21.210059 205.375,24.75 C 207.40607,28.28994 209.42648,32.048819 211.40625,35.3125 C 213.38602,38.576182 215.06762,41.328991 217.71875,43.25 C 220.36987,45.171009 223.50081,45.91637 227.21875,46.78125 C 230.93668,47.646129 235.12856,48.391152 239.125,49.21875 C 243.12143,50.046347 246.91715,50.966787 249.59375,52 C 251.51448,52.74144 252.56925,53.579608 253,53.9375 C 253.13371,53.522484 253.18802,53.204851 253.21875,53.125 C 253.03095,53.000764 251.93576,51.904061 249.59375,51 C 246.91715,49.966787 243.12143,49.046347 239.125,48.21875 C 235.12856,47.391152 230.93668,46.646129 227.21875,45.78125 C 223.50081,44.91637 220.36987,44.171009 217.71875,42.25 C 215.06762,40.328991 213.38602,37.576182 211.40625,34.3125 C 209.42648,31.048819 207.40607,27.28994 205.375,23.75 C 203.34392,20.210059 201.28417,16.877947 199.46875,14.65625 C 197.88026,12.712264 196.52054,12.045423 196.34375,11.90625 z" id="path3317" style="opacity:1;fill:url(#linearGradient2827);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 246.78125,49.937636 C 247.42469,50.142466 248.03845,50.379526 248.59375,50.593886 C 250.93576,51.497946 252.03095,52.594656 252.21875,52.718886 C 252.13867,52.926956 251.88922,54.388676 250.53125,56.500136 C 248.97928,58.913246 246.4578,61.912066 243.71875,64.937636 C 241.91253,66.932796 240.01852,68.963926 238.21875,70.937636 C 220.27881,79.701026 195.76117,85.093886 168.71875,85.093886 C 166.59433,85.093886 164.49568,85.039506 162.40625,84.968886 C 162.4184,85.051736 162.42625,85.135936 162.4375,85.218886 C 164.84534,85.306216 167.26662,85.375136 169.71875,85.375136 C 196.76117,85.375136 221.27881,79.982276 239.21875,71.218886 C 241.01852,69.245176 242.91253,67.214046 244.71875,65.218886 C 247.4578,62.193316 249.97928,59.194496 251.53125,56.781386 C 252.88922,54.669926 253.13867,53.208206 253.21875,53.000136 C 253.03095,52.875906 251.93576,51.779196 249.59375,50.875136 C 248.75868,50.552786 247.80629,50.238636 246.78125,49.937636 z" id="path3325" sodipodi:nodetypes="cscsscsccscsscsc" style="opacity:1;fill:url(#linearGradient2829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ </g>
+ <path d="M 25.190679,119.77989 C 26.414679,122.74238 27.241162,124.11897 31.289475,123.31542 L 30.638356,120.21008 L 29.079766,120.3986 L 28.261711,118.57341 L 25.190679,119.77989 z" id="path3842" sodipodi:nodetypes="cccccc" style="opacity:0.7715356;fill:url(#linearGradient2831);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3928)"/>
+ <path d="M 26.03125,94.25 C 24.983755,105.1142 22.21942,119.85075 26.78125,123.15625 C 33.15283,127.7731 56.287818,111.82251 64.15625,111.8125 C 72.024682,111.80249 95.202691,127.69555 101.5625,123.0625 C 106.10279,119.75495 103.30815,105.10184 102.21875,94.25 L 98.34375,94.25 C 98.677864,97.707156 99.164649,101.42777 99.5625,105.03125 C 100.01038,109.08783 100.31178,112.97888 100.15625,115.84375 C 100.02016,118.35052 99.34151,119.69095 99.28125,119.90625 C 99.057443,119.89786 97.552762,120.17027 95.125,119.53125 C 92.350417,118.80093 88.723899,117.29504 85,115.625 C 81.276103,113.95497 77.426259,112.10169 73.90625,110.625 C 70.386242,109.1483 67.4302,107.93334 64.15625,107.9375 C 60.882303,107.94167 57.891241,109.1706 54.375,110.65625 C 50.858761,112.1419 47.032137,114.00799 43.3125,115.6875 C 39.592862,117.367 35.960216,118.85638 33.1875,119.59375 C 30.761373,120.23895 29.286908,119.99088 29.0625,120 C 29.004012,119.7864 28.29872,118.4439 28.15625,115.9375 C 27.993428,113.07303 28.281199,109.18271 28.71875,105.125 C 29.110886,101.48845 29.580993,97.733027 29.90625,94.25 L 26.03125,94.25 z" id="path3936" style="opacity:1;fill:url(#radialGradient2833);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 64.125001,11.90625 C 63.951211,12.044552 62.583531,12.708229 61.000001,14.65625 C 59.190241,16.88256 57.147061,20.236153 55.125001,23.78125 C 53.102941,27.326347 51.096451,31.075042 49.125001,34.34375 C 47.153541,37.612458 45.489981,40.384748 42.843751,42.3125 C 40.197511,44.24025 37.059481,45.000663 33.343751,45.875 C 29.628021,46.749339 25.431821,47.474737 21.437501,48.3125 C 17.443181,49.150265 13.611451,50.084977 10.937501,51.125 C 8.3618506,52.126785 7.3899506,53.337785 7.3750006,53.25 C 7.4529506,53.457399 7.6679106,54.954496 9.0312506,57.0625 C 10.589351,59.471649 13.128261,62.450158 15.875001,65.46875 C 18.621741,68.487339 21.562991,71.552452 24.062501,74.4375 C 26.562011,77.322553 28.671831,79.762573 29.687501,82.875 C 30.703171,85.987425 30.441681,89.227198 30.125001,93.03125 C 29.808321,96.835302 29.250051,101.0673 28.812501,105.125 C 28.374951,109.18271 28.087181,113.07303 28.250001,115.9375 C 28.392471,118.4439 29.097761,119.7864 29.156251,120 C 29.380661,119.99088 30.855121,120.23895 33.281251,119.59375 C 36.053961,118.85638 39.686611,117.367 43.406251,115.6875 C 47.125881,114.00799 50.952511,112.1419 54.468751,110.65625 C 57.984991,109.1706 60.976051,107.94167 64.250001,107.9375 C 67.523951,107.93334 70.479991,109.1483 74.000001,110.625 C 77.520011,112.10169 81.369851,113.95497 85.093751,115.625 C 88.817651,117.29504 92.444151,118.80093 95.218751,119.53125 C 97.646511,120.17027 99.151181,119.89786 99.375001,119.90625 C 99.435261,119.69095 100.1139,118.35052 100.25,115.84375 C 100.40553,112.97888 100.10412,109.08783 99.656251,105.03125 C 99.208371,100.97466 98.638841,96.740738 98.312501,92.9375 C 97.986141,89.134266 97.710991,85.927495 98.718751,82.8125 C 99.726491,79.697501 101.82033,77.235155 104.3125,74.34375 C 106.80466,71.452346 109.76095,68.369316 112.5,65.34375 C 115.23905,62.318181 117.76053,59.319356 119.3125,56.90625 C 120.67047,54.794782 120.91992,53.333062 121,53.125 C 120.8122,53.000764 119.71701,51.904061 117.375,51 C 114.6984,49.966787 110.90268,49.046347 106.90625,48.21875 C 102.90981,47.391152 98.717931,46.646129 95.000001,45.78125 C 91.282061,44.91637 88.151121,44.171009 85.500001,42.25 C 82.848871,40.328991 81.167271,37.576182 79.187501,34.3125 C 77.207731,31.048819 75.187321,27.28994 73.156251,23.75 C 71.125171,20.210059 69.065421,16.877947 67.250001,14.65625 C 65.661511,12.712264 64.301791,12.045423 64.125001,11.90625 z" id="path3375" sodipodi:nodetypes="cssssssssssssssscssssssscssssssscsssssssc" style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2835);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3391)"/>
+ <g id="g3612" transform="matrix(-1,0,0,1,128.17175,0)">
+ <path d="M 122.34464,49.937924 L 118.89749,52.943128 L 119.6046,54.97606 L 123.93563,55.771555 C 124.26838,53.827011 124.96313,51.882468 122.34464,49.937924 z" id="path3614" sodipodi:nodetypes="ccccc" style="opacity:0.60891089;fill:url(#radialGradient2837);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3564)"/>
+ <path d="M 122.34464,49.937924 L 118.89749,52.943128 L 119.6046,54.97606 L 123.93563,55.771555 C 124.26838,53.827011 124.96313,51.882468 122.34464,49.937924 z" id="path3616" sodipodi:nodetypes="ccccc" style="opacity:0.60891089;fill:url(#radialGradient2839);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3604)"/>
+ </g>
+ <g id="g3608">
+ <path d="M 122.34464,49.937924 L 118.89749,52.943128 L 119.6046,54.97606 L 123.93563,55.771555 C 124.26838,53.827011 124.96313,51.882468 122.34464,49.937924 z" id="path3374" sodipodi:nodetypes="ccccc" style="opacity:0.60891089;fill:url(#radialGradient2841);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3564)"/>
+ <path d="M 122.34464,49.937924 L 118.89749,52.943128 L 119.6046,54.97606 L 123.93563,55.771555 C 124.26838,53.827011 124.96313,51.882468 122.34464,49.937924 z" id="path3568" sodipodi:nodetypes="ccccc" style="opacity:0.60891089;fill:url(#radialGradient2843);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3604)"/>
+ </g>
+ <path d="M 64.125001,11.90625 C 63.951211,12.044552 62.583531,12.708229 61.000001,14.65625 C 59.190241,16.88256 57.147061,20.236153 55.125001,23.78125 C 53.102941,27.326347 51.096451,31.075042 49.125001,34.34375 C 47.153541,37.612458 45.489981,40.384748 42.843751,42.3125 C 40.197511,44.24025 37.059481,45.000663 33.343751,45.875 C 29.628021,46.749339 25.431821,47.474737 21.437501,48.3125 C 17.443181,49.150265 13.611451,50.084977 10.937501,51.125 C 8.3618506,52.126785 7.3899506,53.337785 7.3750006,53.25 C 7.4529506,53.457399 7.6679106,54.954496 9.0312506,57.0625 C 10.589351,59.471649 13.128261,62.450158 15.875001,65.46875 C 18.621741,68.487339 21.562991,71.552452 24.062501,74.4375 C 26.562011,77.322553 28.671831,79.762573 29.687501,82.875 C 30.703171,85.987425 30.441681,89.227198 30.125001,93.03125 C 29.808321,96.835302 29.250051,101.0673 28.812501,105.125 C 28.374951,109.18271 28.087181,113.07303 28.250001,115.9375 C 28.392471,118.4439 29.097761,119.7864 29.156251,120 C 29.380661,119.99088 30.855121,120.23895 33.281251,119.59375 C 36.053961,118.85638 39.686611,117.367 43.406251,115.6875 C 47.125881,114.00799 50.952511,112.1419 54.468751,110.65625 C 57.984991,109.1706 60.976051,107.94167 64.250001,107.9375 C 67.523951,107.93334 70.479991,109.1483 74.000001,110.625 C 77.520011,112.10169 81.369851,113.95497 85.093751,115.625 C 88.817651,117.29504 92.444151,118.80093 95.218751,119.53125 C 97.646511,120.17027 99.151181,119.89786 99.375001,119.90625 C 99.435261,119.69095 100.1139,118.35052 100.25,115.84375 C 100.40553,112.97888 100.10412,109.08783 99.656251,105.03125 C 99.208371,100.97466 98.638841,96.740738 98.312501,92.9375 C 97.986141,89.134266 97.710991,85.927495 98.718751,82.8125 C 99.726491,79.697501 101.82033,77.235155 104.3125,74.34375 C 106.80466,71.452346 109.76095,68.369316 112.5,65.34375 C 115.23905,62.318181 117.76053,59.319356 119.3125,56.90625 C 120.67047,54.794782 120.91992,53.333062 121,53.125 C 120.8122,53.000764 119.71701,51.904061 117.375,51 C 114.6984,49.966787 110.90268,49.046347 106.90625,48.21875 C 102.90981,47.391152 98.717931,46.646129 95.000001,45.78125 C 91.282061,44.91637 88.151121,44.171009 85.500001,42.25 C 82.848871,40.328991 81.167271,37.576182 79.187501,34.3125 C 77.207731,31.048819 75.187321,27.28994 73.156251,23.75 C 71.125171,20.210059 69.065421,16.877947 67.250001,14.65625 C 65.661511,12.712264 64.301791,12.045423 64.125001,11.90625 z" id="path3395" sodipodi:nodetypes="cssssssssssssssscssssssscssssssscsssssssc" style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2845);stroke-width:0.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3401)"/>
+ <path d="M 25.190679,119.77989 C 26.414679,122.74238 27.241162,124.11897 31.289475,123.31542 L 30.638356,120.21008 L 29.079766,120.3986 L 28.261711,118.57341 L 25.190679,119.77989 z" id="path3932" sodipodi:nodetypes="cccccc" style="opacity:0.7715356;fill:url(#linearGradient2847);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3928)" transform="matrix(-1,0,0,1,128.10515,0)"/>
+ <path d="M 26.03125,94.25 C 24.983755,105.1142 22.21942,119.85075 26.78125,123.15625 C 33.15283,127.7731 56.287818,110.82251 64.15625,110.8125 C 72.024682,110.80249 95.202691,127.69555 101.5625,123.0625 C 106.10279,119.75495 103.30815,105.10184 102.21875,94.25 L 98.34375,94.25 C 98.677864,97.707156 99.164649,101.42777 99.5625,105.03125 C 100.01038,109.08783 100.31178,112.97888 100.15625,115.84375 C 100.02016,118.35052 99.34151,119.69095 99.28125,119.90625 C 99.057443,119.89786 97.552762,120.17027 95.125,119.53125 C 92.350417,118.80093 88.723899,117.29504 85,115.625 C 81.276103,113.95497 77.426259,112.10169 73.90625,110.625 C 70.386242,109.1483 67.4302,107.93334 64.15625,107.9375 C 60.882303,107.94167 57.891241,109.1706 54.375,110.65625 C 50.858761,112.1419 47.032137,114.00799 43.3125,115.6875 C 39.592862,117.367 35.960216,118.85638 33.1875,119.59375 C 30.761373,120.23895 29.286908,119.99088 29.0625,120 C 29.004012,119.7864 28.29872,118.4439 28.15625,115.9375 C 27.993428,113.07303 28.281199,109.18271 28.71875,105.125 C 29.110886,101.48845 29.580993,97.733027 29.90625,94.25 L 26.03125,94.25 z" id="path3265" sodipodi:nodetypes="csssccsscssssssscsscc" style="opacity:1;fill:url(#radialGradient2849);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3285)"/>
+ <path d="M 3.96875,51.25 C 3.8072482,51.513327 3.653679,51.78066 3.5625,52.0625 C 1.4104994,58.714465 18.791507,72.91663 24.40625,81.25 C 25.066744,82.219734 25.881994,83.560003 26.151902,85.050699 C 27.656842,84.748586 29.576041,82.642021 28.90625,81.25 C 27.768648,78.87567 26.012193,76.796136 23.96875,74.4375 C 21.469238,71.552452 18.527988,68.487339 15.78125,65.46875 C 13.034512,62.450158 10.495601,59.471649 8.9375,57.0625 C 7.574162,54.954496 7.359205,53.457399 7.28125,53.25 C 7.2955626,53.33402 8.1966214,52.223338 10.53125,51.25 L 3.96875,51.25 z M 117.875,51.25 C 119.79897,52.094675 120.73559,53.012104 120.90625,53.125 C 120.82618,53.333062 120.57672,54.794782 119.21875,56.90625 C 117.66679,59.319356 115.1453,62.318181 112.40625,65.34375 C 109.66721,68.369316 106.71091,71.452346 104.21875,74.34375 C 102.16137,76.730721 100.3775,78.839259 99.25,81.25 C 98.389358,83.690958 101.19084,84.749904 102.08854,84.829728 C 102.41731,83.373922 103.07445,82.301223 103.75,81.25 C 109.28341,72.913848 126.77026,58.575986 124.59375,51.90625 C 124.52034,51.681299 124.39982,51.462951 124.28125,51.25 L 117.875,51.25 z" id="rect3289" sodipodi:nodetypes="cscccssssccccssscccscc" style="opacity:0.84158415;fill:url(#radialGradient2851);fill-opacity:1;stroke:none;stroke-width:6.1420002;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter3362)"/>
+ </g>
+ <g id="g3701">
+ <path d="M 64.03125,8.2 C 56.162818,8.2100117 46.828561,34.754451 40.46875,39.3875 C 34.10894,44.020548 5.9844574,44.776082 3.5625,52.2625 C 1.1405426,59.748917 23.465249,76.813524 25.90625,84.29375 C 28.347251,91.77398 20.40967,118.7394 26.78125,123.35625 C 33.15283,127.9731 56.287818,112.02251 64.15625,112.0125 C 72.024682,112.00249 95.202691,127.89555 101.5625,123.2625 C 107.92231,118.62945 99.890544,91.686414 102.3125,84.2 C 104.73446,76.713583 127.03475,59.58648 124.59375,52.10625 C 122.15275,44.626021 94.027829,43.941849 87.65625,39.325 C 81.28467,34.708152 71.899685,8.1899879 64.03125,8.2 z M 64.03125,12.10625 C 64.208046,12.245423 65.56776,12.912264 67.15625,14.85625 C 68.97167,17.077947 71.031426,20.410059 73.0625,23.95 C 75.093573,27.48994 77.113982,31.248819 79.09375,34.5125 C 81.073519,37.776182 82.75512,40.528991 85.40625,42.45 C 88.057376,44.371009 91.18831,45.11637 94.90625,45.98125 C 98.624192,46.846129 102.81606,47.591152 106.8125,48.41875 C 110.80894,49.246347 114.60465,50.166787 117.28125,51.2 C 119.62327,52.104061 120.71845,53.200764 120.90625,53.325 C 120.82618,53.533062 120.57672,54.994782 119.21875,57.10625 C 117.66679,59.519356 115.1453,62.518181 112.40625,65.54375 C 109.66721,68.569316 106.71091,71.652346 104.21875,74.54375 C 101.72659,77.435155 99.632744,79.897501 98.625,83.0125 C 97.617256,86.127495 97.892393,89.334266 98.21875,93.1375 C 98.545107,96.940738 99.114622,101.17466 99.5625,105.23125 C 100.01038,109.28783 100.31178,113.17888 100.15625,116.04375 C 100.02016,118.55052 99.34151,119.89095 99.28125,120.10625 C 99.057443,120.09786 97.552762,120.37027 95.125,119.73125 C 92.350417,119.00093 88.723899,117.49504 85,115.825 C 81.276103,114.15497 77.426259,112.30169 73.90625,110.825 C 70.386242,109.3483 67.4302,108.13334 64.15625,108.1375 C 60.882303,108.14167 57.891241,109.3706 54.375,110.85625 C 50.858761,112.3419 47.032137,114.20799 43.3125,115.8875 C 39.592862,117.567 35.960216,119.05638 33.1875,119.79375 C 30.761373,120.43895 29.286908,120.19088 29.0625,120.2 C 29.004012,119.9864 28.29872,118.6439 28.15625,116.1375 C 27.993428,113.27303 28.281199,109.38271 28.71875,105.325 C 29.156299,101.2673 29.714573,97.035302 30.03125,93.23125 C 30.347928,89.427198 30.609418,86.187425 29.59375,83.075 C 28.578082,79.962573 26.468263,77.522553 23.96875,74.6375 C 21.469238,71.752452 18.527988,68.687339 15.78125,65.66875 C 13.034512,62.650158 10.495601,59.671649 8.9375,57.2625 C 7.5741618,55.154496 7.3592053,53.657399 7.28125,53.45 C 7.2962039,53.537785 8.2681026,52.326785 10.84375,51.325 C 13.517705,50.284977 17.34943,49.350265 21.34375,48.5125 C 25.33807,47.674737 29.534272,46.949339 33.25,46.075 C 36.96573,45.200663 40.103767,44.44025 42.75,42.5125 C 45.396234,40.584748 47.059794,37.812458 49.03125,34.54375 C 51.002705,31.275042 53.009191,27.526347 55.03125,23.98125 C 57.053308,20.436153 59.096493,17.08256 60.90625,14.85625 C 62.489787,12.908229 63.857465,12.244552 64.03125,12.10625 z" id="path3703" style="opacity:1;fill:url(#radialGradient3263);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3259)"/>
+ <path d="M 153.09403,94.713757 C 144.53658,107.09689 92.616372,93.013297 78.414631,98.001518 C 64.21289,102.98974 32.50348,146.4474 18.082028,142.13539 C 3.6605746,137.82337 1.0106378,84.092245 -8.1220219,72.127031 C -17.254681,60.161818 -68.384124,43.433534 -68.739625,28.385431 C -69.095125,13.337327 -18.812666,-5.7867426 -10.255219,-18.169872 C -1.697772,-30.553002 -1.5880954,-84.349316 12.613645,-89.337536 C 26.815387,-94.325757 60.541592,-52.41396 74.963045,-48.101941 C 89.384498,-43.789923 140.58172,-60.30959 149.71438,-48.344376 C 158.84704,-36.379162 129.40853,8.6478227 129.76403,23.695927 C 130.11953,38.74403 161.65148,82.330628 153.09403,94.713757 z" id="path3705" inkscape:flatsided="false" inkscape:randomized="0" inkscape:rounded="0.20136392" sodipodi:arg1="0.60469864" sodipodi:arg2="1.2330172" sodipodi:cx="52.952892" sodipodi:cy="25.510532" sodipodi:r1="121.72647" sodipodi:r2="76.832565" sodipodi:sides="5" sodipodi:type="star" style="opacity:1;fill:url(#radialGradient2906);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" transform="matrix(0.4934214,0.1726044,-0.1726044,0.4934214,42.377875,49.908537)"/>
+ <path d="M 64.03125,8 C 56.162818,8.0100117 46.828561,34.554451 40.46875,39.1875 C 34.10894,43.820548 5.984457,44.576082 3.5625,52.0625 C 1.140543,59.548917 23.465249,76.613524 25.90625,84.09375 C 28.347251,91.57398 20.40967,118.5394 26.78125,123.15625 C 33.15283,127.7731 56.287818,111.82251 64.15625,111.8125 C 72.024682,111.80249 95.202691,127.69555 101.5625,123.0625 C 107.92231,118.42945 99.890544,91.486414 102.3125,84 C 104.73446,76.513583 127.03475,59.38648 124.59375,51.90625 C 122.15275,44.426021 94.027829,43.741849 87.65625,39.125 C 81.28467,34.508152 71.899685,7.9899879 64.03125,8 z M 64.03125,11.90625 C 64.208046,12.045423 65.56776,12.712264 67.15625,14.65625 C 68.97167,16.877947 71.031426,20.210059 73.0625,23.75 C 75.093573,27.28994 77.113982,31.048819 79.09375,34.3125 C 81.073519,37.576182 82.75512,40.328991 85.40625,42.25 C 88.057376,44.171009 91.18831,44.91637 94.90625,45.78125 C 98.624192,46.646129 102.81606,47.391152 106.8125,48.21875 C 110.80894,49.046347 114.60465,49.966787 117.28125,51 C 119.62327,51.904061 120.71845,53.000764 120.90625,53.125 C 120.82618,53.333062 120.57672,54.794782 119.21875,56.90625 C 117.66679,59.319356 115.1453,62.318181 112.40625,65.34375 C 109.66721,68.369316 106.71091,71.452346 104.21875,74.34375 C 101.72659,77.235155 99.632744,79.697501 98.625,82.8125 C 97.617256,85.927495 97.892393,89.134266 98.21875,92.9375 C 98.545107,96.740738 99.114622,100.97466 99.5625,105.03125 C 100.01038,109.08783 100.31178,112.97888 100.15625,115.84375 C 100.02016,118.35052 99.34151,119.69095 99.28125,119.90625 C 99.057443,119.89786 97.552762,120.17027 95.125,119.53125 C 92.350417,118.80093 88.723899,117.29504 85,115.625 C 81.276103,113.95497 77.426259,112.10169 73.90625,110.625 C 70.386242,109.1483 67.4302,107.93334 64.15625,107.9375 C 60.882303,107.94167 57.891241,109.1706 54.375,110.65625 C 50.858761,112.1419 47.032137,114.00799 43.3125,115.6875 C 39.592862,117.367 35.960216,118.85638 33.1875,119.59375 C 30.761373,120.23895 29.286908,119.99088 29.0625,120 C 29.004012,119.7864 28.29872,118.4439 28.15625,115.9375 C 27.993428,113.07303 28.281199,109.18271 28.71875,105.125 C 29.156299,101.0673 29.714573,96.835302 30.03125,93.03125 C 30.347928,89.227198 30.609418,85.987425 29.59375,82.875 C 28.578082,79.762573 26.468263,77.322553 23.96875,74.4375 C 21.469238,71.552452 18.527988,68.487339 15.78125,65.46875 C 13.034512,62.450158 10.495601,59.471649 8.9375,57.0625 C 7.574162,54.954496 7.359205,53.457399 7.28125,53.25 C 7.296204,53.337785 8.268103,52.126785 10.84375,51.125 C 13.517705,50.084977 17.34943,49.150265 21.34375,48.3125 C 25.33807,47.474737 29.534272,46.749339 33.25,45.875 C 36.96573,45.000663 40.103767,44.24025 42.75,42.3125 C 45.396234,40.384748 47.059794,37.612458 49.03125,34.34375 C 51.002705,31.075042 53.009191,27.326347 55.03125,23.78125 C 57.053308,20.236153 59.096493,16.88256 60.90625,14.65625 C 62.489787,12.708229 63.857465,12.044552 64.03125,11.90625 z" id="path3707" style="opacity:1;fill:url(#linearGradient2475);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 60.98796,9.471226 C 62.846491,8.2143022 64.889907,8.0204702 67.219338,9.471226 L 64.037358,15.614216 L 60.98796,9.471226 z" id="path3709" sodipodi:nodetypes="cccc" style="fill:url(#linearGradient3800);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3838)"/>
+ <path d="M 64.039064,11.90625 C 63.865274,12.044552 62.497594,12.708229 60.914064,14.65625 C 59.104304,16.88256 57.061124,20.236153 55.039064,23.78125 C 53.017004,27.326347 51.010514,31.075042 49.039064,34.34375 C 47.067604,37.612458 45.404044,40.384748 42.757814,42.3125 C 40.111574,44.24025 36.973544,45.000663 33.257814,45.875 C 29.542084,46.749339 25.345884,47.474737 21.351564,48.3125 C 17.357244,49.150265 13.525514,50.084977 10.851564,51.125 C 8.2759131,52.126785 7.3040131,53.337785 7.2890631,53.25 C 7.3670131,53.457399 7.5819731,54.954496 8.9453131,57.0625 C 10.503414,59.471649 13.042324,62.450158 15.789064,65.46875 C 18.535804,68.487339 21.477054,71.552452 23.976564,74.4375 C 26.476074,77.322553 28.585894,79.762573 29.601564,82.875 C 29.865144,83.682722 30.019904,84.511238 30.132814,85.34375 C 32.540654,85.431079 34.961934,85.5 37.414064,85.5 C 64.456484,85.5 88.974124,80.107134 106.91406,71.34375 C 108.71383,69.370041 110.60784,67.338911 112.41406,65.34375 C 115.15311,62.318181 117.67459,59.319356 119.22656,56.90625 C 120.58453,54.794782 120.83398,53.333062 120.91406,53.125 C 120.72626,53.000764 119.63107,51.904061 117.28906,51 C 114.61246,49.966787 110.81674,49.046347 106.82031,48.21875 C 102.82387,47.391152 98.631994,46.646129 94.914064,45.78125 C 91.196124,44.91637 88.065184,44.171009 85.414064,42.25 C 82.762934,40.328991 81.081334,37.576182 79.101564,34.3125 C 77.121794,31.048819 75.101384,27.28994 73.070314,23.75 C 71.039234,20.210059 68.979484,16.877947 67.164064,14.65625 C 65.575574,12.712264 64.215854,12.045423 64.039064,11.90625 z" id="path3711" style="opacity:1;fill:url(#radialGradient3372);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 44.96875,33.9375 L 44.1875,35.21875 C 44.177097,35.219516 44.166653,35.219516 44.15625,35.21875 L 42.8125,36.9375 L 41.53125,38.46875 C 41.521947,38.480223 41.511473,38.490697 41.5,38.5 L 40.03125,39.71875 C 40.021947,39.730223 40.011473,39.740697 40,39.75 L 37.125,41.03125 C 37.115697,41.042723 37.105223,41.053197 37.09375,41.0625 L 33.875,41.96875 L 33.375,45.625 L 35.125,45.21875 L 35.125,45.25 L 39.71875,43.75 L 39.75,43.75 L 42.8125,42.0625 L 42.875,42.03125 L 45.78125,39.09375 L 45.8125,39.0625 L 48.96875,34.03125 L 44.96875,33.9375 z" id="path3713" inkscape:original="M 44.84375 33.71875 L 44 35.09375 L 42.65625 36.8125 L 41.375 38.34375 L 39.90625 39.5625 L 37.03125 40.84375 L 33.6875 41.78125 L 33.09375 45.9375 L 35.1875 45.4375 L 39.8125 43.9375 L 42.96875 42.21875 L 45.96875 39.21875 L 49.375 33.8125 L 44.84375 33.71875 z " inkscape:radius="-0.21269609" sodipodi:type="inkscape:offset" style="opacity:0.64356432;fill:url(#radialGradient3669);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3663)" transform="translate(0,0.3)"/>
+ <path d="M 64.039064,11.90625 C 63.865274,12.044552 62.497594,12.708229 60.914064,14.65625 C 59.104304,16.88256 57.061124,20.236153 55.039064,23.78125 C 53.017004,27.326347 51.010514,31.075042 49.039064,34.34375 C 47.067604,37.612458 45.404044,40.384748 42.757814,42.3125 C 40.111574,44.24025 36.973544,45.000663 33.257814,45.875 C 29.542084,46.749339 25.345884,47.474737 21.351564,48.3125 C 17.357244,49.150265 13.525514,50.084977 10.851564,51.125 C 8.2759131,52.126785 7.3040131,53.337785 7.2890631,53.25 C 7.3670131,53.457399 7.5819731,54.954496 8.9453131,57.0625 C 10.503414,59.471649 13.042324,62.450158 15.789064,65.46875 C 18.535804,68.487339 21.477054,71.552452 23.976564,74.4375 C 26.476074,77.322553 28.585894,79.762573 29.601564,82.875 C 29.865144,83.682722 30.019904,84.511238 30.132814,85.34375 C 32.540654,85.431079 34.961934,85.5 37.414064,85.5 C 64.456484,85.5 88.974124,80.107134 106.91406,71.34375 C 108.71383,69.370041 110.60784,67.338911 112.41406,65.34375 C 115.15311,62.318181 117.67459,59.319356 119.22656,56.90625 C 120.58453,54.794782 120.83398,53.333062 120.91406,53.125 C 120.72626,53.000764 119.63107,51.904061 117.28906,51 C 114.61246,49.966787 110.81674,49.046347 106.82031,48.21875 C 102.82387,47.391152 98.631994,46.646129 94.914064,45.78125 C 91.196124,44.91637 88.065184,44.171009 85.414064,42.25 C 82.762934,40.328991 81.081334,37.576182 79.101564,34.3125 C 77.121794,31.048819 75.101384,27.28994 73.070314,23.75 C 71.039234,20.210059 68.979484,16.877947 67.164064,14.65625 C 65.575574,12.712264 64.215854,12.045423 64.039064,11.90625 z" id="path3715" style="opacity:1;fill:url(#linearGradient3315);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 44.96875,33.9375 L 44.1875,35.21875 C 44.177097,35.219516 44.166653,35.219516 44.15625,35.21875 L 42.8125,36.9375 L 41.53125,38.46875 C 41.521947,38.480223 41.511473,38.490697 41.5,38.5 L 40.03125,39.71875 C 40.021947,39.730223 40.011473,39.740697 40,39.75 L 37.125,41.03125 C 37.115697,41.042723 37.105223,41.053197 37.09375,41.0625 L 33.875,41.96875 L 33.375,45.625 L 35.125,45.21875 L 35.125,45.25 L 39.71875,43.75 L 39.75,43.75 L 42.8125,42.0625 L 42.875,42.03125 L 45.78125,39.09375 L 45.8125,39.0625 L 48.96875,34.03125 L 44.96875,33.9375 z" id="path3717" inkscape:original="M 44.84375 33.71875 L 44 35.09375 L 42.65625 36.8125 L 41.375 38.34375 L 39.90625 39.5625 L 37.03125 40.84375 L 33.6875 41.78125 L 33.09375 45.9375 L 35.1875 45.4375 L 39.8125 43.9375 L 42.96875 42.21875 L 45.96875 39.21875 L 49.375 33.8125 L 44.84375 33.71875 z " inkscape:radius="-0.21269609" sodipodi:type="inkscape:offset" style="opacity:0.64356432;fill:url(#radialGradient3673);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3663)" transform="matrix(-1,0,0,1,128.125,0.3)"/>
+ <g id="g3719" transform="translate(-132.29928,0)">
+ <path d="M 196.34375,11.90625 C 196.16996,12.044552 194.80228,12.708229 193.21875,14.65625 C 191.40899,16.88256 189.36581,20.236153 187.34375,23.78125 C 185.32169,27.326347 183.3152,31.075042 181.34375,34.34375 C 179.37229,37.612458 177.70873,40.384748 175.0625,42.3125 C 172.41626,44.24025 169.27823,45.000663 165.5625,45.875 C 161.84677,46.749339 157.65057,47.474737 153.65625,48.3125 C 149.66193,49.150265 145.8302,50.084977 143.15625,51.125 C 140.5806,52.126785 139.6087,53.337785 139.59375,53.25 C 139.62377,53.329884 139.71528,53.638731 139.84375,54.0625 C 140.2595,53.69998 141.25985,52.862595 143.15625,52.125 C 145.8302,51.084977 149.66193,50.150265 153.65625,49.3125 C 157.65057,48.474737 161.84677,47.749339 165.5625,46.875 C 169.27823,46.000663 172.41626,45.24025 175.0625,43.3125 C 177.70873,41.384748 179.37229,38.612458 181.34375,35.34375 C 183.3152,32.075042 185.32169,28.326347 187.34375,24.78125 C 189.36581,21.236153 191.40899,17.88256 193.21875,15.65625 C 194.80228,13.708229 196.16996,13.044552 196.34375,12.90625 C 196.52054,13.045423 197.88026,13.712264 199.46875,15.65625 C 201.28417,17.877947 203.34392,21.210059 205.375,24.75 C 207.40607,28.28994 209.42648,32.048819 211.40625,35.3125 C 213.38602,38.576182 215.06762,41.328991 217.71875,43.25 C 220.36987,45.171009 223.50081,45.91637 227.21875,46.78125 C 230.93668,47.646129 235.12856,48.391152 239.125,49.21875 C 243.12143,50.046347 246.91715,50.966787 249.59375,52 C 251.51448,52.74144 252.56925,53.579608 253,53.9375 C 253.13371,53.522484 253.18802,53.204851 253.21875,53.125 C 253.03095,53.000764 251.93576,51.904061 249.59375,51 C 246.91715,49.966787 243.12143,49.046347 239.125,48.21875 C 235.12856,47.391152 230.93668,46.646129 227.21875,45.78125 C 223.50081,44.91637 220.36987,44.171009 217.71875,42.25 C 215.06762,40.328991 213.38602,37.576182 211.40625,34.3125 C 209.42648,31.048819 207.40607,27.28994 205.375,23.75 C 203.34392,20.210059 201.28417,16.877947 199.46875,14.65625 C 197.88026,12.712264 196.52054,12.045423 196.34375,11.90625 z" id="path3721" style="opacity:1;fill:url(#linearGradient3369);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 246.78125,49.937636 C 247.42469,50.142466 248.03845,50.379526 248.59375,50.593886 C 250.93576,51.497946 252.03095,52.594656 252.21875,52.718886 C 252.13867,52.926956 251.88922,54.388676 250.53125,56.500136 C 248.97928,58.913246 246.4578,61.912066 243.71875,64.937636 C 241.91253,66.932796 240.01852,68.963926 238.21875,70.937636 C 220.27881,79.701026 195.76117,85.093886 168.71875,85.093886 C 166.59433,85.093886 164.49568,85.039506 162.40625,84.968886 C 162.4184,85.051736 162.42625,85.135936 162.4375,85.218886 C 164.84534,85.306216 167.26662,85.375136 169.71875,85.375136 C 196.76117,85.375136 221.27881,79.982276 239.21875,71.218886 C 241.01852,69.245176 242.91253,67.214046 244.71875,65.218886 C 247.4578,62.193316 249.97928,59.194496 251.53125,56.781386 C 252.88922,54.669926 253.13867,53.208206 253.21875,53.000136 C 253.03095,52.875906 251.93576,51.779196 249.59375,50.875136 C 248.75868,50.552786 247.80629,50.238636 246.78125,49.937636 z" id="path3723" sodipodi:nodetypes="cscsscsccscsscsc" style="opacity:1;fill:url(#linearGradient3345);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ </g>
+ <path d="M 25.190679,119.77989 C 26.414679,122.74238 27.241162,124.11897 31.289475,123.31542 L 30.638356,120.21008 L 29.079766,120.3986 L 28.261711,118.57341 L 25.190679,119.77989 z" id="path3725" sodipodi:nodetypes="cccccc" style="opacity:0.7715356;fill:url(#linearGradient2853);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3928)"/>
+ <path d="M 26.03125,94.25 C 24.983755,105.1142 22.21942,119.85075 26.78125,123.15625 C 33.15283,127.7731 56.287818,111.82251 64.15625,111.8125 C 72.024682,111.80249 95.202691,127.69555 101.5625,123.0625 C 106.10279,119.75495 103.30815,105.10184 102.21875,94.25 L 98.34375,94.25 C 98.677864,97.707156 99.164649,101.42777 99.5625,105.03125 C 100.01038,109.08783 100.31178,112.97888 100.15625,115.84375 C 100.02016,118.35052 99.34151,119.69095 99.28125,119.90625 C 99.057443,119.89786 97.552762,120.17027 95.125,119.53125 C 92.350417,118.80093 88.723899,117.29504 85,115.625 C 81.276103,113.95497 77.426259,112.10169 73.90625,110.625 C 70.386242,109.1483 67.4302,107.93334 64.15625,107.9375 C 60.882303,107.94167 57.891241,109.1706 54.375,110.65625 C 50.858761,112.1419 47.032137,114.00799 43.3125,115.6875 C 39.592862,117.367 35.960216,118.85638 33.1875,119.59375 C 30.761373,120.23895 29.286908,119.99088 29.0625,120 C 29.004012,119.7864 28.29872,118.4439 28.15625,115.9375 C 27.993428,113.07303 28.281199,109.18271 28.71875,105.125 C 29.110886,101.48845 29.580993,97.733027 29.90625,94.25 L 26.03125,94.25 z" id="path3727" style="opacity:1;fill:url(#radialGradient3956);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"/>
+ <path d="M 64.125001,11.90625 C 63.951211,12.044552 62.583531,12.708229 61.000001,14.65625 C 59.190241,16.88256 57.147061,20.236153 55.125001,23.78125 C 53.102941,27.326347 51.096451,31.075042 49.125001,34.34375 C 47.153541,37.612458 45.489981,40.384748 42.843751,42.3125 C 40.197511,44.24025 37.059481,45.000663 33.343751,45.875 C 29.628021,46.749339 25.431821,47.474737 21.437501,48.3125 C 17.443181,49.150265 13.611451,50.084977 10.937501,51.125 C 8.3618506,52.126785 7.3899506,53.337785 7.3750006,53.25 C 7.4529506,53.457399 7.6679106,54.954496 9.0312506,57.0625 C 10.589351,59.471649 13.128261,62.450158 15.875001,65.46875 C 18.621741,68.487339 21.562991,71.552452 24.062501,74.4375 C 26.562011,77.322553 28.671831,79.762573 29.687501,82.875 C 30.703171,85.987425 30.441681,89.227198 30.125001,93.03125 C 29.808321,96.835302 29.250051,101.0673 28.812501,105.125 C 28.374951,109.18271 28.087181,113.07303 28.250001,115.9375 C 28.392471,118.4439 29.097761,119.7864 29.156251,120 C 29.380661,119.99088 30.855121,120.23895 33.281251,119.59375 C 36.053961,118.85638 39.686611,117.367 43.406251,115.6875 C 47.125881,114.00799 50.952511,112.1419 54.468751,110.65625 C 57.984991,109.1706 60.976051,107.94167 64.250001,107.9375 C 67.523951,107.93334 70.479991,109.1483 74.000001,110.625 C 77.520011,112.10169 81.369851,113.95497 85.093751,115.625 C 88.817651,117.29504 92.444151,118.80093 95.218751,119.53125 C 97.646511,120.17027 99.151181,119.89786 99.375001,119.90625 C 99.435261,119.69095 100.1139,118.35052 100.25,115.84375 C 100.40553,112.97888 100.10412,109.08783 99.656251,105.03125 C 99.208371,100.97466 98.638841,96.740738 98.312501,92.9375 C 97.986141,89.134266 97.710991,85.927495 98.718751,82.8125 C 99.726491,79.697501 101.82033,77.235155 104.3125,74.34375 C 106.80466,71.452346 109.76095,68.369316 112.5,65.34375 C 115.23905,62.318181 117.76053,59.319356 119.3125,56.90625 C 120.67047,54.794782 120.91992,53.333062 121,53.125 C 120.8122,53.000764 119.71701,51.904061 117.375,51 C 114.6984,49.966787 110.90268,49.046347 106.90625,48.21875 C 102.90981,47.391152 98.717931,46.646129 95.000001,45.78125 C 91.282061,44.91637 88.151121,44.171009 85.500001,42.25 C 82.848871,40.328991 81.167271,37.576182 79.187501,34.3125 C 77.207731,31.048819 75.187321,27.28994 73.156251,23.75 C 71.125171,20.210059 69.065421,16.877947 67.250001,14.65625 C 65.661511,12.712264 64.301791,12.045423 64.125001,11.90625 z" id="path3729" sodipodi:nodetypes="cssssssssssssssscssssssscssssssscsssssssc" style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2855);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3391)"/>
+ <g id="g3731" transform="matrix(-1,0,0,1,128.17175,0)">
+ <path d="M 122.34464,49.937924 L 118.89749,52.943128 L 119.6046,54.97606 L 123.93563,55.771555 C 124.26838,53.827011 124.96313,51.882468 122.34464,49.937924 z" id="path3733" sodipodi:nodetypes="ccccc" style="opacity:0.60891089;fill:url(#radialGradient3618);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3564)"/>
+ <path d="M 122.34464,49.937924 L 118.89749,52.943128 L 119.6046,54.97606 L 123.93563,55.771555 C 124.26838,53.827011 124.96313,51.882468 122.34464,49.937924 z" id="path3735" sodipodi:nodetypes="ccccc" style="opacity:0.60891089;fill:url(#radialGradient3620);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3604)"/>
+ </g>
+ <g id="g3737">
+ <path d="M 122.34464,49.937924 L 118.89749,52.943128 L 119.6046,54.97606 L 123.93563,55.771555 C 124.26838,53.827011 124.96313,51.882468 122.34464,49.937924 z" id="path3739" sodipodi:nodetypes="ccccc" style="opacity:0.60891089;fill:url(#radialGradient3382);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3564)"/>
+ <path d="M 122.34464,49.937924 L 118.89749,52.943128 L 119.6046,54.97606 L 123.93563,55.771555 C 124.26838,53.827011 124.96313,51.882468 122.34464,49.937924 z" id="path3741" sodipodi:nodetypes="ccccc" style="opacity:0.60891089;fill:url(#radialGradient3570);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3604)"/>
+ </g>
+ <path d="M 64.125001,11.90625 C 63.951211,12.044552 62.583531,12.708229 61.000001,14.65625 C 59.190241,16.88256 57.147061,20.236153 55.125001,23.78125 C 53.102941,27.326347 51.096451,31.075042 49.125001,34.34375 C 47.153541,37.612458 45.489981,40.384748 42.843751,42.3125 C 40.197511,44.24025 37.059481,45.000663 33.343751,45.875 C 29.628021,46.749339 25.431821,47.474737 21.437501,48.3125 C 17.443181,49.150265 13.611451,50.084977 10.937501,51.125 C 8.3618506,52.126785 7.3899506,53.337785 7.3750006,53.25 C 7.4529506,53.457399 7.6679106,54.954496 9.0312506,57.0625 C 10.589351,59.471649 13.128261,62.450158 15.875001,65.46875 C 18.621741,68.487339 21.562991,71.552452 24.062501,74.4375 C 26.562011,77.322553 28.671831,79.762573 29.687501,82.875 C 30.703171,85.987425 30.441681,89.227198 30.125001,93.03125 C 29.808321,96.835302 29.250051,101.0673 28.812501,105.125 C 28.374951,109.18271 28.087181,113.07303 28.250001,115.9375 C 28.392471,118.4439 29.097761,119.7864 29.156251,120 C 29.380661,119.99088 30.855121,120.23895 33.281251,119.59375 C 36.053961,118.85638 39.686611,117.367 43.406251,115.6875 C 47.125881,114.00799 50.952511,112.1419 54.468751,110.65625 C 57.984991,109.1706 60.976051,107.94167 64.250001,107.9375 C 67.523951,107.93334 70.479991,109.1483 74.000001,110.625 C 77.520011,112.10169 81.369851,113.95497 85.093751,115.625 C 88.817651,117.29504 92.444151,118.80093 95.218751,119.53125 C 97.646511,120.17027 99.151181,119.89786 99.375001,119.90625 C 99.435261,119.69095 100.1139,118.35052 100.25,115.84375 C 100.40553,112.97888 100.10412,109.08783 99.656251,105.03125 C 99.208371,100.97466 98.638841,96.740738 98.312501,92.9375 C 97.986141,89.134266 97.710991,85.927495 98.718751,82.8125 C 99.726491,79.697501 101.82033,77.235155 104.3125,74.34375 C 106.80466,71.452346 109.76095,68.369316 112.5,65.34375 C 115.23905,62.318181 117.76053,59.319356 119.3125,56.90625 C 120.67047,54.794782 120.91992,53.333062 121,53.125 C 120.8122,53.000764 119.71701,51.904061 117.375,51 C 114.6984,49.966787 110.90268,49.046347 106.90625,48.21875 C 102.90981,47.391152 98.717931,46.646129 95.000001,45.78125 C 91.282061,44.91637 88.151121,44.171009 85.500001,42.25 C 82.848871,40.328991 81.167271,37.576182 79.187501,34.3125 C 77.207731,31.048819 75.187321,27.28994 73.156251,23.75 C 71.125171,20.210059 69.065421,16.877947 67.250001,14.65625 C 65.661511,12.712264 64.301791,12.045423 64.125001,11.90625 z" id="path3743" sodipodi:nodetypes="cssssssssssssssscssssssscssssssscsssssssc" style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient3385);stroke-width:0.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3401)"/>
+ <path d="M 25.190679,119.77989 C 26.414679,122.74238 27.241162,124.11897 31.289475,123.31542 L 30.638356,120.21008 L 29.079766,120.3986 L 28.261711,118.57341 L 25.190679,119.77989 z" id="path3745" sodipodi:nodetypes="cccccc" style="opacity:0.7715356;fill:url(#linearGradient3934);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3928)" transform="matrix(-1,0,0,1,128.10515,0)"/>
+ <path d="M 26.03125,94.25 C 24.983755,105.1142 22.21942,119.85075 26.78125,123.15625 C 33.15283,127.7731 56.287818,110.82251 64.15625,110.8125 C 72.024682,110.80249 95.202691,127.69555 101.5625,123.0625 C 106.10279,119.75495 103.30815,105.10184 102.21875,94.25 L 98.34375,94.25 C 98.677864,97.707156 99.164649,101.42777 99.5625,105.03125 C 100.01038,109.08783 100.31178,112.97888 100.15625,115.84375 C 100.02016,118.35052 99.34151,119.69095 99.28125,119.90625 C 99.057443,119.89786 97.552762,120.17027 95.125,119.53125 C 92.350417,118.80093 88.723899,117.29504 85,115.625 C 81.276103,113.95497 77.426259,112.10169 73.90625,110.625 C 70.386242,109.1483 67.4302,107.93334 64.15625,107.9375 C 60.882303,107.94167 57.891241,109.1706 54.375,110.65625 C 50.858761,112.1419 47.032137,114.00799 43.3125,115.6875 C 39.592862,117.367 35.960216,118.85638 33.1875,119.59375 C 30.761373,120.23895 29.286908,119.99088 29.0625,120 C 29.004012,119.7864 28.29872,118.4439 28.15625,115.9375 C 27.993428,113.07303 28.281199,109.18271 28.71875,105.125 C 29.110886,101.48845 29.580993,97.733027 29.90625,94.25 L 26.03125,94.25 z" id="path3747" sodipodi:nodetypes="csssccsscssssssscsscc" style="opacity:1;fill:url(#radialGradient3267);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80851269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1;filter:url(#filter3285)"/>
+ <path d="M 3.96875,51.25 C 3.8072482,51.513327 3.653679,51.78066 3.5625,52.0625 C 1.4104994,58.714465 18.791507,72.91663 24.40625,81.25 C 25.066744,82.219734 25.881994,83.560003 26.151902,85.050699 C 27.656842,84.748586 29.576041,82.642021 28.90625,81.25 C 27.768648,78.87567 26.012193,76.796136 23.96875,74.4375 C 21.469238,71.552452 18.527988,68.487339 15.78125,65.46875 C 13.034512,62.450158 10.495601,59.471649 8.9375,57.0625 C 7.574162,54.954496 7.359205,53.457399 7.28125,53.25 C 7.2955626,53.33402 8.1966214,52.223338 10.53125,51.25 L 3.96875,51.25 z M 117.875,51.25 C 119.79897,52.094675 120.73559,53.012104 120.90625,53.125 C 120.82618,53.333062 120.57672,54.794782 119.21875,56.90625 C 117.66679,59.319356 115.1453,62.318181 112.40625,65.34375 C 109.66721,68.369316 106.71091,71.452346 104.21875,74.34375 C 102.16137,76.730721 100.3775,78.839259 99.25,81.25 C 98.389358,83.690958 101.19084,84.749904 102.08854,84.829728 C 102.41731,83.373922 103.07445,82.301223 103.75,81.25 C 109.28341,72.913848 126.77026,58.575986 124.59375,51.90625 C 124.52034,51.681299 124.39982,51.462951 124.28125,51.25 L 117.875,51.25 z" id="path3749" sodipodi:nodetypes="cscccssssccccssscccscc" style="opacity:0.84158415;fill:url(#radialGradient3300);fill-opacity:1;stroke:none;stroke-width:6.1420002;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter3362)"/>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/checkupdate.svg b/launcher/resources/multimc/scalable/checkupdate.svg
new file mode 100644
index 00000000..fc09cb4c
--- /dev/null
+++ b/launcher/resources/multimc/scalable/checkupdate.svg
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) --><svg height="48px" id="svg7270" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46" sodipodi:docname="system-software-update.svg" sodipodi:version="0.32" width="48px" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <metadata>
+ <rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <cc:Work rdf:about="">
+ <dc:title></dc:title>
+ <dc:description></dc:description>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>unsorted</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <dc:publisher>
+ <cc:Agent rdf:about="http://www.openclipart.org/">
+ <dc:title>Open Clip Art Library, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title></dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title></dc:title>
+ </cc:Agent>
+ </dc:rights>
+ <dc:date></dc:date>
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <cc:license rdf:resource="http://creativecommons.org/licenses/GPL/2.0/"/>
+ <dc:language>en</dc:language>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs id="defs7272">
+ <linearGradient id="linearGradient4873" inkscape:collect="always">
+ <stop id="stop4875" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop4877" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(1.5436745,0,0,1.5436393,-63.017183,37.419463)" gradientUnits="userSpaceOnUse" id="linearGradient2704" inkscape:collect="always" x1="62.771355" x2="62.579906" xlink:href="#linearGradient4873" y1="-13.185048" y2="7.4599638"/>
+ <linearGradient id="linearGradient3811">
+ <stop id="stop3813" offset="0" style="stop-color:#42770c;stop-opacity:1"/>
+ <stop id="stop3815" offset="1" style="stop-color:#789e2d;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(0.7287372,0,0,0.7287206,15.796514,14.228326)" gradientUnits="userSpaceOnUse" id="linearGradient3927" inkscape:collect="always" x1="15.046636" x2="15.046636" xlink:href="#linearGradient3811" y1="44.787998" y2="3.8851264"/>
+ <linearGradient id="linearGradient5106">
+ <stop id="stop5108" offset="0" style="stop-color:#a7cc5c;stop-opacity:1"/>
+ <stop id="stop5110" offset="1" style="stop-color:#789e2d;stop-opacity:1"/>
+ </linearGradient>
+ <radialGradient cx="62.200348" cy="-8.5060539" fx="62.200348" fy="-8.5060539" gradientTransform="matrix(-5.9025587,-9.7773057e-8,3.8757273e-8,-2.3442794,400.1412,4.1903282)" gradientUnits="userSpaceOnUse" id="radialGradient2707" inkscape:collect="always" r="9.7552834" xlink:href="#linearGradient5106"/>
+ <linearGradient id="linearGradient10691" inkscape:collect="always">
+ <stop id="stop10693" offset="0" style="stop-color:#000000;stop-opacity:1;"/>
+ <stop id="stop10695" offset="1" style="stop-color:#000000;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="6.702713" cy="73.615715" fx="6.702713" fy="73.615715" gradientTransform="scale(1.902215,0.525703)" gradientUnits="userSpaceOnUse" id="radialGradient3042" inkscape:collect="always" r="7.228416" xlink:href="#linearGradient10691"/>
+ <linearGradient id="linearGradient3540" inkscape:collect="always">
+ <stop id="stop3542" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3544" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient7581" inkscape:collect="always" x1="322.26392" x2="329.12961" xlink:href="#linearGradient3540" y1="55.34375" y2="64.329506"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient7579" inkscape:collect="always" x1="325.65164" x2="328.24478" xlink:href="#linearGradient3497" y1="62.475075" y2="61.084908"/>
+ <linearGradient id="linearGradient7088">
+ <stop id="stop7090" offset="0" style="stop-color:#c17d11;stop-opacity:1;"/>
+ <stop id="stop7092" offset="1" style="stop-color:#e9b96e;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(0.1516675,0,0,0.1638445,300.96358,54.165417)" gradientUnits="userSpaceOnUse" id="linearGradient7577" inkscape:collect="always" x1="251.94502" x2="161.94502" xlink:href="#linearGradient7088" y1="18.836056" y2="46.195984"/>
+ <linearGradient gradientTransform="matrix(0.8365572,0,0,1,52.219983,0)" gradientUnits="userSpaceOnUse" id="linearGradient7575" inkscape:collect="always" x1="334.77948" x2="327.34198" xlink:href="#linearGradient3582" y1="69.490768" y2="69.490768"/>
+ <linearGradient id="linearGradient3532" inkscape:collect="always">
+ <stop id="stop3534" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3536" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient7573" inkscape:collect="always" x1="316.42142" x2="304.1875" xlink:href="#linearGradient3532" y1="58.144356" y2="67.203125"/>
+ <linearGradient id="linearGradient3516" inkscape:collect="always">
+ <stop id="stop3518" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3520" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="318.26694" cy="81.264877" fx="318.26694" fy="81.264877" gradientTransform="matrix(11.694835,-3.1336215,0.2338827,0.8728621,-3435.8358,1012.645)" gradientUnits="userSpaceOnUse" id="radialGradient7571" inkscape:collect="always" r="15.375" xlink:href="#linearGradient3516"/>
+ <linearGradient id="linearGradient3497" inkscape:collect="always">
+ <stop id="stop3499" offset="0" style="stop-color:#c17d11;stop-opacity:1;"/>
+ <stop id="stop3501" offset="1" style="stop-color:#c17d11;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient7569" inkscape:collect="always" x1="317" x2="313.625" xlink:href="#linearGradient3497" y1="65.25" y2="62.475075"/>
+ <linearGradient id="linearGradient7000" inkscape:collect="always">
+ <stop id="stop7002" offset="0" style="stop-color:#eeeeec;stop-opacity:1"/>
+ <stop id="stop7005" offset="1" style="stop-color:#e9b96e;stop-opacity:1"/>
+ </linearGradient>
+ <radialGradient cx="93.23716" cy="132.00948" fx="93.23716" fy="132.00948" gradientTransform="matrix(0.194642,-0.362756,0.2778653,0.17262,263.08914,83.463419)" gradientUnits="userSpaceOnUse" id="radialGradient7567" inkscape:collect="always" r="48.719242" xlink:href="#linearGradient7000"/>
+ <linearGradient id="linearGradient3582" inkscape:collect="always">
+ <stop id="stop3584" offset="0" style="stop-color:#000000;stop-opacity:1;"/>
+ <stop id="stop3586" offset="1" style="stop-color:#000000;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(0.914635,0,0,1,27.274119,0)" gradientUnits="userSpaceOnUse" id="linearGradient7565" inkscape:collect="always" x1="314.81003" x2="321.78522" xlink:href="#linearGradient3582" y1="69.553268" y2="69.490768"/>
+ <linearGradient id="linearGradient3757" inkscape:collect="always">
+ <stop id="stop3759" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3761" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="321.375" cy="70.5" fx="321.375" fy="70.5" gradientTransform="matrix(1,0,0,0.4274194,0,40.366935)" gradientUnits="userSpaceOnUse" id="radialGradient7563" inkscape:collect="always" r="15.5" xlink:href="#linearGradient3757"/>
+ <linearGradient id="linearGradient3737">
+ <stop id="stop3739" offset="0" style="stop-color:#faefde;stop-opacity:1"/>
+ <stop id="stop3741" offset="0.68321055" style="stop-color:#e9b96e;stop-opacity:1"/>
+ <stop id="stop3743" offset="1" style="stop-color:#e9b96e;stop-opacity:0"/>
+ </linearGradient>
+ <radialGradient cx="320.7493" cy="70.499977" fx="320.7493" fy="70.499977" gradientTransform="matrix(1.1851374,0,0,0.1612901,-56.76846,59.129051)" gradientUnits="userSpaceOnUse" id="radialGradient7561" inkscape:collect="always" r="15.500023" xlink:href="#linearGradient3737"/>
+ <linearGradient id="linearGradient3712" inkscape:collect="always">
+ <stop id="stop3714" offset="0" style="stop-color:#e9b96e;stop-opacity:1;"/>
+ <stop id="stop3716" offset="1" style="stop-color:#e9b96e;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="321.7785" cy="96.847473" fx="321.7785" fy="96.847473" gradientTransform="matrix(4.6140944,0,0,0.6613562,-1154.7032,27.949324)" gradientUnits="userSpaceOnUse" id="radialGradient7559" inkscape:collect="always" r="15.5" xlink:href="#linearGradient3712"/>
+ <linearGradient id="linearGradient5967">
+ <stop id="stop5969" offset="0" style="stop-color:#c17d11;stop-opacity:1;"/>
+ <stop id="stop5971" offset="1" style="stop-color:#e9b96e;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(0.1549758,0,0,0.1517241,299.35074,56.193103)" gradientUnits="userSpaceOnUse" id="linearGradient7557" inkscape:collect="always" x1="174.01562" x2="30" xlink:href="#linearGradient5967" y1="236" y2="64"/>
+ <linearGradient id="linearGradient6977">
+ <stop id="stop7039" offset="0" style="stop-color:#ebd3ad;stop-opacity:1;"/>
+ <stop id="stop6981" offset="1" style="stop-color:#e9b96e;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(0.159975,0,0,0.1765787,298.70076,53.371862)" gradientUnits="userSpaceOnUse" id="linearGradient7555" inkscape:collect="always" x1="52" x2="52" xlink:href="#linearGradient6977" y1="40.482288" y2="76.67421"/>
+ <linearGradient id="linearGradient3703">
+ <stop id="stop3705" offset="0" style="stop-color:black;stop-opacity:0;"/>
+ <stop id="stop3711" offset="0.5" style="stop-color:black;stop-opacity:1;"/>
+ <stop id="stop3707" offset="1" style="stop-color:black;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(1.179548,0,0,1,-4.219389,0)" gradientUnits="userSpaceOnUse" id="linearGradient7462" inkscape:collect="always" x1="17.554192" x2="17.554192" xlink:href="#linearGradient3703" y1="46.000275" y2="34.999718"/>
+ <radialGradient cx="5" cy="41.5" fx="5" fy="41.5" gradientTransform="matrix(0.99001,0,0,1.1,-14.88523,-86.15)" gradientUnits="userSpaceOnUse" id="radialGradient7460" inkscape:collect="always" r="5" xlink:href="#linearGradient3681"/>
+ <linearGradient id="linearGradient3681" inkscape:collect="always">
+ <stop id="stop3683" offset="0" style="stop-color:black;stop-opacity:1;"/>
+ <stop id="stop3685" offset="1" style="stop-color:black;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="5" cy="41.5" fx="5" fy="41.5" gradientTransform="matrix(0.990017,0,0,1.1,32.1147,-5.15)" gradientUnits="userSpaceOnUse" id="radialGradient7458" inkscape:collect="always" r="5" xlink:href="#linearGradient3681"/>
+ </defs>
+ <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="24" inkscape:cy="24" inkscape:document-units="px" inkscape:grid-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="690" inkscape:window-width="641" inkscape:window-x="0" inkscape:window-y="331" inkscape:zoom="7" pagecolor="#ffffff" showgrid="true"/>
+ <g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1">
+ <g id="g3713" style="opacity:0.4" transform="matrix(1.000001,0,0,1.2727273,-5e-6,-12.545454)">
+ <rect height="11" id="rect1907" style="opacity:1;fill:url(#radialGradient7458);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1" width="4.9352183" x="37.064781" y="35"/>
+ <rect height="11" id="rect3689" style="opacity:1;fill:url(#radialGradient7460);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1" transform="scale(-1,-1)" width="4.9351835" x="-9.9351835" y="-46"/>
+ <rect height="11" id="rect3693" style="opacity:1;fill:url(#linearGradient7462);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1" width="27.129599" x="9.9351835" y="35"/>
+ </g>
+ <g id="g4045" style="display:inline;enable-background:new" transform="translate(-296.00045,-50)">
+ <path d="M 322.875,54.40625 C 322.64926,54.465917 322.49407,54.672835 322.5,54.90625 L 322.59375,61.5 L 318.40625,61.5 L 318.4375,58.9375 C 318.44168,58.796741 318.39241,58.648956 318.28125,58.5625 C 318.17008,58.476044 318.0104,58.461295 317.875,58.5 L 306.5,61.5 C 306.35947,61.54996 306.25368,61.667506 306.21875,61.8125 L 306.15625,62.03125 C 306.05939,62.200213 305.96316,62.375835 305.90625,62.5625 L 303.5625,70.09375 C 303.51406,70.207689 303.5,70.305686 303.5,70.4375 C 303.4981,70.458668 303.5019,70.478889 303.5,70.5 L 303.5,90.84375 C 303.5,91.371002 303.93699,92.499999 304.46875,92.5 L 334.53125,92.5 C 335.06301,92.5 335.5,91.371004 335.5,90.84375 L 335.5,70.5 L 335.4375,70.09375 L 335.40625,70 C 335.40291,69.966972 335.41634,69.938782 335.40625,69.90625 L 332.9375,61.78125 C 332.9115,61.684204 332.85709,61.597145 332.78125,61.53125 L 323.3125,54.5 C 323.18929,54.402174 323.02747,54.367498 322.875,54.40625 z" id="path3678" sodipodi:nodetypes="cccccsccccccsccccccccccccc" style="fill:url(#linearGradient7555);fill-opacity:1;fill-rule:evenodd;stroke:#8f5902;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0.69999992;stroke-opacity:1;display:inline;enable-background:new"/>
+ <rect height="1" id="rect3606" style="opacity:0.18918918;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.50000001;stroke-opacity:1;display:inline;enable-background:new" width="23.340202" x="309" y="66"/>
+ <path d="M 304.92985,70 C 304.41471,70 304,70.406014 304,70.910345 L 304,90.482758 L 304,91.089654 C 304,91.593985 304.41471,91.999999 304.92985,92 L 334.07015,92 C 334.5853,92 335,91.593984 335,91.089654 L 335,70.910345 C 335,70.406014 334.5853,70 334.07015,70 L 304.92985,70 z" id="path3628" sodipodi:nodetypes="cccccccccc" style="fill:url(#linearGradient7557);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0.69999992;stroke-opacity:1;display:inline;enable-background:new"/>
+ <path d="M 304.92985,70 C 304.41471,70 304,70.406014 304,70.910345 L 304,90.482758 L 304,91.089654 C 304,91.593985 304.41471,91.999999 304.92985,92 L 334.07015,92 C 334.5853,92 335,91.593984 335,91.089654 L 335,70.910345 C 335,70.406014 334.5853,70 334.07015,70 L 304.92985,70 z" id="path3702" sodipodi:nodetypes="cccccccccc" style="opacity:0.81531528;fill:url(#radialGradient7559);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0.69999992;stroke-opacity:1;display:inline;enable-background:new"/>
+ <path d="M 304.71875,68 L 304.03125,70.25 C 304.03158,70.260414 304.03158,70.270836 304.03125,70.28125 C 304.00957,70.33225 304,70.341241 304,70.4375 C 304.00033,70.447914 304.00033,70.458336 304,70.46875 C 304.00297,70.435653 303.99952,70.489668 304,70.5 C 304.00048,70.510332 304.0049,70.47686 304,70.53125 L 304,73 L 335,73 L 335,70.5625 L 335,70.5 L 334.96875,70.25 L 334.9375,70.15625 L 334.28125,68 L 304.71875,68 z" id="path3723" style="opacity:0.5;fill:url(#radialGradient7561);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0.69999992;stroke-opacity:1;display:inline;enable-background:new"/>
+ <rect height="1" id="rect3746" style="opacity:0.62330568;fill:url(#radialGradient7563);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.50000001;stroke-opacity:1;display:inline;enable-background:new" width="31" x="304" y="70"/>
+ <path d="M 307.9375,61.90625 C 307.82039,61.90625 307.7549,61.954412 307.65625,62.09375 C 307.55761,62.233088 307.47013,62.458403 307.40625,62.6875 L 305.28125,70.09375 L 323.5,70.09375 L 323.5,61.90625 L 307.9375,61.90625 z" id="path3558" style="opacity:0.36936939;fill:url(#linearGradient7565);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0.69999992;stroke-opacity:1;display:inline;enable-background:new"/>
+ <rect height="1" id="rect3602" style="opacity:0.39639641;fill:#c17d11;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.50000001;stroke-opacity:1" width="23.340202" x="309" y="65"/>
+ <path d="M 317.875,59.09375 L 306.71875,62.03125 L 304.25,69.8125 L 317.75,66.875 L 317.875,59.09375 z" id="path3466" inkscape:original="M 318 58.9375 L 306.625 61.9375 L 304.0625 70 L 317.875 66.96875 L 318 58.9375 z " inkscape:radius="-0.11941363" sodipodi:type="inkscape:offset" style="fill:url(#radialGradient7567);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:new"/>
+ <path d="M 317.875,58.5 L 306.5,61.5 C 306.35947,61.54996 306.25368,61.667506 306.21875,61.8125 L 303.65625,69.875 C 303.61173,70.021945 303.64677,70.181469 303.74877,70.296228 C 303.85078,70.410987 304.0051,70.464488 304.15625,70.4375 L 317.96875,67.40625 C 318.1724,67.360088 318.31583,67.177536 318.3125,66.96875 L 318.4375,58.9375 C 318.44168,58.796741 318.37845,58.662433 318.2673,58.575977 C 318.15614,58.489521 318.0104,58.461295 317.875,58.5 L 317.875,58.5 z" id="path3489" inkscape:original="M 318 58.9375 L 306.625 61.9375 L 304.0625 70 L 317.875 66.96875 L 318 58.9375 z " inkscape:radius="0.44133759" sodipodi:type="inkscape:offset" style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient7569);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:new"/>
+ <path d="M 323.65625,56.125 L 323.71875,61.46875 C 323.73018,61.77333 323.61637,62.069302 323.40382,62.287756 C 323.19127,62.506209 322.89853,62.628083 322.59375,62.625 L 318.40625,62.625 C 318.10702,62.627829 317.81923,62.510212 317.60763,62.298617 C 317.39604,62.087022 317.27842,61.799227 317.28125,61.5 L 317.3125,59.78125 L 307.1875,62.4375 C 307.18765,62.447916 307.18765,62.458334 307.1875,62.46875 C 307.1693,62.511683 307.14843,62.553432 307.125,62.59375 C 307.03546,62.749944 306.98033,62.837024 306.96875,62.875 C 306.9689,62.885416 306.9689,62.895834 306.96875,62.90625 L 304.625,70.4375 C 304.625,70.405959 304.63178,70.465233 304.625,70.5 L 304.625,70.53125 C 304.64058,70.357706 304.64449,70.377244 304.625,70.59375 L 304.625,90.84375 C 304.625,90.799502 304.68196,91.178267 304.78125,91.375 L 334.21875,91.375 C 334.31804,91.178267 334.375,90.799504 334.375,90.84375 L 334.375,70.5 L 334.375,70.4375 L 334.34375,70.34375 C 334.32611,70.272049 334.31564,70.198772 334.3125,70.125 L 331.9375,62.3125 L 323.65625,56.125 z" id="path3512" inkscape:original="M 322.875 54.40625 C 322.64926 54.465917 322.49407 54.672835 322.5 54.90625 L 322.59375 61.5 L 318.40625 61.5 L 318.4375 58.9375 C 318.44168 58.796741 318.39241 58.648956 318.28125 58.5625 C 318.17008 58.476044 318.0104 58.461295 317.875 58.5 L 306.5 61.5 C 306.35947 61.54996 306.25368 61.667506 306.21875 61.8125 L 306.15625 62.03125 C 306.05939 62.200213 305.96316 62.375835 305.90625 62.5625 L 303.5625 70.09375 C 303.51406 70.207689 303.5 70.305686 303.5 70.4375 C 303.4981 70.458668 303.5019 70.478889 303.5 70.5 L 303.5 90.84375 C 303.5 91.371002 303.93699 92.499999 304.46875 92.5 L 334.53125 92.5 C 335.06301 92.5 335.5 91.371004 335.5 90.84375 L 335.5 70.5 L 335.4375 70.09375 L 335.40625 70 C 335.40291 69.966972 335.41634 69.938782 335.40625 69.90625 L 332.9375 61.78125 C 332.9115 61.684204 332.85709 61.597145 332.78125 61.53125 L 323.3125 54.5 C 323.18929 54.402174 323.02747 54.367498 322.875 54.40625 z " inkscape:radius="-1.1144003" sodipodi:type="inkscape:offset" style="opacity:0.36936939;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#radialGradient7571);stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0.69999992;stroke-opacity:1;display:inline;enable-background:new"/>
+ <path d="M 317.5625,59.5 L 306.96875,62.28125 L 304.6875,69.40625 L 317.4375,66.59375 L 317.5625,59.5 z" id="path3524" inkscape:original="M 318 58.9375 L 306.625 61.9375 L 304.0625 70 L 317.875 66.96875 L 318 58.9375 z " inkscape:radius="-0.43543664" sodipodi:type="inkscape:offset" style="opacity:0.53153154;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient7573);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:new"/>
+ <path d="M 323.0625,61.90625 L 323.0625,70.09375 L 333.71875,70.09375 L 331.59375,62.5625 C 331.53244,62.342161 331.46293,62.136199 331.375,62.03125 C 331.28708,61.926301 331.20816,61.90625 331.0625,61.90625 L 323.0625,61.90625 z" id="path3593" style="opacity:0.36936939;fill:url(#linearGradient7575);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0.69999992;stroke-opacity:1;display:inline;enable-background:new"/>
+ <g id="g3562">
+ <path d="M 323,54.90625 L 323.125,63.03125 L 334.9375,70.0625 L 332.46875,61.9375 L 323,54.90625 z" id="path3468" inkscape:original="M 323 54.90625 L 323.125 63.03125 L 334.9375 70.0625 L 332.46875 61.9375 L 323 54.90625 z " inkscape:radius="-0.0019137046" sodipodi:type="inkscape:offset" style="fill:url(#linearGradient7577);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:new"/>
+ <path d="M 322.875,54.40625 C 322.64926,54.465917 322.49407,54.672835 322.5,54.90625 L 322.625,63.03125 C 322.62421,63.211268 322.71951,63.378038 322.875,63.46875 L 334.6875,70.5 C 334.86776,70.596275 335.08831,70.574158 335.24586,70.444006 C 335.40342,70.313855 335.46677,70.101441 335.40625,69.90625 L 332.9375,61.78125 C 332.9115,61.684204 332.85709,61.597145 332.78125,61.53125 L 323.3125,54.5 C 323.18929,54.402174 323.02747,54.367498 322.875,54.40625 z" id="path3491" inkscape:original="M 323 54.90625 L 323.125 63.03125 L 334.9375 70.0625 L 332.46875 61.9375 L 323 54.90625 z " inkscape:radius="0.50389111" sodipodi:type="inkscape:offset" style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient7579);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:new"/>
+ <path d="M 323.46875,55.84375 L 323.59375,62.75 L 334.125,69.03125 L 332.03125,62.21875 L 323.46875,55.84375 z" id="path3526" inkscape:original="M 323 54.90625 L 323.125 63.03125 L 334.9375 70.0625 L 332.46875 61.9375 L 323 54.90625 z " inkscape:radius="-0.47827792" sodipodi:type="inkscape:offset" style="opacity:0.25225226;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient7581);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:new"/>
+ <path d="M 323,54.90625 L 323.125,63.03125 L 334.9375,70.0625 L 324.0625,62.81342 L 324,56.49908 L 332.46875,61.9375 L 323,54.90625 z" id="path3548" sodipodi:nodetypes="ccccccc" style="opacity:0.36936939;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:new"/>
+ </g>
+ <rect height="1" id="rect3604" style="opacity:0.20720723;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.50000001;stroke-opacity:1;display:inline;enable-background:new" width="3" x="319" y="62"/>
+ </g>
+ <path d="M 26.5,38.700001 A 13.75,3.8 0 1 1 -1,38.700001 A 13.75,3.8 0 1 1 26.5,38.700001 z" id="path2858" sodipodi:cx="12.75" sodipodi:cy="38.700001" sodipodi:rx="13.75" sodipodi:ry="3.8" sodipodi:type="arc" style="opacity:0.5;fill:url(#radialGradient3042);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.80000001;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" transform="matrix(1.0181818,0,0,1.0526316,20.018182,3.2631562)"/>
+ <path d="M 47.500174,31.999486 C 47.500174,40.007969 41.007746,46.500175 33.000183,46.500175 C 24.991885,46.500175 18.499825,40.007895 18.499825,31.999486 C 18.499825,23.99137 24.991885,17.499825 33.000183,17.499825 C 41.007746,17.499825 47.500174,23.99137 47.500174,31.999486 L 47.500174,31.999486 z" id="path6495" style="fill:url(#radialGradient2707);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient3927);stroke-width:0.9996503;stroke-miterlimit:4;stroke-dasharray:none"/>
+ <path d="M 46.500204,31.99952 C 46.500204,39.455718 40.455512,45.500205 33.00017,45.500205 C 25.544146,45.500205 19.499795,39.45565 19.499795,31.99952 C 19.499795,24.543666 25.544146,18.499795 33.00017,18.499795 C 40.455512,18.499795 46.500204,24.543666 46.500204,31.99952 L 46.500204,31.99952 z" id="path8655" style="opacity:0.6;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2704);stroke-width:0.99959099;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
+ <path d="M 23.5,32.5 L 33,22.5 L 42.5,32.5 L 36.75,32.5 L 36.75,41.5 L 29.25,41.5 L 29.25,32.5 L 23.5,32.5 z" id="path4236" sodipodi:nodetypes="cccccccc" style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"/>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/custom-commands.svg b/launcher/resources/multimc/scalable/custom-commands.svg
new file mode 100644
index 00000000..b7f1a149
--- /dev/null
+++ b/launcher/resources/multimc/scalable/custom-commands.svg
@@ -0,0 +1,338 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2"
+ height="64"
+ width="64"
+ version="1.1"
+ sodipodi:docname="custom-commands.svg"
+ inkscape:version="0.92.2 5c3e80d, 2017-08-06">
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="3840"
+ inkscape:window-height="2123"
+ id="namedview52"
+ showgrid="false"
+ inkscape:zoom="20.85965"
+ inkscape:cx="28.409224"
+ inkscape:cy="33.675543"
+ inkscape:window-x="1200"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:bbox-nodes="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid858" />
+ </sodipodi:namedview>
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3931">
+ <stop
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:0"
+ id="stop3933" />
+ <stop
+ offset="0.69999987"
+ style="stop-color:#ffffff;stop-opacity:0.10396039"
+ id="stop3939" />
+ <stop
+ offset="1"
+ style="stop-color:#ffffff;stop-opacity:0.14356436"
+ id="stop3935" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3900">
+ <stop
+ offset="0"
+ style="stop-color:#f6f6f6;stop-opacity:1"
+ id="stop3902" />
+ <stop
+ offset="0.75714284"
+ style="stop-color:#494949;stop-opacity:1"
+ id="stop3904" />
+ <stop
+ offset="1"
+ style="stop-color:#2c2c2c;stop-opacity:1"
+ id="stop3906" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3808">
+ <stop
+ offset="0"
+ style="stop-color:#333333;stop-opacity:1"
+ id="stop3810" />
+ <stop
+ offset="1"
+ style="stop-color:#c8c8c8;stop-opacity:1"
+ id="stop3812" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3030">
+ <stop
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1"
+ id="stop3032" />
+ <stop
+ offset="0.75714284"
+ style="stop-color:#333333;stop-opacity:1"
+ id="stop3038" />
+ <stop
+ offset="1"
+ style="stop-color:#4d4d4d;stop-opacity:1"
+ id="stop3034" />
+ </linearGradient>
+ <radialGradient
+ gradientTransform="matrix(1.3519242,1.8838281,-1.5359217,1.1022493,15.935733,948.08165)"
+ gradientUnits="userSpaceOnUse"
+ xlink:href="#linearGradient3030"
+ id="radialGradient3036"
+ fy="14.242621"
+ fx="29.381905"
+ r="16.375"
+ cy="14.242621"
+ cx="29.381905" />
+ <linearGradient
+ gradientTransform="matrix(1.5,0,0,1,-16,4)"
+ gradientUnits="userSpaceOnUse"
+ xlink:href="#linearGradient3808"
+ id="linearGradient3824"
+ y2="1033.8622"
+ x2="34"
+ y1="1033.8622"
+ x1="30" />
+ <linearGradient
+ gradientTransform="matrix(0.82142857,0,0,1.500001,6.7142857,-522.68214)"
+ gradientUnits="userSpaceOnUse"
+ xlink:href="#linearGradient3808"
+ id="linearGradient3834"
+ y2="1039.3622"
+ x2="32"
+ y1="1043.3622"
+ x1="32" />
+ <radialGradient
+ gradientTransform="matrix(6.479993,1.9525666,-10.415476,2.1794781,10657.845,-1282.8793)"
+ gradientUnits="userSpaceOnUse"
+ xlink:href="#linearGradient3900"
+ id="radialGradient3844"
+ fy="1039.813"
+ fx="30.724609"
+ r="3"
+ cy="1039.813"
+ cx="30.724609" />
+ <radialGradient
+ gradientTransform="matrix(2.5191507,2.9862959,-4.0491019,3.333339,4186.8847,-2518.44)"
+ gradientUnits="userSpaceOnUse"
+ xlink:href="#linearGradient3900"
+ id="radialGradient3852"
+ fy="1039.813"
+ fx="30.724609"
+ r="3"
+ cy="1039.813"
+ cx="30.724609" />
+ <radialGradient
+ gradientTransform="matrix(-2.5191507,2.9863064,4.0491022,3.3333507,-4122.8849,-2518.4524)"
+ gradientUnits="userSpaceOnUse"
+ xlink:href="#linearGradient3900"
+ id="radialGradient3857"
+ fy="1039.813"
+ fx="30.724609"
+ r="3"
+ cy="1039.813"
+ cx="30.724609" />
+ <radialGradient
+ gradientTransform="matrix(-0.69414478,2.3073251,-1.6952184,-0.67174747,96.941544,960.82172)"
+ gradientUnits="userSpaceOnUse"
+ xlink:href="#linearGradient3900"
+ id="radialGradient3937"
+ fy="21.976955"
+ fx="31.946348"
+ r="19.25"
+ cy="21.976955"
+ cx="31.946348" />
+ </defs>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ transform="translate(0,-988.36218)">
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient3036);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.25954175;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect2997"
+ y="992.99194"
+ x="5.6297708"
+ ry="0.70710492"
+ rx="0.70710504"
+ height="53.740437"
+ width="53.740459" />
+ </g>
+ <g
+ id="g1021"
+ transform="translate(1.7703716e-7,-0.42472956)">
+ <g
+ style="fill:#008000"
+ transform="matrix(0.08572572,0,0,0.08572572,9.999999,10.424713)"
+ id="g899">
+ <g
+ style="fill:#008000"
+ id="g867">
+ <g
+ style="fill:#008000"
+ id="g865">
+ <path
+ style="fill:#008000"
+ d="m 226.434,249.503 c 0,-6.995 -2.705,-13.403 -7.846,-18.556 L 61.8,74.165 c -5.128,-5.141 -11.554,-7.852 -18.568,-7.852 -7.026,0 -13.452,2.717 -18.556,7.846 l -16.83,16.83 c -5.129,5.135 -7.84,11.549 -7.84,18.538 0,7.026 2.717,13.452 7.846,18.556 L 129.267,249.503 7.84,370.936 C 2.711,376.071 0,382.491 0,389.486 c 0,7.02 2.717,13.439 7.846,18.544 l 16.775,16.774 c 5.116,5.165 11.555,7.895 18.611,7.895 7.044,0 13.47,-2.723 18.556,-7.846 l 156.813,-156.8 c 5.128,-5.14 7.833,-11.549 7.833,-18.55 z"
+ id="path860"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#008000"
+ d="m 498.866,384.951 h -323.02 c -7.203,0 -13.611,2.583 -18.581,7.528 -4.896,4.92 -7.484,11.327 -7.484,18.531 v 21.536 c 0,7.252 2.607,13.672 7.491,18.543 4.915,4.927 11.34,7.528 18.574,7.528 h 323.02 c 7.239,0 13.659,-2.607 18.531,-7.497 4.927,-4.908 7.533,-11.334 7.533,-18.58 v -21.537 c 0,-7.209 -2.589,-13.616 -7.54,-18.592 -4.913,-4.877 -11.321,-7.46 -18.524,-7.46 z"
+ id="path862"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <g
+ style="fill:#008000"
+ id="g869" />
+ <g
+ style="fill:#008000"
+ id="g871" />
+ <g
+ style="fill:#008000"
+ id="g873" />
+ <g
+ style="fill:#008000"
+ id="g875" />
+ <g
+ style="fill:#008000"
+ id="g877" />
+ <g
+ style="fill:#008000"
+ id="g879" />
+ <g
+ style="fill:#008000"
+ id="g881" />
+ <g
+ style="fill:#008000"
+ id="g883" />
+ <g
+ style="fill:#008000"
+ id="g885" />
+ <g
+ style="fill:#008000"
+ id="g887" />
+ <g
+ style="fill:#008000"
+ id="g889" />
+ <g
+ style="fill:#008000"
+ id="g891" />
+ <g
+ style="fill:#008000"
+ id="g893" />
+ <g
+ style="fill:#008000"
+ id="g895" />
+ <g
+ style="fill:#008000"
+ id="g897" />
+ </g>
+ <g
+ id="g914"
+ transform="matrix(0.08572572,0,0,0.08572572,9.9999994,8.4247072)"
+ style="fill:#00ff00">
+ <g
+ id="g856"
+ style="fill:#00ff00">
+ <g
+ id="g854"
+ style="fill:#00ff00">
+ <path
+ inkscape:connector-curvature="0"
+ id="path850"
+ d="m 226.434,249.503 c 0,-6.995 -2.705,-13.403 -7.846,-18.556 L 61.8,74.165 c -5.128,-5.141 -11.554,-7.852 -18.568,-7.852 -7.026,0 -13.452,2.717 -18.556,7.846 l -16.83,16.83 c -5.129,5.135 -7.84,11.549 -7.84,18.538 0,7.026 2.717,13.452 7.846,18.556 L 129.267,249.503 7.84,370.936 C 2.711,376.071 0,382.491 0,389.486 c 0,7.02 2.717,13.439 7.846,18.544 l 16.775,16.774 c 5.116,5.165 11.555,7.895 18.611,7.895 7.044,0 13.47,-2.723 18.556,-7.846 l 156.813,-156.8 c 5.128,-5.14 7.833,-11.549 7.833,-18.55 z"
+ style="fill:#00ff00" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path852"
+ d="m 498.866,384.951 h -323.02 c -7.203,0 -13.611,2.583 -18.581,7.528 -4.896,4.92 -7.484,11.327 -7.484,18.531 v 21.536 c 0,7.252 2.607,13.672 7.491,18.543 4.915,4.927 11.34,7.528 18.574,7.528 h 323.02 c 7.239,0 13.659,-2.607 18.531,-7.497 4.927,-4.908 7.533,-11.334 7.533,-18.58 v -21.537 c 0,-7.209 -2.589,-13.616 -7.54,-18.592 -4.913,-4.877 -11.321,-7.46 -18.524,-7.46 z"
+ style="fill:#00ff00" />
+ </g>
+ </g>
+ <g
+ id="g858"
+ style="fill:#00ff00" />
+ <g
+ id="g860"
+ style="fill:#00ff00" />
+ <g
+ id="g862"
+ style="fill:#00ff00" />
+ <g
+ id="g864"
+ style="fill:#00ff00" />
+ <g
+ id="g866"
+ style="fill:#00ff00" />
+ <g
+ id="g868"
+ style="fill:#00ff00" />
+ <g
+ id="g870"
+ style="fill:#00ff00" />
+ <g
+ id="g872"
+ style="fill:#00ff00" />
+ <g
+ id="g874"
+ style="fill:#00ff00" />
+ <g
+ id="g876"
+ style="fill:#00ff00" />
+ <g
+ id="g878"
+ style="fill:#00ff00" />
+ <g
+ id="g880"
+ style="fill:#00ff00" />
+ <g
+ id="g882"
+ style="fill:#00ff00" />
+ <g
+ id="g884"
+ style="fill:#00ff00" />
+ <g
+ id="g886"
+ style="fill:#00ff00" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/discord.svg b/launcher/resources/multimc/scalable/discord.svg
new file mode 100644
index 00000000..067be1e8
--- /dev/null
+++ b/launcher/resources/multimc/scalable/discord.svg
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="128mm"
+ height="128mm"
+ viewBox="0 0 453.54331 453.54331"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="discord.svg">
+ <defs
+ id="defs4">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4166">
+ <stop
+ style="stop-color:#7593d7;stop-opacity:1"
+ offset="0"
+ id="stop4168" />
+ <stop
+ style="stop-color:#4f6aa3;stop-opacity:1"
+ offset="1"
+ id="stop4170" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4166"
+ id="linearGradient4174"
+ x1="351.42856"
+ y1="513.79077"
+ x2="351.42856"
+ y2="943.79077"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(24.999996,-5.8267714e-6)" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.98994949"
+ inkscape:cx="249.4082"
+ inkscape:cy="153.28604"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:snap-nodes="true"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="false"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-page="true"
+ inkscape:window-width="1911"
+ inkscape:window-height="2120"
+ inkscape:window-x="2970"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4155" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-1.4285583,-500.24745)">
+ <path
+ style="fill:url(#linearGradient4174);fill-opacity:1;fill-rule:evenodd"
+ d="m 73.928594,513.79076 a 37.5,37.5 0 0 0 -37.36914,34.88476 l -0.13086,0.11524 0,2.5 0,302.5 0.11914,0.11914 a 37.5,37.5 0 0 0 -0.11914,2.38086 37.5,37.5 0 0 0 37.5,37.5 37.5,37.5 0 0 0 2.41406,-0.0859 l 0.0859,0.0859 250.000006,0 -10,-40 100,90 0,-392.5 0,-2.5 -0.0918,-0.0918 a 37.5,37.5 0 0 0 -34.77734,-34.77734 l -0.13086,-0.13086 -2.5,0 -302.500006,0 -0.13477,0.11914 a 37.5,37.5 0 0 0 -2.36523,-0.11914 z"
+ id="path4157"
+ inkscape:connector-curvature="0" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-rule:evenodd"
+ d="m 262.51367,111.60742 -3.53515,4.79688 c 20.92905,7.8755 41.53825,15.82838 52.90625,26.01172 -54.51023,-27.29925 -111.94069,-29.57156 -173.11524,0.25195 13.83558,-12.04023 34.12183,-19.9521 56.31641,-26.64258 l -2.39844,-3.78711 c -19.87573,2.47189 -39.74673,4.97961 -57.48273,19.75608 -1.26426,1.0533 -2.43875,1.7112 -3.75946,3.35134 -1.04157,1.68882 -1.47654,2.94289 -2.20384,4.4259 -16.51968,33.6846 -29.13904,71.47893 -29.23561,122.47449 13.7929,19.97074 35.85963,31.20604 66.79492,33.08203 l 14.01563,-19.0664 c -16.32939,-4.839 -28.63498,-12.99042 -36.86914,-24.4961 68.24127,41.30702 122.04171,25.27927 169.32617,-0.50585 -10.12692,12.60017 -23.08016,21.50538 -39.39649,26.01171 l 14.52149,17.80469 c 31.64413,-2.6726 53.44205,-13.71547 66.03906,-32.57812 -1.25854,-42.6757 -9.40904,-79.47015 -23.71494,-111.01207 -2.51316,-5.54105 -4.27674,-11.49413 -8.10537,-16.14028 -3.45571,-4.1936 -7.96544,-7.13252 -12.90109,-9.9657 -12.42806,-7.13399 -28.6789,-11.32721 -47.20243,-13.77258 z m -76.00195,82.42383 c 12.33462,8e-5 22.33381,10.55717 22.33398,23.58008 -1.7e-4,13.02291 -9.99936,23.58 -22.33398,23.58008 -12.33462,-7e-5 -22.33382,-10.55716 -22.33399,-23.58008 1.7e-4,-13.02292 9.99937,-23.58001 22.33399,-23.58008 z m 79.54883,0 c 12.33462,8e-5 22.33381,10.55717 22.33398,23.58008 -1.7e-4,13.02291 -9.99936,23.58 -22.33398,23.58008 -12.33462,-7e-5 -22.33382,-10.55716 -22.33399,-23.58008 1.7e-4,-13.02292 9.99937,-23.58001 22.33399,-23.58008 z"
+ transform="translate(1.4285583,500.24745)"
+ id="path4176"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccscsccccccccsasccccccccccc" />
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/instances/bee.svg b/launcher/resources/multimc/scalable/instances/bee.svg
new file mode 100644
index 00000000..49f216c8
--- /dev/null
+++ b/launcher/resources/multimc/scalable/instances/bee.svg
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ viewBox="0 0 25.399999 25.4"
+ height="96"
+ width="96">
+ <g transform="translate(-33.928467,-255.46043)">
+ <g transform="rotate(-9.9635201,-96.932986,622.95265)">
+ <path style="fill:#f1f2e0;fill-opacity:1;fill-rule:evenodd;stroke:#999999;stroke-width:0.28753757px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 101.98199,286.42583 -1.1502,0.57512 1.14992,0.5755 2.30058,1.14996 1.15019,-0.57514 -2.30058,-1.14995 z m 2.3003,2.30058 -1.1502,0.57512 1.15002,0.57493 1.15019,-0.57513 z m -1.8e-4,1.15005 -1.1502,0.57512 1.15057,0.57502 1.15019,-0.57512 z m 3.7e-4,1.15014 -1.15019,0.57514 -1.1502,0.57512 1.15001,0.57493 1.1502,-0.57513 1.15019,-0.57512 z m -2.30039,1.15026 -1.15001,-0.57494 -1.150566,-0.57502 -1.150193,0.57512 1.150565,0.57502 1.150014,0.57494 z m -2.300576,-1.14996 1.150196,-0.57513 -1.150014,-0.57493 -1.150097,0.57456 z m -1.149915,-0.5755 1.150193,-0.57512 -1.150012,-0.57492 -1.150194,0.57512 z m 1.81e-4,-1.15004 1.150194,-0.57513 -1.150567,-0.57503 -1.150193,0.57512 z m -3.73e-4,-1.15016 1.150194,-0.57512 1.150189,-0.57513 -1.150008,-0.57492 -1.150193,0.57512 -1.150194,0.57512 z m 1.150567,0.57503 1.149916,0.57548 1.15001,0.57494 1.15019,-0.57512 -1.15001,-0.57494 -1.15047,-0.57558 z" />
+ </g>
+ <g style="fill:none;stroke:#999999" transform="matrix(1.0012918,0.26829532,-0.26829532,1.0012918,86.112205,-31.978257)">
+ <path style="stroke:#999999;stroke-width:1.03661346px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 85.501953 51.128906 C 82.473901 51.748417 79.445938 52.368362 76.417969 52.988281 L 79.886719 56.064453 C 82.914656 55.444381 85.942682 54.824742 88.970703 54.205078 L 85.501953 51.128906 z "
+ transform="matrix(0.24654113,-0.06606049,0.06606049,0.24654113,23.141685,280.86706)" />
+ <path style="stroke:#999999;stroke-width:1.03661346px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 79.636719 40.972656 L 75.095703 41.902344 C 78.563735 44.978675 82.032556 48.054115 85.501953 51.128906 L 90.042969 50.201172 L 79.636719 40.972656 z "
+ transform="matrix(0.24654113,-0.06606049,0.06606049,0.24654113,23.141685,280.86706)" />
+ </g>
+ <path style="fill:#fed668" d="m 41.865965,262.07502 -4.233333,2.11667 7.408333,3.70417 4.233334,-2.11667 z" />
+ <path style="fill:#0a0707" d="m 50.332633,272.39378 v 1.32291 l 1.058333,0.52917 v -1.32292 z" />
+ <path style="fill:#422117" d="m 50.332633,273.7167 v 1.32291 l 1.058333,0.52917 v -1.32292 z" />
+ <path style="fill:#0a0707" d="m 48.215966,273.45211 v 1.32291 l 1.058333,0.52917 v -1.32292 z" />
+ <path style="fill:#422117" d="m 48.215966,274.77503 v 1.32291 l 1.058333,0.52917 v -1.32292 z" />
+ <path style="fill:#422117" d="m 45.040966,275.3042 v 1.32291 l 1.058333,0.52917 v -1.32292 z" />
+ <path style="fill:#78621d" d="m 45.040965,277.15626 4.233334,-2.11665 v -9.26042 l -4.233334,2.11667 z" />
+ <path style="fill:#1d0c08" d="m 50.332632,265.25002 -1.058333,0.52917 v 9.26042 l 1.058333,-0.52917 z" />
+ <path style="fill:#1d0c08" d="m 52.449299,264.19169 -1.058333,0.52917 v 9.26042 l 1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 38.690965,263.66252 1.058334,0.52917 1.058333,-0.52917 1.058333,0.52917 2.116667,-1.05834 -2.116667,-1.05833 z" />
+ <path style="fill:#fed668" d="m 42.924299,261.54586 7.408334,3.70417 1.058333,-0.52917 -7.408334,-3.70417 z" />
+ <path style="fill:#e4ae3b" d="m 40.807633,262.60419 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#fed668" d="m 45.040966,260.48752 7.408333,3.70417 1.058333,-0.52917 -7.408333,-3.70417 z" />
+ <path style="fill:#5f3225" d="m 42.924299,261.54585 -1.058334,0.52917 7.408333,3.70417 1.058334,-0.52917 z" />
+ <path style="fill:#e4ae3b" d="m 42.9243,261.54585 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#5f3225" d="m 45.040965,260.48752 -1.058333,0.52917 7.408333,3.70416 1.058333,-0.52916 z" />
+ <path style="fill:#e4ae3b" d="m 45.040966,260.48752 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#5f3225" d="m 46.099299,259.95835 7.408333,3.70417 2.116666,-1.05833 -7.408333,-3.70417 z" />
+ <path style="fill:#552e22" d="m 47.157633,259.42919 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 43.982633,262.07502 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 46.099299,261.01669 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 40.807633,264.72086 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#e4ae3b" d="m 45.040967,262.60419 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#552e22" d="m 49.2743,260.48753 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 48.215966,262.07502 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 41.865966,266.30836 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#552e22" d="m 50.332633,262.07502 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 46.099299,265.25002 -1.058334,0.52917 1.058334,0.52916 -2.116667,1.05834 1.058333,0.52916 3.175,-1.5875 z" />
+ <path style="fill:#edc343" d="m 48.215967,264.19169 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 50.332633,263.13336 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#e4ae3b" d="m 47.157633,265.77919 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#e4ae3b" d="m 49.2743,264.72085 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#e4ae3b" d="m 51.390966,263.66252 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#edc343" d="m 38.690966,264.72086 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#ac8d2e" d="m 37.632633,273.45211 7.408333,3.70415 v -9.2604 l -7.408333,-3.70417 z" />
+ <path style="fill:#12121a" d="m 37.632633,268.16044 2.116666,1.05834 v 3.96875 l -2.116666,-1.05834 z" />
+ <path style="fill:#78621d" d="m 50.332633,265.25002 v 9.26041 l 1.058333,-0.52916 v -9.26042 z" />
+ <path style="fill:#78621d" d="m 52.449299,264.19169 v 9.26041 l 1.058333,-0.52916 v -9.26042 z" />
+ <path style="fill:#27120b" d="m 49.274299,265.77919 v 2.64583 l 1.058333,-0.52916 v -2.64584 z" />
+ <path style="fill:#09090e" d="m 45.040966,271.86461 1.058333,-0.52917 v 3.96875 l -1.058333,0.52917 -2.116667,-1.05834 v -3.96875 z" />
+ <path style="fill:#150704" d="m 49.2743,271.07085 v 2.64583 l 1.058333,-0.52916 v -2.64584 z" />
+ <path style="fill:#050303" d="m 49.2743,273.71669 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#150704" d="m 51.390966,268.68961 v 2.64583 l 1.058333,-0.52916 v -2.64584 z" />
+ <path style="fill:#050303" d="m 51.390966,271.33544 v 2.64583 l 1.058333,-0.52916 v -2.64584 z" />
+ <path style="fill:#2c140d" d="m 51.390966,264.72086 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#1d0c08" d="m 55.624299,262.60419 -2.116666,1.05833 v 9.26042 l 2.116666,-1.05833 z" />
+ <path style="fill:#150704" d="m 53.507633,268.95419 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#050303" d="m 53.507633,270.2771 v 2.64583 l 1.058333,-0.52916 v -2.64584 z" />
+ <path style="fill:#2c140d" d="m 53.507633,263.66252 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#27120b" d="m 54.565966,263.13335 v 2.64583 l 1.058333,-0.52916 v -2.64584 z" />
+ <path style="fill:#150704" d="m 54.565966,269.74794 v 2.64583 l 1.058333,-0.52916 v -2.64584 z" />
+ <path style="fill:#27120b" d="m 54.565966,267.10211 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#633f19" d="m 45.040965,277.15626 3.175,-1.58749 v -1.32291 l -3.175,1.5875 z" />
+ <path style="fill:#55300a" d="m 48.215966,274.24586 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#694520" d="m 48.215966,272.92294 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#735619" d="m 48.215966,271.60002 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#735619" d="m 47.157633,273.45211 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#131016" d="m 45.040966,271.86461 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#131016" d="m 45.040966,274.51044 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#694520" d="m 45.040966,270.54169 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#735619" d="m 46.0993,270.01252 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#816c31" d="m 45.040966,267.89586 v 1.32291 l 1.058333,-0.52916 v 1.32291 l 1.058334,-0.52916 v -1.32292 l 2.116666,-1.05833 v -1.32292 z" />
+ <path style="fill:#49250c" d="m 50.332633,273.18752 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#633f19" d="m 50.332633,271.86461 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#735619" d="m 50.332633,269.21878 v 2.64582 l 1.058333,-0.52916 v -2.64583 z" />
+ <path style="fill:#816c31" d="m 50.332633,265.25002 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#49250c" d="m 52.449299,270.80627 10e-7,2.64583 1.058333,-0.52916 -10e-7,-2.64584 z" />
+ <path style="fill:#55300a" d="m 52.4493,269.48336 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#694520" d="m 52.4493,268.16044 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#755719" d="m 52.4493,266.83752 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#816c31" d="m 52.4493,264.19169 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#b89b49" d="m 37.632632,265.51461 2.116667,1.05833 v -1.32292 l -2.116667,-1.05833 z" />
+ <path style="fill:#b89b49" d="m 40.807633,267.10211 2.116667,1.05833 v -1.32292 l -2.116667,-1.05833 z" />
+ <path style="fill:#b89b49" d="m 43.982633,270.01253 1.058333,0.52916 v -2.64584 l -1.058333,-0.52916 z" />
+ <path style="fill:#966531" d="m 43.982632,271.33545 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#a57d28" d="m 42.924299,270.80627 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#b89b49" d="m 41.865965,270.27712 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#966531" d="m 43.982632,271.33545 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#a57d28" d="m 39.749299,273.18753 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#a57d28" d="m 41.865965,274.24587 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#a57d28" d="m 40.807633,275.03961 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#8e5c28" d="m 43.982633,276.62711 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#966531" d="m 41.865966,275.56877 2.116667,1.05833 v -1.32292 l -2.116667,-1.05833 z" />
+ <path style="fill:#966531" d="m 38.690966,273.98128 2.116667,1.05833 v -1.32292 l -2.116667,-1.05833 z" />
+ <path style="fill:#8e5c28" d="m 37.632632,273.45212 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#956531" d="m 37.632633,268.16044 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#b89b49" d="m 39.749299,267.89587 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#1f1c25" d="m 38.690966,267.36669 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#1f1c25" d="m 42.924299,269.48337 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#589197" d="m 42.924299,272.12919 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#589197" d="m 38.690966,270.01252 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#12121a" d="m 43.982633,272.65836 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#12121a" d="m 42.924299,273.45211 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#12121a" d="m 43.982633,275.30419 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#1f1c25" d="m 43.982633,273.98127 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#1f1c25" d="m 42.924299,274.77503 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#1f1c25" d="m 38.690966,272.65836 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#1f1c25" d="m 37.632633,270.80627 1.058334,0.52916 v -1.32292 l -1.058334,-0.52916 z" />
+ <path style="fill:#131016" d="m 38.690966,267.10211 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#131016" d="m 36.574299,266.83752 v 1.32291 l 2.116667,-1.05832 v -1.32292 z" />
+ <path style="fill:#131016" d="m 41.865966,268.68961 v 1.32291 l 1.058333,-0.52916 v -1.32292 z" />
+ <path style="fill:#131016" d="m 39.749299,268.42502 v 1.32291 l 2.116667,-1.05832 v -1.32292 z" />
+ <g transform="matrix(1.0012918,0.26829532,-0.26829532,1.0012918,86.112205,-31.978257)">
+ <path style="fill:#f1f2e0" d="m 38.073986,286.23688 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#5f3225" d="m 37.015652,286.76606 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#5f3225" d="m 37.015652,287.82438 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 38.073986,288.35355 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 40.190652,286.23689 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 42.307319,286.23688 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 42.307319,287.29521 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 43.365653,287.82437 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 43.365653,288.88271 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 41.248986,288.88271 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 40.190653,288.35354 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 45.482319,288.88271 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 46.540653,288.35355 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 46.540653,287.29521 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 44.423986,286.23688 2.116667,1.05833 1.058333,-0.52917 -2.116667,-1.05833 z" />
+ </g>
+ <g transform="matrix(1.0703659,-0.18803179,0.18803179,1.0703659,-63.348962,-38.123102)">
+ <path style="fill:#f1f2e0" d="m 48.392735,288.88271 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#5f3225" d="m 47.334402,289.41187 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#5f3225" d="m 45.217735,289.41188 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 44.159402,288.8827 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 43.101069,286.23687 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 43.101068,285.17854 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 46.276068,284.64938 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 45.217735,286.23688 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 44.159402,284.64937 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 43.101068,287.2952 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 44.159402,287.82438 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 48.392735,286.76604 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 48.392735,287.82438 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f1f2e0" d="m 46.276068,286.76604 1.058333,0.52917 1.058333,-0.52917 -1.058333,-0.52917 z" />
+ <path style="fill:#f7fdfd" d="m 47.334402,285.17854 2.116667,1.05833 1.058333,-0.52917 -2.116667,-1.05833 z" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/instances/fox.svg b/launcher/resources/multimc/scalable/instances/fox.svg
new file mode 100644
index 00000000..fcf16b2f
--- /dev/null
+++ b/launcher/resources/multimc/scalable/instances/fox.svg
@@ -0,0 +1,290 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ id="svg8"
+ version="1.1"
+ viewBox="0 0 33.866666 33.866666"
+ height="128"
+ width="128">
+ <defs
+ id="defs2" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(0,-263.13334)"
+ id="layer1">
+ <path
+ id="rect4750"
+ d="m 4.233333,267.36667 v 6.35 6.35 3.175 3.175 3.175 3.175 h 25.4 v -3.175 -3.175 -3.175 -3.175 -6.35 -6.35 h -6.35 v 6.35 h -12.7 v -3.175 -3.175 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#800000;stroke-width:0.79375;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <g
+ id="g4748">
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#f9f4f4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.19062567;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4553"
+ width="6.3500032"
+ height="6.3499975"
+ x="4.233326"
+ y="267.36667" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#b48f83;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4553-6-2"
+ width="3.1750014"
+ height="3.1749992"
+ x="7.4083276"
+ y="270.54166" />
+ <rect
+ y="267.36667"
+ x="23.283335"
+ height="6.3499975"
+ width="6.3500032"
+ id="rect4623"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#f9f4f4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.19062567;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ y="270.54166"
+ x="23.283335"
+ height="3.1749992"
+ width="3.1750014"
+ id="rect4627"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#b48f83;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#e27c21;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4672"
+ width="25.400013"
+ height="15.875009"
+ x="4.233326"
+ y="273.71667" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#cc6920;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4629"
+ width="3.1750016"
+ height="6.3500032"
+ x="4.2333255"
+ y="273.71667" />
+ <rect
+ y="273.71667"
+ x="26.458338"
+ height="6.3500032"
+ width="3.1750016"
+ id="rect4631"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#cc6920;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#e78f41;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4636"
+ width="6.3500032"
+ height="3.1750016"
+ x="13.758331"
+ y="283.24167" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#b05122;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4638"
+ width="3.1750016"
+ height="3.1750016"
+ x="4.2333255"
+ y="280.06668" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#cc6920;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4640"
+ width="3.1750016"
+ height="3.1750016"
+ x="7.4083276"
+ y="280.06668" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#f9f4f4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.84189939;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4644"
+ width="6.3500028"
+ height="3.1750016"
+ x="4.233326"
+ y="283.24167" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#06040e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4642"
+ width="3.1750016"
+ height="3.1750016"
+ x="4.2333255"
+ y="283.24167" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#f9f4f4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.84189939;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4646"
+ width="6.3500028"
+ height="3.1750016"
+ x="23.283337"
+ y="283.24167" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#06040e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4648"
+ width="3.1750016"
+ height="3.1750016"
+ x="26.45834"
+ y="283.24167" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#cc6920;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4650"
+ width="3.1750016"
+ height="3.1750016"
+ x="23.283337"
+ y="280.06668" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#b05122;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4652"
+ width="3.1750016"
+ height="3.1750016"
+ x="26.45834"
+ y="280.06668" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#cc6920;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4654"
+ width="25.400013"
+ height="3.1750016"
+ x="4.2333255"
+ y="286.41666" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#e7d9d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.59531283;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4656"
+ width="25.400013"
+ height="3.1750016"
+ x="4.2333255"
+ y="289.59167" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5612604;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4917"
+ width="25.4"
+ height="0.26457807"
+ x="4.2333331"
+ y="273.71667" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.48607069;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4921"
+ width="0.2645835"
+ height="19.049997"
+ x="4.2333331"
+ y="273.71667" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#f9f4f4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.19062555;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4658"
+ width="12.700007"
+ height="6.3500013"
+ x="10.583333"
+ y="286.41666" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#06040e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.19062555;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4660"
+ width="6.3500037"
+ height="3.1750007"
+ x="13.758333"
+ y="286.41669" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#e7d9d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.19062555;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4662"
+ width="3.1750019"
+ height="3.1750007"
+ x="10.583333"
+ y="286.41669" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#e7d9d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.19062555;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4664"
+ width="3.1750019"
+ height="3.1750007"
+ x="20.108334"
+ y="286.41669" />
+ <rect
+ y="286.41669"
+ x="10.583333"
+ height="6.3499832"
+ width="0.2645835"
+ id="rect4923"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.2806327;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.39686465;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4951"
+ width="12.699996"
+ height="0.26456967"
+ x="10.583333"
+ y="286.41669" />
+ <rect
+ y="273.71667"
+ x="29.36875"
+ height="19.049982"
+ width="0.26458356"
+ id="rect4953"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.48607057;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ y="292.50208"
+ x="23.283333"
+ height="0.26457682"
+ width="6.3499994"
+ id="rect4957"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28062955;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.2806325;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4959"
+ width="0.2645838"
+ height="6.3499656"
+ x="23.018749"
+ y="286.41669" />
+ <rect
+ y="292.50208"
+ x="4.2333331"
+ height="0.26457968"
+ width="6.3499999"
+ id="rect4961"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28063107;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ y="267.36667"
+ x="4.2333331"
+ height="6.3499956"
+ width="0.2645835"
+ id="rect4963"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28063297;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28063536;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4965"
+ width="6.349998"
+ height="0.26458791"
+ x="4.2333331"
+ y="267.36667" />
+ <rect
+ y="267.36667"
+ x="23.283333"
+ height="6.3499956"
+ width="0.2645835"
+ id="rect4963-9"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.280633;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28063536;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4965-1"
+ width="6.349998"
+ height="0.26458791"
+ x="23.283333"
+ y="267.36667" />
+ <rect
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28063306;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4985"
+ width="0.26458356"
+ height="6.3499975"
+ x="29.36875"
+ y="267.36667" />
+ <rect
+ y="267.36667"
+ x="10.318749"
+ height="6.3499975"
+ width="0.26458356"
+ id="rect4987"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.25;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28063306;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/java.svg b/launcher/resources/multimc/scalable/java.svg
new file mode 100644
index 00000000..fd15e5c6
--- /dev/null
+++ b/launcher/resources/multimc/scalable/java.svg
@@ -0,0 +1,773 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="160.00024"
+ height="159.99788"
+ id="svg12972"
+ version="1.1"
+ inkscape:version="0.48.5 r10040"
+ sodipodi:docname="java.svg">
+ <defs
+ id="defs12974">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3935">
+ <stop
+ style="stop-color:#000000;stop-opacity:0"
+ offset="0"
+ id="stop3937" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="1"
+ id="stop3939" />
+ </linearGradient>
+ <linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,-126.08191,-126.08191,0,113.7417,390.27905)"
+ spreadMethod="pad"
+ id="linearGradient1722">
+ <stop
+ style="stop-opacity:1;stop-color:#dfe3e5"
+ offset="0"
+ id="stop1724" />
+ <stop
+ style="stop-opacity:1;stop-color:#bcc5ca"
+ offset="1"
+ id="stop1726" />
+ </linearGradient>
+ <linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,68.061096,68.061096,0,113.7417,266.44751)"
+ spreadMethod="pad"
+ id="linearGradient1760">
+ <stop
+ style="stop-opacity:1;stop-color:#ffffff"
+ offset="0"
+ id="stop1762" />
+ <stop
+ style="stop-opacity:1;stop-color:#000000"
+ offset="1"
+ id="stop1764" />
+ </linearGradient>
+ <linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,-21.014618,-21.014618,0,113.7417,386.83423)"
+ spreadMethod="pad"
+ id="linearGradient1798">
+ <stop
+ style="stop-opacity:1;stop-color:#ffffff"
+ offset="0"
+ id="stop1800" />
+ <stop
+ style="stop-opacity:1;stop-color:#000000"
+ offset="1"
+ id="stop1802" />
+ </linearGradient>
+ <linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,-126.08191,-126.08191,0,113.7417,390.27905)"
+ spreadMethod="pad"
+ id="linearGradient1722-2">
+ <stop
+ style="stop-opacity:1;stop-color:#dfe3e5"
+ offset="0"
+ id="stop1724-7" />
+ <stop
+ style="stop-opacity:1;stop-color:#bcc5ca"
+ offset="1"
+ id="stop1726-8" />
+ </linearGradient>
+ <linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,68.061096,68.061096,0,113.7417,266.44751)"
+ spreadMethod="pad"
+ id="linearGradient1760-4">
+ <stop
+ style="stop-opacity:1;stop-color:#ffffff"
+ offset="0"
+ id="stop1762-0" />
+ <stop
+ style="stop-color:#838383;stop-opacity:1;"
+ offset="1"
+ id="stop1764-8" />
+ </linearGradient>
+ <linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,-21.014618,-21.014618,0,113.7417,386.83423)"
+ spreadMethod="pad"
+ id="linearGradient1798-6">
+ <stop
+ style="stop-opacity:1;stop-color:#ffffff"
+ offset="0"
+ id="stop1800-8" />
+ <stop
+ style="stop-opacity:1;stop-color:#000000"
+ offset="1"
+ id="stop1802-6" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1722-2"
+ id="linearGradient3923"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,-126.08191,-126.08191,0,113.7417,390.27905)"
+ spreadMethod="pad"
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient3933"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3941"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient3943"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient3945"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient3947"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient3949"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient3951"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient3953"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient3955"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3957"
+ gradientUnits="userSpaceOnUse"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3961"
+ gradientUnits="userSpaceOnUse"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3963"
+ gradientUnits="userSpaceOnUse"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3965"
+ gradientUnits="userSpaceOnUse"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3967"
+ gradientUnits="userSpaceOnUse"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3969"
+ gradientUnits="userSpaceOnUse"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3971"
+ gradientUnits="userSpaceOnUse"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3935"
+ id="linearGradient3973"
+ gradientUnits="userSpaceOnUse"
+ x1="112.53288"
+ y1="365.57138"
+ x2="112.52676"
+ y2="386.9866" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4-8"
+ id="linearGradient3943-2"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="114.54707"
+ y2="417.25464" />
+ <linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0,68.061096,68.061096,0,113.7417,266.44751)"
+ spreadMethod="pad"
+ id="linearGradient1760-4-8">
+ <stop
+ style="stop-opacity:1;stop-color:#ffffff"
+ offset="0"
+ id="stop1762-0-6" />
+ <stop
+ style="stop-opacity:1;stop-color:#000000"
+ offset="1"
+ id="stop1764-8-4" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1760-4"
+ id="linearGradient4033"
+ gradientUnits="userSpaceOnUse"
+ x1="113.341"
+ y1="267.14597"
+ x2="113.94098"
+ y2="387.35413" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.979899"
+ inkscape:cx="-0.08087368"
+ inkscape:cy="54.594259"
+ inkscape:document-units="px"
+ inkscape:current-layer="g1676-6"
+ showgrid="false"
+ showborder="true"
+ borderlayer="false"
+ inkscape:showpageshadow="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1613"
+ inkscape:window-height="1026"
+ inkscape:window-x="1677"
+ inkscape:window-y="-4"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid14007"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="-3.4602454px"
+ originy="2.8306126px"
+ spacingx="8px"
+ spacingy="8px" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata12977">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-269.17466,-423.7599)">
+ <g
+ transform="matrix(1.25,0,0,-1.25,203.53691,909.61217)"
+ id="g1674">
+ <g
+ id="g1676"
+ clip-path="url(#clipPath1678)">
+ <g
+ transform="translate(2.7683963,-2.2641865)"
+ id="g1674-5">
+ <g
+ id="g1676-6"
+ clip-path="url(#clipPath1678)">
+ <path
+ id="path3993"
+ style="fill:#ffffff;fill-opacity:1;stroke:none"
+ d="m 71.121479,387.39443 c -9.563,0 -17.336,-7.8193 -17.336,-17.42155 l 0,0 0,-85.75999 c 0,-9.61029 7.773,-17.42255 17.336,-17.42255 l 0,0 85.327991,0 c 9.562,0 17.336,7.81226 17.336,17.42255 l 0,0 0,85.75999 c 0,9.60225 -7.774,17.42155 -17.336,17.42155 l 0,0 z"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sccsccsccsccs" />
+ <g
+ id="g1682">
+ <g
+ id="g1684" />
+ <g
+ id="g1694">
+ <g
+ clip-path="url(#clipPath1686)"
+ id="g1696">
+ <g
+ id="g1698">
+ <g
+ clip-path="url(#clipPath1690)"
+ id="g1700">
+ <g
+ transform="translate(156.4058,390.9377)"
+ id="g1702">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 -85.328,0 c -11.735,0 -21.336,-9.6 -21.336,-21.334 l 0,-85.32 c 0,-11.742 9.601,-21.336 21.336,-21.336 l 85.328,0 c 11.734,0 21.336,9.594 21.336,21.336 l 0,85.32 C 21.336,-9.6 11.734,0 0,0 m 0,-1 c 11.211,0 20.336,-9.123 20.336,-20.334 l 0,-85.32 c 0,-11.219 -9.125,-20.336 -20.336,-20.336 l -85.328,0 c -11.215,0 -20.336,9.117 -20.336,20.336 l 0,85.32 c 0,11.211 9.121,20.334 20.336,20.334 L 0,-1"
+ style="fill:#b3b1b3;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1704" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ <g
+ id="g1706-5">
+ <g
+ id="g1708-7" />
+ <g
+ id="g1728-8">
+ <g
+ clip-path="url(#clipPath1710)"
+ id="g1730-3">
+ <g
+ id="g1732-3">
+ <g
+ clip-path="url(#clipPath1714)"
+ id="g1734-1">
+ <g
+ id="g1736-0">
+ <g
+ id="g1738-5">
+ <g
+ clip-path="url(#clipPath1718)"
+ id="g1740-7">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 71.078,390.946 c -11.735,0 -21.336,-9.6 -21.336,-21.334 l 0,0 0,-85.329 c 0,-11.742 9.601,-21.335 21.336,-21.335 l 0,0 85.328,0 c 11.734,0 21.336,9.593 21.336,21.335 l 0,0 0,85.329 c 0,11.734 -9.602,21.334 -21.336,21.334 l 0,0 -85.328,0 z m -17.336,-106.663 0,85.329 c 0,9.554 7.773,17.334 17.336,17.334 l 0,0 85.328,0 c 9.562,0 17.336,-7.78 17.336,-17.334 l 0,0 0,-85.329 c 0,-9.562 -7.774,-17.335 -17.336,-17.335 l 0,0 -85.328,0 c -9.563,0 -17.336,7.773 -17.336,17.335"
+ style="fill:url(#linearGradient3923);stroke:none"
+ id="path1742-8" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ <g
+ id="g1744-9">
+ <g
+ id="g1746-7" />
+ <g
+ id="g1766-3"
+ style="fill:url(#linearGradient4033);fill-opacity:1">
+ <g
+ style="opacity:0.69999701;fill:url(#linearGradient3955);fill-opacity:1"
+ clip-path="url(#clipPath1748)"
+ id="g1768-8">
+ <g
+ id="g1770-7"
+ style="fill:url(#linearGradient3953);fill-opacity:1">
+ <g
+ clip-path="url(#clipPath1752)"
+ id="g1772-5"
+ style="fill:url(#linearGradient3951);fill-opacity:1">
+ <g
+ id="g1774-4"
+ style="fill:url(#linearGradient3949);fill-opacity:1">
+ <g
+ id="g1776-9"
+ style="fill:url(#linearGradient3947);fill-opacity:1">
+ <g
+ clip-path="url(#clipPath1756)"
+ id="g1778-9"
+ style="fill:url(#linearGradient3945);fill-opacity:1">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 71.078,386.946 c -9.563,0 -17.336,-7.78 -17.336,-17.334 l 0,0 0,-85.329 c 0,-9.562 7.773,-17.335 17.336,-17.335 l 0,0 85.328,0 c 9.562,0 17.336,7.773 17.336,17.335 l 0,0 0,85.329 c 0,9.554 -7.774,17.334 -17.336,17.334 l 0,0 -85.328,0 z m -13.336,-102.663 0,85.329 c 0,7.351 5.984,13.336 13.336,13.336 l 0,0 85.328,0 c 7.351,0 13.336,-5.985 13.336,-13.336 l 0,0 0,-85.329 c 0,-7.351 -5.985,-13.335 -13.336,-13.335 l 0,0 -85.328,0 c -7.352,0 -13.336,5.984 -13.336,13.335"
+ style="fill:url(#linearGradient3943);fill-opacity:1;stroke:none"
+ id="path1780-1" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ <g
+ id="g1820-4"
+ transform="translate(99.772903,299.7866)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c -2.18,-0.91 -2.281,-0.98 -4.141,-2.281 -0.148,-0.223 -0.613,-2.258 1.102,-3.602 2.082,-1.633 7.559,-3.285 15.098,-3.285 4.445,0 11.136,0.598 17.898,3.434 l -0.129,0.882 c -3.148,-0.57 -6.539,-0.843 -10.34,-0.843 -5.386,0 -10.515,0.539 -16.183,1.187 -2.727,0.309 -4.086,0.895 -4.282,1.836 -0.148,0.692 0.414,1.289 0.852,1.641 -0.07,0.281 0.02,0.633 0.117,1 0,0.008 0,0.023 0.008,0.031 m 12.059,-9.969 c -6.692,0 -12.957,1.391 -15.59,3.457 -1.414,1.106 -1.586,2.551 -1.567,3.317 0.016,0.601 0.18,1.343 0.5,1.57 2.02,1.414 2.067,1.437 4.754,2.539 L 0.391,0.965 C 0.543,0.965 0.68,0.898 0.773,0.781 0.961,0.539 0.879,0.215 0.766,-0.23 0.719,-0.406 0.633,-0.734 0.648,-0.836 0.641,-0.766 0.543,-0.668 0.434,-0.668 l 0.308,-0.73 c -0.422,-0.243 -1.008,-0.766 -0.937,-1.11 0.039,-0.191 0.422,-0.844 3.593,-1.211 5.641,-0.64 10.743,-1.179 16.09,-1.179 3.918,0 7.383,0.296 10.598,0.906 l 0.469,-0.336 0.242,-1.594 -0.242,-0.426 C 23.57,-9.34 16.641,-9.969 12.059,-9.969"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1822-3" />
+ </g>
+ <g
+ id="g1824-2"
+ transform="translate(100.3159,298.73581)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 -3.941,-2.289 2.809,-3.063 8.175,-0.933 17.8,-1.976 26.808,-0.269 l 0.238,-1.598 c -13.5,-5.785 -29.367,-3.609 -33.687,-0.215 -2.016,1.586 -1.367,4.043 -1.078,4.243 2,1.398 2,1.398 4.672,2.5 C 0.359,1.84 -0.801,-0.223 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1826-2" />
+ </g>
+ <g
+ id="g1828-6"
+ transform="translate(97.741699,311.24749)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c -0.293,-0.156 -0.641,-0.32 -1.016,-0.492 -2.539,-1.18 -4.589,-2.36 -3.949,-4.93 0.457,-1.84 6.442,-3.797 16.621,-3.797 7.43,0 15.282,1.086 22.149,3.059 l -0.121,0.84 c -4.723,-0.883 -9.606,-1.313 -14.914,-1.313 -6.184,0 -11.832,0.586 -16.372,1.055 -2.457,0.25 -3.785,0.871 -4.062,1.89 C -2,-2.477 -0.641,-1.289 -0.23,-0.965 -0.098,-0.594 -0.016,-0.223 0,0 m 11.656,-10.016 c -8.113,0 -16.64,1.36 -17.398,4.399 -0.852,3.422 2.304,4.883 4.39,5.851 0.383,0.18 0.739,0.344 1.032,0.5 C 0.094,0.961 0.461,0.898 0.656,0.617 0.992,0.133 0.645,-0.922 0.477,-1.352 L 0.344,-1.531 c -0.383,-0.285 -1.43,-1.25 -1.239,-1.946 0.102,-0.367 0.676,-1.031 3.372,-1.304 C 7,-5.25 12.629,-5.832 18.77,-5.832 c 5.41,0 10.371,0.449 15.171,1.371 l 0.473,-0.332 0.234,-1.598 -0.281,-0.441 c -7.023,-2.055 -15.09,-3.184 -22.711,-3.184"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1830-5" />
+ </g>
+ <g
+ id="g1832-4"
+ transform="translate(97.847199,310.0366)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 -4.426,-3.273 2.332,-3.969 8.734,-0.906 19.641,-1.969 31.578,0.328 l 0.238,-1.593 c -18.253,-5.344 -38.582,-3.18 -39.605,0.925 -0.922,3.7 3.383,4.899 5.219,5.903 C 0.613,2.063 0.246,0.641 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1834-7" />
+ </g>
+ <g
+ id="g1836-0"
+ transform="translate(136.40581,358.5717)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c -0.09,0 -0.25,-0.031 -0.512,-0.18 -0.707,-0.402 -1.515,-0.84 -2.394,-1.312 -6.344,-3.43 -16.961,-9.168 -18.367,-16.887 -0.633,-3.465 0.656,-7.07 3.828,-10.726 2.808,-3.231 0.652,-6.844 -0.633,-9 l -0.242,-0.407 c -0.328,-0.562 -0.446,-0.922 -0.481,-1.148 1.52,0.879 6.508,4.047 7.524,8.23 0.461,1.907 0.035,3.801 -1.27,5.637 l -0.687,0.957 c -2.258,3.137 -4.207,5.84 -3.532,9.004 0.86,4.023 5.973,8.508 17.082,14.969 C 0.32,-0.574 0.273,-0.207 0.141,-0.059 0.109,-0.023 0.074,0 0,0 m -18.871,-40.613 -0.324,0.183 -0.11,-0.136 c -0.234,0.187 -0.679,0.804 0.293,2.461 l 0.246,0.41 c 1.25,2.097 3.145,5.269 0.719,8.066 -3.34,3.844 -4.691,7.68 -4.016,11.391 1.477,8.101 12.305,13.953 18.774,17.449 0.875,0.473 1.68,0.91 2.383,1.305 0.699,0.402 1.273,0.371 1.644,-0.043 0.469,-0.524 0.375,-1.508 0.364,-1.618 L 0.906,-1.449 c -10.879,-6.309 -16.086,-10.797 -16.89,-14.555 -0.602,-2.812 1.164,-5.262 3.398,-8.363 l 0.688,-0.961 c 1.445,-2.035 1.914,-4.152 1.398,-6.293 -1.281,-5.281 -8.082,-8.844 -8.371,-8.992"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1838-3" />
+ </g>
+ <g
+ id="g1840-7"
+ transform="translate(118.6597,329.2006)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 2.793,-3.211 0.277,-6.891 -0.918,-8.938 -0.937,-1.597 -0.391,-1.945 -0.391,-1.945 0,0 12.641,6.524 6.832,14.696 -5.421,7.617 -9.574,11.402 12.926,24.457 0,0 0.242,2.207 -1.414,1.269 C 8.871,24.93 -13.129,15.133 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1842-9" />
+ </g>
+ <g
+ id="g1844-3"
+ transform="translate(90.905807,292.3256)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c -4.941,-0.07 -14.234,-4.152 -14.438,-6.926 -0.078,-1.09 1.547,-1.957 4.571,-2.449 10.109,-1.641 20.023,-2.469 29.476,-2.469 20.106,0 34.25,3.938 36.305,7.746 0.219,0.403 0.289,0.793 0.203,1.168 -0.676,-0.711 -2.32,-1.84 -6.316,-3.054 -6.61,-2 -21.028,-3.454 -34.289,-3.454 -10.922,0 -19.16,0.969 -23.196,2.719 -0.918,0.403 -1.355,0.91 -1.293,1.508 0.204,2.047 6.704,3.531 6.766,3.547 1.633,0.351 2.777,0.41 3.336,0.414 0.055,0.328 0,0.617 -0.16,0.836 C 0.773,-0.148 0.43,0 0,0 m 19.609,-12.648 0,0 c -9.492,0 -19.453,0.835 -29.601,2.484 -3.594,0.582 -5.36,1.691 -5.242,3.297 0.257,3.508 10.101,7.594 15.195,7.664 0.734,0 1.305,-0.262 1.652,-0.738 0.356,-0.497 0.43,-1.004 0.207,-1.696 l -0.41,-0.129 -0.238,0 c -0.512,0 -1.61,-0.195 -3.211,-0.539 -2.414,-0.519 -6.055,-2.011 -6.145,-2.914 -0.019,-0.199 0.286,-0.496 0.821,-0.726 3.933,-1.715 12.058,-2.676 22.875,-2.676 13.191,0 27.511,1.432 34.058,3.412 5.657,1.719 6.231,3.202 6.235,3.218 l 0.718,0.105 c 0.54,-0.817 0.571,-1.717 0.094,-2.592 -2.137,-3.961 -15.836,-8.17 -37.008,-8.17"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1846-0" />
+ </g>
+ <g
+ id="g1848-9"
+ transform="translate(147.09331,290.21631)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 -0.367,-1.629 -6.5,-3.492 -11.664,-3.531 -46.965,-5.203 -57.211,-0.75 -3.68,1.605 3.227,3.828 5.398,4.297 2.266,0.484 3.563,0.398 3.563,0.398 0.375,1.141 -0.242,2.07 -1.469,2.055 C -63.281,2.406 -78.988,-5.57 -66.117,-7.664 -24.926,-14.34 3.961,-6.043 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1850-0" />
+ </g>
+ <g
+ id="g1852-7"
+ transform="translate(101.40581,322.55999)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 -0.133,0 c -2.879,0 -10.93,-1.609 -16.5,-4.305 -0.695,-0.336 -0.59,-0.871 -0.383,-1.672 0.055,-0.214 0.094,-0.351 0.102,-0.484 0.086,-1.477 7.898,-3.043 20.367,-3.043 9.969,0 19.508,1.014 26.188,2.764 1.078,0.742 2.308,1.913 3.187,2.8 C 30.172,-4.851 24.617,-6.55 19.008,-7.023 13.605,-7.476 7.953,-7.724 3.09,-7.724 c -3.746,0 -6.848,0.153 -8.981,0.434 -3.007,0.406 -4.371,1 -4.418,1.938 -0.105,2.059 7.961,4.188 10.801,4.872 0.059,0.234 0.094,0.355 -0.019,0.523 C 0.398,0.148 0.242,0 0,0 m 3.453,-10.281 c -2.14,0 -20.953,0.109 -21.164,3.82 -0.004,0.102 -0.043,0.231 -0.082,0.383 -0.168,0.664 -0.527,2.047 0.813,2.695 5.73,2.774 13.761,4.352 16.789,4.578 0.777,0.075 1.152,-0.25 1.324,-0.504 0.43,-0.628 0.086,-1.55 0.043,-1.656 L 0.898,-1.203 c -5.066,-1.203 -10.457,-3.141 -10.406,-4.11 0,-0.023 0.074,-0.695 3.727,-1.179 2.093,-0.281 5.164,-0.43 8.871,-0.43 4.84,0 10.473,0.25 15.848,0.699 7.609,0.641 15.132,3.586 15.207,3.614 l 0.441,-0.637 C 34.484,-3.359 32.035,-6.066 30.039,-7.43 L 29.914,-7.484 C 23.164,-9.262 13.52,-10.281 3.453,-10.281"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1854-4" />
+ </g>
+ <g
+ id="g1856-9"
+ transform="translate(102.2104,321.7436)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 -18.754,-4.457 -6.641,-6.074 5.114,-0.684 15.313,-0.528 24.809,0.269 7.762,0.653 15.316,3.645 15.316,3.645 0,0 -2.496,-2.774 -4.476,-4.125 -16.664,-4.383 -46.906,-3.141 -47.125,0.664 -0.031,0.547 -0.821,2.055 0.508,2.695 5.699,2.758 13.75,4.321 16.64,4.539 C 0.688,1.738 0,0 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1858-7" />
+ </g>
+ <g
+ id="g1860-7"
+ transform="translate(135.56979,302.0444)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 -0.367,0.68 c 0.09,0.086 0.242,0.258 0.281,0.359 L 0.273,0.898 c 11.25,3.071 13.993,7.801 14.618,10.368 0.609,2.5 -0.219,5.004 -2.282,6.867 -2.695,2.445 -6.703,3.133 -10.48,1.812 -0.098,-0.039 -0.152,-0.078 -0.16,-0.101 -0.024,-0.125 0.164,-0.418 0.375,-0.656 0.328,0.109 0.984,0.304 2.019,0.523 3.262,0.687 6.711,-0.977 7.68,-3.695 C 12.742,14.051 12.992,8.766 0.234,2.133 L -0.133,2.844 C 12.066,9.184 11.918,13.98 11.289,15.75 10.379,18.301 7.078,19.461 4.527,18.93 3.066,18.621 2.391,18.355 2.383,18.352 L 1.961,18.43 c -0.152,0.14 -0.914,0.886 -0.773,1.578 0.046,0.219 0.203,0.523 0.675,0.687 4.051,1.43 8.371,0.672 11.285,-1.968 2.282,-2.071 3.204,-4.856 2.52,-7.649 C 14.992,8.301 12.055,3.168 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1862-4" />
+ </g>
+ <g
+ id="g1864-5"
+ transform="translate(135.6245,304.5366)">
+ <path
+ inkscape:connector-curvature="0"
+ d="M 0,0 C 19.359,10.059 10.645,18.129 4.391,16.828 2.867,16.508 2.18,16.234 2.18,16.234 c 0,0 -1.321,1.211 -0.239,1.594 12.364,4.344 23.762,-13.133 -2.097,-19.937 0,0 0.297,0.269 0.39,0.511"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1866-7" />
+ </g>
+ <g
+ id="g1868-4"
+ transform="translate(124.9565,376.80999)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 1.715,-10.453 -5.746,-16.297 -12.973,-21.953 -3.843,-3.008 -7.468,-5.848 -9.785,-9.324 -4.574,-6.864 2.379,-14.461 10.973,-22.301 -0.016,0.066 -0.031,0.137 -0.043,0.203 -0.145,0.656 -0.27,1.227 -0.836,2.043 -0.41,0.598 -0.84,1.199 -1.277,1.809 -4.758,6.636 -10.149,14.16 2.75,24.351 8.101,6.399 12.39,12.633 12.746,18.531 C 1.738,-3.633 0.801,-1.359 0,0 m -11.215,-55.172 c -9.359,8.438 -17.332,16.649 -12.207,24.336 2.387,3.586 6.234,6.602 9.957,9.512 7.438,5.824 15.133,11.847 12.305,22.965 l 0.672,0.379 c 0.125,-0.129 3.164,-3.231 2.843,-8.684 -0.359,-6.145 -4.75,-12.582 -13.046,-19.133 -12.305,-9.719 -7.375,-16.601 -2.602,-23.262 0.438,-0.613 0.875,-1.218 1.289,-1.82 0.652,-0.945 0.801,-1.613 0.957,-2.324 0.098,-0.453 0.203,-0.922 0.465,-1.512 l -0.633,-0.457 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1870-4" />
+ </g>
+ <g
+ id="g1872-9"
+ transform="translate(124.18311,378.5444)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 10.723,-10.723 -10.168,-27.219 -13.789,-10.89 -6.187,-18.664 -1.391,-25.621 1.032,-1.496 0.711,-2.246 1.383,-3.769 -9.781,8.82 -16.961,16.586 -12.14,23.82 C -15.25,-22.18 4.332,-17.031 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1874-2" />
+ </g>
+ <g
+ id="g1876-2"
+ transform="translate(89.905807,281.85381)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0.625,-0.367 3.828,-1.914 14.234,-2.582 3.352,-0.215 6.875,-0.324 10.481,-0.324 16.652,0 36.281,2.457 36.89,9.375 -0.05,0.344 -0.144,0.57 -0.199,0.609 C 61.281,6.906 61.141,6.781 60.977,6.641 59.297,5.172 55.371,1.727 46.723,0.102 41.859,-0.813 35.746,-1.277 28.555,-1.277 17.664,-1.277 6.852,-0.199 0.363,0.813 0.199,0.805 0.16,0.789 0.141,0.762 0.016,0.617 -0.012,0.258 0,0 m 24.719,-4.094 -0.004,0 c -3.621,0 -7.164,0.305 -10.527,0.52 -12.211,0.781 -14.719,2.859 -14.821,2.945 l -0.14,0.295 c -0.016,0.105 -0.141,1.075 0.304,1.595 0.235,0.261 0.551,0.391 0.957,0.328 6.457,-1.007 17.223,-2.072 28.067,-2.072 7.14,0 13.203,0.464 18.019,1.367 8.434,1.59 12.246,4.927 13.871,6.357 0.172,0.148 0.321,0.278 0.45,0.383 0.187,0.156 0.378,0.923 0.574,0.923 0.492,0 0.828,-0.641 0.937,-0.641 l -0.031,0 0.031,-0.968 C 61.828,-0.547 42.559,-4.094 24.719,-4.094"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1878-6" />
+ </g>
+ <g
+ id="g1880-7"
+ transform="translate(104.1167,278.87331)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 18.582,-1.188 47.117,0.664 47.797,9.457 0,0 -0.149,1.434 -0.859,0.844 C 45.516,9.117 41.82,5.238 32.438,3.473 18.891,0.926 -2.184,2.379 -13.785,4.188 -14.828,4.348 -14.59,2.754 -14.59,2.754 -14.59,2.754 -12.219,0.785 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1882-9" />
+ </g>
+ <g
+ id="g1884-1"
+ transform="translate(100.07761,300.3334)">
+ <path
+ inkscape:connector-curvature="0"
+ d="M 0,0 C 0,0 -3.941,-2.289 2.809,-3.063 10.984,-4 15.168,-3.867 24.18,-2.164 c 0,0 2.367,-1.484 5.675,-2.766 C 9.648,-13.594 -15.867,-4.434 0,0"
+ style="fill:#5382a1;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1886-9" />
+ </g>
+ <g
+ id="g1888-1"
+ transform="translate(97.608902,311.63031)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 -4.422,-3.273 2.336,-3.969 8.734,-0.902 15.633,-0.976 27.57,1.321 0,0 1.649,-1.672 4.242,-2.586 C 9.727,-12.379 -17.48,-5.797 0,0"
+ style="fill:#5382a1;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1890-8" />
+ </g>
+ <g
+ id="g1892-5"
+ transform="translate(118.4214,330.79831)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 4.98,-5.73 -1.309,-10.887 -1.309,-10.887 0,0 12.641,6.524 6.832,14.696 -5.421,7.617 -9.574,11.406 12.93,24.461 0,0 -35.328,-8.829 -18.453,-28.27"
+ style="fill:#f8981d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1894-5" />
+ </g>
+ <g
+ id="g1896-7"
+ transform="translate(145.14011,291.9819)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 2.922,-2.406 -3.215,-4.27 -11.66,-3.531 -48.535,-4.597 -58.777,-0.14 -3.68,1.605 3.226,3.824 5.398,4.293 2.262,0.488 3.559,0.398 3.559,0.398 -4.098,2.883 -26.473,-5.664 -11.367,-8.113 C -23.211,-14.508 10.684,-4.824 0,0"
+ style="fill:#5382a1;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1898-7" />
+ </g>
+ <g
+ id="g1900-1"
+ transform="translate(101.9761,323.34131)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 -18.758,-4.457 -6.645,-6.078 5.114,-0.68 15.313,-0.524 24.809,0.273 7.766,0.649 15.555,2.047 15.555,2.047 0,0 -2.735,-1.176 -4.711,-2.527 -19.051,-5.012 -55.84,-2.676 -45.25,2.441 C -7.281,0.492 0,0 0,0"
+ style="fill:#5382a1;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1902-4" />
+ </g>
+ <g
+ id="g1904-9"
+ transform="translate(135.6245,304.5366)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 19.359,10.059 10.406,19.727 4.156,18.422 -1.527,-0.32 -2.215,-0.594 -2.215,-0.594 0,0 0.571,0.895 1.653,1.274 12.367,4.347 21.871,-12.813 -3.985,-19.614 0,0 0.297,0.27 0.391,0.512"
+ style="fill:#5382a1;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1906-4" />
+ </g>
+ <g
+ id="g1908-6"
+ transform="translate(123.94479,380.142)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 0,0 10.723,-10.723 -10.168,-27.223 -16.75,-13.226 -3.824,-20.769 -0.004,-29.39 -9.781,8.824 -16.961,16.59 -12.144,23.82 C -15.25,-22.18 4.332,-17.035 0,0"
+ style="fill:#f8981d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1910-0" />
+ </g>
+ <g
+ id="g1912-6"
+ transform="translate(103.8784,277.2709)">
+ <path
+ inkscape:connector-curvature="0"
+ d="m 0,0 c 18.582,-1.188 47.121,0.664 47.801,9.453 0,0 -1.301,-3.336 -15.36,-5.98 C 16.578,0.488 -2.988,0.836 -14.59,2.75 -14.59,2.75 -12.215,0.781 0,0"
+ style="fill:#5382a1;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path1914-4" />
+ </g>
+ <g
+ id="g3975">
+ <g
+ id="g3977" />
+ <g
+ style="fill:url(#linearGradient3933);fill-opacity:1"
+ id="g3979">
+ <g
+ id="g3981"
+ clip-path="url(#clipPath1748)"
+ style="opacity:0.69999701;fill:url(#linearGradient3955);fill-opacity:1">
+ <g
+ style="fill:url(#linearGradient3953);fill-opacity:1"
+ id="g3983">
+ <g
+ style="fill:url(#linearGradient3951);fill-opacity:1"
+ id="g3985"
+ clip-path="url(#clipPath1752)">
+ <g
+ style="fill:url(#linearGradient3949);fill-opacity:1"
+ id="g3987">
+ <g
+ style="fill:url(#linearGradient3947);fill-opacity:1"
+ id="g3989">
+ <g
+ style="fill:url(#linearGradient3945);fill-opacity:1"
+ id="g3991"
+ clip-path="url(#clipPath1756)" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/language.svg b/launcher/resources/multimc/scalable/language.svg
new file mode 100644
index 00000000..968e3538
--- /dev/null
+++ b/launcher/resources/multimc/scalable/language.svg
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ viewBox="0 0 128 128"
+ height="128"
+ width="128"
+ xml:space="preserve"
+ id="svg2"
+ version="1.1"
+ sodipodi:docname="language.svg"
+ inkscape:version="0.92.2 2405546, 2018-03-11"><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="3840"
+ inkscape:window-height="2123"
+ id="namedview30"
+ showgrid="false"
+ inkscape:zoom="1.84375"
+ inkscape:cx="325.0346"
+ inkscape:cy="134.81864"
+ inkscape:window-x="1200"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g888" /><metadata
+ id="metadata8"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs6" /><g
+ transform="matrix(1.3333333,0,0,-1.3333333,0,128.00004)"
+ id="g10"><g
+ transform="scale(0.1)"
+ id="g12"><g
+ transform="matrix(0.0334181,0,0,0.0334181,77.111273,13.149509)"
+ id="g888"><g
+ id="g14"
+ transform="scale(1.69573)"
+ style="fill:#0066cc;fill-opacity:1"><path
+ d="M 7103.55,14358.7 1603.03,16300 V 4328.22 l 5500.52,1779.59 v 8250.89"
+ style="fill:#0066cc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path16" /></g><g
+ id="g18"
+ transform="scale(1.69635)"><path
+ d="M 6968.96,14359.4 12678,16300 V 4332.6 L 6968.96,6111.53 v 8247.87"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path20" /></g><g
+ id="g22"
+ transform="scale(1.49453)"><path
+ d="M 200.466,2533.04 7910.06,5102.75 V 16300 L 200.466,13730.3 V 2533.04"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path24" /></g><g
+ id="g26"
+ transform="scale(1.20038)"
+ style="fill:#003399;fill-opacity:1"><path
+ d="M 14222.2,2943.15 15582.6,705.027 16300,2784.12 14222.2,2943.15"
+ style="fill:#003399;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path28" /></g><g
+ id="g30"
+ transform="scale(1.16442)"
+ style="fill:#003399;fill-opacity:1"><path
+ d="m 3621.88,15967.1 c -52.56,51.6 68.45,-421.7 236.85,-592 298.61,-301.3 531.85,-340.1 656.04,-345.1 274.81,-11 613.95,68.5 815.34,152.9 194.86,83.1 536.31,257.4 665.56,511.7 27.4,54.4 102.2,145.7 55.22,371.2 -35.64,173.5 -146.08,234.2 -280.74,224.6 -134.66,-9.1 -542.33,-117.8 -739.51,-178.5 -197.26,-59.8 -603.56,-183.5 -780.64,-221.9 -176.65,-38.3 -566.12,17.8 -628.12,77.1"
+ style="fill:#003399;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path32" /></g><g
+ id="g34"
+ transform="scale(1.0917)"
+ style="fill:#003399;fill-opacity:1"><path
+ d="m 9188.43,10995.1 c -83.18,30.2 -1803.98,743 -2047.91,859.8 -199.6,96 -689.02,302.9 -919.3,396.9 648.62,1000.1 1058.07,1754.8 1112.58,1869.8 100.85,210.3 787.39,1553.7 803.42,1636.4 15.57,83.8 35.08,393.4 19.97,467 -15.11,75 -266.83,-69.2 -608.59,-185.1 -342.31,-115.4 -992.86,-538.5 -1244.12,-591.5 -252.17,-52.6 -1058.07,-357.9 -1470.46,-494.7 -412.38,-136.9 -1192.45,-375 -1513.33,-461.6 -321.33,-86.7 -601.81,-93.5 -781.53,-148 0,0 23.91,-251.8 71.63,-327.2 47.18,-75.5 217.19,-260.5 414.86,-312.2 197.67,-52 524.87,-31.2 673.9,2.9 148.95,34.6 406.98,160.7 441.61,215.7 34.99,56 -18.05,228.4 40.85,280.5 59.45,51.6 844.83,235.2 1141.34,324.7 296.51,91.2 1431.53,482.1 1585.42,462.2 C 6860.04,14829 5947.06,13020.6 5653.02,12481.1 5358.89,11941.7 3650.37,9568.39 3286.62,9150.14 3010.54,8832.19 2341.49,8018.6 2109.74,7835.03 c 58.44,-16.12 472.75,19.42 548.23,66.14 470.36,289.73 1253.82,1265 1506.09,1562.06 749.84,879.37 1408.63,1803.07 1931.02,2595.77 h 0.56 c 101.76,-42.4 924.61,-712.8 1139.32,-861.4 214.71,-148.5 1062.01,-621.2 1245.58,-699.7 183.57,-79.4 889.07,-404.6 918.75,-294.5 29.68,111 -127.6,760.1 -210.86,791.7"
+ style="fill:#003399;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path36" /></g><g
+ id="g38"
+ transform="scale(1.12147)"
+ style="fill:#003399;fill-opacity:1"><path
+ d="m 5073.69,1979.54 c 160.5,-98.08 312.09,-178.34 481.51,-258.59 338.84,-169.42 722.26,-347.76 1087.85,-481.51 499.35,-187.25 998.69,-338.838 1498.03,-454.757 276.43,-62.418 579.6,-115.919 873.85,-160.504 26.76,0 820.35,-98.085 980.86,-98.085 h 802.51 c 312.1,26.751 606.4,44.584 918.4,89.169 249.7,35.667 526.1,80.251 793.6,142.669 196.2,44.584 401.3,89.169 597.5,151.587 187.2,53.501 401.2,124.831 606.3,196.171 133.8,44.58 276.4,107 419.1,160.5 115.9,53.5 258.6,115.92 392.3,169.42 160.6,71.34 347.8,169.42 526.1,258.59 142.7,71.34 303.2,160.5 454.8,249.67 115.9,62.42 383.4,267.51 526.1,267.51 160.5,0 267.5,-142.67 267.5,-267.51 0,-258.59 -347.8,-338.84 -508.3,-454.76 -169.4,-115.92 -374.5,-205.08 -552.8,-303.17 -356.7,-187.253 -722.3,-347.756 -1070,-481.509 -454.8,-169.42 -954.1,-329.923 -1400,-436.926 -169.4,-35.667 -338.8,-80.251 -508.2,-107.002 C 12171.5,142.67 11244.1,0 10985.6,0 H 9808.53 c -312.09,26.7505 -642.01,62.4179 -954.1,107.002 -276.42,44.584 -570.68,98.086 -847.1,160.503 -214,44.585 -445.84,107.003 -650.93,169.421 -356.67,98.085 -704.43,222.921 -1043.27,356.674 -615.26,231.84 -1257.28,535.01 -1863.62,936.27 -107,71.33 -115.92,142.67 -115.92,222.92 0,133.75 98.08,258.59 258.59,258.59 142.67,0 428.01,-205.09 481.51,-231.84"
+ style="fill:#003399;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path40" /></g><g
+ id="g42"
+ transform="scale(1.51227)"
+ style="fill:#003399;fill-opacity:1"><path
+ d="M 8014.44,16134.7 V 5025.56 c -6.61,-33.07 -19.84,-66.13 -46.29,-99.19 -13.22,-19.84 -39.67,-46.29 -59.51,-52.9 C 7743.33,4807.34 297.566,2307.79 198.377,2307.79 c -79.351,0 -152.089,52.9 -191.76442,138.86 0,6.62 -6.61258,13.23 -6.61258,26.45 v 11115.7 c 13.2252,33.1 19.8377,79.4 46.288,105.8 52.9006,72.8 145.477,86 204.99,105.8 112.414,39.7 7445.762,2499.6 7551.562,2499.6 66.13,0 211.6,-46.3 211.6,-165.3 z M 7611.07,5190.87 403.367,2790.51 V 13423.5 L 7611.07,15823.9 V 5190.87"
+ style="fill:#003399;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path44" /></g><g
+ id="g46"
+ transform="scale(1.71411)"
+ style="fill:#003399;fill-opacity:1"><path
+ d="M 12723.8,16113.3 V 4311.27 c -5.8,-134.18 -99.2,-192.52 -186.7,-192.52 -75.8,0 -624.2,186.69 -717.6,215.86 -735,227.52 -1475.9,455.05 -2205.18,682.57 -163.35,52.51 -332.54,105.01 -490.05,157.52 -140.02,40.83 -291.7,87.5 -431.71,134.18 -624.23,192.52 -1260.13,385.04 -1884.36,595.06 -23.34,5.83 -81.68,87.51 -81.68,105.01 v 8243.35 c 11.67,29.2 23.34,64.2 52.51,87.5 46.67,52.5 2047.71,717.6 2835.29,980.1 210.02,75.8 2841.08,980.1 2922.78,980.1 105,0 186.7,-75.8 186.7,-186.7 z M 12367.9,4532.96 7076.56,6178.13 V 14083.1 L 12367.9,15880 V 4532.96"
+ style="fill:#003399;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path48" /></g><g
+ id="g50"
+ transform="scale(1.48515)"
+ style="fill:#0066cc;fill-opacity:1"><path
+ d="M 16235.4,2378.81 8076.61,4979.28 8110.74,16300 16235.4,13714.1 V 2378.81"
+ style="fill:#0066cc;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path52" /></g><g
+ id="g54"
+ transform="scale(1.33098)"><path
+ d="m 12990.3,14581.8 1172.9,-355.3 2136.8,-7701.24 -1204.8,365.52 -432.8,1581.01 -2489.7,754.63 -535.4,-1287.92 -1205.1,365.6 z m 536.2,-2038.8 -893.6,-2159.8 1642.8,-497.94 -749.2,2657.74"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="path56" /></g></g></g></g></svg> \ No newline at end of file
diff --git a/launcher/resources/multimc/scalable/logo.svg b/launcher/resources/multimc/scalable/logo.svg
new file mode 100644
index 00000000..8bb0e289
--- /dev/null
+++ b/launcher/resources/multimc/scalable/logo.svg
@@ -0,0 +1,353 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="68.26667"
+ height="68.26667"
+ id="svg4427"
+ version="1.1"
+ inkscape:version="0.92.1 r"
+ sodipodi:docname="multimc-smooth-biginfinity.svg"
+ inkscape:export-filename="/home/peterix/playground/MultiMC-icons/multimc-smooth-biginfinity.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <defs
+ id="defs4429">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4809">
+ <stop
+ style="stop-color:#98c867;stop-opacity:1"
+ offset="0"
+ id="stop4805" />
+ <stop
+ style="stop-color:#5c9a33;stop-opacity:1"
+ offset="1"
+ id="stop4807" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5668"
+ inkscape:collect="always">
+ <stop
+ id="stop5670"
+ offset="0"
+ style="stop-color:#75b54b;stop-opacity:1;" />
+ <stop
+ id="stop5672"
+ offset="1"
+ style="stop-color:#75b54b;stop-opacity:0.6" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5084"
+ inkscape:collect="always">
+ <stop
+ id="stop5086"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:0.8" />
+ <stop
+ id="stop5088"
+ offset="1"
+ style="stop-color:#000000;stop-opacity:0.35" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient5072"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient5082"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3281"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3283"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3286"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2671525,0,0,0.89790119,-0.01941371,-0.00842234)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3288"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3290"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3293"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114"
+ gradientTransform="scale(1.2671525,0.89790119)" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient5580">
+ <stop
+ style="stop-color:#000000;stop-opacity:0.0627451"
+ offset="0"
+ id="stop5576" />
+ <stop
+ style="stop-color:#322217;stop-opacity:0.58823532"
+ offset="1"
+ id="stop5578" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3999"
+ inkscape:collect="always">
+ <stop
+ id="stop3995"
+ offset="0"
+ style="stop-color:#a3704b;stop-opacity:1" />
+ <stop
+ id="stop3997"
+ offset="1"
+ style="stop-color:#6a4a33;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient2727"
+ inkscape:collect="always">
+ <stop
+ id="stop2723"
+ offset="0"
+ style="stop-color:#966c4a;stop-opacity:1" />
+ <stop
+ id="stop2725"
+ offset="1"
+ style="stop-color:#593d29;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2727"
+ id="linearGradient2050"
+ gradientUnits="userSpaceOnUse"
+ x1="36.546478"
+ y1="33.80484"
+ x2="86.415741"
+ y2="97.065842" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3999"
+ id="radialGradient2052"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-9.105292e-4,-0.00104444)"
+ cx="34.133331"
+ cy="34.133335"
+ fx="34.133331"
+ fy="34.133335"
+ r="29.866665" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5580"
+ id="linearGradient2140"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.0010513,-9.083059e-4)"
+ x1="29.866674"
+ y1="29.867579"
+ x2="38.400005"
+ y2="38.400913" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient4790"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2671525,0,0,0.89790119,-0.82864077,-1.0012743)"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4809"
+ id="radialGradient4803"
+ cx="-42.66758"
+ cy="-34.134373"
+ fx="-42.66758"
+ fy="-34.134373"
+ r="34.132812"
+ gradientTransform="matrix(1.7500268,0.1250019,-0.01781176,0.24936465,95.393964,18.110151)"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.6203867"
+ inkscape:cx="52.171166"
+ inkscape:cy="11.292073"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:document-units="px"
+ inkscape:grid-bbox="true"
+ inkscape:window-width="1368"
+ inkscape:window-height="905"
+ inkscape:window-x="2452"
+ inkscape:window-y="723"
+ inkscape:window-maximized="0"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="false"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="false"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-paths="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:snap-center="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4446"
+ empspacing="16"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ spacingx="4.2666667"
+ spacingy="4.2666667"
+ originx="0"
+ originy="0" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata4432">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <g
+ id="g2048"
+ transform="translate(9.113e-4,0.00104183)">
+ <rect
+ rx="8.5333338"
+ ry="8.5333338"
+ style="fill:url(#linearGradient2050);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ id="rect2026"
+ width="68.26667"
+ height="68.26667"
+ x="-1.3322676e-15"
+ y="3.0270508e-06" />
+ <rect
+ rx="4.2666626"
+ y="4.2656283"
+ x="4.2657552"
+ height="59.733334"
+ width="59.73333"
+ id="rect2028"
+ style="fill:url(#radialGradient2052);fill-opacity:1;stroke:none;stroke-width:14.93333435"
+ ry="4.2666669" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4811"
+ d="m 4.2669272,4.2645856 -9.11e-4,8.5333334 h 4.267577 v 4.267579 h 8.5332038 v 4.265625 h 4.265625 V 8.5322946 H 25.6 v 8.5332034 h 4.265625 v -4.267579 h 4.267578 v 8.533204 h 4.265625 v -4.265625 h 4.267578 v 4.265625 h 4.267579 v -4.265625 h 4.265624 v -4.267579 h 4.267579 v 4.267579 h 8.533203 l -1.3e-4,-12.8009124 z"
+ style="opacity:0.6;fill:#593d29;fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccc" />
+ <path
+ style="fill:url(#radialGradient4803);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ d="m 8.5329442,-0.0018207 c -4.7274675,0 -8.5332035,3.805736 -8.5332035,8.533203 v 4.2675787 h 4.265625 V 8.5313823 c 0,-0.521698 0.105433,-1.01339 0.27539,-1.47461 -0.169616,0.460814 -0.27539,0.953462 -0.27539,1.47461 h 4.2675785 v 4.2675787 h 4.2656248 4.267578 v 4.265625 h 4.265625 V 12.798961 8.5313823 4.2657573 h 4.267578 v 4.265625 4.2675787 h 4.265625 V 8.5313823 h 4.267578 v 4.2675787 4.265625 h 4.265625 v -4.265625 h 4.267578 v 4.265625 h 4.267579 v -4.265625 h 4.265624 V 8.5313823 h 4.267579 v 4.2675787 h 4.265625 4.267578 V 8.5313823 h 4.265625 c 0,-4.727467 -3.805737,-8.533203 -8.533203,-8.533203 z m -3.019531,5.513671 c -0.318089,0.317888 -0.570428,0.695824 -0.7753915,1.101563 0.2048795,-0.405231 0.4576385,-0.784012 0.7753915,-1.101563 z"
+ id="path4794"
+ inkscape:connector-curvature="0" />
+ <path
+ style="opacity:1;fill:url(#linearGradient2140);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ d="m 8.5322887,-9.083059e-4 c -4.72747,0 -8.5332,3.8057359059 -8.5332,8.5332029059 V 59.731515 c 0,4.727467 3.80573,8.535156 8.5332,8.535156 H 59.731502 c 4.72747,0 8.5332,-3.807689 8.5332,-8.535156 V 8.5322946 c 0,-4.727467 -3.80573,-8.5332029059 -8.5332,-8.5332029059 z m 0,4.2675779059 H 59.731502 c 2.36373,0 4.26758,1.901892 4.26758,4.265625 V 59.731515 c 0,2.363733 -1.90385,4.267578 -4.26758,4.267578 H 8.5322887 c -2.36373,0 -4.26758,-1.903845 -4.26758,-4.267578 V 8.5322946 c 0,-2.363733 1.90385,-4.265625 4.26758,-4.265625 z"
+ id="path2046"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ id="g1092">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4786"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient4790);fill-opacity:1;stroke:none;stroke-width:1.06666672;opacity:0.5"
+ d="m 38.886673,44.940882 c -0.974277,-0.801673 -2.231353,-2.137814 -3.771231,-4.008427 -2.105641,2.672298 -4.085536,4.598569 -5.939688,5.778816 -2.325625,1.425227 -5.295467,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124578 -10.5594458,-3.37376 C 6.7526311,43.114834 5.275567,39.986037 5.2755773,36.088937 5.275567,32.347763 6.7526311,29.207831 9.7067742,26.669132 12.346618,24.419991 15.897857,23.295407 20.360501,23.295373 c 2.294138,3.4e-5 4.289747,0.334069 5.986829,1.002107 1.979863,0.73491 3.645488,1.737016 4.996881,3.00632 1.257039,1.135751 2.514115,2.471891 3.771231,4.008428 2.105563,-2.672257 4.085457,-4.598527 5.939689,-5.778816 2.325544,-1.425186 5.295385,-2.137794 8.909533,-2.137828 4.242577,3.4e-5 7.762388,1.124618 10.559447,3.37376 2.954063,2.360546 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477134,6.881147 -4.431197,9.419806 -2.639925,2.24918 -6.191163,3.373767 -10.653727,3.373758 -2.294219,9e-6 -4.289826,-0.334026 -5.98683,-1.002106 -1.697101,-0.601255 -3.362726,-1.603361 -4.996881,-3.006321 M 19.747676,44.473233 c 5.185412,1.1e-5 9.333763,-2.672271 12.445062,-8.016856 -3.991253,-5.834464 -8.139602,-8.751705 -12.445062,-8.751733 -3.142715,2.8e-5 -5.515446,0.801713 -7.118198,2.405057 -1.728498,1.71474 -2.592737,3.707818 -2.592722,5.979236 -1.5e-5,2.494152 0.864224,4.509499 2.592722,6.046046 1.759887,1.558846 4.132618,2.338261 7.118198,2.33825 M 50.483209,27.77145 c -4.682663,2.9e-5 -8.831013,2.672312 -12.445062,8.016856 3.959745,5.834503 8.108095,8.751746 12.445062,8.751733 3.142633,1.3e-5 5.515364,-0.801671 7.118198,-2.405056 1.728416,-1.714701 2.592656,-3.707778 2.592722,-5.979238 -6.6e-5,-2.49411 -0.864306,-4.509456 -2.592722,-6.046044 -1.759968,-1.558805 -4.132699,-2.338222 -7.118198,-2.338251" />
+ <path
+ d="m 39.715314,45.942156 c -0.974277,-0.801673 -2.231353,-2.137814 -3.771231,-4.008427 -2.105641,2.672298 -4.085536,4.598569 -5.939688,5.778816 -2.325625,1.425227 -5.295467,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124578 -10.559446,-3.37376 -2.9541431,-2.360505 -4.4312072,-5.489302 -4.4311969,-9.386402 -1.03e-5,-3.741174 1.4770538,-6.881106 4.4311969,-9.419805 2.639844,-2.249141 6.191083,-3.373725 10.653727,-3.373759 2.294138,3.4e-5 4.289747,0.334069 5.986829,1.002107 1.979863,0.73491 3.645488,1.737016 4.996881,3.00632 1.257039,1.135751 2.514115,2.471891 3.771231,4.008428 2.105563,-2.672257 4.085457,-4.598527 5.939689,-5.778816 2.325544,-1.425186 5.295385,-2.137794 8.909533,-2.137828 4.242577,3.4e-5 7.762388,1.124618 10.559447,3.37376 2.954063,2.360546 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477134,6.881147 -4.431197,9.419806 -2.639925,2.24918 -6.191163,3.373767 -10.653727,3.373758 -2.294219,9e-6 -4.289826,-0.334026 -5.98683,-1.002106 -1.697101,-0.601255 -3.362726,-1.603361 -4.996881,-3.006321 M 20.576317,45.474507 c 5.185412,1.1e-5 9.333763,-2.672271 12.445062,-8.016856 -3.991253,-5.834464 -8.139602,-8.751705 -12.445062,-8.751733 -3.142715,2.8e-5 -5.515446,0.801713 -7.118198,2.405057 -1.728498,1.71474 -2.592737,3.707818 -2.592722,5.979236 -1.5e-5,2.494152 0.864224,4.509499 2.592722,6.046046 1.759887,1.558846 4.132618,2.338261 7.118198,2.33825 M 51.31185,28.772724 c -4.682663,2.9e-5 -8.831013,2.672312 -12.445062,8.016856 3.959745,5.834503 8.108095,8.751746 12.445062,8.751733 3.142633,1.3e-5 5.515364,-0.801671 7.118198,-2.405056 1.728416,-1.714701 2.592656,-3.707778 2.592722,-5.979238 -6.6e-5,-2.49411 -0.864306,-4.509456 -2.592722,-6.046044 C 56.67008,29.55217 54.297349,28.772753 51.31185,28.772724"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3293);fill-opacity:1;stroke:none;stroke-width:1.06666672;opacity:0.5"
+ id="path3279"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 37.904564,42.951873 c -0.974278,-0.801672 -2.231352,-2.137814 -3.771231,-4.008428 -2.105642,2.672298 -4.085537,4.598568 -5.939688,5.778817 -2.325625,1.425227 -5.295466,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124577 -10.5594464,-3.37376 -2.9541428,-2.360505 -4.4312068,-5.489302 -4.4311963,-9.386401 -1.05e-5,-3.741175 1.4770535,-6.881107 4.4311963,-9.419805 2.6398444,-2.249142 6.1910824,-3.373727 10.6537284,-3.37376 2.294137,3.3e-5 4.289745,0.334068 5.986829,1.002107 1.979863,0.734909 3.645487,1.737016 4.99688,3.00632 1.257039,1.13575 2.514116,2.471891 3.771231,4.008428 2.105562,-2.672257 4.085456,-4.598528 5.939689,-5.778817 2.325544,-1.425185 5.295387,-2.137795 8.909534,-2.137828 4.242576,3.3e-5 7.762387,1.12462 10.559446,3.373761 2.954062,2.360545 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477135,6.881148 -4.431197,9.419805 -2.639924,2.249182 -6.191164,3.373767 -10.653728,3.37376 -2.294217,7e-6 -4.289826,-0.334028 -5.986828,-1.002107 -1.697101,-0.601254 -3.362727,-1.603361 -4.996882,-3.006321 m -19.138997,-0.46765 c 5.185412,1.3e-5 9.333762,-2.67227 12.445062,-8.016856 -3.991252,-5.834462 -8.139602,-8.751704 -12.445062,-8.751733 -3.142714,2.9e-5 -5.515444,0.801714 -7.118198,2.405056 -1.7284972,1.714743 -2.5927368,3.707819 -2.5927216,5.979239 -1.52e-5,2.49415 0.8642244,4.509496 2.5927216,6.046045 1.759888,1.558845 4.132618,2.338262 7.118198,2.338249 M 49.5011,25.782442 c -4.682663,2.8e-5 -8.831014,2.672311 -12.445063,8.016855 3.959745,5.834504 8.108096,8.751745 12.445063,8.751733 3.142634,1.2e-5 5.515365,-0.801673 7.118198,-2.405056 1.728417,-1.7147 2.592657,-3.707778 2.592721,-5.979238 -6.4e-5,-2.49411 -0.864304,-4.509456 -2.592721,-6.046046 C 54.85933,26.561886 52.486599,25.78247 49.5011,25.782442"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3286);fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ id="path3272"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="ccscsccccccccccccccccccccscscccccsccscccccc"
+ inkscape:connector-curvature="0"
+ id="text5100"
+ d="m 19.4,21.166667 c -4.462644,3.3e-5 -8.026822,1.150858 -10.6666667,3.4 -2.9541428,2.538698 -4.4333436,5.658825 -4.4333333,9.4 -1.03e-5,3.897098 1.4791905,7.039495 4.4333333,9.4 -1.622701,-2.044271 -2.433341,-4.51168 -2.4333333,-7.4 -1.03e-5,-3.741175 1.4791905,-6.861302 4.433333,-9.4 2.639845,-2.249142 6.204023,-3.399967 10.666667,-3.4 2.294138,3.3e-5 4.302916,0.365295 6,1.033333 1.979862,0.73491 3.615274,1.730695 4.966667,3 0.06836,0.06177 0.131637,0.137049 0.2,0.2 -0.731813,-0.797005 -1.468213,-1.538822 -2.2,-2.2 -1.351393,-1.269305 -2.986805,-2.26509 -4.966667,-3 -1.697084,-0.668038 -3.705862,-1.0333 -6,-1.033333 z m 29.6,0.1 c -3.614148,3.3e-5 -6.574457,0.74148 -8.9,2.166666 -1.818222,1.157367 -3.923451,3.291388 -5.983333,5.883334 0.618278,0.658774 1.248369,1.377605 1.866666,2.133333 2.105562,-2.672257 4.262434,-4.836378 6.116667,-6.016667 2.325543,-1.425186 5.285852,-2.166633 8.9,-2.166666 4.242576,3.3e-5 7.769607,1.150858 10.566667,3.4 -0.570388,-0.722129 -1.227721,-1.382884 -2,-2 C 56.769607,22.417525 53.242576,21.2667 49,21.266667 Z m 8.866667,8.1 c 0.9092,1.305235 1.366619,2.857751 1.366666,4.666666 -6.5e-5,2.271461 -0.871584,4.285301 -2.6,6 -1.602834,1.603384 -3.957366,2.400012 -7.1,2.4 -2.653707,8e-6 -5.320858,-1.032242 -7.833333,-3.216666 3.136636,3.509305 6.469807,5.216676 9.833333,5.216666 3.142634,1.2e-5 5.497166,-0.796616 7.1,-2.4 1.728416,-1.714699 2.599935,-3.728539 2.6,-6 -6.5e-5,-2.49411 -0.871584,-4.496744 -2.6,-6.033333 -0.24943,-0.220921 -0.49262,-0.443723 -0.766666,-0.633333 z m -26.633334,4.966666 c -3.1113,5.344585 -7.247921,8.033345 -12.433333,8.033334 -2.58055,1e-5 -4.543473,-0.352086 -6.208333,-1.516667 0.348871,0.50642 0.590094,0.752276 1.075,1.183333 1.759888,1.558846 4.147753,2.333345 7.133333,2.333334 5.185412,1.1e-5 9.322033,-2.688749 12.433333,-8.033334 z m 4.933334,6.5 c -0.04103,0.05207 -0.09239,0.08182 -0.133334,0.133334 0.687326,0.744419 1.306949,1.359747 1.833334,1.8 -0.529404,-0.580895 -1.078447,-1.178283 -1.7,-1.933334 z"
+ style="font-style:normal;font-weight:normal;font-size:76.18933868px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:0.3;fill:#ccff00;fill-opacity:1;stroke:none;stroke-width:1.06666672" />
+ <path
+ sodipodi:nodetypes="ccsccscccccccccccccccccccscscccccsccsccccccc"
+ id="text5058-0"
+ d="m 19.730474,21.54714 c -4.462645,3.3e-5 -8.026823,1.150859 -10.6666669,3.4 -2.9541429,2.538699 -4.433344,5.658826 -4.4333333,9.4 -1.07e-5,3.897099 1.4791904,7.039495 4.4333333,9.4 0.042837,0.03444 0.090155,0.06608 0.1333334,0.1 -2.2392086,-2.228193 -3.3666752,-5.040417 -3.3666667,-8.433333 -1.07e-5,-3.741174 1.4791904,-6.861301 4.4333332,-9.4 2.639844,-2.249141 6.204022,-3.399967 10.666667,-3.4 2.294137,3.3e-5 4.302916,0.365295 6,1.033333 1.870874,0.694455 3.42364,1.628367 4.733333,2.8 -0.314265,-0.308986 -0.652406,-0.582729 -0.966667,-0.866666 -1.351393,-1.269305 -2.986804,-2.265091 -4.966666,-3 -1.697084,-0.668039 -3.705863,-1.033301 -6,-1.033334 z m 29.6,0.1 c -3.614149,3.3e-5 -6.574457,0.741481 -8.9,2.166667 -1.813279,1.154221 -3.963039,3.235656 -6.016667,5.816667 0.355649,0.402628 0.711011,0.798625 1.066667,1.233333 2.105561,-2.672257 4.295767,-4.803044 6.15,-5.983333 2.325543,-1.425187 5.285851,-2.166634 8.9,-2.166667 4.22442,3.3e-5 7.742084,1.136734 10.533333,3.366667 -0.36096,-0.367566 -0.745726,-0.696967 -1.166667,-1.033334 -2.797059,-2.249141 -6.32409,-3.399967 -10.566666,-3.4 z m 8.233333,7.333334 c 1.323326,1.449243 1.999942,3.250987 2,5.433333 -6.5e-5,2.27146 -0.871584,4.2853 -2.6,6 -1.602834,1.603383 -3.957366,2.400012 -7.1,2.4 -2.406328,6e-6 -4.776468,-0.90386 -7.066667,-2.7 2.669147,2.483838 5.436929,3.766674 8.266667,3.766667 3.142634,1.1e-5 5.497166,-0.796617 7.1,-2.4 1.728416,-1.7147 2.599935,-3.72854 2.6,-6 -6.5e-5,-2.49411 -0.871584,-4.496745 -2.6,-6.033334 -0.185641,-0.164422 -0.400724,-0.319587 -0.6,-0.466666 z m -26,5.733333 c -3.1113,5.344584 -7.247921,8.033345 -12.433333,8.033333 -2.612382,1.1e-5 -4.759372,-0.60651 -6.433334,-1.8 0.166027,0.176488 0.313947,0.367942 0.5,0.533334 1.759888,1.558845 4.147754,2.333345 7.133334,2.333333 5.185412,1.2e-5 9.322033,-2.688749 12.433333,-8.033333 z m 4.133333,5.566667 c -0.04657,0.05909 -0.08689,0.108298 -0.133333,0.166666 1.038571,1.18897 1.9748,2.169945 2.7,2.766667 0.06249,0.05364 0.137426,0.08086 0.2,0.133333 -0.792178,-0.781249 -1.706288,-1.778539 -2.766667,-3.066666 z"
+ style="font-style:normal;font-weight:normal;font-size:76.18933868px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:0.6;fill:#ccff00;fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/multimc.svg b/launcher/resources/multimc/scalable/multimc.svg
new file mode 100644
index 00000000..8bb0e289
--- /dev/null
+++ b/launcher/resources/multimc/scalable/multimc.svg
@@ -0,0 +1,353 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="68.26667"
+ height="68.26667"
+ id="svg4427"
+ version="1.1"
+ inkscape:version="0.92.1 r"
+ sodipodi:docname="multimc-smooth-biginfinity.svg"
+ inkscape:export-filename="/home/peterix/playground/MultiMC-icons/multimc-smooth-biginfinity.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <defs
+ id="defs4429">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4809">
+ <stop
+ style="stop-color:#98c867;stop-opacity:1"
+ offset="0"
+ id="stop4805" />
+ <stop
+ style="stop-color:#5c9a33;stop-opacity:1"
+ offset="1"
+ id="stop4807" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5668"
+ inkscape:collect="always">
+ <stop
+ id="stop5670"
+ offset="0"
+ style="stop-color:#75b54b;stop-opacity:1;" />
+ <stop
+ id="stop5672"
+ offset="1"
+ style="stop-color:#75b54b;stop-opacity:0.6" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5084"
+ inkscape:collect="always">
+ <stop
+ id="stop5086"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:0.8" />
+ <stop
+ id="stop5088"
+ offset="1"
+ style="stop-color:#000000;stop-opacity:0.35" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient5072"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient5082"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3281"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3283"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5668"
+ id="linearGradient3286"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2671525,0,0,0.89790119,-0.01941371,-0.00842234)"
+ x1="6.7342591"
+ y1="28.510933"
+ x2="50.506943"
+ y2="61.773685" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3288"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3290"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient3293"
+ gradientUnits="userSpaceOnUse"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114"
+ gradientTransform="scale(1.2671525,0.89790119)" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient5580">
+ <stop
+ style="stop-color:#000000;stop-opacity:0.0627451"
+ offset="0"
+ id="stop5576" />
+ <stop
+ style="stop-color:#322217;stop-opacity:0.58823532"
+ offset="1"
+ id="stop5578" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3999"
+ inkscape:collect="always">
+ <stop
+ id="stop3995"
+ offset="0"
+ style="stop-color:#a3704b;stop-opacity:1" />
+ <stop
+ id="stop3997"
+ offset="1"
+ style="stop-color:#6a4a33;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient2727"
+ inkscape:collect="always">
+ <stop
+ id="stop2723"
+ offset="0"
+ style="stop-color:#966c4a;stop-opacity:1" />
+ <stop
+ id="stop2725"
+ offset="1"
+ style="stop-color:#593d29;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2727"
+ id="linearGradient2050"
+ gradientUnits="userSpaceOnUse"
+ x1="36.546478"
+ y1="33.80484"
+ x2="86.415741"
+ y2="97.065842" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3999"
+ id="radialGradient2052"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-9.105292e-4,-0.00104444)"
+ cx="34.133331"
+ cy="34.133335"
+ fx="34.133331"
+ fy="34.133335"
+ r="29.866665" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5580"
+ id="linearGradient2140"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-0.0010513,-9.083059e-4)"
+ x1="29.866674"
+ y1="29.867579"
+ x2="38.400005"
+ y2="38.400913" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5084"
+ id="linearGradient4790"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2671525,0,0,0.89790119,-0.82864077,-1.0012743)"
+ x1="14.312115"
+ y1="9.7948904"
+ x2="44.097023"
+ y2="82.973114" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4809"
+ id="radialGradient4803"
+ cx="-42.66758"
+ cy="-34.134373"
+ fx="-42.66758"
+ fy="-34.134373"
+ r="34.132812"
+ gradientTransform="matrix(1.7500268,0.1250019,-0.01781176,0.24936465,95.393964,18.110151)"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.6203867"
+ inkscape:cx="52.171166"
+ inkscape:cy="11.292073"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:document-units="px"
+ inkscape:grid-bbox="true"
+ inkscape:window-width="1368"
+ inkscape:window-height="905"
+ inkscape:window-x="2452"
+ inkscape:window-y="723"
+ inkscape:window-maximized="0"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="false"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="false"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-paths="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:snap-center="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4446"
+ empspacing="16"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ spacingx="4.2666667"
+ spacingy="4.2666667"
+ originx="0"
+ originy="0" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata4432">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <g
+ id="g2048"
+ transform="translate(9.113e-4,0.00104183)">
+ <rect
+ rx="8.5333338"
+ ry="8.5333338"
+ style="fill:url(#linearGradient2050);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ id="rect2026"
+ width="68.26667"
+ height="68.26667"
+ x="-1.3322676e-15"
+ y="3.0270508e-06" />
+ <rect
+ rx="4.2666626"
+ y="4.2656283"
+ x="4.2657552"
+ height="59.733334"
+ width="59.73333"
+ id="rect2028"
+ style="fill:url(#radialGradient2052);fill-opacity:1;stroke:none;stroke-width:14.93333435"
+ ry="4.2666669" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4811"
+ d="m 4.2669272,4.2645856 -9.11e-4,8.5333334 h 4.267577 v 4.267579 h 8.5332038 v 4.265625 h 4.265625 V 8.5322946 H 25.6 v 8.5332034 h 4.265625 v -4.267579 h 4.267578 v 8.533204 h 4.265625 v -4.265625 h 4.267578 v 4.265625 h 4.267579 v -4.265625 h 4.265624 v -4.267579 h 4.267579 v 4.267579 h 8.533203 l -1.3e-4,-12.8009124 z"
+ style="opacity:0.6;fill:#593d29;fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccc" />
+ <path
+ style="fill:url(#radialGradient4803);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ d="m 8.5329442,-0.0018207 c -4.7274675,0 -8.5332035,3.805736 -8.5332035,8.533203 v 4.2675787 h 4.265625 V 8.5313823 c 0,-0.521698 0.105433,-1.01339 0.27539,-1.47461 -0.169616,0.460814 -0.27539,0.953462 -0.27539,1.47461 h 4.2675785 v 4.2675787 h 4.2656248 4.267578 v 4.265625 h 4.265625 V 12.798961 8.5313823 4.2657573 h 4.267578 v 4.265625 4.2675787 h 4.265625 V 8.5313823 h 4.267578 v 4.2675787 4.265625 h 4.265625 v -4.265625 h 4.267578 v 4.265625 h 4.267579 v -4.265625 h 4.265624 V 8.5313823 h 4.267579 v 4.2675787 h 4.265625 4.267578 V 8.5313823 h 4.265625 c 0,-4.727467 -3.805737,-8.533203 -8.533203,-8.533203 z m -3.019531,5.513671 c -0.318089,0.317888 -0.570428,0.695824 -0.7753915,1.101563 0.2048795,-0.405231 0.4576385,-0.784012 0.7753915,-1.101563 z"
+ id="path4794"
+ inkscape:connector-curvature="0" />
+ <path
+ style="opacity:1;fill:url(#linearGradient2140);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ d="m 8.5322887,-9.083059e-4 c -4.72747,0 -8.5332,3.8057359059 -8.5332,8.5332029059 V 59.731515 c 0,4.727467 3.80573,8.535156 8.5332,8.535156 H 59.731502 c 4.72747,0 8.5332,-3.807689 8.5332,-8.535156 V 8.5322946 c 0,-4.727467 -3.80573,-8.5332029059 -8.5332,-8.5332029059 z m 0,4.2675779059 H 59.731502 c 2.36373,0 4.26758,1.901892 4.26758,4.265625 V 59.731515 c 0,2.363733 -1.90385,4.267578 -4.26758,4.267578 H 8.5322887 c -2.36373,0 -4.26758,-1.903845 -4.26758,-4.267578 V 8.5322946 c 0,-2.363733 1.90385,-4.265625 4.26758,-4.265625 z"
+ id="path2046"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ id="g1092">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4786"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient4790);fill-opacity:1;stroke:none;stroke-width:1.06666672;opacity:0.5"
+ d="m 38.886673,44.940882 c -0.974277,-0.801673 -2.231353,-2.137814 -3.771231,-4.008427 -2.105641,2.672298 -4.085536,4.598569 -5.939688,5.778816 -2.325625,1.425227 -5.295467,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124578 -10.5594458,-3.37376 C 6.7526311,43.114834 5.275567,39.986037 5.2755773,36.088937 5.275567,32.347763 6.7526311,29.207831 9.7067742,26.669132 12.346618,24.419991 15.897857,23.295407 20.360501,23.295373 c 2.294138,3.4e-5 4.289747,0.334069 5.986829,1.002107 1.979863,0.73491 3.645488,1.737016 4.996881,3.00632 1.257039,1.135751 2.514115,2.471891 3.771231,4.008428 2.105563,-2.672257 4.085457,-4.598527 5.939689,-5.778816 2.325544,-1.425186 5.295385,-2.137794 8.909533,-2.137828 4.242577,3.4e-5 7.762388,1.124618 10.559447,3.37376 2.954063,2.360546 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477134,6.881147 -4.431197,9.419806 -2.639925,2.24918 -6.191163,3.373767 -10.653727,3.373758 -2.294219,9e-6 -4.289826,-0.334026 -5.98683,-1.002106 -1.697101,-0.601255 -3.362726,-1.603361 -4.996881,-3.006321 M 19.747676,44.473233 c 5.185412,1.1e-5 9.333763,-2.672271 12.445062,-8.016856 -3.991253,-5.834464 -8.139602,-8.751705 -12.445062,-8.751733 -3.142715,2.8e-5 -5.515446,0.801713 -7.118198,2.405057 -1.728498,1.71474 -2.592737,3.707818 -2.592722,5.979236 -1.5e-5,2.494152 0.864224,4.509499 2.592722,6.046046 1.759887,1.558846 4.132618,2.338261 7.118198,2.33825 M 50.483209,27.77145 c -4.682663,2.9e-5 -8.831013,2.672312 -12.445062,8.016856 3.959745,5.834503 8.108095,8.751746 12.445062,8.751733 3.142633,1.3e-5 5.515364,-0.801671 7.118198,-2.405056 1.728416,-1.714701 2.592656,-3.707778 2.592722,-5.979238 -6.6e-5,-2.49411 -0.864306,-4.509456 -2.592722,-6.046044 -1.759968,-1.558805 -4.132699,-2.338222 -7.118198,-2.338251" />
+ <path
+ d="m 39.715314,45.942156 c -0.974277,-0.801673 -2.231353,-2.137814 -3.771231,-4.008427 -2.105641,2.672298 -4.085536,4.598569 -5.939688,5.778816 -2.325625,1.425227 -5.295467,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124578 -10.559446,-3.37376 -2.9541431,-2.360505 -4.4312072,-5.489302 -4.4311969,-9.386402 -1.03e-5,-3.741174 1.4770538,-6.881106 4.4311969,-9.419805 2.639844,-2.249141 6.191083,-3.373725 10.653727,-3.373759 2.294138,3.4e-5 4.289747,0.334069 5.986829,1.002107 1.979863,0.73491 3.645488,1.737016 4.996881,3.00632 1.257039,1.135751 2.514115,2.471891 3.771231,4.008428 2.105563,-2.672257 4.085457,-4.598527 5.939689,-5.778816 2.325544,-1.425186 5.295385,-2.137794 8.909533,-2.137828 4.242577,3.4e-5 7.762388,1.124618 10.559447,3.37376 2.954063,2.360546 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477134,6.881147 -4.431197,9.419806 -2.639925,2.24918 -6.191163,3.373767 -10.653727,3.373758 -2.294219,9e-6 -4.289826,-0.334026 -5.98683,-1.002106 -1.697101,-0.601255 -3.362726,-1.603361 -4.996881,-3.006321 M 20.576317,45.474507 c 5.185412,1.1e-5 9.333763,-2.672271 12.445062,-8.016856 -3.991253,-5.834464 -8.139602,-8.751705 -12.445062,-8.751733 -3.142715,2.8e-5 -5.515446,0.801713 -7.118198,2.405057 -1.728498,1.71474 -2.592737,3.707818 -2.592722,5.979236 -1.5e-5,2.494152 0.864224,4.509499 2.592722,6.046046 1.759887,1.558846 4.132618,2.338261 7.118198,2.33825 M 51.31185,28.772724 c -4.682663,2.9e-5 -8.831013,2.672312 -12.445062,8.016856 3.959745,5.834503 8.108095,8.751746 12.445062,8.751733 3.142633,1.3e-5 5.515364,-0.801671 7.118198,-2.405056 1.728416,-1.714701 2.592656,-3.707778 2.592722,-5.979238 -6.6e-5,-2.49411 -0.864306,-4.509456 -2.592722,-6.046044 C 56.67008,29.55217 54.297349,28.772753 51.31185,28.772724"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3293);fill-opacity:1;stroke:none;stroke-width:1.06666672;opacity:0.5"
+ id="path3279"
+ inkscape:connector-curvature="0" />
+ <path
+ d="m 37.904564,42.951873 c -0.974278,-0.801672 -2.231352,-2.137814 -3.771231,-4.008428 -2.105642,2.672298 -4.085537,4.598568 -5.939688,5.778817 -2.325625,1.425227 -5.295466,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124577 -10.5594464,-3.37376 -2.9541428,-2.360505 -4.4312068,-5.489302 -4.4311963,-9.386401 -1.05e-5,-3.741175 1.4770535,-6.881107 4.4311963,-9.419805 2.6398444,-2.249142 6.1910824,-3.373727 10.6537284,-3.37376 2.294137,3.3e-5 4.289745,0.334068 5.986829,1.002107 1.979863,0.734909 3.645487,1.737016 4.99688,3.00632 1.257039,1.13575 2.514116,2.471891 3.771231,4.008428 2.105562,-2.672257 4.085456,-4.598528 5.939689,-5.778817 2.325544,-1.425185 5.295387,-2.137795 8.909534,-2.137828 4.242576,3.3e-5 7.762387,1.12462 10.559446,3.373761 2.954062,2.360545 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477135,6.881148 -4.431197,9.419805 -2.639924,2.249182 -6.191164,3.373767 -10.653728,3.37376 -2.294217,7e-6 -4.289826,-0.334028 -5.986828,-1.002107 -1.697101,-0.601254 -3.362727,-1.603361 -4.996882,-3.006321 m -19.138997,-0.46765 c 5.185412,1.3e-5 9.333762,-2.67227 12.445062,-8.016856 -3.991252,-5.834462 -8.139602,-8.751704 -12.445062,-8.751733 -3.142714,2.9e-5 -5.515444,0.801714 -7.118198,2.405056 -1.7284972,1.714743 -2.5927368,3.707819 -2.5927216,5.979239 -1.52e-5,2.49415 0.8642244,4.509496 2.5927216,6.046045 1.759888,1.558845 4.132618,2.338262 7.118198,2.338249 M 49.5011,25.782442 c -4.682663,2.8e-5 -8.831014,2.672311 -12.445063,8.016855 3.959745,5.834504 8.108096,8.751745 12.445063,8.751733 3.142634,1.2e-5 5.515365,-0.801673 7.118198,-2.405056 1.728417,-1.7147 2.592657,-3.707778 2.592721,-5.979238 -6.4e-5,-2.49411 -0.864304,-4.509456 -2.592721,-6.046046 C 54.85933,26.561886 52.486599,25.78247 49.5011,25.782442"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3286);fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ id="path3272"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="ccscsccccccccccccccccccccscscccccsccscccccc"
+ inkscape:connector-curvature="0"
+ id="text5100"
+ d="m 19.4,21.166667 c -4.462644,3.3e-5 -8.026822,1.150858 -10.6666667,3.4 -2.9541428,2.538698 -4.4333436,5.658825 -4.4333333,9.4 -1.03e-5,3.897098 1.4791905,7.039495 4.4333333,9.4 -1.622701,-2.044271 -2.433341,-4.51168 -2.4333333,-7.4 -1.03e-5,-3.741175 1.4791905,-6.861302 4.433333,-9.4 2.639845,-2.249142 6.204023,-3.399967 10.666667,-3.4 2.294138,3.3e-5 4.302916,0.365295 6,1.033333 1.979862,0.73491 3.615274,1.730695 4.966667,3 0.06836,0.06177 0.131637,0.137049 0.2,0.2 -0.731813,-0.797005 -1.468213,-1.538822 -2.2,-2.2 -1.351393,-1.269305 -2.986805,-2.26509 -4.966667,-3 -1.697084,-0.668038 -3.705862,-1.0333 -6,-1.033333 z m 29.6,0.1 c -3.614148,3.3e-5 -6.574457,0.74148 -8.9,2.166666 -1.818222,1.157367 -3.923451,3.291388 -5.983333,5.883334 0.618278,0.658774 1.248369,1.377605 1.866666,2.133333 2.105562,-2.672257 4.262434,-4.836378 6.116667,-6.016667 2.325543,-1.425186 5.285852,-2.166633 8.9,-2.166666 4.242576,3.3e-5 7.769607,1.150858 10.566667,3.4 -0.570388,-0.722129 -1.227721,-1.382884 -2,-2 C 56.769607,22.417525 53.242576,21.2667 49,21.266667 Z m 8.866667,8.1 c 0.9092,1.305235 1.366619,2.857751 1.366666,4.666666 -6.5e-5,2.271461 -0.871584,4.285301 -2.6,6 -1.602834,1.603384 -3.957366,2.400012 -7.1,2.4 -2.653707,8e-6 -5.320858,-1.032242 -7.833333,-3.216666 3.136636,3.509305 6.469807,5.216676 9.833333,5.216666 3.142634,1.2e-5 5.497166,-0.796616 7.1,-2.4 1.728416,-1.714699 2.599935,-3.728539 2.6,-6 -6.5e-5,-2.49411 -0.871584,-4.496744 -2.6,-6.033333 -0.24943,-0.220921 -0.49262,-0.443723 -0.766666,-0.633333 z m -26.633334,4.966666 c -3.1113,5.344585 -7.247921,8.033345 -12.433333,8.033334 -2.58055,1e-5 -4.543473,-0.352086 -6.208333,-1.516667 0.348871,0.50642 0.590094,0.752276 1.075,1.183333 1.759888,1.558846 4.147753,2.333345 7.133333,2.333334 5.185412,1.1e-5 9.322033,-2.688749 12.433333,-8.033334 z m 4.933334,6.5 c -0.04103,0.05207 -0.09239,0.08182 -0.133334,0.133334 0.687326,0.744419 1.306949,1.359747 1.833334,1.8 -0.529404,-0.580895 -1.078447,-1.178283 -1.7,-1.933334 z"
+ style="font-style:normal;font-weight:normal;font-size:76.18933868px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:0.3;fill:#ccff00;fill-opacity:1;stroke:none;stroke-width:1.06666672" />
+ <path
+ sodipodi:nodetypes="ccsccscccccccccccccccccccscscccccsccsccccccc"
+ id="text5058-0"
+ d="m 19.730474,21.54714 c -4.462645,3.3e-5 -8.026823,1.150859 -10.6666669,3.4 -2.9541429,2.538699 -4.433344,5.658826 -4.4333333,9.4 -1.07e-5,3.897099 1.4791904,7.039495 4.4333333,9.4 0.042837,0.03444 0.090155,0.06608 0.1333334,0.1 -2.2392086,-2.228193 -3.3666752,-5.040417 -3.3666667,-8.433333 -1.07e-5,-3.741174 1.4791904,-6.861301 4.4333332,-9.4 2.639844,-2.249141 6.204022,-3.399967 10.666667,-3.4 2.294137,3.3e-5 4.302916,0.365295 6,1.033333 1.870874,0.694455 3.42364,1.628367 4.733333,2.8 -0.314265,-0.308986 -0.652406,-0.582729 -0.966667,-0.866666 -1.351393,-1.269305 -2.986804,-2.265091 -4.966666,-3 -1.697084,-0.668039 -3.705863,-1.033301 -6,-1.033334 z m 29.6,0.1 c -3.614149,3.3e-5 -6.574457,0.741481 -8.9,2.166667 -1.813279,1.154221 -3.963039,3.235656 -6.016667,5.816667 0.355649,0.402628 0.711011,0.798625 1.066667,1.233333 2.105561,-2.672257 4.295767,-4.803044 6.15,-5.983333 2.325543,-1.425187 5.285851,-2.166634 8.9,-2.166667 4.22442,3.3e-5 7.742084,1.136734 10.533333,3.366667 -0.36096,-0.367566 -0.745726,-0.696967 -1.166667,-1.033334 -2.797059,-2.249141 -6.32409,-3.399967 -10.566666,-3.4 z m 8.233333,7.333334 c 1.323326,1.449243 1.999942,3.250987 2,5.433333 -6.5e-5,2.27146 -0.871584,4.2853 -2.6,6 -1.602834,1.603383 -3.957366,2.400012 -7.1,2.4 -2.406328,6e-6 -4.776468,-0.90386 -7.066667,-2.7 2.669147,2.483838 5.436929,3.766674 8.266667,3.766667 3.142634,1.1e-5 5.497166,-0.796617 7.1,-2.4 1.728416,-1.7147 2.599935,-3.72854 2.6,-6 -6.5e-5,-2.49411 -0.871584,-4.496745 -2.6,-6.033334 -0.185641,-0.164422 -0.400724,-0.319587 -0.6,-0.466666 z m -26,5.733333 c -3.1113,5.344584 -7.247921,8.033345 -12.433333,8.033333 -2.612382,1.1e-5 -4.759372,-0.60651 -6.433334,-1.8 0.166027,0.176488 0.313947,0.367942 0.5,0.533334 1.759888,1.558845 4.147754,2.333345 7.133334,2.333333 5.185412,1.2e-5 9.322033,-2.688749 12.433333,-8.033333 z m 4.133333,5.566667 c -0.04657,0.05909 -0.08689,0.108298 -0.133333,0.166666 1.038571,1.18897 1.9748,2.169945 2.7,2.766667 0.06249,0.05364 0.137426,0.08086 0.2,0.133333 -0.792178,-0.781249 -1.706288,-1.778539 -2.766667,-3.066666 z"
+ style="font-style:normal;font-weight:normal;font-size:76.18933868px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:0.6;fill:#ccff00;fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/new.svg b/launcher/resources/multimc/scalable/new.svg
new file mode 100644
index 00000000..c9cff358
--- /dev/null
+++ b/launcher/resources/multimc/scalable/new.svg
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) --><svg height="48" id="svg249" inkscape:export-filename="/home/luca/Desktop/flare-16.png" inkscape:export-xdpi="30" inkscape:export-ydpi="30" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.45+0.46pre0" sodipodi:docbase="/home/dobey/Projects/gnome-icon-theme/scalable/actions" sodipodi:docname="document-new.svg" sodipodi:version="0.32" version="1.0" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <metadata>
+ <rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <cc:Work rdf:about="">
+ <dc:title>New Document</dc:title>
+ <dc:description></dc:description>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>regular</rdf:li>
+ <rdf:li>plaintext</rdf:li>
+ <rdf:li>text</rdf:li>
+ <rdf:li>document</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <dc:publisher>
+ <cc:Agent rdf:about="http://www.openclipart.org/">
+ <dc:title>Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Jakub Steiner</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title>Jakub Steiner</dc:title>
+ </cc:Agent>
+ </dc:rights>
+ <dc:date></dc:date>
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <cc:license rdf:resource="http://creativecommons.org/licenses/GPL/2.0/"/>
+ <dc:language>en</dc:language>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs id="defs3">
+ <linearGradient id="linearGradient3656" inkscape:collect="always">
+ <stop id="stop3658" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3660" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient4253" inkscape:collect="always" x1="-26.753757" x2="-24.75" xlink:href="#linearGradient3656" y1="11.566258" y2="9.687501"/>
+ <linearGradient id="linearGradient3520" inkscape:collect="always">
+ <stop id="stop3522" offset="0" style="stop-color:#000000;stop-opacity:0.41295547"/>
+ <stop id="stop3524" offset="1" style="stop-color:#000000;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(0.9223058,0,0,0.9185751,-92.447368,1.3256997)" gradientUnits="userSpaceOnUse" id="linearGradient4273" inkscape:collect="always" x1="-18.588562" x2="-28.789402" xlink:href="#linearGradient3520" y1="11.052948" y2="14.069944"/>
+ <linearGradient id="linearGradient3671">
+ <stop id="stop3673" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3691" offset="0.47533694" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3675" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="-26.305403" cy="10.108011" fx="-26.305403" fy="10.108011" gradientTransform="matrix(0.4073362,-0.2798276,0.7510293,1.0932492,-115.18484,-8.4378699)" gradientUnits="userSpaceOnUse" id="radialGradient4276" inkscape:collect="always" r="7.0421038" xlink:href="#linearGradient3671"/>
+ <linearGradient id="linearGradient3741" inkscape:collect="always">
+ <stop id="stop3743" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop3745" offset="1" style="stop-color:#ffffff;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="4" cy="5.2999997" fx="4" fy="5.2999997" gradientTransform="matrix(1.8860258,0,0,1.1764706,-3.5441033,-4.2352941)" gradientUnits="userSpaceOnUse" id="radialGradient4247" inkscape:collect="always" r="17" xlink:href="#linearGradient3741"/>
+ <linearGradient id="linearGradient3613" inkscape:collect="always">
+ <stop id="stop3615" offset="0" style="stop-color:#888a85;stop-opacity:1"/>
+ <stop id="stop3617" offset="1" style="stop-color:#babdb6;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient gradientTransform="translate(-90,0)" gradientUnits="userSpaceOnUse" id="linearGradient4282" inkscape:collect="always" x1="-47.5" x2="-62.75" xlink:href="#linearGradient3613" y1="49.020683" y2="-22.502075"/>
+ <linearGradient id="linearGradient3683">
+ <stop id="stop3685" offset="0" style="stop-color:#f6f6f5;stop-opacity:1;"/>
+ <stop id="stop3689" offset="1" style="stop-color:#d3d7cf;stop-opacity:1"/>
+ </linearGradient>
+ <radialGradient cx="-30.249996" cy="35.357208" fx="-30.249996" fy="35.357208" gradientTransform="matrix(3.9957492,0,0,1.9350367,0.62141,-31.167422)" gradientUnits="userSpaceOnUse" id="radialGradient4280" inkscape:collect="always" r="18.000002" xlink:href="#linearGradient3683"/>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient4241" inkscape:collect="always" x1="25.058096" x2="25.058096" xlink:href="#linearGradient3702" y1="47.027729" y2="39.999443"/>
+ <radialGradient cx="4.9929786" cy="43.5" fx="4.9929786" fy="43.5" gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)" gradientUnits="userSpaceOnUse" id="radialGradient4239" inkscape:collect="always" r="2.5" xlink:href="#linearGradient3688"/>
+ <radialGradient cx="4.9929786" cy="43.5" fx="4.9929786" fy="43.5" gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)" gradientUnits="userSpaceOnUse" id="radialGradient4237" inkscape:collect="always" r="2.5" xlink:href="#linearGradient3688"/>
+ <linearGradient id="linearGradient3702">
+ <stop id="stop3704" offset="0" style="stop-color:black;stop-opacity:0;"/>
+ <stop id="stop3710" offset="0.5" style="stop-color:black;stop-opacity:1;"/>
+ <stop id="stop3706" offset="1" style="stop-color:black;stop-opacity:0;"/>
+ </linearGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient4235" inkscape:collect="always" x1="25.058096" x2="25.058096" xlink:href="#linearGradient3702" y1="47.027729" y2="39.999443"/>
+ <radialGradient cx="4.9929786" cy="43.5" fx="4.9929786" fy="43.5" gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)" gradientUnits="userSpaceOnUse" id="radialGradient4233" inkscape:collect="always" r="2.5" xlink:href="#linearGradient3688"/>
+ <linearGradient id="linearGradient3688" inkscape:collect="always">
+ <stop id="stop3690" offset="0" style="stop-color:black;stop-opacity:1;"/>
+ <stop id="stop3692" offset="1" style="stop-color:black;stop-opacity:0;"/>
+ </linearGradient>
+ <radialGradient cx="4.9929786" cy="43.5" fx="4.9929786" fy="43.5" gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)" gradientUnits="userSpaceOnUse" id="radialGradient4231" inkscape:collect="always" r="2.5" xlink:href="#linearGradient3688"/>
+ <linearGradient id="linearGradient2378">
+ <stop id="stop2380" offset="0" style="stop-color:#ffffff;stop-opacity:1;"/>
+ <stop id="stop4146" offset="0.25" style="stop-color:#fefede;stop-opacity:0.91836733;"/>
+ <stop id="stop2386" offset="0.5" style="stop-color:#f5f328;stop-opacity:1;"/>
+ <stop id="stop2382" offset="1" style="stop-color:#f5f32d;stop-opacity:0.12234043;"/>
+ </linearGradient>
+ <radialGradient cx="38.658855" cy="9.3411446" fx="38.658855" fy="9.3411446" gradientUnits="userSpaceOnUse" id="radialGradient3271" inkscape:collect="always" r="8.341651" xlink:href="#linearGradient2378"/>
+ </defs>
+ <sodipodi:namedview bordercolor="#666666" borderopacity="0.3254902" id="base" inkscape:current-layer="layer5" inkscape:cx="40.26271" inkscape:cy="28.37649" inkscape:document-units="px" inkscape:grid-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:showpageshadow="false" inkscape:window-height="939" inkscape:window-width="929" inkscape:window-x="751" inkscape:window-y="91" inkscape:zoom="1" pagecolor="#ffffff" showborder="true" showgrid="false"/>
+ <g id="layer6" inkscape:groupmode="layer" inkscape:label="Shadow"/>
+ <g id="layer1" inkscape:groupmode="layer" inkscape:label="Base" style="display:inline"/>
+ <g id="layer5" inkscape:groupmode="layer" inkscape:label="Text" style="display:inline">
+ <g id="g4425" style="display:inline" transform="translate(150,0)">
+ <rect height="48" id="rect2503" style="opacity:0;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;display:inline" width="48" x="-150" y="0"/>
+ <g id="g4408">
+ <g id="g3697" inkscape:label="Shadow" style="opacity:0.65587045;display:inline" transform="matrix(1.0464281,0,0,0.8888889,-151.18571,5.7222396)">
+ <g id="g3699" style="opacity:0.4" transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)">
+ <rect height="7" id="rect3701" style="opacity:1;fill:url(#radialGradient4231);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" width="5" x="38" y="40"/>
+ <rect height="7" id="rect3703" style="opacity:1;fill:url(#radialGradient4233);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" transform="scale(-1,-1)" width="5" x="-10" y="-47"/>
+ <rect height="7.0000005" id="rect3705" style="opacity:1;fill:url(#linearGradient4235);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" width="28" x="10" y="40"/>
+ </g>
+ </g>
+ <g id="g3713" inkscape:label="Shadow" style="display:inline" transform="matrix(0.9548466,0,0,0.5555562,-148.98776,19.888875)">
+ <g id="g3715" style="opacity:0.4" transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)">
+ <rect height="7" id="rect3717" style="opacity:1;fill:url(#radialGradient4237);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" width="5" x="38" y="40"/>
+ <rect height="7" id="rect3719" style="opacity:1;fill:url(#radialGradient4239);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" transform="scale(-1,-1)" width="5" x="-10" y="-47"/>
+ <rect height="7.0000005" id="rect3721" style="opacity:1;fill:url(#linearGradient4241);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" width="28" x="10" y="40"/>
+ </g>
+ </g>
+ <path d="M -141.47614,3.5 C -141.47614,3.5 -124,3.5 -122.5,3.5 C -118.62295,3.5729425 -116,6 -113.5,8.5 C -111,11 -108.89232,13.752625 -108.5,17.5 C -108.5,19 -108.5,42.476142 -108.5,42.476142 C -108.5,43.597359 -109.40264,44.5 -110.52385,44.5 L -141.47614,44.5 C -142.59736,44.5 -143.5,43.597359 -143.5,42.476142 L -143.5,5.523858 C -143.5,4.402641 -142.59736,3.5 -141.47614,3.5 z" id="path3499" sodipodi:nodetypes="ccsccccccc" style="fill:url(#radialGradient4280);fill-opacity:1;stroke:url(#linearGradient4282);stroke-width:1;stroke-miterlimit:4;display:inline"/>
+ <path d="M 8.53125,4 C 7.6730803,4 7,4.6730802 7,5.53125 L 7,42.46875 C 7,43.32692 7.6730802,44 8.53125,44 L 39.46875,44 C 40.326919,44 41,43.326918 41,42.46875 C 41,42.46875 41,19 41,17.5 C 41,16.10803 40.513021,13.200521 38.65625,11.34375 C 36.65625,9.34375 35.65625,8.34375 33.65625,6.34375 C 31.799479,4.4869792 28.89197,4 27.5,4 C 26,4 8.53125,4 8.53125,4 z" id="path3735" inkscape:original="M 8.53125 3.5 C 7.410033 3.5 6.5 4.4100329 6.5 5.53125 L 6.5 42.46875 C 6.5 43.589967 7.4100329 44.5 8.53125 44.5 L 39.46875 44.5 C 40.589967 44.5 41.5 43.589966 41.5 42.46875 C 41.5 42.46875 41.5 19 41.5 17.5 C 41.5 16 41 13 39 11 C 37 9 36 8 34 6 C 32 4 29 3.5 27.5 3.5 C 26 3.5 8.5312499 3.5 8.53125 3.5 z " inkscape:radius="-0.4861359" sodipodi:type="inkscape:offset" style="opacity:0.68016196;fill:url(#radialGradient4247);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline" transform="translate(-150,0)"/>
+ <path d="M -122.5,4 C -123.88889,4 -122.54207,4.4970883 -121.15625,5.125 C -119.77043,5.7529117 -116.18337,8.3400517 -117,12 C -112.67669,11.569417 -110.32087,15.122378 -110,16.28125 C -109.67913,17.440122 -109,18.888889 -109,17.5 C -108.97167,13.694419 -111.84543,11.068299 -113.84375,8.84375 C -115.84207,6.6192012 -118.84621,4.4767615 -122.5,4 z" id="path3666" sodipodi:nodetypes="ccccczc" style="fill:url(#radialGradient4276);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline"/>
+ <path d="M -121.39912,5.0143528 C -120.47682,5.0143528 -118.39068,11.210015 -119.31298,15.343603 C -115.01802,14.915844 -110.4596,15.43178 -110,16.28125 C -110.32087,15.122378 -112.67669,11.569417 -117,12 C -116.13534,8.124761 -120.18657,5.3827023 -121.39912,5.0143528 z" id="path3625" sodipodi:nodetypes="ccccc" style="opacity:0.87854249;fill:url(#linearGradient4273);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;display:inline"/>
+ <path d="M -51.46875,4.5 C -52.051916,4.5 -52.5,4.9480842 -52.5,5.53125 L -52.5,42.46875 C -52.5,43.051915 -52.051914,43.5 -51.46875,43.5 L -20.53125,43.5 C -19.948085,43.5 -19.5,43.051914 -19.5,42.46875 C -19.5,42.46875 -19.5,19 -19.5,17.5 C -19.5,16.220971 -19.980469,13.394531 -21.6875,11.6875 C -23.6875,9.6875 -24.6875,8.6875 -26.6875,6.6875 C -28.394531,4.9804687 -31.220971,4.5 -32.5,4.5 C -34,4.5 -51.46875,4.5 -51.46875,4.5 z" id="path3650" inkscape:original="M -51.46875 3.5 C -52.589967 3.5 -53.5 4.4100329 -53.5 5.53125 L -53.5 42.46875 C -53.5 43.589967 -52.589966 44.5 -51.46875 44.5 L -20.53125 44.5 C -19.410033 44.5 -18.5 43.589966 -18.5 42.46875 C -18.5 42.46875 -18.5 19 -18.5 17.5 C -18.5 16 -19 13 -21 11 C -23 9 -24 8 -26 6 C -28 4 -31 3.5 -32.5 3.5 C -34 3.5 -51.468749 3.5 -51.46875 3.5 z " inkscape:radius="-0.99436891" sodipodi:type="inkscape:offset" style="fill:none;fill-opacity:1;stroke:url(#linearGradient4253);stroke-width:1;stroke-miterlimit:4;display:inline" transform="translate(-90,0)"/>
+ </g>
+ </g>
+ <g id="g14103">
+ <path d="M 47.000506 9.3411446 A 8.341651 8.341651 0 1 1 30.317204,9.3411446 A 8.341651 8.341651 0 1 1 47.000506 9.3411446 z" id="path2388" sodipodi:cx="38.658855" sodipodi:cy="9.3411446" sodipodi:rx="8.341651" sodipodi:ry="8.341651" sodipodi:type="arc" style="fill:url(#radialGradient3271);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" transform="matrix(1.14985,0,0,1.14985,-7.595328,0.490895)"/>
+ <path d="M 44.520054,15.50279 C 44.012883,16.381236 39.925351,15.341967 38.998703,15.754538 C 38.072055,16.167108 36.109289,19.900142 35.117113,19.689249 C 34.124936,19.478355 33.850222,15.26973 33.171495,14.515926 C 32.492767,13.762123 28.335913,13.048993 28.229885,12.040207 C 28.123857,11.031421 32.041607,9.4696164 32.548778,8.5911701 C 33.055949,7.7127238 32.449637,3.5389508 33.376285,3.1263806 C 34.302933,2.7138103 36.998949,5.957187 37.991126,6.1680807 C 38.983302,6.3789743 42.765436,4.5125708 43.444163,5.2663741 C 44.122891,6.0201775 41.871371,9.5864995 41.977399,10.595285 C 42.083426,11.604071 45.027225,14.624343 44.520054,15.50279 z " id="path1345" inkscape:flatsided="false" inkscape:randomized="0" inkscape:rounded="0.18352206" sodipodi:arg1="0.52359878" sodipodi:arg2="1.1519173" sodipodi:cx="36.9375" sodipodi:cy="11.125" sodipodi:r1="8.755579" sodipodi:r2="5.0676599" sodipodi:sides="5" sodipodi:type="star" style="opacity:1;color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" transform="matrix(0.674116,0.299577,-0.299577,0.674116,15.46413,-7.192469)"/>
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/news.svg b/launcher/resources/multimc/scalable/news.svg
new file mode 100644
index 00000000..67a370df
--- /dev/null
+++ b/launcher/resources/multimc/scalable/news.svg
@@ -0,0 +1,296 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="128"
+ height="128"
+ id="svg2985"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="news.svg">
+ <defs
+ id="defs2987">
+ <linearGradient
+ id="linearGradient4095">
+ <stop
+ style="stop-color:#fff7d8;stop-opacity:1;"
+ offset="0"
+ id="stop4097" />
+ <stop
+ style="stop-color:#ffeca0;stop-opacity:1;"
+ offset="1"
+ id="stop4099" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3898"
+ x="-0.11333333"
+ width="1.2266667"
+ y="-0.10074074"
+ height="1.2014815">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.5111111"
+ id="feGaussianBlur3900" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4091">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="3.6378601"
+ id="feGaussianBlur4093" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4095"
+ id="linearGradient4101"
+ x1="88.388351"
+ y1="94.942757"
+ x2="99.525276"
+ y2="103.95837"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.25"
+ inkscape:cx="-700.46253"
+ inkscape:cy="-25.005652"
+ inkscape:current-layer="text3832"
+ showgrid="false"
+ inkscape:document-units="px"
+ inkscape:grid-bbox="true"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1"
+ showguides="false"
+ inkscape:guide-bbox="true"
+ inkscape:snap-global="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2993"
+ empspacing="8"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ <inkscape:grid
+ type="xygrid"
+ id="grid2995"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ color="#00ff0b"
+ opacity="0.08235294"
+ empcolor="#00ff22"
+ empopacity="0.23137255" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="41,100"
+ id="guide4165" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="68,106"
+ id="guide4167" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="73,109"
+ id="guide4169" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="80,106"
+ id="guide4171" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="83,102"
+ id="guide4173" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="104,104"
+ id="guide4175" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata2990">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ transform="translate(0,64)">
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path3778"
+ d="m 16,8 96,0 0,76 c -9.14072,13.804136 -19.955033,25.58254 -32,36 l -64,0 z"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;filter:url(#filter4091)"
+ transform="translate(0,-64)" />
+ <path
+ transform="translate(0,-64)"
+ style="color:#000000;fill:#fff6d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 16,8 96,0 0,76 c -9.14072,13.804136 -19.955033,25.58254 -32,36 l -64,0 z"
+ id="rect2997"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ style="color:#000000;fill:none;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 68,-28 36,0 0,56 -16,20 -20,0 z"
+ id="rect4044"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <g
+ id="g4129">
+ <path
+ id="rect4103"
+ d="m 24,36 36,0 0,36 -36,0 z"
+ style="color:#000000;fill:#fff6d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ transform="translate(0,-64)" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect4107"
+ d="m 68,36 36,0 0,57 -16,19 -20,0 z"
+ style="color:#000000;fill:#fff6d3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ transform="translate(0,-64)" />
+ <flowRoot
+ id="flowRoot4115"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ xml:space="preserve"><flowRegion
+ id="flowRegion4117"><use
+ height="128"
+ width="128"
+ id="use4119"
+ xlink:href="#rect4103"
+ y="0"
+ x="0" /><use
+ height="128"
+ width="128"
+ id="use4121"
+ xlink:href="#rect4107"
+ y="0"
+ x="0" /></flowRegion><flowPara
+ style="font-size:2px;font-weight:bold;text-align:justify;text-anchor:start;-inkscape-font-specification:Sans Bold"
+ id="flowPara4123">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis mauris ullamcorper mauris viverra molestie. Donec ultricies faucibus laoreet. Donec convallis congue neque consequat vehicula. Morbi condimentum tempor nulla et rhoncus. Etiam auctor, augue eu pharetra congue, elit justo lacinia risus, non lacinia est justo sed erat. Ut risus urna, viverra id interdum in, molestie non sem. Morbi leo orci, gravida auctor tempor vel, varius et enim. Nulla sem enim, ultricies vel laoreet ac, semper vel mauris. Ut adipiscing sapien sed leo pretium id vulputate erat gravida. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras tempor leo sit amet velit molestie commodo eget tincidunt leo. Cras dictum metus non ante pulvinar pellentesque. Morbi id elit ullamcorper mi vulputate lobortis. Cras ac vehicula felis. Phasellus dictum, tellus at molestie pellentesque, purus purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis mauris ullamcorper mauris viverra molestie. Donec ultricies faucibus laoreet. Donec convallis congue neque consequat vehicula. Morbi condimentum tempor nulla et rhoncus. Etiam auctor, augue eu pharetra congue, elit justo lacinia risus, non lacinia est justo sed erat. Ut risus urna, </flowPara></flowRoot> </g>
+ <path
+ style="opacity:0.41176471;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter3898);enable-background:accumulate"
+ d="M 85.522922,28.087287 C 96.299051,25.849792 101.98214,24.118305 110.44998,20.924851 101.30926,34.728987 91.732381,44.562847 79.687414,54.980307 83.49938,42.627824 86.087749,33.764885 85.522922,28.087287 z"
+ id="path3848"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <g
+ transform="scale(1.3146517,0.76065775)"
+ style="font-size:26.48733711px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#008000;fill-opacity:1;stroke:none;font-family:Sans"
+ id="text3832">
+ <path
+ d="m 30.25786,-47.327461 -2.845319,0 -6.874782,-17.090472 0,17.090472 -2.281973,0 0,-21.034427 3.042631,0 6.84592,17.090472 0,-17.090472 2.281973,0 0,21.034427"
+ style="font-variant:normal;font-stretch:normal;fill:#008000;font-family:Oxygen Mono;-inkscape-font-specification:Oxygen Mono"
+ id="path4157"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccc" />
+ <path
+ d="m 37.27223,-56.530023 0,6.573259 7.606577,0 0.818953,2.629303 -10.707504,0 0,-21.034427 10.649209,0 -0.760658,2.629303 -7.606577,0 0.0038,6.512419 7.602816,0 -3.9e-5,2.690143"
+ style="font-variant:normal;font-stretch:normal;fill:#008000;font-family:Oxygen Mono;-inkscape-font-specification:Oxygen Mono"
+ id="path4159"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccccc" />
+ <path
+ d="m 60.091962,-50.290057 0.760658,-18.071831 2.281973,0 -1.521316,21.034427 -3.04263,0 -2.281974,-7.88791 -2.277041,7.88791 -3.047563,0 -1.61189,-21.034427 2.368129,0 0.765077,18.405124 3.042631,-9.202562 1.521315,0 3.042631,8.869269"
+ style="font-variant:normal;font-stretch:normal;fill:#008000;font-family:Oxygen Mono;-inkscape-font-specification:Oxygen Mono"
+ id="path4161"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccccccc" />
+ <path
+ d="m 67.201805,-51.228789 c 1.498704,1.191317 3.283103,1.786974 5.3532,1.786972 1.124022,2e-6 1.990463,-0.331929 2.599324,-0.995793 0.608839,-0.672953 0.913263,-1.586899 0.913275,-2.741842 -1.2e-5,-0.336472 -0.04684,-0.650214 -0.140504,-0.941229 -0.08431,-0.291 -0.192033,-0.545633 -0.323158,-0.763896 -0.13115,-0.218249 -0.318487,-0.431957 -0.562017,-0.641127 -0.234183,-0.209154 -0.449623,-0.377393 -0.646317,-0.504718 -0.196716,-0.1364 -0.45899,-0.281905 -0.786822,-0.436511 -0.318486,-0.15459 -0.585443,-0.272812 -0.800874,-0.354666 -0.206079,-0.08183 -0.487089,-0.190965 -0.843023,-0.327385 -0.346583,-0.1364 -0.604173,-0.240981 -0.772772,-0.313742 -0.580755,-0.236435 -1.067835,-0.445597 -1.461242,-0.627488 -0.384048,-0.190962 -0.810244,-0.450141 -1.278584,-0.777536 -0.468351,-0.327373 -0.843028,-0.668397 -1.124032,-1.023075 -0.28101,-0.354653 -0.519866,-0.795713 -0.716571,-1.323177 -0.187339,-0.536533 -0.281009,-1.127643 -0.281007,-1.773331 -2e-6,-1.700561 0.594798,-3.019191 1.784399,-3.955891 1.189595,-0.945755 2.721087,-1.418643 4.594479,-1.418664 2.360456,2.1e-5 4.201056,0.504737 5.521806,1.514151 l -0.983527,1.964305 c -1.217713,-0.918476 -2.683637,-1.377724 -4.397774,-1.377741 -1.105305,1.7e-5 -1.985796,0.295573 -2.641474,0.886664 -0.646325,0.582034 -0.969483,1.386852 -0.969477,2.414458 -6e-6,0.400151 0.08429,0.773005 0.252905,1.118562 0.168601,0.336492 0.369989,0.618405 0.604168,0.845743 0.234168,0.218269 0.543277,0.436525 0.927327,0.654768 0.393404,0.218268 0.735296,0.386507 1.025679,0.504717 0.290365,0.109139 0.66036,0.250096 1.10998,0.422871 0.449605,0.163703 0.777446,0.291019 0.983528,0.381948 0.562005,0.245549 1.044402,0.472898 1.44719,0.682049 0.402767,0.20008 0.833645,0.472899 1.292638,0.81846 0.468332,0.336489 0.847692,0.691155 1.13808,1.064 0.290362,0.372862 0.529218,0.832109 0.716571,1.377741 0.196691,0.536553 0.295042,1.123115 0.295058,1.75969 -1.6e-5,1.955213 -0.585447,3.446628 -1.756299,4.474248 -1.161511,1.018529 -2.753887,1.527792 -4.777134,1.527793 -2.519709,-10e-7 -4.6179,-0.541094 -6.294577,-1.623281 l 0.997578,-2.278047"
+ style="font-variant:normal;font-stretch:normal;fill:#008000;font-family:Oxygen Mono;-inkscape-font-specification:Oxygen Mono"
+ id="path4163"
+ inkscape:connector-curvature="0" />
+ </g>
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path3838"
+ d="M 88.181818,93.090909 C 100.37216,91.737298 104.94638,87.547007 112,84 c -9.14072,13.804136 -19.955033,25.58254 -32,36 4.69585,-10.93827 8.746645,-21.231493 8.181818,-26.909091 z"
+ style="color:#000000;fill:url(#linearGradient4101);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;opacity:1"
+ transform="translate(0,-64)" />
+ <rect
+ style="color:#000000;fill:#accc74;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect4138"
+ width="36"
+ height="36"
+ x="24"
+ y="76"
+ transform="translate(0,-64)" />
+ <g
+ id="g4000"
+ transform="matrix(0.60097005,0,0,0.60097012,3.5088114,25.221343)"
+ style="fill:#333333">
+ <g
+ id="g3937"
+ style="fill:#333333">
+ <rect
+ style="color:#000000;fill:#333333;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect3906"
+ width="16"
+ height="16"
+ x="40"
+ y="-16" />
+ <rect
+ style="color:#000000;fill:#333333;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect3908"
+ width="16"
+ height="16"
+ x="72"
+ y="-16" />
+ <path
+ style="color:#000000;fill:#333333;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 56,64 0,8 -8,0 0,24 8,0 0,-8 16,0 0,8 8,0 0,-24 -8,0 0,-8 -16,0 z"
+ transform="translate(0,-64)"
+ id="rect3910"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 24,32 80,0"
+ id="path3998"
+ inkscape:connector-curvature="0"
+ transform="translate(0,-64)" />
+ <rect
+ style="color:#000000;fill:none;stroke:#666666;stroke-width:0.9014551;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect4007"
+ width="36.058205"
+ height="36.058208"
+ x="23.941793"
+ y="12" />
+ <rect
+ style="color:#000000;fill:none;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect4034"
+ width="36"
+ height="32"
+ x="24"
+ y="-28" />
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/proxy.svg b/launcher/resources/multimc/scalable/proxy.svg
new file mode 100644
index 00000000..55ee6f93
--- /dev/null
+++ b/launcher/resources/multimc/scalable/proxy.svg
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ version="1.1"
+ width="64"
+ height="64"
+ id="svg2">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3931">
+ <stop
+ id="stop3933"
+ style="stop-color:#ffffff;stop-opacity:0"
+ offset="0" />
+ <stop
+ id="stop3939"
+ style="stop-color:#ffffff;stop-opacity:0.18039216"
+ offset="0.69999999" />
+ <stop
+ id="stop3935"
+ style="stop-color:#ffffff;stop-opacity:0.3611111"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3900">
+ <stop
+ id="stop3902"
+ style="stop-color:#f6f6f6;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3904"
+ style="stop-color:#494949;stop-opacity:1"
+ offset="0.75714284" />
+ <stop
+ id="stop3906"
+ style="stop-color:#2c2c2c;stop-opacity:1"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3808">
+ <stop
+ id="stop3810"
+ style="stop-color:#333333;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3812"
+ style="stop-color:#c8c8c8;stop-opacity:1"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3030">
+ <stop
+ id="stop3032"
+ style="stop-color:#36c536;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3038"
+ style="stop-color:#1f721f;stop-opacity:1"
+ offset="0.75714284" />
+ <stop
+ id="stop3034"
+ style="stop-color:#134513;stop-opacity:1"
+ offset="1" />
+ </linearGradient>
+ <radialGradient
+ cx="24"
+ cy="16"
+ r="16.375"
+ fx="24"
+ fy="16"
+ id="radialGradient3036"
+ xlink:href="#linearGradient3030"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1497507,1.7246237,-1.4656488,0.97709922,19.856365,-41.024557)" />
+ <linearGradient
+ x1="30"
+ y1="1033.8622"
+ x2="34"
+ y2="1033.8622"
+ id="linearGradient3824"
+ xlink:href="#linearGradient3808"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.5,0,0,1,-16,0)" />
+ <linearGradient
+ x1="32"
+ y1="1043.3622"
+ x2="32"
+ y2="1039.3622"
+ id="linearGradient3834"
+ xlink:href="#linearGradient3808"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.82142857,0,0,1.500001,6.7142857,-522.68214)" />
+ <radialGradient
+ cx="30.724609"
+ cy="1039.813"
+ r="3"
+ fx="30.724609"
+ fy="1039.813"
+ id="radialGradient3844"
+ xlink:href="#linearGradient3900"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.9033547,2.9862934,-4.666644,3.3333362,4792.8997,-2518.4369)" />
+ <radialGradient
+ cx="30.724609"
+ cy="1039.813"
+ r="3"
+ fx="30.724609"
+ fy="1039.813"
+ id="radialGradient3852"
+ xlink:href="#linearGradient3900"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.5191507,2.9862959,-4.0491019,3.333339,4186.8847,-2518.44)" />
+ <radialGradient
+ cx="30.724609"
+ cy="1039.813"
+ r="3"
+ fx="30.724609"
+ fy="1039.813"
+ id="radialGradient3857"
+ xlink:href="#linearGradient3900"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-2.5191507,2.9863064,4.0491022,3.3333507,-4122.8849,-2518.4524)" />
+ <radialGradient
+ cx="31.964285"
+ cy="25.25"
+ r="19.25"
+ fx="31.964285"
+ fy="25.25"
+ id="radialGradient3937"
+ xlink:href="#linearGradient3931"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.97517967,0.03814675,-0.04025373,1.0289295,1.809772,-1.9498045)" />
+ </defs>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(0,-988.36218)"
+ id="layer1">
+ <rect
+ width="6"
+ height="13.000005"
+ rx="0.70710504"
+ ry="0.70710492"
+ x="29"
+ y="1027.3622"
+ id="rect3028"
+ style="color:#000000;fill:url(#linearGradient3824);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="46"
+ height="6.0000076"
+ rx="0.70710504"
+ ry="0.70710492"
+ x="10"
+ y="1036.3622"
+ id="rect2991"
+ style="color:#000000;fill:url(#linearGradient3834);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="32"
+ height="32"
+ rx="0.70710504"
+ ry="0.70710492"
+ x="16"
+ y="8"
+ transform="translate(0,988.36218)"
+ id="rect2997"
+ style="color:#000000;fill:url(#radialGradient3036);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.75;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="6"
+ height="6"
+ x="23"
+ y="1003.3622"
+ id="rect3001"
+ style="color:#000000;fill:#112b00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="6"
+ height="6"
+ x="35"
+ y="1003.3622"
+ id="rect3003"
+ style="color:#000000;fill:#112b00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 29,1009.3622 0,3 -3,0 0,9 3,0 0,-3 6,0 0,3 3,0 0,-9 -3,0 0,-3 -6,0 z"
+ id="rect3005"
+ style="color:#000000;fill:#112b00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="6"
+ height="2.9999955"
+ x="29"
+ y="1009.3622"
+ id="rect3011"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="3"
+ height="2.9999955"
+ x="26"
+ y="1012.3622"
+ id="rect3013"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="3"
+ height="2.9999955"
+ x="35"
+ y="1012.3622"
+ id="rect3015"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 35,1003.3622 0,6 3,0 0,-3 3,0 0,-3 -3,0 -3,0 z"
+ id="rect3017"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 23,1003.3622 0,3 0,3 3,0 0,-3 3,0 0,-3 -3,0 -3,0 z"
+ id="rect3021"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="14"
+ height="8.0000048"
+ rx="0.70710504"
+ ry="0.70710492"
+ x="25"
+ y="1035.3622"
+ id="rect3836"
+ style="color:#000000;fill:url(#radialGradient3844);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.76376289;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 51,1035.3622 5,0 6.073683,4 -6.073683,4 -5,0 c -0.59482,0 -1.073683,-0.4205 -1.073683,-0.9428 l 0,-6.1144 c 0,-0.5223 0.478876,-0.9428 1.073683,-0.9428 z"
+ id="rect3850"
+ style="color:#000000;fill:url(#radialGradient3852);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.71143585;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m 13,1035.3622 -5,0 -6.0736829,4 6.0736829,4 5,0 c 0.59482,0 1.073683,-0.4205 1.073683,-0.9428 l 0,-6.1144 c 0,-0.5223 -0.478877,-0.9428 -1.073683,-0.9428 z"
+ id="path3855"
+ style="color:#000000;fill:url(#radialGradient3857);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.71143711;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ width="38"
+ height="38"
+ rx="0.70710504"
+ ry="0.70710492"
+ x="13"
+ y="5"
+ transform="translate(0,988.36218)"
+ id="rect3927"
+ style="color:#000000;fill:url(#radialGradient3937);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/reddit-alien.svg b/launcher/resources/multimc/scalable/reddit-alien.svg
new file mode 100644
index 00000000..46061a56
--- /dev/null
+++ b/launcher/resources/multimc/scalable/reddit-alien.svg
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ x="0px"
+ y="0px"
+ width="360"
+ height="360"
+ viewBox="0 0 360 360.00001"
+ enable-background="new 0 0 120 40"
+ xml:space="preserve"
+ id="svg21775"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="reddit-alien.svg"
+ inkscape:export-filename="/home/chromakode/Documents/code/reddit/reddit-about/reddit_about/public/static/about/assets/reddit-alien.png"
+ inkscape:export-xdpi="184.19029"
+ inkscape:export-ydpi="184.19029"><metadata
+ id="metadata21839"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs21837" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1680"
+ inkscape:window-height="1030"
+ id="namedview21835"
+ showgrid="false"
+ inkscape:zoom="0.70710678"
+ inkscape:cx="48.338935"
+ inkscape:cy="66.337626"
+ inkscape:window-x="1678"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg21775"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ showguides="false" /><g
+ id="Layer_2"
+ transform="matrix(12.584949,0,0,12.584949,-584.34689,27.734702)"><circle
+ cx="49.683998"
+ cy="11.836"
+ r="2.7019999"
+ id="circle21800"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002" /><circle
+ cx="71.786003"
+ cy="11.836"
+ r="2.7019999"
+ id="circle21802"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002" /><path
+ d="m 68.226,2.352 c 0,0 0,0 -5.404,-1.272 l -1.986,6.278"
+ id="path21804"
+ inkscape:connector-curvature="0"
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round" /><circle
+ cx="70.431"
+ cy="2.789"
+ r="2.1300001"
+ id="circle21808"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002" /><ellipse
+ cx="60.675999"
+ cy="15.552"
+ rx="12.238"
+ ry="7.987"
+ id="ellipse21820"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002" /><g
+ id="g21824"><path
+ style="fill:#ff4500;stroke:none"
+ d="m 135.90625,159.65625 c -13.67995,0 -25.15625,11.4763 -25.15625,25.15625 0,13.67995 11.4763,24.78125 25.15625,24.78125 13.67995,0 24.78125,-11.1013 24.78125,-24.78125 0,-13.67995 -11.1013,-25.15625 -24.78125,-25.15625 z"
+ transform="matrix(0.07946,0,0,0.07946,45.637601,-0.68559946)"
+ id="circle21826"
+ inkscape:connector-curvature="0" /><path
+ style="fill:#ff4500;stroke:none"
+ d="m 244.46875,159.65625 c -13.68691,0 -25.1875,11.46934 -25.1875,25.15625 0,13.68691 11.50059,24.78125 25.1875,24.78125 13.68692,0 24.78125,-11.09434 24.78125,-24.78125 0,-13.68691 -11.09433,-25.15625 -24.78125,-25.15625 z"
+ transform="matrix(0.07946,0,0,0.07946,45.637601,-0.68559946)"
+ id="circle21828"
+ inkscape:connector-curvature="0" /><path
+ d="m 56.507,19.229 c 1.066,1.066 2.786,1.271 4.212,1.271"
+ id="path21830"
+ inkscape:connector-curvature="0"
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round" /><path
+ d="M 64.963,19.229 C 63.896,20.295 62.177,20.5 60.75,20.5"
+ id="path21832"
+ inkscape:connector-curvature="0"
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round" /></g><circle
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002"
+ id="circle4162"
+ r="2.7019999"
+ cy="11.836"
+ cx="49.683998" /><circle
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002"
+ id="circle4164"
+ r="2.7019999"
+ cy="11.836"
+ cx="71.786003" /><path
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round"
+ inkscape:connector-curvature="0"
+ id="path4166"
+ d="m 68.226,2.352 c 0,0 0,0 -5.404,-1.272 l -1.986,6.278" /><circle
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002"
+ id="circle4168"
+ r="2.1300001"
+ cy="2.789"
+ cx="70.431" /><ellipse
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002"
+ id="ellipse4170"
+ ry="7.987"
+ rx="12.238"
+ cy="15.552"
+ cx="60.675999" /><g
+ id="g4172"><path
+ inkscape:connector-curvature="0"
+ id="path4174"
+ transform="matrix(0.07946,0,0,0.07946,45.637601,-0.68559946)"
+ d="m 135.90625,159.65625 c -13.67995,0 -25.15625,11.4763 -25.15625,25.15625 0,13.67995 11.4763,24.78125 25.15625,24.78125 13.67995,0 24.78125,-11.1013 24.78125,-24.78125 0,-13.67995 -11.1013,-25.15625 -24.78125,-25.15625 z"
+ style="fill:#ff4500;stroke:none" /><path
+ inkscape:connector-curvature="0"
+ id="path4176"
+ transform="matrix(0.07946,0,0,0.07946,45.637601,-0.68559946)"
+ d="m 244.46875,159.65625 c -13.68691,0 -25.1875,11.46934 -25.1875,25.15625 0,13.68691 11.50059,24.78125 25.1875,24.78125 13.68692,0 24.78125,-11.09434 24.78125,-24.78125 0,-13.68691 -11.09433,-25.15625 -24.78125,-25.15625 z"
+ style="fill:#ff4500;stroke:none" /><path
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round"
+ inkscape:connector-curvature="0"
+ id="path4178"
+ d="m 56.507,19.229 c 1.066,1.066 2.786,1.271 4.212,1.271" /><path
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round"
+ inkscape:connector-curvature="0"
+ id="path4180"
+ d="M 64.963,19.229 C 63.896,20.295 62.177,20.5 60.75,20.5" /></g><circle
+ cx="49.683998"
+ cy="11.836"
+ r="2.7019999"
+ id="circle4182"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002" /><circle
+ cx="71.786003"
+ cy="11.836"
+ r="2.7019999"
+ id="circle4184"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002" /><path
+ d="m 68.226,2.352 c 0,0 0,0 -5.404,-1.272 l -1.986,6.278"
+ id="path4186"
+ inkscape:connector-curvature="0"
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round" /><circle
+ cx="70.431"
+ cy="2.789"
+ r="2.1300001"
+ id="circle4188"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002" /><ellipse
+ cx="60.675999"
+ cy="15.552"
+ rx="12.238"
+ ry="7.987"
+ id="ellipse4190"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.10000002" /><g
+ id="g4192"><path
+ style="fill:#ff4500;stroke:none"
+ d="m 135.90625,159.65625 c -13.67995,0 -25.15625,11.4763 -25.15625,25.15625 0,13.67995 11.4763,24.78125 25.15625,24.78125 13.67995,0 24.78125,-11.1013 24.78125,-24.78125 0,-13.67995 -11.1013,-25.15625 -24.78125,-25.15625 z"
+ transform="matrix(0.07946,0,0,0.07946,45.637601,-0.68559946)"
+ id="path4194"
+ inkscape:connector-curvature="0" /><path
+ style="fill:#ff4500;stroke:none"
+ d="m 244.46875,159.65625 c -13.68691,0 -25.1875,11.46934 -25.1875,25.15625 0,13.68691 11.50059,24.78125 25.1875,24.78125 13.68692,0 24.78125,-11.09434 24.78125,-24.78125 0,-13.68691 -11.09433,-25.15625 -24.78125,-25.15625 z"
+ transform="matrix(0.07946,0,0,0.07946,45.637601,-0.68559946)"
+ id="path4196"
+ inkscape:connector-curvature="0" /><path
+ d="m 56.507,19.229 c 1.066,1.066 2.786,1.271 4.212,1.271"
+ id="path4198"
+ inkscape:connector-curvature="0"
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round" /><path
+ d="M 64.963,19.229 C 63.896,20.295 62.177,20.5 60.75,20.5"
+ id="path4200"
+ inkscape:connector-curvature="0"
+ style="fill:none;stroke:#000000;stroke-width:1.10000002;stroke-linecap:round" /></g></g><g
+ id="Layer_3"
+ transform="translate(-56.49872,330.00079)" /></svg> \ No newline at end of file
diff --git a/launcher/resources/multimc/scalable/screenshot-placeholder.svg b/launcher/resources/multimc/scalable/screenshot-placeholder.svg
new file mode 100644
index 00000000..a7a2a3d6
--- /dev/null
+++ b/launcher/resources/multimc/scalable/screenshot-placeholder.svg
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="256"
+ height="256"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.5 r10040"
+ sodipodi:docname="screenshot-placeholder.svg">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.49497475"
+ inkscape:cx="-33.672765"
+ inkscape:cy="136.19802"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:snap-nodes="false"
+ inkscape:window-width="1612"
+ inkscape:window-height="1026"
+ inkscape:window-x="1677"
+ inkscape:window-y="-4"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ spacingx="8px"
+ spacingy="8px" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-796.36218)">
+ <g
+ id="g3009"
+ style="fill:#000000">
+ <path
+ id="rect2987"
+ transform="translate(0,796.36218)"
+ d="M 24 8 C 15.136 8 8 15.136 8 24 L 8 232 C 8 240.864 15.136 248 24 248 L 232 248 C 240.864 248 248 240.864 248 232 L 248 24 C 248 15.136 240.864 8 232 8 L 24 8 z M 24 16 L 232 16 C 236.432 16 240 19.568 240 24 L 240 232 C 240 236.432 236.432 240 232 240 L 24 240 C 19.568 240 16 236.432 16 232 L 16 24 C 16 19.568 19.568 16 24 16 z "
+ style="opacity:0.75000000000000000;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75000000000000000;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ style="fill:#000000;opacity:0.75000000000000000;stroke:none"
+ inkscape:connector-curvature="0"
+ d="m 85.749999,937.9006 c 0,24.30067 18.915811,43.99997 42.249991,43.99997 23.33419,0 42.25001,-19.6993 42.25001,-43.99997 0,-24.30069 -18.91582,-43.99999 -42.25001,-43.99999 -23.33418,0 -42.249991,19.6993 -42.249991,43.99999 z M 219,863.43909 h -45.5 c -3.25,-13.53846 -6.50001,-27.07691 -19.5,-27.07691 h -52 c -13.000001,0 -16.250001,13.53845 -19.500001,27.07691 H 37 c -7.15,0 -13,6.09231 -13,13.53846 v 121.84603 c 0,7.44632 5.85,13.53862 13,13.53862 h 182 c 7.15,0 13,-6.0923 13,-13.53862 V 876.97755 c 0,-7.44615 -5.85,-13.53846 -13,-13.53846 z m -91.00001,134.53849 c -31.860147,0 -57.687491,-26.89678 -57.687491,-60.07698 0,-33.1798 25.827344,-60.0769 57.687491,-60.0769 31.86057,0 57.68751,26.8971 57.68751,60.0769 0,33.1802 -25.82653,60.07698 -57.68751,60.07698 z M 219,904.05445 h -26 v -13.53846 h 26 v 13.53846 z"
+ id="path2996" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/screenshots.svg b/launcher/resources/multimc/scalable/screenshots.svg
new file mode 100644
index 00000000..a3d4d8e2
--- /dev/null
+++ b/launcher/resources/multimc/scalable/screenshots.svg
@@ -0,0 +1,1231 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="1100"
+ height="1100"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.5 r10040"
+ sodipodi:docname="screenshots.svg"
+ inkscape:export-filename="/home/peterix/minecraft/src/MultiMC5/resources/multimc/16x16/screenshots.png"
+ inkscape:export-xdpi="1.3099999"
+ inkscape:export-ydpi="1.3099999">
+ <title
+ id="title3887">Golden Picture Frame</title>
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3871">
+ <stop
+ id="stop3873"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" />
+ <stop
+ style="stop-color:#604d00;stop-opacity:1;"
+ offset="0.24157524"
+ id="stop3875" />
+ <stop
+ id="stop3877"
+ offset="1"
+ style="stop-color:#745d00;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3831">
+ <stop
+ style="stop-color:#fff2be;stop-opacity:1;"
+ offset="0"
+ id="stop3835" />
+ <stop
+ id="stop3839"
+ offset="0.5"
+ style="stop-color:#6a5500;stop-opacity:1;" />
+ <stop
+ id="stop3837"
+ offset="1"
+ style="stop-color:#745d00;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3817">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3819" />
+ <stop
+ id="stop3825"
+ offset="0.39502749"
+ style="stop-color:#604d00;stop-opacity:1;" />
+ <stop
+ style="stop-color:#745d00;stop-opacity:1;"
+ offset="1"
+ id="stop3821" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3831"
+ id="linearGradient3821"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.3556705,0,0,2.3556705,-1198.3938,-507.60896)"
+ x1="568.4173"
+ y1="501.39673"
+ x2="588.28278"
+ y2="502.30829" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3817"
+ id="linearGradient3823"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.3556705,0,0,2.3556705,-1198.3938,-507.60896)"
+ x1="731.26221"
+ y1="376.98492"
+ x2="731.15552"
+ y2="364.66559" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3871"
+ id="linearGradient3825"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.3556705,0,0,2.3556705,-1198.3938,-507.60896)"
+ x1="883.03296"
+ y1="502.69846"
+ x2="897.77411"
+ y2="502.96545" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3831"
+ id="linearGradient3827"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.3556705,0,0,2.3556705,-1198.3938,-507.60896)"
+ x1="723.38055"
+ y1="621.29663"
+ x2="721.71783"
+ y2="661.40479" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.16698148"
+ inkscape:cx="-510.5746"
+ inkscape:cy="-1131.4434"
+ inkscape:document-units="px"
+ inkscape:current-layer="g3807"
+ showgrid="false"
+ inkscape:window-width="1612"
+ inkscape:window-height="1026"
+ inkscape:window-x="1677"
+ inkscape:window-y="-4"
+ inkscape:window-maximized="1"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:snap-bbox-edge-midpoints="true" />
+ <g
+ inkscape:label="Capa 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(31.270747,-9.54776)">
+ <g
+ id="g3017"
+ transform="matrix(-1.3082428,0,0,1.1906703,1197.3531,-130.52295)">
+ <g
+ id="g3807"
+ transform="translate(0,-102.53568)">
+ <rect
+ y="365.48618"
+ x="555.12488"
+ height="287.45703"
+ width="344.34955"
+ id="rect3017"
+ style="fill:#000000;fill-opacity:0.78921569"
+ transform="matrix(2.3556705,0,0,2.3556705,-1198.3938,-507.60896)" />
+ <rect
+ y="352.5882"
+ x="141.80692"
+ height="641.88568"
+ width="768.85199"
+ id="rect3019"
+ style="fill:#000000;fill-opacity:1" />
+ <rect
+ style="fill:#fff6d5;fill-opacity:1;stroke:#502d16;stroke-width:2.35567045"
+ id="rect3013"
+ width="723.00299"
+ height="599.56348"
+ x="164.73138"
+ y="374.21686" />
+ <rect
+ style="fill:none;stroke:#808000;stroke-width:2.35567045"
+ id="rect3009"
+ width="603.09033"
+ height="469.07022"
+ x="224.68771"
+ y="438.99588" />
+ <rect
+ style="fill:#999999;stroke:none"
+ id="rect3015"
+ width="553.71448"
+ height="419.69443"
+ x="249.37567"
+ y="463.68384" />
+ <path
+ id="path3802"
+ d="m 120.66044,334.92569 0,677.18161 811.16043,0 0,-677.18161 -811.16043,0 z m 21.12742,17.66753 768.90559,0 0,641.8466 -768.90559,0 0,-641.8466 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:#502d16;stroke-width:2.35567045"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#554400;fill-opacity:1"
+ d="m 559.94852,357.66235 0,287.46875 344.34375,0 -0.43711,-287.04864 z m 8.96875,7.5 326.40625,0 0,272.46875 -326.40625,0 z"
+ id="rect3797"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccc"
+ transform="matrix(2.3556705,0,0,2.3556705,-1198.3938,-507.60896)" />
+ <path
+ style="fill:url(#linearGradient3821);fill-opacity:1;stroke:none"
+ d="m 164.75566,973.3124 0,-599.59176 -22.9678,-21.12742 0,641.8466 z"
+ id="path3815"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:url(#linearGradient3823);fill-opacity:1;stroke:none"
+ d="m 164.75566,373.72064 722.96999,0 22.9678,-21.12742 -768.90559,0 z"
+ id="path3813"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:url(#linearGradient3825);fill-opacity:1;stroke:none"
+ d="m 887.72565,373.72064 0,599.59176 22.9678,21.12742 0,-641.8466 z"
+ id="path3811"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:url(#linearGradient3827);fill-opacity:1;stroke:none"
+ d="m 887.72565,973.3124 -722.96999,0 -22.9678,21.12742 768.90559,0 z"
+ id="rect3804"
+ inkscape:connector-curvature="0" />
+ <image
+ y="463.68384"
+ x="-803.09015"
+ id="image3837"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAACiCAIAAAAhs4RgAAAAA3NCSVQICAjb4U/gAAAgAElEQVR4
+nOy9aYxk2XUmds65974ttszIrTJr6eq9m91NSqQkihI1tkRDgG1BgM3xGJLGY8jwAB4YXjAYj2AD
+hjTzz/YPWwNj7LFmPCMZIk1JlkYLRQ4XSVzEnewm2c1mVy+1ZmXlEnvEW+5yjn+8rOqsLTOrKrur
+uHwIoKIiX7x334tz7j3Ld87F3/2Tz8D3IBBB5H4PAgAAELHZbCZJcu0TFjiTL58vuwB4Hwf24OAx
+/lYbhgceZiE+j49PsHv4M69Go3e2Lo9GI2vt3Y2N7u5r9x0PiPQDgIhMJpPJZCJXx0QITzW23tVc
+J+D7O7YHBENc3P8AAdiBY9/B99yR9AOAFXUP4wL43lWABw1lWQ4GA+/9tU+OxZOf7Jxtquo+juoB
+wRCW9pmvLMSv4XMX6AlG/faN6Sp+qABHhhDCYDAoy/LaUtDS9r2dc8vR+P4O7L7Do5nA3M2fC8A2
+rH4H3zPB+bd/VDV+qABHjMlkMp1Or+mARv6R5voT2SbCA2O03Q8McOmGTypIXsPnLtLj92XiB4Ap
+tF/FZ+/Ptb+/UZalc67dbmutAQARHk77c7p4fnLCyQ/oAx/C4gl4XV11inqwfAkfC/dP9DfwoQnM
+Ad6nEXzfozaH6ugQIgLAvCl+eu6N5ycnRj6736O7DwhoxtKdhx0L0QV8fAxdwCMIkbHcwUkEYArt
+K1dFv/7whwrwFmI6nTrnms0mEQFATOEn2hfO5Es/mBHSAS4FUev4SEBzVOcsODrskdDYwFPDmywx
+DQCtVst7XxTFUQ3rh7iGqqq8961WyxgDAITyVGOrravvTI+FHzAHbAiLQ7pR/t4GFJBt4ENDWLx5
+zSEQDQCz2ez06dOIOBwOR6PR3ljeAw4iYn7QY+0hhOFwuNccWotHLVW+MDmec3y/R/c24ihsnjtC
+Do0reOqWom+In5wvHuuU6oO/9Kt1KmdhYaHVanW73SzLAOCuU2tvJx599NGhNc6W9MDHWKy13vso
+imodiCmsxaMZR7Pwg6QDRw0vdDweBluEEPZ+XkF8AZ+8hI+W2LhB+iPid3Tzn16bHG+6SIn64C/9
+KgAwc57nnU4HEaMoarVaurn0yngOJcTw4KZy5ubmxrT0zdlDApTB9AFXgxBCVVXGGKUUACiUY9EE
+AQY++wF0CY4IaDBkPLimABXE6/jIBXziZtGP1Zuin08Hw2G/KGZvOsFFUWxsbKyurtZT1FwiD600
+v7J5LJKyC1sLshlD+Xbe2WHgnEs1BzQbeHpTTqzIpSW4rOHBNeGYeTgcNhqNNE0REREezXbmTP7N
+yfEf2AjpPWLDdhYRAKCC5Aqe7MEK4I3OVabDM9380U6pCADg5e8+/w9+7ZcYQ6wa1z300WgURdHi
+4i5z4/G5slfq10fpFXjoCpxqwqgrW3OwrSHAgwFrbZbtDoZRb+DpLTm+BJeXZf1BVoPZbOaca7Va
+dXRoweTv65x9YXJiHNL7PbTvPcxCtA3zA+wMYFluJfrPLuSPtEsQPxz0Z7NJUU2eePy55RMrGxuX
+nn3nu/FmNuiJEydarVb9Pgh84vxcv3ozbkXiO9BfkCstGN73Zbvb7XaXVn7v1RtjC0r8Eqw/4GpA
+RO12u44OAQALvDw7dqm6b6SA7zPMxf7p+fyhVoXAn//KRz/z2Y9mZu4zX/kjV/l/9N//iwsXXjt3
+9iyaqz7AXkwmk2azWWcxCeFYZs9OknA14yBIJTb6uNLHFQfGQGXun5AprTrtzplhGq5PiAjSFOe2
+cZWBUpgdIStzjPMa7JE4GyJSViUAGG1qc2jJTGPyfZ/JD1iE9GgxH7ufWJm+Z3mW4fTS5Ve/+/rX
+/4/f/vX/4j/9jYcfeurC+qvbG1vfevErL7/y/MryyRe+8xn1wV/+VUC47gUwm05brXbtq8VK5mN/
+fhID4t7DAuoZdnq4NsYuAMRYEvKNp3qrXwAL3e65cVIy3fzXWg12cJVRpTC79+EJ4hn8kT4ei6BK
+sDj8F/H243fOXYsOIUJHlwtmtm7nbnjaP3wd5rWYuh9bmb5nadaJAyJ8+nN/+Ma5l//Ge39BU3z6
+1JOf/auPv/TyN449vHpl81w5ra7kr07HI/zQn37mlmqUJOmpU6dqIxUAvrWTvdhv7KN2KGEOel3Z
+6kD/6JT5YDz11NN/td6+PDsgmEjil+Dyily6F6NoinNn8J31+7b0TsjryREFBoio1XrTHPr88JEf
+rBTBPeNYZp9bmC0mfm/g56vf/PSp40/87u//k+df+MLywokXX/xqPKfB0+RKkSaNrJtEUXzbyENZ
+FhsbG2tra3VQ6LmFWb/S+8iZoBrA8gCWNLgFuNKVrQRyvN3RRwfnXNMc7JQz6k04tQ21i3zJgLuL
+aw1g4dr7MS68DHMrcGlFLql7jgow82g0zLIsTTN82xNG38uQ1cw9tzBbuF70awyGO88//6Xnv/bF
+Qb4lIpnp/NSP/vzLZ76eD14r7CyxyanHHlcf/OVfvd25ra0QIcsaAICIqw17cRpb3tc8RWRUM+zs
+4NoY5hkohuItLYxqNpttlUOw0xAfSC4QpBl2dmAtoE5hpu5kYAJwAa8v2kCa4twAlg2UKRwBkcQ5
+572Louhi1f1hVHR/IMjpVvUTK9NnForM8A3S75zt93tf+PIn/uD3/u8QFyqiudZyb3trq3ijnc0P
+B72opTDxw2rjtibQNaytrbVabQBAxF6pP3VxLtwJBQ+FO9DrymYH+m/FzHbs2DERCCGwwIbtXCi7
+k5Ac/DUAkrAIl1cOvRpMoX2GfuTWfxNpweCkvJ4chRoAqm/he/0PeYq3AYKcblfPdPN2dIuFty7N
+Gw4H//ov/unzL33m5MLTb1x4UZGeTWZuGpJ54ws2DeVzWZo/aV15sAIQ0cmTJ9N0l8T7+ij58mbr
+LsatxdYJtQRmR6gJ3e7CNUcFAESg5xrny27PN3bdzH2BEpZgY0UuHqgGF/HRbTy+76l4CdZX5fwd
+LSw3QADO4tM3kxZ/CADQyKda1TPdvBXd+IRFpMjzS5fP/7N/+Y+99Y1G+9z2t5g5oebGhcso5IOL
+WkSaZts2aipfcDKndUz7mUDXTj2bza4FheZjXwbamxk4JBjVDNs7uDaCrgBFUN276VwPz5jomt2M
+CJlya/F42Uy8UB4i2V8NkGbY3oa1gCaF2e2GJIAX8LEDapcQZ9jpw0oE9q79nytwcof2U7MfTGjk
+J+eL96+OT7dtrK6LQYtIr7dzeWO919sJnv/qy39wZfzGwtzqL37g71648Gqs0+3tTd1EEXEFSxDy
+0dyjETshjaTp4BWgRpIkp049VM+1geFTl+Z65T2xulG4Df0F2WxD/x7D6kqpJEniONm7FNSoWF8o
+5y9V8+4Q7QNIwgJsHJNLBm4kAu5n/9wKLRmckNdTyA//FQAYwfzr+MzNmfwfZGiUJ+aKp+bzRN8o
+JMx86fK5j376/8kH5YtnviSR+9n3/kfjSf/zX/roz/zUv/+lr/0bcHrryoZpYRwn/QuTbFEjoZ+h
+aYq3EjWVig6xAtTw3leVbbVaiLibHRsnd+QM3AjECrMBLm/DmsNYgzNg7+50IuKcK8uSORCpvWqg
+kRdMfjLpx+RnIfL7qoEg5djehlWP0Q2rwSaeyLF9+CFZTHdg1WHUgPEhYwAlpK/js3JQjd5c7DIs
+D18I8r2LRPGzC7P3rU5ONK2+fk5wzp157cUzr730kY/9ZlkUp1af1im9cuaF/vZ2c771d3/5H/3J
+p3+rqPLBlQGB9s5BGXm2gOBmgWJgKwCQtPVkvTysAsAuQVoajQYARErm6uzYIezs/SGocmz3cHUI
+i3xvppH3vq7HRUSl1DW7iBA6ujwZD9q6LEJUyb5r164arPmrRtEt4j+HAWKOrR04ZsClB7k9AdTr
++KzFA7hA87H7wPFRlzfg+5pDmqrwzsX8fcfGK5m/QfTLstze3trY2Pj05/8gjrLTa08bHT/xyI98
+/GO/9xPv/sCZK1/p97Zm41yEezubZV6aDgTLYELUVD7npG0AIViJO9pOAxLegQIAQFEUxkR1F7RW
+xCKwVRzZVOQxmmB3AEtLcPleflhmtraqqlIE9qoBIjSUPR4PNXLPNw84C+K11SCA7tOxuxuMoBrh
+4hjmU5hGN1lW13AenzywNch87H7uxChSbKtqXs+aqtqxje8z0kRDh+cW8vcdmyxnnq4Xgul0urW1
+ubW1VVUVAGz1Lzz5yHv+zw//WjPtfvIvPtKvLo17405raevC1hsXXizsOJ/lRpIArhz4YNmXXAwc
+EviCSSMRRk2tzE1G84G4cmUjz3dN2+cW8uONI64WcBhP4AgIYcyc57N+vzedTrz31/qUIII5dDcx
+QbWNx8/hU/c4mBzbr+CPXsDHbhncvAInBnBA2Kcbu587Mdrr/61EdeOtB46jfndoR/6njo1/8eH+
+U/PF3llfREaj0dmzZy9dujidTq99zgF++8P/U5EX3bllrWM7Co2svbbwWNI21dT+wr/9n4cC8nIa
+RpqZTabs1DeXo6SjdUJEoBLiIK4Id7YC1JhOp+12WymFCEvRTLmpAJasD4i33AnmYeeoTlXbRd6/
+aRdtu/bA3Qnx+EhSs4g5tnpwTEHYaxGNYP4iPrn/JfZKf6/X6/f7UWSIKCJei0clm+nhUh8PJjqR
+/7Gl6Y8vT+eTsPcxMPOg39/YuDwajUK4kcDSH175zFf+kANfeP3siZOnJ3bn4hsX+u78sYWHn338
+J7/x4l9WIZeS0kaWrAgAuIJ9JWJJgE0UAbGKiOiu2qKEEC5evPDw6YeJSFxxMqlOJkPL1HPNbdfc
+ts0A99SxcQSLHvTRMpmdc845IkqSxKK5X7ZzQHMRH9+RYyfltQZMKojP41OwL/mhG7ufvSr92zvb
+Ozs7ALBbZBwnGuW5xuU5XbySrxzhBPT2YCm1T80Xxxv2BmvHOTcYDAbDwf4F30ppb90H/p0PfupL
+HypmRRKn1dju5Nt+QNtb216Vel4Nt3ZwjCIQLKuIbGFVhBZsZNT0SlX1/V2mG6216+vrJ04ct3bX
+BIqIV+PxajwOgjuu0XPNLduyd5XPF6SBLC3Bxt2NbR/UlZ82uc+CUmDrDPxIFzZzaO7fIyTT4f1r
+41iJiOz0dmrph6sdeZ1zzUYTEU8lg7Yuvjk5cYB//6BAllP33EK+nLobVL8sy/6gPxqNDjxFszkX
+JYbZf+mrn1iJnziz8Y1Glk5GdkBbo3y7GLrOyURYkNAVgZ24nDsntcmULziUjKhNqkLCmoHujqsz
+nU0vra9HJqoTZNegUFai6Uo0fSq7MvTZlm1tu+adhu0GuLwkR68ANQp5AAwGxD4c4Fg3dPjAyWHT
+MADs7Ozs9G40C3db0LXaWus5Xb63c+7F6Vrf70fafWsga1l1OT84JIggy6l7diFfya7Lu9fJ1sFg
+MJ1Nb/fdGzDXWrSuBIS4EV3ZONc4psvtqavs3Oms6FkAmGxU4Cnp6sZy1DuT64QA0ZdcjX22FHEQ
+XwSfB/X43/6tKXQCGgKu6QCHt3ittWVVlFUVONQW9nV3i5AqtxjNTsWD5WiqMQShQ64JDuJ52H4r
+6rmUUhtw6jAZDAR5OO3PQsT3I9KyV/q3t7dvlv4aIlKWJREZbTTyajQKQG9n87nVzP7M2vjJuXw0
+c/uWdMqpZvXTq5N3dIv6pnY/FRmNR1euXOn3+9bdQSOSaT764lc+zoFtaclHk+GoHFv2kMRZmjai
+rpDCjOarkLOVYDluaaVxtm2jlkYAOw4qIWVQM6gJdifQBRANrg39pozaMLg5G3rr2xJgDkVRFEWB
+iHEUm8hEJtqbjUKEti7buhTZLths2vaWa459up/NitiHlTU5d/gnckgopSp/KIFeTNzj6dbDyfbl
+au582X2bc09Pd/OmYRHZ2NgYjQ8wCeoWdK1GE4meSLc6qvhOvrp/1u/esZi4dy3Orpox9I52b6fX
+uPkpIcjJVvVcN+/E16V3mLk29J27G2p6t7OiIyUuzLeXd3pX4o4Grxg8GRjs9LOlqBr5kA7SlilH
+HgDSrikHrrUaU0TVyMVz2o48X8e5RfQQ9eFYH4+BSAJ5G/otGTZgdAC762porq7x2y3zMyaKojiK
+9y4LNVHn4bT3cNqrWG/Z5pZrDVx2yyl2AEsrcOFeiGW3RAWHjf+sZQUAaJRTyeBEPNhxzbPFwiik
+b1v6SUTWL69PJpPDHFxVlXe+1W4ZbY7Fk6auXpiceIuqalrGv2txdrJp9xoLCuUdjStfn5y89nwI
+5bFO+XinuEH0nXP9QX84HN5jUzOlVZnLxtY5FCUEyDpZQOtKl4diywfLAuDyigwFy64IvmQJoBNB
+xHLH2VFQ6e2iQIglNEpobOFJFG7CqCnDNvQzmN18rMAtNiyqoy6z2UwpFZkojmOt9d5oR0z+ZDI8
+mQwd045rbtp232d7Jy2L6Yvwk20YzMl2G/pHpQmHpxmvJTOXc72UEcJyNF2OpkOXXqjmr9j2W60G
+InB46a8ROAyHw2ajmaZpU9mf7Jx7abq66e6AwXEgUhWeW8gf7pRqz92XZbm1vVUUxfzc/Fo0umzn
+FMpjneLpbpHp6361qqp6/d5hfNwDQaQMZkoXnkMyj/l2KGe2rIQ0ImHUUSoFVTSnxSDpUHC6HHid
+kDC4PAABAiiDEA4hDYI0gfkJzm/IaQW+XhZaMDRQXXsIIrfdtCuEUISiKIu65VYURTcYSOZq+IgF
+eq65aVvbrlkvTYx6CEtDXCIJbejPyU4b+vfIIXV4qElxMXE27/f7/TiK0zS9pr1zppgzxaNh+1I1
+f6mcf+v6e77ek+O+ugurazrb7cirCZ5pbmwPmkfiw2iUZxZmT8wVZs/JiqLY6e1cy09NZ9MnG5ud
+TD/VrVJ9naGf53mv35vNbjGB3h2ISCsNCI1mMx9NXcHpYpR0iWeqHDnSwqNkWg7TeeMKBgGdEDvx
+VVCKQAAIVErg7qjqAjGAGcDKAFcAIJVZE4YtGTRhpCDsowM1RKSqqjqPbbSJ4igyUd17YveWEJai
+6VI0FYGhTzdte8u1SjYAwKj2aEK9JvTubk3wcCihWmvYyWACAJWtKltprdMkjeO4VoOGck9mW48k
+O5eq+Qvl/FsRfBxye4I/tiIXlmD9Ttmyla3c0LVbbWOMRrZyTwqgrvIx0z18zBtEf/e6VRXHxdPd
+QaJ342y1j1tvnHMvY7gl0mY6mEBAq2KKW6rqsc2tsLAT0bqqKhBxRXA565TYCRk0WvsiqIh4wsxC
+gvquF/ISGyU0dvA4Ajdg1JRRSwYNnB5m/zrnnfNuBjNFKoqjKIqMMQhvknbmTTFviidgcxbiTdva
+tO08xFBTa2BxhIsIVzVB7mxNCHCoW14yw75/0znz3k+mk+lsmiZpkiaKFAAY4ofT3kNpb6PqXKzm
+J/6I21ox6g18ZAArq3z2TlsNMPNwNGw2mqly7m6zPQBwrFH92PK0tSduU1bl9tb27eKVNUsggYSF
+B4PBYHCXPu5h0EoXAM65ypNGQPTBmphAoQQu+g4EopZiJwDgZkGnCgSARWuiCiFgYBYEfe+mrABN
+YX6K81fgtAbfkEFbBk0ZRlDuc+56uQh7IkjGmDiOo+hNA4kAWrpq6erRdKdkU2vCOCQAKHBVEyS0
+YdCRnY70DqMJJWYH3nJDBygHt7hTkbzI8yKPoijLstouIoDjyWgtHvV943zZ7blDVaIdHiU0ztIz
+begf59fvqDulAFzM0ymaexlOJ/LXyq+qqur1DrDgmbnf7+d5fu8+7sEQAAZh8ZZDxSpGwN2suk5o
+1+FEEBYRAJYQRBgQQCHGRrkAPtwqKv+e1nknauKTvssmIbkjC9KDHuHSCJcAIJa8KcOWDFswuFk0
+b1gnRMRaW7ek1lrXmnDNQKpTCqfT/um0X7Hets1N2xr4TIDeXBMktGA4J9v7a8JhgoMNE25Y3G9A
+PVSlVJqmddNzRFgwswUzm/rofLlwxbaPMnuAOIaFCc0ty6VluXgY28+BuUiPjw/an/RAjKwBgKqq
+dnZ2xuND7faX5/k1uuRbiu7CMp5HJQARVtPAVlREoWJUCCKhYpUQAKiIkJCDsBcElABKgAiJKTE3
++QANVc7rHBFWogkABMGhzwYuG/hstH/k/iZUmFWY9WANhVOYtqXflGEDxgeewnvvvZ/NZkRUa4Ix
+5loEKSZ/IhmeSIaWVd9lm7a945oMJKjGsDDGBRRuweB2mmDhYCdYc36Y7vAhhOl0OpvNkiRJ07QO
++Da1faa58RhvnS8XzpcLB57k8BBUm/jQQJbX+I056O1zZB+X1/HRI9mIZWTVpfXLk4MSEfcFGiMQ
+YC+kUViiNHKF4yCk0FcCAMISrAgLBFEaMSJfsk6IcyAEpRDkJgU4Ho1uCO7WExsABMGJTwY+G/hs
+6NLDM94EKYd2XVFF4lswbMqwLf39bSQAYObaQAKAOoAUx/E1AymicCyeHIsnQbDvGluuuWXbXpQg
+Xa8JO23p1UllUmp2CIO4X5oIFtvQO4z3KSL1IKMoStO01tWYwsPJztEqQA2L6Tl6Rwv6x/mNm5tQ
+WIgu0ZHtwAUAhVffdCdOw/h2j2L/yMdbirVjj4gIILKXqKGqsVMRogdfMuJuYSkieMtxS7MTRNQJ
+QSUsoAgVYF459dO/8ht7T1oEiqotDJWIENHeyD0hJMrPm2I1Hj+U9JeiSUqOkCvRhy/LEKQKswl2
+d+j4AFdKyBiVAXsgHymEYK0tisJay8z12OrhEUJD2eVoyioe2D08H8QKsxEubuPxGbYFEER2cO3A
+QXqMhrTUw9UKEgNWH65Ws27/X4e5tNYMdK68Vwvk1kC0mPXwmIDKYFKLpgD0ceUcPVNi82j3Yqkw
+y7E5J70HbafXyWT4ze9+FhGQsBg6ECBNEurfGUhhvTLUT4O9SBAMwCKIEJMiwsCC//DPb7wrknBS
+zszLNgAopYwxxhit9d6Q5V5ESePrw2NX8rtnCtQ2Ur0sNGByyAdNRPWacM1A2sIT3+zt17IFQe6C
+M5zKdF625mXrkPQQAEBEiTsvuHfe6bXuFJGUa/x6BpNL9PgYj37BuYa29E/zS/d9C5KinF5cf01Y
+/uhPf2vQ36JMGD0izrYrVzBprGnPoWIAUIbIYLBcf+hLJoWcCwZoxkYRBmb8tZsUAABAZEE2jsvr
+e2WxDtTUyrDXKO90OqTM5y537kUH3ryKhD02UnFIaY2iKMtS13z4Cxv3lPhMVHh38+ymbV+uOgVH
+18VzhFswXIKNtvTloFWfgV6jdxZ3Ukd/9xBBkJtb4x85WtI/zd9RyG+PzTMa9xpZW2sDAN67y5tv
+fOOFz24PLw2HvcH2TpI0+v0eKTEtEABfhGrsAUECSBBUKCwAECwDgIoIANgLVSClhCBxpBqJhttu
+k4rYw7VCGg/xd6OrWyTtDdQAQK0GVzkO8P610V9f7mzcsw5c82Uvw6NGyqaM2tBryGj/xlXWWufc
+wtwBYdCMqlTZgWvcLkSzlpXX2EoTH2/YzqZtV2wAAJAm0D3ZjU8k2cWLF/e/0Do+9jZJPwAgvj2l
+MBPsnqN3nObvvKW9LkPwr5998bVz35xOx+965v1FOS2K4oVvfW5ndElr0+9viYjNnfXkqkrHFJFB
+hFAxEiAhi4hAcKxjQkIk9GVAhXbiSRMBakMWgvMsAFrRfh5hjp0z9O6H+OUWDG/+ax2rqT3UY8eO
+acL3r40+eXF+WB1ZTz+HyQCTAayAyLxsnZJX9jlYRFI8oEB5KZo+nm05pm3X2rHNnmvc4MovxTP2
+u/yflq5aeuuxdGvs01oTNMHT3fzS+e39r8JRp5DuWykk9w0T7J6lZx7ml45cB2az8WvnvhXEvvji
+V0XAOzfs97Z3NjxXw2HP2rKs8nLkdEJIYDJl86EyiFSXeiESAmKwggSIiAQcAFy9WKEEIYUcGFgR
+ISmKCEVAbo4C3YCA5g16rgWDpowSmWYw1TfNxMPhEACOHTumCH/2xPDTF+fG9qj7WiIOYWlVzu5v
+hSNbTexv3743IwsAhngtHq3FoyDYc40t2+67hhUdEWd+p9fr1StbHXEi3OX/PJldwbidT4r9s/rG
+mE7bLMPrPdfYsHNbtvl91rhhivNHqAOT6WA43Dlz9oXtnUuX1y/k5YQdcPBlmFlXYg9Qoa9YWIiQ
+FHAQFNAEOiZmkQChYtJKRRQs6xg5AHsmhaTAVxAqNk0FAiZTzAAOsASjkAWYBdRhylMQJ9CdYBcA
+ECSSIpVpE8apTJKrm68Mh0Nr7cmTJ2NFht4SC1GQhri0JOv7HONdlSqe3F4BQt6bhbJOsSGiQqk5
+niww9BmaxnQ0hqtU1ul0ei05rZQihEz79fWt/cdZ94xBhMVothjNKlYb1dx6Nff91MrqqHSgKGff
+/u4XvvCFT2ijyqIcDLesLxWaAK4mrplEAUDwAgBVvlsdlczpYFkYhKUc+mROR0q7PPja/bWMiMEy
+JmRSMimRRm8FEQgEIgCLBmk8q3Qa+SB3ygVCi5nFbATLAIAQEskbMEpkWuVjuHhxee2hfnn3/KIa
+J+L+JCQ3V8wM8Nj+CmCtSzRPb+8sYCjrPCUi1uGjmt9GCF2TE5Wb1+e/ak0AgPrIsiwP3EV8MpnU
+qbEkSZRSMYXTae+hpDf02aVqfsceDTfzvmN2hzogAGFPo4PA4eLlM+uXzn7q8x/pb/VIIwD4iqOG
+qqYVIIAAKWIWpYldEAYkIMJq7CuEuKXtLAACKizHXqcKFRChBAFEJNAx6UQJC2kkhb7wcdfYWWDL
+bEB7MFpZFyK9rw9wiLtSBbYKaAECgKjKxxf4HpkwmsLj2SYhBMGxT/u+MfHJ0KdBVIlZDs0MbktS
+cM61o7Bz+xbl1+y3upKwLMvJZLJbuxPH+9g21zThMKhL7/M8N8YkSVKzm+ZNPm9yx3TFdi7buak/
+uC65ZZxnKcIDunTMcP4sPfswv7iPDghAAc0RLblo4VQ2gf4rzlUX1mSWDxoAACAASURBVF+5dOns
+F7720d7gCooCgeCYCIkwWAYQEHRFiBoKGIJlBEQNyijxoiMiRFcGDlKTfJRBOw1xW/vSRk1tp4EU
+kCZfBADwJSBC1NIg4IuACh1yjCqJVFEFlqMgw10FBjD5vco/zKlZ3SRDodRCAwAsOA1x3zWkaoG7
+rQJYa7M2324AJF7dKpR0rXbnyCMp19QmjuMkSYwxhvhkMjgRD0Yh3ag6m7Zzm4oCeaxTvrM7Hgz6
+fdc8X3WHD2QjxBnOnYVnT/OLN9OTSsgGtGJNd6lJzzarpXSGSBsy9ycf+1ef/eIfl3mZl9NgGSSw
+Zw4imgDAROS8iEjUUIBQDl0yZ5RB8eKnXhhQoQhwJexZGETApFSNPRKwExSIMuWLAEGSOVP0XLpo
+guXaWDKxUilxIjyEWKlZ4bwH9f6//Rv34cndHi5Amp/zrqpj7VczvhCTnzNFO9qNO90SIhK1lvbZ
+xymgrkuf3+aETp0kLsuyzq8rRQn5pWh6Ihkk5Co2e3sFxMQ/tTp5cr5QhEWRZ8quxaMFMw1CMz6C
+ZqxHC4dJju2ObNePtISsh6v96JGotXAsnf7YCTretBQmL73ytcFg+ytf/4uP/+WHBv2+rSoEAAbS
+yEGEQRkEBFeEqKnFCyAQIWlSESGDzZlZGCF4ESdSig8cHLOXKFFKYzKniVAYTFOhJuHdDAB7cbNg
+MoUKmcVOA7CIA4NEhD4w/rO/3JqEZBbiivUDYp4u8voav16/R8Q64VCn34hoNBrdjqmGAK0Tz3z+
+ygEJUZKQwaTF/aYM09sbVG8djNZJml4jfovAJCR1pHWlEd6zNHnpix8f76yfePq9qKI0TaOrhTjn
+y+5rxcrbP+AD0ZBRh3sjWlxoqjj/rh+99uLLX/v2i1/9hX/3V86efeXb3/3Slc2Lg+2+sAABEQYv
+EuoO/WinofYBhEVFJCLAgBrtJJBBCRIsIyN4IUbxQogsUvqAAoTYWIsbJ6Jix7lZUAkJgC9DOm+q
+iSciDhzPmWroVEwmJm8lOOZS2l4HL6UL+Mkvfae+By80C9EsxNOQzEIy8G9f9feNEHksPH9LW18p
+RYTO3dYTXT752Kc2Dmb7XIMW25DRstrOfO/A/O6RI47jOEmupdUZ1Fw7+5Pf+d8+/7GPGE3Ly0s/
+8zf/27nlU3VDuzhJLrvFV4u7bNP7VmM6uDi48mL+6u9yVWxuXVLKbPfXx6Ph/Pzi5vplnShfBQAA
+AdQIDKiwmniTKEARARBABBWRCJSjXUu1XhzEsXGkiOofCBEQMQR2gRExXtCNk3E18PG8Lgc+aqp8
+4KRi3VTV2LOTmikVd3SUKZVQvm3txHdMpAKWNuiaWAYAGrmjy44uAUZRFPmo+/x28x53wbhLIF5S
+TyzyeiKzFKZ7tTCEEPbN9iouCYUPvXGBx2iES6c6JDt3040UEe9FbWryHBHFSRLHsTE4Gw0+//Hf
+IwQRKaajl7/05+/5+b8TJVme59O87JnVu77WW4px7/Uv/tE/mA2vaDfa3LwQJ0mZFypSwYYebJqG
+liDsRRiC52w+8pZBJG5pQAAGlzMAoAI7dAAgQYRh181lEJZMq7r7gggo2iVB+iCKULwAACocny8p
+Qu/YFwERi55DhfXZkIAU1oWR4qW5GqNDHAgi6H6vV5sZSilV/6N1HMft2H/gxPDsJP72TrMMb7dp
+VGLzknoSAEh8CtNUpg0ZZTI5kJjpnY1IynBnaxfeqv7rMDiSRYOZizwv8nwy2Prob//PIQQgIgBm
+ufjyV1yZ/8zf+vsjXLpMj3p54CJCw63v9NZfeuOFD1146ZNpu1PNxjqmqiwBkRRgrCSIq4JJFSAg
+AQK6IpBB9gIiwbEEAUAOAiLCIEHEg6BwkDp6CgJOQhIpRBRBImQRFiBCZomI6mIXlRIHEctIu3uM
+s2MRqAmhwbEvWcdEEc62rDaUoSoxaLi6w8reMF+R5ysrK2maPtKu1jL7Yq9xdpzcl96rgjqHuRzn
+enACALRUDRknMs1knML05noX71yqQ3WHGuuK8YPQU/PVb31psH25tKGRYBqb0bRqpmbj3Mvf/Oy/
+Pv6z/40mDA8SHzn4cuvCV9944SPrZz5VzobKUDEe1akoX9bETBEAUsBehKXmKpMGYfGlsBcRES8C
+II4hADIgQ20SlZ4JEK5a4cxAWgGDiHjPgTkEBpB6+wf2IiAcRIJwEJ0qdhysoAJh4SAm3i0KK4ZO
+RRS1EEQKHZRF9Tdv1R7dez8cDp1zSZLEhtaadrVhh1YX/q1tNnYgGHWFjRnND+nYDp0Y0HIFDYcx
+imhwCKCUOuuOyx3u3dTDlQl2K0wB4O2PEdV44XN/9tcf+xAAECKzOM+eJY30eFYV2288emLhXQ93
+er5p+UFQVRAJs8tf+Oon/vHFlz5lqwk7Fxy7WbCzAICkUFiCE2Gx0xBKBti18qHuJuiYA7ATZmEr
+4kEFNEgiQoSKqO7QiYiEaOY0IqolZUfB+WBdsD4wS2QUAFKHQhCxki4a0uhybq5EpLAae50QaZQA
+iFBz49wscJBq7JAwOCHaNxE2Go3G4/HCwkK32+0m8IETw7Pj5Ju9htt/r+y3CwJkIeurDABAAQpn
+Mo7KMtAde+8ClGM7h/YOnASRTCZdGqyqzbeimcctceX8mS98/EMsohC1osAiAs3UBBEXWEQ++eH/
+tdlZaK6tTvbrv3k34OCEgzJJ8Ha0/crcytP7S8UuBF798u8ML3+3KqpgOTgJltnXmSkijewBANgL
+e2EWlwedkAQAEA4gQURk18dlAQFFiggFSBEAYLyoq5FP27rxUDy9WCWrhhKcXiqFIVk0zBIKFgWa
+UQBUhOXMuxkyAzsuBw4QRSDKdDX2iihqK0SIWzpu66Jn47YWAWQgfVNnuLoM1zlX5/xFZGdnZzgc
+Li0tdTqdRzplJ/Z/cekIdnA5eiDlOJffe+gKscB2Nqd1sV/d7RGiKvPnP/dnAFBv5+QDa0XMAgCT
+3CaREgEk+JPf+vXn/v7PHa6v136YDi8MN1959esfXnvsb9hydPHlfzMdnVt+6MddLuP+a3GWHXvk
+fc++/79OGrfYtIaDH2y+uH3x65df/cz6K3893hrU0l8XIyhDIhIsc8Br/qsEESehFLCCCdWiLwJS
+p84ESCEaNA0Vd3W55SlCKYUyTNeyYsvZYSCFRd9RjNmJqLjoTFvllyvdUNG8xhxBg68CEZVDX5/Q
+zgIRmliFXBpLkU6VK4Od+nheFz3nreiE5k6l3nLRt/jhm7ZJjaJoYWGh1WpZa0fjcVVWIYQQgjFm
+aWlx2899dXvuXn+EBx5PZlfM6Mzbc61P/f4/PfPC553nSJMIyFXXmkW6rWScWwTothMRaD78Uw/9
+yr/Cu6192Tr/ldH2mZc+/y+DK6qin48vAwCpGLUDAVcGCFrEmaaaX330He/7e4+/+1dvWA2G2698
+5c//B60al175y3zQ95ZJoTCwZ1QYrAAAEoiAXLXIJYB2dbgMIAbRUAc9RQQJlaakY1CLnYZIq3Lo
+O08l00uVEtJdZbecRKBisnnQMblJ6Mw3vHhXBl+xblGdMmMRUoiEduJRMFuNSGPZd6RRp4SE+Y71
+Jac1HciJiCw83vBlKIdeffCX/7Mbpsy62cF4PI7jWAC0ieIkSdLMRHFlXQzlqaS/HE3nddFUNiFP
+IAx1TcaDlaS8F8wsVpAJIAET3GFQ6U6wvX72y5/4CAePCKUNkVGR3i3FbmWRJpqVbnEuJURC9KNL
+07NfyFaf0807rja2xfCNF/7o23/1v0+HF2ejizafAIIw2LyqTW5fBOFgMqU0eT9af+1Tl1/7VGv+
+dNY+7l1OZPLRxmx0YTbYeOPrH/N2xhyQcDdeKQCCIsBe2Ak7FitSCXoEL1opRYgKURAEdKaUobhl
+Fh9pliPXOZ66mtlGAAi+EooQEXzBpqvZC1cSzxkyxJ6d94wiDGBARFCjAISKTaKQQGkChZ2HEiJw
+BUctnXYNAhQD1zqepN3IpKoaeZMqDuJLbixH+OsfswYqA1UMlZEiltJAZcDiVXYHESlSpHZBSl3d
+ixcBUAAAEBAcqyKYgk0eooJ1EUweHgh37d4RSZ7KrAGjVGYJ5HCdi4zr+EiJjYaMMpg0ZKxuv6cB
+g6Lrw1avf+uLn/zIbzJzrWBF5V3gELhyARBFJDE6MMeR6jTiVhZFmgAgWX3Xyb/1z3TjxoR3VQzH
+vTcW1t55w7Q96Z/vb3z7zNd/59J3P2uLmQRBjfX0XKdg6/b51dgDYtxUUUPVxSJICKLa3cem/Y1m
+d7UcF8VsPYqXt994TSekDNXhneCEgwALM4gTFQAF4epGEyySdSMVqair820bNVUxcp1T6XijJIU2
+9zpWpFFl6HY8tUkYkBAB7MjrphIrzrJOyBfBNJRJlQRI2kY1cLZZ+ZJNU4kX9tJaS0RkfKmMmxoQ
+ghM3CypBk6pixyfzKm7rwRtF1NRxS6mIpleq5mqsA5kApoTm3jbEKGzARlAasDFUkVRpsMYXcPXX
+rbfD0FohqVpDtIK2kRZYgBwATRTt+PbL/cTfy2baDwYsZhayESwBgBKXQt6AaQbTFIrLcHKICwBQ
+QrMHgMIxlA0YN2DauD5KO4L5y3CSgBswWYuG2k9m4/4nP/JPhDkEnhRuVjhGXVmn44xUFoDj2GyO
+xnNz82VVDGa5gkkri+aaccs/P3j+/136mf+yPjN7u/7qX73y1Q+vv/z5AP3O8kOP/eh/4spCMH/4
+uf9wce3d57/z0XMv/tn6K59jz6SJg4jjehZERF8F0uQrJo3BSTUNAhBlCgDYsy/81ujbpMhWAxBw
+ecgHZ0lhsMI+BMu1PQMAtdFPAJqIWYgQAERAIfoihJLzUaUiEqW44ulWJSJ2FlRMwbEIatKsQCoR
+lmCZFEUdDQS24mRO1+dXMZHBfOwQAGaQ77iopRpL0WzTVmMXtbwyqBOyM++t+JKjhmIrlfNRi+I5
+ozSiQg5MRgMCRViO/G24+0gOEgd7KbuSYLlC220c1yZqEGAnsmfCozehtDFrDd+Oim9sJW9/Hu2t
+A6OZQWcGHQC42hl+D5AqyCrI+gAAkkCZwayJs6HMjaENgAxAsT6xErOf/+M//23v/axwk8LaQCab
+azZaPB43Gg0iChza7XbwMN9dVEp576ez8WQ2HWyMs1i7T/wv8z/6H5euPPvNP37pr//5aOt1FYME
+iZp6vPPGF/6//7HZOZnPLr36jd9+6KlfRMlGG5dq8jB7BgRSyKG2VQIH8eV1q1aouOg7nZCOqW47
+hQrYCymIO1qGjj0SARIGy+wEEOreJKQBBKAErejq0xERYIFsOaIqhMAuD61T6XSzzBbifGDZSzoX
+TbdLQQme41hbyyoiVMgiUgkitE8lbhZEpH08cXmQbQsGdEwqQl+wnQRAyBYjl4fKiYowW4qmlys3
+9Y0lE7U0OymHHgFmm1aCuBlPQ1VHq2x5B21TsYT0PJ9qULVAAwPWoDfg65+fiLJGA4D6OVvnDfpy
+MG02OGu2HqTUzZFjV/gTJcezcuZx4kzu6WrGEEtIS0j7cp293tBuMpnks9mZb315Z1SUTihuNztN
+YwwRaa13O38JEO46A7sVC0nMcwubly+W5XBQIJnku5/9v176639e5dukdXBe6o1vDYHIdHhBGGw+
+OfO13837LljWCe2yaBwL7xIT6pjM1bvBOiJZC7QEIUJUWDMr2YuKSIDrXmsmVQJgUoXEdf8FHava
+iFeEwBCqoFtaBNKFuBhWGKHkkrRNOXLVxKPCcuwgiDJkZ769moTAySkTLIdNSRdMNfKE2HwomVyu
+xhdLk5LLeXSpMJkijXFTl2OHhMm8trOgIorbyk5CUbqsZQDQV6xThYpMQuOdUgKUQ+8ti0j3sUxF
+VPRdOXSttfiOq7dyjnPe5WMRcITOYIjYRxPfTs0mZ6OgAACBzShEU7CBvldsoESxANxpFrkV8dNd
+R2y5/jbQ1Oncq7FTM0+eb7z79Vm8ky8uxw0Vt3xgFTdbc10ACBAIanLom5OGgOzy5IQZWGvdXVya
+9fL5H/8P/vIj/9Xr3/jD4D0qFGeRsO795JnZi8lU3NbBCYiQBpfX0Umpu8lyALBCDAowAHhhqWdx
+hUioNAIiaWQWn4e4qYMXIvQVx0YDgslUTcSXIBqovnTNJDaRjudMuV011xp24CijYlixlWrinWUs
+AnsJjpEQCdL5iFkQgTQVY+erYGcBEaOmqsY+71nTUKSgGPhQERKESsSHuK0pQmXIZJB0tC+56DuT
+Ur5j47Z2Jdd7w2RdQwjjS1WoJFuO4rYyOeU7thr52pTyJfuS1b/1d35jN35z5y9B9KAtmELiKac9
+G1d1AhvrzQSUFXXXJ3+bX4TydNetZn61ETqxpEY0QRFw/28tpvJkN4gvq7KqCSoIEGtpGllIwkoW
+FlMBhJm/9lgg1nCiTcfaevX008Pt9cloQDpi5nq+r6rq2mYcSZpMp9Nms1l3Ha3bx8fN7ul/7++9
+8tKfXn71MyxcH7kbZBQgjXDVKFeGlEFE4AChZJdzKBgrAAe+ZFOTkRUpJGBgFNSoYwJAMkiESEgK
+dUS+5Kip2TNp3HV8gyhDiAgCdU0WAPiSOUiwrIiCDaCQhYUBFApJ2jFRQwfHZEgZSjtGgrRXk3Li
+lFFRpm0emquRiogt+1KihkrmzORymc5H2aLJdywitE8kpNDlQccq37bFwLMVX7Kv2OXMVnRKJiEQ
+mF6x3ccyDoKE6bxBhdPLFRlEwmBltmWDFZMqgMM1y/9BQGZAQ6gluKFDpinTMKjeXA0SBZGC3IO/
+WvyUajndYeccM6JOoa58rflbUFOyyBAr3F0DNcJKA5YzUcjO+YUT7/iVf/hbv/ub/925V54XEQLa
+O/czc20FhRAcuAgjQmJmjJOotVROxuXY+4qzBQMAwTIpDI6D5Zr0Agh2FpShqKFURKahAEGGrBCJ
+UEAQQSsEREUYRBwK1ELMEiwmHS0sAOgto0KXh7q+xM4C1FUmBQMIKarr1l3ho8QggTbKpAoJvOWk
+HVUzn2TaexIBVwQdU9qNBufz1kpip/7Ki+NkzpQT58pAGkRgvF7OP5KFkhFRxQQAduYj0GxFxwoR
+in691jEQzj+cxm09ulhGTdVcifMdW/RcNfYqIh2TK7gceUTMuibvu2wxAgSTqGrio6au41flKOgn
+5gEAqgCewTNUHryAZ6juaSOi7z2IwMhFtZTXYtgr3zTeUg2PzoEmEAHPUHgoA4iAD4AY4R6GFF4f
+JRXAhZQSDS7AQgqKBAB8AACD2hidxo1O3XKPmRGRqObDUP1fRGRmAwYA6s+r8dZg8xVXThFBGXR5
+qJ1RERAv4IEEBCGg1H0CXR5UTFGm2IgkYDJFMUJOKAAGzZzGSkJPpPLCV9cQ2q3S8iXXnM3d1BXf
+SH7VGSlFwUlrITWx8hV7x9XUJw2DFJoLSXB5YyEZbeRI4G1gJpr4uKEH5/MoU+m8mfVsYyFmz1FH
+gUDU0HYaAMAOnMlUMm8aS1G+YxsrUdRQsy0LgMm8AQEQKQaOgyBBHZ6abVmdUNqNTEqkcLZl63ZA
+4/XKFaEA5yuu6xAQAUSCRfaiASAz0Lg+ZF/fZhDwDKUHzxDkujdyU4P/73UUHs+ONdQ9gDVogvHV
+Lluphkfnd9dKRDAKjII2AAACKIYb6+huWFQjgEjt/SMiEQOMKticwcTSLjdgD2o1uNZ88lrVgYiQ
+kNYJh4AKUWqiJdeUG/JogLRCFGQlbABj2m0QC2IyxTFRhDpV1JJy6ARFqmAaKmlFxTlfp7TqGsV6
+gocCas4MMKAAASoER7vvUIH3ISbtKx+luiY+NLtxMbImVcHz4OIseB5vFkigUxU2pbkUFUPbOZ72
+zs5UREQwZcnmTTly44tlumRQAYi4gilCVJjvWF9wPSSTKRVT52Q023G+ZDJUDp0IBMt1JQ0gsJOi
+Z3MG9oIK2bEY9CULiDKkzG4yARF8yXWNvH5tCIZgLoZmBM0I6knv2pKtCZJbBYoCgxewATzDzMGg
++P7pgyYAhQfYE+CcT0Df3jG+0xBvYBiUsJWDO+iRXbPv3xybCCKdfubnv/ul358N+v8/de8aq1ma
+nQety3vZe3/fd651qqqrenp6eu62x45nDOM4yA4CB0XEQABFJBIW8CcCBPyL+EMQihDEEhYQCaEo
+WAlOIkeRbRRbYXDs+G4zvsSeW09Pe6Znuru6bqfO+e577/ey1uLH/qqmZ2wzntghw/uj6ug7+1zq
+q/W+71rPep5nPSMAQzYWZELviJkAjGckAQRsMgxEQmTkiHVUUCOH7akvo5paHdWf+dOXupp0WJa8
+rSoGYvg06BEAaUrjkAm1ivNUFUhB0UwgtK4OEk6dFBs3BRH7VXaBzFMZKyIA4UQTMjWptr/Kk3PJ
+5q3h5Pluf5XTttx4z7yq7C9zPHJ1yGHmTKyOCgDzmyH3Mq5rHZXdVANQ2so08ggB6qi7B8nM1GDi
+w2k1JCSHkkzrpLZBEKtJ/cxUrAyChLOL4BCgKjwZ4MkAjHBzBuctfE3ghgkYIDIAwGkDt2bw2vIb
+N2sKDC8eQxZIAlmgL4cQ/wOu5QhZITJEhsa9/Tj/GqsqXPawHKFhiA5aB0ngegA59M+/eiEiIBgc
+Dvvpxel+eLYZVLWNx//af/jXf/QHv2979XBy4SAFJgSEMjnfHDHPkT2SmlZzkSbmmRRDnk5TKr2B
+mVbjAJKVPXrmqS4clmV4lGG0qe7GpwsAVA0RY+cXJ3FYp5KFkVW1OQoukFYDBA6U9wUQEY08NXP/
+5I2tVO8blqKlr76l0su4LkBIDsnj+bvn+6sMzuogT17Zh4Ur+wqAvqFw5GrSvBNEMIX1vREMkA50
+6zIoAEyCGBMzA9/y1N52LfmOSy+lt8XdCAb7x3kyiwYDLUYO6qju7Y0cBdA/mJGfGQwVLntwBIwQ
+GIr+0brS/1GuKXCfXWUPdzB+PXs1K+S30aIRIDJE97YtQQAIVwOM9fApT7BOcP30YtxX2L9ty33l
+G/UVyQ8CflVGNNFyp9CH6RIAvXj+W/71v/i/v/obP3bv1V+9uvc5zSOKAYI7ZZoRTHS0YgCHnlcd
+1MDqoABgar5j37GplX2tA+ZN9R37GU/ThLobvqzqVPXaZCEIBzGuqjGRieVdiXPfhgbMyHjY5db5
+cVt8y7mvInp6p7l+czc/b1YP+uNbHUWoSX3LruE489uHaViX2XkY12VcF98wOwwLNzzJxGhiw3U9
+bNon2QBMD5NgEEHF2BMHrEknWqBW8y27htK2AkI8cpK0DAog5AgQSq9I4FrSat1ZyPsq2ShgTfrV
+KNBlD5sER+GQB3sCQmD66jsBETyBI1h+ZWR8Y66xwutrCAyBwRP05Q/7qyb56uuOEX5PudbX/EHP
+HngW9Pi2L3o2DkdVzTRrKpaf3H95/OKv1zK865v/VNOefut3f/9v/P2/tn30ReqIOsIpie/FtZS3
+MvWwpkoREMkjB5Jijacpsc5bkaqSrY46fy5OetwQediIPv0XsaP2OKR9kWph7puTJs795mqIDkDQ
+2Mq+ukjtcSip+pZlq5evbQBARY9utyYmScZNyX1lT8MyI8HsRsz7WpOevNClbYEJMqtQk0o58DJM
+zdRcw5PCpp2yrGWJN5wklayVERAm43/fEQDUpHVU8lO/D+tYOaAkRQR06NvJNkI5YnvqyaNrlx/n
+Zr7HG9Ae/DaywpOv1IEQACAEAkIIDITgCZig/iFO/UBg8P+WB59GOI5Q9FB5bxLkf9o6QwF2Bd5u
+ivVHflnpP+33nKhSo+4LJoeuckVsALyqPoODAGA9Xj/evpVLdg3d/9/+Qi25DGLVScns2fbCgNiR
+PRXCcqAJ8dBqxAh0aBGwJwMgAlXzLYOB6wAzIoEBTOJxFcOAAEB0mH/EgRBxdt7OZjGbzDjsxhw6
+V7LULP4gVoT99ciBXOAySjN3gLi83/uGweDk+da3fP7u2eb+iIzNsU+7cny3HZalOfLjpjQL51uO
+MzeZR6iAqbVnvo6qRcPM5T2krbBHAKxJJy5GdxHyrk6D8dJOmmMPCOOykMPuRpCkU8N7mpdqamHB
+AEYe62iSzQzc47/778Xz28h+9u6P2um35OZFOv0gtjfeXt1NxhLTmfdV2TMCEEJkMIOiU1kCAHAU
+oHVgAFlADYqCGIgCICw8FIXnF8AIAFAV1GCdYTWCGkzNeACoBp3/8s1zo4XXNzB+Pbn7N/4qadv3
+l1f5QZ/3SIe6rYkt2THws8QDkwzbvCqSXUsmppaniQ9pUwDBUM2BnztakBnIoOQPLSpy6Gesxdgj
+8oSW2nQh9I8zeSQ+OGlqVfaY1tXPmCNNPRFmnLa1JAm3umGTydORb1b9oNWa6IYkYFaqIGMzc+Mm
+N63bXg5pX1wgRDi9OyPCfpVMIO/l6rV9f5XO3z13wW8fj6BQenn8yoYD7a+Saw452EHvYHAAYSuU
+QRChJpURyGMdhBjbM48Ikq09837GeSdIgIxTDQBm010XZhRmXAbNOyl7MYPSq+9Iq+a9ut1me/TC
++xRpHsuTN39us9kDgIazi/d/r5ud8+yuIeXwHLru9/tfnHl4fnGI1Akm7wsMAvMAkb/6XDT76lc8
+AwDcdHCzAwCoCmbQF1gmuLeF52aHBxzBi0ewHKEqVANR6P9/vhl+/sf+q5d/7Uf6zSMkkCrsycTy
+Xux4GGF0c7epEQCe1HuyKoZGnqZ8RquZCTFOwJ+pISHNUJOCQ1PTCpIN7FAXglkdDxYjqAAIOqVZ
+CXCCRxCAQEVr0rwXkKeQvx1SNN+6Okp7FFBxN6bQuKRlzCU0zsxC69K+SDJEzPviW3f6/GzzcDi+
+0y3v7ePCS1EpNr+I3bmvg0xjq4+fa5+8tptfNORx9zgtbjdalAOxJw5TjLJmcy3HYxoe53jm4pHr
+H2eo1t4KNelUFgNA6Q/tuVyFHNakizsNgG3vp7hg33HeycQ8/Sxb9AAAIABJREFUbU48ANRB4sKF
+uatJHb74ke7Ou4nUObp754apVlGm3l/+I790MbiHj673Q40X3+TPP0AnH4zHdyCcwriE5iCMHCp8
+YQWdA0YgBEeACLsM63S4RDwDAgQGfNvHkX9vOMUTAEBgOGkA4DDTa6xwPQICnLdf/qp9gSKQpwsk
+/fMeXvV1rsdvfeoz//cPD9srJAwz5xoS0bIX11Ccc02qKHtcUYilFDMtg0g2k4MHzpSrTBCKZCWG
+ulNlk960qClYMRJARTWrohOaOXHrJs1xEQEARIqBslMjADVTULA6aq0KCDqZCxiUUbgRU3Odm5QA
+Ab2ptUehXyd2hIhEGGZOqqZt8ZEBYft4lKKbh4NvePdkbM99GaQ9DYtbsV8WF8lFZo+IHDpeXMTc
+y/bRGGYMZDWZbwkQJp42NSSDYWcG4Dsij7ozMEBWSdqee3I4LnM4cs8mI03EjancL72EGbuWyiDE
+GBYu7yRtJcwY//yf+5N3n7sZGw7ezWad99w2jZkAQgxNP/Svv/GYmYNnH/zRYp53y+HRm7tHb/HF
++9z7/43m7ne60/chT8RpBZX6+FN861vxd6mNf/daRJh54KdFtps4Ob/Pw1NXDhHC7/XEk+GQQX3j
+r1c/8Q9+5f/8q6vLL5a0Q0AD02qu5TTZuiNMehRJSg4n6MO3XJPuH6dJ+Adv60JqNa0KBl4ICAzB
+0ESNBFmAiRAxF1FVZorez9uQax1SSVnUzDG20VUzY6yjIAIaZFEknExZ2VGcOQpMikCmDmPnaxEC
+LEUAgJjSPpuB89Sehf116k5DGaQ99uv7/ek7Zvc/tTx7cQ4GpVapOm5KnLsyKntih915WL3Rdzdi
+HaUMcnSn3V+mcVckKyEa2HTGs6fm3OdNRQQ/57SuZtCcOGTcvjXOb0d0uHsrzZ+LgNA/KdOYMGKY
+wn3/KB2/2NZBtdpkSdQ/yeQQAJ1zLqUkSjhrV+t98Lxe782UmYn319eb3a5HxJTLYt6aQgjx5MUP
+dHfe9cYX77X3fs4vfzUujtuLD5Tu/XW3Hl//5c3v/OLio/9p+63//tcMhX2G/VeafCLClHZO98CU
+/EwR7+mAXb79npkeY4KLDm60UO2Qg031htjhg12Bb5wL4kuv/KPH9z4Jk4YQLG2KFKNdaU592tY6
+alpXAIhHjj0B2sRlcA0hohQzMag2watqk3ILAMEbImLj3KJruiZ0TZw1cd7EWRuD4wdXa1Frgjtd
+zE4XbT/mJ+vdk81ute13w7AdUknqHIfAI0gwbownRh55dnNnBBhI1UoWJJzsDbs27ldpdh7HXe6O
+w9SRrUlqkrQvUjTM/PbJ6FsnxfqrcfFcW1M+vtvVUeqoce42DwYA4Mi+ZS06v9nkfSVG37FWa2/4
+tK7syXW0fTNpVu4ICkzOP82Jy9sq2Vzk/kmZLsOaFAG06PQAAIyrun+cKdC0K0ztAAw4IgZTcPNZ
+c3p2HAOF4Lu2DYFCaEpJSIjAXes//4XU9wdWwG7fNzXmLE+u1o/W9WYcnWfab0k/GeMrkWFxk87P
+Pvrm41+RT/Xx4tvc2fu5+fosJBQADCb/wz8IWo8AiIDwe+yZhgEQHMJY4Xexkv+/Xm+99mu/8rH/
+kZ2uHryGijA53xgAIvKhPxVmjIgTjIOMNeuUuGsxYoquG6GnbFYAYer7chdjN4uLLh7FpmsiExIi
+4ATeTxZD6l2YNWHTj6KmqmYwa2IbwztvnU/Ya5/yL332C5fbLTpccGDAJrqxSgWJNwI6qllKqt47
+FxnMSpKDmiTQuMsuMADWopw0dI4cTX7/aZubRehOAwB057EOMmGgoeN45Ccr3GmA6bjKeS/tSahJ
+XMu1KBLsH2bXkarVUV1L3pM/duOmlr1osUmjw4E4ElcUE0AsewEAcmhi/VU5pAQIk1ICFADB0gEU
+nmxP3NHMb9Zb773z1PfFOUTYsEN2DgzWm/2+T2B6585FE13TxjaGJ1ebvh9v3z5rW9+1MUa/2w+P
+LldtE5BgHOrV9RofvaX2k00MFzeO5senFE+3/v3HH/xzXzNQ8rjcXH2O/fHJxfu/LvuDiaQ5/flV
+Bur0+2dW/6xXKcOnf+1HN6s333j145//zE9H9F1sVA0MTEyrTuYIWixvKyJKVXJk1eooIbAmMLE6
+CJj5E4biahWoBgDzLn70Ay/ZpF95GwdPzQiJCZ+10yaO3URkEDMDIzzMsTIAJuyaeOtisaLRAbbG
+JeieiqBSoNXQHy1mxQSZlaz04jzNj6OoqahkcZGJkRlnJ1FMp45snHlymFbZFlZGLX09e3FeRhHT
+cVumBhY7QkJu2MQmAtzyzR4ATIw8kieranJwlUPColaXZarpASGtp06ZwIEhgmb2tnvepo764ZCB
+Lxf0YKD65afcUReWvbIjEb26Xjnnzk4XNRWvwOzMNOdcqzx8eNU0fj7vQghvvPFwGHOMUaqNKTHz
+48erXT88f+dmbNzt22cXF0dv3rvcbgcwG8ZksFJZKnwpwMtMTv0RcPTOWTg3AGb/YDfvxc2Ob/m4
++OLLf//zn/g7pxfv+q7v+yHmrz1G5Rt8PXjr0z/+N/4iB9JqTISCeaySTYuqmopqNR6REcVskpVX
+E2SkE2cGtaiqIWMZlb2BAT5tHSCgfiVhTidbNSQ1VQQ1E9VU63adRIwCVrZtHftlDs4Fx5GdZ2ZF
+zzz3sfTVOq5aHXABETHKVvq6q0OIDqoUstks7napZGFPvmEVlaoAsL0ejy5aVCTCfpkBDRDdwiNh
+LWKMy3v7qaELAGYmxaSoKfQpPytonn326QFmkgwBZAQDIABRmN6ByTwPAZhwIhMe7BngEOUTqQTe
+jjo+OwLf1mj0zrs37l2CbwFRqg5j6toQY+jaUKW0TTxatCfH85TL83duhOBiEzyHk+Mu+FAlmVGM
+Yb3ZrzZ7791mu4/ZS7Xtdr/Z9P2Q5rOGiRwjMD25Wn/uMy9fnB+H6GZde/96fbTo2q4h7z7/s7+5
+urxENFIdamkDP7j8/Odu/9Cdl/74jee+/Z9RaP7hV067EOdS0htf/A1Ey3lIwwYM1tcPF+dnm9WD
+3/iFH76898WprkVE3QIaAsJxO7PWAEDV+v0ouTAhMZoHDGgEriMzcIGMzBRcS2HGdVQkFEd1KbIV
+UUVEYACEIlJNq0hNWkWrSK2aFdWQ2TFhzQOhMREzESERMWEVBTMwuFxutNq8OmMa0bKpZDMzZuRI
+RKhshOSJx30xsdC4NJS8r7XIdHSawurh3hSesVYn38+yLapGiFUMzAjRFMCMCbOpqCGAGRCiqSEi
+qDHRNNx0+g0RUdS8oyrKTAAgYuRQ1JwjUXUezSMAMDvHjpm9C46plFqLUM3FhJEDMah6555/8T3d
+bDHr5j4EEXGhac4vzpGAGbyLROw9BeeIKeWxaWZ370AVSyn3wz70ueuic1RJmLyo1CJPLlcI8Nyt
+8xCo69q28VWqAagpEamp92G73W13wzCMpdTFol2t9m+8+ej2rbP5op11zYe/44PE35Jz+czHPxmu
+1h5omZaf+63/5frVH3/Hu//k/eWbTXf75t1vPTp9Z2xOQ+yWT16bH905Onvxn0vci2QE/Nl/+D//
+1P/x37pIF7fe8/orn5zI93CoSYE8TuPZAKDsJa3r8WLmPKNHH5gYwcAFlqohumW/KaNAg3xEiMAO
+J2sQmfzEs9YRTQERyAHPyPZGHSHTZ+7fP4oRARyzc2wAjpkIrVitojxHZhd813X3Xn9tu93KNEXa
+pniSUlUBmJAJJ2+PULBWyamGmXOEslF1ZsH6ZQqt0wyEZNX2mzShjWZQs0wf6dNX4GD8ZjZMSCsa
+mCOsYuSoihIhALJiLYqIYFNAAzusZoiTi+iXmbCTHzoTgRkSAhjioe+kakd+/qFv+3DTNkxkgGA6
+DMP11bUP4d69NxvPHg+/lJIxUd7t3v3e908KvlqKC03LzjlGIvABPTOixTaCqWqoks9OZ6Lypdd3
+682+bcKsa1XNoIYwYwevvfagH/KNGyeI5rxDxCqwXO5KETNzjoMPZuqcb5tAhGenixB5MZ8N41hq
+rbWawXK13e3HYUi7PgmgiLoR+M2Cs8vX7/09x+5JyUv6MSEAovvbYmH4V/6tv3xx8R88C0rVQ1fs
+D+Rr+YdYpQz/9X/+Aak1jXupBZAe3Hs5zJkjSbKaBADDjCUrMrJHrRaPnIrlUphJzfptUtGTi5kM
+OkkZObAkMTQQs0lfe5gNMfW8YBoHTYyuIRU769rAVMDMbNn3alaK2OQxqFBFUxFEeP4dJzHEEMJ8
+PsvVtvvhaTwRH5yXsXWsAFNN0jguWxFTICi9oDsk/TIqEOZcyBGwtt4FaURkFClTTT35wRE81Ugi
+gJmiZgKzabCFPQ1nxGekOmSaCBeEgExTKg+Ak/QH1YyfeasQqtqz7zD9TDMjxL7fE0ItxXcdIZrR
+0fHR0dExANx57vbjh289ev01QFJVZq4Kjx89fH69ffzkaj6fzbrWnZwcMYFqJWZVbOYzBDGrpvD6
+m4/HlFTwaNEeLWbzWeM8kKP5bIZQ2dF6sxlTRoIQPDFKrRhDKSXn2jTu+bt3ncPFYsZky9WOCL1z
+TGQGpui9L7mWon0/Xl9v9v146+bZ3e/6Y13rneNape/T/c+/kS6vEWDugiMdi4yjwHK7TONP/vAP
+/Po//ps377wU2/bRg5eJHHsHxnff+ZHv/TP/zRSsu+3jnPZnN971RxX9P/fTP/Sxn/wfKq3NA+Q6
+ESpNTYtOKnJMqFXz3sLMwdM+pW+JPKXrCgA+sIqWDIgYGlfyU885BFREOpB2VADRJsARCBCQG2RP
+ddTQ8b6WWTMLAIGIEdvg71/vStVZdPuxjLW20YmqQ4wxOue6tj05Ob6+uprS30PPHoARwYA8alX1
+mFkxgCNiJhVzLRkBFSRGbmji2yChNrYbRyiABfyAKqgADtCx0yiV1ASAwdDKaJqnTMbMwPOh8iZE
+MZvGQsLTNN2e5kJTik+IVY0ImTFlAYAJujEDRGTCKjB1Kgys1nJytFgtVw/uPwg+PHfnuaZtAK3f
+9QqcddrjpiqEWET7fnv3znPTZehWq23TzT2bVEUaVAzBnOfLy+vtdhzGMefqPXjviYmQa005k2eX
+87jbJmY6OZrnnDeb0rahiV2p5cUXL2bd3DsMTXjr3iUip1RKUQAdxhyjG9PomAaRsq8p5aOj7uat
+066NDx5eX19b28bY+JxlN5bNdgAARgDE/ZDR0QsXx8/bETGm1eOXX33VH4fZSaRrwYbG1lbXbwzb
+R4ieub3/1j/5l7/3P3n+uXeZqUpl9/VNl3jl5V9++TO/UEqazmAk/OWf+dHHD78kVUPHHCltKyLm
+vZga7aQ5cRMom7dSetWqB8h80pJUyLU3MEfuHXfe2bXd537nswhKhJIFAVRtGoM+Maa0GqgxkFUT
+0JoBVDhSkrrgUIsAQiZ1jDlZr9XQikgGTVqnSnGSzE8Iifd+CiB8qmszBTETFfIEHVZTizABKb5j
+yZoGmZTEUmziKYGBZrWGgEBMBewEQ+BACFMoZ6QeSw0KjGIGAZ2gqnmmXEUNHKMpOkdWhIkIFRGL
+6LQrpgqCCEXNMeo0C0MBENWmb6KOUOskaCYzCJ7GLK99/tUPfdu3X1xcNDG2bQdEYGoAuVRAirP5
+ZreuaIAYDZjw/r3XZ7Oj3X7vvHfMPA7bQjTZHo55G1xorF1v0m7X51JNYRyL6Q5JurZDQtXRs0Oi
+R4+v2XHfp1pt3w9I3XK9Cx4BzPGo4nf7/Wq9N1PHPOsiIphpyrUUyaWKaEpluhCr6HY7XC+3Jdfz
+8yMznc9ni4vTfamP7t8jh2OW5y5u5dWWKlUoWa0WnTWNFNUntZqRJxe4DsPnPvHTB2TQwy/+9P+a
+xv6VT//MMF698OJ3/Onv+8t/kNAvJX3qE//4x3/0B77wO78hxSZVIRJINY4ISGVQFXORAMBVND3Y
+IhCjVmOPKhCP/OS6M6k30lBrVtcwE7zw/Dtv3Dh/9bOfRVUxm3zUMAEVEzVRM4Iq2gbXeG4bV0Zd
+79PJUavJpGqzcEYwCyF6NoTtPp3EMI8hV7Vo1zakIu96t1YtcOBRW9M09rYBJ4eWuVpwDhBKVTzw
+h8G3lHeCAOxJkmpR1zAHLIPWUcjRNNNuYgr1Uh0SAJkaIrhKoWMpBh3o3qhF6+0wGJNosnhBOphn
+MOFhTiMYADKTTa7wgNNxwBMPT5VpwobQESocBsNMYTPtrr7vQ4iI1M1mtVYyBTQmf3Z+agYt6b0v
+DZMUdTKys/22lBxD6LqZm4ZRet+E0NQ8Esec+1olRmcQAbo0FlEsNS9mM0J0nrxjQMxlPDtbOO9U
+dLPpvacb5xcqCcCqwGbbBxfGVLa7npCIcHojiBAQplcck583RESItUqtEoMTkeVqC2ZVbBxLv+1h
+AGbkCi++552/+Uu/tU79ogvI2DiOjTOwbU4K2I9l4aKMFnvIWUJkveuW169/7Cf+CgA03Xy7frHf
+rbr517a2/tRv/8wP/sCf5wZ957AXJEypwNRhUTA15GfyWZBiriGrwMkN16NjnBDntFQzowYrHCYi
+ukAI4IOfz+fz+ZwQDcAxVVCY1PQT/8ygYy+gnrhrfBudeKtZT0IcSy0oTsgxpVIDEzONuRLhJmUm
+UtFBagX97Cbf9PU8AgKoats2bfDTVeQYuyas+zGpVlIboaqaGbc0MYs4komZqms5FyUPU39aiiIp
+BwxzN3WXqmpSYTIiJEQTK6TiDQuQIwOrVier98mTa0pvDCw4zlWYsFRwzHpwTAfvCL5cABzgIMek
+0zGBaKLMONUMo6gBqFkteRxH9l5VRMWIHLuqddj3q9Xm6sljFTXANmDJwo76LPv18uz2HUBwogUB
+UxrJOJcKkL2PtUophYiYGzDMJd997qYPLrpWtfbDrul82/rj40WMXdvOzIqkER165jGVL73+eKIe
+ssPbt85EDMDSmHKVnOti1k41A4Ayc/AekZxzY8o5paaJwzC2bRMjv++9zy8WbS0vqsisixe3zlff
+/NKnP/WKQCljZeJJhpdAyOB0CDBqPqN6jFCwCrp7lW44QbOljPPNbz36ic998uMycMnyl/7qTxyd
+XDy89/mSx9nitJsdx6bDp+qT6+v7ppC36gLFuTdQUfIdk+GNkeaEgbUUsNF60AdMNZAROCDveari
+JkTPdNITwlTLAoIWDfMoKqqKCIZgDrsQAjETImARHUvd5+IckUPnyDH5gL6663FQhNiwBqhkx/N2
+zHmzyTEyIBzFuMt52+foeE4+L507nk55MzPvfGCe6k4iar0r3juk6F0T+Ho7FhFiNIdSFRGAcSLe
++RlLMkBwzVMgfcJnCIAgVQnGpYp3DAAGKDulAMgIBAKGM7Qp6yqAgKKGjsxMGShQKYqCAMCI+gzr
+nE5HBASYTFzUjBCrKhI6JjNDgirWNb5UnV557fOfu/vOl7x3R4tjUxWV9XJjpvPFfExDkjGlNKQi
+qlXVO7p8eH8UjE10CF6KDONOO0DAUtMwJjNUtWEYkbIpitZ+GBqJ1alaZXZSEZhWy1Vs+nHYNbFB
+QJO6r/urq10qSaSqmEgFhKmhawBSlRCPj+cxhJQHHwIzt003my8kD0jdcrm/Xm3n89YMhrEM41rV
+1Ozs/ITZkPCDH3z3e991V6T+/C/8VlruGuCi4oWPZ83aUqkW1g46GwPgXgFBthUFLCskaj2X8qQf
+8+rh+NlP/OJHv+ff/u//0vft1o+ZsVb97/7Gb968cyiX3/fBP/79/9EP/O2/9V/mfd09Se25M4Nh
+Wc6r+0Bq3tk0vsI+52y2KcUhv3LdqxpiMTPvQ62lCTzBGvtUoEU/IxmVIxMDBwS17WabnbbId86O
+a5EiimCqVgTAwCMF5EBsZkOuBDhHv5ccmVpwrkIT3An5Hdi+ZE/EhM5wTn5Txino0XBqeE5ayqZt
+RRUnqAbhkGhNJv2ATFRRyRCVpgEcWifrK6hZTSF0XE0nGhlOcI2hVlOwIoKIWmXa8jaAJCMkQ9Oq
+GsFEXcehdbgHMoAIGFBHswC0FmDQvarZBJFNhlwqxo6m/3rPlMUATQ2YMFdFBGYqtU4qe2JKRXMa
+z85OTW233SARE52en6oIEs269g3W1YM3gUxYjQAErjZPzsm+/Vv/BVelSpXYtOMwAkKtlcgRNyIV
+kIdhrFW6LtYqexnmi5siPRGJmAkAMpHb7ff73YAETRtLTuvtQOhU6jbtnAvMXPKgYGBht++dw+1u
+K7NTNJSCORXV3X6/YyZ29OhylXNmdqMVMEOiMeXgWY1A62az7domzuJur+agD2IeMyo7Gk2W2xSC
+awGWuxKPfRkreM7b2kaHTHkj8cTXRn1gZvjkr33sEx//2PLJg7Z1IqZqf+W/+B4kQvT/04/8zjte
++KZ3vPBNn335l8pYl5cPP//qxwnx23j2Tg6dpF0aL3yYIbgqEOOtfrsDfoAiAAbgHKtMmOxB3EsI
+pkCBiGCaE3q9vF6uLqNDA3i83QfiSFTN9rUQozKogTpQhoDuOo/siD1VsYpqrOxAgu626zFXCJgs
+N87tah2HWrwBQBIxhDLsj2Gqhs0xi4eKGgEZYJtyQQWP2NJyTOgsBO8RNZnf43pMgGBkxMSAjol2
+OA4qqkjIjLgDMQ3EMxeMTQEcIRBBkQrmBWGNEgw9kgARlY0Ex5OlRSXD3jxgNnVG2SkAME/cjcNw
+EANjglKVEEWNJoNUnroBaAYiOu1ex1hFAWG5Wj15fDlbzK+Xy+ur5fHR8XN3b3vvReqXvvQ6mp7G
+gAp9KWTofXCoOI7LJ1f8kQ+/t9Riis55RJdzUbFh3KnaOCREVqUQEIFNKOXtMOxVxQAUteRSsgTf
+OO8YA/uujPXx5bJKUQHEiWcqAJyz9MM+Z2FGRyGnYUwjIKqqVPM+IjgVevjoiSrWWmrVKlZr3W57
+Iiyl1FIBuRYx49Vq/+DVN1vj47NWnB0dN7gx6euZb4Zacac3IJahztiTAt8KNmcCnxfNLOtsQ92d
+d/yL3/3v/sO/99dEqpmVLIiQU07j2LRHd9/5vlt33w0A3/kn/ux3fc+/U3bDb//s/9UN8C+Fo+ez
+0PFxGcY7TWydD12HKXWzxT6XKxMBEIAmNiplGmVbxQQBPXDk0BEQmlrg2JHfXz680XYOEOSgOifE
+zodZ8KjgEGfeB6Kq2hAfx+b2fKbFOvanMZ7H5vnFIvc1AB+HsGD/wsmxF5SsRyFE5IZ47gMCz49P
+zcx7F2PYP7ksLJkVEJxgYG7ZnbYNKZLBzLmLxSwS16q1aiDuvJ9wRscUPJtYZNd53wU/C2Hm3SLG
+1rnOh6PgWucb4kA8YxeZIYCy1apIoAKOSNCAwQLg1qqoionaUGrN6o1UjQ+lAjKCqAHgJMCfANOp
+j+Ydl/KUjgqASKUa4AE4WizmFxe3Tk5Ozs5OT09PwcBMRez4aNEtFlQyWV00oWF31AZRm6wu+Du+
+/QNAPAy9VJ2WKZYiJdeqlsZUa53PGkLHzqsqAFVRFSilOh9LLWaaUpIKUvNmtx36UmtV05wzGFbR
+WmrONY11GHMMbrrawKjkgkSqUmpRc5vtarMZJtZ4LjWXUkrdbHsAEJFaVVWrSC76+PLqrcuH/sgN
+TtyCxWxQkYA047FKOHaCtu9cjWAN6CBurXUscRDMWkzxKP32r//UftM3kVWnsWjOOWJHNfcPH772
+3X/q+58VxO/55u/4yb/9gy3Ye2Lbene12foYT50D1ZrG6v243781jpc8YXjYNm0p+cb7Z+HUGUAG
+JUeukmzUCR3PWgcwpH6wvCu5LzWLCJqoemIm2kkZrFaySlZMi6kFLCr7nHelFFQhK6jrMa3zOIBm
+k4pWVJdjWo1jUR1LrWBZZD+msxu3EMA7H7x/8PD+Po8AIJOODAAAiHDINVUBwja46N025bGKY3aM
+iOgdOabgWEQnjUATnGNkZgRjJsdkh1nvQIRiU3yDoEk2KcaIolqrOSXZKQfSYurMCVVVyIYADIeR
+AnqAaSB4EgXPJGqOkJmKGCIcsg8AAyNCMHi6PbRtm5u3b0/93SICcHDaK1IJGSRpGgygAoGpGjgf
+1NT148BMSKToSh7GIQOiiAFALlmEiCQlGYYt4c4H531gxlqkbdtasoGVKqpoHvvt+up6N/TZOQai
+kjMYOO9qraXIOKZSpNamyugrM5OZ5lqCd8zcw7jZ7Icxi9iUtpqZiPb7hABjyjH4ccwheKahCeH7
+/tU/sXVDJdFRnXf9MLLnAE4uS9eELzx6DK6ehsjeJKv2cnnvOkt1RAJmaucQ3K2jm+/9M6/8yo+w
+JzUK4TlgMNMM7/2bf/1n17vXHbx0fvP54HY5j+foTBVKlRhRBNRMhJzH/b46h84ZTjAR5ZQ44MmL
+nWv56ou77SsFKk62zHMOz8f5VEgW9kPKxZQCeqblbhxLVbPWuRuLjhGJDtjQ8by5XvfbId+czxzT
+rdP5bkzLzXAaWwHzTLNZXK37kuWsa1Op0bGY9WM5ePkgqSoRITsdlAg908QBci1vcxEV9NTOfQJF
+xaHUNjrvOES2bA6RmdvAtdiYK9Oh2TuJxVCtikBADYoGlayqqrMMWoqCMxSsg6oYeChFyGPdSHbq
+iQorAiBgrtoQqxkjTe3kqva0gwEARkS5CoBN3VwiRIRcFBidw1o1eMIC6+XVbtc3bXTOVVURMVCT
++ujBo+VqZWU4bwmJycQQRdRRZVE3RZqqlppNxQxLzrXWqUgax3w072oVnMZIiZbah+BTSimnEAMh
+eB8AMI2jSN3vR1UrVabUE+DQpd8Pfc4Chn2fiqS2aZkxhphGSWMOPiBC35dSpWlC18b5vF0suuOj
+bj5rZ7Pm+Hg269q2DSE4d3gLnoHaXzYWmf7a7oaJWWVgTAQACvR3/tY/ePnVL6knJoTA7oZ7cdHe
+OHvzw3/hT5c8+6lf/TaTAHRcSlk+xn/y+AnAwuAx8xddn0qdAAAgAElEQVS3qx8xAO8Y2VEItlp1
+JydWMjoPKQ1H7Ruyz8Was5j2lRsaHiffEEVkoFkTb99CBYszBwbDrjzAAREx2zgeWGJsWEdNIIxY
+1RwQAXjHEwvAM1u1XLRWzamGJlgVLZZSZSBHEJlbxSdJnaIzJHYAIKJHL5SxVhVB4qkb0IbGGZKR
+M3TM3vGJb9ZlzASNc6euiYGvNkNE9kxt8Cdd82RyzzJD4ja4VOoBVtLDG5uqmpmiCkJ1WlS5RWTU
+UdEBETZzJ1kna5O0rqAADtrWS1HXUGC2auN1ta0xkagRQhULjiZLFlHzjnNVVXOOwGCyTJ2IcVOP
+T1RLRWYaU16vrnOehRgce8dca10uV0fHx8cnJ6vVctM/MBYDATElyGxV1KUxGxghiwgR9/3ODFRR
+FXLJtSgSpySiSVTa2CKhGXjXmsE4FCQwo5xzCL4WG4Y8dSjMzHnnmJvGx+gvbh7H4LsuLuZtCH4+
+74KntmlCcE0T2jY6R4jYtpGnvsCX19ui2wwAxjHff3DlvZ+mkpgpAIkoAbAP19fLz77y5nzW5TKE
+6Bfzk+BYNDvnIQsX1Sxhy1XtUamPdsM83nzfO+bf+WHb7dtPv2Iqwcyqlhi9yeOafiFd/3LbeCOs
+JtaPngiHAWKQ/X63mL1Cu9+56SkaBwxTRyfQ2YeaeE60wpNZ7IwRcS4+VdkJt8EjwdZyAHSBguM+
+5d5q9Dzvwm7IpcjIVeyAAFbRtebRVTriGnFDdbVaFVTosIexi2GZ95Swt4xMmyEfuYhgVyn1D0Uy
+fOhd5hHrBNQwJVQCEzAPIGqrcRykihmYbEsajHc1Z1ZgBKtzUyJMRRFUpUbvzEABsshEw6xVpj0M
+cxRTYQOFcV2bU1ermkJccN4LGCDBuCyuYxk0nB1orURQs6V17c78sMmTU4sBTgk9ERwAUIPpLJgU
+D6LG9OV+9jMOBSJU0d1ue+Pmzc16k9IKFNpZO5/Paq2IMJ/P1/umz2sONBk+ZqvA5koVREi1ELJo
+UcVSq6qllFXMeRap6BiJ5m03m3VmtYnRe44xmkkb42LRsiMiFNGPfOQ9bRO6rpnPmhi8D84xsyM+
+TE4iIqSDSxMAQJnGQKvs+/Tw0fXlk03XhCqAONmjaAiNc14VrJbQtirjpz7zOgKdHB/VmtRqiD6E
+djE7qnUAlNVq6IfegB1jGjTny+AjO1zKxm43AJTHk236UK4vHMflsD9OvfuZ119q2ljViDHEOqbS
+NMFkP4w1jS/R0Z8V298v5WM0nF/kd9zl13OJod1s10pjsYsP332h3K5AAKzASh7ezC+nfY0dby5L
+nktgl6wUp+qpmmC1PmgFUDCR2qsU0IBckhDgUCulElQRgIgcU+fciQ8IuPChif6tfmNqTfBV5bxp
+d1DWu7GjBhGXddyVxEQRiPoABmPf03xhYLXWrmlnziOjqBIhBx5LFVH0hHMeTMo2VbDFLDYLr6MW
+swk1Z6KxiHOAHVlAE5AGcxIIgEBS1c1ItiZigDZ5TPiOkaD0qkUBMMx54oeGBY+rioh5V0PhcOwy
+Yy1qDCogoj66UtUzlao8EWAAJlacqDGhyjRKHqsYIpaqTXDTBiCEq0cP5vOjpm3NbBjG3X5/enaK
+COMw3rv3Voh0HMOulGEoE+MQGd1i0XZt4xx1XfTOzeatY57NmrYNbRNj9DH4EJx3znvHjr0jnrKQ
+qflnNkkxJlB234+r1W63H9+6v1xvtjE45zzRxHAFkZJy6bpjJnA+aEk+REAjkk9/+vXdPpvpc7fO
+Y2yGYWeo3jvmXRObJnZmsht2jsNyue1mYb0xZgoh1ARDv8xpJCR2+ODh5TiWUsw5DMEzM1j1Ft/3
+3pfuvOsmalTEn//5b56fvxFmv9ktIuP77udv2m57dgeqFhLmrGhdjLMY75p9xACO59cf/WPDh977
+6PSYf/23vnDv/iqj3+4GT83yer3ZbJjIORdCnB03SN5H9QmbHegGqhN/1J4uYhBc7/r9OHrnnrs4
+Gse63Y6+SnFqEZrTiKmOOy0BKqmIMqIpBiCv5JAkmde6loSGJSsBbfqUqva5eCYzqKq1qmMzs5e+
+Ka3WUKvUWpk55WwmBMiGkT0CzDlsU3JE3d1mskGZLTwvtXHubnfUc9kOaa0mIhnACDVSd7MRUaiG
+gbrQkEOuqGDFpETer3OtFczKTl1HaS0THdC1WAapg1LAshUkBLLmxD3zXtdiYc55JY4JpzbFpJJT
+cw5VbMp/zGzKi9xBJwCOKeU67RNVM4Ock6oiYIyNY4dEKY1EbIB37t4RkViv3r84f2u5eWu13acC
+Au4/+4//TUIyg5xLP9bddqeqIQRmEtExjaVyLqOalFJFNIRgpogkVVUUAY/Ob6+u7hPDG288vr7a
+D2lgP9tv18dH85OTeRNBpKQ04P/D1Js825Jd532r2U0259zmvVev+ioQXREASVAwwyRlUg5HKDRQ
+hCOssWce+O/x1BN74IHHHjhCcli0aYmyQrRIAiwSqCqgUM2r19/uNJm5916NB3kuoeHtBzdz79V8
+3+9jWKVH0zR1KSFEFV/KDtCY8rzU4/GYcrjb3+VSiTDFBErHeRbRZSlgEHN4tXs5TQXRTYkDiwoT
+E5OIEzpSPh5ra6rqMUZpnrpghjDXH/72B32fui5txu6f/zET/xbxOwS8GdLt7vn//mfdPEOp7e03
+lRCWRf+Pf9Nf3UZABgAiqvbOX348P33ZPTx/Le0h5+3mvD5+g1682i/L4eL8vInM8+wA7bDItkZH
+JjT3bBiV+6NdojcVq6QVukRnBZOQCIrhJaee45s0HLH1TGNKACBkHGidzyzWxjFJ0dd3U0qhoh5A
+lPzaiwewC3eVOoll9wwNDcx/ugO7hYd4m3JCiNM0zbUV1AAEbmyIrRVXc+emEBEYlGGpUqqO3VxN
+91Irm0QwNEjE5wgEKQcAAAMHF3Hqgxaz5h6AM7YFrHlIaMVCT2tyESKEjlYfTPcw7r5eUs/UEwU0
+sXwRyq0AYAwk6xLaT0e+mLtDU7svihAc1tkUIhJAE0VEN4+BlqqRuTTd7+84BFVdloInh7SCg6gQ
+0ly5NXk8jh3w9XG5K0v4V//nX86LTFMpS3MgIn386DyEEDgFDgCw2911Y1xXiCGkfnwUoGzOHoku
+Zbozs7vDJynmy4sHZXlRWmlNStmV2kor00S1NkBDoBSjNtBmt8vLcRz6bmTkKsswDN88e1qKLUtV
+a13KDq46D306yRaVgIN6Y+Nl9tqk02TqIos2DpFjSk0mBg61Hg6zu5eKMZYYYmoxxobgz55fn5+P
+203v7mdb3A4D84gECHB57v/tfzMdj+WnP/vV3WF/tr2gS/uv/ujbL68vn18hc7/dxk03ffk0fPbV
+2/tHbxD9zlLx1RUwuzubNqbbbb+zLLuloO3L8lMrX+U3QnycF27zcnY82twmi6b54qDnC5zvyo6i
+7uO4dDvGXcWhHFxsauxNnQ2Do4oxUUDqqbugdK1lhNhDYMxr58+EVbSZKcDsda5i4JFpUc0vuqa6
+6P7VNBHC0bW5QkAcWGcV1cUsDUyBylFElDtCwvCQGuPXPCEjnePI/ZYJ7qPrVWzNnlndMIjYFnVz
+RGLwwCGQQgfSLG247mU1p4ODzMYBOdF01ZARGaer1p0HTrSmWhAhTdDWHfZ9GAL4yTaAiOqnYWhp
+GgHMnBhVPQYy8yZm5hBAzV69fFmbbc+262KhiQB4re2wP5RS0NuWz7dDjoHHFBNRePb8NnCHlGo5
+iunjRw8ZU6CYUlrK1KTETL6q85hLWabpi81mU8oSY0RcTRA+zdOr19dXN7eqXoq2JqXWhw83tTXz
+0A0XrdyyuKoC+DBsiMJSqmpLMR0Ox/2uHKdjbT6OnYi7Tw42mfd970i7wyFFZoyqcJimUurEPE9L
+ypGZsmVVXZO8b+6udvsZwGMMZgGBQ1DEeH42vPP2w7OzYbvpc44pMiK21v76p79aypJSDKEzFWQ8
+HqrpHRH+1/+0XV4uAO6+R8R7du3qwUVAeP4S/9+/7n7286DqbzwcA2/M4a8/5ptbRPiDeYZnr+6I
+zhHI3K3zYhCVAICyi7uZR+Q0GHe+qlmqucNs7ckBXjk8N3+JdkOyGWQY+fbL/YupHY2huEUhUk8c
+VpsIwdqmemsamRBh5ICIBnxsdTGJTD1xcozIF9xNoS6mDDRiOOu62/2szrzQkJOqqRkCYApzLWWt
+3AMBmEUEBApIiLBaEAO6+wr+r1XNzTNAQ5k0bxkZeQU1V+OI2rztlBOiU7oIGcN81ShS6IgY22Kr
+01/VQuBSZNWIBqbStIukVU+qKvfVJ2Dqa4u8umeYaO0rpRXvlxd3NzkNuKKtjY6HY4yxG7vjQW6n
+0ue4ThIdINRaBAWczGFZSmnT7tgChzBHQCNkJCaPqsoYyrKY6+EwxRD6vGmtVZk2mzNV2B8mM25t
+MVtrNTzspz4PBfbTvEMA6bquyyJS7mpMnHJGIPfMjLV5KVJrWQpL8xCQmCtKbdr3GcBr05TGeX97
+fb0T8VoboItpDKFWTSkyI2GtzT94/43Ly+3DB2cXF5vN2G23/TgM45BjDKuHSFWXUmBpDoEYbq4P
+atr3KcZ4e7ef5lZFcuyI6WS+wJOoAdcQUAR3A8M3H/m/+Gfzv/hncJLq3n/fr760n/49/9u/jE+e
+nrujrQHSZrRuV9zX08QBVG0dKRr6vdqxT/H7RB+tUAMiElUF2K/SMLlyeT7BC8DX5i/RK5QHZ4FY
+Xxza0yNUiWYECoAG5KDm1c0NzDywr29La9rUS5PoPCHm2OamKwN3wTakEDkwEQF0sWdwZiKiUsUU
+MlOpVqosoMEBIlaR2YojSlRnIEOIgBsGhLwNJ3aDu1bPWw45aDUasB217pQienUKWI+az0I5SEB0
+czPHU6CrI8L6mdX8VZquoJd1VAi2uooxBGoiRJACu1qpx7Ah9Wn9Va6ekrsV0TkGv/JlqPEsphio
+CYbpOHMI6GEucwgcQnAAUSOOXX8+7Z93PTdpbqBSHXy73a7M+N1xR4SRc21Fqu12ZX+YAGCei6j3
+fSaK5sgU3FxMpMGhFUDbnJ0f97cOAEZluUsd39zsSqlNRAWaz0ECB2KmpSxNWmAKIS7zaze9uTuo
+uLsCQErJDGJwYoihGzf5nbPxu995d7MZNmOfUggBmSlwQrT/689/2po3WUKk7eYyBFZZmuD+OMUY
+YBZVXma7vT10fZqwiOjuMMUQ4mnzQA7uZuCISEAnjc19gsRJzXx3d2zl6tvveqR0e/w2Avzyi/DZ
+r0MTXuUrTLReI6uWnRkdcV2ChkDr5FvNTr2g2bpyIgREYH4Y+CHAj05yTELJVl3AKw61k52256ov
+ml8R7RDvHG6iYSu92Kw4MTY0WmoSq57BI2D0XbA6gpsHMAVvYmiQgBAhxdDMSW0tyolwNmD0RLSN
+0R2QEGO/lJJiPGKpqrusbV8lolfFdII5u2J+QG1RnS32hIhtr8OjWPfq7t15NHUESFu2OwNEVSNE
+UUuRxXwlNHaJm+jqjncDcw9ETU559GuRdloduNebSiku1kA9KpY1BJZgDY1095/vy/cePBhjZMKQ
+creUIq04oJnudofteF6WpXU2L4d52p/DGQAS0Tqpvbne5ZxTyiFENwdIHEbUY6niBrWpOixLZfaF
+cdI5d9msDd12fzikmLo+T4c9Es1L6YdHy3x9czcvS52mJYRQamttCTEyUU4JACPjVEpKrioI3Hf5
+rTcfXF5szs83Z2f9dtOPY9/l2Pc9oj97dkUUapWvb58TsTshgqq7tf1+Wu3YqjAdn47DhgM9f/76
+cKh9jy1obXZ1fQNAKnr+YLi9PZ6djUQUzHy9XhGIGdY1uq2+7fVSthXzhoiXl9vLy425/+gHLcZ5
+fVL3B//4E/x//j383afxOPfr3Np95c+e/LK27vmR1nkaoq9/gAgDngy87m5mJ48kuKrfn4w9YZ/S
+OeQP1qsDoZlV9+ZeJU0qk1lFaIjkDgQEUACW6oemB44lxAttb4q9drpiuvuTn7z/9gOMrGVepLbp
+uNzeHXe3u/kwL0t1dz3JSIFIzEFEIsTotPU0hRCIDtymY0sjtyv1HrR5P8Yadd2RxUy4QNxQO2o7
+iDWXe6BQYGzqKRL5KotwJrwf/+O9fANEneDkd4uB1xZZzXNkcCtHebvbMsUDNALIqouZqQlgNmpq
+6nazmzePEhGGWiWFXtvijn0fiLjUklJn1twtpu54XMZhQxhEGnOMfb65fr7ZbMGdKRIplWOppSxS
+SmsitUkTRcym2LTZBBzhOB8QobZapRJhShEMtb0WLfPcpmVZlpZXIa5JzqO7EFPOcexT1419l8/O
+B2b+8P03N5uh62KInFcPvkiM/fG4/+xXT7/86lWXu3k5hkiBQ9d1OWWzBuCvrm5THgMLIcUYSm3R
+u3mWw+FYa4uJGZcP3n/r7bcfPLjcXFycbceu69I67TVzcwVDdEQkxNPT6I7mQEj3eix1AzylENLh
+ONfazPD11fzs6fTBW/r+m36c8jcv8udfj3fT2wB40jQ6OAIBnR5pWLE66O4G/vCifudDfOdNf+8d
+fO9teHg5ueGzF+lXX+GX38QnT/HpS6rtVGn56T46RQSbOwZgNURDRELW1ZENCiDEvVEOTCLSRb3X
+89CnT4ZPv8FAh4C3kQ/jsFy+2R59cHd5JoEPTHXaz25SlrrfHa+u9vu7favtcChMZAZqNmiiAro4
+E2lx35sMRg2sGG2xHQwGpwLdZZRqnAgc0hnVvawaNSaszVOiUi0EWl1iqyhI1JkRwMUgRSpr6j2c
+rgLRk7IarCHzGDtl12XZBDYzc3DERKbqIlpFETDM8+I+uxEgMCVXciS139j9AeBwmACVA8cQays5
+D4jUtOb+8TJfkfl+Px8OR3OvTVpTMwXnucxmGoKLIZGkmN0RnVtV1crEqpJiPhyO4PT+e288fLi9
+vDjbbvvNptuM/TDknGLOcehHM/nqyYu+60pt9WZ/OB4QyYFj7Op8CCmDt19/+WKaplpbYHblpbYm
+bcIJ0B2gVVc9EkGMQQyaSCi2O0zTVJlDdO6G+P67j95++41xk7scUwwnYb0BERGSqfoKa3VAxMBB
+TWGdCKuaOXNEMlV1dxF98eL61dVOxUVZJLq5qh2Pr4/TFugtAjCEE9kcf6OuIVrl9qdbAgB++O2/
+/N4Hx7OL7u233nx4sTnb9jHwW4/lxz/Cad6VUqc5/Mv/2//ln7+7rkv1pDVcf9wREAnBydd7y92B
+AAkhipG7reICdQB3RFK1WhszKw4VR2ywW/zZ9QmqRuhIGukq0K5Lx7NNfft79aPN1KcjwlKXWmsD
+s+uru+fPr2/vDre7eT5OIoYVIlJVo2uMgGxUq9Y7CT3hBApgnSEhGKyupBVhvb7Dq6dsbcNW9K8Z
+5ETugOAh8FwkMAWmKrZqNJYmUd1hLSO9iIYTEAlLPTnO9nPpYghq7ubSWu74OM05dfvDPucuBG6t
+bDcXalVdA8WYewBR1dW9JSLuN60el2XZ7aoDLEsVsVJrCmleKriqtZx7REBUxNBaBcRS5nEYQ+Zx
+7FIKDx++8/3vfXC2HXMOfd91XVr1OylmIlZtNze7f/Wv/6bvYp97M13qlLtIxCmlPguAlbp3sFev
+rpmCQzfN+67LiBA8ch8IwnGa7nb7EDnFKM1SZxXQvTx+dPGDjz68OB/Pz4eL8+3Zduj7FGNCAjMF
+BzeHUziXI9Gq1DVVBK61MIcYkqgQIWJoUgiZODDCdhu/O7zz7d96++5uvtsf/os/euP5i/mLr+66
+Ljx+w91++fzq/PnV2VdPt9MScZX3rmmmhgDrE3qSss3zcDheXd/ePH16m3JnIufn6YP33l7Kom6b
+8dF0ePatd8+bKADQfS++YjCJGN0jM5wctMZ0ajDV3B1d5TdGVUARDYGPU0kpphWzutqrERHQTAER
+gSo+Rni8X+jV3vE53vN5C8JdoJuP3vuL/+wPfyfnSEzTcbm9Ozx58urf/PlfTcdCLm5GTLbmSTVo
+zcz8rO9YfIcFAzRZ8bCuCutywNCbGBOKGa8G+UArHOVkGCA0dyQwtRTDUnWpAhEDUwy8Gu1LsxhQ
+RAlx9dDspxK3HEzNMZV26PpAGNwQCZelxsgc6HDchRhEGsJSZAnMDs5MTIGQ6zKrWuC8lOPhOLnB
+stTaJMXA7CHEGLvLB5dd4hT5/GzbD3G7GTbjkBKlFIZ+bVVD7jpVaa221g7Hg6m1at1wttvvVOZ/
+/x8+2e1mvBzdwFxijNqwWpvneUkLU3BUBJjm2mf2+WgmCJEYG9UmQhiO01RrWzFETCFWZmYi+t53
+3nvwYHN+tsldyCmklNZ1NXsg5tX4pCohdGrN1ZADgIQYzBzMEanWyswcWNViCACk1prYV0+unj5/
+icgppBDxg/ffev/9zQfvb2rTFy+WFy+PP/5dPD9rm/Hu+q7//34W/uwv/JsXCQAdnBDN4QSwJDsU
+WJaCyIf9vhd11y5HUyqlOsDL49ddHvaHwz+Mad1XUOAqqTdANFW8v2QcAMyZWc0c1jnLOsZyJGTH
+JkqE01xqZSLs8ilBGu+Bu4j3nb/bvSBndfxF00Fq+atP/vBvPjky7QJPY1+GPKPb937wfWYV0Vra
+xx//allKYKqmTLgOWgmpz3mpxRxWE4yt1SG4O/4n2wBIzEuVwMHdELGJBqYmtoZmr3fFXCWFMJcT
+iykQEWEVXd3nqEbMS5UmGhzweLgzsxjORbWJuFvO2dyk2j25LpgbeiCMIk3cIFhpFREDx1JkngsA
+jWP34QdvP3y4uTjfbLdj38eUuOtS3+Wh79216/LQj4fj/Pc//2WXO9WduR+Ps6O2VkOIw/ggx0DM
+Zd65vyCmcdgeDnOTVltTYw7BQdazchg6xCAqMXa11ONxqUVSCjHE1g7D0BNDa46or15d74/T0A9M
+Po5p6PPZtn9wefbuuw/Otuc5ryI8BHDmrFrNDRVX5RJgMKsrj3PtdxEYvHEIpwxFw9ZqSv3qVSUn
+DPjdb79zfpa/+OrF3e3eXG5udsOQYkgphg/e37z/3thEX129/vjTp988f35zd/fRd8vjN99/8fI7
+V9cf1LaFezexm9elLtWIFJ3msoDD7e7u5m5AJIQgUu7KjmmDtIoFjBDX6wsQHYB47R1xLd78XlW2
+PrUICG5s68UDSBgQm5qbN2tMqIHXayDnlFYyoaiqmf6GMEsEbgbgquY4iGfEh1Wrt3pz2JscbA00
+9gp+BNtdPJim/ctlqQC6yqtFLRBFwoOshaFHpiZidtoG1KYpkhRBXnuw+5cZwAGIcBXqmcM6RFKz
+EiwNDAZarRm4Wb4vqwAA3ZhxrhKmaTHFEGIVZaCmVVQOx+PYjwBAjAASAi7zDAC7ww4BcpdNsElD
+gkbNzX/0ww8/eP/x40eXOceUYj/0kRnJU2QkYOZAobbleFz+3X/42d/9/VeX5+Mw5Bx70TbPU+pD
+YG5N97vXNeXAkYjcoUp59ux1rbqUsswd4gHQYopjP5rbbjcDSopZhfbHnTRj8lKltkaI68wrBEZo
+52dnH3747oPL8fJiu9l0Q5+GYcg55JSIIOceyQlRRNQqUTATADQFJjZzwpX2BC5mbkRxLVBUlZkB
+MFISKYhIEIiZCVtrjx5ePH704Dgvz55d15X+2RMzrD+oovvj/OrmxdcvnqxV+Nh/9q0PPvutb9Hu
++Obd/vvXr781L49cAZBUWnOJIaCzux6OMs1L6i6k7lRtuznbjN3KmDAARwy8NuIn/KibAREi+f3k
+xMxclYhPHTcRODieeCknsBuSOs5zSV0C81almBJRCCHEsHpT1hdARVVB1JB7sB7AKTB4k/oKUImj
+Y3MT12Y+vvt4/uhbHxJ/sKpvDof57u5we3V3d3W3/n1mcge1EyUOV8DA2tjAWswoAaoa8WqFWasm
+Z8QTNguhiauZ9AHMvSMwB6dZ3cVdAQxETdVaMfwnf/K7renZZowpAACgqmrOXd+PtUzu1nf9MPRI
+dr7dOujZdsPsZ2fj+dlmM/Ypcd8PXZdSDsyACF3qiE51szuE2IHb1fX13//i65//4gvVeHt3/fab
+D7uciElNECDGhIAirWntutTnUUXFy9CPT56+/Oab65RCl7oYg5kCODPHxCEERI9po7K8en3z6uVe
+rDJRCNzljIQIvhmHcZO/951333j0YLPt+y7lHGNAIgqhMyuIvJIb1VY6lTiQmwWOohJjJjZEarWt
+dyG4OxgCEjIGOn1s6g5Eyb2tBLIUkpiu+ugmNXJeSalIQMQnCbcJOhyn6ZdfPvn481/9+pvnp/82
+wOr1Phw2+/23ej++sXmJiCEwIDBRjN1775wjYeBgDgDS5c3/8L/8MQCaqZsTr+W781rIrTEr7nSP
+1PETI9mJ1jW/merqRsFTVU2tCQVmPGVFhhDW0Bg3w3WbAeimuAJ3OawsDHdr7a7VCSAgRgodIKkU
+bcW8uVUmy3EBuyHaBzpG3ue4vP3G8XC3O+yOn/zqm3le7P6cXrcBDtCaxkArUq6JpXtWbgp8XFpk
+aqJdDq2ZA+TE09L6LvJluBfSny4rv0cBrBxfNw+tCSLEhF3HKaW+T0R4frbpujx0KaWYMvVd7vs+
+RR6HjgMi+jgOwzAQUVzlzoRqDQBbm6vMgaIZLGXqu/725uaTz5798tdPlqlMc5E2A6C6Tsuc4kCc
+1ibT1MxtGEZCqiKmGmNXi87HNs/V3cpSU0pInlMWNSRsssQY5/lGtd7e7BxsHLoHD87HIT+4PLu4
+GM+2/WYcuj6mGNYbJ3dZpBGCqqoVcHBXRAIg5lXuAhzQgNe+T6SgUogYYgBAFTEwgmAm6kJKyHEt
+Ut1VdD4JYyioa0qDyozEDo4I6ERErRUiN9WQUo7R3Dfj+Ps//Ogf/eiHt/u7T7/86j9+/IvnV1cx
+ktTWxZvh0a1PodbETGbGgdYKRPUMFKbpOIwdEZM6JMAAACAASURBVIjZxcWT29t3AAnQYH1MEVQV
+zNfqCO4lPGCGzKqKSFobIiAH5GCmCEhMXQ4IEAOaATMBkYo6oKsS82nAtEaMMQFSawpiiKByZ3Jn
+WvwE00Mojkju7BgBAmIPGIEHTGQuc1smrV7k4YM/e/xm99a7PjX5+c+/AAAR7XMwO82biXBFAMGK
+knaIgZtYA1vVo8y0uuPNXMQQwNT1KE738QknnssJvuSn/T7gf//f/fPtZuyHLkVKOcTAm80YA/d9
+SpH7vkspbsZtCAigIVBMAQz6YRBpqzs+hlhLFW2AkOKwHmzM9Pr6+uOff/Hq9TIfD8zxOE13d4dl
+qQ764PKcKYSU3JXIUoghBlU19ZQjETKFFEdA+cUnX7x8dbMZ+5Xr7eiBWUW7riMiZnR3pqCm7737
+6OxsvLzYdF3q+9jlmHMXI63tBxLFkNVKinkd4KhqDF1tkzuYW049oCOStAKAIo0pmquDx5DdNYTU
+pBCiaIuhE6lMoUrpuzPzwsytCa7HoiEzqik4MEUOjOQqsqKWWpUQk8O6NPCYsoo5qorkPCzL8fZu
++tknn/3tp5/e3O1FNEJvE61k8JRiiNlM3n3roap2XUopdl0SaX/19edFcXf71u72/en4/jI9RAon
+SQWzirg7h+CmRKwibsYxrlIud0NaqQ0IboSAHOj02gCtoyUiFVm/AQDXYu5EXiHUdqd6a1LBHZlO
+YjZzQGSCEJppVUVVdOSUewQGCEgJKaocAhVCRt/V5bMnv/xf1zlVZFqqdplL1RjIT5EIxnRyC6DD
+aldoaoFPNKbSJEVegezGAPHeUoW/eRP+k17Iw49/7/spMgfougygMVDXdX3XESMTdF0e+p6JRRpx
+yt1oVgFsng/mkHNnqqINCSJFAEwpqcDTZ69+/ouvnz6/qlK7tG2ih+NkigBkjjFEUWVK0ioitCph
+6KSBGuSun+d9SknAS7mNie52B1VvYvMyu3uKq1ol1NpqLeMwhoi5ox989K2HDy7GsU85DH0XmJjB
+wQMlc3E3pqjaAidRNQdtC2GY5h1TWKsZxOiwmJmopJiZKaZO2oJAqusO1QMTILODmZgbIwcOSzkG
+JncKHGurhKhWkWKrdeg3pc4OPaKZWYxJTbu+L3Xuu3FZ5nXyaCoxdSETgAdOZxv449///R9/9INn
+L198/Onnn332ZKmNOTBza9baJNKanDNRqYWQKwoiSrMmJeVfXT749PzCbq7+ZHf7p6aGRO6GCEis
+rQE4EmMIYKaqHCO4Iwdt1dUoMIeITKYG6K6KRGbgbiC6zkMoxFVSBQ4UWNuNyp1rdTNkQmZANBFA
+dHdCiaGANyYg9BV/WOdbd0SKiGw2uQkhBWbkBPQeM8NKNQUIjKoOCGqnbYAZrMAlMxAxJEyMTddJ
+rJt5YHaH1TcfkCDQPxQ8pyEA/OYdAIDw6MGI6MPQ55wALIawGbZNi0Pru77vB3MjCDmH1PWtLu6K
+FEWLWlNtKaXA+TDfBYoxxmfPXn7yyxeff/EFOpVaRMV035qY09LacZpqtZw7EzSC9XxFoN1uFyLn
+nMsyOWCprR8vpsPLq5tlXmoplQgddBw3fQ7D0F2cb87Ph1Xdeb7d5C7mHLvc545CIAA84VoxmLac
+ulpqLfMwnDsUBIohuBERI3IMqdSjuXvbR+6Yse9ia8XMpS2ttRiSmSI6YhYxIuQQVI3IzCoSMyoC
+1zJ33TZGAoAUN6ot505diCKiqoqZETEjA3pKWaScBF9miNhqQQRARUBAFm1N5ocXF3/wO7/9kx98
+9OLl3a+/fP75r5/N8xICg2NrPrXDMAxioosc69yWcvV6p4t0Z50uwstXiMAxmqq2RkiciDmriqty
+Sg6AIWhrpkLMIUSIK6rUTVbEEa11gqmuYH4KEdylVUQkZqk3re2sFXdDIuKwll7rAtVUAGYpey24
+qgU5BEREcEIXU9Ombe/e3IwQPHWoAZBDfCjt9TrdXyWDgWndZzW1NTWDCFWdGAnRVrUfISJWtRho
+HQSZQXAIHZv/RlEHDqfq/37NG954dClW+37cbs5VZiTLmUYaW5WuH6UtCKhWAAGrl6WYa99Tl3Jp
+EENkzGWaTeDV7uYXnz559foorQVOBmZm0qzVpbamAk1ExGqtpZCK7fa7cdyotS7364bFvTpo13Wt
+SqsvVNs81Vrb229fPnp4eX42nJ31Z2ebceiGPqcUUkrjMCI1RM85dWlAdOIgUt1N1dwLAEkT9dZ1
+Y61TjCmEuJQpcJjLIYb+ON3FlIJjbSXFoDYjUtf3yzwzsQcF4iGNpZXapjXDUqSkmM1irYWRpErO
+CQmXcowhiEqKAMhIILIgrCNqS7Gb52M/DIEGtQkoEjQRTTE6IK3sBoSmDtqI/PLiYp7nrn/QShvH
+/sMP3vzHf/TjL7/6+lefP//6yctlWQz96ng33ZRDm0ppzPTg0QhNa1NedC7tda0UIxAhRjcTUQQA
+QkBsy+IrKiIwUVrnQqdwC0IgxHXzrWruFCMSg5vUtlY+KrtabtdHn4iQGIlWkMd9l6wqN6YN3O9p
+5yiqbpZiGIbewaTtG6E0UnBzMFcXBTOnC4DXtorb7gkU5iB6Or1XXdBpVUzo4IHJ7leJqzp6/ZKo
+J0PuT5eAyUl6sjoN1oY49EPs81nKSWpxtHX+2Grr+x5IurE3UXAPnGqr3dDN83Q4Hs/OH3cUapmN
+luu7u59/8uSrr5/nfIYY52UPUBCJIIYAKk4I4m1ZalthEwZOGEIstSL6vMyqgkDjMADgoR05BFML
+HKe5/eTH33v33cdjn87ON32fkSzHwAG3m63IcrbJxP2yzDltgIQDuRuAx5AMtBbputGtqnGTwhQQ
+YJ73wzAu8xI4gVsIEYzVq7u7S6uSUhZRQFQXNY1su8OOKeQ0lHZM+VFQUFMAIiZwSF0SbYHy/rh8
+8eKbx4/Ow1lEUmLMqVuXR0rioJvNw3m+XXxOMQFoSn2KUFsBNwFZYZI5dRYoxQieEaFWCRxh0pQC
+h/L+e4+6MdNgv/z6iYKfjkmHlAMHcnOnMI4JL4deDvL6L47H7x32b5ngqu42N7CT3QSZKCY3M1EH
+J2YKYW0StTU3jzmmoUdAldVys/pa0U1Ng+uIFBAFXBzQmpzGLQjgR7QaGRxZzQAJiFbKDSCK+XGa
+TA/gjRBDoBgDrGHGKuBO8Y02f2bmhh6ZxMQMfGUnqhOi+KprgqZODrRKnk51/gm1rWoxcBO1ot1F
+NHNXN8b7S8DtpFP38PDBuaqWZWIO2+1DYlKZEGkpkzYPQVd1SsMVf0ld6iFDJC1uL1/dfvz3X97c
+HA/TgZhqnWClxZgR57kcRA0hiEgpTcRqFUIyx3mZRSSEdSOLfdfX2mqTeZ76vqfm6sJcf+93PvzW
+h2+en58HRmY6P9uKti6HEAIRup672zQfN5uLVmtABohmVcxFJ3ePsdvtXw3dGRGqakiD+TGmJGai
+0nejgbS5DkMA4FZFpHLI6KhaUkwiwhgIMYaU82hiAHg8XIXIhIEQDwd9dfXq+vp2t6+H3eRItS53
+t8c//dMf/t6PfldkKq0QsoOoQEpxXm7E4MtvXnzz+lkf809+8IOcc0gZQQBCqxy70FoJKRKm2g5M
+uN0OpdTdsX726y8+f/LVF0+eqVqtwkyJiQPVqnCv2IuRzTwmlmYQytsf/C3nT+Y572/fPe4/un7+
+yCQiM6cM7qairVEIQBRicFUpZV3AccrroLOVBgjojogc2XnllSNxIkoYAhKbFJPZsSCJ1FvTnWkF
+M3BDDhyCu5uIuyORm6mYw2TWTuNJRAREpr5LKmvF/9Z0a4HRfcXLwz/McFbb16rLaGqEsKaM4b3A
+dl2GiNppxuOwHOU8cnMDB1uvNAUnX3fXKoZ//9f/M2EAV0Aj4lpnDlENpmmP6CklJk55KLVOh7uY
+Ypd6xvTNs9ef/OrFkydfrZKT2pZaG4cgzdahuLmHEKZpRqfDNLWm81zmpfVdjiE4qJpsxnOR6q6b
+cRMibsdxGPPQx4uL8fGjR30fYuDN9iIE6focQwSwLnWAfpyOm81D17aU49rP5Dya1lrrOG6WeVlX
+4oiAEAhhrsfAmRhdPeXEIZU6M+K8HLtuczzuiXDszqZlH1OX05n6XpqFGJd5yrFDhmVeYsyvrq6/
+fvbNdBBt+frmukk1s5zPA/MyH0qbl0X3+yVl/v533vkv/8nvM0HO481+/8U333z9/JvPv/56d5hM
+zNz7IaYQ//E/+vEf/PAnzK5WwcixqUDOudRpKfbk1dUvvvji82+eLsvciqzGq5R4PeRqEQDIXVQ1
+M3c8YaUoBkfCGFTU1E0t5uAOKae7qzevXrx3uPtWWc5cHZkAYB1orqmiiEghrKs3QiQiCuzuFIKL
+qjQEIKaQOyRyFW2nZtdUzG7bcm1a3BuCAKivLQ2Am96vbNl0YlY38ZOSgoCIEAMBMRMTAUwv/7da
+d0ttazvr7oHIAZpoDLTaKqrIOgllQmaqVYlODg0iFHMmVDVEfPe7F7AhFXM7BU/drwLczfEXf/0/
+AQJzGMaLZT6IlBDY3OZ5MjfGQMjzMiNB3/Wm8NU3z3/xydd3t/OaCsIUdodbd6ylGZib1yrMwdwQ
+WE1q1VrbNM3z3A7H6fxszDnGyDGGy8uLPvNm0w9D3m6GEGk7DjnllIkDPHzwkIhS4GEcDbTW4mai
+NYZkjqpl7M7V2rLMfX9mXlOOtTQiIiAAB4JadBg2KmUpCzMbQOLooK21nLtaCuJqqbMUB2m1+SLN
+h75rTbrcK9Dr6+e//urJy9fPr293r69vRJWJmPidB9+tRRwUkaZp6roegUXl9nZXivRDvji/3G7z
+XXl9t+yP04QItSoi5syrncjdicnNx274w9/94X/++79HSIjx+c3NL774/Ge/+OXdfFiHCESY+uhq
+2jQwStNVslaL5C5Iux9+N50O5fau5m1/8cY2pmBqxLTG8YJDmRsxxhRUbX+zXZaP9re/dbg9d0Ui
+CrkDIqkVVuQbs4lwjABrRWTrJ9dO9zSFWc1BhJxSW65VdlJn1ebuzIFicDPyGoKZi2kRFSSWWgkR
+XFeaOqC7NVdZ9UVgBohkd1ZfWLth3AWqSy2iomZVdBXMrma6U/IA4ap9QEQRJaIYqFQNq3DD7PFb
+2+69NW/XpZnJfQ+84kE//sv/EQljjGVpTQqgj/2WiGtdHDzFPnKqreyPt0+evf78188PByFO0+HW
+HRC4SUVkcBJrauZGtS4IvJSZmGptpbRS6lLastSL880bjy6HsRv6mHPYbDY5h7Hvupxiotzly/NL
+wtYPGUC7bggUHKy1Ngzj8bjnwAQk2lLom4hqdfBxc15LYSbm4GClViaYl6nLQ2uqVjb9GRIfj7uU
++hAzwCKiXbdZlmOXBnMptXTdGbo1KfMy/92nn93sDt88e/Ly9TWA329P/H4Sz6UqOl8Ob0fqzXyp
+M4CvA6K7u6Map4Db7ZC78Bd/89OY8fLhsNnmNQEpd+GwL10fV/NrrauuCfrY526coVapiFiWxmuW
+HUAIVBZZW00VAzNTW8dcKYfjvhz2S6u6TLUWXSZpBkDYjWlzMbzx7uW98AelauqCVPXVfslYF3nx
+5Mf7u3+KhG62hsmAORByDGCGRK5mtk6BKHQ9uGurboa06rJOW2ddllO2BzQMLu2gbVJpYfVXuvNq
+LJbFrKoU1QoAuEr73d1WsrABiIOCm4miG7ibK1JBvQqwa+UK/UCoSxUnKCIMWJsyIazhA//ACmi6
+Bi6I2HaTP/jJo+Jq6toM1gDC+7Y49EMfOC1lyV2i5tN8rOqbYWsA02Gvuq+Bv/7m1SefPH356op4
+SCFJFVGfl4mJY+jVtNbFzJBYZR0erM5gNQUzqFViiN/+7Xc3myFH7ofUdTFGCoHPz85Sou3Z1qSe
+nW02Yxe4b63G3ANgzDQvZXt+VsvSdT0A1FYoJAyJXShEM1sdZKBaa+lyZyIhxS537hYDBxvd4Djf
+9nkTUrfMh5xzYG6t1GYAU6lzikMt8zTfjcNZn8f/+Lc/ff36rhuiqq4PPTHVImYWAi2LiJhqA3y2
+jW9pAweNoSdqAAmxzPMxbIYmYrPWQ5nvdJna2++d90MUMWraj4kQpPlxX46HclhptGrHRfohP3i8
+2Zz3wyavGdTdEPe387jN0tQdRQwcYo5SZX87a7O6K1U09CEhlbs5N3cKLXl7cXh1KLcv98NZN2y7
+7eWq7KI88HRYylxbkVoa+ys31bZK/zDEqCKx69o8mdqa8scpx5xNxVr7TebHasBR1VJP8jpwQKQw
+IHGMG/RGVAArYnVYqwMwS+Ahpk0AAAaVYrK4LoBEFM0EMROSSUWogAKgAAQQIWwbgKM6otmew43R
+15lfmXqsa56No7grEMB6LaxECWY6TlUPCiMQYRhDnRXdCQgTmmAgoiaFmRycKJxtL2LM3o5gmlL6
+6smrTz97+vrqujUx0AClaIVT0DI7oEKq9dZOaV9o7stciLm1BgCqXqtuz4b33nl0fr5JMWzGYXt2
+FgPGFHIKIvNmHLoUJ5tzig6VY8dhCJzEJhEDx1cvX/TdwMSOps04CqM5hVKLqqyizJSjq3Oi3noE
+FEW1xrFzd9MSQqyyIFnuOhADQgDcDL2I5JjVxFGGbtuqMPtPfud3//W//XfrwbxmZ7QmMVKtqmpA
+hCE4gcTwWl6f86UssD9cMQW3eV5mcBCTWokQeKbqdfby5Kubs/P+4kFfFpem+7ulLOIO3RDRQWah
+5lC1UHv62ev+sn/r/cvtedeqEGFMrOrrSyhVyiLlZW1Nu8SEYEVxkboIK59TkGBQNVQpCDw3M285
+XB1uXz+97cZ8djlyoHo3V/Pxoo9XDec7v1/bIgUVMZE6TWC2+mOQmUJopZgIrJGmhJySu3OMOk3r
+/gncQ9dzStoaugNA6jv3bCLIDACyHMAL4ELUQjBkdofAWbD3sLbGlaO5NQAFisToFpGROQChazUp
+jtURKJ4DbUO6c7p2A4umeiro3cGauTmJr9iIVUN3fLWUgyNhSMSRThJABQAIt7e3sJIEKbnHw2G/
+2z1rrZXaXry4efrsJoauLApkiHQ4HlJMJtC0lVoCpXl+GUMiTkhqBqoCgCLihmZeav3wwzffenyZ
+czeOPaExU46A6GAtcBiHC0Trcr/dbNQXEQ28XcrsJv2wASRECKnTtszlOPRj6vJSjoGTuzGDGQzD
+G7VdhZDmaQdOtS459EQIEGo5Dv1IsZ/nSf3/J+rdfmzLrvO+cZtzrsveuy7nHHaTTbUoiaKsMLIt
+iXZsR0ACWJBjC4oDJC8B4sAvfgryD+QpyP8TJE8BbCRBkMSwEoeWZYm0RImiyO5mX86lqvbe6zIv
+Y4w8zGr6PNTTQu062GvNNS7f9/uslIroQ0ocXJsRAyqAcxSurQDhMBxbW/7KN3/hD7///bcPjyVr
+GiRE9uY0xNZqmGIt2udt29bSGN7mtzPeApA6qsG+F1WPMUIkN53JqfGSlWZ8/em55DZMwc312nxv
+zlAR4NrGzd08WdovOwKsj9snzWKSr33jfl/LMMWa23Ld97VKYGa0rFiNdzclbDY1vK7ViUhwSuFp
+uby4mZ+uu1dfUfWaZY7lcVvNAfx4O0OzWDQ/rLfFFZ4+FzHVMI5t36ypdcUCcxhHQAI3LQV6H6zq
+7khY9+ymbc/PW1VEZHYALUVrfZZit0bMHCMgujnJaBqYDgjO8+yWvV2hLSKKFNxjq8EAgUBrQaxA
+BbC6FTB3NaRAzIgDEpk2FOWhALD1hbx5X3VZc2P8dw9DnyIZXNZyOAw5tz0rEmrrFFKQwPyf/oPf
++uyLh08+eVfhdN39Jz/+0ePTozsQ8+u351KamalXM6tFS63oVFvtuwImcXc3KjWbmXnYtxUJO0OO
+CL71za+//96rcRpTomEI0zwdjpMIjWM6Hg7ESCK3ty8RqwQ4HA9myhIYGBG2dQki27q1kpk5hiHX
+XTC5kbnueQ1hGKbjtj4Scq21g+LMQXWvWoZ0yGVfticiiWHI+2auh8Or0lYA37ctxmMp6zhMiJiG
+LrDTMc3CPKT0Rz/4C2dW5MZSFKp2MYy5wTCFUlqIEiI3tb1t4mFb9rxv25bdgRkRPYXp7U8/Y+Ka
+Wytq5tMcqbgBlK3ZpTa19lBEbg5iIci2bscUSm5oIHOopb357Gzurem6ZHvYtSgASBR5l+Fabznd
+D8MIsOWWhF/djHtp1axUrWohTbUWrKp7g91H86o6OI7AdS/jtehSUhjeO9HH29/SZtbas9cQAInC
+OLmqlqKtQZf7MCIRh8gsWqsjeO/5mON8QEQi0lIAgCS4GSAgSyul5Wytmemzw43ZHQDELZpPQCez
+ZAZhnLRmFPH+cSCIScIxHV8gJUTq8gokAiRw5PSHiI2423KAGImIIyEiMSIhEnYPJAo21ffnWZRu
+6Vikt9CAAKed+Xd++zdaNQfY9qfL+QtBDhKHcSCSjz96E8Owl03VEAQcRTjXKvzsXRIKtWpphZlr
+a/u2EsmyrAB0d3v81je/fjrO0zTHSCkFZgRTBE8hMSMCMNM8pjElZjQ11cbErjaMERhCYERgCUxi
+bvu+jdM9IpoWR+vQOwATDoTkiCFEAogSmcjMW8tBIgIRStM8jGkc71zVrBCySGi6LcuCxKVtpe4E
+7IBbvjCHr7z4ykdv3r45n5t6CJK3GiKb+TCG/gaoRelLiE0tTbl5Ia3Q+VAxBAnsZpfX75Rjyxkx
+haaRmOE+Pl0B4EDSdo3MXnP/n3T3xyCsZq2qqKs7MQUmZNRrxU01Nz3XEw9zoEFoL0V5iqgxyF7b
+spWtNDU7TYMBoetpGhgQMU6CUnV0uB1Ot+iMyAC3UwSwjy6/rkpmCgAcIsdEwlab9QaOCMwBnVhM
+zWrt66qu4EBCGSat1VrTWrvkAdxJGJBCSqZGneDt7u4ckwwjIoGZqSIiSnBD4immW5EbtIjIXWrd
+0y9MjVAQI+DIMhNGd5KEMv6BRDQFlmdU2TMqGr58YIUQoXvJiOhuHN6fZozzWDw1oOZ98My/9w/+
+lps3raXsqtq0EtFxvl+u+ZNPP9/21QwYo5k3q0wxxMGMVdUNSyt9X11qdSVt3mpj4fde3X7zl74e
+ogwxSsAhDSFQGgYWIXISn8YpprSX6zikIJEYWssS0jjdqa3uvq1bjHMue5DornFIbgCgQxx6PAcH
+KjUzpRhCbXurNcYIDtu2uFkMAwIzU5QRHErb1mUzr+bFDYPEOEwxTuM4Oxg9mwZJrdbSHCxKCgL/
++vt/PkwREXtmR4iyXHIahJhaVWZqTfuo0cGXvOVL2/fa5dYhBDd689nrgHYYE4Mh4P2QDmJfe3Eo
+e1v2cn+aYpD743hdMyJe9yIi1ansGZtFHrFUaTDxaJcNA83ZbddoiIhJ8O44rXsZo1y3fc1V1Y5T
+2qqVppcto3e4ECLCq1PaciXE45Qm8Xma3P32MDLRw2V7bN+ufkCkdDy6g5tZrW5ORBwTmCFhGEbX
+51u8D/URiVMiEoBurH8e+feZDoWgpbhpEEbwbsMlQhnGlrOrdlkedNaYG0mgEKArhsLMMovcEh3C
+dJIwOLjW3G3ZSAEgIF/j9MdmzoGsWo/6RQTqUZuEfcyPz/8AAMz9PqVtuQbCr9zOH9yfvHptxv/g
+d/6mmpZavrSYQc753bu3f/nxx9tWENnMAW3PubXaVHPOrdY9r61prc0BSs3arLVqCkjwCz//3nvv
+3aQhDiH2Ff3p5gbAhiiHaWT2GCQEIaCUwulwNCjz8TgMN2hQ6iWGsUtwz5cHAmpWqmZ3rLW5q7sh
+CiG1aiIS41jrdnN3qq0ejrfrdpmng0hA9lxyGm4BHUFFYkpDDEEdx+lgtW3bZV2u0zBczmcECDEw
+SW1lGg5gRISnw/DZuzdv3j15D0ll8h5nDZD3CgB5r8slX57288P69G5Fx0OchiFte0lpQIR9vS6P
+l1x1y/XmMDS1m3m4LHs11HhkK0MMb89rrlpV11w69QlcX5xmIVKDQyBxOM033zimdilNjYm+9uI4
+JzaArbSqtpdyvmZ3R6Rc27WYNhUmJLxuuap1kreZi/AQJQobD7XkNRd3j0Gmrz3SBLXdaA294u8r
+256xFedZS+Ug2r7cIQEAwHA8PdONWnNVYpGUTBUAwjAioNUSpbtkAIlZWGLSWlmk+2+Q+PlZIkLm
+tm0OoE1bre6ASBwCxWMtgjAjzkgDc0QCrRlo2bd/BQDU9XX4TObrHrVukiR+5nb2cqg0/YWXt4PI
+mqs75KqMRI78D3/vt0RCikOMg7ki8eVyrq0ua23N0vRq359aa2paizVrfanX1NZlI6KcM4K4Q85l
+msK3/71vzFM6zKdpHJmdmYcUQ2A3dWgpxmk8MNM4DIfpGKMs+xkMUozbdna3IU3jfCJiIp6mG/C2
+5004xjjs+wLoKYxqtWo2t5SOCA5k2tyalbyVUgRT04qItVTwNsaRCEsttZRaG4LXUmOKw5wcnRjH
+cVR1cOx+X3VL43HfViJ47+X9v/yjP+lT+emQuuT4etmXS96WUnJT9VbVmiHgsPjdq/vDPKYhXJed
+EMF8fbzGwF0eaQ5fvT+4+82ctOyXNT9ctiGFZnZ/HFeTkvO6F0LMtRHh7RxL1cOYRtIURSklhg9e
+nc5rzqU9XndEvG4FEYNwdQoMT9d8d0hfPC7MNEQZU8AeQ2jeA3+YaYh8SnzZ8l4aAAzHm02ebr/+
+xYv3/jjGnxBy05dmAEjEJDHmZXHVXvn0Vwox9/0A9qLfv9SM1hrnGRFZmIS/FChrr5Ti4UhM3d3Y
+736OEQBcjZhZQtt3a/rsXQMwc2BpOfcnxAwBguOIOHO4TfPXa9F8Xbbzeb/uNVvPDYD+0qPn1px7
+oj08MzYYkBTMfR4jI5kj37zPv/v3/lYp+bpccimnwx0Crdui5ufz1jkKOa8xJgAyQDd3p9IKeoeS
+N6aYS9amX33/xS9+42vdohEjj1OMUQ6negEkHwAAIABJREFUOQiPw5BSOExTs8wh3t2/b3UHrjHK
+zfEGwClERmbgXNbz5eE4Hx8fHqwVEUIktQbuCKEVFeHS9iHOw3xrrZSyElCp2cFKrbUaspa6O9jd
+/fvL+rRsZ3RKcWKSdb2y8Onu59btdQhhWzeWsZYqAVmIGIdhQhBkH1OKIczDtOzrj37yWR9cfvrx
+4/n1sl5yzi3vbZyjMNHS2lPBbK/m+fjyfkjDPA3DINfrbs0uj+cxxdKMCas+K0Rqs4fLuuyFEA9j
+fLhsVW3ZctU2xlCa5qod3wsA8xCSyJprHMZtWx+XvXM+DtOwlypM8xjVfC2277m/6HPta54e1ksh
+hCgUmJnp6brPQ3y8bstWxyFOKTw+XdIhlUDaWhrPNy9+CP7d5fybKBKGse17P139Swd8mEaJkXrs
+Ui0AhsjPu7Zeophard6aqfZ8dlVFYmsNHNzUTXur4K1Za+nmpLX1YRF8WUL1YzvOM6cIiFpr99lw
+CPBMIuDj3XcG+TvD/Pc4/ArCK9xxPz+s17UVdfvSA4kI4MTU4+kB4EAREbdSzWwaxPaLLNdtr4u5
+zvNh3a7ruuy5LEuuxWqxphd3qFmrtu4mA07r+TGEqKpNm7sy09c/vL+/O06HQx9LDCnkfRvHSVtL
+IYJbGoKIhMgxcsAWj7O26m5bXmqpIQxBZLeVWG7nec+rCJlbrRBkZG5EyMAxypaXZb3WUm8Y1v0y
+DccQ+TS81+rWtJo1cLvUYhXOj2+1KbMAQq37mq+3L27qbuXyQMjWcJ5OasWhXS77zeE+t011W65r
+DGkY5p989ukPP/74hx999sWnZ3AHBEL0a/XEtDZ6OZTH3Q3mxSaO53WXF5wGEYmEME3hME8//POP
+mlquChIBdUrBAXLVXJUI378/fnFtVQ0Armt+cZp+/MXGRNMQAaA1W60QoTmY+/v3xzdPS1UrVRFQ
+zSzOW1kQrDQVJtdq7lW1N8FdtgjQiKgZgECu7fY4j9Ns7inI3XF6KHjdVmFiAxYqua2X/O6zc6tf
+4YCqbqaO7F77Ke7g4+1t3XYDbPvetW7IIjHWdUPGOB0JvO21vxCeb2UgGUY3M3fNxc3AlETQjZiJ
+yPYdTPPlDEiuz56bvnjWVq1UM7fWENDBtTVXQwRiAYM0jq1wGH+txl+12WeHoj92+Dhvf5qX77fy
+DhE5EPHzz2urVVSQovDrx2UrDQEkt42YGMnB97y1ailMn1+f3AIxmRkY7G1HAlUrtbZ6TXHY8t6q
+EvHpNH3rlz8ch4nRiZ2pdBJWjFzyNk+3zKBQavNWyzQPXdPGzAQ9XcZljObKIUxhQmRCyvt+OByb
+NrV6vT4O49043S1Pn/Xd3Dy932qtTQ+HOysl7/XzL/7ka+9/PW9ba1UCj8OkarXlFMbW1BUdNcWE
+GsbjlNdzLdrKyoLmigjNfWtLa/npunzy+uHjN2+++4c/kIF6ZPnhNF7fLcCYS6Vq5OBZ93OOq8rh
+loY4cQnCH37wCsbJwcD9MN80e/TgMcqaC2G9gA/TEXOdhjDEAACP193dH6+lNJ2GeNmKMO2lRWF1
+GFME0xTl3XkNd4cf/vSd9fhR95c30w8+fnNHT4chvLus6tZaEyYiMoEyIkuKW4O9XbcypqBaIbCa
+L3s2isuuY5LBHcvWz2hb83bht58+btc63v5DwN8ql6puVmvf7hILEgJiWVYO4fk8JnZVNyi1Tvd3
+dd24s3skgJm1CoBuGo+HviWw1rQWREIWDFFL8dYkhDBNdjkTQJd+wnMaGCBJ23ZkQiYyMW2E6Kpd
+rsdB8rqGwwjMbErApSkRDvwL8fjref+7plrzRzX/ZVm/V/Y/3y6fggMH2l5Np8Cl6TSEHgsrzBIk
+EZJ6MQemhAClqrAQclMrtYgERJTAeW9m0NxMLcR4e5p+8RtfC5GariENKcUhJRaUEFhkHJJqjnE8
+Hk/jdHj79qfEcRhiEFKty3YZ0nB78wv7/tZs3/ftel2Ox/vWNqGYrYiwSBoHB9hR12mac973vDWt
+tVbBJBNRwFra3c19bTsxCkirlZiZAkdhoiipaWuqy359qA9fff9rpW4hxBgSi0zHV4/vPnl4+8U/
+/8M//tMff/RwvsZB8lbUfT8XCTzO8XQ/rz9d6r7jwIlIGsiQruecgvi6FF7bYXDwpdiR/bysD5fl
+k3/7J8u2s5C8N+0fL6Yahcu2jHNatrqXJsxBCLZNzYLwspWqWqoy0ZJrCpKrurWUooTAhO7+/v1x
+zfWs8fPHJQj394C7J+GsoTLk0TEmqurueDcSQDqXfG33cwTw85K16WHU2oAQ3+kujDFIVX1Y98sn
+e7m8GO7/iev71HZm0trMO/QKOaXeY7qblmKmPwvmRCJwy5erqVotSIxgfUT07LRs2iXWWkp39CKA
+ratpi/NBtVFTlOheiUl1BwewRiHIMAAkAKjr6m698UBmq8VaUyIgKsvyjABDZAkIYKa6bdCaxMj4
+8xI+mOa/Q4ClPZb9z2v+t4/5R6Lvlr0MMeSqPeM+gKFLCOGA7gbbdV0JBYFz3dzAHdV8z7t1bYhz
+bc3cv/7+zauXp2EMhJDSFAIPwzQfjloXIL05HNQIIKaUgoDV7e7mdJiO4F50OxyOw3hjtW7Xzzmg
+hFFbPZ1Ol8sDAsfjuOclNy+7xhBDDNu2sAgiBR4BPKSEiLW1aU4hza1pSkM9X2OK83zIZdvzuq7r
+z33jN5bzF+AZwQ/zKXCorbIIS/ro089++vDm//s3f/qwXbYtA2IIvC651sZMzJQGGaZ0fVrjKDcv
+p/iO3122u8P47rxuOQ9R7k/Te3eHLx6vZjbE8Obp4V9//tMGrVUDgK7B5Dkcfv5WPz3vub48TQCQ
+WyOl3Cq6xcDMdN6Lj2IhsFpYdVszIeRSmfC8ZJaQqzJTz+itT48VoDbLtQGiCGviNhpEoT6qcpBA
+vQcdToPlVh62ds5DFCIyhyhk7nnPxGGcZb0fKPIdY7ufgH56fbjHHn0Nz7lz6XSyZoCgpYAZELOI
+xKSttm0lljCktu+uPYnSAICIEfuFVMvOEvqy2MyIxbUBEXXncdaiKzKbu+XcN2XmjVNy05azmYHq
+M4EekZjarhzjM82lx2h0seuzyhkoBgFHInNnRXdA5ii3Yf4OHL9Dvsj+P9we2B3U7Lpm/rv/0W+o
+OUly81quVfd3j0/bWlV1z7tIqK2oNlV391yKm8+H9Gvf/qW729PxdBtDCCLCaFrnKQVB1TIOaZzi
+YZ5jFGY4jKcxTUEikgPZIDMg17qXsvVs1laLuSIJUGLSvaxIOE/HF68++Ozzj9QKcwgSAeHx8bVT
+Haa7YX5R6yOLPD0+akPC0NpGSLWWEOKQJqawba9TjCkORNxqq97+4Ps/+H++9+f/7Pf/39//3h9/
+/4d/uda9lta1U3mrMcl0SO59rdjn2liLQuJx91ZUmNZcf+XnXh2nmEsDhOtW9tIel321tpG5OwKk
+MZh7GrrX0j1Q/XKIiQBDDMuWYTg0rGUSvBsrARASc7hNoGgND6NUNSYA1ykFc8+ta/4BEZmoqb2t
+pZzSlkiGQMJuzkLDFAFhGEMfJVNgOSS5HdzBcjPzjlcZh2CvTusRaQgAbs2J9zj82XT4fcB3Zjd5
+nwhBYhexNi3FVLsEGtxbKYjIIWJ3SNcGHSxj+oygQwrzzCGwBCTSnMGdRYjJzXrwFUnQWqw2U/Pn
+jJ+OMGQEbKUwCxJRH6QCgJlrA4DDq5d134lIhqFzvpq25zxj9HA4doSGqZEpEiGxpIFjaHknnkb8
+MbTXe6nLVqOwlNIQoek7AEBxJnr95qFVQ2Az2/O+rtuQJgBvVVttL1+evvHz77EYoUeBIQUkSzEN
+KQ5DRMRpPEogYRIKKCJCYL3Db5flrK3d3eIU59ZySkMIMU03SLxd3xrYIZAqX65P3nBbt337+Hg4
+EsMQplw2R335lZdla17a3r4QjkHGwwQkWMpSchvCqem67Yuqrtf97v6+5PbxZx/98Cd/+ad/8aOP
+3p2RqXud0hCvT7uDj1Pc13L36mBqHYf69vPLOEUkjA77WogxJFkmAv9wefeTFOSLx2tXkry7bIFp
+HuKyl42RqOODXIRa1VqaOyNhPCb6EPdPrilwruoE41cOfox7hjjIeskSevIbSGD+Ktva6ruNEKPI
+PMR1r0TYzJ5aTUFSlDVhjSEejm4+Rt7XggjDHPsviUNv3tzNJbBEcbMsLO8f85vl8rjdvzis94Mn
+ExYksOYdoUWE7vX+gz895u/Pj++dH76zXX6J4+DPTS2QBNPqwAgQpqlum9XWmcugYLV+WRr5eHdX
+1wUBtdb+VKAIIFIIdd9JRIYR+1ARoWtOCZ6zwSRGZHY34mf5A5ghi6kCIgdZ375DxJqzIsYYeovM
+xObeWsN19Wd/M7o7goF5y4rYN8u4pL9u9IfI8GKeAxD+9//dPwZwM2WmcUp7zn/2Z59s2yYcgMAd
+TAEAcski8uHXv/K1r77sjhYJdDrOaQiIMKQoPUO1+1nB1+1CnL7y/i/uy9tWVxZgESHJOYcogESG
+zKLWlu366sV7Odec92kau2++mypQw7ItDnY6nva6zONRZCJJdTvvZenJJZ1Ivu07ER/muWl198uy
+f/rFmz/+kx+8eXyH5Oaec7U0Xs5536oEPpxSLSqB81bn0yCB12teLvs4xb72ksB5rynJthRAmOa0
+fP9Bl/183XNtN4exi84/eHVTnR7asNqbMlAagqm1qiFKH62W3NwhJWlbw6eqA+MhAKFW0x40zdRK
+C0k62LSL9XVv+fMrVpuHMKdQ1aYhXGDEWPUgEAgBSm7uPkyxk54AQSJrVXcYprivJU3RzbU91wYS
+OSTZzpuhu3lIQoQ1NweIQwB3YnoWipnFMWpVh9t3n3378vhXVacOhnkuRQiRqJUKpj9zsfwMZkoi
+XVmNhK2UDnLt/bS1FsaBQqzbmg5HLaXmjACuVTqZDDweb4i5q63rcoXntbNQkLat7gCI6XDcnh5N
+NTA7OBEhPof2cEymzdw5Jmu1C/j6RgzAJaV04D3/N4BqzXQ3ASDVYt4caVn1fF6JZJzG1qzWKhya
+llrrNA6//M2v3t4ejje34zC6X+dpBLday8uX74t4q/s0hU4vCxKIEBnq9npKgyd29xBFvZYK43TL
+KDUvzUqt7TjfgsO6XWotBjWl4TDeL9sTocQxnk6363a5rudc9pbteGP5+rqUcnN40R8k4VTberqN
+y3L507/4kx/+5OPXD2+v61KrLZe9NQuRb25HQlwuOwDOxzTOcb1mU69FWUibrZe9Vjvdjq0aEg1j
+2LfqDvvW4hBCZES0+5Afllw1iLRmh8NwWfNn7y4phnV5B7cxBAaAVnWYIji0qtiQmQDRmeP90I4a
+wbUqIPJA22IhMiBIiG7ec2mZCQmGU+IkeM7r62UewnFK28g2QpwmK8rC+5IlcH91xCHY8+gTZRQz
+a1XHQypb5cBdxdRJlSUXEGREAJDAy3nv8tJ+D0tkbaZVSUibDodU9/Ptq//r5Vf/5bp8e1n+g/Pr
+AwBwEInpWTIN6PYzQ3rPzAuIiGDuBka9dSYJpooAHMTNrFVX289nRHIzU+Pn4Y8jMYdQrld3dzWw
+Xjywm7maqXW8irux9NQ+ICAzA2g9CQGZkZDUUIS0GoCZkjwvEFpp7W0ejn+l4feAECPKdPvhw+t/
+a67WTNXevbuoorm1VtW0w1Tef//+W9/82jCkeZyHYUDaQxyGGEMQFqzlTDCM4ai1hRhM22VbUhoD
+pSFOVdd1XVStx365wXJ+nOdJoThYHCQNQsjzdMi5MHHZc+NKJIB2vV4udpkOQxpj01LahngUCUGi
+akW1OJw++uyTjz/7i+/+4Q8+f/1GAi5LGcagzRBhnGKtz+5BANBmzYADlawi3ECHOZraes2t6v1X
+jrX0lbc/PVQWYmFt1pqRsAGM7x3f/ujdPMbaNARBQiZ6vGyvbvkb793+1EtlkkAsiZlqaX0Vz0MK
+Y1wvu+bWlUWmO7iHKODQ7bwitK8ljQER0xjK3lrVNIV4N8IUmuPTMckYEkDH6JrZ4XYse0tTzGvt
+xjEW1GYSuRWXEMz8cDeFKNrM3bYlm/e0FW9Vu8xsnKOZfxn25Ns1H++mVpWIpsOwXva+QJVop/hH
+x5s/Wh9/T/VbHMKzQ1IY8VkBCm7mFqY5pMG1SYxai+67u5OEL7MG+3PyjCXtUVNuRoSIbO5uRsT5
+6QwIHJNBfT633UDNTAk83dy4Q1234XRspXitCL2BBgTQpoIgIQFC2feOQCEJSCwxAkK+XEgE9Nsg
+3yNCSiwPb3+Uc3b3ZVkRsTavrSzLEkMCQGL6tX//w1/8ha8LD4wmASXCkG7HIZW6EcPlfB7SEGYp
+egGAsiEohUhAXSi5M8s83lgzYGNmczNryD6Pc841pWFbt62eT4fbEKNaxaaPD4+39x/e3X/w9OZH
+ta2l7AD88v59d8slj4fb159//pNPPv7k9ad/8Ec/IAFEULWUJOcGACU3ZqzVY5TDMbVm10tOSeaD
+VKCyNxHIuYEDuNeiAB6TXM9bh2dIZGhAzCHFUjMzcRBGqLl94zsfPnz3p1XmzVo5r01tSCEIvbus
+dhtqbtNhrqWp+XgzXy85TbFV9a2IkATuqXhxTnXNAJDG0F9BZa8sPB2GsteyNwBPY3B3IDp9cKvq
+qMZCphZT2GqOKbTS4hgAIA6yXfcQRVgkdBQuTMdhX3In7G7LbmYSqRWPQ+gtRzeFEBMLAoKpAcA4
+p8u7NY7B3S8PSxgCmDNj2eu+lNcfP1LaWnNXIxGO0fZdW+kcHndL85FjslrAfT+f3RSJSYSIZRzr
+crVWKUSOyVp7lig8wxj9y4BgIpa+Z+iDHwdwbRSiqzoASijLQsTgtr57R8wpBgGXKGo9VcC0NSCq
+64YIpgpmAAbkPdjFAVWt1b9G6X90MACQdXnstMdpmrQBwlJrYwrgFBP/7e/82otXJwmC0JgwBk4p
+ImppqwRRw9PNiRC2vKQUT6e7w+H2009/XOomijEdhzTUlh8vr0OUaXx5OH31ev6JkJj5Z59+Ngwn
+wrFpIabr+gSA4zgxhfvbqHpenuo0DrWxmTate1u//yd/9umbh09fv/3izWdm5uZplGejFsDlnJkx
+DXI4pHUpMSIS2Je3+Pm8ewhGbGbX886E/cQ9nIaO1tnWYmpKboBhjMOUzAwBzPzNp0/TcTjcDJeH
+tYzUXj811fvj9Hjdj2Nac3X3WklGKcUUyBFr85ACImo1DJymUHMb55SmuJ73MCXuzCdwABgPKTZj
+xhClVUVCChyntF1zPwD7Dd1tHPPNaGqmz7TQvJdhSsQ4HoeyVW0qgd19Oo77mpfLCuhxEDMntu2a
+0xQkSL+sVR0mKXtjpuZmZhy4iyjTFIcpunvJbXncHj6/IP81sr+KWtumjug9MAoAWdyUWMBd896F
+DKYNAaw1AFdvrWQ3k2Fw79DzZ74iEmnJ3RYM4BQHGQYU6QLmdj0TYY8jx66YdwczFCFga7X7mB0B
+kERQEF3J3KwUBGAJLKKldPw1GDQ3REfCePshwq82+75E4r/5m99ydwQCp8t127baWk3jdHMT/8Zv
+/PJ779+HQOB1nqab42k+HI6nl4FCCCHEaYiJEXLdBaNQaLW9efOZap3neR5PgLrXpVmJMQnFgHFb
+36gWd3CDIU0ssG1nBBn42MnuanW5ns0tcIoy5pqblsfHh//tX/yL//mf/u/f//O/+PSLL7Z9CZGv
+l1yKdjnaPKfDcViuZT6maQqff3oRJjfIu5pBrcpMwxgccNl0PiY3z3s9nAYW6rI2QBzm4XLOJByH
+uC3F3Wtuvc4OSZ6VKgjpZlw+eeozzSFJ15k9lRpfHVyEgsQhImKrrX+L0zGZeRpTq8qBy1YlMABw
+Cq4WorTSAOB4OwEACY2nsalzYFOz1sEhzwMiEjI1CdJn32mMvbsdDwMR7kvpQXHDlAAgJlEzYmQh
+UydGQOx9v5ubehwEAPZr6R/h7hJFm2q16ZTWSx6Pabvmd5+e3376NBx/Ow7/te2lqfWVGBEyMxEj
+eEcZW2sOICH0IQwSAWIPHHCzME3j7W1ZV0SSlJ4RVqodsN4nQMPtrdVGzG3dWtkDUTeCsUiY597R
+UogSEhJbyQggjIGx7woREYkkBCQCrYyIhABgquhAImDWGXVainDD+D1A5N/62389cKpFwfHxacml
+1FZ/9Ve+/u1f/eDVq/sQKMUwDmMQBrSQRgIzL9t+TVGIoGkmohApJiEgQnLUm8N9rXUr13mep/EU
+5MTIQObUjoc7CbG24miXy/Xp6Xw63lbbi26l5eW6a4NhiO7ancoI3BT+l//z/+hQexEapwAA56f9
+cEwxibZniKSDx8jbWvPeugpqnKKZEdP1UvLekOn06nh+2NxsPg6ItC4lTel6ycRcciPCw81YS+sD
+li8lZSiBT3dzq+rqcY6h+gHocdnVXW4Sv3eYfu4Ghcbj0Kr2Oj6vdToMrVkfRJpapzJ1I1GIktfi
+iMeboeQaB0FENahqLGzNxjldnzYWIiEAIKLxkMBBAvcbOg5hedrGOQFiK43kGSHRARASeF/LdBxM
+XUIgwuvjCg4SpTfliBAH2ZfyPA5iGubYqnVaRAdSrOf98rBeP7vI8Ls0/BewZVCral8W88/ChY5F
+6dg5InJTcDBtgIDMrt5nRt60bJs1bfuuJXtT1+ZuCNDV18+XqWrJgIjWKYv6LDXN2VsH2QMHQew1
+lIcgHREOAM8tcggUoyNJSmmaNO+M+O+CF7oOT83bIcz/3NT4N3/9m1Ubk5j54/kyTem3/+PvfOub
+P3c4TOM0xxA6A7lpXbdljAEB1nULEoIEB+2/FQCSjCkMqnaYTkQoEY7zLWGkMGrba9v3fXWF1uq6
+nc0UwUvT43FWK7UVABiG8YMPf6l5fffuCyISjMJDiOFfff+P/+xHP3p62GvTWtXMtPnxJu1bRcS7
+F5OpL5eyb7VPQg7HIUYW4X2rqr7vNQgfTgkJz5e6rcUB05RKUUlh3+p0SL0ecId9q3krIYkEvn11
+LHsDQIksQoi4XnPNrRBc3i56COPP32UGmQICDkNw89YMAESYmIip1jYdx33JLbc4hhhlPg2mXnND
+wjjEfi/WasgShkBEvSKvRYcpuUNMARFYqH9/iFiLhiTa7PmOd7h5cYhDkMAS2cH7A8CBlvPeSnOH
+kESCdImlRG6labOQngWnwxyJaV9KH2LGQZDocDN+/pOH5Wmj+J/E+T93czQPTBXoyyk7ODgixvkg
+KQFAnGbqsn7rzpVOELLe8SITh2i1gAM4ducA9bUMIgL0IqrDiJCZEZiwh4kgQM99iofDeHuTL1dk
+DvPsDkNgJtJWkUiEVXv3FiVGcK/7TqbdW9CNjmEarWQzB5o5/NDxDf/ar33Yd2nny+Xly9Pf/3t/
+43CIgHa6fRE5gpmTdl3pi/uXALCsZ7WC5Cw4hDmGpFbHYR7SMA6TBKqaL8tjK1pKDYku5zdPl4ch
+zcMwjdMoHKbxFEPkeDOMR9eSy96aTsPMmK7n8yc//TGzsPAwjmZt25f/6Z/+sy3nYQy3d+MwhnWp
+7t5RORJouZZ1KSJ8czeWoiKUBsm7lvy84h3HOM8BAHIxSBGJeqydJGHhWlrNrRbtFtIX751aNXA4
+3I7Xx02bjYdYtrotRaIwIQsNc2oDd3Fsv4es2TBHt753p561o2qHm7HTTQ63U0iyXvY+o4wpSJQ0
+RomsBn1y34rGIZRcy17TGLQ9727SFLfLrs0AgJmQsE/30xg6D+v0Yl6eNlUjQXDXZvNpOL9dWDhE
+iYm7aAIcRLhvTMdDylstW2Wh9ZpNTULXL6C7l7199IMvro9Z4n+G/DsxpB51A6aurQeu93nOeHvn
+PczdrG5by3tvfJEoDEMYhrZvAEghhjgAYtv359Q7BAAn/NkEFTgm77teRABkhC6sQA7PYmxEb+pm
+XXekubibcDcnGkB3bjoSyTTbvtZ9RwdwR/DW9aT+fCW0xsIIFeR7/Ju//su5ZGL44IMXv/7XfpEI
+3Jyc+oxLrTJRZzCxcIwRnIQDMaN3ADyFENytFVNtRJDLigil5RcvXsaQAPEwnhCBmcfD/XC4K/nS
+NGtbk0iQ2KwKh1abeRGhu5v76TCmMAmOVfc/+/Env/8H/yYEFmFTL6URYUoyzylGzlmJ6P7llAap
+RZdrcYdta0TPapwOZpIhLGtDpsdzG6Y4zLEWZSYJfHnczPxwGvNeVa3kZuoilIZQiy6XvX/1Mcly
+2U2tlubmpnZ92gBBhGIKaQzMpM2GMZBw2et4HGquveapufXyiZn6MIeEetV+uBl7FrcEPj8sZa8h
+CQDEIdTSDrcTIl4e115ss7BEAQdTQ8TpMCCiNdOmbhaTlFzjEBBhu+YeocXCxJS3kqbYb3pT7wRz
+bdYfZhaeDqm/VcCxFn3706d9aWH4R8L/oddKrWlt4s3N0D2EUJq6G8ckw9D2rZXiat65KeBEBIDW
+Wsu7NUNmYuYQWs79pYFE7kBdY90d8mnodRSnFMbRSxYi957KCmaKSCTspqZKTP483sFAHgiICIi4
+a2IBvLXWjFioM3PUgMh7ZHLvmwHCODG/Mvxf+b/6L3/7r/zyh7/6rV/84KvvDymJSIppGIZpHocx
+ultKYZ4nCQEccikxxnmazLXqXlsepuN0uFuuj8gm0VEAEGppMQV32/YrOBB0OnZ9enqN2pj43cM7
+4TBPh+t6dvPWWhpiCGG/6MPjw+vXb2KM6mXL6//9L//V4+XcuWvjFPetdVv69Vr62W/m61JrNVP7
+EpRpiNjUW3MQSlNUc3coWY9fuSl7e3xzFSEzlyital9mE+F0HObTgAjreW99TfvlAd/rjXGOeW+m
+Np+GOAStGoegTUOUNIb25XJXkpTny0YWmm/G+TgQo5m5gQQepjQdh23J62XPW5lP474WFg5J4hCI
+KG9FmyGANtNmaYxxEHD4mRE5jhEdVapaAAAS0ElEQVQJp8Ngam5m5izUC7l9Kf1Ju315WM5bGqOp
+mcF2LT3gh4hClC9TgmxfynTsFGvUZq8/fjRFDv+I8DdJPTGZY/c0V7UYg0hIQUREHduekYgkgJmb
+9h1wb5zQFVTH+zuS0PLOIXCMbuBm2P0A+BwLDg4yTCQc59lqBURopa+cOUQSkWFwbW4KSBJjPBzL
+tnWjfRR+PuQdzdwcHImEW6kILrHDVAD7n4SI4D33CYnC8QXg9/if/OPf67RaJBOWHkVB5CyoTUOM
+Kc3rsoD7zekmJnFvyCaBSy53L3/udHoP6haYkBydAw9fefX+8XA089sXH8yHl5YzAqq2N29fI9Ld
+7f2+Z2u27cuynodxRMAg6XJ5QqdhSne3r17ev9dzqwz0n3/3u+bW29zlWpiw976tWhpknEKIbO6t
+qprXqhI4jWE6DkB4vZbjadi3llJgptPN8PCYa1YWcvcQOG81DiFGuZ53dzjejuslr5ddO1WTKY3h
+cDMOU2xVEWG57NYsRAHEPm3c18L8vF0iIQmUUjAHJIpJlvN+uJ1MrZWGhNNx7J9Y9trb5d5saDMJ
+DAh5Lb2liikQY7/Szadj0matqqmrapqiqfXoeeleL0YO3HKTKCFK2WscwnreQ5SebtaqtlL77L8L
+N2tRUz/cjmWrDhACP71dPv/xu7zxdP/fhvQ3bFnkeWXliN7zG9XhZwTpgQEAvNfupuDORIDyM6cv
+IIV5Ijdr2icw3av+s/MbEd2VRMb7F21by7paa5qzEBACkjwjGWPQnE2Vg/SQDqulLwpSkC77486q
+c0cAEnHwTvICNwUCty5l7UUeEIN7WVbwK//u3//rxJCm4+39z3MUgDzPMzNfzk8APM93rW3uTYS3
+bTG3aZyiDKA0DqP7Rl5iYBGJcSDGmOiyPL19eBPTYNYs70BYfScMp8Pt7c3tsp4v10ckTylJEAQe
+46lpTmkY05zCwCxbudSav3jz2Xf/6Ht/8ZOf1qqt2b61ELi3biHQOMVhkBg5742ZuppSYhgPKQ2x
+qeW9hSjnx72vC2rR88NWgYhZAh9uxjTG5ZK1at4rER1uhut5z3s18/k0TMfU5RL/f0/nsivJkpXp
+dTMzv0Ts2Lkz89SpKqqRELR6whQh8QKMkBBiBFKrJZCQKCaIx2LMgBdg3CAQork0B8hz8rJvER7u
+bpe1FgOLrHkoYhDuZuvy/f+PhNs1t6qtaIhyOI0hyf27w75kN2tVa1YkDMPt38pbQffTu+N6ySFJ
+TGFbchrDfi3ICADz3Xgb6psRkQj3Yt3U4hBCCrWjQVste5uOQ6vaSuuCWndnYTePSUjY1IY5IaIw
+A+D1dct77Z+JY2hVJUiIfL3sZa8xBQkcB+ljaK0aIjMzIJatrJf9y3+98BbSw5+D/6ouCwEigZoT
+s+rNCteBkEhiclMwJ5F923vwHmM3qL7pufrjrqVY055n1AsYaxUcGLH35TfGrruw9J7KjRG7WwQg
+uKmVYrUCMQkzMxDftI6IaRx6sGm/dvpqjadJJBCzTBPGBPtqXctLjH1j7GYOguB6lGFIQ5rH8bQt
+H9wLMW/bCg4pDYR+uXwUDpGngMIJWaiWesmvRBBjujs8ENKWL/u+19oAjRhjDAS8nte7E0Hkmnc3
+Vyjuba86jvOQDpfloq0CgFGt5TnFQaHtbWkXUtXDcXbQ0+nuuw8fTT1GMffWTJsZUwjYehipOwsh
+czOnIB12MPPrkvtMk4Ri4hCZmFh8nOO2tKY63w0lNyza4bNhjjFJ3mo/7b756T04bNdsPQ56cVNL
+Y8AorWrJTYS284quh2Pc1yKB4xAAwNTzViTwMMV92ZgpbyWmEAcx9zRGZmqg56drt4WNg7DwvpUx
+CtHNv7Zc9jiEvNXemLbSDqcxDsHdy1bNvPNCHHg6pMcfzpfnay16OI1IOEy3MNbxmC5PV0C4njcO
+3MVMIQkx7tdCjNoUoLOr8PCju+//9cvj92fEke//VORXW87IgbCFGFut0APRgJoqEcb5oECB6RZa
+vazeCgKgCNzC6L17ooObxNFa7aYp6I6mwgTYGaF+iFPnKdxdUuyvAOOtPkN37eF4iOnuwMOUX56h
+tXS867bj5qiAbnb7TnMANQNG5HGq29ajgwmpEQETInEcXbUsFxcR/laSHBi5lOcQJKU36NjRAEBb
+LotInIZDrdtW1hAF1Gtu03CYphmRai2d2Q4iQ4/+Q0MCExWOhBKCzIfp+fklpTikoTUruWzbJhwP
+8zHXdd0u5nbdzm/f/xTguC7fD2Oodk1j+vTh+d+++yEmLqVtaw2BfvlXHtalPH65mnut/rNfedj3
+dv68zsf05t385eOSc+vF+jAGEWJhYQqBX55WYZrmiAAhyn7NxNR3T+uy68WWV9dmfWf8+cOLq8cx
+9DqkZDXzVq1P7ry1wJGZp8OwrUWEr9ciQVptABiSDGO8e5jPL1tIjLcFDXR87XrZ4yBu/u4n948/
+vJq5MOatElKrLY0RAIY5sZBtJU0RHJbX9eHbU78EVP30dj4/Xt1dmz1/vvRFQRpTqy0NMaRgaut1
+BwQWlsgSDKmzBlCLxiSmqhUAgRgcPG/t+dPlh///SPyWhz9B+ilLcHUr5WZxqT1ugvpIM8yzm0uS
+ure6XXu1QxKYACWwBCLKl1c3AwkSYpin7eW5z8tuZQ84IvgvjKxEOEjb91sBg4REagqmJNHMwNWR
+OEjbM0roSRjr66sIgzu6JoLG7ABM5ARuloZQm9XXl06BmhqAg7oCEgFpQxYkcXND5//9B7+N0phZ
+JJrV0lbV+nJ+/vL5CyLO86SeHRUQVK1lCFFQzKCpdQoKmSNjsGaABmAhjO5e6oasLFRKJSIEzqUt
+50uQ+Ob+gRiqZQBlpmVZf/xLv353/zMr6xBTKZuqtWL/8C//b62X7VpV7XBIIfKXz2tzmO7GPatE
+zllr0ekQQ5Dnx7UWDYFbNWLctwqIZna6n4ZRSu65EhanQVIw85CkFV0vuwjPd8M4p7y3mCSmkIZA
+TLW0VhsR9d46RT7epVZb3dv7H5+6QxY4fPOT03Leb8zzKKZuBqV6N+Zn4WGONSsxHU7j9XUbD4O7
+r0vuO6yuZMpbIUYJUksLUdbzlrd6uJ+0qrunIdbaOnTZvg5Dj2/ny9MVEVtVIpQoiJjGaM0Od2Of
+UxEzArx8WsZDZKay1/79HWEARDN//nh+/PCK+DaEnxO8B3c2t5ojo5mBtqYOiMLQ3NPpJOOg16Vs
+e92zaTM1MyMkQOrUp+aspQARMYeU2p6tS4FvyDZ0TL+f7o4oMXUP257Cbd1RHQz8q7KsBwIAuqqV
+0tdYVquZMYKgqRo4GKABmgMCeLNWm3t3QyHssUVdV0BkTVvesbN3Ivx7v/ubboBOtW3mDbvmx+14
+PEzT0KyqNWaepsOPvv0lR3t5fURAQgo0BR7iEABt288KpVkJw3E4vC3lOaU4jlOp9eX5+d03PxvH
++31fiGCc0mU557yHEIZxcqXDfNrWj+vyg7ZMjPN4jGEEhL/8q79+elqWS5bAaYrbrhRYDfLWDndD
+iFKrqnnJui7FHdIgIfD7b4+tWmsWAsUoaQi16nLeS9F9q7n248Bb0X0rEuV4GnsDPUxxOqQYhYSu
+rxsH7k9/DJQSp0GGKXaz1dendb1kImLh7Zq1makjIjFzDLno22/vzFybjYe0LXl53VpuraoIj3Ms
+e01jTFPcrnl52ZAwphCShCh5r26gTYc59YFPa1b2GmLwm8U+DFOqta2vOyAQUUhyuJ/Wy+4AwxRD
+kuVldfOef96qAniIARDjEA6nkSMjoKohwOOH1/PT6v4Q0p8RviVVdmu1khu6taY3CSSjqjoKOHTz
+ZzNwRDDtCXjeW17TlouVjCLI0m2lf+GnQiI3XTE49RUKIRPHwwxESBTnOUyj7RuBuysQufeL4muu
+nRszkQRw7Skevd/o4oTbC4YIyI5uqqZKRBIjIvaDA4nclBAAUN0YUaZJDvOdCJkqcSJBlhEwtrpY
+21QbAMzzFON4vHv/+vz86dOHIKLuIQwxcQq87+vnx08S+Hic37z7n+Pw5vXlX9CBSa7X9eX1WSh9
+/+//weIk4EZatpA4xCFwKmvJ+w5kSPz2/kdB0uvlcdlea2l//0//vOfsiHGKYQzqcP8wmcG+1857
+9sGoMMWJt7UigAhLpOfHFRHevp9b032rZ92IcRiDqTVGD6EYaOvkIYH7ct4l0PE4taYI6OjW7O5h
+1qauSijwdRH7+cPrvtVhChLIAVqzuzdhXfzufqxtNaQwpT6M//jdExLePcx5K63qdByY6XAaLy/r
+9XVXtf2akbDsVYSmQ+pdeGvKRLVUYiIhSex+S6vWpmmKxGjNzs9XRJRAKaTpOLx8vrSqEhgAtyXn
+rYxzcoCQBBB6T0KEnW3unXGfgH387mlfcxx+zf2PXAOZCmM37WfCpr0aAUA0NZFYb75tgCm6G9Ts
+AEAMpmBfc5ascZDp/fuyrG1fIQZOCZC8ZFdFZtDmAI4Erm5Ow6hNOcUwDPl8QaZ+RZAEIjZAjhHd
+y/XiDsQBRcJ8KJcz1OqAbhCHoOpdgkyATdVMY0zIWHXXVlEEkFACWANTYnEHcI0s1VT3nf/4//zO
+ON2nMBECcyRm8ixE67aYWuRBG7baPn368PT0iZmnaTzOpyGNW75Uyw56uj9FSUKjl3K5fKhtNbNS
+KpMc5hMLpEHUm1Cc0imGaGrIXnVbtwuAEUngQSuu68Vd81ZyyX/zd3/3dFnMoKsITJ2J9q2eX/ZW
+b6v4kttP/se9CL08rYBw/zCVoq/PGzGWovve3DyNIe+NGOEm1YVta2kI03HY17KvZToMh/tpedlq
+bnmrWvXuNHhr+7L3NrpV25bSzcbGOfZ1RKesbvyJQ0yy7Y0Jl/POX7Gczth02vkGziC4OzHdvz/W
+0hAwRDncz9r08rwCgFZLY2TmHtLo3Zi7aIjMwjW3mIKZ71sZ5+F63t2sl3zjIe3XvC05RHb3ste+
+0GWidcnjMXXS4fhm2i55W/KHf/2yvKwkvza8+QsobPsWuIdMkqn2tpFFDNBNgRhDQDdVlRiBWHMG
+QFcFBCQmBIJe5iCHJNNYt61PPDkEb7ULX7zdkiSRbja20/tvwDxfLlqrlaKtRb69An3IgxIAQfcN
+EUikDzFbrWA3eJr5Rvj0ZQghmhtx6D+B7ghu2gKTmXWna2RCwKZKzOYuh4cf13zJ+cW8oePAM3LY
+y7X3rM0rk8Y4vJ/fEaNaSTIRBIPMwvf3J0JaLzlwQGdHlQTD8DCPd9p0ub4OQ1QLL8+vrdjhNDXN
+1/2ViMio7HW7lru7+2Ga3F1t50CAHCJvmf/2H/5tmMLxNPRtf1fr7ns1s+OUhil8/riEQN//5wsA
+piGMU+h8REcMET0Ejkk+fjhPc9z3Cg604XVTmUYzf/zh3MuJ+/eHvJWcaxDKa0lJCL3k1qoe30xv
+vzl8/x8vNdeamzkQkQOS8HRgV0eE19d1OqT5kJalXM97iMJMIQkRAcC2ZAk8HYa8lf2a0xiGOV1f
+ty/fvxBR2SsAbNccogxjJKZWtNVm6giwvKynh1nEzDxNaVvyfs1lq8eHSZu2ptMxmsHDt3fa7PnT
+JURBJjMvuUzHoTWNQ8hrOZ6maR602n7Nzx8vLPTxu6e2ZAz/i4efl6XBvvVsXFVnciRorRJLF5m5
+AxO3plYbImgtoIqIWquqBREDZEAD75J/07Y9PntH/6x0g6B+8H91yqLu308i5boS9gg9J2FrzYBM
+Fc1QgjX1tiAyAgKSqYYQSAiInBnMgcgotpKxp5WBmxkBOiFKJEQtVeaJzWxb8Ssr4a3PTAHcEIn/
+8Pd/SwjXbb2u1xgHZrosL7XWWhsiDuOgDfalPj49fnn62LJO03y+Pu95HceESIiHcXoHpsykmDv6
+8vj0cS/Lntfz5fzx08dPnz+nIYUkBg0RzE1VvfHxdOeU1+3i/XI11oIhpP/7z//4sj9rs5fnjQhN
+/XgaAOBy3iXwfEi1aEoyH9LpzcRM+1pN3czzXpEAAWvWXm2///GRhbdrcQcifPP+OL87lq2qWkfT
+rudtfd3u7tJ8iLW0EKR73x7uRzP/9F+v6yXfZucOIrxedkBoRXvr3B3jusYqF3MHCbwtmYiGOY1z
+6lGNcQj9gKyl1lxP7w5pTG4WkgxT6vNTAFe1kGSc0zCn9bzHQbYlj1M8nMZ9LaqKhDW3NAYmRKLt
+svd6Ka+lbHWcIzEd30xpjL0vL3tBukXHOcDysn7896daWsTfSG9+7o20FEImhBAEHJjAHM1ABGsz
+CRLmQ392OUSZJmutZ664KQubOZiaOQk7IIEzEQD8IhUYe7Xz9QYkIaSvftEitxR7FmJBcDQVoi5D
+IwBEQDfQZm4yjsjS41Y5JiQGa0wYY2AhMCVCpu7fBcSMwhQCMpmaxNhyQdNOIHGMQZhCVFME/G8K
++1s8lHyeawAAAABJRU5ErkJggg==
+"
+ height="419.69443"
+ width="553.71448"
+ transform="scale(-1,1)" />
+ </g>
+ </g>
+ </g>
+ <metadata
+ id="metadata40">
+ <rdf:RDF>
+ <cc:Work>
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
+ <dc:publisher>
+ <cc:Agent
+ rdf:about="http://openclipart.org/">
+ <dc:title>Open Clip Art Library</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:title>Golden Picture Frame</dc:title>
+ <dc:date>2012-05-24T10:08:07</dc:date>
+ <dc:description>Golden picture frame, Landscape</dc:description>
+ <dc:source>http://openclipart.org/detail/170182/golden-picture-frame-by-tasper</dc:source>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>tasper</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>clip art</rdf:li>
+ <rdf:li>clipart</rdf:li>
+ <rdf:li>frame</rdf:li>
+ <rdf:li>golden</rdf:li>
+ <rdf:li>landscape</rdf:li>
+ <rdf:li>photo</rdf:li>
+ <rdf:li>picture</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <dc:contributor>
+ <cc:Agent>
+ <dc:title>edited by Paul Sherman</dc:title>
+ </cc:Agent>
+ </dc:contributor>
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/publicdomain/">
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Distribution" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+</svg>
diff --git a/launcher/resources/multimc/scalable/status-bad.svg b/launcher/resources/multimc/scalable/status-bad.svg
new file mode 100644
index 00000000..9f47307e
--- /dev/null
+++ b/launcher/resources/multimc/scalable/status-bad.svg
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg3891"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="status-bad.svg"
+ inkscape:export-filename="/home/peterix/minecraft/src/MultiMC5/resources/multimc/64x64/status-bad.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <defs
+ id="defs3893">
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter3850">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.5"
+ id="feGaussianBlur3852" />
+ </filter>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3827"
+ id="radialGradient3823"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)"
+ cx="3.9371533"
+ cy="7.5016646"
+ fx="3.9371533"
+ fy="7.5016646"
+ r="2.5" />
+ <linearGradient
+ id="linearGradient3827">
+ <stop
+ id="stop3829"
+ offset="0"
+ style="stop-color:#b80000;stop-opacity:1;" />
+ <stop
+ id="stop3831"
+ offset="1"
+ style="stop-color:#600000;stop-opacity:1;" />
+ </linearGradient>
+ <radialGradient
+ r="2.5"
+ fy="7.5016646"
+ fx="3.9371533"
+ cy="7.5016646"
+ cx="3.9371533"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient3889"
+ xlink:href="#linearGradient3827"
+ inkscape:collect="always" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="5.985117"
+ inkscape:cy="16.047085"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="532"
+ inkscape:window-height="454"
+ inkscape:window-x="2159"
+ inkscape:window-y="166"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata3896">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-381.14286,-467.79075)">
+ <path
+ transform="matrix(6.4,0,0,6.4,361.94286,422.99075)"
+ d="M 8,9.5 C 8,10.880712 6.8807119,12 5.5,12 4.1192881,12 3,10.880712 3,9.5 3,8.1192881 4.1192881,7 5.5,7 6.8807119,7 8,8.1192881 8,9.5 z"
+ sodipodi:ry="2.5"
+ sodipodi:rx="2.5"
+ sodipodi:cy="9.5"
+ sodipodi:cx="5.5"
+ id="path3811"
+ style="color:#000000;fill:url(#radialGradient3889);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ transform="matrix(0.93333333,0,0,0.93333444,382.20952,468.85732)"
+ d="M 31,16 C 31,24.284271 24.284271,31 16,31 7.7157288,31 1,24.284271 1,16 1,7.7157288 7.7157288,1 16,1 24.284271,1 31,7.7157288 31,16 z"
+ sodipodi:ry="15"
+ sodipodi:rx="15"
+ sodipodi:cy="16"
+ sodipodi:cx="16"
+ id="path3813"
+ style="color:#000000;fill:#df0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3840"
+ d="m 392.64286,476.29082 -3,3 4.5,4.5 -4.5,4.5 3,3 4.5,-4.5 4.5,4.5 3,-3 -4.5,-4.5 4.5,-4.5 -3,-3 -4.5,4.5 -4.5,-4.5 z"
+ style="fill:none;stroke:#000000;filter:url(#filter3850)" />
+ <path
+ style="fill:#ffffff;stroke:#000000"
+ d="m 392.64286,476.29082 -3,3 4.5,4.5 -4.5,4.5 3,3 4.5,-4.5 4.5,4.5 3,-3 -4.5,-4.5 4.5,-4.5 -3,-3 -4.5,4.5 -4.5,-4.5 z"
+ id="path3819"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3854"
+ d="m 392.64286,476.29082 -3,3 4.5,4.5 -4.5,4.5 3,3 4.5,-4.5 4.5,4.5 3,-3 -4.5,-4.5 4.5,-4.5 -3,-3 -4.5,4.5 -4.5,-4.5 z"
+ style="opacity:0.51879697;fill:#ffffff;stroke:none" />
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/status-good.svg b/launcher/resources/multimc/scalable/status-good.svg
new file mode 100644
index 00000000..0a35c80f
--- /dev/null
+++ b/launcher/resources/multimc/scalable/status-good.svg
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="status-good.svg"
+ inkscape:export-filename="/home/peterix/minecraft/src/MultiMC5/resources/multimc/64x64/status-good.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3827">
+ <stop
+ id="stop3829"
+ offset="0"
+ style="stop-color:#b80000;stop-opacity:1;" />
+ <stop
+ id="stop3831"
+ offset="1"
+ style="stop-color:#600000;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3801">
+ <stop
+ style="stop-color:#00b81a;stop-opacity:1;"
+ offset="0"
+ id="stop3803" />
+ <stop
+ style="stop-color:#00600d;stop-opacity:1;"
+ offset="1"
+ id="stop3805" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3005">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3007" />
+ <stop
+ id="stop3781"
+ offset="0.8142857"
+ style="stop-color:#ffffff;stop-opacity:0.49803922;" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop3009" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3797">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.52592593"
+ id="feGaussianBlur3799" />
+ </filter>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3801"
+ id="radialGradient3807"
+ cx="3.9371533"
+ cy="7.5016646"
+ fx="3.9371533"
+ fy="7.5016646"
+ r="2.5"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3827"
+ id="radialGradient3823"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)"
+ cx="3.9371533"
+ cy="7.5016646"
+ fx="3.9371533"
+ fy="7.5016646"
+ r="2.5" />
+ <filter
+ inkscape:collect="always"
+ id="filter3850">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.5"
+ id="feGaussianBlur3852" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.8"
+ inkscape:cx="8.6376236"
+ inkscape:cy="10.641334"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="false"
+ inkscape:snap-bbox="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="532"
+ inkscape:window-height="454"
+ inkscape:window-x="2290"
+ inkscape:window-y="202"
+ inkscape:window-maximized="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2993"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="15,16"
+ id="guide3783" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="16,14"
+ id="guide3785" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="94,38"
+ id="guide3833" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="84,34"
+ id="guide3835" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1020.3622)">
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;color:#000000;fill:url(#radialGradient3807);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.50000000000000000;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;stroke-miterlimit:4;stroke-dasharray:none"
+ id="path2997"
+ sodipodi:cx="5.5"
+ sodipodi:cy="9.5"
+ sodipodi:rx="2.5"
+ sodipodi:ry="2.5"
+ d="m 8,9.5 a 2.5,2.5 0 1 1 -5,0 2.5,2.5 0 1 1 5,0 z"
+ transform="matrix(6.4,0,0,6.4,-19.2,975.5622)" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#00c900;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.50000000000000000;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path2999"
+ sodipodi:cx="16"
+ sodipodi:cy="16"
+ sodipodi:rx="15"
+ sodipodi:ry="15"
+ d="M 31,16 A 15,15 0 1 1 1,16 15,15 0 1 1 31,16 z"
+ transform="matrix(0.93333333,0,0,0.93333444,1.0666666,1021.4288)" />
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path3787"
+ d="m 6.5,1037.3622 3,-3 4.5,4.5 9.5,-9.5 3,3 -12.5,12.5 z"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3797)" />
+ <path
+ style="fill:#ffffff;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 6.5,1037.3622 3,-3 4.5,4.5 9.5,-9.5 3,3 -12.5,12.5 z"
+ id="path3001"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path3809"
+ d="m 6.5,1037.3622 3,-3 4.5,4.5 9.5,-9.5 3,3 -12.5,12.5 z"
+ style="fill:#ffffff;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.54887218" />
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/status-running.svg b/launcher/resources/multimc/scalable/status-running.svg
new file mode 100644
index 00000000..18209940
--- /dev/null
+++ b/launcher/resources/multimc/scalable/status-running.svg
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ width="32"
+ height="32"
+ id="svg2"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="status-running.svg"
+ inkscape:export-filename="/home/peterix/minecraft/src/MultiMC5/application/resources/multimc/64x64/status-running.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="3840"
+ inkscape:window-height="2125"
+ id="namedview32"
+ showgrid="true"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-nodes="false"
+ inkscape:bbox-paths="false"
+ inkscape:snap-bbox-midpoints="false"
+ inkscape:snap-bbox-edge-midpoints="false"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="false"
+ inkscape:zoom="10.429825"
+ inkscape:cx="2.5496161"
+ inkscape:cy="28.936353"
+ inkscape:window-x="1200"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4160"
+ spacingx="0.5"
+ spacingy="0.5"
+ empspacing="8" />
+ </sodipodi:namedview>
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4162">
+ <stop
+ offset="0"
+ style="stop-color:#0071f1;stop-opacity:1"
+ id="stop4164" />
+ <stop
+ offset="1"
+ style="stop-color:#007ec3;stop-opacity:1"
+ id="stop4166" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3827">
+ <stop
+ id="stop3829"
+ style="stop-color:#b80000;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3831"
+ style="stop-color:#600000;stop-opacity:1"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3801">
+ <stop
+ id="stop3803"
+ style="stop-color:#f1ab00;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3805"
+ style="stop-color:#c39a00;stop-opacity:1"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3005">
+ <stop
+ id="stop3007"
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3781"
+ style="stop-color:#ffffff;stop-opacity:0.49803922"
+ offset="0.8142857" />
+ <stop
+ id="stop3009"
+ style="stop-color:#ffffff;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <filter
+ color-interpolation-filters="sRGB"
+ id="filter3797">
+ <feGaussianBlur
+ id="feGaussianBlur3799"
+ stdDeviation="0.52592593" />
+ </filter>
+ <radialGradient
+ cx="3.9371533"
+ cy="7.5016646"
+ r="2.5"
+ fx="3.9371533"
+ fy="7.5016646"
+ id="radialGradient3807"
+ xlink:href="#linearGradient4162"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)" />
+ <radialGradient
+ cx="3.9371533"
+ cy="7.5016646"
+ r="2.5"
+ fx="3.9371533"
+ fy="7.5016646"
+ id="radialGradient3823"
+ xlink:href="#linearGradient3827"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)" />
+ <radialGradient
+ cx="3.9371533"
+ cy="7.5016646"
+ r="2.5"
+ fx="3.9371533"
+ fy="7.5016646"
+ id="radialGradient3786"
+ xlink:href="#linearGradient3801"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)" />
+ </defs>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(0,-1020.3622)"
+ id="layer1">
+ <path
+ d="m 8,9.5 a 2.5,2.5 0 1 1 -5,0 2.5,2.5 0 1 1 5,0 z"
+ transform="matrix(6.4,0,0,6.4,-19.2,975.5622)"
+ id="path2997"
+ style="color:#000000;fill:url(#radialGradient3807);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="M 31,16 A 15,15 0 1 1 1,16 15,15 0 1 1 31,16 z"
+ transform="matrix(0.93333333,0,0,0.93333444,1.0666666,1021.4288)"
+ id="path2999"
+ style="color:#000000;fill:#009cff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <g
+ id="g4187">
+ <path
+ inkscape:connector-curvature="0"
+ id="path4168"
+ d="m 10.455414,1028.3241 0,16.0761 L 24,1036.3622 Z"
+ style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:0.95251006px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ style="opacity:0.54887217;fill:#ffffff;stroke:none"
+ id="path3809-3"
+ d="m 10.5,1044.3622 0,-16 13.5,8 z" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/status-yellow.svg b/launcher/resources/multimc/scalable/status-yellow.svg
new file mode 100644
index 00000000..140e6082
--- /dev/null
+++ b/launcher/resources/multimc/scalable/status-yellow.svg
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ version="1.1"
+ width="32"
+ height="32"
+ id="svg2">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3827">
+ <stop
+ id="stop3829"
+ style="stop-color:#b80000;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3831"
+ style="stop-color:#600000;stop-opacity:1"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3801">
+ <stop
+ id="stop3803"
+ style="stop-color:#f1ab00;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3805"
+ style="stop-color:#c39a00;stop-opacity:1"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3005">
+ <stop
+ id="stop3007"
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3781"
+ style="stop-color:#ffffff;stop-opacity:0.49803922"
+ offset="0.8142857" />
+ <stop
+ id="stop3009"
+ style="stop-color:#ffffff;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <filter
+ color-interpolation-filters="sRGB"
+ id="filter3797">
+ <feGaussianBlur
+ id="feGaussianBlur3799"
+ stdDeviation="0.52592593" />
+ </filter>
+ <radialGradient
+ cx="3.9371533"
+ cy="7.5016646"
+ r="2.5"
+ fx="3.9371533"
+ fy="7.5016646"
+ id="radialGradient3807"
+ xlink:href="#linearGradient3801"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)" />
+ <radialGradient
+ cx="3.9371533"
+ cy="7.5016646"
+ r="2.5"
+ fx="3.9371533"
+ fy="7.5016646"
+ id="radialGradient3823"
+ xlink:href="#linearGradient3827"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)" />
+ <radialGradient
+ cx="3.9371533"
+ cy="7.5016646"
+ r="2.5"
+ fx="3.9371533"
+ fy="7.5016646"
+ id="radialGradient3786"
+ xlink:href="#linearGradient3801"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4496779,1.5407764,-0.90127514,0.84794135,4.9906134,-4.9255796)" />
+ </defs>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(0,-1020.3622)"
+ id="layer1">
+ <path
+ d="m 8,9.5 a 2.5,2.5 0 1 1 -5,0 2.5,2.5 0 1 1 5,0 z"
+ transform="matrix(6.4,0,0,6.4,-19.2,975.5622)"
+ id="path2997"
+ style="color:#000000;fill:url(#radialGradient3807);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="M 31,16 A 15,15 0 1 1 1,16 15,15 0 1 1 31,16 z"
+ transform="matrix(0.93333333,0,0,0.93333444,1.0666666,1021.4288)"
+ id="path2999"
+ style="color:#000000;fill:#ffcc00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <g
+ id="g3010">
+ <path
+ d="m 7.5,1034.3622 17,0 0,4 -17,0 z"
+ id="path3787"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3797)" />
+ <path
+ d="m 7.5,1034.3622 17,0 0,4 -17,0 z"
+ id="path3001"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="m 7.5,1038.3622 0,-4 17,0 0,4 z"
+ id="path3809"
+ style="opacity:0.54887217;fill:#ffffff;stroke:none" />
+ </g>
+ <path
+ d="m 8,9.5 a 2.5,2.5 0 1 1 -5,0 2.5,2.5 0 1 1 5,0 z"
+ transform="matrix(6.4,0,0,6.4,-170.62857,989.56227)"
+ id="path3776"
+ style="color:#000000;fill:url(#radialGradient3786);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="M 31,16 A 15,15 0 1 1 1,16 15,15 0 1 1 31,16 z"
+ transform="matrix(0.93333333,0,0,0.93333444,-150.3619,1035.4289)"
+ id="path3778"
+ style="color:#000000;fill:#ffcc00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ d="m -144.92857,1051.3623 3,-3 4.5,4.5 9.5,-9.5 3,3 -12.5,12.5 z"
+ id="path3780"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3797)" />
+ <path
+ d="m -144.92857,1051.3623 3,-3 4.5,4.5 9.5,-9.5 3,3 -12.5,12.5 z"
+ id="path3782"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="m -144.92857,1051.3623 3,-3 4.5,4.5 9.5,-9.5 3,3 -12.5,12.5 z"
+ id="path3784"
+ style="opacity:0.54887217;fill:#ffffff;stroke:none" />
+ </g>
+</svg>
diff --git a/launcher/resources/multimc/scalable/technic.svg b/launcher/resources/multimc/scalable/technic.svg
new file mode 100644
index 00000000..91cbd3d7
--- /dev/null
+++ b/launcher/resources/multimc/scalable/technic.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="2546.7" height="2526" version="1.1" viewBox="0 0 1491.6 1479.7" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <metadata>
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <path d="m658.25 18.964c159.05-19.094 324.19 14.764 461.94 96.756 143.3 84.188 256.41 218.77 313.51 374.94 62.353 168.13 58.63 359.69-10.432 525.21-63.691 154.53-182.78 285.02-330.23 363.42-149.71 80.709-327.85 105.84-494.33 71.979-172.46-34.279-331.02-133.53-437.23-273.73-96.759-126.04-149.83-284.84-147.76-443.82 0.548-167.95 63.08-334.56 172.34-462 118.05-139.77 290.39-232.01 472.19-252.75m-120.67 181.49c-131.41 49.538-243.48 148.49-308.51 273-60.641 114.51-80.038 249.94-54.906 377.02 25.076 130.19 97.609 250.19 200.16 334.01 101.76 84.247 232.92 131.9 365.06 132.75-0.795-37.831-0.366-75.591-1.04-113.42-0.792-31.415-30.866-53.744-59.722-58.563-0.917 28.305 0.119 62.895-24.771 82.113-23.731 16.288-55.638 15.919-82.479 8.782-27.149-7.626-42.216-35.808-43.011-62.532-1.647-40.565-0.244-81.196-0.729-121.83-10.86-0.059-21.781-0.059-32.702-0.059-0.06-35.442-0.06-70.892 0-106.27 10.92-0.065 21.84-0.125 32.762-0.125 0-12.076 0-24.093-0.06-36.115-14.033-4.635-30.263-10.188-35.934-25.379-14.033-33.186 3.417-70.462-10.188-103.89-13.911-35.203-27.759-74.125-14.825-111.89 7.992-26.598 34.042-43.56 61.007-45.694-0.06-76.808 0-153.55-0.06-230.36-0.429-40.754 16.288-82.725 48.924-108.16 37.215-29.525 86.57-37.092 132.81-36.786 102.92 0.061 203.7 48.864 270.2 126.89-0.124 18.362 0 36.727-0.124 55.149-92.185 0.061-184.36-0.063-276.54 0-10.006-1.828-19.523 7.505-17.754 17.569 0.304 58.812-0.549 117.68-0.308 176.49 17.021 0.061 34.104 0.304 51.125-0.488 2.866-11.774 6.038-23.425 9.454-35.019 74.982 18.424 149.89 36.789 224.81 55.335-2.502 8.6-4.938 17.266-7.382 25.927-6.588 2.5-13.177 5.001-19.767 7.504-2.621 203.15-4.331 406.36-6.648 609.51 176.25-60.091 319.8-208.83 369.76-388.73 39.415-137.02 25.262-288.62-39.527-415.7-63.628-126.65-175.58-227.98-307.72-279.1-133.78-52.65-287.09-53.321-421.36-1.954m140.74 710.66c0 11.401 0 22.817 0.063 34.221 10.125 0.06 20.253 0.06 30.381 0.06 0.061 35.448 0.061 70.896 0 106.34-10.128 0-20.315 0-30.442 0.059v32.333c19.949 0.06 39.961 0.06 59.909-0.061-0.917-57.646-0.365-115.31-1.283-172.95a9391.2 9391.2 0 0 0-58.628 0z" fill="#1389d2" stroke="#000" stroke-width="27.532"/>
+</svg>
diff --git a/launcher/resources/multimc/scalable/viewfolder.svg b/launcher/resources/multimc/scalable/viewfolder.svg
new file mode 100644
index 00000000..4ba0ed0a
--- /dev/null
+++ b/launcher/resources/multimc/scalable/viewfolder.svg
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) --><svg height="128" id="svg2811" inkscape:export-filename="/home/david/document-open.png" inkscape:export-xdpi="22.5" inkscape:export-ydpi="22.5" inkscape:output_extension="org.inkscape.output.svgz.inkscape" inkscape:version="0.45.1" sodipodi:docbase="/home/jakob/dev/kde/src/kdebase/runtime/pics/oxygen/scalable/actions" sodipodi:docname="document-open-folder.svgz" sodipodi:version="0.32" version="1.0" width="128" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <metadata>
+ <rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <cc:Work rdf:about="">
+ <dc:title></dc:title>
+ <dc:description></dc:description>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>unsorted</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <dc:publisher>
+ <cc:Agent rdf:about="http://www.openclipart.org/">
+ <dc:title>Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title></dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title></dc:title>
+ </cc:Agent>
+ </dc:rights>
+ <dc:date></dc:date>
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/ or http://creativecommons.org/licenses/LGPL/2.1/"/>
+ <dc:language>en</dc:language>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" gridempspacing="2" gridspacingx="4px" gridspacingy="4px" gridtolerance="10000" guidetolerance="10.0" id="base" inkscape:current-layer="svg2811" inkscape:cx="64" inkscape:cy="64" inkscape:grid-bbox="true" inkscape:grid-points="true" inkscape:guide-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="736" inkscape:window-width="825" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="1" objecttolerance="10.0" pagecolor="#ffffff" showborder="false" showgrid="false" showguides="true"/>
+ <defs id="defs2813">
+ <linearGradient gradientTransform="matrix(1.0033808,0,0,1,-8.2378002,8)" gradientUnits="userSpaceOnUse" id="linearGradient2937" x1="122.74438" x2="122.39215" y1="96.721588" y2="20.043535">
+ <stop id="stop2939" offset="0" style="stop-color:#72b4f4;stop-opacity:1"/>
+ <stop id="stop2941" offset="0.13053299" style="stop-color:#b3d9ff;stop-opacity:1"/>
+ <stop id="stop2943" offset="0.34621301" style="stop-color:#b3d9ff;stop-opacity:1"/>
+ <stop id="stop2945" offset="0.72006166" style="stop-color:#71a8f5;stop-opacity:1"/>
+ <stop id="stop2947" offset="1" style="stop-color:#508ed9;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient gradientTransform="translate(242.00093,332.5)" gradientUnits="userSpaceOnUse" id="linearGradient2927" x1="-178" x2="-178" y1="-228.3945" y2="-304.61469">
+ <stop id="stop2929" offset="0" style="stop-color:#cfe7ff;stop-opacity:1"/>
+ <stop id="stop2931" offset="0.1" style="stop-color:#71a8f5;stop-opacity:1"/>
+ <stop id="stop2933" offset="1" style="stop-color:#2c72c7;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient id="linearGradient2822">
+ <stop id="stop2824" offset="0" style="stop-color:#ffffff;stop-opacity:1"/>
+ <stop id="stop2826" offset="1" style="stop-color:#ffffff;stop-opacity:0"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(1.0033404,0,0,1,-8.2374684,8)" gradientUnits="userSpaceOnUse" id="XMLID_9_" x1="71.999496" x2="71.999496" y1="14.2578" y2="19.9583">
+ <stop id="stop46" offset="0.25" style="stop-color:#71a8f5;stop-opacity:0"/>
+ <stop id="stop48" offset="1" style="stop-color:#0057ae;stop-opacity:1"/>
+ </linearGradient>
+ <filter height="1.768" id="filter2807" width="1.0512" x="-0.025599999" y="-0.384">
+ <feGaussianBlur id="feGaussianBlur2809" inkscape:collect="always" stdDeviation="1.28"/>
+ </filter>
+ <linearGradient gradientTransform="translate(-6.999995,8)" gradientUnits="userSpaceOnUse" id="XMLID_6_" x1="72.000504" x2="72.000504" y1="96" y2="0.00048828119">
+ <stop id="stop7" offset="0" style="stop-color:#00479e;stop-opacity:1"/>
+ <stop id="stop9" offset="0.0769" style="stop-color:#2c72c7;stop-opacity:1"/>
+ <stop id="stop11" offset="0.58579999" style="stop-color:#6ea1df;stop-opacity:1"/>
+ <stop id="stop13" offset="0.96450001" style="stop-color:#adcbee;stop-opacity:1"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(1.0033808,0,0,1,-8.2378,8)" gradientUnits="userSpaceOnUse" id="linearGradient3109" x1="122.74438" x2="122.74438" xlink:href="#linearGradient2937" y1="96" y2="20"/>
+ <linearGradient gradientTransform="translate(242.00093,332.5)" gradientUnits="userSpaceOnUse" id="linearGradient2923" x1="-168.99216" x2="-168.99216" xlink:href="#linearGradient2822" y1="-300.5" y2="-296.48441"/>
+ <linearGradient gradientTransform="translate(242.00093,332.5)" gradientUnits="userSpaceOnUse" id="linearGradient2925" x1="-178" x2="-178" xlink:href="#linearGradient2927" y1="-228.5" y2="-304.61469"/>
+ <linearGradient gradientTransform="translate(242.00093,364.5)" gradientUnits="userSpaceOnUse" id="linearGradient2197" inkscape:collect="always" x1="-168.99216" x2="-168.99216" xlink:href="#linearGradient2822" y1="-300.5" y2="-296.48441"/>
+ <linearGradient gradientTransform="matrix(1,0,0,0.7368421,242.00093,284.36842)" gradientUnits="userSpaceOnUse" id="linearGradient2201" inkscape:collect="always" x1="-178" x2="-178" xlink:href="#linearGradient2927" y1="-228.5" y2="-304.61469"/>
+ <linearGradient gradientTransform="matrix(1.0033404,0,0,1,-7.2374684,40)" gradientUnits="userSpaceOnUse" id="linearGradient2204" inkscape:collect="always" x1="71.999496" x2="71.999496" xlink:href="#XMLID_9_" y1="14.2578" y2="19.9583"/>
+ <linearGradient gradientTransform="matrix(1.0033808,0,0,0.7368421,-8.2378,45.263158)" gradientUnits="userSpaceOnUse" id="linearGradient2207" inkscape:collect="always" x1="122.74438" x2="122.74438" xlink:href="#linearGradient2937" y1="96" y2="20"/>
+ <linearGradient gradientTransform="translate(-6.999995,20)" gradientUnits="userSpaceOnUse" id="linearGradient2212" inkscape:collect="always" x1="72.000504" x2="72.000504" xlink:href="#XMLID_6_" y1="96" y2="0.00048828119"/>
+ <filter id="filter2770" inkscape:collect="always">
+ <feGaussianBlur id="feGaussianBlur2772" inkscape:collect="always" stdDeviation="2.0786429"/>
+ </filter>
+ <linearGradient gradientUnits="userSpaceOnUse" id="linearGradient10213" inkscape:collect="always" x1="98.617439" x2="91.228737" xlink:href="#linearGradient10207" y1="106.41443" y2="99.254974"/>
+ <radialGradient cx="102" cy="112.3047" fx="102" fy="112.3047" gradientTransform="matrix(1.295034,1.3831431e-7,-1.3627884e-7,1.2946006,-30.093452,-33.119615)" gradientUnits="userSpaceOnUse" id="radialGradient9437" inkscape:collect="always" r="139.55859" xlink:href="#XMLID_8_"/>
+ <clipPath clipPathUnits="userSpaceOnUse" id="clipPath7084">
+ <path d="M 72,88 L 40,120 L 32,120 L 32,80 L 72,80 L 72,88 z" id="path7086" style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
+ </clipPath>
+ <filter height="1.3839999" id="filter6697" inkscape:collect="always" width="1.3839999" x="-0.19200002" y="-0.19199999">
+ <feGaussianBlur id="feGaussianBlur6699" inkscape:collect="always" stdDeviation="1.9447689"/>
+ </filter>
+ <radialGradient cx="102" cy="112.3047" gradientUnits="userSpaceOnUse" id="XMLID_8_" r="139.55859">
+ <stop id="stop41" offset="0" style="stop-color:#b7b8b9;stop-opacity:1;"/>
+ <stop id="stop47" offset="0.18851049" style="stop-color:#ECECEC"/>
+ <stop id="stop49" offset="0.25718147" style="stop-color:#FAFAFA"/>
+ <stop id="stop51" offset="0.30111277" style="stop-color:#FFFFFF"/>
+ <stop id="stop53" offset="0.5313" style="stop-color:#FAFAFA"/>
+ <stop id="stop55" offset="0.8449" style="stop-color:#EBECEC"/>
+ <stop id="stop57" offset="1" style="stop-color:#E1E2E3"/>
+ </radialGradient>
+ <linearGradient gradientUnits="userSpaceOnUse" id="XMLID_12_" x1="96" x2="88.000198" y1="104" y2="96.000198">
+ <stop id="stop83" offset="0" style="stop-color:#888A85"/>
+ <stop id="stop85" offset="0.0072" style="stop-color:#8C8E89"/>
+ <stop id="stop87" offset="0.0673" style="stop-color:#ABACA9"/>
+ <stop id="stop89" offset="0.1347" style="stop-color:#C5C6C4"/>
+ <stop id="stop91" offset="0.2652576" style="stop-color:#DBDBDA"/>
+ <stop id="stop93" offset="0.37646064" style="stop-color:#EBEBEB"/>
+ <stop id="stop95" offset="0.48740286" style="stop-color:#F7F7F6"/>
+ <stop id="stop97" offset="0.6324091" style="stop-color:#FDFDFD"/>
+ <stop id="stop99" offset="1" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <linearGradient id="linearGradient10207">
+ <stop id="stop10209" offset="0" style="stop-color:#a2a2a2;stop-opacity:1;"/>
+ <stop id="stop10211" offset="1" style="stop-color:#ffffff;stop-opacity:1;"/>
+ </linearGradient>
+ <linearGradient gradientTransform="matrix(1.0172054,0,0,1.5,246.03226,514.75)" gradientUnits="userSpaceOnUse" id="linearGradient3385" inkscape:collect="always" x1="-168.99216" x2="-168.99216" xlink:href="#linearGradient2822" y1="-300.5" y2="-296.48441"/>
+ <linearGradient gradientTransform="matrix(1,0,0,0.7368421,242.00093,284.36842)" gradientUnits="userSpaceOnUse" id="linearGradient3387" inkscape:collect="always" x1="-178" x2="-178" xlink:href="#linearGradient2927" y1="-232.84966" y2="-304.61469"/>
+ </defs>
+ <path d="M 118.983,31 C 118.992,29.35 117.64999,28 115.99999,28 L 40.961007,28 C 40.961007,28 32.061006,20 30.961,20 L 14.999998,20 C 12.799996,20 10.999999,21.8 10.999999,24 L 10.999999,31 C 10.999999,31 11.999999,116 8,116 L 122,116 C 117.99999,116 118.983,31 118.983,31 z " id="path15" style="fill:url(#linearGradient2212)"/>
+ <g id="g17" style="opacity:0.6;filter:url(#filter2807)" transform="matrix(1.0033404,0,0,1,-8.2374684,20)">
+ <path d="M 132,96 C 132,98.2 128.4,100 124,100 L 20,100 C 15.6,100 12,98.2 12,96 C 12,93.8 15.6,92 20,92 L 124,92 C 128.4,92 132,93.8 132,96 z " id="path19"/>
+ </g>
+ <path d="M 10.884862,54 C 10.893892,55.75 10.902922,57.755 10.910952,60 L 119.09511,60 C 119.10414,57.755 119.11317,55.75 119.1212,54 L 10.884862,54 z " id="path50" style="opacity:0.5;fill:url(#linearGradient2204)"/>
+ <path d="M 119.99722,31 C 120.00622,29.35 118.66422,28 117.01422,28 L 42.975222,28 L 36.389222,21.414 C 35.611222,20.636 34.075222,20 32.975222,20 L 12.014222,20 C 9.8142222,20 8.0142222,21.8 8.0142222,24 C 8.0142222,24 7.9822222,54.499299 8.0142222,60.031299 L 12.014222,60.031299 C 12.014222,53.222299 12.014222,24 12.014222,24 L 32.901222,23.997 C 33.083222,24.019 33.470222,24.179 33.560222,24.243 L 41.318222,32 C 41.318222,32 114.02722,32 115.99922,32 C 115.99922,32.435 116.00022,56.400299 116.00222,60.031299 L 120.01422,60.031299 C 120.04522,54.499299 119.99722,31 119.99722,31 z " id="path2896" sodipodi:nodetypes="ccccccccccccccccc" style="fill:#5e95e3;fill-opacity:1"/>
+ <path d="M 124.36598,113.79242 C 124.27969,115.00674 122.85389,116 121.19831,116 L 6.812906,116 C 5.157329,116 3.731522,115.00674 3.644228,113.79242 L 0.007982,62.204632 C -0.112423,60.992526 1.143808,60 2.799384,60 L 125.21183,60 C 126.86741,60 128.11762,60.991789 127.9912,62.203895 L 124.36598,113.79242 z " id="path30" style="opacity:0.9;fill:url(#linearGradient2207);fill-opacity:1"/>
+ <path d="M 125.21293,60 L 2.7999261,60 C 1.1449261,60 -0.11207393,60.992526 0.0079260701,62.204632 L 3.6439261,113.79242 C 3.7309261,115.00674 5.1569261,116 6.8129261,116 L 121.19793,116 C 122.85393,116 124.27993,115.00674 124.36593,113.79242 L 127.99093,62.203895 C 128.11893,60.991789 126.86793,60 125.21293,60 z M 120.41393,113.05263 C 118.87493,113.05263 9.1349261,113.05263 7.5979261,113.05263 C 7.2299261,107.83726 4.5229261,70.627562 4.0659261,64.149246 C 6.5189261,64.149246 121.45793,64.149246 123.93493,64.149246 C 123.81393,65.85872 120.49293,111.92821 120.41393,113.05263 z " id="path2894" sodipodi:nodetypes="cccccccccccccc" style="fill:url(#linearGradient3387)"/>
+ <path d="M 4,64 C 4.0273488,64.775875 4.1802721,68.801119 4.2225137,70 C 7.123925,70 122.78934,70 125.71499,70 C 125.74343,69.191222 125.93026,64.204735 125.9375,64 C 123.41788,64 6.4952049,64 4,64 z " id="path2908" style="fill:url(#linearGradient3385);fill-opacity:1;opacity:0.835"/>
+</svg>
diff --git a/launcher/resources/pe_blue/index.theme b/launcher/resources/pe_blue/index.theme
new file mode 100644
index 00000000..c9e0d93a
--- /dev/null
+++ b/launcher/resources/pe_blue/index.theme
@@ -0,0 +1,11 @@
+[Icon Theme]
+Name=pe_blue
+Comment=Icons by pexner (blue)
+Inherits=multimc
+Directories=scalable
+
+[scalable]
+Size=48
+Type=Scalable
+MinSize=16
+MaxSize=256
diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc
new file mode 100644
index 00000000..98445d88
--- /dev/null
+++ b/launcher/resources/pe_blue/pe_blue.qrc
@@ -0,0 +1,38 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/icons/pe_blue">
+ <file>index.theme</file>
+ <file>scalable/about.svg</file>
+ <file>scalable/accounts.svg</file>
+ <file>scalable/bug.svg</file>
+ <file>scalable/centralmods.svg</file>
+ <file>scalable/checkupdate.svg</file>
+ <file>scalable/copy.svg</file>
+ <file>scalable/coremods.svg</file>
+ <file>scalable/externaltools.svg</file>
+ <file>scalable/help.svg</file>
+ <file>scalable/instance-settings.svg</file>
+ <file>scalable/jarmods.svg</file>
+ <file>scalable/java.svg</file>
+ <file>scalable/language.svg</file>
+ <file>scalable/loadermods.svg</file>
+ <file>scalable/log.svg</file>
+ <file>scalable/minecraft.svg</file>
+ <file>scalable/multimc.svg</file>
+ <file>scalable/new.svg</file>
+ <file>scalable/news.svg</file>
+ <file>scalable/notes.svg</file>
+ <file>scalable/patreon.svg</file>
+ <file>scalable/proxy.svg</file>
+ <file>scalable/quickmods.svg</file>
+ <file>scalable/refresh.svg</file>
+ <file>scalable/resourcepacks.svg</file>
+ <file>scalable/screenshots.svg</file>
+ <file>scalable/settings.svg</file>
+ <file>scalable/status-bad.svg</file>
+ <file>scalable/status-good.svg</file>
+ <file>scalable/status-yellow.svg</file>
+ <file>scalable/viewfolder.svg</file>
+ <file>scalable/worlds.svg</file>
+ </qresource>
+</RCC>
diff --git a/launcher/resources/pe_blue/scalable/about.svg b/launcher/resources/pe_blue/scalable/about.svg
new file mode 100644
index 00000000..56e7fc9b
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/about.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <circle fill="#DAEEFF" cx="16" cy="16" r="12"/>
+ <g>
+ <polygon fill="#666666" points="17.8,21 17.8,11.9 11.9,11.9 11.9,14.2 14.2,14.2 14.2,21 11.9,21 11.9,23.3 20.1,23.3 20.1,21
+ "/>
+ <circle fill="#666666" cx="16" cy="8.3" r="1.8"/>
+ </g>
+ <path fill="#3366CC" d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16C32,24.8,24.8,32,16,32L16,32z M16,4
+ C9.4,4,4,9.4,4,16s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/accounts.svg b/launcher/resources/pe_blue/scalable/accounts.svg
new file mode 100644
index 00000000..77e3f45a
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/accounts.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#999999" d="M25.8,17.5c1.4-1.4,2.2-3.4,2.2-5.5V8c0-4.4-3.6-8-8-8
+ c-0.6,0-1.2,0.1-1.8,0.2c1.3,1.1,2.4,2.4,3,4C22.8,4.7,24,6.2,24,8v4c0,1.8-1.2,3.3-2.8,3.8c-0.2,0.4-0.4,0.8-0.6,1.2
+ c1.5,1.2,2.8,2.6,3.7,4.2c1.8,1.1,3.1,2.7,3.6,4.7c0,1.1-0.9,2-2,2h-0.3c-0.4,1.6-1.3,3-2.5,4H26c3.3,0,6-2.7,6-6
+ C31.4,22.3,28.9,19.2,25.8,17.5z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M18,32H6c-3.3,0-6-2.7-6-6l0,0c0.6-3.7,3.1-6.8,6.2-8.5
+ C4.9,16.1,4,14.1,4,12V8c0-4.4,3.6-8,8-8s8,3.6,8,8v4c0,2.1-0.9,4.1-2.2,5.5c3.1,1.7,5.6,4.8,6.2,8.5l0,0C24,29.3,21.3,32,18,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M16,12V8c0-2.2-1.8-4-4-4S8,5.8,8,8v4c0,2.2,1.8,4,4,4
+ S16,14.2,16,12z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M20,26c-0.9-3.4-4.3-6-8-6l0,0l0,0c-3.7,0-7.1,2.6-8,6l0,0
+ c0,1.1,0.9,2,2,2h12C19.1,28,20,27.1,20,26L20,26z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/bug.svg b/launcher/resources/pe_blue/scalable/bug.svg
new file mode 100644
index 00000000..75a19e29
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/bug.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect x="0" fill="none" width="32" height="32"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M22.7,5.3c-0.3-1.2-0.9-2.3-1.8-3.2c-2.7-2.8-7.2-2.8-9.9,0
+ c-0.9,0.9-1.5,2-1.8,3.2C13.5,3.3,18.5,3.3,22.7,5.3z"/>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#6699CC" d="M30,17.9h-4.1c0-0.7,0.1-1.3,0.1-2c0-0.7,0-1.4-0.1-2
+ c0,0,0,0,0.1,0v0h2c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-2v0c-0.3,0-0.6,0.1-0.8,0.1c-1.2-3.1-3.9-4.1-9.2-4.1s-7.9,1-9.2,4.1
+ C6.6,9.9,6.3,9.8,6,9.8v0H4c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h2v0c0,0,0,0,0.1,0c0,0.6-0.1,1.3-0.1,2c0,0.7,0,1.3,0.1,2H2
+ c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h4.5c0.3,1.4,0.7,2.8,1.2,4H6c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h2v0c0.7,0,1.4-0.1,2.1-0.3
+ c1.5,1.4,3.4,2.3,5.9,2.3s4.4-0.9,5.9-2.3c0.7,0.2,1.4,0.3,2.1,0.3v0h2c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-1.7
+ c0.5-1.2,0.9-2.6,1.2-4H30c1.1,0,2-0.9,2-2C32,18.8,31.1,17.9,30,17.9z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/centralmods.svg b/launcher/resources/pe_blue/scalable/centralmods.svg
new file mode 100644
index 00000000..cda39b1f
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/centralmods.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#DAEEFF" d="M28,10c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z"
+ />
+<rect fill="none" width="32" height="32"/>
+<path fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h8c2.6,0,4.8,1.7,5.6,4H26c3.3,0,6,2.7,6,6v16
+ C32,29.3,29.3,32,26,32z M28,10c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z
+ "/>
+<path fill="#C1272D" d="M16,10.2l2.2,4.5l5,0.7L19.6,19l0.8,4.9L16,21.6l-4.4,2.3l0.8-4.9l-3.6-3.5l5-0.7L16,10.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/checkupdate.svg b/launcher/resources/pe_blue/scalable/checkupdate.svg
new file mode 100644
index 00000000..a7d9ee81
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/checkupdate.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#6699CC" d="M26,24h-0.4c0-0.1-0.1-0.2-0.1-0.3c-0.7-1.6-1.5-2.8-3.5-3.4V18c0-3.3-2.7-6-6-6c-3.3,0-6,2.7-6,6v2.3
+ c-2,0.6-2.9,1.8-3.5,3.4c0,0,0,0.1,0,0.1C2.8,23.1,0,19.9,0,16v-2c0-4.4,3.6-8,8-8c0.8,0,1.5,0.1,2.2,0.3C10.9,2.7,14.2,0,18,0
+ c4.4,0,8,3.6,8,8c0,0.7-0.1,1.4-0.3,2c0.1,0,0.2,0,0.3,0c3.3,0,6,2.7,6,6v2C32,21.3,29.3,24,26,24z"/>
+<path fill="#39B54A" d="M12,24h2v-6c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2v6h2c0.8,0,1.5,0.5,1.8,1.2c0.1,0.2,0.1,0.4,0.1,0.6
+ c0.1,0.6-0.1,1.2-0.6,1.6l-4,4C17,31.8,16.5,32,16,32c-0.5,0-1-0.2-1.4-0.6l-4-4c-0.4-0.4-0.6-1-0.6-1.6c0-0.2,0.1-0.4,0.1-0.6
+ C10.5,24.5,11.2,24,12,24z"/>
+<rect fill="none" width="32" height="32"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/copy.svg b/launcher/resources/pe_blue/scalable/copy.svg
new file mode 100644
index 00000000..7ce014ed
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/copy.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#DAEEFF" d="M4,26c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V14H4V26z"/>
+<path fill="#6699CC" d="M28,25.6V10c0-3.3-2.7-6-6-6H6.4c0.8-2.3,3-4,5.6-4h14c3.3,0,6,2.7,6,6v14C32,22.6,30.3,24.8,28,25.6z"/>
+<path fill="#3366CC" d="M26,12v14c0,3.3-2.7,6-6,6H6c-3.3,0-6-2.7-6-6V12c0-3.3,2.7-6,6-6h14C23.3,6,26,8.7,26,12z M4,26
+ c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V14H4V26z M6,8c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S7.1,8,6,8z"/>
+<rect fill="none" width="32" height="32"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/coremods.svg b/launcher/resources/pe_blue/scalable/coremods.svg
new file mode 100644
index 00000000..4cc030d0
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/coremods.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" cx="16" cy="16" r="12"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16
+ C32,24.8,24.8,32,16,32z M16,4C9.4,4,4,9.4,4,16c0,6.6,5.4,12,12,12s12-5.4,12-12C28,9.4,22.6,4,16,4z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#C1272D" d="M16,6l2.9,6l6.6,1l-4.8,4.6l1.1,6.5L16,21l-5.9,3.1l1.1-6.5
+ l-4.8-4.6l6.6-1L16,6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/externaltools.svg b/launcher/resources/pe_blue/scalable/externaltools.svg
new file mode 100644
index 00000000..45b73496
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/externaltools.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M6,32h20c3.3,0,6-2.7,6-6V6c0-3.3-2.7-6-6-6h-9.1
+ C17.6,1.2,18,2.6,18,4h8c1.1,0,2,0.9,2,2v20c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2v-4.5l-4-3V26C0,29.3,2.7,32,6,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#39B54A" d="M8.8,17.6C9.2,17.9,9.6,18,10,18c0.3,0,0.6-0.1,0.9-0.2
+ c0.7-0.3,1.1-1,1.1-1.8v-1.9V14c6.3,0,11.7,4.2,13.4,10c0.4-1.3,0.6-2.6,0.6-4c0-7.7-6.3-14-14-14V4c0-0.8-0.4-1.5-1.1-1.8
+ C10.6,2.1,10.3,2,10,2C9.6,2,9.2,2.1,8.8,2.4l-8,6C0.3,8.8,0,9.4,0,10c0,0.6,0.3,1.2,0.8,1.6L8.8,17.6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/help.svg b/launcher/resources/pe_blue/scalable/help.svg
new file mode 100644
index 00000000..e98540cb
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/help.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata19"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs17" /><g
+ id="g12"><circle
+ id="circle2"
+ r="12"
+ cy="16"
+ cx="16"
+ fill="#DAEEFF" /><path
+ id="path10"
+ d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16C32,24.8,24.8,32,16,32L16,32z M16,4 C9.4,4,4,9.4,4,16s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z"
+ fill="#3366CC" /></g><g
+ transform="translate(41.863574,-2.0092638)"
+ id="g861"><g
+ aria-label="?"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:27.34714317px;line-height:85.45981598px;font-family:'Nimbus Sans L';-inkscape-font-specification:'Nimbus Sans L';letter-spacing:0px;word-spacing:0px;fill:#666666;fill-opacity:1;stroke:none;stroke-width:3.4183929px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text832"><path
+ d="m -20.968435,13.920532 c 0,0.6381 -0.07293,1.239737 -0.218777,1.804911 -0.145852,0.565174 -0.401092,1.066538 -0.76572,1.504093 -0.401092,0.492249 -0.774836,0.902456 -1.121233,1.230621 -0.346397,0.328166 -0.656332,0.6381 -0.929803,0.929803 -0.25524,0.273471 -0.464902,0.556059 -0.628985,0.847762 -0.145851,0.291703 -0.218777,0.656331 -0.218777,1.093885 v 0.38286 h -3.144921 V 20.72997 c 0,-0.601637 0.136736,-1.221505 0.410207,-1.859605 0.273471,-0.656332 0.756604,-1.285316 1.449399,-1.886953 0.346397,-0.309935 0.638099,-0.583406 0.875108,-0.820415 0.237009,-0.25524 0.428439,-0.492248 0.57429,-0.711025 0.145851,-0.237009 0.246124,-0.483133 0.300819,-0.738373 0.05469,-0.273471 0.08204,-0.57429 0.08204,-0.902456 0,-0.528711 -0.12762,-0.984497 -0.38286,-1.367357 -0.237009,-0.401091 -0.656331,-0.601637 -1.257968,-0.601637 -1.057423,0 -1.640829,0.692794 -1.750218,2.078383 h -3.06288 c 0.01823,-0.747489 0.145853,-1.431167 0.38286,-2.051036 0.237009,-0.6381 0.565175,-1.185043 0.984498,-1.640829 0.419323,-0.474017 0.920687,-0.8386455 1.504092,-1.0938855 0.601637,-0.25524 1.2762,-0.38286 2.023689,-0.38286 0.838646,0 1.567903,0.1367357 2.187771,0.4102072 0.619869,0.25524 1.130349,0.6198684 1.53144,1.0938853 0.401092,0.455786 0.692795,1.002729 0.875109,1.640829 0.200546,0.619869 0.300819,1.294432 0.30082,2.023689 z"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Monofonto;-inkscape-font-specification:Monofonto;fill:#666666;fill-opacity:1;stroke-width:3.4183929px"
+ id="path855" /></g><circle
+ style="fill:#666666"
+ cx="-26.38899"
+ cy="25.466606"
+ r="1.8"
+ id="circle6-6" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_blue/scalable/instance-settings.svg b/launcher/resources/pe_blue/scalable/instance-settings.svg
new file mode 100644
index 00000000..43f0b2f2
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/instance-settings.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<rect x="16" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<rect x="8" y="18" fill-rule="evenodd" clip-rule="evenodd" width="6" height="6"/>
+<rect x="10" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" width="2" height="2"/>
+<rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" width="6" height="6"/>
+<rect x="10" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" width="2" height="2"/>
+<rect x="16" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/jarmods.svg b/launcher/resources/pe_blue/scalable/jarmods.svg
new file mode 100644
index 00000000..bb75f4b1
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/jarmods.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M12.2,21.2c0,0-1.3,0.7,0.9,1
+ c2.7,0.3,4,0.3,7-0.3c0,0,0.8,0.5,1.9,0.9C15.3,25.7,7,22.7,12.2,21.2"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M11.4,17.5c0,0-1.4,1.1,0.8,1.3
+ c2.9,0.3,5.1,0.3,9-0.4c0,0,0.5,0.5,1.4,0.8C14.5,21.6,5.7,19.4,11.4,17.5"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M26.9,23.9c0,0,1,0.8-1,1.4
+ c-3.8,1.2-15.8,1.5-19.2,0c-1.2-0.5,1.1-1.2,1.8-1.4c0.7-0.2,1.2-0.1,1.2-0.1c-1.3-0.9-8.6,1.8-3.7,2.6
+ C19.3,28.7,30.4,25.5,26.9,23.9"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M12.8,13.7c0,0-6.1,1.5-2.2,2
+ c1.7,0.2,5,0.2,8.1-0.1c2.5-0.2,5.1-0.7,5.1-0.7s-0.9,0.4-1.5,0.8C16,17.4,4,16.6,7.5,15C10.4,13.6,12.8,13.7,12.8,13.7"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M23.8,19.9c6.3-3.3,3.4-6.4,1.4-6
+ c-0.5,0.1-0.7,0.2-0.7,0.2s0.2-0.3,0.5-0.4c4-1.4,7.1,4.2-1.3,6.4C23.7,20,23.7,19.9,23.8,19.9"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M13.4,28.7c6.1,0.4,15.4-0.2,15.6-3.1
+ c0,0-0.4,1.1-5,2c-5.2,1-11.6,0.9-15.3,0.2C8.7,27.9,9.4,28.5,13.4,28.7"/>
+<path fill="#F15A24" d="M16.5,0.6l2.2,4.5l5,0.7l-3.6,3.5l0.8,4.9l-4.4-2.3l-4.4,2.3l0.8-4.9L9.4,5.8l5-0.7L16.5,0.6z"/>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/java.svg b/launcher/resources/pe_blue/scalable/java.svg
new file mode 100644
index 00000000..5e369203
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/java.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path id="XMLID_5_" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z"
+ />
+</g>
+<path fill="none" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z"/>
+<g>
+ <path fill="#F15A24" d="M16.7,15.6l1-0.8c0.1-0.1,1.9-1.6,0.4-3.4c-1.3-1.6-1.8-3.1-1.4-4.5c1.1-3.4,7.2-4.9,7.4-5l1-0.2l-0.9,0.5
+ C20.7,4.3,19,5.8,18.8,7c-0.2,0.9,0.4,1.7,1.1,2.7l0.2,0.3c0.5,0.7,0.6,1.4,0.5,2.1C20.2,13.8,18,15,17.9,15L16.7,15.6z"/>
+</g>
+<g>
+ <path fill="#6699CC" d="M15.9,24.2c-2.9,0-5.3-0.8-5.5-1.8c-0.1-0.4,0.2-0.9,1.7-1.4l1.3-0.4l-1.2,0.7c-0.1,0.1-0.3,0.2-0.3,0.3
+ c0,0,0.1,0.3,1.2,0.4c1.2,0.1,2.1,0.2,2.8,0.2c1.2,0,2.3-0.1,4.1-0.5l0.1,0l0,0c0,0,0.8,0.5,1.9,0.9l0.4,0.1l-0.3,0.1
+ C19.7,24,17.4,24.2,15.9,24.2z"/>
+</g>
+<g>
+ <path fill="#6699CC" d="M15.3,20.4c-3.3,0-5.7-0.6-5.8-1.5c-0.1-0.5,0.6-1,1.9-1.5l0.8-0.3l-0.7,0.5c-0.1,0.1-0.4,0.4-0.4,0.6
+ c0,0.1,0.2,0.3,1.1,0.4c1.3,0.1,2.3,0.2,3.2,0.2c1.7,0,3.4-0.2,5.7-0.6l0.1,0l0.1,0.1c0,0,0.5,0.5,1.3,0.8l0.5,0.2l-0.5,0.1
+ C20.3,20,17.7,20.4,15.3,20.4z"/>
+</g>
+<g>
+ <path fill="#6699CC" d="M15.3,27.4C15.3,27.4,15.3,27.4,15.3,27.4c-3.1,0-6.3-0.3-9.4-0.8c-1.2-0.2-1.8-0.5-1.8-1
+ c0.1-1,3.1-2,4.7-2c0.4,0,0.7,0.1,0.9,0.2l0.6,0.4l-0.7-0.1c0,0-0.4-0.1-1.1,0.1c-1.6,0.5-2,0.8-2,0.9c0,0,0,0.1,0.2,0.2
+ c1.4,0.6,4.6,1,8.7,1c4.3,0,8.4-0.4,10.3-1c0.9-0.3,1.1-0.6,1.1-0.7c0-0.2-0.2-0.4-0.2-0.4l-0.9-0.8l1.1,0.5
+ c0.7,0.3,0.8,0.7,0.7,0.9C27.4,26,22.4,27.4,15.3,27.4z"/>
+</g>
+<g>
+ <path fill="#6699CC" d="M13.6,16.7c-3,0-6.7-0.3-6.9-1.2c-0.1-0.4,0.5-0.7,0.7-0.8c2.5-1.1,4.6-1.2,5.2-1.2c0.1,0,0.1,0,0.1,0l1,0
+ l-1,0.3c-1.8,0.4-3.4,1.1-3.4,1.4c0,0,0,0.2,1.2,0.4c0.8,0.1,1.9,0.1,3.1,0.1c1.6,0,3.4-0.1,5-0.2c2.5-0.2,5.1-0.7,5.1-0.7l1.3-0.2
+ L23.9,15c0,0-0.9,0.4-1.5,0.8l0,0C20.1,16.4,16.9,16.7,13.6,16.7z"/>
+</g>
+<g>
+ <path fill="#6699CC" d="M23.5,20.3v-0.2c0-0.1,0-0.3,0.2-0.3c3.8-2,3.8-3.7,3.7-4.3c-0.2-0.9-1.1-1.4-1.9-1.4c-0.1,0-0.2,0-0.3,0
+ c-0.5,0.1-0.7,0.2-0.7,0.2l-0.5,0.2l0.3-0.5c0,0,0.2-0.3,0.6-0.5c0.4-0.2,0.9-0.2,1.3-0.2c1.5,0,2.7,0.9,2.9,2.2
+ c0.2,1.5-1.1,3.5-5.4,4.7L23.5,20.3z"/>
+</g>
+<g>
+ <path fill="#6699CC" d="M16.9,29c-1.2,0-2.4,0-3.5-0.1c-3.8-0.2-4.8-0.8-4.8-1v-0.2l0.2,0c1.8,0.3,4.2,0.5,6.8,0.5
+ c3,0,5.9-0.3,8.4-0.7c4.4-0.9,4.9-1.9,4.9-1.9l0.3-0.8l-0.1,0.9c-0.1,1.2-1.5,2.1-4.1,2.6C22.9,28.7,20.1,29,16.9,29z"/>
+</g>
+<path fill="#F15A24" d="M16.8,4.2c1.8-1.4,3-2.8,3.7-4.2h-1.7c-0.8,1-1.9,1.8-2.9,2.7c-1.2,1-2.5,2-3.3,3.2c-1.6,2.6,1,5.2,4,8
+ l0.5,0.5l-0.3-0.6c-0.3-0.7-0.7-1.3-1.2-1.9C14.2,9.6,12.7,7.4,16.8,4.2z"/>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/language.svg b/launcher/resources/pe_blue/scalable/language.svg
new file mode 100644
index 00000000..92868516
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/language.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ id="Calque_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 32 32"
+ enable-background="new 0 0 32 32"
+ xml:space="preserve"><metadata
+ id="metadata45"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs43" /><path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ fill="#3366CC"
+ d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6 v20C32,29.3,29.3,32,26,32z"
+ id="path2" /><path
+ fill="#DAEEFF"
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
+ id="path4" /><g
+ id="g10" /><g
+ id="g12" /><g
+ id="g14" /><g
+ id="g16" /><g
+ id="g18" /><g
+ id="g20" /><g
+ id="g22" /><g
+ id="g24" /><g
+ id="g26" /><g
+ id="g28" /><g
+ id="g30" /><g
+ id="g32" /><g
+ id="g34" /><g
+ id="g36" /><g
+ id="g38" /><path
+ d="m 9.1897907,10.114301 c -0.4838401,0 -0.9011011,0.341924 -0.9962772,0.815883 L 6.1640606,21.085639 c -0.1100564,0.55031 0.2460855,1.083011 0.7953819,1.193069 0.5562003,0.113982 1.0840209,-0.246085 1.1930722,-0.79538 l 0.6518824,-3.247122 h 2.8699319 l 0.647785,3.247122 c 0.110996,0.557485 0.658771,0.906193 1.197171,0.79538 0.549363,-0.110056 0.905437,-0.642759 0.795381,-1.193069 L 12.285213,10.930184 c -0.09517,-0.473959 -0.512431,-0.815883 -0.996274,-0.815883 z m 0.6149825,2.287746 h 0.4345928 l 0.811779,4.063002 H 8.9929943 Z M 20.710512,9.7002137 c -0.561209,0 -1.016777,0.4578063 -1.016777,1.0208743 v 1.016774 h -3.046227 c -0.561205,0 -1.012673,0.457809 -1.012673,1.020876 0,0.563065 0.451466,1.016776 1.012673,1.016776 h 0.274696 c 0.577658,1.85973 1.425829,3.313572 2.37794,4.460692 -0.745862,0.684158 -1.478369,1.245052 -2.271343,1.873654 -0.437271,0.35118 -0.506831,0.995228 -0.155795,1.434967 0.349411,0.439057 0.990377,0.507857 1.426766,0.155794 0.861615,-0.682321 1.601995,-1.252055 2.410742,-1.996652 0.808748,0.744597 1.618828,1.314331 2.480441,1.996652 0.436393,0.352059 1.077353,0.283261 1.426766,-0.155794 0.351038,-0.439739 0.277375,-1.083787 -0.159884,-1.434967 -0.792979,-0.628602 -1.591082,-1.189496 -2.336946,-1.873654 0.952113,-1.147188 1.869915,-2.601029 2.447642,-4.460692 h 0.270592 c 0.561209,0 1.016774,-0.453711 1.016774,-1.016776 0,-0.563067 -0.455565,-1.020876 -1.016774,-1.020876 h -3.111838 v -1.016774 c 0,-0.563068 -0.455567,-1.0208743 -1.016775,-1.0208743 z m -1.668658,4.0753003 h 3.402916 c -0.438423,1.181552 -1.08891,2.13806 -1.734258,2.951929 -0.645345,-0.813869 -1.230238,-1.770309 -1.668658,-2.951929 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#c1272d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.29744864;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect977" /></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_blue/scalable/loadermods.svg b/launcher/resources/pe_blue/scalable/loadermods.svg
new file mode 100644
index 00000000..a54dc211
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/loadermods.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#C1272D" d="M16,6.4l2.9,6l6.6,1l-4.8,4.6l1.1,6.5L16,21.4l-5.9,3.1l1.1-6.5
+ l-4.8-4.6l6.6-1L16,6.4z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/log.svg b/launcher/resources/pe_blue/scalable/log.svg
new file mode 100644
index 00000000..89d373f4
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/log.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M30,28c-3.1,0.1-8.9,0.7-12.1,2.3C17.8,31.3,17,32,16,32
+ c-1,0-1.8-0.7-1.9-1.7c-3.2-1.6-9-2.2-12.1-2.3c-1.1,0-2-0.9-2-2V2c0-1.1,0.9-2,2-2c0.1,0,0.1,0,0.2,0C16,0.4,16,4,16,4
+ s0-3.6,13.8-4c0.1,0,0.1,0,0.2,0c1.1,0,2,0.9,2,2v24C32,27.1,31.1,28,30,28z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M14,6.3C11.4,5,7.2,4.4,4,4.2v19.9c5.4,0.3,8.4,1.1,10,1.9V6.3z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M28,4.2C24.8,4.4,20.6,5,18,6.3V26c1.6-0.8,4.6-1.6,10-1.9V4.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/minecraft.svg b/launcher/resources/pe_blue/scalable/minecraft.svg
new file mode 100644
index 00000000..2fe6a028
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/minecraft.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#754C24" points="16,14.2 30,7.1 30,24.9 16,32 "/>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#8C6239" points="16,14.2 2,7.1 2,24.9 16,32 "/>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#39B54A" points="2,7.1 16,14.2 30,7.1 16,0 "/>
+ </g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/multimc.svg b/launcher/resources/pe_blue/scalable/multimc.svg
new file mode 100644
index 00000000..820c0b53
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/multimc.svg
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="#754C24" width="32" height="32"/>
+<polygon fill="#39B54A" points="0,5 1,5 1,4 4,4 4,5 8,5 8,8 9,8 9,2 12,2 12,5 13,5 13,4 16,4 16,8 17,8 17,6 18,6 18,5 20,5 20,8
+ 21,8 21,6 22,6 22,5 24,5 24,4 26,4 26,5 29,5 29,4 32,4 32,0 0,0 "/>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-963.8778" y1="47.5718" x2="-963.0319" y2="49.6501" gradientTransform="matrix(20.79 0 0 14.7315 20048.0879 -696.8257)">
+ <stop offset="0" style="stop-color:#000000;stop-opacity:0.8"/>
+ <stop offset="1" style="stop-color:#000000;stop-opacity:0.35"/>
+</linearGradient>
+<path fill="url(#SVGID_1_)" d="M18.5,21.5c-0.5-0.4-1-1-1.8-1.9c-1,1.2-1.9,2.1-2.8,2.7c-1.1,0.7-2.5,1-4.1,1c-2,0-3.6-0.5-4.9-1.6
+ c-1.4-1.1-2.1-2.6-2.1-4.4c0-1.7,0.7-3.2,2.1-4.4c1.2-1,2.9-1.6,5-1.6c1.1,0,2,0.2,2.8,0.5c0.9,0.3,1.7,0.8,2.3,1.4
+ c0.6,0.5,1.2,1.2,1.8,1.9c1-1.2,1.9-2.1,2.8-2.7c1.1-0.7,2.5-1,4.1-1c2,0,3.6,0.5,4.9,1.6c1.4,1.1,2.1,2.6,2.1,4.4
+ c0,1.7-0.7,3.2-2.1,4.4c-1.2,1-2.9,1.6-5,1.6c-1.1,0-2-0.2-2.8-0.5S19.2,22.1,18.5,21.5 M9.6,21.3c2.4,0,4.3-1.2,5.8-3.7
+ c-1.9-2.7-3.8-4.1-5.8-4.1c-1.5,0-2.6,0.4-3.3,1.1c-0.8,0.8-1.2,1.7-1.2,2.8c0,1.2,0.4,2.1,1.2,2.8C7.1,20.9,8.2,21.3,9.6,21.3
+ M23.9,13.5c-2.2,0-4.1,1.2-5.8,3.7c1.8,2.7,3.8,4.1,5.8,4.1c1.5,0,2.6-0.4,3.3-1.1c0.8-0.8,1.2-1.7,1.2-2.8c0-1.2-0.4-2.1-1.2-2.8
+ C26.3,13.8,25.2,13.5,23.9,13.5"/>
+<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-964.0289" y1="48.0195" x2="-962.7877" y2="48.9627" gradientTransform="matrix(20.79 0 0 14.7315 20047.7695 -696.964)">
+ <stop offset="0" style="stop-color:#75B54B"/>
+ <stop offset="1" style="stop-color:#75B54B;stop-opacity:0.6"/>
+</linearGradient>
+<path fill="url(#SVGID_2_)" d="M17.6,20.1c-0.5-0.4-1-1-1.8-1.9c-1,1.2-1.9,2.1-2.8,2.7c-1.1,0.7-2.5,1-4.1,1c-2,0-3.6-0.5-4.9-1.6
+ c-1.4-1.1-2.1-2.6-2.1-4.4c0-1.7,0.7-3.2,2.1-4.4c1.2-1,2.9-1.6,5-1.6c1.1,0,2,0.2,2.8,0.5c0.9,0.3,1.7,0.8,2.3,1.4
+ c0.6,0.5,1.2,1.2,1.8,1.9c1-1.2,1.9-2.1,2.8-2.7c1.1-0.7,2.5-1,4.1-1c2,0,3.6,0.5,4.9,1.6c1.4,1.1,2.1,2.6,2.1,4.4
+ c0,1.7-0.7,3.2-2.1,4.4c-1.2,1-2.9,1.6-5,1.6c-1.1,0-2-0.2-2.8-0.5C19.2,21.2,18.4,20.8,17.6,20.1 M8.7,19.9c2.4,0,4.3-1.2,5.8-3.7
+ c-1.9-2.7-3.8-4.1-5.8-4.1c-1.5,0-2.6,0.4-3.3,1.1C4.6,14,4.2,14.9,4.2,16c0,1.2,0.4,2.1,1.2,2.8C6.2,19.5,7.3,19.9,8.7,19.9
+ M23,12.1c-2.2,0-4.1,1.2-5.8,3.7c1.8,2.7,3.8,4.1,5.8,4.1c1.5,0,2.6-0.4,3.3-1.1c0.8-0.8,1.2-1.7,1.2-2.8c0-1.2-0.4-2.1-1.2-2.8
+ C25.5,12.5,24.4,12.1,23,12.1"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/new.svg b/launcher/resources/pe_blue/scalable/new.svg
new file mode 100644
index 00000000..dcc8579e
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/new.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<g>
+ <path fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z M6,2
+ C4.9,2,4,2.9,4,4s0.9,2,2,2s2-0.9,2-2S7.1,2,6,2z"/>
+ <path fill="#DAEEFF" d="M28,8H4v18c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V8z"/>
+ <path fill="#666666" d="M20,20h-2v2c0,1.1-0.9,2-2,2s-2-0.9-2-2v-2h-2c-1.1,0-2-0.9-2-2s0.9-2,2-2h2v-2c0-1.1,0.9-2,2-2s2,0.9,2,2
+ v2h2c1.1,0,2,0.9,2,2S21.1,20,20,20z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/news.svg b/launcher/resources/pe_blue/scalable/news.svg
new file mode 100644
index 00000000..3ca3be37
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/news.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<g>
+ <g>
+ <path fill="#3366CC" d="M26,0H6C2.6,0,0,2.6,0,6v14c0,3.4,2.6,6,6,6l4,6l4-6h12c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z M16,20H6
+ v-2h10V20z M26,16H6v-2h20V16z M26,12H6v-2h20V12z M26,8H6V6h20V8z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/notes.svg b/launcher/resources/pe_blue/scalable/notes.svg
new file mode 100644
index 00000000..d0991259
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/notes.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<g>
+ <rect x="8" y="16" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="12" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/patreon.svg b/launcher/resources/pe_blue/scalable/patreon.svg
new file mode 100644
index 00000000..644b9b41
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/patreon.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#F15A24" d="M16,0C7.2,0,0,7.2,0,16v16h15.3c0.2,0,0.4,0,0.7,0c8.8,0,16-7.2,16-16S24.8,0,16,0L16,0z"/>
+<path fill="#FFFFFF" d="M16,3.7C9.2,3.7,3.7,9.2,3.7,16v7.3v5V32h4.9V16c0-4.1,3.3-7.3,7.3-7.3s7.3,3.3,7.3,7.3
+ c0,4.1-3.3,7.3-7.3,7.3c-1.5,0-2.9-0.4-4-1.2v5.3c0.8,0.5,2.5,0.8,4.6,0.8C23.1,28,28.3,22.6,28.3,16C28.3,9.2,22.8,3.7,16,3.7
+ L16,3.7z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/proxy.svg b/launcher/resources/pe_blue/scalable/proxy.svg
new file mode 100644
index 00000000..8266f9b8
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/proxy.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M21.9,4H11.6c-0.8-2.3-3-4-5.6-4C2.7,0,0,2.7,0,6s2.7,6,6,6
+ c2.6,0,4.8-1.7,5.6-4h10.3c1.1,0,2,0.9,2,2v1.4h4V10C27.9,6.7,25.2,4,21.9,4z M6,8C4.9,8,4,7.1,4,6s0.9-2,2-2c1.1,0,2,0.9,2,2
+ S7.1,8,6,8z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M23.9,20.6V22c0,1.1-0.9,2-2,2H11.6c-0.8-2.3-3-4-5.6-4
+ c-3.3,0-6,2.7-6,6s2.7,6,6,6c2.6,0,4.8-1.7,5.6-4h10.3c3.3,0,6-2.7,6-6v-1.4H23.9z M6,28c-1.1,0-2-0.9-2-2s0.9-2,2-2
+ c1.1,0,2,0.9,2,2S7.1,28,6,28z"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#C1272D" d="M32,22H20V10h12V22z M28,14h-4v4h4V14z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/quickmods.svg b/launcher/resources/pe_blue/scalable/quickmods.svg
new file mode 100644
index 00000000..8b577376
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/quickmods.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#C1272D" d="M10.6,24.2c-0.3,0-0.6-0.1-0.8-0.2C9.3,23.7,9,23.2,9,22.6V9.8
+ c0-0.6,0.3-1.1,0.8-1.4c0.3-0.2,0.5-0.2,0.8-0.2c0.2,0,0.5,0.1,0.7,0.2l12.8,6.4c0.5,0.3,0.9,0.8,0.9,1.4c0,0.6-0.3,1.2-0.9,1.4
+ L11.3,24C11.1,24.1,10.8,24.2,10.6,24.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/refresh.svg b/launcher/resources/pe_blue/scalable/refresh.svg
new file mode 100644
index 00000000..a3d2281d
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/refresh.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#39B54A" d="M31.8,17.4c-0.4-0.9-1.2-1.4-2.1-1.4h-2.3c0-7.6-6.1-13.7-13.7-13.7C6.1,2.3,0,8.4,0,16
+ c0,7.6,6.1,13.7,13.7,13.7c2.5,0,4.8-0.7,6.8-1.8l-3.4-3.4c-1,0.4-2.2,0.7-3.4,0.7c-5,0-9.1-4.1-9.1-9.1s4.1-9.1,9.1-9.1
+ c5.1,0,9.1,4.1,9.1,9.1h-2.3c-0.9,0-1.8,0.6-2.1,1.4c-0.4,0.9-0.2,1.8,0.5,2.5l4.6,4.6c0.4,0.4,1,0.7,1.6,0.7c0.6,0,1.2-0.2,1.6-0.7
+ l4.6-4.6C32,19.2,32.2,18.3,31.8,17.4z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/resourcepacks.svg b/launcher/resources/pe_blue/scalable/resourcepacks.svg
new file mode 100644
index 00000000..a17e7e82
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/resourcepacks.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M28,10c0-1.1-0.9-2-2-2H6c-1.1,0-2,0.9-2,2v16c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V10z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M26,4h-4V2c0-1.1-0.9-2-2-2h-8c-1.1,0-2,0.9-2,2v2H6
+ c-3.3,0-6,2.7-6,6v16c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V10C32,6.7,29.3,4,26,4z M12,3c0-0.6,0.4-1,1-1h6c0.6,0,1,0.4,1,1v1h-8V3z
+ M6,8h20c1.1,0,2,0.9,2,2v4H4v-4C4,8.9,4.9,8,6,8z M18,17c0,1.1-0.9,2-2,2s-2-0.9-2-2c0-0.4,0.1-0.7,0.3-1h3.4
+ C17.9,16.3,18,16.6,18,17z M26,28H6c-1.1,0-2-0.9-2-2V16h8.1c-0.1,0.3-0.1,0.7-0.1,1c0,2.2,1.8,4,4,4s4-1.8,4-4c0-0.3-0.1-0.7-0.1-1
+ H28v10C28,27.1,27.1,28,26,28z"/>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/screenshots.svg b/launcher/resources/pe_blue/scalable/screenshots.svg
new file mode 100644
index 00000000..1aa4e559
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/screenshots.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M24,26H8c-1.1,0-2-0.9-2-2v-0.7c0.8-1.7,1.5-3.8,2-4.8
+ c1.1-2.4,3.5,3.5,4.9,3.5c2.8,0,6.2-10.7,9.1-7.4c1.4,1.6,2.9,5.8,4,8.4V24C26,25.1,25.1,26,24,26z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F15A24" d="M9,6c1.7,0,3,1.3,3,3c0,1.7-1.3,3-3,3c-1.7,0-3-1.3-3-3
+ C6,7.3,7.3,6,9,6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/settings.svg b/launcher/resources/pe_blue/scalable/settings.svg
new file mode 100644
index 00000000..43f0b2f2
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/settings.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<rect x="16" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<rect x="8" y="18" fill-rule="evenodd" clip-rule="evenodd" width="6" height="6"/>
+<rect x="10" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" width="2" height="2"/>
+<rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" width="6" height="6"/>
+<rect x="10" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#DAEEFF" width="2" height="2"/>
+<rect x="16" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/status-bad.svg b/launcher/resources/pe_blue/scalable/status-bad.svg
new file mode 100644
index 00000000..4a48b5d8
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/status-bad.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#C1272D" d="M26,32H6c-3.4,0-6-2.6-6-6V6c0-3.4,2.6-6,6-6h20c3.4,0,6,2.6,6,6v20C32,29.4,29.4,32,26,32z"/>
+<path fill="#FFFFFF" d="M21.6,21.6c-0.8,0.8-2,0.8-2.8,0L16,18.8l-2.8,2.8c-0.8,0.8-2,0.8-2.8,0c-0.8-0.8-0.8-2,0-2.8l2.8-2.8
+ l-2.8-2.8c-0.8-0.8-0.8-2,0-2.8c0.8-0.8,2-0.8,2.8,0l2.8,2.8l2.8-2.8c0.8-0.8,2-0.8,2.8,0s0.8,2,0,2.8L18.8,16l2.8,2.8
+ C22.4,19.6,22.4,20.8,21.6,21.6z"/>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/status-good.svg b/launcher/resources/pe_blue/scalable/status-good.svg
new file mode 100644
index 00000000..4cfa56f0
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/status-good.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="none" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z M28,26c0,1.2-0.8,2-2,2
+ H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2
+ c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+</g>
+<path fill="#39B54A" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z"/>
+<path fill="#39B54A" d="M28,26c0,1.2-0.8,2-2,2H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z"/>
+<path fill="#FFFFFF" d="M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2c0.8,0.8,2,0.8,2.8,0l8.4-8.4
+ c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/status-yellow.svg b/launcher/resources/pe_blue/scalable/status-yellow.svg
new file mode 100644
index 00000000..0551fed2
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/status-yellow.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="#E3A333" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z M28,26
+ c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+</g>
+<path fill="#E3A333" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z"/>
+<path fill="#E3A333" d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+<g>
+ <path fill="#FFFFFF" d="M6,20c0-5.8,2.4-7.9,5.8-7.9c2,0,3.4,0.7,5.3,1.9c1.6,1,2.5,1.5,3.3,1.5c0.7,0,1.2-0.7,1.3-3.1H26
+ c0.2,6.1-2.3,8-5.5,8c-2,0-3.5-0.8-5.3-1.9c-1.7-1-2.6-1.6-3.3-1.6c-0.8,0-1.4,0.5-1.6,3.2H6z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/viewfolder.svg b/launcher/resources/pe_blue/scalable/viewfolder.svg
new file mode 100644
index 00000000..2634f8ff
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/viewfolder.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#DAEEFF" d="M28,10c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z"
+ />
+<rect fill="none" width="32" height="32"/>
+<path fill="#3366CC" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h8c2.6,0,4.8,1.7,5.6,4H26c3.3,0,6,2.7,6,6v16
+ C32,29.3,29.3,32,26,32z M28,10c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z
+ "/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_blue/scalable/worlds.svg b/launcher/resources/pe_blue/scalable/worlds.svg
new file mode 100644
index 00000000..1670c035
--- /dev/null
+++ b/launcher/resources/pe_blue/scalable/worlds.svg
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata45"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs43" /><path
+ id="path2"
+ d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6 v20C32,29.3,29.3,32,26,32z"
+ fill="#3366CC"
+ clip-rule="evenodd"
+ fill-rule="evenodd" /><path
+ fill="#DAEEFF"
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ id="path4"
+ d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z" /><g
+ id="g10" /><g
+ id="g12" /><g
+ id="g14" /><g
+ id="g16" /><g
+ id="g18" /><g
+ id="g20" /><g
+ id="g22" /><g
+ id="g24" /><g
+ id="g26" /><g
+ id="g28" /><g
+ id="g30" /><g
+ id="g32" /><g
+ id="g34" /><g
+ id="g36" /><g
+ id="g38" /><g
+ transform="rotate(29.970903,16,16)"
+ id="g881"><ellipse
+ ry="8.9473686"
+ rx="8.9473696"
+ cy="16"
+ cx="16"
+ id="path845"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><ellipse
+ ry="8.9473686"
+ rx="3.4915254"
+ cy="15.947369"
+ cx="16"
+ id="path845-3"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><path
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 8.1848294,12.430327 C 12.570183,14.790801 20.250652,13.785896 22.94338,12"
+ id="path870" /><path
+ id="path872"
+ d="M 8.2166335,20.492522 C 12.601987,18.132048 21.148735,17.776739 23.841463,19.562635"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_colored/index.theme b/launcher/resources/pe_colored/index.theme
new file mode 100644
index 00000000..b757bbd7
--- /dev/null
+++ b/launcher/resources/pe_colored/index.theme
@@ -0,0 +1,11 @@
+[Icon Theme]
+Name=pe_colored
+Comment=Icons by pexner (colored)
+Inherits=multimc
+Directories=scalable
+
+[scalable]
+Size=48
+Type=Scalable
+MinSize=16
+MaxSize=256
diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc
new file mode 100644
index 00000000..fbaaf9e4
--- /dev/null
+++ b/launcher/resources/pe_colored/pe_colored.qrc
@@ -0,0 +1,38 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/icons/pe_colored">
+ <file>index.theme</file>
+ <file>scalable/about.svg</file>
+ <file>scalable/accounts.svg</file>
+ <file>scalable/bug.svg</file>
+ <file>scalable/centralmods.svg</file>
+ <file>scalable/checkupdate.svg</file>
+ <file>scalable/copy.svg</file>
+ <file>scalable/coremods.svg</file>
+ <file>scalable/externaltools.svg</file>
+ <file>scalable/help.svg</file>
+ <file>scalable/instance-settings.svg</file>
+ <file>scalable/jarmods.svg</file>
+ <file>scalable/java.svg</file>
+ <file>scalable/language.svg</file>
+ <file>scalable/loadermods.svg</file>
+ <file>scalable/log.svg</file>
+ <file>scalable/minecraft.svg</file>
+ <file>scalable/multimc.svg</file>
+ <file>scalable/new.svg</file>
+ <file>scalable/news.svg</file>
+ <file>scalable/notes.svg</file>
+ <file>scalable/patreon.svg</file>
+ <file>scalable/proxy.svg</file>
+ <file>scalable/quickmods.svg</file>
+ <file>scalable/refresh.svg</file>
+ <file>scalable/resourcepacks.svg</file>
+ <file>scalable/screenshots.svg</file>
+ <file>scalable/settings.svg</file>
+ <file>scalable/status-bad.svg</file>
+ <file>scalable/status-good.svg</file>
+ <file>scalable/status-yellow.svg</file>
+ <file>scalable/viewfolder.svg</file>
+ <file>scalable/worlds.svg</file>
+ </qresource>
+</RCC>
diff --git a/launcher/resources/pe_colored/scalable/about.svg b/launcher/resources/pe_colored/scalable/about.svg
new file mode 100644
index 00000000..95e99689
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/about.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<circle fill="#F2F2F2" cx="16" cy="16" r="12"/>
+<g>
+ <polygon fill="#666666" points="17.8,21 17.8,11.9 11.9,11.9 11.9,14.2 14.2,14.2 14.2,21 11.9,21 11.9,23.3 20.1,23.3 20.1,21
+ "/>
+ <circle fill="#666666" cx="16" cy="8.3" r="1.8"/>
+</g>
+<g>
+ <path fill="none" d="M16,4c-4,0-7.6,2-9.7,5C4.8,11,4,13.4,4,16c0,6.6,5.4,12,12,12c6.6,0,12-5.4,12-12c0-2.6-0.8-5-2.3-7
+ C23.6,6,20,4,16,4z"/>
+ <path fill="#39B54A" d="M16,4c4,0,7.6,2,9.7,5h4.6c-2.6-5.3-8-9-14.4-9S4.2,3.7,1.6,9h4.6C8.4,6,12,4,16,4z"/>
+ <path fill="#8C6239" d="M32,16L32,16c0-2.6-0.6-4.9-1.6-7h-4.6c1.4,2,2.3,4.4,2.3,7c0,6.6-5.4,12-12,12C9.4,28,4,22.6,4,16
+ c0-2.6,0.8-5,2.3-7H1.6c-1,2.1-1.6,4.5-1.6,7c0,8.8,7.2,16,16,16h0C24.8,32,32,24.9,32,16z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/accounts.svg b/launcher/resources/pe_colored/scalable/accounts.svg
new file mode 100644
index 00000000..301eb368
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/accounts.svg
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#999999" d="M25.8,17.5c1.4-1.4,2.2-3.4,2.2-5.5V8c0-4.4-3.6-8-8-8
+ c-0.6,0-1.2,0.1-1.8,0.2c1.3,1.1,2.4,2.4,3,4C22.8,4.7,24,6.2,24,8v4c0,1.8-1.2,3.3-2.8,3.8c-0.2,0.4-0.4,0.8-0.6,1.2
+ c1.5,1.2,2.8,2.6,3.7,4.2c1.8,1.1,3.1,2.7,3.6,4.7c0,1.1-0.9,2-2,2h-0.3c-0.4,1.6-1.3,3-2.5,4H26c3.3,0,6-2.7,6-6
+ C31.4,22.3,28.9,19.3,25.8,17.5z"/>
+<g>
+ <polygon fill="none" points="12,0 12,0 12,0 "/>
+ <path fill="#39B54A" d="M20,8c0-4.4-3.6-8-8-8h0C7.6,0,4,3.6,4,8v1h16V8z"/>
+ <path fill="#8C6239" d="M6,32h12c3.3,0,6-2.7,6-6c-0.6-3.7-3.1-6.8-6.2-8.5c1.4-1.4,2.2-3.4,2.2-5.5V9H4v3c0,2.1,0.9,4.1,2.2,5.5
+ C3.1,19.2,0.6,22.3,0,26C0,29.3,2.7,32,6,32z"/>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M16,12V8c0-2.2-1.8-4-4-4S8,5.8,8,8v4c0,2.2,1.8,4,4,4
+ S16,14.2,16,12z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M20,26c-0.9-3.4-4.3-6-8-6l0,0l0,0c-3.7,0-7.1,2.6-8,6l0,0
+ c0,1.1,0.9,2,2,2h12C19.1,28,20,27.1,20,26L20,26z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/bug.svg b/launcher/resources/pe_colored/scalable/bug.svg
new file mode 100644
index 00000000..8c92df0a
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/bug.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect x="0" fill="none" width="32" height="32"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#39B54A" d="M22.7,5.3c-0.3-1.2-0.9-2.3-1.8-3.2c-2.7-2.8-7.2-2.8-9.9,0
+ c-0.9,0.9-1.5,2-1.8,3.2C13.5,3.3,18.5,3.3,22.7,5.3z"/>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#8C6239" d="M30,17.9h-4.1c0-0.7,0.1-1.3,0.1-2c0-0.7,0-1.4-0.1-2
+ c0,0,0,0,0.1,0v0h2c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-2v0c-0.3,0-0.6,0.1-0.8,0.1c-1.2-3.1-3.9-4.1-9.2-4.1s-7.9,1-9.2,4.1
+ C6.6,9.9,6.3,9.8,6,9.8v0H4c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h2v0c0,0,0,0,0.1,0c0,0.6-0.1,1.3-0.1,2c0,0.7,0,1.3,0.1,2H2
+ c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h4.5c0.3,1.4,0.7,2.8,1.2,4H6c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h2v0c0.7,0,1.4-0.1,2.1-0.3
+ c1.5,1.4,3.4,2.3,5.9,2.3s4.4-0.9,5.9-2.3c0.7,0.2,1.4,0.3,2.1,0.3v0h2c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-1.7
+ c0.5-1.2,0.9-2.6,1.2-4H30c1.1,0,2-0.9,2-2C32,18.8,31.1,17.9,30,17.9z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/centralmods.svg b/launcher/resources/pe_colored/scalable/centralmods.svg
new file mode 100644
index 00000000..57a97259
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/centralmods.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#F2F2F2" d="M28,10c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z"
+ />
+<rect x="0" fill="none" width="32" height="32"/>
+<g>
+ <path fill="none" d="M26,8H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v3v17c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10
+ c0-0.4-0.1-0.7-0.3-1C27.4,8.4,26.7,8,26,8z"/>
+ <path fill="#39B54A" d="M4,6c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2v2h10c0.7,0,1.4,0.4,1.7,1h4.2C31.4,6.2,29,4,26,4h-6.4
+ c-0.8-2.3-3-4-5.6-4H6C2.7,0,0,2.7,0,6v3h4V6z"/>
+ <path fill="#8C6239" d="M27.7,9c0.2,0.3,0.3,0.6,0.3,1v16c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V9H0v17c0,3.3,2.7,6,6,6h20
+ c3.3,0,6-2.7,6-6V10c0-0.3,0-0.7-0.1-1H27.7z"/>
+</g>
+<path fill="#C1272D" d="M16,10.2l2.2,4.5l5,0.7L19.6,19l0.8,4.9L16,21.6l-4.4,2.3l0.8-4.9l-3.6-3.5l5-0.7L16,10.2z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/checkupdate.svg b/launcher/resources/pe_colored/scalable/checkupdate.svg
new file mode 100644
index 00000000..0adc8eeb
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/checkupdate.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#6699CC" d="M26,24h-0.4c0-0.1-0.1-0.2-0.1-0.3c-0.7-1.6-1.5-2.8-3.5-3.4V18c0-3.3-2.7-6-6-6c-3.3,0-6,2.7-6,6v2.3
+ c-2,0.6-2.9,1.8-3.5,3.4c0,0,0,0.1,0,0.1C2.8,23.1,0,19.9,0,16v-2c0-4.4,3.6-8,8-8c0.8,0,1.5,0.1,2.2,0.3C10.9,2.7,14.2,0,18,0
+ c4.4,0,8,3.6,8,8c0,0.7-0.1,1.4-0.3,2c0.1,0,0.2,0,0.3,0c3.3,0,6,2.7,6,6v2C32,21.3,29.3,24,26,24z"/>
+<path fill="#39B54A" d="M12,24h2v-6c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2v6h2c0.8,0,1.5,0.5,1.8,1.2c0.1,0.2,0.1,0.4,0.1,0.6
+ c0.1,0.6-0.1,1.2-0.6,1.6l-4,4C17,31.8,16.5,32,16,32c-0.5,0-1-0.2-1.4-0.6l-4-4c-0.4-0.4-0.6-1-0.6-1.6c0-0.2,0.1-0.4,0.1-0.6
+ C10.5,24.5,11.2,24,12,24z"/>
+<rect fill="none" width="32" height="32"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/copy.svg b/launcher/resources/pe_colored/scalable/copy.svg
new file mode 100644
index 00000000..b9b0f1b1
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/copy.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#F2F2F2" d="M4,26c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V14H4V26z"/>
+<path fill="#39B54A" d="M28,25.6V10c0-3.3-2.7-6-6-6H6.4c0.8-2.3,3-4,5.6-4h14c3.3,0,6,2.7,6,6v14C32,22.6,30.3,24.8,28,25.6z"/>
+<g>
+ <circle fill="none" cx="6" cy="10" r="2"/>
+ <path fill="none" d="M6,28h14c1.1,0,2-0.9,2-2V15v-1H4v1v11C4,27.1,4.9,28,6,28z"/>
+ <path fill="#39B54A" d="M0,12v3h4v-1h18v1h4v-3c0-3.3-2.7-6-6-6H6C2.7,6,0,8.7,0,12z M8,10c0,1.1-0.9,2-2,2c-1.1,0-2-0.9-2-2
+ c0-1.1,0.9-2,2-2C7.1,8,8,8.9,8,10z"/>
+ <path fill="#8C6239" d="M22,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V15H0v11c0,3.3,2.7,6,6,6h14c3.3,0,6-2.7,6-6V15h-4V26z"/>
+</g>
+<rect fill="none" width="32" height="32"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/coremods.svg b/launcher/resources/pe_colored/scalable/coremods.svg
new file mode 100644
index 00000000..ca7a22f0
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/coremods.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" cx="16" cy="16" r="12"/>
+<g>
+ <path fill="none" d="M16,4c-4,0-7.6,2-9.7,5C4.8,11,4,13.4,4,16c0,6.6,5.4,12,12,12s12-5.4,12-12c0-2.6-0.8-5-2.3-7
+ C23.6,6,20,4,16,4z"/>
+ <path fill="#39B54A" d="M16,4c4,0,7.6,2,9.7,5h4.6c-2.6-5.3-8-9-14.4-9C9.7,0,4.2,3.7,1.6,9h4.6C8.4,6,12,4,16,4z"/>
+ <path fill="#8C6239" d="M25.7,9c1.4,2,2.3,4.4,2.3,7c0,6.6-5.4,12-12,12S4,22.6,4,16c0-2.6,0.8-5,2.3-7H1.6c-1,2.1-1.6,4.5-1.6,7
+ c0,8.8,7.2,16,16,16c8.8,0,16-7.2,16-16c0-2.5-0.6-4.9-1.6-7H25.7z"/>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#C1272D" d="M16,6l2.9,6l6.6,1l-4.8,4.6l1.1,6.5L16,21l-5.9,3.1l1.1-6.5
+ l-4.8-4.6l6.6-1L16,6z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/externaltools.svg b/launcher/resources/pe_colored/scalable/externaltools.svg
new file mode 100644
index 00000000..1469674f
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/externaltools.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="#39B54A" d="M26,0h-9.1C17.6,1.2,18,2.6,18,4h8c1.1,0,2,0.9,2,2v3h4V6C32,2.7,29.3,0,26,0z"/>
+ <path fill="#8C6239" d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2v-4.5l-4-3V26c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9h-4V26z"/>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#009245" d="M8.8,17.6C9.2,17.9,9.6,18,10,18c0.3,0,0.6-0.1,0.9-0.2
+ c0.7-0.3,1.1-1,1.1-1.8v-1.9V14c6.3,0,11.7,4.2,13.4,10c0.4-1.3,0.6-2.6,0.6-4c0-7.7-6.3-14-14-14V4c0-0.8-0.4-1.5-1.1-1.8
+ C10.6,2.1,10.3,2,10,2C9.6,2,9.2,2.1,8.8,2.4l-8,6C0.3,8.8,0,9.4,0,10c0,0.6,0.3,1.2,0.8,1.6L8.8,17.6z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/help.svg b/launcher/resources/pe_colored/scalable/help.svg
new file mode 100644
index 00000000..c1ee5258
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/help.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata23"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs21" /><circle
+ id="circle2"
+ r="12"
+ cy="16"
+ cx="16"
+ fill="#F2F2F2" /><g
+ id="g16"><path
+ id="path10"
+ d="M16,4c-4,0-7.6,2-9.7,5C4.8,11,4,13.4,4,16c0,6.6,5.4,12,12,12c6.6,0,12-5.4,12-12c0-2.6-0.8-5-2.3-7 C23.6,6,20,4,16,4z"
+ fill="none" /><path
+ id="path12"
+ d="M16,4c4,0,7.6,2,9.7,5h4.6c-2.6-5.3-8-9-14.4-9S4.2,3.7,1.6,9h4.6C8.4,6,12,4,16,4z"
+ fill="#39B54A" /><path
+ id="path14"
+ d="M32,16L32,16c0-2.6-0.6-4.9-1.6-7h-4.6c1.4,2,2.3,4.4,2.3,7c0,6.6-5.4,12-12,12C9.4,28,4,22.6,4,16 c0-2.6,0.8-5,2.3-7H1.6c-1,2.1-1.6,4.5-1.6,7c0,8.8,7.2,16,16,16h0C24.8,32,32,24.9,32,16z"
+ fill="#8C6239" /></g><g
+ transform="translate(41.863574,-2.0092638)"
+ id="g861"><g
+ aria-label="?"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:27.34714317px;line-height:85.45981598px;font-family:'Nimbus Sans L';-inkscape-font-specification:'Nimbus Sans L';letter-spacing:0px;word-spacing:0px;fill:#666666;fill-opacity:1;stroke:none;stroke-width:3.4183929px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text832"><path
+ d="m -20.968435,13.920532 c 0,0.6381 -0.07293,1.239737 -0.218777,1.804911 -0.145852,0.565174 -0.401092,1.066538 -0.76572,1.504093 -0.401092,0.492249 -0.774836,0.902456 -1.121233,1.230621 -0.346397,0.328166 -0.656332,0.6381 -0.929803,0.929803 -0.25524,0.273471 -0.464902,0.556059 -0.628985,0.847762 -0.145851,0.291703 -0.218777,0.656331 -0.218777,1.093885 v 0.38286 h -3.144921 V 20.72997 c 0,-0.601637 0.136736,-1.221505 0.410207,-1.859605 0.273471,-0.656332 0.756604,-1.285316 1.449399,-1.886953 0.346397,-0.309935 0.638099,-0.583406 0.875108,-0.820415 0.237009,-0.25524 0.428439,-0.492248 0.57429,-0.711025 0.145851,-0.237009 0.246124,-0.483133 0.300819,-0.738373 0.05469,-0.273471 0.08204,-0.57429 0.08204,-0.902456 0,-0.528711 -0.12762,-0.984497 -0.38286,-1.367357 -0.237009,-0.401091 -0.656331,-0.601637 -1.257968,-0.601637 -1.057423,0 -1.640829,0.692794 -1.750218,2.078383 h -3.06288 c 0.01823,-0.747489 0.145853,-1.431167 0.38286,-2.051036 0.237009,-0.6381 0.565175,-1.185043 0.984498,-1.640829 0.419323,-0.474017 0.920687,-0.8386455 1.504092,-1.0938855 0.601637,-0.25524 1.2762,-0.38286 2.023689,-0.38286 0.838646,0 1.567903,0.1367357 2.187771,0.4102072 0.619869,0.25524 1.130349,0.6198684 1.53144,1.0938853 0.401092,0.455786 0.692795,1.002729 0.875109,1.640829 0.200546,0.619869 0.300819,1.294432 0.30082,2.023689 z"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Monofonto;-inkscape-font-specification:Monofonto;fill:#666666;fill-opacity:1;stroke-width:3.4183929px"
+ id="path855" /></g><circle
+ style="fill:#666666"
+ cx="-26.38899"
+ cy="25.466606"
+ r="1.8"
+ id="circle6-6" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_colored/scalable/instance-settings.svg b/launcher/resources/pe_colored/scalable/instance-settings.svg
new file mode 100644
index 00000000..72032f8a
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/instance-settings.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect x="0" y="0" fill="none" width="32" height="32"/>
+<polygon fill="none" points="26,0 6,0 6,0 26,0 "/>
+<path fill="#39B54A" d="M26,0H6C2.7,0,0,2.7,0,6v3h32V6C32,2.7,29.3,0,26,0z"/>
+<path fill="#8C6239" d="M0,26c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9H0V26z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<rect x="16" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<rect x="8" y="18" fill-rule="evenodd" clip-rule="evenodd" width="6" height="6"/>
+<rect x="10" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" width="2" height="2"/>
+<rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" width="6" height="6"/>
+<rect x="10" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" width="2" height="2"/>
+<rect x="16" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/jarmods.svg b/launcher/resources/pe_colored/scalable/jarmods.svg
new file mode 100644
index 00000000..bb75f4b1
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/jarmods.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M12.2,21.2c0,0-1.3,0.7,0.9,1
+ c2.7,0.3,4,0.3,7-0.3c0,0,0.8,0.5,1.9,0.9C15.3,25.7,7,22.7,12.2,21.2"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M11.4,17.5c0,0-1.4,1.1,0.8,1.3
+ c2.9,0.3,5.1,0.3,9-0.4c0,0,0.5,0.5,1.4,0.8C14.5,21.6,5.7,19.4,11.4,17.5"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M26.9,23.9c0,0,1,0.8-1,1.4
+ c-3.8,1.2-15.8,1.5-19.2,0c-1.2-0.5,1.1-1.2,1.8-1.4c0.7-0.2,1.2-0.1,1.2-0.1c-1.3-0.9-8.6,1.8-3.7,2.6
+ C19.3,28.7,30.4,25.5,26.9,23.9"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M12.8,13.7c0,0-6.1,1.5-2.2,2
+ c1.7,0.2,5,0.2,8.1-0.1c2.5-0.2,5.1-0.7,5.1-0.7s-0.9,0.4-1.5,0.8C16,17.4,4,16.6,7.5,15C10.4,13.6,12.8,13.7,12.8,13.7"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M23.8,19.9c6.3-3.3,3.4-6.4,1.4-6
+ c-0.5,0.1-0.7,0.2-0.7,0.2s0.2-0.3,0.5-0.4c4-1.4,7.1,4.2-1.3,6.4C23.7,20,23.7,19.9,23.8,19.9"/>
+<path fill="#6699CC" stroke="#6699CC" stroke-width="0.3" stroke-miterlimit="10" d="M13.4,28.7c6.1,0.4,15.4-0.2,15.6-3.1
+ c0,0-0.4,1.1-5,2c-5.2,1-11.6,0.9-15.3,0.2C8.7,27.9,9.4,28.5,13.4,28.7"/>
+<path fill="#F15A24" d="M16.5,0.6l2.2,4.5l5,0.7l-3.6,3.5l0.8,4.9l-4.4-2.3l-4.4,2.3l0.8-4.9L9.4,5.8l5-0.7L16.5,0.6z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/java.svg b/launcher/resources/pe_colored/scalable/java.svg
new file mode 100644
index 00000000..32c0225b
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/java.svg
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path id="XMLID_1_" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z"
+ />
+</g>
+<path fill="none" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z"/>
+<g>
+ <path fill="#F15A24" d="M16.7,15.6l1-0.8c0.1-0.1,1.9-1.6,0.4-3.4c-1.3-1.6-1.8-3.1-1.4-4.5c1.1-3.4,7.2-4.9,7.4-5l1-0.2l-0.9,0.5
+ C20.7,4.3,19,5.8,18.8,7c-0.2,0.9,0.4,1.7,1.1,2.7l0.2,0.3c0.5,0.7,0.6,1.4,0.5,2.1C20.2,13.8,18,15,17.9,15L16.7,15.6z"/>
+</g>
+<g>
+ <g>
+ <path fill="#6699CC" d="M15.9,24.2c-2.9,0-5.3-0.8-5.5-1.8c-0.1-0.4,0.2-0.9,1.7-1.4l1.3-0.4l-1.2,0.7c-0.1,0.1-0.3,0.2-0.3,0.3
+ c0,0,0.1,0.3,1.2,0.4c1.2,0.1,2.1,0.2,2.8,0.2c1.2,0,2.3-0.1,4.1-0.5l0.1,0l0,0c0,0,0.8,0.5,1.9,0.9l0.4,0.1l-0.3,0.1
+ C19.7,24,17.4,24.2,15.9,24.2z"/>
+ </g>
+ <g>
+ <path fill="#6699CC" d="M15.3,20.4c-3.3,0-5.7-0.6-5.8-1.5c-0.1-0.5,0.6-1,1.9-1.5l0.8-0.3l-0.7,0.5c-0.1,0.1-0.4,0.4-0.4,0.6
+ c0,0.1,0.2,0.3,1.1,0.4c1.3,0.1,2.3,0.2,3.2,0.2c1.7,0,3.4-0.2,5.7-0.6l0.1,0l0.1,0.1c0,0,0.5,0.5,1.3,0.8l0.5,0.2l-0.5,0.1
+ C20.3,20,17.7,20.4,15.3,20.4z"/>
+ </g>
+ <g>
+ <path fill="#6699CC" d="M15.3,27.4C15.3,27.4,15.3,27.4,15.3,27.4c-3.1,0-6.3-0.3-9.4-0.8c-1.2-0.2-1.8-0.5-1.8-1
+ c0.1-1,3.1-2,4.7-2c0.4,0,0.7,0.1,0.9,0.2l0.6,0.4l-0.7-0.1c0,0-0.4-0.1-1.1,0.1c-1.6,0.5-2,0.8-2,0.9c0,0,0,0.1,0.2,0.2
+ c1.4,0.6,4.6,1,8.7,1c4.3,0,8.4-0.4,10.3-1c0.9-0.3,1.1-0.6,1.1-0.7c0-0.2-0.2-0.4-0.2-0.4l-0.9-0.8l1.1,0.5
+ c0.7,0.3,0.8,0.7,0.7,0.9C27.4,26,22.4,27.4,15.3,27.4z"/>
+ </g>
+ <g>
+ <path fill="#6699CC" d="M13.6,16.7c-3,0-6.7-0.3-6.9-1.2c-0.1-0.4,0.5-0.7,0.7-0.8c2.5-1.1,4.6-1.2,5.2-1.2c0.1,0,0.1,0,0.1,0l1,0
+ l-1,0.3c-1.8,0.4-3.4,1.1-3.4,1.4c0,0,0,0.2,1.2,0.4c0.8,0.1,1.9,0.1,3.1,0.1c1.6,0,3.4-0.1,5-0.2c2.5-0.2,5.1-0.7,5.1-0.7
+ l1.3-0.2L23.9,15c0,0-0.9,0.4-1.5,0.8l0,0C20.1,16.4,16.9,16.7,13.6,16.7z"/>
+ </g>
+ <g>
+ <path fill="#6699CC" d="M23.5,20.3v-0.2c0-0.1,0-0.3,0.2-0.3c3.8-2,3.8-3.7,3.7-4.3c-0.2-0.9-1.1-1.4-1.9-1.4c-0.1,0-0.2,0-0.3,0
+ c-0.5,0.1-0.7,0.2-0.7,0.2l-0.5,0.2l0.3-0.5c0,0,0.2-0.3,0.6-0.5c0.4-0.2,0.9-0.2,1.3-0.2c1.5,0,2.7,0.9,2.9,2.2
+ c0.2,1.5-1.1,3.5-5.4,4.7L23.5,20.3z"/>
+ </g>
+ <g>
+ <path fill="#6699CC" d="M16.9,29c-1.2,0-2.4,0-3.5-0.1c-3.8-0.2-4.8-0.8-4.8-1v-0.2l0.2,0c1.8,0.3,4.2,0.5,6.8,0.5
+ c3,0,5.9-0.3,8.4-0.7c4.4-0.9,4.9-1.9,4.9-1.9l0.3-0.8l-0.1,0.9c-0.1,1.2-1.5,2.1-4.1,2.6C22.9,28.7,20.1,29,16.9,29z"/>
+ </g>
+</g>
+<path fill="#F15A24" d="M16.8,4.2c1.8-1.4,3-2.8,3.7-4.2h-1.7c-0.8,1-1.9,1.8-2.9,2.7c-1.2,1-2.5,2-3.3,3.2c-1.6,2.6,1,5.2,4,8
+ l0.5,0.5l-0.3-0.6c-0.3-0.7-0.7-1.3-1.2-1.9C14.2,9.6,12.7,7.4,16.8,4.2z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/language.svg b/launcher/resources/pe_colored/scalable/language.svg
new file mode 100644
index 00000000..80c1dcad
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/language.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata19"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs17" />
+<path
+ id="path2"
+ d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
+ fill="#F2F2F2"
+ clip-rule="evenodd"
+ fill-rule="evenodd" />
+
+<g
+ id="g12">
+ <path
+ id="path6"
+ d="M6,28h20c1.1,0,2-0.9,2-2V9V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v3v17C4,27.1,4.9,28,6,28z"
+ fill="none" />
+ <path
+ id="path8"
+ d="M26,0H6C2.7,0,0,2.7,0,6v3h4V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2v3h4V6C32,2.7,29.3,0,26,0z"
+ fill="#39B54A" />
+ <path
+ id="path10"
+ d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V9H0v17c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9h-4V26z"
+ fill="#8C6239" />
+</g>
+<path
+ d="m 9.1897919,10.114302 c -0.483841,0 -0.901102,0.341924 -0.996278,0.815883 l -2.029453,10.155454 c -0.110056,0.55031 0.246086,1.083011 0.795382,1.193069 0.556201,0.113982 1.084021,-0.246085 1.193073,-0.79538 l 0.651882,-3.247121 H 11.67433 l 0.647785,3.247121 c 0.110996,0.557485 0.658771,0.906193 1.197171,0.79538 0.549363,-0.110056 0.905437,-0.642759 0.795381,-1.193069 L 12.285214,10.930185 c -0.09517,-0.473959 -0.512431,-0.815883 -0.996274,-0.815883 z m 0.614982,2.287746 h 0.4345931 l 0.811779,4.063002 H 8.9929949 Z M 20.710512,9.7002137 c -0.561209,0 -1.016777,0.4578073 -1.016777,1.0208753 v 1.016774 h -3.046227 c -0.561204,0 -1.012672,0.457809 -1.012672,1.020876 0,0.563065 0.451466,1.016776 1.012672,1.016776 h 0.274696 c 0.577658,1.85973 1.425829,3.313572 2.37794,4.460692 -0.745862,0.684158 -1.478369,1.245052 -2.271343,1.873653 -0.437271,0.35118 -0.506831,0.995228 -0.155795,1.434967 0.349411,0.439057 0.990377,0.507857 1.426766,0.155794 0.861615,-0.682321 1.601995,-1.252055 2.410742,-1.996652 0.808748,0.744597 1.618828,1.314331 2.480441,1.996652 0.436393,0.352059 1.077353,0.283261 1.426766,-0.155794 0.351038,-0.439739 0.277375,-1.083787 -0.159884,-1.434967 -0.792979,-0.628601 -1.591082,-1.189495 -2.336946,-1.873653 0.952113,-1.147188 1.869915,-2.601029 2.447642,-4.460692 h 0.270592 c 0.561209,0 1.016774,-0.453711 1.016774,-1.016776 0,-0.563067 -0.455565,-1.020876 -1.016774,-1.020876 h -3.111838 v -1.016774 c 0,-0.563068 -0.455567,-1.0208753 -1.016775,-1.0208753 z m -1.668658,4.0753013 h 3.402916 c -0.438423,1.181552 -1.08891,2.13806 -1.734258,2.951929 -0.645345,-0.813869 -1.230238,-1.770309 -1.668658,-2.951929 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#c1272d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.29744864;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect977" /></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_colored/scalable/loadermods.svg b/launcher/resources/pe_colored/scalable/loadermods.svg
new file mode 100644
index 00000000..2d80c7f3
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/loadermods.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#C1272D" d="M16,6.4l2.9,6l6.6,1l-4.8,4.6l1.1,6.5L16,21.4l-5.9,3.1l1.1-6.5
+ l-4.8-4.6l6.6-1L16,6.4z"/>
+<g>
+ <path fill="none" d="M6,28h20c1.1,0,2-0.9,2-2V9V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v3v17C4,27.1,4.9,28,6,28z"/>
+ <path fill="#39B54A" d="M26,0H6C2.7,0,0,2.7,0,6v3h4V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2v3h4V6C32,2.7,29.3,0,26,0z"/>
+ <path fill="#8C6239" d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V9H0v17c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9h-4V26z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/log.svg b/launcher/resources/pe_colored/scalable/log.svg
new file mode 100644
index 00000000..42659b53
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/log.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <polygon fill="none" points="30,0 30,0 30,0 "/>
+ <polygon fill="none" points="2,0 2,0 2,0 "/>
+ <path fill="#39B54A" d="M30,0L30,0c-0.1,0-0.1,0-0.2,0C16,0.4,16,4,16,4s0-3.6-13.8-4C2.1,0,2.1,0,2,0h0C0.9,0,0,0.9,0,2v7h32V2
+ C32,0.9,31.1,0,30,0z"/>
+ <path fill="#8C6239" d="M0,26c0,1.1,0.9,2,2,2c3.1,0.1,8.9,0.7,12.1,2.3c0.2,1,0.9,1.7,1.9,1.7c1,0,1.8-0.7,1.9-1.7
+ c3.2-1.6,9-2.2,12.1-2.3c1.1,0,2-0.9,2-2V9H0V26z"/>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M14,6.3C11.4,5,7.2,4.4,4,4.2v19.9c5.4,0.3,8.4,1.1,10,1.9V6.3z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,4.2C24.8,4.4,20.6,5,18,6.3V26c1.6-0.8,4.6-1.6,10-1.9V4.2z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/minecraft.svg b/launcher/resources/pe_colored/scalable/minecraft.svg
new file mode 100644
index 00000000..52815487
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/minecraft.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="-463 265 32 32" enable-background="new -463 265 32 32" xml:space="preserve">
+<rect x="-463" y="265" fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#754C24" points="-447,279.2 -433,272.1 -433,289.9 -447,297 "/>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#8C6239" points="-447,279.2 -461,272.1 -461,289.9 -447,297 "/>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#39B54A" points="-461,272.1 -447,279.2 -433,272.1 -447,265 "/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/multimc.svg b/launcher/resources/pe_colored/scalable/multimc.svg
new file mode 100644
index 00000000..a146c52e
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/multimc.svg
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="-463 265 32 32" enable-background="new -463 265 32 32" xml:space="preserve">
+<rect x="-463" y="265" fill="#754C24" width="32" height="32"/>
+<polygon fill="#39B54A" points="-463,270 -462,270 -462,269 -459,269 -459,270 -455,270 -455,273 -454,273 -454,267 -451,267
+ -451,270 -450,270 -450,269 -447,269 -447,273 -446,273 -446,271 -445,271 -445,270 -443,270 -443,273 -442,273 -442,271 -441,271
+ -441,270 -439,270 -439,269 -437,269 -437,270 -434,270 -434,269 -431,269 -431,265 -463,265 "/>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1426.8778" y1="312.5718" x2="-1426.032" y2="314.6501" gradientTransform="matrix(20.79 0 0 14.7315 29210.8574 -4335.6729)">
+ <stop offset="0" style="stop-color:#000000;stop-opacity:0.8"/>
+ <stop offset="1" style="stop-color:#000000;stop-opacity:0.35"/>
+</linearGradient>
+<path fill="url(#SVGID_1_)" d="M-444.5,286.5c-0.5-0.4-1-1-1.8-1.9c-1,1.2-1.9,2.1-2.8,2.7c-1.1,0.7-2.5,1-4.1,1
+ c-2,0-3.6-0.5-4.9-1.6c-1.4-1.1-2.1-2.6-2.1-4.4c0-1.7,0.7-3.2,2.1-4.4c1.2-1,2.9-1.6,5-1.6c1.1,0,2,0.2,2.8,0.5
+ c0.9,0.3,1.7,0.8,2.3,1.4c0.6,0.5,1.2,1.2,1.8,1.9c1-1.2,1.9-2.1,2.8-2.7c1.1-0.7,2.5-1,4.1-1c2,0,3.6,0.5,4.9,1.6
+ c1.4,1.1,2.1,2.6,2.1,4.4c0,1.7-0.7,3.2-2.1,4.4c-1.2,1-2.9,1.6-5,1.6c-1.1,0-2-0.2-2.8-0.5S-443.8,287.1-444.5,286.5 M-453.4,286.3
+ c2.4,0,4.3-1.2,5.8-3.7c-1.9-2.7-3.8-4.1-5.8-4.1c-1.5,0-2.6,0.4-3.3,1.1c-0.8,0.8-1.2,1.7-1.2,2.8c0,1.2,0.4,2.1,1.2,2.8
+ C-455.9,285.9-454.8,286.3-453.4,286.3 M-439.1,278.5c-2.2,0-4.1,1.2-5.8,3.7c1.8,2.7,3.8,4.1,5.8,4.1c1.5,0,2.6-0.4,3.3-1.1
+ c0.8-0.8,1.2-1.7,1.2-2.8c0-1.2-0.4-2.1-1.2-2.8C-436.7,278.8-437.8,278.5-439.1,278.5"/>
+<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-1427.0289" y1="313.0195" x2="-1425.7877" y2="313.9627" gradientTransform="matrix(20.79 0 0 14.7315 29210.5391 -4335.8115)">
+ <stop offset="0" style="stop-color:#75B54B"/>
+ <stop offset="1" style="stop-color:#75B54B;stop-opacity:0.6"/>
+</linearGradient>
+<path fill="url(#SVGID_2_)" d="M-445.4,285.1c-0.5-0.4-1-1-1.8-1.9c-1,1.2-1.9,2.1-2.8,2.7c-1.1,0.7-2.5,1-4.1,1
+ c-2,0-3.6-0.5-4.9-1.6c-1.4-1.1-2.1-2.6-2.1-4.4c0-1.7,0.7-3.2,2.1-4.4c1.2-1,2.9-1.6,5-1.6c1.1,0,2,0.2,2.8,0.5
+ c0.9,0.3,1.7,0.8,2.3,1.4c0.6,0.5,1.2,1.2,1.8,1.9c1-1.2,1.9-2.1,2.8-2.7c1.1-0.7,2.5-1,4.1-1c2,0,3.6,0.5,4.9,1.6
+ c1.4,1.1,2.1,2.6,2.1,4.4c0,1.7-0.7,3.2-2.1,4.4c-1.2,1-2.9,1.6-5,1.6c-1.1,0-2-0.2-2.8-0.5C-443.8,286.2-444.6,285.8-445.4,285.1
+ M-454.3,284.9c2.4,0,4.3-1.2,5.8-3.7c-1.9-2.7-3.8-4.1-5.8-4.1c-1.5,0-2.6,0.4-3.3,1.1c-0.8,0.8-1.2,1.7-1.2,2.8
+ c0,1.2,0.4,2.1,1.2,2.8C-456.8,284.5-455.7,284.9-454.3,284.9 M-440,277.1c-2.2,0-4.1,1.2-5.8,3.7c1.8,2.7,3.8,4.1,5.8,4.1
+ c1.5,0,2.6-0.4,3.3-1.1c0.8-0.8,1.2-1.7,1.2-2.8c0-1.2-0.4-2.1-1.2-2.8C-437.5,277.5-438.6,277.1-440,277.1"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/new.svg b/launcher/resources/pe_colored/scalable/new.svg
new file mode 100644
index 00000000..f18ed28a
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/new.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<g>
+ <circle fill="none" cx="6" cy="4" r="2"/>
+ <path fill="#39B54A" d="M26,0H6C2.7,0,0,2.7,0,6v3h32V6C32,2.7,29.3,0,26,0z M6,6C4.9,6,4,5.1,4,4c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2
+ C8,5.1,7.1,6,6,6z"/>
+ <path fill="#8C6239" d="M0,26c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9H0V26z"/>
+</g>
+<path fill="#F2F2F2" d="M28,8H4v18c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V8z"/>
+<path fill="#009245" d="M20,20h-2v2c0,1.1-0.9,2-2,2s-2-0.9-2-2v-2h-2c-1.1,0-2-0.9-2-2s0.9-2,2-2h2v-2c0-1.1,0.9-2,2-2s2,0.9,2,2v2
+ h2c1.1,0,2,0.9,2,2S21.1,20,20,20z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/news.svg b/launcher/resources/pe_colored/scalable/news.svg
new file mode 100644
index 00000000..4f924cd8
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/news.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
+<rect fill="none" width="16" height="16"/>
+<g>
+ <g>
+ <path fill="#39B54A" d="M13,0H3C1.3,0,0,1.3,0,3v7c0,1.7,1.3,3,3,3l2,3l2-3h6c1.7,0,3-1.3,3-3V3C16,1.3,14.7,0,13,0z M8,10H3V9h5
+ V10z M13,8H3V7h10V8z M13,6H3V5h10V6z M13,4H3V3h10V4z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/notes.svg b/launcher/resources/pe_colored/scalable/notes.svg
new file mode 100644
index 00000000..55ece163
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/notes.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <rect x="6" y="0" fill="none" width="20" height="0"/>
+ <polygon fill="none" points="0,6 0,6 0,26 0,26 0,9 "/>
+ <polygon fill="none" points="32,6 32,9 32,26 32,26 32,6 "/>
+ <path fill="#39B54A" d="M32,9V6c0-3.3-2.7-6-6-6H6C2.7,0,0,2.7,0,6v3H32z"/>
+ <path fill="#8C6239" d="M0,9v17c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9H0z"/>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<g>
+ <rect x="8" y="16" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="12" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/patreon.svg b/launcher/resources/pe_colored/scalable/patreon.svg
new file mode 100644
index 00000000..d3c6d2d5
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/patreon.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="-463 265 32 32" enable-background="new -463 265 32 32" xml:space="preserve">
+<rect x="-463" y="265" fill="none" width="32" height="32"/>
+<path fill="#F15A24" d="M-447,265c-8.8,0-16,7.2-16,16v16h15.3c0.2,0,0.4,0,0.7,0c8.8,0,16-7.2,16-16S-438.2,265-447,265L-447,265z"
+ />
+<path fill="#FFFFFF" d="M-447,268.7c-6.8,0-12.3,5.5-12.3,12.3v7.3v5v3.7h4.9v-16c0-4.1,3.3-7.3,7.3-7.3s7.3,3.3,7.3,7.3
+ c0,4.1-3.3,7.3-7.3,7.3c-1.5,0-2.9-0.4-4-1.2v5.3c0.8,0.5,2.5,0.8,4.6,0.8c6.5-0.3,11.7-5.7,11.7-12.3
+ C-434.7,274.2-440.2,268.7-447,268.7L-447,268.7z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/proxy.svg b/launcher/resources/pe_colored/scalable/proxy.svg
new file mode 100644
index 00000000..0aee69b7
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/proxy.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="-463 265 32 32" enable-background="new -463 265 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#8C6239" d="M-441.1,269h-10.3c-0.8-2.3-3-4-5.6-4c-3.3,0-6,2.7-6,6s2.7,6,6,6
+ c2.6,0,4.8-1.7,5.6-4h10.3c1.1,0,2,0.9,2,2v1.4h4V275C-435.1,271.7-437.8,269-441.1,269z M-457,273c-1.1,0-2-0.9-2-2s0.9-2,2-2
+ c1.1,0,2,0.9,2,2S-455.9,273-457,273z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#754C24" d="M-439.1,285.6v1.4c0,1.1-0.9,2-2,2h-10.3c-0.8-2.3-3-4-5.6-4
+ c-3.3,0-6,2.7-6,6s2.7,6,6,6c2.6,0,4.8-1.7,5.6-4h10.3c3.3,0,6-2.7,6-6v-1.4H-439.1z M-457,293c-1.1,0-2-0.9-2-2s0.9-2,2-2
+ c1.1,0,2,0.9,2,2S-455.9,293-457,293z"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#C1272D" d="M-431,287h-12v-12h12V287z M-435,279h-4v4h4V279z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/quickmods.svg b/launcher/resources/pe_colored/scalable/quickmods.svg
new file mode 100644
index 00000000..199b2dae
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/quickmods.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill="none" d="M6,28h20c1.1,0,2-0.9,2-2V9V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v3v17C4,27.1,4.9,28,6,28z"/>
+<path fill="#39B54A" d="M26,0H6C2.7,0,0,2.7,0,6v3h4V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2v3h4V6C32,2.7,29.3,0,26,0z"/>
+<path fill="#8C6239" d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V9H0v17c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9h-4V26z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#C1272D" d="M10.6,24.2c-0.3,0-0.6-0.1-0.8-0.2C9.3,23.7,9,23.2,9,22.6V9.8
+ c0-0.6,0.3-1.1,0.8-1.4c0.3-0.2,0.5-0.2,0.8-0.2c0.2,0,0.5,0.1,0.7,0.2l12.8,6.4c0.5,0.3,0.9,0.8,0.9,1.4c0,0.6-0.3,1.2-0.9,1.4
+ L11.3,24C11.1,24.1,10.8,24.2,10.6,24.2z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/refresh.svg b/launcher/resources/pe_colored/scalable/refresh.svg
new file mode 100644
index 00000000..c2e7e91f
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/refresh.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#39B54A" d="M31.8,17.4c-0.4-0.9-1.2-1.4-2.1-1.4h-2.3c0-7.6-6.1-13.7-13.7-13.7C6.1,2.3,0,8.4,0,16
+ c0,7.6,6.1,13.7,13.7,13.7c2.5,0,4.8-0.7,6.8-1.8l-3.4-3.4c-1,0.4-2.2,0.7-3.4,0.7c-5,0-9.1-4.1-9.1-9.1s4.1-9.1,9.1-9.1
+ c5.1,0,9.1,4.1,9.1,9.1h-2.3c-0.9,0-1.8,0.6-2.1,1.4c-0.4,0.9-0.2,1.8,0.5,2.5l4.6,4.6c0.4,0.4,1,0.7,1.6,0.7c0.6,0,1.2-0.2,1.6-0.7
+ l4.6-4.6C32,19.2,32.2,18.3,31.8,17.4z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/resourcepacks.svg b/launcher/resources/pe_colored/scalable/resourcepacks.svg
new file mode 100644
index 00000000..0318354c
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/resourcepacks.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,10c0-1.1-0.9-2-2-2H6c-1.1,0-2,0.9-2,2v16c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V10z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#39B54A" d="M26,4h-4V2c0-1.1-0.9-2-2-2h-8c-1.1,0-2,0.9-2,2v2H6
+ C3,4,0.6,6.2,0.1,9h4.2c0.3-0.6,1-1,1.7-1h20c0.7,0,1.4,0.4,1.7,1h4.2C31.4,6.2,29,4,26,4z M20,4h-8V3c0-0.6,0.4-1,1-1h6
+ c0.6,0,1,0.4,1,1V4z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#8C6239" d="M31.9,9h-4.2c0.2,0.3,0.3,0.6,0.3,1v4H4v-4c0-0.4,0.1-0.7,0.3-1
+ H0.1C0,9.3,0,9.7,0,10v16c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V10C32,9.7,32,9.3,31.9,9z M14.3,16h3.4c0.2,0.3,0.3,0.6,0.3,1
+ c0,1.1-0.9,2-2,2s-2-0.9-2-2C14,16.6,14.1,16.3,14.3,16z M26,28H6c-1.1,0-2-0.9-2-2V16h8.1c-0.1,0.3-0.1,0.7-0.1,1c0,2.2,1.8,4,4,4
+ s4-1.8,4-4c0-0.3-0.1-0.7-0.1-1H28v10C28,27.1,27.1,28,26,28z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/screenshots.svg b/launcher/resources/pe_colored/scalable/screenshots.svg
new file mode 100644
index 00000000..844fcbaa
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/screenshots.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="#39B54A" d="M26,0H6C2.7,0,0,2.7,0,6v3h32V6C32,2.7,29.3,0,26,0z"/>
+ <path fill="#8C6239" d="M0,26c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9H0V26z"/>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M24,26H8c-1.1,0-2-0.9-2-2v-0.7c0.8-1.7,1.5-3.8,2-4.8
+ c1.1-2.4,3.5,3.5,4.9,3.5c2.8,0,6.2-10.7,9.1-7.4c1.4,1.6,2.9,5.8,4,8.4V24C26,25.1,25.1,26,24,26z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F15A24" d="M9,6c1.7,0,3,1.3,3,3c0,1.7-1.3,3-3,3c-1.7,0-3-1.3-3-3
+ C6,7.3,7.3,6,9,6z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/settings.svg b/launcher/resources/pe_colored/scalable/settings.svg
new file mode 100644
index 00000000..72032f8a
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/settings.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect x="0" y="0" fill="none" width="32" height="32"/>
+<polygon fill="none" points="26,0 6,0 6,0 26,0 "/>
+<path fill="#39B54A" d="M26,0H6C2.7,0,0,2.7,0,6v3h32V6C32,2.7,29.3,0,26,0z"/>
+<path fill="#8C6239" d="M0,26c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9H0V26z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<rect x="16" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<rect x="8" y="18" fill-rule="evenodd" clip-rule="evenodd" width="6" height="6"/>
+<rect x="10" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" width="2" height="2"/>
+<rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" width="6" height="6"/>
+<rect x="10" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" width="2" height="2"/>
+<rect x="16" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/status-bad.svg b/launcher/resources/pe_colored/scalable/status-bad.svg
new file mode 100644
index 00000000..bc42c248
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/status-bad.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#C1272D" d="M26,32H6c-3.4,0-6-2.6-6-6V6c0-3.4,2.6-6,6-6h20c3.4,0,6,2.6,6,6v20C32,29.2,29.4,32,26,32z"/>
+<path fill="#FFFFFF" d="M21.6,21.6c-0.8,0.8-2,0.8-2.8,0L16,18.8l-2.8,2.8c-0.8,0.8-2,0.8-2.8,0c-0.8-0.8-0.8-2,0-2.8l2.8-2.8
+ l-2.8-2.8c-0.8-0.8-0.8-2,0-2.8c0.8-0.8,2-0.8,2.8,0l2.8,2.8l2.8-2.8c0.8-0.8,2-0.8,2.8,0s0.8,2,0,2.8L18.8,16l2.8,2.8
+ C22.4,19.6,22.4,20.8,21.6,21.6z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/status-good.svg b/launcher/resources/pe_colored/scalable/status-good.svg
new file mode 100644
index 00000000..4cfa56f0
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/status-good.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="none" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z M28,26c0,1.2-0.8,2-2,2
+ H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2
+ c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+</g>
+<path fill="#39B54A" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z"/>
+<path fill="#39B54A" d="M28,26c0,1.2-0.8,2-2,2H6c-1.2,0-2-0.8-2-2V6c0-1.2,0.8-2,2-2h20c1.2,0,2,0.8,2,2V26z"/>
+<path fill="#FFFFFF" d="M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2c0.8,0.8,2,0.8,2.8,0l8.4-8.4
+ c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/status-yellow.svg b/launcher/resources/pe_colored/scalable/status-yellow.svg
new file mode 100644
index 00000000..0551fed2
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/status-yellow.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="#E3A333" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z M28,26
+ c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+</g>
+<path fill="#E3A333" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z"/>
+<path fill="#E3A333" d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+<g>
+ <path fill="#FFFFFF" d="M6,20c0-5.8,2.4-7.9,5.8-7.9c2,0,3.4,0.7,5.3,1.9c1.6,1,2.5,1.5,3.3,1.5c0.7,0,1.2-0.7,1.3-3.1H26
+ c0.2,6.1-2.3,8-5.5,8c-2,0-3.5-0.8-5.3-1.9c-1.7-1-2.6-1.6-3.3-1.6c-0.8,0-1.4,0.5-1.6,3.2H6z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/viewfolder.svg b/launcher/resources/pe_colored/scalable/viewfolder.svg
new file mode 100644
index 00000000..91832577
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/viewfolder.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#F2F2F2" d="M28,10c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z"
+ />
+<rect x="0" fill="none" width="32" height="32"/>
+<g>
+ <path fill="none" d="M26,8H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v3v17c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10
+ c0-0.4-0.1-0.7-0.3-1C27.4,8.4,26.7,8,26,8z"/>
+ <path fill="#39B54A" d="M4,6c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2v2h10c0.7,0,1.4,0.4,1.7,1h4.2C31.4,6.2,29,4,26,4h-6.4
+ c-0.8-2.3-3-4-5.6-4H6C2.7,0,0,2.7,0,6v3h4V6z"/>
+ <path fill="#8C6239" d="M27.7,9c0.2,0.3,0.3,0.6,0.3,1v16c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V9H0v17c0,3.3,2.7,6,6,6h20
+ c3.3,0,6-2.7,6-6V10c0-0.3,0-0.7-0.1-1H27.7z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_colored/scalable/worlds.svg b/launcher/resources/pe_colored/scalable/worlds.svg
new file mode 100644
index 00000000..087ba7c9
--- /dev/null
+++ b/launcher/resources/pe_colored/scalable/worlds.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata19"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs17" /><g
+ id="g6"><path
+ id="path2"
+ d="M26,0H6C2.7,0,0,2.7,0,6v3h32V6C32,2.7,29.3,0,26,0z"
+ fill="#39B54A" /><path
+ id="path4"
+ d="M0,26c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9H0V26z"
+ fill="#8C6239" /></g><path
+ fill="#F2F2F2"
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ id="path8"
+ d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z" /><g
+ transform="rotate(29.970903,16,15.973684)"
+ id="g881"><ellipse
+ ry="8.9473686"
+ rx="8.9473696"
+ cy="16"
+ cx="16"
+ id="path845"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><ellipse
+ ry="8.9473686"
+ rx="3.4915254"
+ cy="15.947369"
+ cx="16"
+ id="path845-3"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><path
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 8.1848294,12.430327 C 12.570183,14.790801 20.250652,13.785896 22.94338,12"
+ id="path870" /><path
+ id="path872"
+ d="M 8.2166335,20.492522 C 12.601987,18.132048 21.148735,17.776739 23.841463,19.562635"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_dark/index.theme b/launcher/resources/pe_dark/index.theme
new file mode 100644
index 00000000..b7d1ad01
--- /dev/null
+++ b/launcher/resources/pe_dark/index.theme
@@ -0,0 +1,11 @@
+[Icon Theme]
+Name=pe_dark
+Comment=Icons by pexner (dark)
+Inherits=multimc
+Directories=scalable
+
+[scalable]
+Size=48
+Type=Scalable
+MinSize=16
+MaxSize=256
diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc
new file mode 100644
index 00000000..a57b6a14
--- /dev/null
+++ b/launcher/resources/pe_dark/pe_dark.qrc
@@ -0,0 +1,38 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/icons/pe_dark">
+ <file>index.theme</file>
+ <file>scalable/about.svg</file>
+ <file>scalable/accounts.svg</file>
+ <file>scalable/bug.svg</file>
+ <file>scalable/centralmods.svg</file>
+ <file>scalable/checkupdate.svg</file>
+ <file>scalable/copy.svg</file>
+ <file>scalable/coremods.svg</file>
+ <file>scalable/externaltools.svg</file>
+ <file>scalable/help.svg</file>
+ <file>scalable/instance-settings.svg</file>
+ <file>scalable/jarmods.svg</file>
+ <file>scalable/java.svg</file>
+ <file>scalable/language.svg</file>
+ <file>scalable/loadermods.svg</file>
+ <file>scalable/log.svg</file>
+ <file>scalable/minecraft.svg</file>
+ <file>scalable/multimc.svg</file>
+ <file>scalable/new.svg</file>
+ <file>scalable/news.svg</file>
+ <file>scalable/notes.svg</file>
+ <file>scalable/patreon.svg</file>
+ <file>scalable/proxy.svg</file>
+ <file>scalable/quickmods.svg</file>
+ <file>scalable/refresh.svg</file>
+ <file>scalable/resourcepacks.svg</file>
+ <file>scalable/screenshots.svg</file>
+ <file>scalable/settings.svg</file>
+ <file>scalable/status-bad.svg</file>
+ <file>scalable/status-good.svg</file>
+ <file>scalable/status-yellow.svg</file>
+ <file>scalable/viewfolder.svg</file>
+ <file>scalable/worlds.svg</file>
+ </qresource>
+</RCC>
diff --git a/launcher/resources/pe_dark/scalable/about.svg b/launcher/resources/pe_dark/scalable/about.svg
new file mode 100644
index 00000000..e75ea6ca
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/about.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <g>
+ <polygon fill="#666666" points="17.8,21 17.8,11.9 11.9,11.9 11.9,14.2 14.2,14.2 14.2,21 11.9,21 11.9,23.3 20.1,23.3 20.1,21
+ "/>
+ <circle fill="#666666" cx="16" cy="8.3" r="1.8"/>
+ </g>
+ <path d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16C32,24.8,24.8,32,16,32L16,32z M16,4C9.4,4,4,9.4,4,16
+ s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/accounts.svg b/launcher/resources/pe_dark/scalable/accounts.svg
new file mode 100644
index 00000000..6d46b2df
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/accounts.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#999999" d="M25.8,17.5c1.4-1.4,2.2-3.4,2.2-5.5V8c0-4.4-3.6-8-8-8
+ c-0.6,0-1.2,0.1-1.8,0.2c1.3,1.1,2.4,2.4,3,4C22.8,4.7,24,6.2,24,8v4c0,1.8-1.2,3.3-2.8,3.8c-0.2,0.4-0.4,0.8-0.6,1.2
+ c1.5,1.2,2.8,2.6,3.7,4.2c1.8,1.1,3.1,2.7,3.6,4.7c0,1.1-0.9,2-2,2h-0.3c-0.4,1.6-1.3,3-2.5,4H26c3.3,0,6-2.7,6-6
+ C31.4,22.3,28.9,19.2,25.8,17.5z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M18,32H6c-3.3,0-6-2.7-6-6l0,0c0.6-3.7,3.1-6.8,6.2-8.5C4.9,16.1,4,14.1,4,12V8
+ c0-4.4,3.6-8,8-8s8,3.6,8,8v4c0,2.1-0.9,4.1-2.2,5.5c3.1,1.7,5.6,4.8,6.2,8.5l0,0C24,29.3,21.3,32,18,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M16,12V8c0-2.2-1.8-4-4-4S8,5.8,8,8v4c0,2.2,1.8,4,4,4
+ S16,14.2,16,12z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M20,26c-0.9-3.4-4.3-6-8-6l0,0l0,0c-3.7,0-7.1,2.6-8,6l0,0
+ c0,1.1,0.9,2,2,2h12C19.1,28,20,27.1,20,26L20,26z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/bug.svg b/launcher/resources/pe_dark/scalable/bug.svg
new file mode 100644
index 00000000..9da71adb
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/bug.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect x="0" y="0" fill="none" width="32" height="32"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M22.7,5.3c-0.3-1.2-0.9-2.3-1.8-3.2c-2.7-2.8-7.2-2.8-9.9,0
+ c-0.9,0.9-1.5,2-1.8,3.2C13.5,3.3,18.5,3.3,22.7,5.3z"/>
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M30,17.9h-4.1c0-0.7,0.1-1.3,0.1-2c0-0.7,0-1.4-0.1-2c0,0,0,0,0.1,0v0h2
+ c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-2v0c-0.3,0-0.6,0.1-0.8,0.1c-1.2-3.1-3.9-4.1-9.2-4.1s-7.9,1-9.2,4.1C6.6,9.9,6.3,9.8,6,9.8v0H4
+ c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h2v0c0,0,0,0,0.1,0c0,0.6-0.1,1.3-0.1,2c0,0.7,0,1.3,0.1,2H2c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2
+ h4.5c0.3,1.4,0.7,2.8,1.2,4H6c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h2v0c0.7,0,1.4-0.1,2.1-0.3c1.5,1.4,3.4,2.3,5.9,2.3
+ s4.4-0.9,5.9-2.3c0.7,0.2,1.4,0.3,2.1,0.3v0h2c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-1.7c0.5-1.2,0.9-2.6,1.2-4H30c1.1,0,2-0.9,2-2
+ C32,18.8,31.1,17.9,30,17.9z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/centralmods.svg b/launcher/resources/pe_dark/scalable/centralmods.svg
new file mode 100644
index 00000000..f3b0c0e4
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/centralmods.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h8c2.6,0,4.8,1.7,5.6,4H26c3.3,0,6,2.7,6,6v16C32,29.3,29.3,32,26,32z M28,10
+ c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z"/>
+<path fill="#666666" d="M16,10.2l2.2,4.5l5,0.7L19.6,19l0.8,4.9L16,21.6l-4.4,2.3l0.8-4.9l-3.6-3.5l5-0.7L16,10.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/checkupdate.svg b/launcher/resources/pe_dark/scalable/checkupdate.svg
new file mode 100644
index 00000000..97585447
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/checkupdate.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#666666" d="M26,24h-0.4c0-0.1-0.1-0.2-0.1-0.3c-0.7-1.6-1.5-2.8-3.5-3.4V18c0-3.3-2.7-6-6-6c-3.3,0-6,2.7-6,6v2.3
+ c-2,0.6-2.9,1.8-3.5,3.4c0,0,0,0.1,0,0.1C2.8,23.1,0,19.9,0,16v-2c0-4.4,3.6-8,8-8c0.8,0,1.5,0.1,2.2,0.3C10.9,2.7,14.2,0,18,0
+ c4.4,0,8,3.6,8,8c0,0.7-0.1,1.4-0.3,2c0.1,0,0.2,0,0.3,0c3.3,0,6,2.7,6,6v2C32,21.3,29.3,24,26,24z"/>
+<path d="M12,24h2v-6c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2v6h2c0.8,0,1.5,0.5,1.8,1.2c0.1,0.2,0.1,0.4,0.1,0.6c0.1,0.6-0.1,1.2-0.6,1.6
+ l-4,4C17,31.8,16.5,32,16,32c-0.5,0-1-0.2-1.4-0.6l-4-4c-0.4-0.4-0.6-1-0.6-1.6c0-0.2,0.1-0.4,0.1-0.6C10.5,24.5,11.2,24,12,24z"/>
+<rect fill="none" width="32" height="32"/>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/copy.svg b/launcher/resources/pe_dark/scalable/copy.svg
new file mode 100644
index 00000000..8c30ac0b
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/copy.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#666666" d="M28,25.6V10c0-3.3-2.7-6-6-6H6.4c0.8-2.3,3-4,5.6-4h14c3.3,0,6,2.7,6,6v14C32,22.6,30.3,24.8,28,25.6z"/>
+<path d="M26,12v14c0,3.3-2.7,6-6,6H6c-3.3,0-6-2.7-6-6V12c0-3.3,2.7-6,6-6h14C23.3,6,26,8.7,26,12z M4,26c0,1.1,0.9,2,2,2h14
+ c1.1,0,2-0.9,2-2V14H4V26z M6,8c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S7.1,8,6,8z"/>
+<rect fill="none" width="32" height="32"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/coremods.svg b/launcher/resources/pe_dark/scalable/coremods.svg
new file mode 100644
index 00000000..1e2eb227
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/coremods.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" cx="16" cy="16" r="12"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16
+ C32,24.8,24.8,32,16,32z M16,4C9.4,4,4,9.4,4,16c0,6.6,5.4,12,12,12s12-5.4,12-12C28,9.4,22.6,4,16,4z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M16,6l2.9,6l6.6,1l-4.8,4.6l1.1,6.5L16,21l-5.9,3.1l1.1-6.5
+ l-4.8-4.6l6.6-1L16,6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/externaltools.svg b/launcher/resources/pe_dark/scalable/externaltools.svg
new file mode 100644
index 00000000..29b45f26
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/externaltools.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6,32h20c3.3,0,6-2.7,6-6V6c0-3.3-2.7-6-6-6h-9.1C17.6,1.2,18,2.6,18,4h8
+ c1.1,0,2,0.9,2,2v20c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2v-4.5l-4-3V26C0,29.3,2.7,32,6,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M8.8,17.6C9.2,17.9,9.6,18,10,18c0.3,0,0.6-0.1,0.9-0.2
+ c0.7-0.3,1.1-1,1.1-1.8v-1.9V14c6.3,0,11.7,4.2,13.4,10c0.4-1.3,0.6-2.6,0.6-4c0-7.7-6.3-14-14-14V4c0-0.8-0.4-1.5-1.1-1.8
+ C10.6,2.1,10.3,2,10,2C9.6,2,9.2,2.1,8.8,2.4l-8,6C0.3,8.8,0,9.4,0,10c0,0.6,0.3,1.2,0.8,1.6L8.8,17.6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/help.svg b/launcher/resources/pe_dark/scalable/help.svg
new file mode 100644
index 00000000..2a1518ae
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/help.svg
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata17"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs15" /><g
+ id="g10"><path
+ id="path8"
+ d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16C32,24.8,24.8,32,16,32L16,32z M16,4C9.4,4,4,9.4,4,16 s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z" /><g
+ transform="translate(41.863574,-2.0092638)"
+ id="g861"><g
+ aria-label="?"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:27.34714317px;line-height:85.45981598px;font-family:'Nimbus Sans L';-inkscape-font-specification:'Nimbus Sans L';letter-spacing:0px;word-spacing:0px;fill:#666666;fill-opacity:1;stroke:none;stroke-width:3.4183929px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text832"><path
+ d="m -20.968435,13.920532 c 0,0.6381 -0.07293,1.239737 -0.218777,1.804911 -0.145852,0.565174 -0.401092,1.066538 -0.76572,1.504093 -0.401092,0.492249 -0.774836,0.902456 -1.121233,1.230621 -0.346397,0.328166 -0.656332,0.6381 -0.929803,0.929803 -0.25524,0.273471 -0.464902,0.556059 -0.628985,0.847762 -0.145851,0.291703 -0.218777,0.656331 -0.218777,1.093885 v 0.38286 h -3.144921 V 20.72997 c 0,-0.601637 0.136736,-1.221505 0.410207,-1.859605 0.273471,-0.656332 0.756604,-1.285316 1.449399,-1.886953 0.346397,-0.309935 0.638099,-0.583406 0.875108,-0.820415 0.237009,-0.25524 0.428439,-0.492248 0.57429,-0.711025 0.145851,-0.237009 0.246124,-0.483133 0.300819,-0.738373 0.05469,-0.273471 0.08204,-0.57429 0.08204,-0.902456 0,-0.528711 -0.12762,-0.984497 -0.38286,-1.367357 -0.237009,-0.401091 -0.656331,-0.601637 -1.257968,-0.601637 -1.057423,0 -1.640829,0.692794 -1.750218,2.078383 h -3.06288 c 0.01823,-0.747489 0.145853,-1.431167 0.38286,-2.051036 0.237009,-0.6381 0.565175,-1.185043 0.984498,-1.640829 0.419323,-0.474017 0.920687,-0.8386455 1.504092,-1.0938855 0.601637,-0.25524 1.2762,-0.38286 2.023689,-0.38286 0.838646,0 1.567903,0.1367357 2.187771,0.4102072 0.619869,0.25524 1.130349,0.6198684 1.53144,1.0938853 0.401092,0.455786 0.692795,1.002729 0.875109,1.640829 0.200546,0.619869 0.300819,1.294432 0.30082,2.023689 z"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Monofonto;-inkscape-font-specification:Monofonto;fill:#666666;fill-opacity:1;stroke-width:3.4183929px"
+ id="path855" /></g><circle
+ style="fill:#666666"
+ cx="-26.38899"
+ cy="25.466606"
+ r="1.8"
+ id="circle6-6" /></g></g></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_dark/scalable/instance-settings.svg b/launcher/resources/pe_dark/scalable/instance-settings.svg
new file mode 100644
index 00000000..c9f701e7
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/instance-settings.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect y="0" fill="none" width="32" height="32"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20
+ C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<rect x="16" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8,18h6v6H8V18z M10,22h2v-2h-2V22z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8,8h6v6H8V8z M10,12h2v-2h-2V12z"/>
+<rect x="16" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/jarmods.svg b/launcher/resources/pe_dark/scalable/jarmods.svg
new file mode 100644
index 00000000..cb9a97ba
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/jarmods.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20
+ C32,29.3,29.3,32,26,32z"/>
+<path fill="#999999" d="M16.5,0.6l2.2,4.5l5,0.7l-3.6,3.5l0.8,4.9l-4.4-2.3l-4.4,2.3l0.8-4.9L9.4,5.8l5-0.7L16.5,0.6z"/>
+<g>
+ <g>
+ <path fill="#FFFFFF" d="M15.9,24.2c-2.9,0-5.3-0.8-5.5-1.8c-0.1-0.4,0.2-0.9,1.7-1.4l1.3-0.4l-1.2,0.7c-0.1,0.1-0.3,0.2-0.3,0.3
+ c0,0,0.1,0.3,1.2,0.4c1.2,0.1,2.1,0.2,2.8,0.2c1.2,0,2.3-0.1,4.1-0.5l0.1,0l0,0c0,0,0.8,0.5,1.9,0.9l0.4,0.1l-0.3,0.1
+ C19.7,24,17.4,24.2,15.9,24.2z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M15.3,20.4c-3.3,0-5.7-0.6-5.8-1.5c-0.1-0.5,0.6-1,1.9-1.5l0.8-0.3l-0.7,0.5c-0.1,0.1-0.4,0.4-0.4,0.6
+ c0,0.1,0.2,0.3,1.1,0.4c1.3,0.1,2.3,0.2,3.2,0.2c1.7,0,3.4-0.2,5.7-0.6l0.1,0l0.1,0.1c0,0,0.5,0.5,1.3,0.8l0.5,0.2l-0.5,0.1
+ C20.3,20,17.7,20.4,15.3,20.4z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M15.3,27.4C15.3,27.4,15.3,27.4,15.3,27.4c-3.1,0-6.3-0.3-9.4-0.8c-1.2-0.2-1.8-0.5-1.8-1
+ c0.1-1,3.1-2,4.7-2c0.4,0,0.7,0.1,0.9,0.2l0.6,0.4l-0.7-0.1c0,0-0.4-0.1-1.1,0.1c-1.6,0.5-2,0.8-2,0.9c0,0,0,0.1,0.2,0.2
+ c1.4,0.6,4.6,1,8.7,1c4.3,0,8.4-0.4,10.3-1c0.9-0.3,1.1-0.6,1.1-0.7c0-0.2-0.2-0.4-0.2-0.4l-0.9-0.8l1.1,0.5
+ c0.7,0.3,0.8,0.7,0.7,0.9C27.4,26,22.4,27.4,15.3,27.4z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M13.6,16.7c-3,0-6.7-0.3-6.9-1.2c-0.1-0.4,0.5-0.7,0.7-0.8c2.5-1.1,4.6-1.2,5.2-1.2c0.1,0,0.1,0,0.1,0l1,0
+ l-1,0.3c-1.8,0.4-3.4,1.1-3.4,1.4c0,0,0,0.2,1.2,0.4c0.8,0.1,1.9,0.1,3.1,0.1c1.6,0,3.4-0.1,5-0.2c2.5-0.2,5.1-0.7,5.1-0.7
+ l1.3-0.2L23.9,15c0,0-0.9,0.4-1.5,0.8l0,0C20.1,16.4,16.9,16.7,13.6,16.7z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M23.5,20.3v-0.2c0-0.1,0-0.3,0.2-0.3c3.8-2,3.8-3.7,3.7-4.3c-0.2-0.9-1.1-1.4-1.9-1.4c-0.1,0-0.2,0-0.3,0
+ c-0.5,0.1-0.7,0.2-0.7,0.2l-0.5,0.2l0.3-0.5c0,0,0.2-0.3,0.6-0.5c0.4-0.2,0.9-0.2,1.3-0.2c1.5,0,2.7,0.9,2.9,2.2
+ c0.2,1.5-1.1,3.5-5.4,4.7L23.5,20.3z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M16.9,29c-1.2,0-2.4,0-3.5-0.1c-3.8-0.2-4.8-0.8-4.8-1v-0.2l0.2,0c1.8,0.3,4.2,0.5,6.8,0.5
+ c3,0,5.9-0.3,8.4-0.7c4.4-0.9,4.9-1.9,4.9-1.9l0.3-0.8l-0.1,0.9c-0.1,1.2-1.5,2.1-4.1,2.6C22.9,28.7,20.1,29,16.9,29z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/java.svg b/launcher/resources/pe_dark/scalable/java.svg
new file mode 100644
index 00000000..9e1091fa
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/java.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path id="XMLID_1_" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z"/>
+</g>
+<path fill="none" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z"/>
+<g>
+ <path fill="#999999" d="M16.7,15.6l1-0.8c0.1-0.1,1.9-1.6,0.4-3.4c-1.3-1.6-1.8-3.1-1.4-4.5c1.1-3.4,7.2-4.9,7.4-5l1-0.2l-0.9,0.5
+ C20.7,4.3,19,5.8,18.8,7c-0.2,0.9,0.4,1.7,1.1,2.7l0.2,0.3c0.5,0.7,0.6,1.4,0.5,2.1C20.2,13.8,18,15,17.9,15L16.7,15.6z"/>
+</g>
+<g>
+ <g>
+ <path fill="#FFFFFF" d="M15.9,24.2c-2.9,0-5.3-0.8-5.5-1.8c-0.1-0.4,0.2-0.9,1.7-1.4l1.3-0.4l-1.2,0.7c-0.1,0.1-0.3,0.2-0.3,0.3
+ c0,0,0.1,0.3,1.2,0.4c1.2,0.1,2.1,0.2,2.8,0.2c1.2,0,2.3-0.1,4.1-0.5l0.1,0l0,0c0,0,0.8,0.5,1.9,0.9l0.4,0.1l-0.3,0.1
+ C19.7,24,17.4,24.2,15.9,24.2z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M15.3,20.4c-3.3,0-5.7-0.6-5.8-1.5c-0.1-0.5,0.6-1,1.9-1.5l0.8-0.3l-0.7,0.5c-0.1,0.1-0.4,0.4-0.4,0.6
+ c0,0.1,0.2,0.3,1.1,0.4c1.3,0.1,2.3,0.2,3.2,0.2c1.7,0,3.4-0.2,5.7-0.6l0.1,0l0.1,0.1c0,0,0.5,0.5,1.3,0.8l0.5,0.2l-0.5,0.1
+ C20.3,20,17.7,20.4,15.3,20.4z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M15.3,27.4C15.3,27.4,15.3,27.4,15.3,27.4c-3.1,0-6.3-0.3-9.4-0.8c-1.2-0.2-1.8-0.5-1.8-1
+ c0.1-1,3.1-2,4.7-2c0.4,0,0.7,0.1,0.9,0.2l0.6,0.4l-0.7-0.1c0,0-0.4-0.1-1.1,0.1c-1.6,0.5-2,0.8-2,0.9c0,0,0,0.1,0.2,0.2
+ c1.4,0.6,4.6,1,8.7,1c4.3,0,8.4-0.4,10.3-1c0.9-0.3,1.1-0.6,1.1-0.7c0-0.2-0.2-0.4-0.2-0.4l-0.9-0.8l1.1,0.5
+ c0.7,0.3,0.8,0.7,0.7,0.9C27.4,26,22.4,27.4,15.3,27.4z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M13.6,16.7c-3,0-6.7-0.3-6.9-1.2c-0.1-0.4,0.5-0.7,0.7-0.8c2.5-1.1,4.6-1.2,5.2-1.2c0.1,0,0.1,0,0.1,0l1,0
+ l-1,0.3c-1.8,0.4-3.4,1.1-3.4,1.4c0,0,0,0.2,1.2,0.4c0.8,0.1,1.9,0.1,3.1,0.1c1.6,0,3.4-0.1,5-0.2c2.5-0.2,5.1-0.7,5.1-0.7
+ l1.3-0.2L23.9,15c0,0-0.9,0.4-1.5,0.8l0,0C20.1,16.4,16.9,16.7,13.6,16.7z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M23.5,20.3v-0.2c0-0.1,0-0.3,0.2-0.3c3.8-2,3.8-3.7,3.7-4.3c-0.2-0.9-1.1-1.4-1.9-1.4c-0.1,0-0.2,0-0.3,0
+ c-0.5,0.1-0.7,0.2-0.7,0.2l-0.5,0.2l0.3-0.5c0,0,0.2-0.3,0.6-0.5c0.4-0.2,0.9-0.2,1.3-0.2c1.5,0,2.7,0.9,2.9,2.2
+ c0.2,1.5-1.1,3.5-5.4,4.7L23.5,20.3z"/>
+ </g>
+ <g>
+ <path fill="#FFFFFF" d="M16.9,29c-1.2,0-2.4,0-3.5-0.1c-3.8-0.2-4.8-0.8-4.8-1v-0.2l0.2,0c1.8,0.3,4.2,0.5,6.8,0.5
+ c3,0,5.9-0.3,8.4-0.7c4.4-0.9,4.9-1.9,4.9-1.9l0.3-0.8l-0.1,0.9c-0.1,1.2-1.5,2.1-4.1,2.6C22.9,28.7,20.1,29,16.9,29z"/>
+ </g>
+</g>
+<path fill="#999999" d="M16.8,4.2c1.8-1.4,3-2.8,3.7-4.2h-1.7c-0.8,1-1.9,1.8-2.9,2.7c-1.2,1-2.5,2-3.3,3.2c-1.6,2.6,1,5.2,4,8
+ l0.5,0.5l-0.3-0.6c-0.3-0.7-0.7-1.3-1.2-1.9C14.2,9.6,12.7,7.4,16.8,4.2z"/>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/language.svg b/launcher/resources/pe_dark/scalable/language.svg
new file mode 100644
index 00000000..1a9b4c5c
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/language.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ id="Calque_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 32 32"
+ enable-background="new 0 0 32 32"
+ xml:space="preserve"><metadata
+ id="metadata45"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs43" /><path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20 C32,29.3,29.3,32,26,32z"
+ id="path2" /><path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ fill="#F2F2F2"
+ d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
+ id="path4" /><g
+ id="g10" /><g
+ id="g12" /><g
+ id="g14" /><g
+ id="g16" /><g
+ id="g18" /><g
+ id="g20" /><g
+ id="g22" /><g
+ id="g24" /><g
+ id="g26" /><g
+ id="g28" /><g
+ id="g30" /><g
+ id="g32" /><g
+ id="g34" /><g
+ id="g36" /><g
+ id="g38" /><path
+ d="m 9.1897911,10.114301 c -0.48384,0 -0.901101,0.341924 -0.996278,0.815883 l -2.029452,10.155455 c -0.110057,0.55031 0.246085,1.083011 0.795381,1.193069 0.556201,0.113982 1.084021,-0.246085 1.193073,-0.79538 l 0.651882,-3.247122 h 2.8699319 l 0.647785,3.247122 c 0.110996,0.557485 0.658771,0.906193 1.197171,0.79538 0.549363,-0.110056 0.905437,-0.642759 0.795381,-1.193069 L 12.285213,10.930184 c -0.09517,-0.473959 -0.512431,-0.815883 -0.996274,-0.815883 z m 0.614982,2.287746 h 0.4345929 l 0.811779,4.063002 H 8.9929941 Z M 20.710512,9.7002137 c -0.561209,0 -1.016777,0.4578063 -1.016777,1.0208743 v 1.016774 h -3.046227 c -0.561205,0 -1.012673,0.457809 -1.012673,1.020876 0,0.563065 0.451466,1.016776 1.012673,1.016776 h 0.274696 c 0.577658,1.85973 1.425829,3.313572 2.37794,4.460692 -0.745862,0.684158 -1.478369,1.245052 -2.271343,1.873654 -0.437271,0.35118 -0.506831,0.995228 -0.155795,1.434967 0.349411,0.439057 0.990377,0.507857 1.426766,0.155794 0.861615,-0.682321 1.601995,-1.252055 2.410742,-1.996652 0.808748,0.744597 1.618828,1.314331 2.480441,1.996652 0.436393,0.352059 1.077353,0.283261 1.426766,-0.155794 0.351038,-0.439739 0.277375,-1.083787 -0.159884,-1.434967 -0.792979,-0.628602 -1.591082,-1.189496 -2.336946,-1.873654 0.952113,-1.147188 1.869915,-2.601029 2.447642,-4.460692 h 0.270592 c 0.561209,0 1.016774,-0.453711 1.016774,-1.016776 0,-0.563067 -0.455565,-1.020876 -1.016774,-1.020876 h -3.111838 v -1.016774 c 0,-0.563068 -0.455567,-1.0208743 -1.016775,-1.0208743 z m -1.668658,4.0753003 h 3.402916 c -0.438423,1.181552 -1.08891,2.13806 -1.734258,2.951929 -0.645345,-0.813869 -1.230238,-1.770309 -1.668658,-2.951929 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.29744864;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect977" /></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_dark/scalable/loadermods.svg b/launcher/resources/pe_dark/scalable/loadermods.svg
new file mode 100644
index 00000000..24226a09
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/loadermods.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M16,6.4l2.9,6l6.6,1l-4.8,4.6l1.1,6.5L16,21.4l-5.9,3.1l1.1-6.5
+ l-4.8-4.6l6.6-1L16,6.4z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20
+ C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/log.svg b/launcher/resources/pe_dark/scalable/log.svg
new file mode 100644
index 00000000..68686a7d
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/log.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M30,28c-3.1,0.1-8.9,0.7-12.1,2.3C17.8,31.3,17,32,16,32c-1,0-1.8-0.7-1.9-1.7
+ c-3.2-1.6-9-2.2-12.1-2.3c-1.1,0-2-0.9-2-2V2c0-1.1,0.9-2,2-2c0.1,0,0.1,0,0.2,0C16,0.4,16,4,16,4s0-3.6,13.8-4c0.1,0,0.1,0,0.2,0
+ c1.1,0,2,0.9,2,2v24C32,27.1,31.1,28,30,28z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M14,6.3C11.4,5,7.2,4.4,4,4.2v19.9c5.4,0.3,8.4,1.1,10,1.9V6.3z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,4.2C24.8,4.4,20.6,5,18,6.3V26c1.6-0.8,4.6-1.6,10-1.9V4.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/minecraft.svg b/launcher/resources/pe_dark/scalable/minecraft.svg
new file mode 100644
index 00000000..01baf575
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/minecraft.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" points="16,14.2 30,7.1 30,24.9 16,32 "/>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#666666" points="16,14.2 2,7.1 2,24.9 16,32 "/>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#999999" points="2,7.1 16,14.2 30,7.1 16,0 "/>
+ </g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/multimc.svg b/launcher/resources/pe_dark/scalable/multimc.svg
new file mode 100644
index 00000000..e4cf7b7f
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/multimc.svg
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="#666666" width="32" height="32"/>
+<polygon fill="#999999" points="0,5 1,5 1,4 4,4 4,5 8,5 8,8 9,8 9,2 12,2 12,5 13,5 13,4 16,4 16,8 17,8 17,6 18,6 18,5 20,5 20,8
+ 21,8 21,6 22,6 22,5 24,5 24,4 26,4 26,5 29,5 29,4 32,4 32,0 0,0 "/>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-963.8778" y1="47.5718" x2="-963.0319" y2="49.6501" gradientTransform="matrix(20.79 0 0 14.7315 20048.0879 -696.8257)">
+ <stop offset="0" style="stop-color:#000000;stop-opacity:0.8"/>
+ <stop offset="1" style="stop-color:#000000;stop-opacity:0.35"/>
+</linearGradient>
+<path fill="url(#SVGID_1_)" d="M18.5,21.5c-0.5-0.4-1-1-1.8-1.9c-1,1.2-1.9,2.1-2.8,2.7c-1.1,0.7-2.5,1-4.1,1c-2,0-3.6-0.5-4.9-1.6
+ c-1.4-1.1-2.1-2.6-2.1-4.4c0-1.7,0.7-3.2,2.1-4.4c1.2-1,2.9-1.6,5-1.6c1.1,0,2,0.2,2.8,0.5c0.9,0.3,1.7,0.8,2.3,1.4
+ c0.6,0.5,1.2,1.2,1.8,1.9c1-1.2,1.9-2.1,2.8-2.7c1.1-0.7,2.5-1,4.1-1c2,0,3.6,0.5,4.9,1.6c1.4,1.1,2.1,2.6,2.1,4.4
+ c0,1.7-0.7,3.2-2.1,4.4c-1.2,1-2.9,1.6-5,1.6c-1.1,0-2-0.2-2.8-0.5S19.2,22.1,18.5,21.5 M9.6,21.3c2.4,0,4.3-1.2,5.8-3.7
+ c-1.9-2.7-3.8-4.1-5.8-4.1c-1.5,0-2.6,0.4-3.3,1.1c-0.8,0.8-1.2,1.7-1.2,2.8c0,1.2,0.4,2.1,1.2,2.8C7.1,20.9,8.2,21.3,9.6,21.3
+ M23.9,13.5c-2.2,0-4.1,1.2-5.8,3.7c1.8,2.7,3.8,4.1,5.8,4.1c1.5,0,2.6-0.4,3.3-1.1c0.8-0.8,1.2-1.7,1.2-2.8c0-1.2-0.4-2.1-1.2-2.8
+ C26.3,13.8,25.2,13.5,23.9,13.5"/>
+<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-964.0289" y1="48.0195" x2="-962.7877" y2="48.9627" gradientTransform="matrix(20.79 0 0 14.7315 20047.7695 -696.964)">
+ <stop offset="0" style="stop-color:#999999"/>
+ <stop offset="1" style="stop-color:#999999;stop-opacity:0.6"/>
+</linearGradient>
+<path fill="url(#SVGID_2_)" d="M17.6,20.1c-0.5-0.4-1-1-1.8-1.9c-1,1.2-1.9,2.1-2.8,2.7c-1.1,0.7-2.5,1-4.1,1c-2,0-3.6-0.5-4.9-1.6
+ c-1.4-1.1-2.1-2.6-2.1-4.4c0-1.7,0.7-3.2,2.1-4.4c1.2-1,2.9-1.6,5-1.6c1.1,0,2,0.2,2.8,0.5c0.9,0.3,1.7,0.8,2.3,1.4
+ c0.6,0.5,1.2,1.2,1.8,1.9c1-1.2,1.9-2.1,2.8-2.7c1.1-0.7,2.5-1,4.1-1c2,0,3.6,0.5,4.9,1.6c1.4,1.1,2.1,2.6,2.1,4.4
+ c0,1.7-0.7,3.2-2.1,4.4c-1.2,1-2.9,1.6-5,1.6c-1.1,0-2-0.2-2.8-0.5C19.2,21.2,18.4,20.8,17.6,20.1 M8.7,19.9c2.4,0,4.3-1.2,5.8-3.7
+ c-1.9-2.7-3.8-4.1-5.8-4.1c-1.5,0-2.6,0.4-3.3,1.1C4.6,14,4.2,14.9,4.2,16c0,1.2,0.4,2.1,1.2,2.8C6.2,19.5,7.3,19.9,8.7,19.9
+ M23,12.1c-2.2,0-4.1,1.2-5.8,3.7c1.8,2.7,3.8,4.1,5.8,4.1c1.5,0,2.6-0.4,3.3-1.1c0.8-0.8,1.2-1.7,1.2-2.8c0-1.2-0.4-2.1-1.2-2.8
+ C25.5,12.5,24.4,12.1,23,12.1"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/new.svg b/launcher/resources/pe_dark/scalable/new.svg
new file mode 100644
index 00000000..0377aceb
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/new.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z M6,2C4.9,2,4,2.9,4,4s0.9,2,2,2
+ s2-0.9,2-2S7.1,2,6,2z M28,8H4v18c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V8z"/>
+<path fill="#666666" d="M20,20h-2v2c0,1.1-0.9,2-2,2s-2-0.9-2-2v-2h-2c-1.1,0-2-0.9-2-2s0.9-2,2-2h2v-2c0-1.1,0.9-2,2-2s2,0.9,2,2v2
+ h2c1.1,0,2,0.9,2,2S21.1,20,20,20z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/news.svg b/launcher/resources/pe_dark/scalable/news.svg
new file mode 100644
index 00000000..84979dcb
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/news.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<g>
+ <g>
+ <path d="M26,0H6C2.6,0,0,2.6,0,6v14c0,3.4,2.6,6,6,6l4,6l4-6h12c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z M16,20H6v-2h10V20z M26,16
+ H6v-2h20V16z M26,12H6v-2h20V12z M26,8H6V6h20V8z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/notes.svg b/launcher/resources/pe_dark/scalable/notes.svg
new file mode 100644
index 00000000..72649721
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/notes.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20
+ C32,29.3,29.3,32,26,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<g>
+ <rect x="8" y="16" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="12" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="16" height="2"/>
+ <rect x="8" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/patreon.svg b/launcher/resources/pe_dark/scalable/patreon.svg
new file mode 100644
index 00000000..01cb279a
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/patreon.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#666666" d="M16,0C7.2,0,0,7.2,0,16v16h15.3c0.2,0,0.4,0,0.7,0c8.8,0,16-7.2,16-16S24.8,0,16,0L16,0z"/>
+<path fill="#FFFFFF" d="M16,3.7C9.2,3.7,3.7,9.2,3.7,16v7.3v5V32h4.9V16c0-4.1,3.3-7.3,7.3-7.3s7.3,3.3,7.3,7.3
+ c0,4.1-3.3,7.3-7.3,7.3c-1.5,0-2.9-0.4-4-1.2v5.3c0.8,0.5,2.5,0.8,4.6,0.8C23.1,28,28.3,22.6,28.3,16C28.3,9.2,22.8,3.7,16,3.7
+ L16,3.7z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/proxy.svg b/launcher/resources/pe_dark/scalable/proxy.svg
new file mode 100644
index 00000000..98bcfac1
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/proxy.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9,4H11.6c-0.8-2.3-3-4-5.6-4C2.7,0,0,2.7,0,6s2.7,6,6,6c2.6,0,4.8-1.7,5.6-4
+ h10.3c1.1,0,2,0.9,2,2v1.4h4V10C27.9,6.7,25.2,4,21.9,4z M6,8C4.9,8,4,7.1,4,6s0.9-2,2-2c1.1,0,2,0.9,2,2S7.1,8,6,8z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M23.9,20.6V22c0,1.1-0.9,2-2,2H11.6c-0.8-2.3-3-4-5.6-4c-3.3,0-6,2.7-6,6
+ s2.7,6,6,6c2.6,0,4.8-1.7,5.6-4h10.3c3.3,0,6-2.7,6-6v-1.4H23.9z M6,28c-1.1,0-2-0.9-2-2s0.9-2,2-2c1.1,0,2,0.9,2,2S7.1,28,6,28z"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M32,22H20V10h12V22z M28,14h-4v4h4V14z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/quickmods.svg b/launcher/resources/pe_dark/scalable/quickmods.svg
new file mode 100644
index 00000000..346729f1
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/quickmods.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20
+ C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M10.6,24.2c-0.3,0-0.6-0.1-0.8-0.2C9.3,23.7,9,23.2,9,22.6V9.8
+ c0-0.6,0.3-1.1,0.8-1.4c0.3-0.2,0.5-0.2,0.8-0.2c0.2,0,0.5,0.1,0.7,0.2l12.8,6.4c0.5,0.3,0.9,0.8,0.9,1.4c0,0.6-0.3,1.2-0.9,1.4
+ L11.3,24C11.1,24.1,10.8,24.2,10.6,24.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/refresh.svg b/launcher/resources/pe_dark/scalable/refresh.svg
new file mode 100644
index 00000000..c227cd6c
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/refresh.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path d="M31.8,17.4c-0.4-0.9-1.2-1.4-2.1-1.4h-2.3c0-7.6-6.1-13.7-13.7-13.7C6.1,2.3,0,8.4,0,16c0,7.6,6.1,13.7,13.7,13.7
+ c2.5,0,4.8-0.7,6.8-1.8l-3.4-3.4c-1,0.4-2.2,0.7-3.4,0.7c-5,0-9.1-4.1-9.1-9.1s4.1-9.1,9.1-9.1c5.1,0,9.1,4.1,9.1,9.1h-2.3
+ c-0.9,0-1.8,0.6-2.1,1.4c-0.4,0.9-0.2,1.8,0.5,2.5l4.6,4.6c0.4,0.4,1,0.7,1.6,0.7c0.6,0,1.2-0.2,1.6-0.7l4.6-4.6
+ C32,19.2,32.2,18.3,31.8,17.4z"/>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/resourcepacks.svg b/launcher/resources/pe_dark/scalable/resourcepacks.svg
new file mode 100644
index 00000000..0db2beb1
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/resourcepacks.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,10c0-1.1-0.9-2-2-2H6c-1.1,0-2,0.9-2,2v16c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V10z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M26,4h-4V2c0-1.1-0.9-2-2-2h-8c-1.1,0-2,0.9-2,2v2H6c-3.3,0-6,2.7-6,6v16
+ c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V10C32,6.7,29.3,4,26,4z M12,3c0-0.6,0.4-1,1-1h6c0.6,0,1,0.4,1,1v1h-8V3z M6,8h20
+ c1.1,0,2,0.9,2,2v4H4v-4C4,8.9,4.9,8,6,8z M18,17c0,1.1-0.9,2-2,2s-2-0.9-2-2c0-0.4,0.1-0.7,0.3-1h3.4C17.9,16.3,18,16.6,18,17z
+ M26,28H6c-1.1,0-2-0.9-2-2V16h8.1c-0.1,0.3-0.1,0.7-0.1,1c0,2.2,1.8,4,4,4s4-1.8,4-4c0-0.3-0.1-0.7-0.1-1H28v10
+ C28,27.1,27.1,28,26,28z"/>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/screenshots.svg b/launcher/resources/pe_dark/scalable/screenshots.svg
new file mode 100644
index 00000000..2803b9aa
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/screenshots.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20
+ C32,29.3,29.3,32,26,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#666666" d="M24,26H8c-1.1,0-2-0.9-2-2v-0.7c0.8-1.7,1.5-3.8,2-4.8
+ c1.1-2.4,3.5,3.5,4.9,3.5c2.8,0,6.2-10.7,9.1-7.4c1.4,1.6,2.9,5.8,4,8.4V24C26,25.1,25.1,26,24,26z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#999999" d="M9,6c1.7,0,3,1.3,3,3c0,1.7-1.3,3-3,3c-1.7,0-3-1.3-3-3
+ C6,7.3,7.3,6,9,6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/settings.svg b/launcher/resources/pe_dark/scalable/settings.svg
new file mode 100644
index 00000000..c9f701e7
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/settings.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect y="0" fill="none" width="32" height="32"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20
+ C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<rect x="16" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8,18h6v6H8V18z M10,22h2v-2h-2V22z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8,8h6v6H8V8z M10,12h2v-2h-2V12z"/>
+<rect x="16" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#666666" width="8" height="2"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/status-bad.svg b/launcher/resources/pe_dark/scalable/status-bad.svg
new file mode 100644
index 00000000..f455965a
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/status-bad.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path d="M26,32H6c-3.4,0-6-2.6-6-6V6c0-3.4,2.6-6,6-6h20c3.4,0,6,2.6,6,6v20C32,29.4,29.4,32,26,32z"/>
+<path fill="#FFFFFF" d="M21.6,21.4c-0.8,0.8-2,0.8-2.8,0L16,18.6l-2.8,2.8c-0.8,0.8-2,0.8-2.8,0c-0.8-0.8-0.8-2,0-2.8l2.8-2.8
+ L10.4,13c-0.8-0.8-0.8-2,0-2.8c0.8-0.8,2-0.8,2.8,0L16,13l2.8-2.8c0.8-0.8,2-0.8,2.8,0s0.8,2,0,2.8l-2.8,2.8l2.8,2.8
+ C22.4,19.4,22.4,20.6,21.6,21.4z"/>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/status-good.svg b/launcher/resources/pe_dark/scalable/status-good.svg
new file mode 100644
index 00000000..4ba91f2d
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/status-good.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="none" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z"/>
+<path fill="none" d="M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2c0.8,0.8,2,0.8,2.8,0l8.4-8.4
+ c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+<path d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z"/>
+<path fill="#FFFFFF" d="M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2c0.8,0.8,2,0.8,2.8,0l8.4-8.4
+ c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/status-yellow.svg b/launcher/resources/pe_dark/scalable/status-yellow.svg
new file mode 100644
index 00000000..69133817
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/status-yellow.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="#E3A333" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z M28,26
+ c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+</g>
+<path d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z"/>
+<path d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+<g>
+ <path fill="#FFFFFF" d="M6,20c0-5.8,2.4-7.9,5.8-7.9c2,0,3.4,0.7,5.3,1.9c1.6,1,2.5,1.5,3.3,1.5c0.7,0,1.2-0.7,1.3-3.1H26
+ c0.2,6.1-2.3,8-5.5,8c-2,0-3.5-0.8-5.3-1.9c-1.7-1-2.6-1.6-3.3-1.6c-0.8,0-1.4,0.5-1.6,3.2H6z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/viewfolder.svg b/launcher/resources/pe_dark/scalable/viewfolder.svg
new file mode 100644
index 00000000..3af36240
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/viewfolder.svg
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect y="0" fill="none" width="32" height="32"/>
+<path d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h8c2.6,0,4.8,1.7,5.6,4H26c3.3,0,6,2.7,6,6v16C32,29.3,29.3,32,26,32z M28,10
+ c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_dark/scalable/worlds.svg b/launcher/resources/pe_dark/scalable/worlds.svg
new file mode 100644
index 00000000..2b01070f
--- /dev/null
+++ b/launcher/resources/pe_dark/scalable/worlds.svg
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata45"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs43" /><path
+ id="path2"
+ d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20 C32,29.3,29.3,32,26,32z"
+ clip-rule="evenodd"
+ fill-rule="evenodd" /><path
+ id="path4"
+ d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
+ fill="#F2F2F2"
+ clip-rule="evenodd"
+ fill-rule="evenodd" /><g
+ id="g10" /><g
+ id="g12" /><g
+ id="g14" /><g
+ id="g16" /><g
+ id="g18" /><g
+ id="g20" /><g
+ id="g22" /><g
+ id="g24" /><g
+ id="g26" /><g
+ id="g28" /><g
+ id="g30" /><g
+ id="g32" /><g
+ id="g34" /><g
+ id="g36" /><g
+ id="g38" /><g
+ style="stroke:#666666;stroke-opacity:1"
+ transform="rotate(29.970903,16,16)"
+ id="g881"><ellipse
+ ry="8.9473686"
+ rx="8.9473696"
+ cy="16"
+ cx="16"
+ id="path845"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><ellipse
+ ry="8.9473686"
+ rx="3.4915254"
+ cy="15.947369"
+ cx="16"
+ id="path845-3"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><path
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 8.1848294,12.430327 C 12.570183,14.790801 20.250652,13.785896 22.94338,12"
+ id="path870" /><path
+ id="path872"
+ d="M 8.2166335,20.492522 C 12.601987,18.132048 21.148735,17.776739 23.841463,19.562635"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#666666;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_light/index.theme b/launcher/resources/pe_light/index.theme
new file mode 100644
index 00000000..c106acc8
--- /dev/null
+++ b/launcher/resources/pe_light/index.theme
@@ -0,0 +1,11 @@
+[Icon Theme]
+Name=pe_light
+Comment=Icons by pexner (light)
+Inherits=multimc
+Directories=scalable
+
+[scalable]
+Size=48
+Type=Scalable
+MinSize=16
+MaxSize=256
diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc
new file mode 100644
index 00000000..6d77c835
--- /dev/null
+++ b/launcher/resources/pe_light/pe_light.qrc
@@ -0,0 +1,39 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/icons/pe_light">
+ <file>index.theme</file>
+ <file>scalable/about.svg</file>
+ <file>scalable/accounts.svg</file>
+ <file>scalable/bug.svg</file>
+ <file>scalable/centralmods.svg</file>
+ <file>scalable/checkupdate.svg</file>
+ <file>scalable/copy.svg</file>
+ <file>scalable/coremods.svg</file>
+ <file>scalable/externaltools.svg</file>
+ <file>scalable/help.svg</file>
+ <file>scalable/instance-settings.svg</file>
+ <file>scalable/jarmods.svg</file>
+ <file>scalable/java.svg</file>
+ <file>scalable/language.svg</file>
+ <file>scalable/loadermods.svg</file>
+ <file>scalable/log.svg</file>
+ <file>scalable/minecraft.svg</file>
+ <file>scalable/multimc.svg</file>
+ <file>scalable/new.svg</file>
+ <file>scalable/news.svg</file>
+ <file>scalable/notes.svg</file>
+ <file>scalable/patreon.svg</file>
+ <file>scalable/proxy.svg</file>
+ <file>scalable/quickmods.svg</file>
+ <file>scalable/refresh.svg</file>
+ <file>scalable/resourcepacks.svg</file>
+ <file>scalable/screenshots.svg</file>
+ <file>scalable/settings.svg</file>
+ <file>scalable/status-bad.svg</file>
+ <file>scalable/status-good.svg</file>
+ <file>scalable/status-yellow.svg</file>
+ <file>scalable/viewfolder.svg</file>
+ <file>scalable/worlds.svg</file>
+ </qresource>
+</RCC>
+
diff --git a/launcher/resources/pe_light/scalable/about.svg b/launcher/resources/pe_light/scalable/about.svg
new file mode 100644
index 00000000..8d00c32e
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/about.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <g>
+ <polygon fill="#F2F2F2" points="17.8,21 17.8,11.9 11.9,11.9 11.9,14.2 14.2,14.2 14.2,21 11.9,21 11.9,23.3 20.1,23.3 20.1,21
+ "/>
+ <circle fill="#F2F2F2" cx="16" cy="8.3" r="1.8"/>
+ </g>
+ <path fill="#F2F2F2" d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16C32,24.8,24.8,32,16,32L16,32z M16,4
+ C9.4,4,4,9.4,4,16s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/accounts.svg b/launcher/resources/pe_light/scalable/accounts.svg
new file mode 100644
index 00000000..3a092d03
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/accounts.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#999999" d="M25.8,17.5c1.4-1.4,2.2-3.4,2.2-5.5V8c0-4.4-3.6-8-8-8
+ c-0.6,0-1.2,0.1-1.8,0.2c1.3,1.1,2.4,2.4,3,4C22.8,4.7,24,6.2,24,8v4c0,1.8-1.2,3.3-2.8,3.8c-0.2,0.4-0.4,0.8-0.6,1.2
+ c1.5,1.2,2.8,2.6,3.7,4.2c1.8,1.1,3.1,2.7,3.6,4.7c0,1.1-0.9,2-2,2h-0.3c-0.4,1.6-1.3,3-2.5,4H26c3.3,0,6-2.7,6-6
+ C31.4,22.3,28.9,19.2,25.8,17.5z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M18,32H6c-3.3,0-6-2.7-6-6l0,0c0.6-3.7,3.1-6.8,6.2-8.5
+ C4.9,16.1,4,14.1,4,12V8c0-4.4,3.6-8,8-8s8,3.6,8,8v4c0,2.1-0.9,4.1-2.2,5.5c3.1,1.7,5.6,4.8,6.2,8.5l0,0C24,29.3,21.3,32,18,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M16,12V8c0-2.2-1.8-4-4-4S8,5.8,8,8v4c0,2.2,1.8,4,4,4
+ S16,14.2,16,12z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M20,26c-0.9-3.4-4.3-6-8-6l0,0l0,0c-3.7,0-7.1,2.6-8,6l0,0
+ c0,1.1,0.9,2,2,2h12C19.1,28,20,27.1,20,26L20,26z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/bug.svg b/launcher/resources/pe_light/scalable/bug.svg
new file mode 100644
index 00000000..ccb64bc5
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/bug.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect x="0" fill="none" width="32" height="32"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#CCCCCC" d="M22.7,5.3c-0.3-1.2-0.9-2.3-1.8-3.2c-2.7-2.8-7.2-2.8-9.9,0
+ c-0.9,0.9-1.5,2-1.8,3.2C13.5,3.3,18.5,3.3,22.7,5.3z"/>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M30,17.9h-4.1c0-0.7,0.1-1.3,0.1-2c0-0.7,0-1.4-0.1-2
+ c0,0,0,0,0.1,0v0h2c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-2v0c-0.3,0-0.6,0.1-0.8,0.1c-1.2-3.1-3.9-4.1-9.2-4.1s-7.9,1-9.2,4.1
+ C6.6,9.9,6.3,9.8,6,9.8v0H4c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h2v0c0,0,0,0,0.1,0c0,0.6-0.1,1.3-0.1,2c0,0.7,0,1.3,0.1,2H2
+ c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h4.5c0.3,1.4,0.7,2.8,1.2,4H6c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h2v0c0.7,0,1.4-0.1,2.1-0.3
+ c1.5,1.4,3.4,2.3,5.9,2.3s4.4-0.9,5.9-2.3c0.7,0.2,1.4,0.3,2.1,0.3v0h2c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-1.7
+ c0.5-1.2,0.9-2.6,1.2-4H30c1.1,0,2-0.9,2-2C32,18.8,31.1,17.9,30,17.9z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/centralmods.svg b/launcher/resources/pe_light/scalable/centralmods.svg
new file mode 100644
index 00000000..050fdc58
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/centralmods.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h8c2.6,0,4.8,1.7,5.6,4H26c3.3,0,6,2.7,6,6v16
+ C32,29.3,29.3,32,26,32z M28,10c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z
+ "/>
+<path fill="#CCCCCC" d="M16,10.2l2.2,4.5l5,0.7L19.6,19l0.8,4.9L16,21.6l-4.4,2.3l0.8-4.9l-3.6-3.5l5-0.7L16,10.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/checkupdate.svg b/launcher/resources/pe_light/scalable/checkupdate.svg
new file mode 100644
index 00000000..08b8dcd5
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/checkupdate.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#CCCCCC" d="M26,24h-0.4c0-0.1-0.1-0.2-0.1-0.3c-0.7-1.6-1.5-2.8-3.5-3.4V18c0-3.3-2.7-6-6-6c-3.3,0-6,2.7-6,6v2.3
+ c-2,0.6-2.9,1.8-3.5,3.4c0,0,0,0.1,0,0.1C2.8,23.1,0,19.9,0,16v-2c0-4.4,3.6-8,8-8c0.8,0,1.5,0.1,2.2,0.3C10.9,2.7,14.2,0,18,0
+ c4.4,0,8,3.6,8,8c0,0.7-0.1,1.4-0.3,2c0.1,0,0.2,0,0.3,0c3.3,0,6,2.7,6,6v2C32,21.3,29.3,24,26,24z"/>
+<path fill="#F2F2F2" d="M12,24h2v-6c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2v6h2c0.8,0,1.5,0.5,1.8,1.2c0.1,0.2,0.1,0.4,0.1,0.6
+ c0.1,0.6-0.1,1.2-0.6,1.6l-4,4C17,31.8,16.5,32,16,32c-0.5,0-1-0.2-1.4-0.6l-4-4c-0.4-0.4-0.6-1-0.6-1.6c0-0.2,0.1-0.4,0.1-0.6
+ C10.5,24.5,11.2,24,12,24z"/>
+<rect fill="none" width="32" height="32"/>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/copy.svg b/launcher/resources/pe_light/scalable/copy.svg
new file mode 100644
index 00000000..abdcce09
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/copy.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#CCCCCC" d="M28,25.6V10c0-3.3-2.7-6-6-6H6.4c0.8-2.3,3-4,5.6-4h14c3.3,0,6,2.7,6,6v14C32,22.6,30.3,24.8,28,25.6z"/>
+<path fill="#F2F2F2" d="M26,12v14c0,3.3-2.7,6-6,6H6c-3.3,0-6-2.7-6-6V12c0-3.3,2.7-6,6-6h14C23.3,6,26,8.7,26,12z M4,26
+ c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V14H4V26z M6,8c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S7.1,8,6,8z"/>
+<rect fill="none" width="32" height="32"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/coremods.svg b/launcher/resources/pe_light/scalable/coremods.svg
new file mode 100644
index 00000000..c8fb0eb9
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/coremods.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<circle fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" cx="16" cy="16" r="12"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16
+ C32,24.8,24.8,32,16,32z M16,4C9.4,4,4,9.4,4,16c0,6.6,5.4,12,12,12s12-5.4,12-12C28,9.4,22.6,4,16,4z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M16,6l2.9,6l6.6,1l-4.8,4.6l1.1,6.5L16,21l-5.9,3.1l1.1-6.5
+ l-4.8-4.6l6.6-1L16,6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/externaltools.svg b/launcher/resources/pe_light/scalable/externaltools.svg
new file mode 100644
index 00000000..4d232bcf
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/externaltools.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M6,32h20c3.3,0,6-2.7,6-6V6c0-3.3-2.7-6-6-6h-9.1
+ C17.6,1.2,18,2.6,18,4h8c1.1,0,2,0.9,2,2v20c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2v-4.5l-4-3V26C0,29.3,2.7,32,6,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M8.8,17.6C9.2,17.9,9.6,18,10,18c0.3,0,0.6-0.1,0.9-0.2
+ c0.7-0.3,1.1-1,1.1-1.8v-1.9V14c6.3,0,11.7,4.2,13.4,10c0.4-1.3,0.6-2.6,0.6-4c0-7.7-6.3-14-14-14V4c0-0.8-0.4-1.5-1.1-1.8
+ C10.6,2.1,10.3,2,10,2C9.6,2,9.2,2.1,8.8,2.4l-8,6C0.3,8.8,0,9.4,0,10c0,0.6,0.3,1.2,0.8,1.6L8.8,17.6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/help.svg b/launcher/resources/pe_light/scalable/help.svg
new file mode 100644
index 00000000..f820c679
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/help.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata17"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs15" /><g
+ id="g10"><path
+ id="path8"
+ d="M16,32C7.2,32,0,24.8,0,16C0,7.2,7.2,0,16,0c8.8,0,16,7.2,16,16C32,24.8,24.8,32,16,32L16,32z M16,4 C9.4,4,4,9.4,4,16s5.4,12,12,12s12-5.4,12-12S22.6,4,16,4z"
+ fill="#F2F2F2" /></g><g
+ style="fill:#f2f2f2;fill-opacity:1"
+ transform="translate(41.863574,-2.0092638)"
+ id="g861"><g
+ aria-label="?"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:27.34714317px;line-height:85.45981598px;font-family:'Nimbus Sans L';-inkscape-font-specification:'Nimbus Sans L';letter-spacing:0px;word-spacing:0px;fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-width:3.4183929px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text832"><path
+ d="m -20.968435,13.920532 c 0,0.6381 -0.07293,1.239737 -0.218777,1.804911 -0.145852,0.565174 -0.401092,1.066538 -0.76572,1.504093 -0.401092,0.492249 -0.774836,0.902456 -1.121233,1.230621 -0.346397,0.328166 -0.656332,0.6381 -0.929803,0.929803 -0.25524,0.273471 -0.464902,0.556059 -0.628985,0.847762 -0.145851,0.291703 -0.218777,0.656331 -0.218777,1.093885 v 0.38286 h -3.144921 V 20.72997 c 0,-0.601637 0.136736,-1.221505 0.410207,-1.859605 0.273471,-0.656332 0.756604,-1.285316 1.449399,-1.886953 0.346397,-0.309935 0.638099,-0.583406 0.875108,-0.820415 0.237009,-0.25524 0.428439,-0.492248 0.57429,-0.711025 0.145851,-0.237009 0.246124,-0.483133 0.300819,-0.738373 0.05469,-0.273471 0.08204,-0.57429 0.08204,-0.902456 0,-0.528711 -0.12762,-0.984497 -0.38286,-1.367357 -0.237009,-0.401091 -0.656331,-0.601637 -1.257968,-0.601637 -1.057423,0 -1.640829,0.692794 -1.750218,2.078383 h -3.06288 c 0.01823,-0.747489 0.145853,-1.431167 0.38286,-2.051036 0.237009,-0.6381 0.565175,-1.185043 0.984498,-1.640829 0.419323,-0.474017 0.920687,-0.8386455 1.504092,-1.0938855 0.601637,-0.25524 1.2762,-0.38286 2.023689,-0.38286 0.838646,0 1.567903,0.1367357 2.187771,0.4102072 0.619869,0.25524 1.130349,0.6198684 1.53144,1.0938853 0.401092,0.455786 0.692795,1.002729 0.875109,1.640829 0.200546,0.619869 0.300819,1.294432 0.30082,2.023689 z"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Monofonto;-inkscape-font-specification:Monofonto;fill:#f2f2f2;fill-opacity:1;stroke-width:3.4183929px"
+ id="path855" /></g><circle
+ style="fill:#f2f2f2;fill-opacity:1"
+ cx="-26.38899"
+ cy="25.466606"
+ r="1.8"
+ id="circle6-6" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_light/scalable/instance-settings.svg b/launcher/resources/pe_light/scalable/instance-settings.svg
new file mode 100644
index 00000000..83b92a52
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/instance-settings.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect y="0" fill="none" width="32" height="32"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<rect x="16" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#CCCCCC" width="8" height="2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M8,18h6v6H8V18z M10,22h2v-2h-2V22z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M8,8h6v6H8V8z M10,12h2v-2h-2V12z"/>
+<rect x="16" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#CCCCCC" width="8" height="2"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/jarmods.svg b/launcher/resources/pe_light/scalable/jarmods.svg
new file mode 100644
index 00000000..9852c805
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/jarmods.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill="#4D4D4D" d="M16.5,0.6l2.2,4.5l5,0.7l-3.6,3.5l0.8,4.9l-4.4-2.3l-4.4,2.3l0.8-4.9L9.4,5.8l5-0.7L16.5,0.6z"/>
+<g>
+ <g>
+ <path fill="#666666" d="M15.9,24.2c-2.9,0-5.3-0.8-5.5-1.8c-0.1-0.4,0.2-0.9,1.7-1.4l1.3-0.4l-1.2,0.7c-0.1,0.1-0.3,0.2-0.3,0.3
+ c0,0,0.1,0.3,1.2,0.4c1.2,0.1,2.1,0.2,2.8,0.2c1.2,0,2.3-0.1,4.1-0.5l0.1,0l0,0c0,0,0.8,0.5,1.9,0.9l0.4,0.1l-0.3,0.1
+ C19.7,24,17.4,24.2,15.9,24.2z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M15.3,20.4c-3.3,0-5.7-0.6-5.8-1.5c-0.1-0.5,0.6-1,1.9-1.5l0.8-0.3l-0.7,0.5c-0.1,0.1-0.4,0.4-0.4,0.6
+ c0,0.1,0.2,0.3,1.1,0.4c1.3,0.1,2.3,0.2,3.2,0.2c1.7,0,3.4-0.2,5.7-0.6l0.1,0l0.1,0.1c0,0,0.5,0.5,1.3,0.8l0.5,0.2l-0.5,0.1
+ C20.3,20,17.7,20.4,15.3,20.4z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M15.3,27.4C15.3,27.4,15.3,27.4,15.3,27.4c-3.1,0-6.3-0.3-9.4-0.8c-1.2-0.2-1.8-0.5-1.8-1
+ c0.1-1,3.1-2,4.7-2c0.4,0,0.7,0.1,0.9,0.2l0.6,0.4l-0.7-0.1c0,0-0.4-0.1-1.1,0.1c-1.6,0.5-2,0.8-2,0.9c0,0,0,0.1,0.2,0.2
+ c1.4,0.6,4.6,1,8.7,1c4.3,0,8.4-0.4,10.3-1c0.9-0.3,1.1-0.6,1.1-0.7c0-0.2-0.2-0.4-0.2-0.4l-0.9-0.8l1.1,0.5
+ c0.7,0.3,0.8,0.7,0.7,0.9C27.4,26,22.4,27.4,15.3,27.4z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M13.6,16.7c-3,0-6.7-0.3-6.9-1.2c-0.1-0.4,0.5-0.7,0.7-0.8c2.5-1.1,4.6-1.2,5.2-1.2c0.1,0,0.1,0,0.1,0l1,0
+ l-1,0.3c-1.8,0.4-3.4,1.1-3.4,1.4c0,0,0,0.2,1.2,0.4c0.8,0.1,1.9,0.1,3.1,0.1c1.6,0,3.4-0.1,5-0.2c2.5-0.2,5.1-0.7,5.1-0.7
+ l1.3-0.2L23.9,15c0,0-0.9,0.4-1.5,0.8l0,0C20.1,16.4,16.9,16.7,13.6,16.7z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M23.5,20.3v-0.2c0-0.1,0-0.3,0.2-0.3c3.8-2,3.8-3.7,3.7-4.3c-0.2-0.9-1.1-1.4-1.9-1.4c-0.1,0-0.2,0-0.3,0
+ c-0.5,0.1-0.7,0.2-0.7,0.2l-0.5,0.2l0.3-0.5c0,0,0.2-0.3,0.6-0.5c0.4-0.2,0.9-0.2,1.3-0.2c1.5,0,2.7,0.9,2.9,2.2
+ c0.2,1.5-1.1,3.5-5.4,4.7L23.5,20.3z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M16.9,29c-1.2,0-2.4,0-3.5-0.1c-3.8-0.2-4.8-0.8-4.8-1v-0.2l0.2,0c1.8,0.3,4.2,0.5,6.8,0.5
+ c3,0,5.9-0.3,8.4-0.7c4.4-0.9,4.9-1.9,4.9-1.9l0.3-0.8l-0.1,0.9c-0.1,1.2-1.5,2.1-4.1,2.6C22.9,28.7,20.1,29,16.9,29z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/java.svg b/launcher/resources/pe_light/scalable/java.svg
new file mode 100644
index 00000000..0584058a
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/java.svg
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path id="XMLID_2_" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z"
+ />
+</g>
+<path fill="none" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z"/>
+<g>
+ <path fill="#4D4D4D" d="M16.7,15.6l1-0.8c0.1-0.1,1.9-1.6,0.4-3.4c-1.3-1.6-1.8-3.1-1.4-4.5c1.1-3.4,7.2-4.9,7.4-5l1-0.2l-0.9,0.5
+ C20.7,4.3,19,5.8,18.8,7c-0.2,0.9,0.4,1.7,1.1,2.7l0.2,0.3c0.5,0.7,0.6,1.4,0.5,2.1C20.2,13.8,18,15,17.9,15L16.7,15.6z"/>
+</g>
+<g>
+ <g>
+ <path fill="#666666" d="M15.9,24.2c-2.9,0-5.3-0.8-5.5-1.8c-0.1-0.4,0.2-0.9,1.7-1.4l1.3-0.4l-1.2,0.7c-0.1,0.1-0.3,0.2-0.3,0.3
+ c0,0,0.1,0.3,1.2,0.4c1.2,0.1,2.1,0.2,2.8,0.2c1.2,0,2.3-0.1,4.1-0.5l0.1,0l0,0c0,0,0.8,0.5,1.9,0.9l0.4,0.1l-0.3,0.1
+ C19.7,24,17.4,24.2,15.9,24.2z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M15.3,20.4c-3.3,0-5.7-0.6-5.8-1.5c-0.1-0.5,0.6-1,1.9-1.5l0.8-0.3l-0.7,0.5c-0.1,0.1-0.4,0.4-0.4,0.6
+ c0,0.1,0.2,0.3,1.1,0.4c1.3,0.1,2.3,0.2,3.2,0.2c1.7,0,3.4-0.2,5.7-0.6l0.1,0l0.1,0.1c0,0,0.5,0.5,1.3,0.8l0.5,0.2l-0.5,0.1
+ C20.3,20,17.7,20.4,15.3,20.4z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M15.3,27.4C15.3,27.4,15.3,27.4,15.3,27.4c-3.1,0-6.3-0.3-9.4-0.8c-1.2-0.2-1.8-0.5-1.8-1
+ c0.1-1,3.1-2,4.7-2c0.4,0,0.7,0.1,0.9,0.2l0.6,0.4l-0.7-0.1c0,0-0.4-0.1-1.1,0.1c-1.6,0.5-2,0.8-2,0.9c0,0,0,0.1,0.2,0.2
+ c1.4,0.6,4.6,1,8.7,1c4.3,0,8.4-0.4,10.3-1c0.9-0.3,1.1-0.6,1.1-0.7c0-0.2-0.2-0.4-0.2-0.4l-0.9-0.8l1.1,0.5
+ c0.7,0.3,0.8,0.7,0.7,0.9C27.4,26,22.4,27.4,15.3,27.4z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M13.6,16.7c-3,0-6.7-0.3-6.9-1.2c-0.1-0.4,0.5-0.7,0.7-0.8c2.5-1.1,4.6-1.2,5.2-1.2c0.1,0,0.1,0,0.1,0l1,0
+ l-1,0.3c-1.8,0.4-3.4,1.1-3.4,1.4c0,0,0,0.2,1.2,0.4c0.8,0.1,1.9,0.1,3.1,0.1c1.6,0,3.4-0.1,5-0.2c2.5-0.2,5.1-0.7,5.1-0.7
+ l1.3-0.2L23.9,15c0,0-0.9,0.4-1.5,0.8l0,0C20.1,16.4,16.9,16.7,13.6,16.7z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M23.5,20.3v-0.2c0-0.1,0-0.3,0.2-0.3c3.8-2,3.8-3.7,3.7-4.3c-0.2-0.9-1.1-1.4-1.9-1.4c-0.1,0-0.2,0-0.3,0
+ c-0.5,0.1-0.7,0.2-0.7,0.2l-0.5,0.2l0.3-0.5c0,0,0.2-0.3,0.6-0.5c0.4-0.2,0.9-0.2,1.3-0.2c1.5,0,2.7,0.9,2.9,2.2
+ c0.2,1.5-1.1,3.5-5.4,4.7L23.5,20.3z"/>
+ </g>
+ <g>
+ <path fill="#666666" d="M16.9,29c-1.2,0-2.4,0-3.5-0.1c-3.8-0.2-4.8-0.8-4.8-1v-0.2l0.2,0c1.8,0.3,4.2,0.5,6.8,0.5
+ c3,0,5.9-0.3,8.4-0.7c4.4-0.9,4.9-1.9,4.9-1.9l0.3-0.8l-0.1,0.9c-0.1,1.2-1.5,2.1-4.1,2.6C22.9,28.7,20.1,29,16.9,29z"/>
+ </g>
+</g>
+<path fill="#4D4D4D" d="M16.8,4.2c1.8-1.4,3-2.8,3.7-4.2h-1.7c-0.8,1-1.9,1.8-2.9,2.7c-1.2,1-2.5,2-3.3,3.2c-1.6,2.6,1,5.2,4,8
+ l0.5,0.5l-0.3-0.6c-0.3-0.7-0.7-1.3-1.2-1.9C14.2,9.6,12.7,7.4,16.8,4.2z"/>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/language.svg b/launcher/resources/pe_light/scalable/language.svg
new file mode 100644
index 00000000..57d5e3de
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/language.svg
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata43"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs41" />
+<path
+ id="path2"
+ d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
+ fill="#4D4D4D"
+ clip-rule="evenodd"
+ fill-rule="evenodd" />
+
+<path
+ id="path6"
+ d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6 v20C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"
+ fill="#F2F2F2"
+ clip-rule="evenodd"
+ fill-rule="evenodd" />
+<g
+ id="g8">
+</g>
+<g
+ id="g10">
+</g>
+<g
+ id="g12">
+</g>
+<g
+ id="g14">
+</g>
+<g
+ id="g16">
+</g>
+<g
+ id="g18">
+</g>
+<g
+ id="g20">
+</g>
+<g
+ id="g22">
+</g>
+<g
+ id="g24">
+</g>
+<g
+ id="g26">
+</g>
+<g
+ id="g28">
+</g>
+<g
+ id="g30">
+</g>
+<g
+ id="g32">
+</g>
+<g
+ id="g34">
+</g>
+<g
+ id="g36">
+</g>
+<path
+ d="m 9.1897911,10.114301 c -0.48384,0 -0.901101,0.341924 -0.996278,0.815883 l -2.029452,10.155455 c -0.110057,0.55031 0.246085,1.083011 0.795381,1.193069 0.556201,0.113982 1.084021,-0.246085 1.193073,-0.79538 l 0.651882,-3.247122 h 2.8699319 l 0.647785,3.247122 c 0.110996,0.557485 0.658771,0.906193 1.197171,0.79538 0.549363,-0.110056 0.905437,-0.642759 0.795381,-1.193069 L 12.285213,10.930184 c -0.09517,-0.473959 -0.512431,-0.815883 -0.996274,-0.815883 z m 0.614982,2.287746 h 0.4345929 l 0.811779,4.063002 H 8.9929941 Z M 20.710512,9.7002137 c -0.561209,0 -1.016777,0.4578063 -1.016777,1.0208743 v 1.016774 h -3.046227 c -0.561205,0 -1.012673,0.457809 -1.012673,1.020876 0,0.563065 0.451466,1.016776 1.012673,1.016776 h 0.274696 c 0.577658,1.85973 1.425829,3.313572 2.37794,4.460692 -0.745862,0.684158 -1.478369,1.245052 -2.271343,1.873654 -0.437271,0.35118 -0.506831,0.995228 -0.155795,1.434967 0.349411,0.439057 0.990377,0.507857 1.426766,0.155794 0.861615,-0.682321 1.601995,-1.252055 2.410742,-1.996652 0.808748,0.744597 1.618828,1.314331 2.480441,1.996652 0.436393,0.352059 1.077353,0.283261 1.426766,-0.155794 0.351038,-0.439739 0.277375,-1.083787 -0.159884,-1.434967 -0.792979,-0.628602 -1.591082,-1.189496 -2.336946,-1.873654 0.952113,-1.147188 1.869915,-2.601029 2.447642,-4.460692 h 0.270592 c 0.561209,0 1.016774,-0.453711 1.016774,-1.016776 0,-0.563067 -0.455565,-1.020876 -1.016774,-1.020876 h -3.111838 v -1.016774 c 0,-0.563068 -0.455567,-1.0208743 -1.016775,-1.0208743 z m -1.668658,4.0753003 h 3.402916 c -0.438423,1.181552 -1.08891,2.13806 -1.734258,2.951929 -0.645345,-0.813869 -1.230238,-1.770309 -1.668658,-2.951929 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.29744864;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect977" /></svg> \ No newline at end of file
diff --git a/launcher/resources/pe_light/scalable/loadermods.svg b/launcher/resources/pe_light/scalable/loadermods.svg
new file mode 100644
index 00000000..913c1968
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/loadermods.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M16,6.4l2.9,6l6.6,1l-4.8,4.6l1.1,6.5L16,21.4l-5.9,3.1l1.1-6.5
+ l-4.8-4.6l6.6-1L16,6.4z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/log.svg b/launcher/resources/pe_light/scalable/log.svg
new file mode 100644
index 00000000..82282ca4
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/log.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M30,28c-3.1,0.1-8.9,0.7-12.1,2.3C17.8,31.3,17,32,16,32
+ c-1,0-1.8-0.7-1.9-1.7c-3.2-1.6-9-2.2-12.1-2.3c-1.1,0-2-0.9-2-2V2c0-1.1,0.9-2,2-2c0.1,0,0.1,0,0.2,0C16,0.4,16,4,16,4
+ s0-3.6,13.8-4c0.1,0,0.1,0,0.2,0c1.1,0,2,0.9,2,2v24C32,27.1,31.1,28,30,28z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M14,6.3C11.4,5,7.2,4.4,4,4.2v19.9c5.4,0.3,8.4,1.1,10,1.9V6.3z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M28,4.2C24.8,4.4,20.6,5,18,6.3V26c1.6-0.8,4.6-1.6,10-1.9V4.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/minecraft.svg b/launcher/resources/pe_light/scalable/minecraft.svg
new file mode 100644
index 00000000..d772111f
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/minecraft.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill-rule="evenodd" clip-rule="evenodd" fill="none" width="32" height="32"/>
+<g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#999999" points="16,14.2 30,7.1 30,24.9 16,32 "/>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#CCCCCC" points="16,14.2 2,7.1 2,24.9 16,32 "/>
+ <g>
+ <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" points="2,7.1 16,14.2 30,7.1 16,0 "/>
+ </g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/multimc.svg b/launcher/resources/pe_light/scalable/multimc.svg
new file mode 100644
index 00000000..8b2cb631
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/multimc.svg
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="#999999" width="32" height="32"/>
+<polygon fill="#F2F2F2" points="0,5 1,5 1,4 4,4 4,5 8,5 8,8 9,8 9,2 12,2 12,5 13,5 13,4 16,4 16,8 17,8 17,6 18,6 18,5 20,5 20,8
+ 21,8 21,6 22,6 22,5 24,5 24,4 26,4 26,5 29,5 29,4 32,4 32,0 0,0 "/>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="2.8" y1="17.35" x2="30.7" y2="17.35">
+ <stop offset="0" style="stop-color:#000000;stop-opacity:0.8"/>
+ <stop offset="1" style="stop-color:#000000;stop-opacity:0.35"/>
+</linearGradient>
+<path fill="url(#SVGID_1_)" d="M18.5,21.5c-0.5-0.4-1-1-1.8-1.9c-1,1.2-1.9,2.1-2.8,2.7c-1.1,0.7-2.5,1-4.1,1c-2,0-3.6-0.5-4.9-1.6
+ c-1.4-1.1-2.1-2.6-2.1-4.4c0-1.7,0.7-3.2,2.1-4.4c1.2-1,2.9-1.6,5-1.6c1.1,0,2,0.2,2.8,0.5c0.9,0.3,1.7,0.8,2.3,1.4
+ c0.6,0.5,1.2,1.2,1.8,1.9c1-1.2,1.9-2.1,2.8-2.7c1.1-0.7,2.5-1,4.1-1c2,0,3.6,0.5,4.9,1.6c1.4,1.1,2.1,2.6,2.1,4.4
+ c0,1.7-0.7,3.2-2.1,4.4c-1.2,1-2.9,1.6-5,1.6c-1.1,0-2-0.2-2.8-0.5S19.2,22.1,18.5,21.5 M9.6,21.3c2.4,0,4.3-1.2,5.8-3.7
+ c-1.9-2.7-3.8-4.1-5.8-4.1c-1.5,0-2.6,0.4-3.3,1.1c-0.8,0.8-1.2,1.7-1.2,2.8c0,1.2,0.4,2.1,1.2,2.8C7.1,20.9,8.2,21.3,9.6,21.3
+ M23.9,13.5c-2.2,0-4.1,1.2-5.8,3.7c1.8,2.7,3.8,4.1,5.8,4.1c1.5,0,2.6-0.4,3.3-1.1c0.8-0.8,1.2-1.7,1.2-2.8c0-1.2-0.4-2.1-1.2-2.8
+ C26.3,13.8,25.2,13.5,23.9,13.5"/>
+<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-964.0289" y1="48.0195" x2="-962.7877" y2="48.9627" gradientTransform="matrix(20.79 0 0 14.7315 20047.7695 -696.964)">
+ <stop offset="0" style="stop-color:#CCCCCC"/>
+ <stop offset="1" style="stop-color:#F2F2F2;stop-opacity:0.6"/>
+</linearGradient>
+<path fill="url(#SVGID_2_)" d="M17.6,20.1c-0.5-0.4-1-1-1.8-1.9c-1,1.2-1.9,2.1-2.8,2.7c-1.1,0.7-2.5,1-4.1,1c-2,0-3.6-0.5-4.9-1.6
+ c-1.4-1.1-2.1-2.6-2.1-4.4c0-1.7,0.7-3.2,2.1-4.4c1.2-1,2.9-1.6,5-1.6c1.1,0,2,0.2,2.8,0.5c0.9,0.3,1.7,0.8,2.3,1.4
+ c0.6,0.5,1.2,1.2,1.8,1.9c1-1.2,1.9-2.1,2.8-2.7c1.1-0.7,2.5-1,4.1-1c2,0,3.6,0.5,4.9,1.6c1.4,1.1,2.1,2.6,2.1,4.4
+ c0,1.7-0.7,3.2-2.1,4.4c-1.2,1-2.9,1.6-5,1.6c-1.1,0-2-0.2-2.8-0.5C19.2,21.2,18.4,20.8,17.6,20.1 M8.7,19.9c2.4,0,4.3-1.2,5.8-3.7
+ c-1.9-2.7-3.8-4.1-5.8-4.1c-1.5,0-2.6,0.4-3.3,1.1C4.6,14,4.2,14.9,4.2,16c0,1.2,0.4,2.1,1.2,2.8C6.2,19.5,7.3,19.9,8.7,19.9
+ M23,12.1c-2.2,0-4.1,1.2-5.8,3.7c1.8,2.7,3.8,4.1,5.8,4.1c1.5,0,2.6-0.4,3.3-1.1c0.8-0.8,1.2-1.7,1.2-2.8c0-1.2-0.4-2.1-1.2-2.8
+ C25.5,12.5,24.4,12.1,23,12.1"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/new.svg b/launcher/resources/pe_light/scalable/new.svg
new file mode 100644
index 00000000..96fd1f5b
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/new.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6v20C32,29.3,29.3,32,26,32z M6,2
+ C4.9,2,4,2.9,4,4s0.9,2,2,2s2-0.9,2-2S7.1,2,6,2z M28,8H4v18c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V8z"/>
+<path fill="#CCCCCC" d="M20,20h-2v2c0,1.1-0.9,2-2,2s-2-0.9-2-2v-2h-2c-1.1,0-2-0.9-2-2s0.9-2,2-2h2v-2c0-1.1,0.9-2,2-2s2,0.9,2,2v2
+ h2c1.1,0,2,0.9,2,2S21.1,20,20,20z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/news.svg b/launcher/resources/pe_light/scalable/news.svg
new file mode 100644
index 00000000..6f184afc
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/news.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,0H6C2.7,0,0,2.7,0,6v14c0,3.3,2.7,6,6,6l4,6l4-6h12
+ c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z M16,20H6v-2h10V20z M26,16H6v-2h20V16z M26,12H6v-2h20V12z M26,8H6V6h20V8z"/>
+ </g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/notes.svg b/launcher/resources/pe_light/scalable/notes.svg
new file mode 100644
index 00000000..02dc11ec
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/notes.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<g>
+ <rect x="8" y="16" fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" width="16" height="2"/>
+ <rect x="8" y="12" fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" width="16" height="2"/>
+ <rect x="8" y="8" fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" width="16" height="2"/>
+ <rect x="8" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" width="8" height="2"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/patreon.svg b/launcher/resources/pe_light/scalable/patreon.svg
new file mode 100644
index 00000000..0bd08826
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/patreon.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#F2F2F2" d="M16,0C7.2,0,0,7.2,0,16v16h15.3c0.2,0,0.4,0,0.7,0c8.8,0,16-7.2,16-16S24.8,0,16,0L16,0z"/>
+<path d="M16,3.7C9.2,3.7,3.7,9.2,3.7,16v7.3v5V32h4.9V16c0-4.1,3.3-7.3,7.3-7.3s7.3,3.3,7.3,7.3c0,4.1-3.3,7.3-7.3,7.3
+ c-1.5,0-2.9-0.4-4-1.2v5.3c0.8,0.5,2.5,0.8,4.6,0.8C23.1,28,28.3,22.6,28.3,16C28.3,9.2,22.8,3.7,16,3.7L16,3.7z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/proxy.svg b/launcher/resources/pe_light/scalable/proxy.svg
new file mode 100644
index 00000000..9de8d6d1
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/proxy.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M21.9,4H11.6c-0.8-2.3-3-4-5.6-4C2.7,0,0,2.7,0,6s2.7,6,6,6
+ c2.6,0,4.8-1.7,5.6-4h10.3c1.1,0,2,0.9,2,2v1.4h4V10C27.9,6.7,25.2,4,21.9,4z M6,8C4.9,8,4,7.1,4,6s0.9-2,2-2c1.1,0,2,0.9,2,2
+ S7.1,8,6,8z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M23.9,20.6V22c0,1.1-0.9,2-2,2H11.6c-0.8-2.3-3-4-5.6-4
+ c-3.3,0-6,2.7-6,6s2.7,6,6,6c2.6,0,4.8-1.7,5.6-4h10.3c3.3,0,6-2.7,6-6v-1.4H23.9z M6,28c-1.1,0-2-0.9-2-2s0.9-2,2-2
+ c1.1,0,2,0.9,2,2S7.1,28,6,28z"/>
+<g>
+ <path fill-rule="evenodd" clip-rule="evenodd" fill="#999999" d="M32,22H20V10h12V22z M28,14h-4v4h4V14z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/quickmods.svg b/launcher/resources/pe_light/scalable/quickmods.svg
new file mode 100644
index 00000000..6dbeab52
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/quickmods.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M10.6,24.2c-0.3,0-0.6-0.1-0.8-0.2C9.3,23.7,9,23.2,9,22.6V9.8
+ c0-0.6,0.3-1.1,0.8-1.4c0.3-0.2,0.5-0.2,0.8-0.2c0.2,0,0.5,0.1,0.7,0.2l12.8,6.4c0.5,0.3,0.9,0.8,0.9,1.4c0,0.6-0.3,1.2-0.9,1.4
+ L11.3,24C11.1,24.1,10.8,24.2,10.6,24.2z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/refresh.svg b/launcher/resources/pe_light/scalable/refresh.svg
new file mode 100644
index 00000000..9a724d91
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/refresh.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#F2F2F2" d="M31.8,17.4c-0.4-0.9-1.2-1.4-2.1-1.4h-2.3c0-7.6-6.1-13.7-13.7-13.7C6.1,2.3,0,8.4,0,16
+ c0,7.6,6.1,13.7,13.7,13.7c2.5,0,4.8-0.7,6.8-1.8l-3.4-3.4c-1,0.4-2.2,0.7-3.4,0.7c-5,0-9.1-4.1-9.1-9.1s4.1-9.1,9.1-9.1
+ c5.1,0,9.1,4.1,9.1,9.1h-2.3c-0.9,0-1.8,0.6-2.1,1.4c-0.4,0.9-0.2,1.8,0.5,2.5l4.6,4.6c0.4,0.4,1,0.7,1.6,0.7c0.6,0,1.2-0.2,1.6-0.7
+ l4.6-4.6C32,19.2,32.2,18.3,31.8,17.4z"/>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/resourcepacks.svg b/launcher/resources/pe_light/scalable/resourcepacks.svg
new file mode 100644
index 00000000..7d6323f2
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/resourcepacks.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M28,10c0-1.1-0.9-2-2-2H6c-1.1,0-2,0.9-2,2v16c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V10z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,4h-4V2c0-1.1-0.9-2-2-2h-8c-1.1,0-2,0.9-2,2v2H6
+ c-3.3,0-6,2.7-6,6v16c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V10C32,6.7,29.3,4,26,4z M12,3c0-0.6,0.4-1,1-1h6c0.6,0,1,0.4,1,1v1h-8V3z
+ M6,8h20c1.1,0,2,0.9,2,2v4H4v-4C4,8.9,4.9,8,6,8z M18,17c0,1.1-0.9,2-2,2s-2-0.9-2-2c0-0.4,0.1-0.7,0.3-1h3.4
+ C17.9,16.3,18,16.6,18,17z M26,28H6c-1.1,0-2-0.9-2-2V16h8.1c-0.1,0.3-0.1,0.7-0.1,1c0,2.2,1.8,4,4,4s4-1.8,4-4c0-0.3-0.1-0.7-0.1-1
+ H28v10C28,27.1,27.1,28,26,28z"/>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/screenshots.svg b/launcher/resources/pe_light/scalable/screenshots.svg
new file mode 100644
index 00000000..f2887be6
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/screenshots.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#4D4D4D" d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20
+ c1.1,0,2-0.9,2-2V6z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M24,26H8c-1.1,0-2-0.9-2-2v-0.7c0.8-1.7,1.5-3.8,2-4.8
+ c1.1-2.4,3.5,3.5,4.9,3.5c2.8,0,6.2-10.7,9.1-7.4c1.4,1.6,2.9,5.8,4,8.4V24C26,25.1,25.1,26,24,26z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#CCCCCC" d="M9,6c1.7,0,3,1.3,3,3c0,1.7-1.3,3-3,3c-1.7,0-3-1.3-3-3
+ C6,7.3,7.3,6,9,6z"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/settings.svg b/launcher/resources/pe_light/scalable/settings.svg
new file mode 100644
index 00000000..83b92a52
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/settings.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect y="0" fill="none" width="32" height="32"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6
+ v20C32,29.3,29.3,32,26,32z M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V6z"/>
+<rect x="16" y="10" fill-rule="evenodd" clip-rule="evenodd" fill="#CCCCCC" width="8" height="2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M8,18h6v6H8V18z M10,22h2v-2h-2V22z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M8,8h6v6H8V8z M10,12h2v-2h-2V12z"/>
+<rect x="16" y="20" fill-rule="evenodd" clip-rule="evenodd" fill="#CCCCCC" width="8" height="2"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/status-bad.svg b/launcher/resources/pe_light/scalable/status-bad.svg
new file mode 100644
index 00000000..2c24970c
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/status-bad.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="#F2F2F2" d="M26,32H6c-3.4,0-6-2.6-6-6V6c0-3.4,2.6-6,6-6h20c3.4,0,6,2.6,6,6v20C32,29.4,29.4,32,26,32z"/>
+<path fill="#4D4D4D" d="M21.6,21.6c-0.8,0.8-2,0.8-2.8,0L16,18.8l-2.8,2.8c-0.8,0.8-2,0.8-2.8,0c-0.8-0.8-0.8-2,0-2.8l2.8-2.8
+ l-2.8-2.8c-0.8-0.8-0.8-2,0-2.8c0.8-0.8,2-0.8,2.8,0l2.8,2.8l2.8-2.8c0.8-0.8,2-0.8,2.8,0s0.8,2,0,2.8L18.8,16l2.8,2.8
+ C22.4,19.6,22.4,20.8,21.6,21.6z"/>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/status-good.svg b/launcher/resources/pe_light/scalable/status-good.svg
new file mode 100644
index 00000000..bf9a4174
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/status-good.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<path fill="none" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z"/>
+<path fill="none" d="M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2c0.8,0.8,2,0.8,2.8,0l8.4-8.4
+ c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+<path fill="#F2F2F2" d="M26,0H6C2.6,0,0,2.6,0,6v20c0,3.4,2.6,6,6,6h20c3.4,0,6-2.6,6-6V6C32,2.6,29.4,0,26,0z"/>
+<path fill="#4D4D4D" d="M21,10.4l-7,7l-2.8-2.8c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l4.2,4.2c0.8,0.8,2,0.8,2.8,0l8.4-8.4
+ c0.8-0.8,0.8-2,0-2.8S21.8,9.6,21,10.4z"/>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/status-yellow.svg b/launcher/resources/pe_light/scalable/status-yellow.svg
new file mode 100644
index 00000000..f7d2236b
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/status-yellow.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<g>
+ <path fill="#E3A333" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z M28,26
+ c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+</g>
+<path fill="#F2F2F2" d="M26,0H6C2.7,0,0,2.7,0,6v20c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V6C32,2.7,29.3,0,26,0z"/>
+<path fill="#F2F2F2" d="M28,26c0,1.1-0.9,2-2,2H6c-1.1,0-2-0.9-2-2V6c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2V26z"/>
+<g>
+ <path fill="#4D4D4D" d="M6,20c0-5.8,2.4-7.9,5.8-7.9c2,0,3.4,0.7,5.3,1.9c1.6,1,2.5,1.5,3.3,1.5c0.7,0,1.2-0.7,1.3-3.1H26
+ c0.2,6.1-2.3,8-5.5,8c-2,0-3.5-0.8-5.3-1.9c-1.7-1-2.6-1.6-3.3-1.6c-0.8,0-1.4,0.5-1.6,3.2H6z"/>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/viewfolder.svg b/launcher/resources/pe_light/scalable/viewfolder.svg
new file mode 100644
index 00000000..b36343fe
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/viewfolder.svg
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<rect fill="none" width="32" height="32"/>
+<path fill="#F2F2F2" d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h8c2.6,0,4.8,1.7,5.6,4H26c3.3,0,6,2.7,6,6v16
+ C32,29.3,29.3,32,26,32z M28,10c0-1.1-0.9-2-2-2H16V6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20c1.1,0,2-0.9,2-2V10z
+ "/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/launcher/resources/pe_light/scalable/worlds.svg b/launcher/resources/pe_light/scalable/worlds.svg
new file mode 100644
index 00000000..bf4c21a3
--- /dev/null
+++ b/launcher/resources/pe_light/scalable/worlds.svg
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xml:space="preserve"
+ enable-background="new 0 0 32 32"
+ viewBox="0 0 32 32"
+ y="0px"
+ x="0px"
+ id="Calque_1"
+ version="1.1"><metadata
+ id="metadata45"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs43" /><path
+ id="path2"
+ d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6 v20C32,29.3,29.3,32,26,32z"
+ fill="#F2F2F2"
+ clip-rule="evenodd"
+ fill-rule="evenodd" /><path
+ id="path4"
+ d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
+ fill="#4D4D4D"
+ clip-rule="evenodd"
+ fill-rule="evenodd" /><g
+ id="g10" /><g
+ id="g12" /><g
+ id="g14" /><g
+ id="g16" /><g
+ id="g18" /><g
+ id="g20" /><g
+ id="g22" /><g
+ id="g24" /><g
+ id="g26" /><g
+ id="g28" /><g
+ id="g30" /><g
+ id="g32" /><g
+ id="g34" /><g
+ id="g36" /><g
+ id="g38" /><g
+ style="stroke:#ffffff;stroke-opacity:1"
+ transform="rotate(29.970903,16,16)"
+ id="g881"><ellipse
+ ry="8.9473686"
+ rx="8.9473696"
+ cy="16"
+ cx="16"
+ id="path845"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><ellipse
+ ry="8.9473686"
+ rx="3.4915254"
+ cy="15.947369"
+ cx="16"
+ id="path845-3"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /><path
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 8.1848294,12.430327 C 12.570183,14.790801 20.250652,13.785896 22.94338,12"
+ id="path870" /><path
+ id="path872"
+ d="M 8.2166335,20.492522 C 12.601987,18.132048 21.148735,17.776739 23.841463,19.562635"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /></g></svg> \ No newline at end of file
diff --git a/launcher/resources/sources/clucker.svg b/launcher/resources/sources/clucker.svg
new file mode 100644
index 00000000..0c1727eb
--- /dev/null
+++ b/launcher/resources/sources/clucker.svg
@@ -0,0 +1,404 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="clucker.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/chicken128.png"
+ inkscape:export-xdpi="360"
+ inkscape:export-ydpi="360">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4008">
+ <stop
+ id="stop4010"
+ offset="0"
+ style="stop-color:#4a0000;stop-opacity:1;" />
+ <stop
+ id="stop4012"
+ offset="1"
+ style="stop-color:#360000;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3945">
+ <stop
+ id="stop3947"
+ offset="0"
+ style="stop-color:#4e300d;stop-opacity:1;" />
+ <stop
+ id="stop3949"
+ offset="1"
+ style="stop-color:#604000;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3939">
+ <stop
+ id="stop3941"
+ offset="0"
+ style="stop-color:#5b2400;stop-opacity:1;" />
+ <stop
+ id="stop3943"
+ offset="1"
+ style="stop-color:#8d3800;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3838">
+ <stop
+ style="stop-color:#af6c1d;stop-opacity:1;"
+ offset="0"
+ id="stop3840" />
+ <stop
+ style="stop-color:#604000;stop-opacity:1;"
+ offset="1"
+ id="stop3842" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3830">
+ <stop
+ style="stop-color:#ff7f2a;stop-opacity:1;"
+ offset="0"
+ id="stop3832" />
+ <stop
+ style="stop-color:#8d3800;stop-opacity:1"
+ offset="1"
+ id="stop3834" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3820">
+ <stop
+ style="stop-color:#ff0000;stop-opacity:1;"
+ offset="0"
+ id="stop3822" />
+ <stop
+ style="stop-color:#360000;stop-opacity:1"
+ offset="1"
+ id="stop3824" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3816"
+ x="-0.22469135"
+ width="1.4493827"
+ y="-0.16851852"
+ height="1.337037">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.6851852"
+ id="feGaussianBlur3818" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3820"
+ id="linearGradient3828"
+ x1="12.904698"
+ y1="1043.2582"
+ x2="19.445436"
+ y2="1052.4506"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-1,5.2165626e-5)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3820"
+ id="linearGradient3909"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1,0,0,0.125,-2,911.19195)"
+ x1="12.904698"
+ y1="1043.2582"
+ x2="19.445436"
+ y2="1052.4506" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3820"
+ id="linearGradient3911"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-2,1.0000522)"
+ x1="12.904698"
+ y1="1043.2582"
+ x2="19.445436"
+ y2="1052.4506" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4008"
+ id="linearGradient3915"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.125,0,0,1,16.5,1.0000522)"
+ x1="12.904698"
+ y1="1043.2582"
+ x2="19.445436"
+ y2="1052.4506" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3820"
+ id="linearGradient4032"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-25,-2.9999652)"
+ x1="12.904698"
+ y1="1043.2582"
+ x2="19.445436"
+ y2="1052.4506" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4008"
+ id="linearGradient4034"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.125,0,0,1,-6.5,-2.9999652)"
+ x1="12.904698"
+ y1="1043.2582"
+ x2="19.445436"
+ y2="1052.4506" />
+ <filter
+ inkscape:collect="always"
+ id="filter4053"
+ x="-0.12098765"
+ width="1.2419753"
+ y="-0.090740741"
+ height="1.1814815">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.058642"
+ id="feGaussianBlur4055" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3830"
+ id="linearGradient3840"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1,0,0,0.70710371,1032.3713,730.70724)"
+ x1="13.258252"
+ y1="1033.889"
+ x2="17.500893"
+ y2="1041.7556" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3830"
+ id="linearGradient3846"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-4,3.0000194)"
+ x1="13.258252"
+ y1="1033.889"
+ x2="17.500893"
+ y2="1041.7556" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3838"
+ id="linearGradient3849"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-4,3.0000194)"
+ x1="14.142136"
+ y1="1038.0433"
+ x2="18.119612"
+ y2="1047.1473" />
+ <filter
+ inkscape:collect="always"
+ id="filter3879">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.42040957"
+ id="feGaussianBlur3881" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3850">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.04"
+ id="feGaussianBlur3852" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3854">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.04"
+ id="feGaussianBlur3856" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="11.313708"
+ inkscape:cx="2.6058272"
+ inkscape:cy="11.408405"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1614"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1020.3622)">
+ <path
+ style="opacity:0.87866109;fill:#070707;fill-opacity:1;stroke:none;filter:url(#filter4053)"
+ d="m 10,1023.3622 -3,3 0,20 4,0 0,5 8,0 1,-1 0,-4 5,0 3,-3 0,-20 -18,0 z"
+ id="path4022"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3806"
+ d="m 6.1703915,1025.53 0,4 0,16 5.0000005,0 0,4 8,0 0,-4 5,0 0,-16 0,-4 -18.0000005,0 z"
+ style="opacity:0.75313804;fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter3816)" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path3861"
+ d="m 23,1025.3622 4,-3 0,20 -3,3 -1,-1 z"
+ style="fill:#6c6c6c;fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#e2e2e2;fill-opacity:1;stroke:none"
+ d="m 6,1025.3622 3,-3 18,0 -3,3 0,20 -18,0 z"
+ id="rect3858"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <rect
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ id="rect3759"
+ width="4"
+ height="4"
+ x="7"
+ y="1029.3622" />
+ <rect
+ style="fill:#000000;fill-opacity:1;stroke:none"
+ id="rect3761"
+ width="4"
+ height="4"
+ x="19"
+ y="1029.3622" />
+ <rect
+ style="fill:url(#linearGradient3828);fill-opacity:1;stroke:none"
+ id="rect3769"
+ width="8"
+ height="8"
+ x="11"
+ y="1041.3622" />
+ <rect
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter3854)"
+ id="rect3773"
+ width="1"
+ height="1"
+ x="9"
+ y="1030.3622" />
+ <rect
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter3850)"
+ id="rect3775"
+ width="1"
+ height="1"
+ x="21"
+ y="1030.3622" />
+ <path
+ style="fill:url(#linearGradient3911);fill-opacity:1;stroke:none"
+ d="m 10,1042.3622 8,0 0,8 -8,0 z"
+ id="rect3902"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path3907"
+ d="m 11,1041.3622 8,0 -1,1 -8,0 z"
+ style="fill:url(#linearGradient3909);fill-opacity:1;stroke:none"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3913"
+ d="m 18,1042.3622 1,-1 0,8 -1,1 z"
+ style="fill:url(#linearGradient3915);fill-opacity:1;stroke:none"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;stroke:none"
+ d="m 9,1022.3622 18,0 -3,3 -18,0 z"
+ id="rect4019"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:url(#linearGradient3846);fill-opacity:1;stroke:none"
+ d="m 3.9999995,1036.3622 3.0000005,-3 16,0 0,8 -3,3 -16,0 z"
+ id="rect3815"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <rect
+ style="fill:#b34f0e;fill-opacity:1;stroke:none;opacity:1"
+ id="rect3851"
+ width="4.2426286"
+ height="4.0000043"
+ x="28.284193"
+ y="1056.3621"
+ transform="matrix(0.70710873,-0.70710483,0,1,0,0)" />
+ <rect
+ style="fill:#8d5910;fill-opacity:1;stroke:none;opacity:1"
+ id="rect3855"
+ width="4.2426286"
+ height="4"
+ x="28.284189"
+ y="1060.3621"
+ transform="matrix(0.70710883,-0.70710473,0,1,0,0)" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3970"
+ d="m 20,1036.3622 c 0,1 0,6 0,7 -3,0 -11.0351165,-0.059 -15.5,0.7502 l 1.230393,1.1835 15.5625,-0.094 2.707107,-2.8397 0,-4 0,-4 -1,-1 z"
+ style="opacity:0.63598326;fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter3879)"
+ sodipodi:nodetypes="cccccccccc" />
+ <rect
+ y="1040.3622"
+ x="3.9999995"
+ height="4"
+ width="16"
+ id="rect3917"
+ style="fill:url(#linearGradient3849);fill-opacity:1;stroke:none" />
+ <rect
+ y="1036.3622"
+ x="3.9999995"
+ height="4"
+ width="16"
+ id="rect3923"
+ style="fill:url(#linearGradient3846);fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:url(#linearGradient3840);fill-opacity:1;stroke:none"
+ id="rect3931"
+ width="16.000004"
+ height="4.242579"
+ x="1040.3712"
+ y="1461.4012"
+ transform="matrix(1,0,-0.70710986,0.7071037,0,0)" />
+ </g>
+</svg>
diff --git a/launcher/resources/sources/creeper.svg b/launcher/resources/sources/creeper.svg
new file mode 100644
index 00000000..2a2e39b6
--- /dev/null
+++ b/launcher/resources/sources/creeper.svg
@@ -0,0 +1,775 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="creeper.svg">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3917">
+ <stop
+ id="stop3919"
+ offset="0"
+ style="stop-color:#115008;stop-opacity:1;" />
+ <stop
+ style="stop-color:#39c228;stop-opacity:1;"
+ offset="0.49999952"
+ id="stop3921" />
+ <stop
+ id="stop3923"
+ offset="1"
+ style="stop-color:#c7f7c2;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3892">
+ <stop
+ style="stop-color:#1a1a1a;stop-opacity:1;"
+ offset="0"
+ id="stop3894" />
+ <stop
+ style="stop-color:#1a1a1a;stop-opacity:0;"
+ offset="1"
+ id="stop3896" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3871">
+ <stop
+ style="stop-color:#115008;stop-opacity:1;"
+ offset="0"
+ id="stop3873" />
+ <stop
+ id="stop3900"
+ offset="0.5"
+ style="stop-color:#39c228;stop-opacity:1;" />
+ <stop
+ style="stop-color:#c7f7c2;stop-opacity:1;"
+ offset="1"
+ id="stop3875" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3867">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.792"
+ id="feGaussianBlur3869" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3892"
+ id="linearGradient3898"
+ x1="16"
+ y1="31"
+ x2="16"
+ y2="26"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3871"
+ id="linearGradient5605"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,1020.3622)"
+ x1="27"
+ y1="26"
+ x2="5"
+ y2="3" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3871-3"
+ id="linearGradient5605-2"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,1020.3622)"
+ x1="27"
+ y1="26"
+ x2="5"
+ y2="3" />
+ <linearGradient
+ id="linearGradient3871-3">
+ <stop
+ style="stop-color:#115008;stop-opacity:1;"
+ offset="0"
+ id="stop3873-5" />
+ <stop
+ id="stop3900-7"
+ offset="0.5"
+ style="stop-color:#39c228;stop-opacity:1;" />
+ <stop
+ style="stop-color:#c7f7c2;stop-opacity:1;"
+ offset="1"
+ id="stop3875-9" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3917"
+ id="linearGradient3147"
+ x1="31"
+ y1="1052.3622"
+ x2="4"
+ y2="1024.3622"
+ gradientUnits="userSpaceOnUse"
+ spreadMethod="reflect" />
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4028"
+ x="-0.14708571"
+ width="1.2941715"
+ y="-0.25740001"
+ height="1.5148">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.429"
+ id="feGaussianBlur4030" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4056">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.676"
+ id="feGaussianBlur4058" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter5711"
+ x="-0.1728"
+ width="1.3456"
+ y="-0.3456"
+ height="1.6912">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.144"
+ id="feGaussianBlur5713" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter5719"
+ x="-0.18"
+ width="1.36"
+ y="-0.36"
+ height="1.72">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.15"
+ id="feGaussianBlur5721" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter5723"
+ x="-0.21"
+ width="1.42"
+ y="-0.28"
+ height="1.56">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.35"
+ id="feGaussianBlur5725" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter5727"
+ x="-0.21"
+ width="1.42"
+ y="-0.28"
+ height="1.56">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.35"
+ id="feGaussianBlur5729" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4"
+ inkscape:cx="57.618613"
+ inkscape:cy="29.528242"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1020.3622)">
+ <rect
+ style="fill:#2b1100;fill-opacity:1;stroke:none"
+ id="rect3063"
+ width="22"
+ height="22"
+ x="5"
+ y="5"
+ transform="translate(0,1020.3622)"
+ rx="5"
+ ry="5" />
+ <path
+ id="path3065"
+ d="m 6,1026.3622 0,22 22,0 0,-22 z m 2,2 6,0 0,6 6,0 0,-6 6,0 0,6 -6,0 0,3 3,0 0,9 -3,0 0,-3 -6,0 0,3 -3,0 0,-9 3,0 0,-3 -6,0 z"
+ style="fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter3867)"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccccccccccccccccccc" />
+ <path
+ style="fill:url(#linearGradient5605);fill-opacity:1;stroke:none"
+ d="m 7,1022.3622 c -2.77,0 -5,2.23 -5,5 l 0,25 28,0 0,-25 c 0,-2.77 -2.23,-5 -5,-5 z m 0,5 6,0 0,6 6,0 0,-6 6,0 0,6 -6,0 0,3 3,0 0,9 -3,0 0,-3 -6,0 0,3 -3,0 0,-9 3,0 0,-3 -6,0 z"
+ id="rect3045"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccsssccccccccccccccccccccc" />
+ <g
+ id="g5466"
+ style="opacity:0.3"
+ transform="translate(0,-1)">
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3902"
+ d="m 3.999974,1022.3622 3,0 0,3 -3,0 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3904"
+ d="m 6.999974,1022.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3906"
+ d="m 3.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3908"
+ d="m 0.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3914"
+ d="m 3.999974,1028.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3916"
+ d="m 0.999974,1028.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3918"
+ d="m 6.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3924"
+ d="m 9.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3926"
+ d="m 12.999974,1028.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3930"
+ d="m 12.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3932"
+ d="m 9.999974,1022.3622 3,0 0,3 -3,0 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3936"
+ d="m 12.999974,1022.3622 3,0 0,3 -3,0 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3999"
+ d="m 18.99998,1028.3622 0,-3 3,0 0,3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4007"
+ d="m 15.99998,1031.3622 0,-3 3,0 0,3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4009"
+ d="m 24.99998,1031.3622 0,-3 3,0 0,3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4011"
+ d="m 24.99998,1034.3622 0,-3 3,0 0,3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4013"
+ d="m 21.99998,1028.3622 0,-3 3,0 0,3 z"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4015"
+ d="m 24.99998,1028.3622 0,-3 3,0 0,3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4017"
+ d="m 15.99998,1028.3622 0,-3 3,0 0,3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4019"
+ d="m 21.99998,1025.3622 0,-3 3,0 0,3 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4023"
+ d="m 24.99998,1025.3622 0,-3 3,0 0,3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4027"
+ d="m 18.99998,1025.3622 0,-3 3,0 0,3 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4029"
+ d="m 15.99998,1025.3622 0,-3 3,0 0,3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4035"
+ d="m 15.99998,1034.3622 0,-3 3,0 0,3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4039"
+ d="m 6.99998,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4041"
+ d="m 6.99998,1037.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4043"
+ d="m 9.99998,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4049"
+ d="m 3.99998,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4053"
+ d="m 12.99998,1031.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4059"
+ d="m 3.99998,1037.3622 0,3 3,0 0,-3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4063"
+ d="m 12.99998,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4069"
+ d="m 6.99998,1040.3622 0,3 3,0 0,-3 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4071"
+ d="m 3.99998,1040.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4073"
+ d="m 6.99998,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4075"
+ d="m 3.99998,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4077"
+ d="m 3.99998,1031.3622 0,3 3,0 0,-3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4133"
+ d="m 28,1028.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4143"
+ d="m 28,1031.3622 0,3 3,0 0,-3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4155"
+ d="m 28,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4159"
+ d="m 28,1037.3622 0,3 3,0 0,-3 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4161"
+ d="m 28,1025.3622 0,3 3,0 0,-3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4175"
+ d="m 28,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4185"
+ d="m 28,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4197"
+ d="m 28,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4203"
+ d="m 28,1040.3622 0,3 3,0 0,-3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4207"
+ d="m 6.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4211"
+ d="m 9.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4213"
+ d="m 9.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4215"
+ d="m 6.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4217"
+ d="m 3.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4219"
+ d="m 12.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4221"
+ d="m 12.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4245"
+ d="m 3.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4261"
+ d="m 0.99998,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4263"
+ d="m 0.99998,1031.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4267"
+ d="m 0.99998,1037.3622 0,3 3,0 0,-3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4273"
+ d="m 0.99998,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4275"
+ d="m 0.99998,1040.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4303"
+ d="m 0.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4305"
+ d="m 0.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4093"
+ d="m 12.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4099"
+ d="m 15.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4105"
+ d="m 21.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4109"
+ d="m 21.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4113"
+ d="m 18.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4115"
+ d="m 21.999974,1037.3622 3,0 0,3 -3,0 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4117"
+ d="m 21.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4119"
+ d="m 9.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4377"
+ d="m 21.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4379"
+ d="m 18.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4381"
+ d="m 15.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4383"
+ d="m 15.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4387"
+ d="m 18.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4389"
+ d="m 15.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4391"
+ d="m 21.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4393"
+ d="m 21.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4395"
+ d="m 21.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4397"
+ d="m 24.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4399"
+ d="m 27.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4401"
+ d="m 24.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4403"
+ d="m 27.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4405"
+ d="m 24.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4407"
+ d="m 24.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4409"
+ d="m 27.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4411"
+ d="m 27.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4417"
+ d="m 18.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4419"
+ d="m 21.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4433"
+ d="m 21.999974,1037.3622 3,0 0,3 -3,0 z"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4435"
+ d="m 21.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4439"
+ d="m 24.999974,1037.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4441"
+ d="m 27.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4443"
+ d="m 24.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4445"
+ d="m 27.999974,1037.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4447"
+ d="m 24.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4449"
+ d="m 24.999974,1031.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4451"
+ d="m 27.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4453"
+ d="m 27.999974,1031.3622 3,0 0,3 -3,0 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4455"
+ d="m 15.999974,1031.3622 3,0 0,3 -3,0 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ </g>
+ <path
+ style="fill:#000000;stroke:none"
+ d="m 7,1020.3622 c -3.878,0 -7,3.122 -7,7 l 0,25 2,0 0,-25 c 0,-2.77 2.23,-5 5,-5 l 18,0 c 2.77,0 5,2.23 5,5 l 0,25 2,0 0,-25 c 0,-3.878 -3.122,-7 -7,-7 z"
+ id="rect3887"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccssssccsss" />
+ <path
+ style="opacity:0.34999999999999998;fill:url(#linearGradient3147);fill-opacity:1;stroke:none"
+ d="m 7,1021.3622 c -3.324,0 -6,2.676 -6,6 l 0,25 1,0 0,-25 c 0,-2.77 2.23,-5 5,-5 l 18,0 c 2.77,0 5,2.23 5,5 l 0,25 1,0 0,-25 c 0,-3.324 -2.676,-6 -6,-6 l -18,0 z"
+ id="rect3098"
+ inkscape:connector-curvature="0" />
+ <rect
+ style="fill:url(#linearGradient3898);fill-opacity:1;stroke:none"
+ id="rect3890"
+ width="32"
+ height="6"
+ x="0"
+ y="26"
+ transform="translate(0,1020.3622)" />
+ </g>
+</svg>
diff --git a/launcher/resources/sources/enderpearl.svg b/launcher/resources/sources/enderpearl.svg
new file mode 100644
index 00000000..ac9378f5
--- /dev/null
+++ b/launcher/resources/sources/enderpearl.svg
@@ -0,0 +1,271 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="enderpearl.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/enderpearl.png"
+ inkscape:export-xdpi="45"
+ inkscape:export-ydpi="45">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4030">
+ <stop
+ id="stop4032"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ <stop
+ id="stop4034"
+ offset="1"
+ style="stop-color:#0c493f;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3954">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3956" />
+ <stop
+ style="stop-color:#0f5f52;stop-opacity:1;"
+ offset="1"
+ id="stop3958" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3884">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.505"
+ id="feGaussianBlur3886" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3898"
+ x="-0.20110182"
+ width="1.4022036"
+ y="-0.20210065"
+ height="1.4042013">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.0604979"
+ id="feGaussianBlur3900" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3916"
+ x="-0.11491533"
+ width="1.2298307"
+ y="-0.11548609"
+ height="1.2309722">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.6059988"
+ id="feGaussianBlur3918" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3992">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.44"
+ id="feGaussianBlur3994" />
+ </filter>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954"
+ id="radialGradient4024"
+ gradientUnits="userSpaceOnUse"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)" />
+ <filter
+ inkscape:collect="always"
+ id="filter4082"
+ x="-0.13465964"
+ width="1.2693193"
+ y="-0.12490681"
+ height="1.2498136">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.763901"
+ id="feGaussianBlur4084" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4110"
+ x="-0.17716113"
+ width="1.3543223"
+ y="-0.17804105"
+ height="1.3560821">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.93424815"
+ id="feGaussianBlur4112" />
+ </filter>
+ <linearGradient
+ id="linearGradient3954-9">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3956-4" />
+ <stop
+ style="stop-color:#0f5f52;stop-opacity:1;"
+ offset="1"
+ id="stop3958-2" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954"
+ id="radialGradient4230"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954"
+ id="radialGradient4250"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="-241.15414"
+ inkscape:cy="194.64078"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="2"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="11.25,96.5"
+ id="guide4252" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-988.36218)">
+ <path
+ sodipodi:type="arc"
+ style="fill:#032620;fill-opacity:1;stroke:none;filter:url(#filter3992)"
+ id="path3005"
+ sodipodi:cx="32"
+ sodipodi:cy="32"
+ sodipodi:rx="24"
+ sodipodi:ry="24"
+ d="M 56,32 A 24,24 0 1 1 8,32 24,24 0 1 1 56,32 z"
+ transform="matrix(1.1666667,0,0,1.1666667,-5.3333344,983.02885)" />
+ <g
+ id="g4116"
+ transform="matrix(1.0195929,0,0,1.0195936,-0.15674338,-19.992546)">
+ <path
+ transform="matrix(1.1769403,0,0,1.1769403,-6.1232836,982.70009)"
+ d="M 52,32 C 52,43.045695 43.045695,52 32,52 20.954305,52 12,43.045695 12,32 12,20.954305 20.954305,12 32,12 c 11.045695,0 20,8.954305 20,20 z"
+ sodipodi:ry="20"
+ sodipodi:rx="20"
+ sodipodi:cy="32"
+ sodipodi:cx="32"
+ id="path3014"
+ style="fill:url(#radialGradient4024);fill-opacity:1;stroke:none"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:nodetypes="ssccs"
+ inkscape:connector-curvature="0"
+ id="path3866"
+ transform="matrix(1.1769403,0,0,1.1769403,-5.6620909,982.68843)"
+ d="m 28,16 c -6.627417,0 -12,5.372583 -12,12 0,0.20181 0.0214,0.394383 0.03125,0.59375 C 20,20 22,22 28.65625,16.03125 28.436252,16.019242 28.222996,16 28,16 z"
+ style="fill:#00ffcc;fill-opacity:1;stroke:none;filter:url(#filter3884)" />
+ <path
+ transform="matrix(1.487885,0,0,1.487885,-10.597031,-493.22036)"
+ style="fill:#00ffcc;fill-opacity:1;stroke:none;filter:url(#filter3898)"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 3.96875,-8.5937 5.96875,-6.5937 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ id="path3888"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccs" />
+ <path
+ sodipodi:nodetypes="ssccs"
+ inkscape:connector-curvature="0"
+ id="path3902"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 3.96875,-8.5937 5.96875,-6.5937 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ style="fill:#00ffcc;fill-opacity:1;stroke:none;filter:url(#filter3916)"
+ transform="matrix(-1.8359746,0,0,-1.8359746,81.432966,2885.3572)" />
+ <path
+ transform="matrix(-1.1914115,0,0,-1.1914115,69.087304,2235.4229)"
+ style="fill:#d7f4d7;fill-opacity:1;stroke:none;filter:url(#filter3916)"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 3.96875,-8.5937 5.96875,-6.5937 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ id="path3920"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccs" />
+ <path
+ sodipodi:nodetypes="csccsc"
+ inkscape:connector-curvature="0"
+ id="path3964"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 5.570868,-4.4816 8.905966,-4.4575 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4110)"
+ transform="matrix(0.58516297,0,0,0.58516297,6.7391969,416.10681)" />
+ <path
+ transform="translate(0,988.36218)"
+ sodipodi:nodetypes="ccccccccccccccccccssssscsscsscccccccccccc"
+ inkscape:connector-curvature="0"
+ id="path4036"
+ d="M 30,29 30.450431,19.407328 32.447263,28.5316 35,27 l 1,-6 -1,-4 1,-2 0,3 1.125,3.375 -0.07418,2.744617 L 44.0828,21.873859 37,26 l -1,4 2,2 3.375,0 5.0625,-3.0625 -4.3125,4.375 L 36,34 c 0,0 2.686048,7.256632 1.846439,8.096462 -0.326542,0.326628 -1.289256,-3.475594 -2.437243,-4.77335 C 34.192758,35.947974 32.79132,37.0625 32.5625,37.0625 c -0.602669,0 -2.136064,2.519769 -3.25945,4.522134 -0.205342,0.36601 0.728015,9.714733 0.308259,6.715119 C 29.482708,47.380759 28,44 28,44 c 0,0 0,-2 0,-3 0,-1 2,-3 2,-4 0,-1 0,-3 0,-3 0,0 -4.25,-1.125 -5.25,-1.125 -0.47674,0 -1.893857,0.89205 -3.377184,1.040709 C 19.744745,34.078874 18.0625,33.5 18.0625,33.5 L 16.774038,34.754808 17,39 l -2,-4 2,-3 3.8125,-0.0625 1.170423,-6.693425 3.092068,-2.055016 -2.122126,3.038537 -0.913529,5.350612 L 28,31 z"
+ style="opacity:0.86178864;fill:#000000;fill-opacity:0.86666667;stroke:none;filter:url(#filter4082)" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/sources/flame.svg b/launcher/resources/sources/flame.svg
new file mode 100644
index 00000000..8a6da2f7
--- /dev/null
+++ b/launcher/resources/sources/flame.svg
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 127.99999 128"
+ height="128"
+ width="128"
+ id="svg2"
+ version="1.1">
+ <metadata
+ id="metadata8">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs6" />
+ <path
+ id="path4509"
+ d="m 93.706631,98.374916 c 2e-6,12.224574 -12.676714,22.134544 -28.314236,22.134544 -15.637522,0 -28.314238,-9.90997 -28.314236,-22.134544 10e-7,-12.224569 11.178608,-36.815994 26.816128,-36.815994 15.637521,0 29.812343,24.591425 29.812344,36.815994 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke-width:1.17175078;marker:none;enable-background:accumulate" />
+ <g
+ transform="matrix(0.22142825,0,0,0.22142825,33.524659,-1.9444021)"
+ id="g4505">
+ <path
+ style="fill:#ff7500;fill-opacity:1;fill-rule:evenodd"
+ d="m 285.26215,237.39406 c -4.43799,0.93416 -40.69637,2.61517 -41.21184,41.12844 -0.51547,38.51328 52.00568,64.86852 53.07625,129.21055 1.02262,61.46009 -43.4082,133.87308 -118.22034,141.52542 51.89013,-43.65773 59.51106,-100.48068 13.98305,-151.27119 -23.73802,-26.48184 -56.20743,-44.93245 -54.2373,-96.18644 1.97013,-51.25399 80.13525,-121.18396 146.61018,-64.40678 z"
+ id="path4491" />
+ <path
+ id="path4493"
+ d="m 49.139955,83.857637 c 5.14236,1.40482 47.15538,3.93282 47.75267,61.850853 0.59728,57.91808 -115.98924,109.53719 -117.22972,206.29773 -1.18493,92.42657 41.90823,185.7443 128.593975,197.25225 C 48.131155,483.60392 47.690095,413.73124 100.44395,337.35009 127.9495,297.52546 221.3018,257.7937 219.01898,180.71555 216.73616,103.63741 126.16528,-1.5265416 49.139955,83.857637 Z"
+ style="fill:#ff7500;fill-opacity:1;fill-rule:evenodd;stroke-width:1.32004774" />
+ <path
+ id="path4497"
+ d="m 244.91289,277.23386 c -3.04702,-0.0596 -26.88302,-4.71504 -34.06545,22.18053 -7.18245,26.89556 22.22556,53.80661 11.48022,99.05033 -10.26404,43.2172 -51.9816,86.79466 -101.90647,80.11983 41.44697,-22.23442 56.49745,-60.81281 35.97375,-103.71573 -10.70094,-22.3693 -28.49783,-40.51738 -18.10576,-76.10378 10.3921,-35.5864 73.56677,-71.99754 106.62371,-21.53118 z"
+ style="fill:#ff7500;fill-opacity:1;fill-rule:evenodd;stroke-width:0.69522083" />
+ <path
+ style="fill:#ff7500;fill-opacity:1;fill-rule:evenodd;stroke-width:0.5843063"
+ d="m 54.097869,231.55891 c 2.12507,0.34666 19.5376,-1.05581 19.45707,26.31399 -0.0805,27.36978 -25.54842,49.88894 -26.61138,95.75261 -1.01537,43.80938 19.72217,92.1553 55.613891,92.23888 C 77.989639,418.51587 74.811129,378.62585 97.126179,339.21931 108.76111,318.67301 124.52413,303.21607 124.01412,266.88402 123.50408,230.55198 86.531659,186.39085 54.097869,231.55891 Z"
+ id="path4499" />
+ </g>
+</svg>
diff --git a/launcher/resources/sources/ftb-glow.svg b/launcher/resources/sources/ftb-glow.svg
new file mode 100644
index 00000000..be78c78e
--- /dev/null
+++ b/launcher/resources/sources/ftb-glow.svg
@@ -0,0 +1,606 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32px"
+ height="32px"
+ id="svg2985"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="ftb-glow.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/ftb_glow128.png"
+ inkscape:export-xdpi="360"
+ inkscape:export-ydpi="360">
+ <defs
+ id="defs2987">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4462">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop4464" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0;"
+ offset="1"
+ id="stop4466" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3981">
+ <stop
+ style="stop-color:#000083;stop-opacity:1;"
+ offset="0"
+ id="stop3983" />
+ <stop
+ id="stop3985"
+ offset="0.25"
+ style="stop-color:#1919ff;stop-opacity:1;" />
+ <stop
+ id="stop3987"
+ offset="0.5"
+ style="stop-color:#1919ff;stop-opacity:1;" />
+ <stop
+ style="stop-color:#1919ff;stop-opacity:1;"
+ offset="0.75"
+ id="stop3989" />
+ <stop
+ style="stop-color:#00007d;stop-opacity:1;"
+ offset="1"
+ id="stop3991" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3941">
+ <stop
+ id="stop3943"
+ offset="0"
+ style="stop-color:#00830e;stop-opacity:1;" />
+ <stop
+ style="stop-color:#00c017;stop-opacity:1;"
+ offset="0.25"
+ id="stop3945" />
+ <stop
+ style="stop-color:#00ff1e;stop-opacity:1;"
+ offset="0.5"
+ id="stop3947" />
+ <stop
+ id="stop3949"
+ offset="0.75"
+ style="stop-color:#00ff1e;stop-opacity:1;" />
+ <stop
+ id="stop3951"
+ offset="1"
+ style="stop-color:#007d0f;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3801">
+ <stop
+ style="stop-color:#830000;stop-opacity:1"
+ offset="0"
+ id="stop3803" />
+ <stop
+ id="stop3811"
+ offset="0.25"
+ style="stop-color:#ff0000;stop-opacity:1;" />
+ <stop
+ id="stop3809"
+ offset="0.5"
+ style="stop-color:#ff0000;stop-opacity:1;" />
+ <stop
+ style="stop-color:#ff0000;stop-opacity:1;"
+ offset="0.75"
+ id="stop3813" />
+ <stop
+ style="stop-color:#7d0000;stop-opacity:1"
+ offset="1"
+ id="stop3805" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter4107"
+ x="-0.14444444"
+ width="1.2888889"
+ y="-0.12380952"
+ height="1.247619">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.4444444"
+ id="feGaussianBlur4109" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4452">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.85596708"
+ id="feGaussianBlur4454" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4462"
+ id="linearGradient4468"
+ x1="-34.75"
+ y1="14"
+ x2="72.5"
+ y2="18.5"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(4.228145,0,0,1,111.61491,355.31802)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3801"
+ id="linearGradient4476"
+ gradientUnits="userSpaceOnUse"
+ x1="-8"
+ y1="2"
+ x2="-4"
+ y2="6" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3941"
+ id="linearGradient4478"
+ gradientUnits="userSpaceOnUse"
+ x1="-8"
+ y1="2"
+ x2="-4"
+ y2="6" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3981"
+ id="linearGradient4480"
+ gradientUnits="userSpaceOnUse"
+ x1="-8"
+ y1="2"
+ x2="-4"
+ y2="6" />
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4107-6"
+ x="-0.14444444"
+ width="1.2888889"
+ y="-0.12380952"
+ height="1.247619">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.4444444"
+ id="feGaussianBlur4109-1" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3941"
+ id="linearGradient3336"
+ gradientUnits="userSpaceOnUse"
+ x1="-8"
+ y1="6"
+ x2="4"
+ y2="17" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3981"
+ id="linearGradient3338"
+ gradientUnits="userSpaceOnUse"
+ x1="-16"
+ y1="2"
+ x2="-4"
+ y2="14" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3801"
+ id="linearGradient3340"
+ gradientUnits="userSpaceOnUse"
+ x1="-8"
+ y1="6"
+ x2="4"
+ y2="18" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3801"
+ id="linearGradient3342"
+ gradientUnits="userSpaceOnUse"
+ x1="-8"
+ y1="2"
+ x2="-4"
+ y2="6" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3801"
+ id="linearGradient3344"
+ gradientUnits="userSpaceOnUse"
+ x1="-8"
+ y1="2"
+ x2="-4"
+ y2="6" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="5.6568542"
+ inkscape:cx="6.8684247"
+ inkscape:cy="24.209238"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1"
+ showguides="true"
+ inkscape:guide-bbox="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2993"
+ empspacing="2"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="4,48"
+ id="guide3346" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="-29,35"
+ id="guide3348" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="-62,42"
+ id="guide3350" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="-58,69"
+ id="guide3352" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata2990">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <rect
+ style="fill:url(#linearGradient4468);fill-opacity:1;stroke:none"
+ id="rect4460"
+ width="495.75"
+ height="96.25"
+ x="-76.537521"
+ y="319.06802" />
+ <g
+ id="g3280"
+ transform="translate(2,53)"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/vectors/ftb.png"
+ inkscape:export-xdpi="360"
+ inkscape:export-ydpi="360">
+ <g
+ transform="translate(-2,-53)"
+ id="g8144"
+ style="filter:url(#filter4107)">
+ <path
+ style="fill:#5353ff;fill-opacity:1;stroke:none"
+ d="m 4,2 12,0 0,4 -8,0 0,4 4,0 0,4 -4,0 0,8 -4,0 z"
+ id="path8146"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccc" />
+ <path
+ style="fill:#009a00;fill-opacity:1;stroke:none"
+ d="m 8,6 12,0 0,4 -4,0 0,16 -4,0 0,-16 -4,0 z"
+ id="path8148"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccc" />
+ <path
+ style="fill:#ac0000;fill-opacity:1;stroke:none"
+ d="m 16,10 0,20 8,0 0,-4 -4,0 0,-4 4,0 0,-4 -4,0 0,-4 4,0 0,-4 -8,0 z m 8,4 0,4 4,0 0,-4 -4,0 z m 0,8 0,4 4,0 0,-4 -4,0 z"
+ id="path8150"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ id="g8256"
+ transform="translate(32,-53)">
+ <g
+ id="g8258"
+ transform="translate(-18,4)">
+ <path
+ sodipodi:nodetypes="ccccccccccc"
+ inkscape:connector-curvature="0"
+ id="path8260"
+ d="m -8,2 4,0 8,0 0,4 -4,0 0,16 -4,0 0,-16 0,0 -4,0 z"
+ style="fill:url(#linearGradient3336);fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.50196078;stroke:none"
+ d="m -8,2 1,1 0,2 -1,1 z"
+ id="path8262"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8264"
+ d="M 4,2 3,3 -7,3 -8,2 z"
+ style="fill:#ffffff;fill-opacity:0.50196078;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8266"
+ d="m -4,22 1,-1 2,0 1,1 z"
+ style="fill:#000000;fill-opacity:0.3466667;stroke:none" />
+ <path
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none"
+ d="M 4,6 3,5 3,3 4,2 z"
+ id="path8268"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.3466667;stroke:none"
+ d="m -8,6 1,-1 4,0 -1,1 z"
+ id="path8270"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8272"
+ d="M 0,6 -1,5 3,5 4,6 z"
+ style="fill:#000000;fill-opacity:0.3466667;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8274"
+ d="m -4,6 1,-1 0,16 -1,1 z"
+ style="fill:#ffffff;fill-opacity:0.50196078;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8276"
+ d="M 0,22 -1,21 -1,5 0,6 z"
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none" />
+ </g>
+ <g
+ transform="translate(-14,0)"
+ id="g8278">
+ <path
+ sodipodi:nodetypes="ccccccccccc"
+ inkscape:connector-curvature="0"
+ id="path8280"
+ d="m -16,2 12,0 0,4 -8,0 0,4 4,0 0,4 -4,0 0,8 -4,0 z"
+ style="fill:url(#linearGradient3338);fill-opacity:1;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8282"
+ d="m -16,2 1,1 0,18 -1,1 z"
+ style="fill:#ffffff;fill-opacity:0.50222222;stroke:none" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.40888887;stroke:none"
+ d="M -4,2 -5,3 -15,3 -16,2 z"
+ id="path8284"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.3466667;stroke:none"
+ d="m -16,22 1,-1 2,0 1,1 z"
+ id="path8286"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8288"
+ d="M -4,6 -5,5 -5,3 -4,2 z"
+ style="fill:#ffffff;fill-opacity:0.25777779;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8290"
+ d="m -12,14 -1,-1 4,0 1,1 z"
+ style="fill:#ffffff;fill-opacity:0.3466667;stroke:none" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.3466667;stroke:none"
+ d="m -12,6 -1,-1 8,0 1,1 z"
+ id="path8292"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.25777779;stroke:none"
+ d="m -8,14 -1,-1 0,-2 1,-1 z"
+ id="path8294"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8296"
+ d="m -8,10 -1,1 -4,0 1,-1 z"
+ style="fill:#ffffff;fill-opacity:0.40888887;stroke:none" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.25777779;stroke:none"
+ d="m -12,10 -1,1 0,-6 1,1 z"
+ id="path8298"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8300"
+ d="m -12,22 -1,-1 0,-8 1,1 z"
+ style="fill:#ffffff;fill-opacity:0.25777779;stroke:none" />
+ </g>
+ <g
+ id="g8302">
+ <g
+ id="g8304"
+ transform="translate(-10,8)">
+ <path
+ style="fill:url(#linearGradient3340);fill-opacity:1;stroke:none"
+ d="m -8,2 8,0 0,4 -4,0 0,4 4,0 0,4 -4,0 0,4 4,0 0,4 -8,0 z"
+ id="path8306"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccc" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.39111113;stroke:none"
+ d="m -8,2 1,1 0,18 -1,1 z"
+ id="path8308"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8310"
+ d="M 0,2 -1,3 -7,3 -8,2 z"
+ style="fill:#ffffff;fill-opacity:0.23999999;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8312"
+ d="m -8,22 1,-1 6,0 1,1 z"
+ style="fill:#000000;fill-opacity:0.43111112;stroke:none" />
+ <path
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none"
+ d="M 0,6 -1,5 -1,3 0,2 z"
+ id="path8314"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.43111112;stroke:none"
+ d="M -4,6 -5,5 -1,5 0,6 z"
+ id="path8316"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8318"
+ d="m -4,14 -1,-1 4,0 1,1 z"
+ style="fill:#000000;fill-opacity:0.43111112;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8320"
+ d="m -4,10 -1,1 0,-6 1,1 z"
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none" />
+ <path
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none"
+ d="m -4,18 -1,1 0,-6 1,1 z"
+ id="path8322"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8324"
+ d="m 0,22 -1,-1 0,-2 1,-1 z"
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.23999999;stroke:none"
+ d="m 0,10 -1,1 -4,0 1,-1 z"
+ id="path8326"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8328"
+ d="m 0,18 -1,1 -4,0 1,-1 z"
+ style="fill:#ffffff;fill-opacity:0.23999999;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8330"
+ d="m 0,14 -1,-1 0,-2 1,-1 z"
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none" />
+ </g>
+ <g
+ transform="translate(-2,12)"
+ id="g8332">
+ <rect
+ style="fill:url(#linearGradient3342);fill-opacity:1;stroke:none"
+ id="rect8334"
+ width="4"
+ height="4"
+ x="-8"
+ y="2" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.39111113;stroke:none"
+ d="m -8,2 1,1 0,2 -1,1 z"
+ id="path8336"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8338"
+ d="M -4,2 -5,3 -7,3 -8,2 z"
+ style="fill:#ffffff;fill-opacity:0.23999999;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8340"
+ d="m -8,6 1,-1 2,0 1,1 z"
+ style="fill:#000000;fill-opacity:0.43111112;stroke:none" />
+ <path
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none"
+ d="M -4,6 -5,5 -5,3 -4,2 z"
+ id="path8342"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ </g>
+ <g
+ id="g8344"
+ transform="translate(-2,20)">
+ <rect
+ y="2"
+ x="-8"
+ height="4"
+ width="4"
+ id="rect8346"
+ style="fill:url(#linearGradient3344);fill-opacity:1;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8348"
+ d="m -8,2 1,1 0,2 -1,1 z"
+ style="fill:#ffffff;fill-opacity:0.39111113;stroke:none" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.23999999;stroke:none"
+ d="M -4,2 -5,3 -7,3 -8,2 z"
+ id="path8350"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ style="fill:#000000;fill-opacity:0.43111112;stroke:none"
+ d="m -8,6 1,-1 2,0 1,1 z"
+ id="path8352"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path8354"
+ d="M -4,6 -5,5 -5,3 -4,2 z"
+ style="fill:#000000;fill-opacity:0.25777779;stroke:none" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/sources/ftb-logo.svg b/launcher/resources/sources/ftb-logo.svg
new file mode 100644
index 00000000..8cf73567
--- /dev/null
+++ b/launcher/resources/sources/ftb-logo.svg
@@ -0,0 +1,257 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ width="128"
+ height="128"
+ sodipodi:docname="ftb-logo.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/ftb_logo128.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <metadata
+ id="metadata8">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs6">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3027">
+ <stop
+ style="stop-color:#418cc6;stop-opacity:1"
+ offset="0"
+ id="stop3029" />
+ <stop
+ style="stop-color:#014b85;stop-opacity:1"
+ offset="1"
+ id="stop3031" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3019">
+ <stop
+ style="stop-color:#41cd55;stop-opacity:1"
+ offset="0"
+ id="stop3021" />
+ <stop
+ style="stop-color:#018d15;stop-opacity:1"
+ offset="1"
+ id="stop3023" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3011">
+ <stop
+ style="stop-color:#d24141;stop-opacity:1"
+ offset="0"
+ id="stop3013" />
+ <stop
+ style="stop-color:#910101;stop-opacity:1"
+ offset="1"
+ id="stop3015" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3826"
+ color-interpolation-filters="sRGB">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="3.4237769"
+ id="feGaussianBlur3828" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3019"
+ id="linearGradient3830"
+ gradientUnits="userSpaceOnUse"
+ x1="567.10803"
+ y1="231.67372"
+ x2="567.10803"
+ y2="300.68848" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3027"
+ id="linearGradient3832"
+ gradientUnits="userSpaceOnUse"
+ x1="548.19916"
+ y1="255.50847"
+ x2="548.19916"
+ y2="324.71759" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3011"
+ id="linearGradient3834"
+ gradientUnits="userSpaceOnUse"
+ x1="600.95337"
+ y1="257.41525"
+ x2="600.95337"
+ y2="324.16464" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3019"
+ id="linearGradient3848"
+ gradientUnits="userSpaceOnUse"
+ x1="567.10803"
+ y1="231.67372"
+ x2="567.10803"
+ y2="300.68848" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3027"
+ id="linearGradient3850"
+ gradientUnits="userSpaceOnUse"
+ x1="548.19916"
+ y1="255.50847"
+ x2="548.19916"
+ y2="324.71759" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3011"
+ id="linearGradient3852"
+ gradientUnits="userSpaceOnUse"
+ x1="600.95337"
+ y1="257.41525"
+ x2="600.95337"
+ y2="324.16464" />
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ id="namedview4"
+ showgrid="false"
+ showguides="false"
+ inkscape:guide-bbox="true"
+ inkscape:zoom="4"
+ inkscape:cx="32.725294"
+ inkscape:cy="54.630313"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g3838">
+ <sodipodi:guide
+ orientation="1,0"
+ position="352.46726,316.96211"
+ id="guide2991" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="347.41115,278.08622"
+ id="guide2993" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="431.31454,337.77645"
+ id="guide3001" />
+ <inkscape:grid
+ type="xygrid"
+ id="grid3005"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="431.28645,312"
+ id="guide3007" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="73.25,82.125"
+ id="guide3856" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="75.5,79.25"
+ id="guide3858" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="35.125,49.75"
+ id="guide3860" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="39.686368,60.45763"
+ id="guide3862" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="41.9375,48.59375"
+ id="guide3864" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="28.416854,59.142853"
+ id="guide3866" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="28.416854,56.623785"
+ id="guide3868" />
+ </sodipodi:namedview>
+ <g
+ id="g3838"
+ transform="matrix(1,0,0,1.018245,3.8570626,-2.6546202)">
+ <g
+ style="opacity:0.33054397;fill:#000000;fill-opacity:1;filter:url(#filter3826)"
+ transform="matrix(0.83116569,0,0,0.83116569,-409.72999,-161.96543)"
+ id="g3040">
+ <path
+ sodipodi:nodetypes="ccccccccccccccc"
+ inkscape:connector-curvature="0"
+ id="path3042"
+ d="m 547.55325,231.83317 c -0.0986,2.38736 42.68285,2.06721 42.92078,0 l 3.59546,21.34804 -2.92131,0 c -3.03427,-7.13555 -5.22493,-14.772 -17.52786,-16.40428 l 0,59.32506 5.6179,1.34829 0,3.37075 -20.67388,0 0,-3.14603 5.73213,-1.53592 0,-59.58687 c -6.03051,0.84659 -12.06101,1.92591 -18.09152,15.95485 l -2.69659,0 z"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ <path
+ sodipodi:nodetypes="cccccccccccccccccccccc"
+ inkscape:connector-curvature="0"
+ id="path3044"
+ d="m 505.54497,324.79984 20.44916,0 0,-3.14603 -5.73026,-1.0674 0,-28.9322 9.10101,-0.11236 c 3.72116,2.12145 5.47848,6.20675 6.51676,11.01109 l 2.6966,-0.22472 0,-24.26934 -3.14603,0 c -1.29037,3.73961 -0.27178,8.24887 -6.51677,10.33694 l -8.65157,-0.11236 0,-27.13447 c 15.85546,-0.55543 21.31445,0.2909 29.77489,16.46046 l 2.92131,0 -4.71904,-22.02218 c -0.99909,3.14752 -12.86953,2.24716 -42.69606,2.24716 l 0.0281,2.72468 c 3.6912,0.29962 3.59931,0.57116 5.14038,0.87078 l -0.0562,59.2127 -5.05612,1.51683 z"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3046"
+ d="m 612.29661,257.60381 -27.75,0.125 0,3.125 5.15625,0.90625 0,58.875 -5.40625,1.46875 0,2.46875 27.65625,0 c 32.83223,-0.42026 25.38759,-33.26589 5.15625,-36.40625 19.87788,-2.96053 19.96965,-30.36986 -4.8125,-30.5625 z m -2.375,3.34375 c 16.40273,0 18.2699,25.53125 -1.6875,25.53125 l -9.21875,0 0,-25.5 10.90625,-0.0312 z m -1.1875,28.84375 c 10.10368,0.81978 14.43973,6.63609 15.71875,15.6875 2.50148,17.70261 -22.21516,16.36814 -25.375,15.15625 l 0,-30.78125 9.65625,-0.0625 z"
+ style="fill:#000000;fill-opacity:1;stroke:none" />
+ </g>
+ <g
+ transform="matrix(0.83116569,0,0,0.83116569,-412.777,-169.25441)"
+ id="g3035">
+ <path
+ style="fill:url(#linearGradient3848);fill-opacity:1;stroke:none"
+ d="m 547.55325,231.83317 c -0.0986,2.38736 42.68285,2.06721 42.92078,0 l 3.59546,21.34804 -2.92131,0 c -3.03427,-7.13555 -5.22493,-14.772 -17.52786,-16.40428 l 0,59.32506 5.6179,1.34829 0,3.37075 -20.67388,0 0,-3.14603 5.73213,-1.53592 0,-59.58687 c -6.03051,0.84659 -12.06101,1.92591 -18.09152,15.95485 l -2.69659,0 z"
+ id="path2987"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccccc" />
+ <path
+ style="fill:url(#linearGradient3850);fill-opacity:1;stroke:none"
+ d="m 505.54497,324.79984 20.44916,0 0,-3.14603 -5.73026,-1.0674 0,-28.9322 9.0706,-0.0112 c 7.23229,-0.009 6.17144,5.6893 6.17144,10.66981 l 3.07233,0 0,-24.25397 -3.14603,0 c 0.0737,4.503 1.04842,10.24675 -6.09774,10.23698 l -9.0706,-0.0124 0,-27.13447 c 15.85546,-0.55543 21.31445,0.2909 29.77489,16.46046 l 2.92131,0 -4.71904,-22.02218 c -0.99909,3.14752 -12.86953,2.24716 -42.69606,2.24716 l 0.0281,2.72468 c 3.6912,0.29962 3.59931,0.57116 5.14038,0.87078 l -0.0562,59.2127 -5.05612,1.51683 z"
+ id="path2989"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccsccccsccccccccccc" />
+ <path
+ style="fill:url(#linearGradient3852);fill-opacity:1;stroke:none"
+ d="m 612.29661,257.60381 -27.75,0.125 0,3.125 5.15625,0.90625 0,58.875 -5.40625,1.46875 0,2.46875 27.65625,0 c 32.83223,-0.42026 25.38759,-33.26589 5.15625,-36.40625 19.87788,-2.96053 19.96965,-30.36986 -4.8125,-30.5625 z m -2.375,3.34375 c 16.40273,0 18.2699,25.53125 -1.6875,25.53125 l -9.21875,0 0,-25.5 10.90625,-0.0312 z m -1.1875,28.84375 c 10.10368,0.81978 14.43973,6.63609 15.71875,15.6875 2.50148,17.70261 -22.21516,16.36814 -25.375,15.15625 l 0,-30.78125 9.65625,-0.0625 z"
+ id="path2995"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/sources/gear.svg b/launcher/resources/sources/gear.svg
new file mode 100644
index 00000000..c0169aec
--- /dev/null
+++ b/launcher/resources/sources/gear.svg
@@ -0,0 +1,434 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="gear.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/gear128.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4030">
+ <stop
+ id="stop4032"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ <stop
+ id="stop4034"
+ offset="1"
+ style="stop-color:#0c493f;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3954">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3956" />
+ <stop
+ style="stop-color:#0f5f52;stop-opacity:1;"
+ offset="1"
+ id="stop3958" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3954-9">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3956-4" />
+ <stop
+ style="stop-color:#0f5f52;stop-opacity:1;"
+ offset="1"
+ id="stop3958-2" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954"
+ id="radialGradient4230"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954"
+ id="radialGradient4250"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954"
+ id="radialGradient3069"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient3917"
+ x1="43.78125"
+ y1="42.687496"
+ x2="15.453125"
+ y2="21.578121"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1428571,0,0,1.1428571,-2.2857143,-2.28571)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3911-5"
+ id="linearGradient3929-9"
+ gradientUnits="userSpaceOnUse"
+ x1="40.5"
+ y1="36.999996"
+ x2="9"
+ y2="19.499996" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3911-5">
+ <stop
+ style="stop-color:#8c3800;stop-opacity:1"
+ offset="0"
+ id="stop3913-6" />
+ <stop
+ style="stop-color:#ff6701;stop-opacity:1"
+ offset="1"
+ id="stop3915-3" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient3986"
+ gradientUnits="userSpaceOnUse"
+ x1="28"
+ y1="28"
+ x2="44"
+ y2="32" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient3925-4"
+ x1="16"
+ y1="16"
+ x2="48"
+ y2="48"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient3919-9">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop3921-2" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop3923-3" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-12,-12)"
+ y2="48"
+ x2="48"
+ y1="16"
+ x1="16"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient4025"
+ xlink:href="#linearGradient3919-9"
+ inkscape:collect="always" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3919-9-3">
+ <stop
+ style="stop-color:#ff9e06;stop-opacity:1"
+ offset="0"
+ id="stop3921-2-9" />
+ <stop
+ style="stop-color:#af4600;stop-opacity:1"
+ offset="1"
+ id="stop3923-3-4" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-8,-8)"
+ y2="48"
+ x2="48"
+ y1="16"
+ x1="16"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient4065"
+ xlink:href="#linearGradient3919-9-3"
+ inkscape:collect="always" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3919-9-35">
+ <stop
+ style="stop-color:#ff9e06;stop-opacity:1"
+ offset="0"
+ id="stop3921-2-3" />
+ <stop
+ style="stop-color:#af4600;stop-opacity:1"
+ offset="1"
+ id="stop3923-3-5" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-8,-8)"
+ y2="48"
+ x2="48"
+ y1="16"
+ x1="16"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient4098"
+ xlink:href="#linearGradient3919-9-35"
+ inkscape:collect="always" />
+ <linearGradient
+ gradientTransform="translate(92.25,1040.3622)"
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient3986-9"
+ gradientUnits="userSpaceOnUse"
+ x1="28"
+ y1="28"
+ x2="44"
+ y2="32" />
+ <linearGradient
+ id="linearGradient3919-9-7">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop3921-2-93" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop3923-3-0" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient3917-1"
+ x1="43.78125"
+ y1="42.687496"
+ x2="15.453125"
+ y2="21.578121"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1428571,0,0,1.1428571,89.96429,1038.0765)" />
+ <linearGradient
+ id="linearGradient4172">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop4174" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop4176" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(92.25,1040.3622)"
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient3925-4-8"
+ x1="16"
+ y1="16"
+ x2="48"
+ y2="48"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient4179">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop4181" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop4183" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5178-5"
+ id="linearGradient5184-3"
+ x1="-58.570175"
+ y1="1003.1377"
+ x2="-18.635777"
+ y2="1034.4441"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient5178-5">
+ <stop
+ style="stop-color:#13daba;stop-opacity:0.50196081;"
+ offset="0"
+ id="stop5180-8" />
+ <stop
+ style="stop-color:#03251f;stop-opacity:1"
+ offset="1"
+ id="stop5182-3" />
+ </linearGradient>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4462-8">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.68"
+ id="feGaussianBlur4464-6" />
+ </filter>
+ <linearGradient
+ gradientTransform="translate(164.98654,52.0355)"
+ y2="1034.4441"
+ x2="-18.635777"
+ y1="1003.1377"
+ x1="-58.570175"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient4197"
+ xlink:href="#linearGradient5178-5"
+ inkscape:collect="always" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5178-5"
+ id="linearGradient4308"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(164.98654,52.0355)"
+ x1="-58.570175"
+ y1="1003.1377"
+ x2="-18.635777"
+ y2="1034.4441" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient4310"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(92.25,1040.3622)"
+ x1="16"
+ y1="16"
+ x2="48"
+ y2="48" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient4312"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1428571,0,0,1.1428571,89.96429,1038.0765)"
+ x1="43.78125"
+ y1="42.687496"
+ x2="15.453125"
+ y2="21.578121" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient4314"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(92.25,1040.3622)"
+ x1="28"
+ y1="28"
+ x2="44"
+ y2="32" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2"
+ inkscape:cx="63.31906"
+ inkscape:cy="60.240415"
+ inkscape:document-units="px"
+ inkscape:current-layer="g4300"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ spacingx="1px"
+ spacingy="1px" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="11.25,96.5"
+ id="guide4252" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-988.36218)">
+ <g
+ id="g4300"
+ transform="translate(-92.25,-52.00002)">
+ <path
+ sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccc"
+ inkscape:connector-curvature="0"
+ id="rect4162-6"
+ d="m 120.25,1044.3622 -4,4 0,4 -3,-3 -6,0 -6,6 0,6 3,3 -4,0 -4,4 0,8 4,4 4,0 -3,3 0,6 6,6 6,0 3,-3 0,4 4,4 8,0 4,-4 0,-4 3,3 6,0 6,-6 0,-6 -3,-3 4,0 4,-4 0,-8 -4,-4 -4,0 3,-3 0,-6 -6,-6 -6,0 -3,3 0,-4 -4,-4 z"
+ style="color:#000000;fill:url(#linearGradient4308);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter4462-8);enable-background:accumulate" />
+ <path
+ id="rect3071-3"
+ d="m 120.25,1048.3622 0,4.4062 c -0.22274,0.045 -0.43627,0.1037 -0.65625,0.1563 -0.4105,0.098 -0.81848,0.19 -1.21875,0.3125 -0.0218,0.01 -0.0408,0.024 -0.0625,0.031 -0.59186,0.1833 -1.18309,0.3898 -1.75,0.625 -0.50056,0.2088 -0.98977,0.4401 -1.46875,0.6875 -0.0827,0.043 -0.16797,0.081 -0.25,0.125 -0.55648,0.2973 -1.10149,0.6219 -1.625,0.9687 l -3.125,-3.125 -5.65625,5.6562 3.125,3.125 c -0.34167,0.5158 -0.67512,1.046 -0.96875,1.5938 -0.048,0.09 -0.0783,0.1905 -0.125,0.2812 -0.23239,0.45 -0.45783,0.9059 -0.65625,1.375 -0.0127,0.03 -0.0187,0.063 -0.0312,0.094 -0.23753,0.5694 -0.44005,1.1553 -0.625,1.75 -0.13065,0.4218 -0.24046,0.848 -0.34375,1.2812 -0.0526,0.22 -0.11102,0.4335 -0.15625,0.6563 l -4.40625,0 0,8 4.40625,0 c 0.0452,0.2227 0.10369,0.4363 0.15625,0.6562 0.0979,0.4105 0.19004,0.8185 0.3125,1.2188 0.007,0.022 0.0245,0.041 0.0312,0.062 0.18332,0.5918 0.38983,1.1831 0.625,1.75 0.2088,0.5005 0.44015,0.9897 0.6875,1.4687 0.0467,0.091 0.077,0.1914 0.125,0.2813 0.0175,0.033 0.0448,0.061 0.0625,0.094 0.27878,0.5148 0.58417,1.0139 0.90625,1.5 l -3.125,3.125 5.65625,5.6563 3.125,-3.125 c 0.52351,0.3468 1.06852,0.6714 1.625,0.9687 0.082,0.044 0.16729,0.082 0.25,0.125 0.45001,0.2324 0.90589,0.4579 1.375,0.6563 0.0303,0.013 0.0634,0.019 0.0937,0.031 0.56941,0.2376 1.15536,0.4401 1.75,0.625 0.42179,0.1307 0.84805,0.2405 1.28125,0.3438 0.21027,0.05 0.41221,0.1127 0.625,0.1562 0.01,0 0.0212,0 0.0312,0 l 0,4.4063 8,0 0,-4.4063 c 0.22283,-0.045 0.43616,-0.1037 0.65625,-0.1562 0.92356,-0.2207 1.81865,-0.4999 2.6875,-0.8438 0.61879,-0.2435 1.22575,-0.5106 1.8125,-0.8125 0.0825,-0.043 0.16813,-0.081 0.25,-0.125 0.55807,-0.2971 1.10017,-0.621 1.625,-0.9687 l 3.125,3.125 5.65625,-5.6563 -3.125,-3.125 c 0.34276,-0.5174 0.6751,-1.044 0.96875,-1.5937 0.0483,-0.09 0.0781,-0.1904 0.125,-0.2813 0.30187,-0.5867 0.56896,-1.1937 0.8125,-1.8125 0.34385,-0.8688 0.62307,-1.7639 0.84375,-2.6875 0.0525,-0.2201 0.111,-0.4334 0.15625,-0.6562 l 4.40625,0 0,-8 -4.40625,0 c -0.002,-0.01 0.002,-0.021 0,-0.031 -0.0435,-0.2127 -0.10613,-0.4148 -0.15625,-0.625 -0.22068,-0.9235 -0.4999,-1.8186 -0.84375,-2.6875 -0.24354,-0.6188 -0.51063,-1.2257 -0.8125,-1.8125 -0.0469,-0.091 -0.0767,-0.1912 -0.125,-0.2812 -0.29365,-0.5497 -0.62599,-1.0764 -0.96875,-1.5938 l 3.125,-3.125 -5.65625,-5.6562 -3.125,3.125 c -0.48499,-0.3213 -0.98661,-0.6273 -1.5,-0.9062 -0.0409,-0.022 -0.0839,-0.041 -0.125,-0.062 -0.0819,-0.044 -0.16745,-0.082 -0.25,-0.125 -0.58675,-0.3019 -1.19371,-0.569 -1.8125,-0.8125 -0.86885,-0.3439 -1.76394,-0.6231 -2.6875,-0.8438 -0.21019,-0.05 -0.4123,-0.1127 -0.625,-0.1562 -0.01,0 -0.0214,0 -0.0312,0 l 0,-4.4063 -8,0 z m 4,20 c 2.20914,0 4,1.7908 4,4 0,2.2091 -1.79086,4 -4,4 -0.27614,0 -0.55211,-0.041 -0.8125,-0.094 -1.82273,-0.373 -3.1875,-1.9732 -3.1875,-3.9062 0,-0.2762 0.0405,-0.5521 0.0937,-0.8125 0.37299,-1.8228 1.97325,-3.1875 3.90625,-3.1875 z"
+ style="color:#000000;fill:url(#linearGradient4310);fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path3874-0"
+ d="m 124.25,1056.3622 c -8.83656,0 -16,7.1634 -16,16 0,8.8365 7.16344,16 16,16 8.83656,0 16,-7.1635 16,-16 0,-8.8366 -7.16344,-16 -16,-16 z m 0,12 c 2.20914,0 4,1.7908 4,4 0,2.2091 -1.79086,4 -4,4 -2.20914,0 -4,-1.7909 -4,-4 0,-2.2092 1.79086,-4 4,-4 z"
+ style="fill:url(#linearGradient4312);fill-opacity:1;stroke:none"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path3876-7"
+ d="m 124.25,1064.3622 c -4.41828,0 -8,3.5817 -8,8 0,4.4183 3.58172,8 8,8 4.41828,0 8,-3.5817 8,-8 0,-4.4183 -3.58172,-8 -8,-8 z m 0,4 c 2.20914,0 4,1.7908 4,4 0,2.2091 -1.79086,4 -4,4 -2.20914,0 -4,-1.7909 -4,-4 0,-2.2092 1.79086,-4 4,-4 z"
+ style="fill:url(#linearGradient4314);fill-opacity:1;stroke:none"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3927-9"
+ d="m 124.25,1056.3622 c -8.83656,0 -16,7.1634 -16,16 0,4.8907 2.19848,9.2526 5.65625,12.1875 -2.28249,-2.7658 -3.65625,-6.3215 -3.65625,-10.1875 0,-8.8366 7.16344,-16 16,-16 3.86599,0 7.42176,1.3737 10.1875,3.6562 -2.93487,-3.4577 -7.29683,-5.6562 -12.1875,-5.6562 z"
+ style="fill:#000000;fill-opacity:0.39130435;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3970-3"
+ d="m 131.8125,1069.7997 c 0.27256,0.8054 0.4375,1.665 0.4375,2.5625 0,4.4183 -3.58172,8 -8,8 -2.72065,0 -5.11721,-1.3623 -6.5625,-3.4375 1.06926,3.1597 4.04169,5.4375 7.5625,5.4375 4.41828,0 8,-3.5817 8,-8 0,-1.6976 -0.53567,-3.2676 -1.4375,-4.5625 z"
+ style="fill:#000000;fill-opacity:0.39130435;stroke:none" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/sources/herobrine.svg b/launcher/resources/sources/herobrine.svg
new file mode 100644
index 00000000..7392ba36
--- /dev/null
+++ b/launcher/resources/sources/herobrine.svg
@@ -0,0 +1,583 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="herobrine.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/herobrine.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ id="filter3927"
+ x="-0.10864197"
+ width="1.2172839"
+ y="-0.10864198"
+ height="1.217284">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.1769547"
+ id="feGaussianBlur3929" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4035"
+ x="-0.066666667"
+ width="1.1333333"
+ y="-0.13333333"
+ height="1.2666667">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.16666667"
+ id="feGaussianBlur4037" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#484848"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="-64.2584"
+ inkscape:cy="64.238479"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2996"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1020.3622)">
+ <path
+ style="fill:#170b28;fill-opacity:1;stroke:none;filter:url(#filter3927)"
+ d="m 5.0606609,1024.2461 -1,1 0,24 1,1 24.0000001,0 1,-1 0,-24 -1,-1 z"
+ id="rect3223-4"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccc" />
+ <path
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none"
+ d="m 6,1024.3622 20,0 c 1,0 2,1 2,2 l 0,7 -3,0 0,-3 -18,0 0,3 -3,0 0,-7.005 c 0,-0.995 1,-1.995 2,-1.995 z"
+ id="rect3978"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccc" />
+ <path
+ transform="translate(0,1020.3622)"
+ style="fill:#421d0a;fill-opacity:1;stroke:none"
+ d="m 10,22 3,0 0,3 6,0 0,-3 3,0 0,6 -12,0 z"
+ id="rect3981"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccc" />
+ <path
+ transform="translate(0,1020.3622)"
+ style="fill:#9c694c;fill-opacity:1;stroke:none"
+ d="m 7,10 18,0 0,3 3,0 0,13 c 0,1 -1,2 -2,2 l -4,0 0,-6 -3,0 0,-3 6,0 0,-3 -6,0 0,3 -6,0 0,-3 -6,0 0,3 6,0 0,3 -3,0 0,6 -4,0 C 5,28 4,27 4,26 l 0,-10 0,-3 3,0 z"
+ id="rect3941"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccccccccccccccccccccc" />
+ <rect
+ style="fill:#bb8972;fill-opacity:1;stroke:none"
+ id="rect3939"
+ width="24"
+ height="24"
+ x="42"
+ y="1052.6122" />
+ <rect
+ y="1024.3622"
+ x="6.9999995"
+ height="3"
+ width="3"
+ id="rect3011-9"
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#2f1f0f;fill-opacity:1;stroke:none"
+ id="rect3013-0"
+ width="3"
+ height="3"
+ x="10"
+ y="1024.3622" />
+ <rect
+ style="fill:#281c0b;fill-opacity:1;stroke:none"
+ id="rect3017-9"
+ width="3"
+ height="3"
+ x="13"
+ y="1024.3622" />
+ <rect
+ y="1024.3622"
+ x="16"
+ height="3"
+ width="3"
+ id="rect3019-6"
+ style="fill:#241808;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#261a0a;fill-opacity:1;stroke:none"
+ id="rect3021-8"
+ width="3"
+ height="3"
+ x="19"
+ y="1024.3622" />
+ <rect
+ y="1024.3622"
+ x="22"
+ height="3"
+ width="3"
+ id="rect3023-6"
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none" />
+ <rect
+ y="1027.3622"
+ x="3.9999998"
+ height="3"
+ width="3"
+ id="rect3027-7"
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none"
+ id="rect3029-6"
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1027.3622" />
+ <rect
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none"
+ id="rect3033-4"
+ width="3"
+ height="3"
+ x="10"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="13"
+ height="3"
+ width="3"
+ id="rect3035-4"
+ style="fill:#332411;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#422a12;fill-opacity:1;stroke:none"
+ id="rect3037-2"
+ width="3"
+ height="3"
+ x="16"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="19"
+ height="3"
+ width="3"
+ id="rect3039-4"
+ style="fill:#3f2a15;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#2c1e0e;fill-opacity:1;stroke:none"
+ id="rect3041-2"
+ width="3"
+ height="3"
+ x="22"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="25"
+ height="3"
+ width="3"
+ id="rect3043-1"
+ style="fill:#281c0b;fill-opacity:1;stroke:none" />
+ <rect
+ y="1030.3622"
+ x="3.9999998"
+ height="3"
+ width="3"
+ id="rect3045-3"
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none" />
+ <rect
+ y="1030.3622"
+ x="25"
+ height="3"
+ width="3"
+ id="rect3061-7"
+ style="fill:#342512;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#6a4030;fill-opacity:1;stroke:none"
+ id="rect3177-4"
+ width="6"
+ height="3"
+ x="13"
+ y="1039.3622" />
+ <rect
+ style="fill:#41210c;fill-opacity:1;stroke:none"
+ id="rect3085"
+ width="3"
+ height="3"
+ x="10"
+ y="1042.3622" />
+ <rect
+ style="fill:#8a4c3d;fill-opacity:1;stroke:none"
+ id="rect3089"
+ width="6"
+ height="3"
+ x="13"
+ y="1042.3622" />
+ <rect
+ y="1042.3622"
+ x="19"
+ height="3"
+ width="3"
+ id="rect3091"
+ style="fill:#41210c;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#421d0a;fill-opacity:1;stroke:none"
+ id="rect3093"
+ width="3"
+ height="3"
+ x="13"
+ y="1045.3622" />
+ <rect
+ y="1045.3622"
+ x="19"
+ height="3"
+ width="3"
+ id="rect3095"
+ style="fill:#45220e;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#45220e;fill-opacity:1;stroke:none"
+ id="rect3097"
+ width="3"
+ height="3"
+ x="10"
+ y="1045.3622" />
+ <rect
+ y="1045.3622"
+ x="16"
+ height="3"
+ width="3"
+ id="rect3099"
+ style="fill:#45220e;fill-opacity:1;stroke:none" />
+ <g
+ id="g3944"
+ transform="translate(-28,1.7382813e-5)">
+ <rect
+ y="1030.3622"
+ x="35"
+ height="3"
+ width="3"
+ id="rect3047-1"
+ style="fill:#b6896c;fill-opacity:1;stroke:none" />
+ <rect
+ y="1030.3622"
+ x="38"
+ height="3"
+ width="3"
+ id="rect3051-9"
+ style="fill:#bd8e72;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#c69680;fill-opacity:1;stroke:none"
+ id="rect3053-2"
+ width="3"
+ height="3"
+ x="41"
+ y="1030.3622" />
+ <rect
+ y="1030.3622"
+ x="44"
+ height="3"
+ width="3"
+ id="rect3055-9"
+ style="fill:#bd8b72;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#bd8e74;fill-opacity:1;stroke:none"
+ id="rect3057-7"
+ width="3"
+ height="3"
+ x="47"
+ y="1030.3622" />
+ <rect
+ y="1030.3622"
+ x="50"
+ height="3"
+ width="3"
+ id="rect3059-2"
+ style="fill:#ac765a;fill-opacity:1;stroke:none" />
+ <rect
+ y="1033.3622"
+ x="32"
+ height="3"
+ width="3"
+ id="rect3063-8"
+ style="fill:#aa7d66;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#b4846d;fill-opacity:1;stroke:none"
+ id="rect3065-6"
+ width="3"
+ height="3"
+ x="35"
+ y="1033.3622" />
+ <rect
+ style="fill:#aa7d66;fill-opacity:1;stroke:none"
+ id="rect3069-9"
+ width="3"
+ height="3"
+ x="38"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="41"
+ height="3"
+ width="3"
+ id="rect3071-7"
+ style="fill:#ad806d;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#9c725c;fill-opacity:1;stroke:none"
+ id="rect3073-1"
+ width="3"
+ height="3"
+ x="44"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="47"
+ height="3"
+ width="3"
+ id="rect3075-8"
+ style="fill:#bb8972;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#9c694c;fill-opacity:1;stroke:none"
+ id="rect3077-8"
+ width="3"
+ height="3"
+ x="50"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="53"
+ height="3"
+ width="3"
+ id="rect3079-2"
+ style="fill:#9c694c;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#b4846d;fill-opacity:1;stroke:none"
+ id="rect3153-6"
+ width="3"
+ height="3"
+ x="32"
+ y="1036.3622" />
+ <rect
+ style="fill:#b57b67;fill-opacity:1;stroke:none"
+ id="rect3159-7"
+ width="3"
+ height="3"
+ x="41"
+ y="1036.3622" />
+ <rect
+ y="1036.3622"
+ x="44"
+ height="3"
+ width="3"
+ id="rect3161-0"
+ style="fill:#bb8972;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#aa7d66;fill-opacity:1;stroke:none"
+ id="rect3167-3"
+ width="3"
+ height="3"
+ x="53"
+ y="1036.3622" />
+ <rect
+ y="1039.3622"
+ x="32"
+ height="3"
+ width="3"
+ id="rect3169-4"
+ style="fill:#9c6346;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#b37b62;fill-opacity:1;stroke:none"
+ id="rect3171-8"
+ width="3"
+ height="3"
+ x="35"
+ y="1039.3622" />
+ <rect
+ style="fill:#b78272;fill-opacity:1;stroke:none"
+ id="rect3175-0"
+ width="3"
+ height="3"
+ x="38"
+ y="1039.3622" />
+ <rect
+ y="1039.3622"
+ x="47"
+ height="3"
+ width="3"
+ id="rect3181-9"
+ style="fill:#be886c;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#a26a47;fill-opacity:1;stroke:none"
+ id="rect3183-7"
+ width="3"
+ height="3"
+ x="50"
+ y="1039.3622" />
+ <rect
+ y="1039.3622"
+ x="53"
+ height="3"
+ width="3"
+ id="rect3185-6"
+ style="fill:#805334;fill-opacity:1;stroke:none" />
+ <rect
+ y="1042.3622"
+ x="32"
+ height="3"
+ width="3"
+ id="rect3187-1"
+ style="fill:#905e43;fill-opacity:1;stroke:none" />
+ <rect
+ y="1042.3622"
+ x="53"
+ height="3"
+ width="3"
+ id="rect3203-8"
+ style="fill:#815339;fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#6f452c;fill-opacity:1;stroke:none"
+ d="m 32,1045.3622 3,0 0,3 -1,0 c -1,0 -2,-1 -2,-2 z"
+ id="rect3205-4"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <rect
+ y="1045.3622"
+ x="35"
+ height="3"
+ width="3"
+ id="rect3207-2"
+ style="fill:#6d432a;fill-opacity:1;stroke:none" />
+ <rect
+ y="1045.3622"
+ x="50"
+ height="3"
+ width="3"
+ id="rect3219-4"
+ style="fill:#83553b;fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#7a4e33;fill-opacity:1;stroke:none"
+ d="m 53,1045.3622 3,0 0,1 c 0,1 -1,2 -2,2 l -1,0 z"
+ id="rect3221-5"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <rect
+ style="fill:#965f40;fill-opacity:1;stroke:none"
+ id="rect3083"
+ width="3"
+ height="3"
+ x="35"
+ y="1042.3622" />
+ <rect
+ y="1042.3622"
+ x="50"
+ height="3"
+ width="3"
+ id="rect3101"
+ style="fill:#8f5e3e;fill-opacity:1;stroke:none" />
+ </g>
+ <g
+ id="g4045"
+ transform="matrix(1.05,0,0,1.05,14.2,-51.893092)">
+ <rect
+ y="1036.3622"
+ x="-7"
+ height="3"
+ width="6"
+ id="rect4011"
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4035)" />
+ <rect
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4035)"
+ id="rect4039"
+ width="6"
+ height="3"
+ x="-7"
+ y="1036.3622" />
+ <rect
+ y="1036.3622"
+ x="-7"
+ height="3"
+ width="6"
+ id="rect4041"
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4035)" />
+ <rect
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4035)"
+ id="rect4043"
+ width="6"
+ height="3"
+ x="-7"
+ y="1036.3622" />
+ </g>
+ <g
+ transform="matrix(1.05,0,0,1.05,26.2,-51.893092)"
+ id="g4051">
+ <rect
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4035)"
+ id="rect4053"
+ width="6"
+ height="3"
+ x="-7"
+ y="1036.3622" />
+ <rect
+ y="1036.3622"
+ x="-7"
+ height="3"
+ width="6"
+ id="rect4055"
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4035)" />
+ <rect
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4035)"
+ id="rect4057"
+ width="6"
+ height="3"
+ x="-7"
+ y="1036.3622" />
+ <rect
+ y="1036.3622"
+ x="-7"
+ height="3"
+ width="6"
+ id="rect4059"
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4035)" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/sources/magitech.svg b/launcher/resources/sources/magitech.svg
new file mode 100644
index 00000000..c6dd6bc0
--- /dev/null
+++ b/launcher/resources/sources/magitech.svg
@@ -0,0 +1,886 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="magitech.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/magitech128.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4053">
+ <stop
+ id="stop4055"
+ offset="0"
+ style="stop-color:#19c6a1;stop-opacity:0.75294119;" />
+ <stop
+ id="stop4057"
+ offset="0.30864197"
+ style="stop-color:#a4f4e3;stop-opacity:1;" />
+ <stop
+ style="stop-color:#d9f7f1;stop-opacity:1;"
+ offset="0.65432096"
+ id="stop4059" />
+ <stop
+ id="stop4061"
+ offset="1"
+ style="stop-color:#009b78;stop-opacity:0.75294119;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4001">
+ <stop
+ id="stop4003"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ <stop
+ id="stop4005"
+ offset="1"
+ style="stop-color:#0f5f52;stop-opacity:0" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3882">
+ <stop
+ style="stop-color:#19c6a1;stop-opacity:1;"
+ offset="0"
+ id="stop3884" />
+ <stop
+ style="stop-color:#a4f4e3;stop-opacity:1;"
+ offset="0.30864197"
+ id="stop3894" />
+ <stop
+ id="stop3935"
+ offset="0.65432096"
+ style="stop-color:#d9f7f1;stop-opacity:1;" />
+ <stop
+ style="stop-color:#009b7a;stop-opacity:1;"
+ offset="1"
+ id="stop3886" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5178">
+ <stop
+ style="stop-color:#13daba;stop-opacity:0.50196081;"
+ offset="0"
+ id="stop5180" />
+ <stop
+ style="stop-color:#03251f;stop-opacity:1"
+ offset="1"
+ id="stop5182" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4030">
+ <stop
+ id="stop4032"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ <stop
+ id="stop4034"
+ offset="1"
+ style="stop-color:#0c493f;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3954">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3956" />
+ <stop
+ style="stop-color:#0f5f52;stop-opacity:1;"
+ offset="1"
+ id="stop3958" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3954-9">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3956-4" />
+ <stop
+ style="stop-color:#0f5f52;stop-opacity:1;"
+ offset="1"
+ id="stop3958-2" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954"
+ id="radialGradient4230"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954"
+ id="radialGradient4250"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient3917"
+ x1="43.78125"
+ y1="42.687496"
+ x2="15.453125"
+ y2="21.578121"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1428571,0,0,1.1428571,-2.2857143,-2.28571)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3911-5"
+ id="linearGradient3929-9"
+ gradientUnits="userSpaceOnUse"
+ x1="40.5"
+ y1="36.999996"
+ x2="9"
+ y2="19.499996" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3911-5">
+ <stop
+ style="stop-color:#8c3800;stop-opacity:1"
+ offset="0"
+ id="stop3913-6" />
+ <stop
+ style="stop-color:#ff6701;stop-opacity:1"
+ offset="1"
+ id="stop3915-3" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient3986"
+ gradientUnits="userSpaceOnUse"
+ x1="28"
+ y1="28"
+ x2="44"
+ y2="32" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient3925-4"
+ x1="16"
+ y1="16"
+ x2="48"
+ y2="48"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient3919-9">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop3921-2" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop3923-3" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-12,-12)"
+ y2="48"
+ x2="48"
+ y1="16"
+ x1="16"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient4025"
+ xlink:href="#linearGradient3919-9"
+ inkscape:collect="always" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3919-9-3">
+ <stop
+ style="stop-color:#ff9e06;stop-opacity:1"
+ offset="0"
+ id="stop3921-2-9" />
+ <stop
+ style="stop-color:#af4600;stop-opacity:1"
+ offset="1"
+ id="stop3923-3-4" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-8,-8)"
+ y2="48"
+ x2="48"
+ y1="16"
+ x1="16"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient4065"
+ xlink:href="#linearGradient3919-9-3"
+ inkscape:collect="always" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3919-9-35">
+ <stop
+ style="stop-color:#ff9e06;stop-opacity:1"
+ offset="0"
+ id="stop3921-2-3" />
+ <stop
+ style="stop-color:#af4600;stop-opacity:1"
+ offset="1"
+ id="stop3923-3-5" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-8,-8)"
+ y2="48"
+ x2="48"
+ y1="16"
+ x1="16"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient4098"
+ xlink:href="#linearGradient3919-9-35"
+ inkscape:collect="always" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954-5"
+ id="radialGradient4024"
+ gradientUnits="userSpaceOnUse"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)" />
+ <linearGradient
+ id="linearGradient3954-5">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3956-5" />
+ <stop
+ style="stop-color:#0f5f52;stop-opacity:1;"
+ offset="1"
+ id="stop3958-3" />
+ </linearGradient>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter3916-4"
+ x="-0.11491533"
+ width="1.2298307"
+ y="-0.11548609"
+ height="1.2309722">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.6059988"
+ id="feGaussianBlur3918-4" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4082-1"
+ x="-0.13465963"
+ width="1.2693193"
+ y="-0.12490681"
+ height="1.2498136">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.763901"
+ id="feGaussianBlur4084-4" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient4440"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1428571,0,0,1.1428571,-2.2857143,-2.28571)"
+ x1="43.78125"
+ y1="42.687496"
+ x2="15.453125"
+ y2="21.578121" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient4442"
+ gradientUnits="userSpaceOnUse"
+ x1="28"
+ y1="28"
+ x2="44"
+ y2="32" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954-5"
+ id="radialGradient4453"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.3784287,1.5357628,-0.89303843,0.80155051,16.46751,-42.794012)"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20" />
+ <filter
+ inkscape:collect="always"
+ id="filter4462">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.68"
+ id="feGaussianBlur4464" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-1"
+ id="linearGradient4438-1"
+ gradientUnits="userSpaceOnUse"
+ x1="16"
+ y1="16"
+ x2="48"
+ y2="48" />
+ <linearGradient
+ id="linearGradient3919-9-1">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop3921-2-92" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop3923-3-6" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9"
+ id="linearGradient5119"
+ gradientUnits="userSpaceOnUse"
+ x1="16"
+ y1="16"
+ x2="48"
+ y2="48" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5178"
+ id="linearGradient5184"
+ x1="10.429825"
+ y1="1007.6377"
+ x2="38.114223"
+ y2="1024.6941"
+ gradientUnits="userSpaceOnUse" />
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter3992">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.44"
+ id="feGaussianBlur3994" />
+ </filter>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3954-1"
+ id="radialGradient4024-7"
+ gradientUnits="userSpaceOnUse"
+ cx="32"
+ cy="32"
+ fx="32"
+ fy="32"
+ r="20"
+ gradientTransform="matrix(1.1486906,1.2798015,-0.74419869,0.66795831,19.056258,-30.328315)" />
+ <linearGradient
+ id="linearGradient3954-1">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3956-2" />
+ <stop
+ style="stop-color:#0f5f52;stop-opacity:1;"
+ offset="1"
+ id="stop3958-9" />
+ </linearGradient>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter3884">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.505"
+ id="feGaussianBlur3886" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter3898"
+ x="-0.20110182"
+ width="1.4022036"
+ y="-0.20210065"
+ height="1.4042013">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.0604979"
+ id="feGaussianBlur3900" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter3916"
+ x="-0.11491533"
+ width="1.2298307"
+ y="-0.11548609"
+ height="1.2309722">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.6059988"
+ id="feGaussianBlur3918" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4110"
+ x="-0.17716113"
+ width="1.3543223"
+ y="-0.17804106"
+ height="1.3560821">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.93424815"
+ id="feGaussianBlur4112" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4082"
+ x="-0.13465963"
+ width="1.2693193"
+ y="-0.12490681"
+ height="1.2498136">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.763901"
+ id="feGaussianBlur4084" />
+ </filter>
+ <linearGradient
+ gradientTransform="translate(-72.736544,988.32667)"
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient3986-9"
+ gradientUnits="userSpaceOnUse"
+ x1="28"
+ y1="28"
+ x2="44"
+ y2="32" />
+ <linearGradient
+ id="linearGradient3919-9-7">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop3921-2-93" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop3923-3-0" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient3917-1"
+ x1="43.78125"
+ y1="42.687496"
+ x2="15.453125"
+ y2="21.578121"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1428571,0,0,1.1428571,-75.022254,986.04097)" />
+ <linearGradient
+ id="linearGradient5362">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop5364" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop5366" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-72.736544,988.32667)"
+ inkscape:collect="always"
+ xlink:href="#linearGradient3919-9-7"
+ id="linearGradient3925-4-8"
+ x1="16"
+ y1="16"
+ x2="48"
+ y2="48"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient5369">
+ <stop
+ style="stop-color:#fff306;stop-opacity:1;"
+ offset="0"
+ id="stop5371" />
+ <stop
+ style="stop-color:#ff7f01;stop-opacity:1;"
+ offset="1"
+ id="stop5373" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3882"
+ id="linearGradient3888"
+ x1="42.125"
+ y1="10.375"
+ x2="24.75"
+ y2="56.125"
+ gradientUnits="userSpaceOnUse"
+ spreadMethod="pad" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5178-5"
+ id="linearGradient5184-3"
+ x1="-58.570175"
+ y1="1003.1377"
+ x2="-18.635777"
+ y2="1034.4441"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient5178-5">
+ <stop
+ style="stop-color:#13daba;stop-opacity:1"
+ offset="0"
+ id="stop5180-8" />
+ <stop
+ style="stop-color:#03251f;stop-opacity:1"
+ offset="1"
+ id="stop5182-3" />
+ </linearGradient>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4462-8">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.68"
+ id="feGaussianBlur4464-6" />
+ </filter>
+ <linearGradient
+ gradientTransform="translate(0.03199987,-988.33014)"
+ y2="1024.6941"
+ x2="38.114223"
+ y1="1007.6377"
+ x1="10.429825"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient3913"
+ xlink:href="#linearGradient5178-5"
+ inkscape:collect="always" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4001"
+ id="linearGradient3999"
+ x1="38.625706"
+ y1="34.124741"
+ x2="31.201086"
+ y2="28.82144"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4053"
+ id="linearGradient4023"
+ gradientUnits="userSpaceOnUse"
+ spreadMethod="pad"
+ x1="42.125"
+ y1="10.375"
+ x2="24.75"
+ y2="56.125"
+ gradientTransform="translate(0.375,988.42468)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4001"
+ id="linearGradient4081"
+ gradientUnits="userSpaceOnUse"
+ x1="38.183765"
+ y1="33.064079"
+ x2="31.201086"
+ y2="28.82144" />
+ <filter
+ inkscape:collect="always"
+ id="filter4101"
+ x="-0.2597809"
+ width="1.5195618"
+ y="-0.34475598"
+ height="1.689512">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.3585616"
+ id="feGaussianBlur4103" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4153"
+ x="-0.10485404"
+ width="1.2097081"
+ y="-0.054822425"
+ height="1.1096448">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.0071355"
+ id="feGaussianBlur4155" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="20"
+ inkscape:cx="33.36206"
+ inkscape:cy="39.612969"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ spacingx="1px"
+ spacingy="1px" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="11.25,96.5"
+ id="guide4252" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-988.36218)">
+ <g
+ id="g4430">
+ <path
+ style="color:#000000;fill:url(#linearGradient5184);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter4462);enable-background:accumulate;opacity:1"
+ d="m 28,992.36214 -4,4.00004 0,4.00002 -3,-3.00002 -6,0 -6,6.00002 0,6 3,3 -4,0 -4,4 0,8 4,4 4,0 -3,3 0,6 6,6 6,0 c 1.147043,0.435 1.437261,0.5242 1.5,3.3125 2.236628,0.5754 7.083754,1.6875 9.5,1.6875 15.46397,0 28,-12.536 28,-28 0,-11.5626 -6.99928,-21.50902 -17,-25.78131 -0.666614,1.59048 -2.504535,2.24015 -3,1.78129 l -4,-4.00004 z"
+ id="rect4162"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccccccccsscccc" />
+ <path
+ id="rect3071"
+ d="m 28,8 0,4.40625 c -0.222736,0.04523 -0.43627,0.103686 -0.65625,0.15625 -0.410499,0.09787 -0.818477,0.190041 -1.21875,0.3125 -0.02176,0.0067 -0.04077,0.02449 -0.0625,0.03125 -0.591863,0.183324 -1.183088,0.389835 -1.75,0.625 -0.500562,0.208804 -0.989768,0.440151 -1.46875,0.6875 -0.08271,0.04255 -0.167971,0.08132 -0.25,0.125 -0.556479,0.297288 -1.101494,0.621939 -1.625,0.96875 l -3.125,-3.125 -5.65625,5.65625 3.125,3.125 c -0.341672,0.515748 -0.675122,1.045976 -0.96875,1.59375 -0.04801,0.08987 -0.07834,0.190559 -0.125,0.28125 -0.232389,0.450012 -0.457832,0.905888 -0.65625,1.375 -0.01273,0.03027 -0.01866,0.0634 -0.03125,0.09375 -0.237526,0.569414 -0.440047,1.155357 -0.625,1.75 -0.130645,0.421788 -0.240463,0.848047 -0.34375,1.28125 C 12.509936,27.56373 12.451477,27.777264 12.40625,28 L 8,28 l 0,8 4.40625,0 c 0.04523,0.222736 0.103686,0.43627 0.15625,0.65625 0.09787,0.410499 0.190041,0.818477 0.3125,1.21875 0.0067,0.02176 0.02449,0.04077 0.03125,0.0625 0.183324,0.591863 0.389835,1.183088 0.625,1.75 0.208804,0.500562 0.440151,0.989768 0.6875,1.46875 0.04666,0.09069 0.07699,0.191375 0.125,0.28125 0.01751,0.03266 0.04481,0.06119 0.0625,0.09375 0.278784,0.514793 0.584173,1.01383 0.90625,1.5 l -3.125,3.125 5.65625,5.65625 3.125,-3.125 c 0.523506,0.346811 1.068521,0.671462 1.625,0.96875 0.08203,0.04368 0.167292,0.08245 0.25,0.125 0.450012,0.232389 0.905888,0.457832 1.375,0.65625 0.03027,0.01273 0.0634,0.01866 0.09375,0.03125 0.569414,0.237526 1.155357,0.440047 1.75,0.625 0.421788,0.130645 0.848047,0.240463 1.28125,0.34375 0.21027,0.05024 0.412209,0.112707 0.625,0.15625 0.01001,0.002 0.02124,-0.002 0.03125,0 l 8,0 c 0.22283,-0.04525 0.436163,-0.103776 0.65625,-0.15625 0.923563,-0.220682 1.81865,-0.499897 2.6875,-0.84375 0.618794,-0.243545 1.225752,-0.510633 1.8125,-0.8125 0.08255,-0.04263 0.168126,-0.08126 0.25,-0.125 0.558072,-0.297141 1.100165,-0.621059 1.625,-0.96875 l 5.65625,-5.65625 c 0.342758,-0.517388 0.6751,-1.044039 0.96875,-1.59375 0.04827,-0.09005 0.07808,-0.190386 0.125,-0.28125 0.301867,-0.586748 0.568955,-1.193706 0.8125,-1.8125 0.343853,-0.86885 0.623068,-1.763937 0.84375,-2.6875 0.05247,-0.220087 0.111003,-0.43342 0.15625,-0.65625 l 0,-8 c -0.002,-0.0098 0.002,-0.02142 0,-0.03125 -0.04353,-0.2127 -0.106134,-0.414805 -0.15625,-0.625 -0.220682,-0.923563 -0.499897,-1.81865 -0.84375,-2.6875 -0.243545,-0.618794 -0.510633,-1.225752 -0.8125,-1.8125 -0.04692,-0.09086 -0.07673,-0.191205 -0.125,-0.28125 C 49.3626,22.012789 49.030258,21.486138 48.6875,20.96875 L 43.03125,15.3125 c -0.484994,-0.321298 -0.986607,-0.627358 -1.5,-0.90625 -0.04094,-0.02217 -0.08389,-0.04061 -0.125,-0.0625 -0.08187,-0.04374 -0.167451,-0.08237 -0.25,-0.125 -0.586748,-0.301867 -1.193706,-0.568955 -1.8125,-0.8125 -0.86885,-0.343853 -1.763937,-0.623068 -2.6875,-0.84375 -0.210195,-0.05012 -0.4123,-0.112724 -0.625,-0.15625 -0.0098,-0.002 -0.02142,0.002 -0.03125,0 L 36,8 z m 4,20 c 2.209139,0 4,1.790861 4,4 0,2.209139 -1.790861,4 -4,4 -0.276142,0 -0.55211,-0.04047 -0.8125,-0.09375 C 29.364767,35.533265 28,33.932997 28,32 28,31.723858 28.040466,31.44789 28.09375,31.1875 28.466735,29.364767 30.067003,28 32,28 z"
+ style="color:#000000;fill:url(#linearGradient5119);fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccssscsss"
+ transform="translate(0,988.36218)" />
+ <path
+ id="path3874"
+ transform="translate(0,988.36218)"
+ d="m 32,16 c -8.836556,0 -16,7.163444 -16,16 0,8.836556 7.163444,16 16,16 8.836556,0 16,-7.163444 16,-16 0,-8.836556 -7.163444,-16 -16,-16 z m 0,12 c 2.209139,0 4,1.790861 4,4 0,2.209139 -1.790861,4 -4,4 -2.209139,0 -4,-1.790861 -4,-4 0,-2.209139 1.790861,-4 4,-4 z"
+ style="fill:url(#linearGradient4440);fill-opacity:1;stroke:none"
+ inkscape:connector-curvature="0" />
+ <path
+ id="path3876"
+ transform="translate(0,988.36218)"
+ d="m 32,24 c -4.418278,0 -8,3.581722 -8,8 0,4.418278 3.581722,8 8,8 4.418278,0 8,-3.581722 8,-8 0,-4.418278 -3.581722,-8 -8,-8 z m 0,4 c 2.209139,0 4,1.790861 4,4 0,2.209139 -1.790861,4 -4,4 -2.209139,0 -4,-1.790861 -4,-4 0,-2.209139 1.790861,-4 4,-4 z"
+ style="fill:url(#linearGradient4442);fill-opacity:1;stroke:none"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3927"
+ d="m 32,1004.3622 c -8.836556,0 -16,7.1634 -16,16 0,4.8907 2.198478,9.2526 5.65625,12.1875 C 19.373756,1029.7839 18,1026.2282 18,1022.3622 c 0,-8.8366 7.163444,-16 16,-16 3.86599,0 7.42176,1.3737 10.1875,3.6562 -2.93487,-3.4577 -7.29683,-5.6562 -12.1875,-5.6562 z"
+ style="fill:#000000;fill-opacity:0.39130435;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3970"
+ d="m 39.5625,1017.7997 c 0.272555,0.8054 0.4375,1.665 0.4375,2.5625 0,4.4183 -3.581722,8 -8,8 -2.720651,0 -5.117207,-1.3623 -6.5625,-3.4375 1.069255,3.1597 4.041685,5.4375 7.5625,5.4375 4.418278,0 8,-3.5817 8,-8 0,-1.6976 -0.535669,-3.2676 -1.4375,-4.5625 z"
+ style="fill:#000000;fill-opacity:0.39130435;stroke:none" />
+ </g>
+ <g
+ id="g5168">
+ <path
+ style="fill:url(#linearGradient4081);fill-opacity:1;stroke:none"
+ d="M 42.3125,10.34375 C 42.036647,17.156583 44.403627,25.298186 33,31 18.729736,38.135058 22.534188,45.86375 26.144531,55.257813 c 0.677562,0.217822 -0.12077,-0.0069 0.580514,0.151895 C 28.421984,55.79405 30.185284,56 32,56 45.254834,56 56,45.254843 56,32 56,22.438466 50.403908,14.202051 42.3125,10.34375 z"
+ id="path3997"
+ transform="translate(0,988.36218)"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cscsssc" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter4101);enable-background:accumulate"
+ id="path4083"
+ sodipodi:cx="13.611806"
+ sodipodi:cy="37.881245"
+ sodipodi:rx="6.2755728"
+ sodipodi:ry="4.7287765"
+ d="m 19.887379,37.881245 c 0,2.611631 -2.80967,4.728776 -6.275573,4.728776 -3.465903,0 -6.2755729,-2.117145 -6.2755729,-4.728776 0,-2.611631 2.8096699,-4.728777 6.2755729,-4.728777 3.465903,0 6.275573,2.117146 6.275573,4.728777 z"
+ transform="matrix(1.338311,0,0,1.4370006,15.945267,978.21621)" />
+ <path
+ id="path3014"
+ d="m 42.3125,10.34375 c 1.626929,3.407127 2.260167,8.676072 -0.03685,13.414814 -1.556024,3.210082 -3.304624,6.220765 -7.658776,8.522607 -4.247767,2.245602 -8.002291,2.8045 -9.732287,4.512181 -4.014531,3.96275 -2.278905,11.00222 1.274261,18.448146 C 28.475952,55.986401 29.435328,56 32,56 45.254834,56 56,45.254843 56,32 56,22.438466 50.403908,14.202051 42.3125,10.34375 z"
+ style="opacity:0.87029275;fill:url(#radialGradient4453);fill-opacity:1;stroke:none"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cssscssc"
+ transform="translate(0,988.36218)" />
+ <path
+ sodipodi:nodetypes="ssccs"
+ inkscape:connector-curvature="0"
+ id="path3902"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 3.96875,-8.5937 5.96875,-6.5937 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ style="fill:#00ffcc;fill-opacity:1;stroke:none;filter:url(#filter3916-4);opacity:1"
+ transform="matrix(-1.8719467,0,0,-1.871948,82.871731,2921.8992)" />
+ <path
+ transform="matrix(-1.2147547,0,0,-1.2147555,70.284181,2259.2303)"
+ style="fill:#d7f4d7;fill-opacity:1;stroke:none;filter:url(#filter3916-4);opacity:1"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 3.96875,-8.5937 5.96875,-6.5937 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ id="path3920"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccs" />
+ <path
+ transform="translate(0,988.36218)"
+ id="path4036"
+ d="M 44.78125 21.6875 L 41.28125 22.8125 C 41.121938 23.226574 40.928384 23.623793 40.71875 24.03125 L 44.78125 21.6875 z M 36.90625 28.53125 C 35.824388 29.387261 34.550485 30.224758 33 31 C 31.216966 31.891508 29.738803 32.739507 28.5 33.5625 C 29.458327 33.796351 30.4375 34.03125 30.4375 34.03125 L 30.4375 37.09375 C 30.4375 38.113344 28.40625 40.167906 28.40625 41.1875 L 28.40625 44.25 C 28.40625 44.25 29.900129 47.688 30.03125 48.625 C 30.45923 51.683387 29.509385 42.154431 29.71875 41.78125 C 30.864146 39.739651 32.416773 37.15625 33.03125 37.15625 C 33.264553 37.15625 34.697228 36.035418 35.9375 37.4375 C 37.107979 38.760684 38.10456 42.614278 38.4375 42.28125 C 39.293559 41.424965 36.5625 34.03125 36.5625 34.03125 L 42.78125 33.34375 L 47.1875 28.875 L 42.03125 32 L 38.59375 32 L 36.5625 29.96875 L 36.90625 28.53125 z "
+ style="opacity:0.86178864;fill:#000000;fill-opacity:0.86666667;stroke:none;filter:url(#filter4082-1)" />
+ <path
+ style="opacity:0.9497908;fill:url(#linearGradient3888);fill-opacity:1;stroke:none"
+ d="m 42.327724,10.343784 c -0.548738,-0.247754 -2.642269,-0.03831 -5.109835,2.376588 l 2.056042,0.657478 c 1.127203,-0.71102 1.815028,-0.50787 1.967315,-0.459692 1.494171,0.472705 0.645754,4.388833 0.1693,6.47919 -1.552718,8.361211 -9.002523,11.324205 -15.159208,14.152866 -3.345171,1.559801 -4.737296,4.36486 -5.23858,8.881728 -0.501284,4.516868 2.508904,8.819746 5.132693,12.829082 0.669966,0.210667 -0.142632,-0.006 0.54783,0.143209 0,0 -4.830399,-7.064617 -3.406092,-13.736155 1.213309,-5.683211 5.53111,-6.436498 9.626097,-8.496005 7.176567,-3.609337 10.985289,-9.424678 11.4425,-16.679509 0.19205,-3.226247 -1.020059,-5.693669 -2.028062,-6.14878 z"
+ id="path3112"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sccscczccsscs"
+ transform="translate(0,988.36218)" />
+ <path
+ sodipodi:nodetypes="ccczccsscc"
+ inkscape:connector-curvature="0"
+ id="path4021"
+ d="m 42.985537,999.28252 c 0.436543,5.62048 0.126695,2.81738 -1.199991,8.53948 -1.552718,8.3612 -6.315023,11.7617 -12.471708,14.5904 -3.345171,1.5598 -6.976573,2.6274 -7.477857,7.1442 -0.501284,4.5169 1.758904,9.6635 4.382693,13.6728 0.669966,0.2107 -0.142632,-0.01 0.54783,0.1432 0,0 -4.450497,-6.5927 -3.02619,-13.2642 1.213309,-5.6833 5.46861,-6.3427 9.563597,-8.4022 7.176567,-3.6094 11.032164,-9.3934 11.489375,-16.6483 0.19205,-3.2262 -0.760684,-4.5078 -1.807749,-5.77538 z"
+ style="opacity:1;fill:url(#linearGradient4023);fill-opacity:1;stroke:none;filter:url(#filter4153)" />
+ </g>
+ <g
+ transform="translate(72.5,-1.500004)"
+ id="g4268">
+ <path
+ transform="matrix(1.1666667,0,0,1.1666667,-5.3333344,983.02885)"
+ d="M 56,32 C 56,45.254834 45.254834,56 32,56 18.745166,56 8,45.254834 8,32 8,18.745166 18.745166,8 32,8 45.254834,8 56,18.745166 56,32 z"
+ sodipodi:ry="24"
+ sodipodi:rx="24"
+ sodipodi:cy="32"
+ sodipodi:cx="32"
+ id="path3005"
+ style="fill:#032620;fill-opacity:1;stroke:none;filter:url(#filter3992)"
+ sodipodi:type="arc" />
+ <g
+ transform="matrix(1.0195929,0,0,1.0195936,-0.15674338,-19.992546)"
+ id="g4116">
+ <path
+ sodipodi:type="arc"
+ style="fill:url(#radialGradient4024-7);fill-opacity:1;stroke:none"
+ id="path3014-0"
+ sodipodi:cx="32"
+ sodipodi:cy="32"
+ sodipodi:rx="20"
+ sodipodi:ry="20"
+ d="M 52,32 C 52,43.045695 43.045695,52 32,52 20.954305,52 12,43.045695 12,32 12,20.954305 20.954305,12 32,12 c 11.045695,0 20,8.954305 20,20 z"
+ transform="matrix(1.1769403,0,0,1.1769403,-6.1232836,982.70009)" />
+ <path
+ style="fill:#00ffcc;fill-opacity:1;stroke:none;filter:url(#filter3884)"
+ d="m 28,16 c -6.627417,0 -12,5.372583 -12,12 0,0.20181 0.0214,0.394383 0.03125,0.59375 C 20,20 22,22 28.65625,16.03125 28.436252,16.019242 28.222996,16 28,16 z"
+ transform="matrix(1.1769403,0,0,1.1769403,-5.6620909,982.68843)"
+ id="path3866"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccs" />
+ <path
+ sodipodi:nodetypes="ssccs"
+ inkscape:connector-curvature="0"
+ id="path3888"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 3.96875,-8.5937 5.96875,-6.5937 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ style="fill:#00ffcc;fill-opacity:1;stroke:none;filter:url(#filter3898)"
+ transform="matrix(1.487885,0,0,1.487885,-10.597031,-493.22036)" />
+ <path
+ transform="matrix(-1.8359746,0,0,-1.8359746,81.432966,2885.3572)"
+ style="fill:#00ffcc;fill-opacity:1;stroke:none;filter:url(#filter3916)"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 3.96875,-8.5937 5.96875,-6.5937 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ id="path3902-7"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssccs" />
+ <path
+ sodipodi:nodetypes="ssccs"
+ inkscape:connector-curvature="0"
+ id="path3920-6"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 3.96875,-8.5937 5.96875,-6.5937 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ style="fill:#d7f4d7;fill-opacity:1;stroke:none;filter:url(#filter3916)"
+ transform="matrix(-1.1914115,0,0,-1.1914115,69.087304,2235.4229)" />
+ <path
+ transform="matrix(0.58516297,0,0,0.58516297,6.7391969,416.10681)"
+ style="fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter4110)"
+ d="m 28,1004.3622 c -6.627417,0 -12,5.3726 -12,12 0,0.2018 0.0214,0.3944 0.03125,0.5937 5.570868,-4.4816 8.905966,-4.4575 12.625,-12.5625 -0.219998,-0.012 -0.433254,-0.031 -0.65625,-0.031 z"
+ id="path3964"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="csccsc" />
+ <path
+ style="opacity:0.86178864;fill:#000000;fill-opacity:0.86666667;stroke:none;filter:url(#filter4082)"
+ d="M 30,29 30.450431,19.407328 32.447263,28.5316 35,27 l 1,-6 -1,-4 1,-2 0,3 1.125,3.375 -0.07418,2.744617 L 44.0828,21.873859 37,26 l -1,4 2,2 3.375,0 5.0625,-3.0625 -4.3125,4.375 L 36,34 c 0,0 2.686048,7.256632 1.846439,8.096462 -0.326542,0.326628 -1.289256,-3.475594 -2.437243,-4.77335 C 34.192758,35.947974 32.79132,37.0625 32.5625,37.0625 c -0.602669,0 -2.136064,2.519769 -3.25945,4.522134 -0.205342,0.36601 0.728015,9.714733 0.308259,6.715119 C 29.482708,47.380759 28,44 28,44 c 0,0 0,-2 0,-3 0,-1 2,-3 2,-4 0,-1 0,-3 0,-3 0,0 -4.25,-1.125 -5.25,-1.125 -0.47674,0 -1.893857,0.89205 -3.377184,1.040709 C 19.744745,34.078874 18.0625,33.5 18.0625,33.5 L 16.774038,34.754808 17,39 l -2,-4 2,-3 3.8125,-0.0625 1.170423,-6.693425 3.092068,-2.055016 -2.122126,3.038537 -0.913529,5.350612 L 28,31 z"
+ id="path4036-3"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccccccccssssscsscsscccccccccccc"
+ transform="translate(0,988.36218)" />
+ </g>
+ </g>
+ <path
+ style="color:#000000;fill:url(#linearGradient5184-3);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter4462-8);enable-background:accumulate"
+ d="m -44.736544,992.32667 -4,4 0,4.00003 -3,-3.00003 -6,0 -6,6.00003 0,6 3,3 -4,0 -4,4 0,8 4,4 4,0 -3,3 0,6 6,6 6,0 3,-3 0,4 4,4 8,0 4,-4 0,-4 3,3 6,0 6,-6 0,-6 -3,-3 4,0 4,-4 0,-8 -4,-4 -4,0 3,-3 0,-6 -6,-6.00003 -6,0 -3,3.00003 0,-4.00003 -4,-4 z"
+ id="rect4162-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ style="color:#000000;fill:url(#linearGradient3925-4-8);fill-opacity:1;fill-rule:nonzero;stroke:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m -44.736544,996.32667 0,4.40623 c -0.222736,0.045 -0.43627,0.1037 -0.65625,0.1563 -0.410499,0.098 -0.818477,0.19 -1.21875,0.3125 -0.0218,0.01 -0.0408,0.024 -0.0625,0.031 -0.59186,0.1833 -1.18309,0.3898 -1.75,0.625 -0.50056,0.2088 -0.98977,0.4401 -1.46875,0.6875 -0.0827,0.043 -0.16797,0.081 -0.25,0.125 -0.55648,0.2973 -1.10149,0.6219 -1.625,0.9687 l -3.125,-3.125 -5.65625,5.6562 3.125,3.125 c -0.34167,0.5158 -0.67512,1.046 -0.96875,1.5938 -0.048,0.09 -0.0783,0.1905 -0.125,0.2812 -0.23239,0.45 -0.45783,0.9059 -0.65625,1.375 -0.0127,0.03 -0.0187,0.063 -0.0312,0.094 -0.23753,0.5694 -0.44005,1.1553 -0.625,1.75 -0.13065,0.4218 -0.24046,0.848 -0.34375,1.2812 -0.0526,0.22 -0.11102,0.4335 -0.15625,0.6563 l -4.40625,0 0,8 4.40625,0 c 0.0452,0.2227 0.10369,0.4363 0.15625,0.6562 0.0979,0.4105 0.19004,0.8185 0.3125,1.2188 0.007,0.022 0.0245,0.041 0.0312,0.062 0.18332,0.5918 0.38983,1.1831 0.625,1.75 0.2088,0.5005 0.44015,0.9897 0.6875,1.4687 0.0467,0.091 0.077,0.1914 0.125,0.2813 0.0175,0.033 0.0448,0.061 0.0625,0.094 0.27878,0.5148 0.58417,1.0139 0.90625,1.5 l -3.125,3.125 5.65625,5.6563 3.125,-3.125 c 0.52351,0.3468 1.06852,0.6714 1.625,0.9687 0.082,0.044 0.16729,0.082 0.25,0.125 0.45001,0.2324 0.90589,0.4579 1.375,0.6563 0.0303,0.013 0.0634,0.019 0.0937,0.031 0.56941,0.2376 1.15536,0.4401 1.75,0.625 0.42179,0.1307 0.848047,0.2405 1.28125,0.3438 0.21027,0.05 0.412209,0.1127 0.625,0.1562 0.01001,0 0.02124,0 0.03125,0 l 0,4.4063 8,0 0,-4.4063 c 0.22283,-0.045 0.436163,-0.1037 0.65625,-0.1562 0.923563,-0.2207 1.81865,-0.4999 2.6875,-0.8438 0.618794,-0.2435 1.225752,-0.5106 1.8125,-0.8125 0.08255,-0.043 0.168126,-0.081 0.25,-0.125 0.558072,-0.2971 1.100165,-0.621 1.625,-0.9687 l 3.125,3.125 5.65625,-5.6563 -3.125,-3.125 c 0.342758,-0.5174 0.6751,-1.044 0.96875,-1.5937 0.04827,-0.09 0.07808,-0.1904 0.125,-0.2813 0.301867,-0.5867 0.568955,-1.1937 0.8125,-1.8125 0.343853,-0.8688 0.623068,-1.7639 0.84375,-2.6875 0.05247,-0.2201 0.111003,-0.4334 0.15625,-0.6562 l 4.40625,0 0,-8 -4.40625,0 c -0.002,-0.01 0.002,-0.021 0,-0.031 -0.04353,-0.2127 -0.106134,-0.4148 -0.15625,-0.625 -0.220682,-0.9235 -0.499897,-1.8186 -0.84375,-2.6875 -0.243545,-0.6188 -0.510633,-1.2257 -0.8125,-1.8125 -0.04692,-0.091 -0.07673,-0.1912 -0.125,-0.2812 -0.29365,-0.5497 -0.625992,-1.0764 -0.96875,-1.5938 l 3.125,-3.125 -5.65625,-5.6562 -3.125,3.125 c -0.484994,-0.3213 -0.986607,-0.6273 -1.5,-0.9062 -0.04094,-0.022 -0.08389,-0.041 -0.125,-0.062 -0.08187,-0.044 -0.167451,-0.082 -0.25,-0.125 -0.586748,-0.3019 -1.193706,-0.569 -1.8125,-0.8125 -0.86885,-0.3439 -1.763937,-0.6231 -2.6875,-0.8438 -0.210195,-0.05 -0.4123,-0.1127 -0.625,-0.1562 -0.0098,0 -0.02142,0 -0.03125,0 l 0,-4.40633 -8,0 z m 4,20.00003 c 2.209139,0 4,1.7908 4,4 0,2.2091 -1.790861,4 -4,4 -0.276142,0 -0.55211,-0.041 -0.8125,-0.094 -1.822733,-0.373 -3.1875,-1.9732 -3.1875,-3.9062 0,-0.2762 0.04047,-0.5521 0.09375,-0.8125 0.372985,-1.8228 1.973253,-3.1875 3.90625,-3.1875 z"
+ id="rect3071-3" />
+ <path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient3917-1);fill-opacity:1;stroke:none"
+ d="m -40.736544,1004.3267 c -8.83656,0 -16,7.1634 -16,16 0,8.8365 7.16344,16 16,16 8.836556,0 16,-7.1635 16,-16 0,-8.8366 -7.163444,-16 -16,-16 z m 0,12 c 2.209139,0 4,1.7908 4,4 0,2.2091 -1.790861,4 -4,4 -2.209139,0 -4,-1.7909 -4,-4 0,-2.2092 1.790861,-4 4,-4 z"
+ id="path3874-0" />
+ <path
+ inkscape:connector-curvature="0"
+ style="fill:url(#linearGradient3986-9);fill-opacity:1;stroke:none"
+ d="m -40.736544,1012.3267 c -4.418278,0 -8,3.5817 -8,8 0,4.4183 3.581722,8 8,8 4.418278,0 8,-3.5817 8,-8 0,-4.4183 -3.581722,-8 -8,-8 z m 0,4 c 2.209139,0 4,1.7908 4,4 0,2.2091 -1.790861,4 -4,4 -2.209139,0 -4,-1.7909 -4,-4 0,-2.2092 1.790861,-4 4,-4 z"
+ id="path3876-7" />
+ <path
+ style="fill:#000000;fill-opacity:0.39130435;stroke:none"
+ d="m -40.736544,1004.3267 c -8.83656,0 -16,7.1634 -16,16 0,4.8907 2.19848,9.2526 5.65625,12.1875 -2.28249,-2.7658 -3.65625,-6.3215 -3.65625,-10.1875 0,-8.8366 7.16344,-16 16,-16 3.86599,0 7.42176,1.3737 10.1875,3.6562 -2.93487,-3.4577 -7.29683,-5.6562 -12.1875,-5.6562 z"
+ id="path3927-9"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#000000;fill-opacity:0.39130435;stroke:none"
+ d="m -33.174044,1017.7642 c 0.272555,0.8054 0.4375,1.665 0.4375,2.5625 0,4.4183 -3.581722,8 -8,8 -2.720651,0 -5.117207,-1.3623 -6.5625,-3.4375 1.069255,3.1597 4.041685,5.4375 7.5625,5.4375 4.418278,0 8,-3.5817 8,-8 0,-1.6976 -0.535669,-3.2676 -1.4375,-4.5625 z"
+ id="path3970-3"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/launcher/resources/sources/meat.svg b/launcher/resources/sources/meat.svg
new file mode 100644
index 00000000..69a20073
--- /dev/null
+++ b/launcher/resources/sources/meat.svg
@@ -0,0 +1,371 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32px"
+ height="32px"
+ id="svg2985"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="meat.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/meat128.png"
+ inkscape:export-xdpi="360"
+ inkscape:export-ydpi="360">
+ <defs
+ id="defs2987">
+ <filter
+ inkscape:collect="always"
+ id="filter4454"
+ x="-0.16296296"
+ width="1.3259259"
+ y="-0.16296296"
+ height="1.3259259">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.2222222"
+ id="feGaussianBlur4456" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4863">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.53333333"
+ id="feGaussianBlur4865" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4974"
+ x="-0.18666667"
+ width="1.3733333"
+ y="-0.18666667"
+ height="1.3733333">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.8666667"
+ id="feGaussianBlur4976" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="16"
+ inkscape:cx="9.6045985"
+ inkscape:cy="9.7795413"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid3004"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata2990">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <path
+ style="fill:#ffcc00;stroke:none;filter:url(#filter4974)"
+ d="m 4,10 6,-6 4,0 10,10 0,4 2,0 2,2 0,4 -4,4 -6,0 0,-4 -4,0 -2,-2 -2,0 -6,-6 z"
+ id="path4952"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccccccccc" />
+ <g
+ transform="translate(-68,24)"
+ id="g4869" />
+ <g
+ id="g3473"
+ transform="translate(4,4)">
+ <rect
+ y="2"
+ x="6"
+ height="2"
+ width="4"
+ id="rect3006"
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none"
+ id="rect3014"
+ width="2"
+ height="2"
+ x="4"
+ y="4" />
+ <rect
+ y="6"
+ x="2"
+ height="6"
+ width="2"
+ id="rect3016"
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none" />
+ <rect
+ y="4"
+ x="10"
+ height="2"
+ width="2"
+ id="rect3018"
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none"
+ id="rect3020"
+ width="2"
+ height="2"
+ x="4"
+ y="12" />
+ <rect
+ y="14"
+ x="6"
+ height="2"
+ width="2"
+ id="rect3022"
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none"
+ id="rect3024"
+ width="2"
+ height="2"
+ x="12"
+ y="6" />
+ <rect
+ y="8"
+ x="14"
+ height="2"
+ width="2"
+ id="rect3026"
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none"
+ id="rect3028"
+ width="2"
+ height="2"
+ x="8"
+ y="14" />
+ <rect
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none"
+ id="rect3030"
+ width="4"
+ height="2"
+ x="18"
+ y="16" />
+ <rect
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none"
+ id="rect3034"
+ width="2"
+ height="4"
+ x="16"
+ y="18" />
+ <rect
+ y="18"
+ x="20"
+ height="2"
+ width="2"
+ id="rect3036"
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none"
+ id="rect3038"
+ width="2"
+ height="2"
+ x="18"
+ y="20" />
+ <rect
+ y="18"
+ x="18"
+ height="2"
+ width="2"
+ id="rect3040"
+ style="fill:#fff7dc;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#e2d5aa;fill-opacity:1;stroke:none"
+ id="rect3042"
+ width="2"
+ height="2"
+ x="16"
+ y="16" />
+ <rect
+ y="12"
+ x="10"
+ height="2"
+ width="4"
+ id="rect3044"
+ style="fill:#7b512d;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#613c1b;fill-opacity:1;stroke:none"
+ id="rect3046"
+ width="2"
+ height="2"
+ x="8"
+ y="12" />
+ <rect
+ y="10"
+ x="6"
+ height="2"
+ width="4"
+ id="rect3048"
+ style="fill:#7b512d;fill-opacity:1;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="rect3050"
+ d="m 10,8 2,0 0,2 2,0 0,2 -4,0 z"
+ style="fill:#9d6d43;fill-opacity:1;stroke:none" />
+ <rect
+ y="8"
+ x="8"
+ height="2"
+ width="2"
+ id="rect3053"
+ style="fill:#b88458;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#b88458;fill-opacity:1;stroke:none"
+ id="rect3055"
+ width="2"
+ height="2"
+ x="10"
+ y="6" />
+ <rect
+ y="8"
+ x="12"
+ height="2"
+ width="2"
+ id="rect3057"
+ style="fill:#b88458;fill-opacity:1;stroke:none" />
+ <rect
+ y="12"
+ x="6"
+ height="2"
+ width="2"
+ id="rect3059"
+ style="fill:#613c1b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#dfb18f;fill-opacity:1;stroke:none"
+ id="rect3061"
+ width="2"
+ height="2"
+ x="6"
+ y="6" />
+ <rect
+ y="4"
+ x="8"
+ height="2"
+ width="2"
+ id="rect3063"
+ style="fill:#b21818;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#b21818;fill-opacity:1;stroke:none"
+ id="rect3065"
+ width="2"
+ height="2"
+ x="4"
+ y="8" />
+ <rect
+ y="6"
+ x="4"
+ height="2"
+ width="2"
+ id="rect3067"
+ style="fill:#d42a2a;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#d42a2a;fill-opacity:1;stroke:none"
+ id="rect3069"
+ width="2"
+ height="2"
+ x="6"
+ y="4" />
+ <rect
+ y="6"
+ x="8"
+ height="2"
+ width="2"
+ id="rect3071"
+ style="fill:#d42a2a;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#d42a2a;fill-opacity:1;stroke:none"
+ id="rect3073"
+ width="2"
+ height="2"
+ x="6"
+ y="8" />
+ <rect
+ y="16"
+ x="10"
+ height="2"
+ width="6"
+ id="rect4470"
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#0c0d0b;fill-opacity:1;stroke:none"
+ id="rect4472"
+ width="2"
+ height="6"
+ x="16"
+ y="10" />
+ <rect
+ y="14"
+ x="12"
+ height="2"
+ width="4"
+ id="rect4474"
+ style="fill:#613c1b;fill-opacity:1;stroke:none" />
+ <rect
+ y="12"
+ x="14"
+ height="2"
+ width="2"
+ id="rect4476"
+ style="fill:#9d6d43;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#502d16;fill-opacity:1;stroke:none"
+ id="rect4478"
+ width="2"
+ height="2"
+ x="10"
+ y="14" />
+ <rect
+ style="fill:#b88458;fill-opacity:1;stroke:none"
+ id="rect4480"
+ width="2"
+ height="2"
+ x="14"
+ y="10" />
+ <rect
+ style="fill:#613c1b;fill-opacity:1;stroke:none"
+ id="rect4978"
+ width="2"
+ height="2"
+ x="4"
+ y="10" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/sources/multimc-discord.svg b/launcher/resources/sources/multimc-discord.svg
new file mode 100644
index 00000000..c3c73044
--- /dev/null
+++ b/launcher/resources/sources/multimc-discord.svg
@@ -0,0 +1,265 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ version="1.1"
+ id="svg4427"
+ height="68.26667"
+ width="68.26667">
+ <defs
+ id="defs4429">
+ <linearGradient
+ id="linearGradient4809">
+ <stop
+ id="stop4805"
+ offset="0"
+ style="stop-color:#98c867;stop-opacity:1" />
+ <stop
+ id="stop4807"
+ offset="1"
+ style="stop-color:#5c9a33;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5668">
+ <stop
+ style="stop-color:#75b54b;stop-opacity:1;"
+ offset="0"
+ id="stop5670" />
+ <stop
+ style="stop-color:#75b54b;stop-opacity:0.6"
+ offset="1"
+ id="stop5672" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5084">
+ <stop
+ style="stop-color:#000000;stop-opacity:0.8"
+ offset="0"
+ id="stop5086" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0.35"
+ offset="1"
+ id="stop5088" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ gradientUnits="userSpaceOnUse"
+ y2="61.773685"
+ x2="50.506943"
+ y1="28.510933"
+ x1="6.7342591"
+ id="linearGradient5072"
+ xlink:href="#linearGradient5668" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="82.973114"
+ x2="44.097023"
+ y1="9.7948904"
+ x1="14.312115"
+ id="linearGradient5082"
+ xlink:href="#linearGradient5084" />
+ <linearGradient
+ y2="61.773685"
+ x2="50.506943"
+ y1="28.510933"
+ x1="6.7342591"
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient3281"
+ xlink:href="#linearGradient5668" />
+ <linearGradient
+ y2="61.773685"
+ x2="50.506943"
+ y1="28.510933"
+ x1="6.7342591"
+ gradientTransform="translate(-0.01532073,-0.00938002)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient3283"
+ xlink:href="#linearGradient5668" />
+ <linearGradient
+ y2="61.773685"
+ x2="50.506943"
+ y1="28.510933"
+ x1="6.7342591"
+ gradientTransform="matrix(1.2671525,0,0,0.89790119,-0.01941371,-0.00842234)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient3286"
+ xlink:href="#linearGradient5668" />
+ <linearGradient
+ y2="82.973114"
+ x2="44.097023"
+ y1="9.7948904"
+ x1="14.312115"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient3288"
+ xlink:href="#linearGradient5084" />
+ <linearGradient
+ y2="82.973114"
+ x2="44.097023"
+ y1="9.7948904"
+ x1="14.312115"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient3290"
+ xlink:href="#linearGradient5084" />
+ <linearGradient
+ gradientTransform="scale(1.2671525,0.89790119)"
+ y2="82.973114"
+ x2="44.097023"
+ y1="9.7948904"
+ x1="14.312115"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient3293"
+ xlink:href="#linearGradient5084" />
+ <linearGradient
+ id="linearGradient5580">
+ <stop
+ id="stop5576"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:0.0627451" />
+ <stop
+ id="stop5578"
+ offset="1"
+ style="stop-color:#322217;stop-opacity:0.58823532" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3999">
+ <stop
+ style="stop-color:#a3704b;stop-opacity:1"
+ offset="0"
+ id="stop3995" />
+ <stop
+ style="stop-color:#6a4a33;stop-opacity:1"
+ offset="1"
+ id="stop3997" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient2727">
+ <stop
+ style="stop-color:#966c4a;stop-opacity:1"
+ offset="0"
+ id="stop2723" />
+ <stop
+ style="stop-color:#593d29;stop-opacity:1"
+ offset="1"
+ id="stop2725" />
+ </linearGradient>
+ <linearGradient
+ y2="97.065842"
+ x2="86.415741"
+ y1="33.80484"
+ x1="36.546478"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient2050"
+ xlink:href="#linearGradient2727" />
+ <radialGradient
+ r="29.866665"
+ fy="34.133335"
+ fx="34.133331"
+ cy="34.133335"
+ cx="34.133331"
+ gradientTransform="matrix(1.1428572,0,0,1.1428572,-4.8771039,-4.8772393)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2052"
+ xlink:href="#linearGradient3999" />
+ <linearGradient
+ y2="38.400913"
+ x2="38.400005"
+ y1="29.867579"
+ x1="29.866674"
+ gradientTransform="translate(77.635668,-7.276299)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient2140"
+ xlink:href="#linearGradient5580" />
+ <linearGradient
+ y2="82.973114"
+ x2="44.097023"
+ y1="9.7948904"
+ x1="14.312115"
+ gradientTransform="matrix(1.2671525,0,0,0.89790119,-0.82864077,-1.0012743)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient4790"
+ xlink:href="#linearGradient5084" />
+ <radialGradient
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.7500268,0.1250019,-0.01781176,0.24936465,95.393964,18.110151)"
+ r="34.132812"
+ fy="-34.134373"
+ fx="-42.66758"
+ cy="-34.134373"
+ cx="-42.66758"
+ id="radialGradient4803"
+ xlink:href="#linearGradient4809" />
+ </defs>
+ <metadata
+ id="metadata4432">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1">
+ <g
+ transform="translate(9.113e-4,0.00104183)"
+ id="g2048">
+ <rect
+ y="3.0270508e-06"
+ x="-1.3322676e-15"
+ height="68.26667"
+ width="68.26667"
+ id="rect2026"
+ style="fill:url(#linearGradient2050);fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ ry="8.5333338"
+ rx="8.5333338" />
+ <rect
+ ry="0"
+ style="fill:url(#radialGradient2052);fill-opacity:1;stroke:none;stroke-width:17.06666946"
+ id="rect2028"
+ width="68.26667"
+ height="68.26667"
+ x="-0.00091432704"
+ y="-0.0010418301"
+ rx="0" />
+ <path
+ style="opacity:0.6;fill:#593d29;fill-opacity:1;stroke:none;stroke-width:17.06666756"
+ d="m 4.2669272,4.2645856 -9.11e-4,8.5333334 h 4.267577 v 4.267579 h 8.5332038 v 4.265625 h 4.265625 V 8.5322946 H 25.6 v 8.5332034 h 4.265625 v -4.267579 h 4.267578 v 8.533204 h 4.265625 v -4.265625 h 4.267578 v 4.265625 h 4.267579 v -4.265625 h 4.265624 v -4.267579 h 4.267579 v 4.267579 h 8.533203 l -1.3e-4,-12.8009124 z"
+ id="path4811" />
+ <path
+ id="path4794"
+ d="m -9.113e-4,-0.0010388 6.52e-4,8.5324211 v 4.2675787 h 4.265625 V 8.5313823 c 0,-0.521698 0.105433,-1.01339 0.27539,-1.47461 -0.169616,0.460814 -0.27539,0.953462 -0.27539,1.47461 h 4.2675785 v 4.2675787 h 4.2656248 4.267578 v 4.265625 h 4.265625 V 12.798961 8.5313823 4.2657573 h 4.267578 v 4.265625 4.2675787 h 4.265625 V 8.5313823 h 4.267578 v 4.2675787 4.265625 h 4.265625 v -4.265625 h 4.267578 v 4.265625 h 4.267579 v -4.265625 h 4.265624 V 8.5313823 h 4.267579 v 4.2675787 h 4.265625 4.267578 V 8.5313823 h 4.265625 l 3.9e-4,-8.5324211 z m 5.5143245,5.5128891 c -0.318089,0.317888 -0.570428,0.695824 -0.7753915,1.101563 0.2048795,-0.405231 0.4576385,-0.784012 0.7753915,-1.101563 z"
+ style="fill:url(#radialGradient4803);fill-opacity:1;stroke:none;stroke-width:17.06666756" />
+ </g>
+ <g
+ id="g1092">
+ <path
+ d="m 38.886673,44.940882 c -0.974277,-0.801673 -2.231353,-2.137814 -3.771231,-4.008427 -2.105641,2.672298 -4.085536,4.598569 -5.939688,5.778816 -2.325625,1.425227 -5.295467,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124578 -10.5594458,-3.37376 C 6.7526311,43.114834 5.275567,39.986037 5.2755773,36.088937 5.275567,32.347763 6.7526311,29.207831 9.7067742,26.669132 12.346618,24.419991 15.897857,23.295407 20.360501,23.295373 c 2.294138,3.4e-5 4.289747,0.334069 5.986829,1.002107 1.979863,0.73491 3.645488,1.737016 4.996881,3.00632 1.257039,1.135751 2.514115,2.471891 3.771231,4.008428 2.105563,-2.672257 4.085457,-4.598527 5.939689,-5.778816 2.325544,-1.425186 5.295385,-2.137794 8.909533,-2.137828 4.242577,3.4e-5 7.762388,1.124618 10.559447,3.37376 2.954063,2.360546 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477134,6.881147 -4.431197,9.419806 -2.639925,2.24918 -6.191163,3.373767 -10.653727,3.373758 -2.294219,9e-6 -4.289826,-0.334026 -5.98683,-1.002106 -1.697101,-0.601255 -3.362726,-1.603361 -4.996881,-3.006321 M 19.747676,44.473233 c 5.185412,1.1e-5 9.333763,-2.672271 12.445062,-8.016856 -3.991253,-5.834464 -8.139602,-8.751705 -12.445062,-8.751733 -3.142715,2.8e-5 -5.515446,0.801713 -7.118198,2.405057 -1.728498,1.71474 -2.592737,3.707818 -2.592722,5.979236 -1.5e-5,2.494152 0.864224,4.509499 2.592722,6.046046 1.759887,1.558846 4.132618,2.338261 7.118198,2.33825 M 50.483209,27.77145 c -4.682663,2.9e-5 -8.831013,2.672312 -12.445062,8.016856 3.959745,5.834503 8.108095,8.751746 12.445062,8.751733 3.142633,1.3e-5 5.515364,-0.801671 7.118198,-2.405056 1.728416,-1.714701 2.592656,-3.707778 2.592722,-5.979238 -6.6e-5,-2.49411 -0.864306,-4.509456 -2.592722,-6.046044 -1.759968,-1.558805 -4.132699,-2.338222 -7.118198,-2.338251"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient4790);fill-opacity:1;stroke:none;stroke-width:1.06666672;opacity:0.5"
+ id="path4786" />
+ <path
+ id="path3279"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3293);fill-opacity:1;stroke:none;stroke-width:1.06666672;opacity:0.5"
+ d="m 39.715314,45.942156 c -0.974277,-0.801673 -2.231353,-2.137814 -3.771231,-4.008427 -2.105641,2.672298 -4.085536,4.598569 -5.939688,5.778816 -2.325625,1.425227 -5.295467,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124578 -10.559446,-3.37376 -2.9541431,-2.360505 -4.4312072,-5.489302 -4.4311969,-9.386402 -1.03e-5,-3.741174 1.4770538,-6.881106 4.4311969,-9.419805 2.639844,-2.249141 6.191083,-3.373725 10.653727,-3.373759 2.294138,3.4e-5 4.289747,0.334069 5.986829,1.002107 1.979863,0.73491 3.645488,1.737016 4.996881,3.00632 1.257039,1.135751 2.514115,2.471891 3.771231,4.008428 2.105563,-2.672257 4.085457,-4.598527 5.939689,-5.778816 2.325544,-1.425186 5.295385,-2.137794 8.909533,-2.137828 4.242577,3.4e-5 7.762388,1.124618 10.559447,3.37376 2.954063,2.360546 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477134,6.881147 -4.431197,9.419806 -2.639925,2.24918 -6.191163,3.373767 -10.653727,3.373758 -2.294219,9e-6 -4.289826,-0.334026 -5.98683,-1.002106 -1.697101,-0.601255 -3.362726,-1.603361 -4.996881,-3.006321 M 20.576317,45.474507 c 5.185412,1.1e-5 9.333763,-2.672271 12.445062,-8.016856 -3.991253,-5.834464 -8.139602,-8.751705 -12.445062,-8.751733 -3.142715,2.8e-5 -5.515446,0.801713 -7.118198,2.405057 -1.728498,1.71474 -2.592737,3.707818 -2.592722,5.979236 -1.5e-5,2.494152 0.864224,4.509499 2.592722,6.046046 1.759887,1.558846 4.132618,2.338261 7.118198,2.33825 M 51.31185,28.772724 c -4.682663,2.9e-5 -8.831013,2.672312 -12.445062,8.016856 3.959745,5.834503 8.108095,8.751746 12.445062,8.751733 3.142633,1.3e-5 5.515364,-0.801671 7.118198,-2.405056 1.728416,-1.714701 2.592656,-3.707778 2.592722,-5.979238 -6.6e-5,-2.49411 -0.864306,-4.509456 -2.592722,-6.046044 C 56.67008,29.55217 54.297349,28.772753 51.31185,28.772724" />
+ <path
+ id="path3272"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:76.18933868px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;fill:url(#linearGradient3286);fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ d="m 37.904564,42.951873 c -0.974278,-0.801672 -2.231352,-2.137814 -3.771231,-4.008428 -2.105642,2.672298 -4.085537,4.598568 -5.939688,5.778817 -2.325625,1.425227 -5.295466,2.137836 -8.909534,2.137828 -4.242656,8e-6 -7.762467,-1.124577 -10.5594464,-3.37376 -2.9541428,-2.360505 -4.4312068,-5.489302 -4.4311963,-9.386401 -1.05e-5,-3.741175 1.4770535,-6.881107 4.4311963,-9.419805 2.6398444,-2.249142 6.1910824,-3.373727 10.6537284,-3.37376 2.294137,3.3e-5 4.289745,0.334068 5.986829,1.002107 1.979863,0.734909 3.645487,1.737016 4.99688,3.00632 1.257039,1.13575 2.514116,2.471891 3.771231,4.008428 2.105562,-2.672257 4.085456,-4.598528 5.939689,-5.778817 2.325544,-1.425185 5.295387,-2.137795 8.909534,-2.137828 4.242576,3.3e-5 7.762387,1.12462 10.559446,3.373761 2.954062,2.360545 4.431127,5.489343 4.431197,9.386401 -7e-5,3.741216 -1.477135,6.881148 -4.431197,9.419805 -2.639924,2.249182 -6.191164,3.373767 -10.653728,3.37376 -2.294217,7e-6 -4.289826,-0.334028 -5.986828,-1.002107 -1.697101,-0.601254 -3.362727,-1.603361 -4.996882,-3.006321 m -19.138997,-0.46765 c 5.185412,1.3e-5 9.333762,-2.67227 12.445062,-8.016856 -3.991252,-5.834462 -8.139602,-8.751704 -12.445062,-8.751733 -3.142714,2.9e-5 -5.515444,0.801714 -7.118198,2.405056 -1.7284972,1.714743 -2.5927368,3.707819 -2.5927216,5.979239 -1.52e-5,2.49415 0.8642244,4.509496 2.5927216,6.046045 1.759888,1.558845 4.132618,2.338262 7.118198,2.338249 M 49.5011,25.782442 c -4.682663,2.8e-5 -8.831014,2.672311 -12.445063,8.016855 3.959745,5.834504 8.108096,8.751745 12.445063,8.751733 3.142634,1.2e-5 5.515365,-0.801673 7.118198,-2.405056 1.728417,-1.7147 2.592657,-3.707778 2.592721,-5.979238 -6.4e-5,-2.49411 -0.864304,-4.509456 -2.592721,-6.046046 C 54.85933,26.561886 52.486599,25.78247 49.5011,25.782442" />
+ <path
+ style="font-style:normal;font-weight:normal;font-size:76.18933868px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:0.3;fill:#ccff00;fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ d="m 19.4,21.166667 c -4.462644,3.3e-5 -8.026822,1.150858 -10.6666667,3.4 -2.9541428,2.538698 -4.4333436,5.658825 -4.4333333,9.4 -1.03e-5,3.897098 1.4791905,7.039495 4.4333333,9.4 -1.622701,-2.044271 -2.433341,-4.51168 -2.4333333,-7.4 -1.03e-5,-3.741175 1.4791905,-6.861302 4.433333,-9.4 2.639845,-2.249142 6.204023,-3.399967 10.666667,-3.4 2.294138,3.3e-5 4.302916,0.365295 6,1.033333 1.979862,0.73491 3.615274,1.730695 4.966667,3 0.06836,0.06177 0.131637,0.137049 0.2,0.2 -0.731813,-0.797005 -1.468213,-1.538822 -2.2,-2.2 -1.351393,-1.269305 -2.986805,-2.26509 -4.966667,-3 -1.697084,-0.668038 -3.705862,-1.0333 -6,-1.033333 z m 29.6,0.1 c -3.614148,3.3e-5 -6.574457,0.74148 -8.9,2.166666 -1.818222,1.157367 -3.923451,3.291388 -5.983333,5.883334 0.618278,0.658774 1.248369,1.377605 1.866666,2.133333 2.105562,-2.672257 4.262434,-4.836378 6.116667,-6.016667 2.325543,-1.425186 5.285852,-2.166633 8.9,-2.166666 4.242576,3.3e-5 7.769607,1.150858 10.566667,3.4 -0.570388,-0.722129 -1.227721,-1.382884 -2,-2 C 56.769607,22.417525 53.242576,21.2667 49,21.266667 Z m 8.866667,8.1 c 0.9092,1.305235 1.366619,2.857751 1.366666,4.666666 -6.5e-5,2.271461 -0.871584,4.285301 -2.6,6 -1.602834,1.603384 -3.957366,2.400012 -7.1,2.4 -2.653707,8e-6 -5.320858,-1.032242 -7.833333,-3.216666 3.136636,3.509305 6.469807,5.216676 9.833333,5.216666 3.142634,1.2e-5 5.497166,-0.796616 7.1,-2.4 1.728416,-1.714699 2.599935,-3.728539 2.6,-6 -6.5e-5,-2.49411 -0.871584,-4.496744 -2.6,-6.033333 -0.24943,-0.220921 -0.49262,-0.443723 -0.766666,-0.633333 z m -26.633334,4.966666 c -3.1113,5.344585 -7.247921,8.033345 -12.433333,8.033334 -2.58055,1e-5 -4.543473,-0.352086 -6.208333,-1.516667 0.348871,0.50642 0.590094,0.752276 1.075,1.183333 1.759888,1.558846 4.147753,2.333345 7.133333,2.333334 5.185412,1.1e-5 9.322033,-2.688749 12.433333,-8.033334 z m 4.933334,6.5 c -0.04103,0.05207 -0.09239,0.08182 -0.133334,0.133334 0.687326,0.744419 1.306949,1.359747 1.833334,1.8 -0.529404,-0.580895 -1.078447,-1.178283 -1.7,-1.933334 z"
+ id="text5100" />
+ <path
+ style="font-style:normal;font-weight:normal;font-size:76.18933868px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:0.6;fill:#ccff00;fill-opacity:1;stroke:none;stroke-width:1.06666672"
+ d="m 19.730474,21.54714 c -4.462645,3.3e-5 -8.026823,1.150859 -10.6666669,3.4 -2.9541429,2.538699 -4.433344,5.658826 -4.4333333,9.4 -1.07e-5,3.897099 1.4791904,7.039495 4.4333333,9.4 0.042837,0.03444 0.090155,0.06608 0.1333334,0.1 -2.2392086,-2.228193 -3.3666752,-5.040417 -3.3666667,-8.433333 -1.07e-5,-3.741174 1.4791904,-6.861301 4.4333332,-9.4 2.639844,-2.249141 6.204022,-3.399967 10.666667,-3.4 2.294137,3.3e-5 4.302916,0.365295 6,1.033333 1.870874,0.694455 3.42364,1.628367 4.733333,2.8 -0.314265,-0.308986 -0.652406,-0.582729 -0.966667,-0.866666 -1.351393,-1.269305 -2.986804,-2.265091 -4.966666,-3 -1.697084,-0.668039 -3.705863,-1.033301 -6,-1.033334 z m 29.6,0.1 c -3.614149,3.3e-5 -6.574457,0.741481 -8.9,2.166667 -1.813279,1.154221 -3.963039,3.235656 -6.016667,5.816667 0.355649,0.402628 0.711011,0.798625 1.066667,1.233333 2.105561,-2.672257 4.295767,-4.803044 6.15,-5.983333 2.325543,-1.425187 5.285851,-2.166634 8.9,-2.166667 4.22442,3.3e-5 7.742084,1.136734 10.533333,3.366667 -0.36096,-0.367566 -0.745726,-0.696967 -1.166667,-1.033334 -2.797059,-2.249141 -6.32409,-3.399967 -10.566666,-3.4 z m 8.233333,7.333334 c 1.323326,1.449243 1.999942,3.250987 2,5.433333 -6.5e-5,2.27146 -0.871584,4.2853 -2.6,6 -1.602834,1.603383 -3.957366,2.400012 -7.1,2.4 -2.406328,6e-6 -4.776468,-0.90386 -7.066667,-2.7 2.669147,2.483838 5.436929,3.766674 8.266667,3.766667 3.142634,1.1e-5 5.497166,-0.796617 7.1,-2.4 1.728416,-1.7147 2.599935,-3.72854 2.6,-6 -6.5e-5,-2.49411 -0.871584,-4.496745 -2.6,-6.033334 -0.185641,-0.164422 -0.400724,-0.319587 -0.6,-0.466666 z m -26,5.733333 c -3.1113,5.344584 -7.247921,8.033345 -12.433333,8.033333 -2.612382,1.1e-5 -4.759372,-0.60651 -6.433334,-1.8 0.166027,0.176488 0.313947,0.367942 0.5,0.533334 1.759888,1.558845 4.147754,2.333345 7.133334,2.333333 5.185412,1.2e-5 9.322033,-2.688749 12.433333,-8.033333 z m 4.133333,5.566667 c -0.04657,0.05909 -0.08689,0.108298 -0.133333,0.166666 1.038571,1.18897 1.9748,2.169945 2.7,2.766667 0.06249,0.05364 0.137426,0.08086 0.2,0.133333 -0.792178,-0.781249 -1.706288,-1.778539 -2.766667,-3.066666 z"
+ id="text5058-0" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/sources/netherstar.svg b/launcher/resources/sources/netherstar.svg
new file mode 100644
index 00000000..4046e4ec
--- /dev/null
+++ b/launcher/resources/sources/netherstar.svg
@@ -0,0 +1,342 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="netherstar.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/netherstar.png"
+ inkscape:export-xdpi="45"
+ inkscape:export-ydpi="45">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4065">
+ <stop
+ id="stop4067"
+ offset="0"
+ style="stop-color:#556b6b;stop-opacity:1" />
+ <stop
+ style="stop-color:#3b8585;stop-opacity:0.49803922;"
+ offset="0.5"
+ id="stop4069" />
+ <stop
+ id="stop4071"
+ offset="1"
+ style="stop-color:#fafbfb;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient4053">
+ <stop
+ id="stop4055"
+ offset="0"
+ style="stop-color:#556b6b;stop-opacity:1" />
+ <stop
+ style="stop-color:#3b8585;stop-opacity:0.49803922;"
+ offset="0.5"
+ id="stop4057" />
+ <stop
+ id="stop4059"
+ offset="1"
+ style="stop-color:#fafbfb;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3802">
+ <stop
+ style="stop-color:#cbd6d6;stop-opacity:1;"
+ offset="0"
+ id="stop3804" />
+ <stop
+ style="stop-color:#637e7e;stop-opacity:1"
+ offset="1"
+ id="stop3806" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3006">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3008" />
+ <stop
+ id="stop3955"
+ offset="0.14285807"
+ style="stop-color:#fff656;stop-opacity:1;" />
+ <stop
+ id="stop3949"
+ offset="0.28571615"
+ style="stop-color:#fdd300;stop-opacity:1;" />
+ <stop
+ style="stop-color:#f6d01a;stop-opacity:1;"
+ offset="0.50000137"
+ id="stop3951" />
+ <stop
+ style="stop-color:#fafd00;stop-opacity:0;"
+ offset="1"
+ id="stop3010" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3893">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.3"
+ id="feGaussianBlur3895" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3901">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.3"
+ id="feGaussianBlur3903" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3905">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.3000003"
+ id="feGaussianBlur3907" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3913">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.3"
+ id="feGaussianBlur3915" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3945">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.74074074"
+ id="feGaussianBlur3947" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3967">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.4979424"
+ id="feGaussianBlur3969" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4053"
+ id="linearGradient3985"
+ gradientUnits="userSpaceOnUse"
+ x1="51"
+ y1="1014.3622"
+ x2="35"
+ y2="997.36218" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4065"
+ id="linearGradient3987"
+ gradientUnits="userSpaceOnUse"
+ x1="25"
+ y1="1039.3622"
+ x2="9"
+ y2="1023.3622" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3802"
+ id="linearGradient3989"
+ gradientUnits="userSpaceOnUse"
+ x1="28"
+ y1="28"
+ x2="36"
+ y2="36" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3006"
+ id="radialGradient3991"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1034483,-2.1791858e-6,2.1791909e-6,1.103451,-3.1035149,985.25875)"
+ cx="30"
+ cy="30"
+ fx="30"
+ fy="30"
+ r="14.5" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3802"
+ id="linearGradient3993"
+ gradientUnits="userSpaceOnUse"
+ x1="24"
+ y1="48"
+ x2="36"
+ y2="48" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3802"
+ id="linearGradient3995"
+ gradientUnits="userSpaceOnUse"
+ x1="24"
+ y1="1000.3622"
+ x2="36"
+ y2="1000.3622" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3802"
+ id="linearGradient3997"
+ gradientUnits="userSpaceOnUse"
+ x1="44"
+ y1="1018.3622"
+ x2="52"
+ y2="1018.3622" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3802"
+ id="linearGradient3999"
+ gradientUnits="userSpaceOnUse"
+ x1="8"
+ y1="1018.3622"
+ x2="16"
+ y2="1018.3622" />
+ <filter
+ inkscape:collect="always"
+ id="filter4108">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.39"
+ id="feGaussianBlur4110" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="6"
+ inkscape:cx="32.789148"
+ inkscape:cy="31.233555"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ spacingx="1px"
+ spacingy="1px" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-988.36218)">
+ <g
+ id="g3971"
+ transform="translate(2,1.99998)">
+ <path
+ style="fill:#cccccc;fill-opacity:1;stroke:none;filter:url(#filter3967)"
+ d="m 57,27.00002 0,6 -8,8 -8,0 0,8 -8,8 -6,0 -8,-8 0,-8 -8,0 -8,-8 0,-6 8,-8 8,0 0,-7 8,-9 6,0 8,8 0,8 8,0 z"
+ id="path3957"
+ inkscape:connector-curvature="0"
+ transform="translate(0,988.36218)"
+ sodipodi:nodetypes="ccccccccccccccccccccc" />
+ <path
+ transform="translate(0,988.36218)"
+ inkscape:connector-curvature="0"
+ id="path3000"
+ d="m 56,28 0,4 -8,8 -8,0 0,8 -8,8 -4,0 -8,-8 0,-8 -8,0 -8,-8 0,-4 8,-8 8,0 0,-8 8,-8 4,0 4,4 4,4 0,8 8,0 z"
+ style="fill:#eff2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3788"
+ d="m 52,1024.3622 0,-8 -4,-4 -8,0 -4,-4 0,-8 -4,-4.00002 -8,0 4,-4 4,0 8,8.00002 0,8 8,0 8,8 0,4 z"
+ style="fill:url(#linearGradient3985);fill-opacity:1;stroke:none" />
+ <path
+ style="fill:url(#linearGradient3987);fill-opacity:1;stroke:none"
+ d="m 8,1012.3622 0,8 4,4 8,0 4,4 0,8 4,4 8.00001,0 -4,4 -4.00001,0 -8,-8 0,-8 -8,0 -8,-8 0,-4 z"
+ id="path3790"
+ inkscape:connector-curvature="0" />
+ <path
+ transform="translate(0,988.36218)"
+ inkscape:connector-curvature="0"
+ id="path3002"
+ d="m 36,8 -8,0 -4,4 0,8 -4,4 -8,0 -4,4 0,8 -4,-4 0,-4 8,-8 8,0 0,-8 8,-8 4,0 z"
+ style="opacity:0.67364017;fill:#ffffff;stroke:none" />
+ <path
+ transform="translate(0,988.36218)"
+ inkscape:connector-curvature="0"
+ id="path3800"
+ d="m 28,20 -8,8 0,4 8,8 4,0 8,-8 0,-4 -8,-8 z"
+ style="fill:url(#linearGradient3989);fill-opacity:1;stroke:none;filter:url(#filter3945)" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3004"
+ d="m 28,1000.3622 0,16 -16,0 0,4 16,0 0,16 4,0 0,-16 16,0 0,-4 -16,0 0,-16 z"
+ style="fill:url(#radialGradient3991);fill-opacity:1;stroke:none"
+ sodipodi:nodetypes="ccccccccccccc" />
+ <path
+ style="fill:#556b6b;fill-opacity:1;stroke:none"
+ d="m 23.99998,1040.3622 8,0 4,-4 0,-8 4,-4 8,0 4.00002,-4 0,-8 4,4 0,4 -8.00002,8 -8,0 0,8 -8,8 -4,0 z"
+ id="path3786"
+ inkscape:connector-curvature="0" />
+ <path
+ transform="translate(0,988.36218)"
+ inkscape:connector-curvature="0"
+ id="path3792"
+ d="m 32,48 -4,0 -4,-4 0,4 4,4 4,0 4,-4 0,-4 z"
+ style="fill:url(#linearGradient3993);fill-opacity:1;stroke:none;filter:url(#filter3893)" />
+ <path
+ style="fill:url(#linearGradient3995);fill-opacity:1;stroke:none;filter:url(#filter3905)"
+ d="m 32,1000.3622 -4,0 -4,4 0,-4 4,-4.00002 4,0 4,4.00002 0,4 z"
+ id="path3794"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3796"
+ d="m 48,1020.3622 0,-4 -4,-4 4,0 4,4 0,4 -4,4 -4,0 z"
+ style="fill:url(#linearGradient3997);fill-opacity:1;stroke:none;filter:url(#filter3901)" />
+ <path
+ style="fill:url(#linearGradient3999);fill-opacity:1;stroke:none;filter:url(#filter3913)"
+ d="m 12,1016.3622 0,4 4,4 -4,0 -4,-4 0,-4 4,-4 4,0 z"
+ id="path3798"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#eff2f2;fill-opacity:0.10628019000000000;stroke:none;filter:url(#filter4108)"
+ d="m 28,992.3622 -8,8 0,8 -8,0 -8.0000001,8 0,4 8.0000001,8 8,0 0,8 8,8 4,0 8,-8 0,-8 8,0 8,-8 0,-4 -8,-8 -8,0 0,-8 -4,-4 -4,-4 -4,0 z m 2.90625,2.25 c 1.790052,0.15723 3.5491,1.39991 5.03125,4.1875 5.21676,3.1721 -1.835545,14.0536 7.28125,11.3125 4.64748,-0.9978 13.08236,5.9966 9.65625,10.3437 -2.87943,6.7429 -10.8243,4.9933 -14.84375,6.6563 1.50952,6.5659 -1.74938,11.1566 -6.8125,15 -4.92726,-0.671 -10.654448,-6.4059 -9.1875,-12 1.831567,-8.2895 -10.191583,-0.2312 -12.4375001,-7.2813 -7.428566,-3.9347 -0.557092,-9.4767 3.4375001,-12.7187 7.835946,1.7735 10.684647,-1.4404 9,-9.1875 L 22.5,1000.4559 c 2.310711,-3.39825 5.42283,-6.10574 8.40625,-5.8437 z"
+ id="path4073"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+</svg>
diff --git a/launcher/resources/sources/pskeleton.svg b/launcher/resources/sources/pskeleton.svg
new file mode 100644
index 00000000..c2783df8
--- /dev/null
+++ b/launcher/resources/sources/pskeleton.svg
@@ -0,0 +1,581 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="32"
+ height="32"
+ id="svg2">
+ <defs
+ id="defs4">
+ <filter
+ x="-0.18000001"
+ y="-0.36000001"
+ width="1.36"
+ height="1.72"
+ color-interpolation-filters="sRGB"
+ id="filter5719">
+ <feGaussianBlur
+ id="feGaussianBlur5721"
+ stdDeviation="0.15" />
+ </filter>
+ <filter
+ x="-0.20999999"
+ y="-0.28"
+ width="1.42"
+ height="1.5599999"
+ color-interpolation-filters="sRGB"
+ id="filter5723">
+ <feGaussianBlur
+ id="feGaussianBlur5725"
+ stdDeviation="0.35" />
+ </filter>
+ <filter
+ x="-0.1728"
+ y="-0.34560001"
+ width="1.3456"
+ height="1.6912"
+ color-interpolation-filters="sRGB"
+ id="filter5711">
+ <feGaussianBlur
+ id="feGaussianBlur5713"
+ stdDeviation="0.144" />
+ </filter>
+ <filter
+ x="-0.20999999"
+ y="-0.28"
+ width="1.42"
+ height="1.5599999"
+ color-interpolation-filters="sRGB"
+ id="filter5727">
+ <feGaussianBlur
+ id="feGaussianBlur5729"
+ stdDeviation="0.35" />
+ </filter>
+ <filter
+ x="-0.14708571"
+ y="-0.25740001"
+ width="1.2941715"
+ height="1.5148"
+ color-interpolation-filters="sRGB"
+ id="filter4028-6">
+ <feGaussianBlur
+ id="feGaussianBlur4030-9"
+ stdDeviation="0.429" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ id="filter4056-6">
+ <feGaussianBlur
+ id="feGaussianBlur4058-6"
+ stdDeviation="0.676" />
+ </filter>
+ </defs>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(0,-1020.3622)"
+ id="layer1">
+ <rect
+ width="24"
+ height="24"
+ x="3.9999998"
+ y="1024.3622"
+ id="rect4063-2"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <rect
+ width="26"
+ height="26"
+ x="3"
+ y="3"
+ transform="matrix(1.0769231,0,0,1.0769237,-1.2307693,1019.1314)"
+ id="rect4038-2"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none;filter:url(#filter4056-6)" />
+ <rect
+ width="20"
+ height="5.0000172"
+ x="5.9999995"
+ y="1041.3622"
+ id="rect3189-4"
+ style="fill:#494949;fill-opacity:1;stroke:none" />
+ <path
+ d="m 6.0000007,1041.3622 20.0000003,0 0,2 -18.0000003,0 0,3 -2,0 z"
+ id="path4034-6"
+ style="fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter4028-6)" />
+ <rect
+ width="8"
+ height="5.0000172"
+ x="5.9999995"
+ y="1035.3622"
+ id="rect3155-9"
+ style="fill:#494949;fill-opacity:1;stroke:none" />
+ <rect
+ width="8"
+ height="5.0000172"
+ x="18"
+ y="1035.3622"
+ id="rect3163-4"
+ style="fill:#494949;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1024.3622"
+ id="rect3009-1"
+ style="fill:#a1a1a1;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1024.3622"
+ id="rect3011-9"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1024.3622"
+ id="rect3013-0"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="13"
+ y="1024.3622"
+ id="rect3017-9"
+ style="fill:#919191;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="16"
+ y="1024.3622"
+ id="rect3019-6"
+ style="fill:#888888;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="19"
+ y="1024.3622"
+ id="rect3021-8"
+ style="fill:#8f8f8f;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="22"
+ y="1024.3622"
+ id="rect3023-6"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="25"
+ y="1024.3622"
+ id="rect3025-7"
+ style="fill:#989898;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1027.3622"
+ id="rect3027-7"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1027.3622"
+ id="rect3029-6"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1027.3622"
+ id="rect3031-0"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1027.3622"
+ id="rect3033-4"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="13"
+ y="1027.3622"
+ id="rect3035-4"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="16"
+ y="1027.3622"
+ id="rect3037-2"
+ style="fill:#c1c1c1;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="19"
+ y="1027.3622"
+ id="rect3039-4"
+ style="fill:#bebebe;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="22"
+ y="1027.3622"
+ id="rect3041-2"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="25"
+ y="1027.3622"
+ id="rect3043-1"
+ style="fill:#919191;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1030.3622"
+ id="rect3045-3"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1030.3622"
+ id="rect3047-1"
+ style="fill:#c7c7c7;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1030.3622"
+ id="rect3049-5"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1030.3622"
+ id="rect3051-9"
+ style="fill:#cacaca;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="13"
+ y="1030.3622"
+ id="rect3053-2"
+ style="fill:#d8d8d8;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="16"
+ y="1030.3622"
+ id="rect3055-9"
+ style="fill:#cfcfcf;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="19"
+ y="1030.3622"
+ id="rect3057-7"
+ style="fill:#cfcfcf;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="22"
+ y="1030.3622"
+ id="rect3059-2"
+ style="fill:#bababa;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="25"
+ y="1030.3622"
+ id="rect3061-7"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <path
+ d="m 6.0000007,1035.3622 8.0000003,0 0,2 -6.0000003,0 0,3 -2,0 z"
+ id="path4032-3"
+ style="fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter4028-6)" />
+ <rect
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1033.3622"
+ id="rect3063-8"
+ style="fill:#bababa;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1033.3622"
+ id="rect3065-6"
+ style="fill:#c4c4c4;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1033.3622"
+ id="rect3067-1"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1033.3622"
+ id="rect3069-9"
+ style="fill:#bababa;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="13"
+ y="1033.3622"
+ id="rect3071-7"
+ style="fill:#c1c1c1;fill-opacity:1;stroke:none" />
+ <path
+ d="m 18.000001,1035.3622 8,0 0,2 -6,0 0,3 -2,0 z"
+ id="path4036-6"
+ style="fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter4028-6)" />
+ <rect
+ width="3"
+ height="3"
+ x="16"
+ y="1033.3622"
+ id="rect3073-1"
+ style="fill:#afafaf;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="19"
+ y="1033.3622"
+ id="rect3075-8"
+ style="fill:#cacaca;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="22"
+ y="1033.3622"
+ id="rect3077-8"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="25"
+ y="1033.3622"
+ id="rect3079-2"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1036.3622"
+ id="rect3153-6"
+ style="fill:#c4c4c4;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="13"
+ y="1036.3622"
+ id="rect3159-7"
+ style="fill:#c4c4c4;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="16"
+ y="1036.3622"
+ id="rect3161-0"
+ style="fill:#cacaca;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="25"
+ y="1036.3622"
+ id="rect3167-3"
+ style="fill:#bababa;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1039.3622"
+ id="rect3169-4"
+ style="fill:#a7a7a7;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1039.3622"
+ id="rect3171-8"
+ style="fill:#c1c1c1;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1039.3622"
+ id="rect3173-5"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1039.3622"
+ id="rect3175-0"
+ style="fill:#cacaca;fill-opacity:1;stroke:none" />
+ <rect
+ width="6"
+ height="3"
+ x="13"
+ y="1039.3622"
+ id="rect3177-4"
+ style="fill:#828282;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="19"
+ y="1039.3622"
+ id="rect3181-9"
+ style="fill:#cacaca;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="22"
+ y="1039.3622"
+ id="rect3183-7"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="25"
+ y="1039.3622"
+ id="rect3185-6"
+ style="fill:#8f8f8f;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1042.3622"
+ id="rect3187-1"
+ style="fill:#9e9e9e;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="25"
+ y="1042.3622"
+ id="rect3203-8"
+ style="fill:#919191;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1045.3622"
+ id="rect3205-4"
+ style="fill:#828282;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1045.3622"
+ id="rect3207-2"
+ style="fill:#7f7f7f;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1045.3622"
+ id="rect3209-3"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="10"
+ y="1045.3622"
+ id="rect3211-3"
+ style="fill:#858585;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="13"
+ y="1045.3622"
+ id="rect3213-9"
+ style="fill:#858585;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="16"
+ y="1045.3622"
+ id="rect3215-7"
+ style="fill:#919191;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="19"
+ y="1045.3622"
+ id="rect3217-0"
+ style="fill:#858585;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="22"
+ y="1045.3622"
+ id="rect3219-4"
+ style="fill:#949494;fill-opacity:1;stroke:none" />
+ <rect
+ width="3"
+ height="3"
+ x="25"
+ y="1045.3622"
+ id="rect3221-5"
+ style="fill:#888888;fill-opacity:1;stroke:none" />
+ <path
+ d="m 4.0000007,1023.3622 -1,1 0,24 1,1 24.0000003,0 1,-1 0,-24 -1,-1 z m 0,1 24.0000003,0 0,24 -24.0000003,0 z"
+ id="rect3223-4"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <rect
+ width="4"
+ height="3"
+ x="7.9999995"
+ y="1036.3622"
+ id="rect4124-4"
+ style="fill:#008080;fill-opacity:1;stroke:none;filter:url(#filter5727)" />
+ <rect
+ width="2"
+ height="1"
+ x="9"
+ y="1037.3622"
+ id="rect4084-3"
+ style="fill:#00ffff;fill-opacity:1;stroke:none;filter:url(#filter5711)" />
+ <rect
+ width="4"
+ height="3"
+ x="20"
+ y="1036.3622"
+ id="rect4124-9-2"
+ style="fill:#008080;fill-opacity:1;stroke:none;filter:url(#filter5723)" />
+ <rect
+ width="2"
+ height="1"
+ x="21"
+ y="1037.3622"
+ id="rect4086-7"
+ style="fill:#00ffff;fill-opacity:1;stroke:none;filter:url(#filter5719)" />
+ </g>
+</svg>
diff --git a/launcher/resources/sources/skeleton.svg b/launcher/resources/sources/skeleton.svg
new file mode 100644
index 00000000..5d55f272
--- /dev/null
+++ b/launcher/resources/sources/skeleton.svg
@@ -0,0 +1,610 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="skeleton.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/skeleton128.png"
+ inkscape:export-xdpi="360"
+ inkscape:export-ydpi="360">
+ <defs
+ id="defs4">
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter5723"
+ x="-0.20999999"
+ width="1.42"
+ y="-0.28"
+ height="1.5599999">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.35"
+ id="feGaussianBlur5725" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter5727"
+ x="-0.20999999"
+ width="1.42"
+ y="-0.28"
+ height="1.5599999">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.35"
+ id="feGaussianBlur5729" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4028-6"
+ x="-0.14708571"
+ width="1.2941715"
+ y="-0.25740001"
+ height="1.5148">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.429"
+ id="feGaussianBlur4030-9" />
+ </filter>
+ <filter
+ color-interpolation-filters="sRGB"
+ inkscape:collect="always"
+ id="filter4056-6">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.676"
+ id="feGaussianBlur4058-6" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="11.313708"
+ inkscape:cx="-18.309169"
+ inkscape:cy="22.958832"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="1614"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2996"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1020.3622)">
+ <rect
+ style="fill:#999999;fill-opacity:1;stroke:none"
+ id="rect4063-2"
+ width="24"
+ height="24"
+ x="3.9999998"
+ y="1024.3622" />
+ <rect
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none;filter:url(#filter4056-6)"
+ id="rect4038-2"
+ width="26"
+ height="26"
+ x="3"
+ y="3"
+ transform="matrix(1.0769231,0,0,1.0769237,-1.2307693,1019.1314)" />
+ <rect
+ y="1041.3622"
+ x="5.9999995"
+ height="5.0000172"
+ width="20"
+ id="rect3189-4"
+ style="fill:#494949;fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter4028-6)"
+ d="m 6.0000007,1041.3622 20.0000003,0 0,2 -18.0000003,0 0,3 -2,0 z"
+ id="path4034-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <rect
+ style="fill:#494949;fill-opacity:1;stroke:none"
+ id="rect3155-9"
+ width="8"
+ height="5.0000172"
+ x="5.9999995"
+ y="1035.3622" />
+ <rect
+ y="1035.3622"
+ x="18"
+ height="5.0000172"
+ width="8"
+ id="rect3163-4"
+ style="fill:#494949;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#a1a1a1;fill-opacity:1;stroke:none"
+ id="rect3009-1"
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1024.3622" />
+ <rect
+ y="1024.3622"
+ x="6.9999995"
+ height="3"
+ width="3"
+ id="rect3011-9"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none"
+ id="rect3013-0"
+ width="3"
+ height="3"
+ x="10"
+ y="1024.3622" />
+ <rect
+ style="fill:#919191;fill-opacity:1;stroke:none"
+ id="rect3017-9"
+ width="3"
+ height="3"
+ x="13"
+ y="1024.3622" />
+ <rect
+ y="1024.3622"
+ x="16"
+ height="3"
+ width="3"
+ id="rect3019-6"
+ style="fill:#888888;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#8f8f8f;fill-opacity:1;stroke:none"
+ id="rect3021-8"
+ width="3"
+ height="3"
+ x="19"
+ y="1024.3622" />
+ <rect
+ y="1024.3622"
+ x="22"
+ height="3"
+ width="3"
+ id="rect3023-6"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#989898;fill-opacity:1;stroke:none"
+ id="rect3025-7"
+ width="3"
+ height="3"
+ x="25"
+ y="1024.3622" />
+ <rect
+ y="1027.3622"
+ x="3.9999998"
+ height="3"
+ width="3"
+ id="rect3027-7"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none"
+ id="rect3029-6"
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="10"
+ height="3"
+ width="3"
+ id="rect3031-0"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none"
+ id="rect3033-4"
+ width="3"
+ height="3"
+ x="10"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="13"
+ height="3"
+ width="3"
+ id="rect3035-4"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#c1c1c1;fill-opacity:1;stroke:none"
+ id="rect3037-2"
+ width="3"
+ height="3"
+ x="16"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="19"
+ height="3"
+ width="3"
+ id="rect3039-4"
+ style="fill:#bebebe;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none"
+ id="rect3041-2"
+ width="3"
+ height="3"
+ x="22"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="25"
+ height="3"
+ width="3"
+ id="rect3043-1"
+ style="fill:#919191;fill-opacity:1;stroke:none" />
+ <rect
+ y="1030.3622"
+ x="3.9999998"
+ height="3"
+ width="3"
+ id="rect3045-3"
+ style="fill:#9b9b9b;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#c7c7c7;fill-opacity:1;stroke:none"
+ id="rect3047-1"
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1030.3622" />
+ <rect
+ y="1030.3622"
+ x="10"
+ height="3"
+ width="3"
+ id="rect3049-5"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#cacaca;fill-opacity:1;stroke:none"
+ id="rect3051-9"
+ width="3"
+ height="3"
+ x="10"
+ y="1030.3622" />
+ <rect
+ y="1030.3622"
+ x="13"
+ height="3"
+ width="3"
+ id="rect3053-2"
+ style="fill:#d8d8d8;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#cfcfcf;fill-opacity:1;stroke:none"
+ id="rect3055-9"
+ width="3"
+ height="3"
+ x="16"
+ y="1030.3622" />
+ <rect
+ y="1030.3622"
+ x="19"
+ height="3"
+ width="3"
+ id="rect3057-7"
+ style="fill:#cfcfcf;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#bababa;fill-opacity:1;stroke:none"
+ id="rect3059-2"
+ width="3"
+ height="3"
+ x="22"
+ y="1030.3622" />
+ <rect
+ y="1030.3622"
+ x="25"
+ height="3"
+ width="3"
+ id="rect3061-7"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <path
+ sodipodi:nodetypes="ccccccc"
+ inkscape:connector-curvature="0"
+ id="path4032-3"
+ d="m 6.0000007,1035.3622 8.0000003,0 0,2 -6.0000003,0 0,3 -2,0 z"
+ style="fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter4028-6)" />
+ <rect
+ style="fill:#bababa;fill-opacity:1;stroke:none"
+ id="rect3063-8"
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="6.9999995"
+ height="3"
+ width="3"
+ id="rect3065-6"
+ style="fill:#c4c4c4;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none"
+ id="rect3067-1"
+ width="3"
+ height="3"
+ x="10"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="10"
+ height="3"
+ width="3"
+ id="rect3069-9"
+ style="fill:#bababa;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#c1c1c1;fill-opacity:1;stroke:none"
+ id="rect3071-7"
+ width="3"
+ height="3"
+ x="13"
+ y="1033.3622" />
+ <path
+ style="fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter4028-6)"
+ d="m 18.000001,1035.3622 8,0 0,2 -6,0 0,3 -2,0 z"
+ id="path4036-6"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ <rect
+ y="1033.3622"
+ x="16"
+ height="3"
+ width="3"
+ id="rect3073-1"
+ style="fill:#afafaf;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#cacaca;fill-opacity:1;stroke:none"
+ id="rect3075-8"
+ width="3"
+ height="3"
+ x="19"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="22"
+ height="3"
+ width="3"
+ id="rect3077-8"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none"
+ id="rect3079-2"
+ width="3"
+ height="3"
+ x="25"
+ y="1033.3622" />
+ <rect
+ y="1036.3622"
+ x="3.9999998"
+ height="3"
+ width="3"
+ id="rect3153-6"
+ style="fill:#c4c4c4;fill-opacity:1;stroke:none" />
+ <rect
+ y="1036.3622"
+ x="13"
+ height="3"
+ width="3"
+ id="rect3159-7"
+ style="fill:#c4c4c4;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#cacaca;fill-opacity:1;stroke:none"
+ id="rect3161-0"
+ width="3"
+ height="3"
+ x="16"
+ y="1036.3622" />
+ <rect
+ y="1036.3622"
+ x="25"
+ height="3"
+ width="3"
+ id="rect3167-3"
+ style="fill:#bababa;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#a7a7a7;fill-opacity:1;stroke:none"
+ id="rect3169-4"
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1039.3622" />
+ <rect
+ y="1039.3622"
+ x="6.9999995"
+ height="3"
+ width="3"
+ id="rect3171-8"
+ style="fill:#c1c1c1;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none"
+ id="rect3173-5"
+ width="3"
+ height="3"
+ x="10"
+ y="1039.3622" />
+ <rect
+ y="1039.3622"
+ x="10"
+ height="3"
+ width="3"
+ id="rect3175-0"
+ style="fill:#cacaca;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#828282;fill-opacity:1;stroke:none"
+ id="rect3177-4"
+ width="6"
+ height="3"
+ x="13"
+ y="1039.3622" />
+ <rect
+ style="fill:#cacaca;fill-opacity:1;stroke:none"
+ id="rect3181-9"
+ width="3"
+ height="3"
+ x="19"
+ y="1039.3622" />
+ <rect
+ y="1039.3622"
+ x="22"
+ height="3"
+ width="3"
+ id="rect3183-7"
+ style="fill:#aaaaaa;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#8f8f8f;fill-opacity:1;stroke:none"
+ id="rect3185-6"
+ width="3"
+ height="3"
+ x="25"
+ y="1039.3622" />
+ <rect
+ style="fill:#9e9e9e;fill-opacity:1;stroke:none"
+ id="rect3187-1"
+ width="3"
+ height="3"
+ x="3.9999998"
+ y="1042.3622" />
+ <rect
+ style="fill:#919191;fill-opacity:1;stroke:none"
+ id="rect3203-8"
+ width="3"
+ height="3"
+ x="25"
+ y="1042.3622" />
+ <rect
+ y="1045.3622"
+ x="3.9999998"
+ height="3"
+ width="3"
+ id="rect3205-4"
+ style="fill:#828282;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#7f7f7f;fill-opacity:1;stroke:none"
+ id="rect3207-2"
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1045.3622" />
+ <rect
+ y="1045.3622"
+ x="10"
+ height="3"
+ width="3"
+ id="rect3209-3"
+ style="fill:#a3a3a3;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#858585;fill-opacity:1;stroke:none"
+ id="rect3211-3"
+ width="3"
+ height="3"
+ x="10"
+ y="1045.3622" />
+ <rect
+ y="1045.3622"
+ x="13"
+ height="3"
+ width="3"
+ id="rect3213-9"
+ style="fill:#858585;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#919191;fill-opacity:1;stroke:none"
+ id="rect3215-7"
+ width="3"
+ height="3"
+ x="16"
+ y="1045.3622" />
+ <rect
+ y="1045.3622"
+ x="19"
+ height="3"
+ width="3"
+ id="rect3217-0"
+ style="fill:#858585;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#949494;fill-opacity:1;stroke:none"
+ id="rect3219-4"
+ width="3"
+ height="3"
+ x="22"
+ y="1045.3622" />
+ <rect
+ y="1045.3622"
+ x="25"
+ height="3"
+ width="3"
+ id="rect3221-5"
+ style="fill:#888888;fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#333333;fill-opacity:1;stroke:none"
+ d="m 4.0000007,1023.3622 -1,1 0,24 1,1 24.0000003,0 1,-1 0,-24 -1,-1 z m 0,1 24.0000003,0 0,24 -24.0000003,0 z"
+ id="rect3223-4"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccccccc" />
+ <rect
+ style="fill:#008080;fill-opacity:1;stroke:none;filter:url(#filter5727)"
+ id="rect4124-4"
+ width="4"
+ height="3"
+ x="7.9999995"
+ y="1036.3622" />
+ <rect
+ style="fill:#00ffff;fill-opacity:1;stroke:none;"
+ id="rect4084-3"
+ width="2"
+ height="1"
+ x="9"
+ y="1037.3622" />
+ <rect
+ style="fill:#008080;fill-opacity:1;stroke:none;filter:url(#filter5723)"
+ id="rect4124-9-2"
+ width="4"
+ height="3"
+ x="20"
+ y="1036.3622" />
+ <rect
+ style="fill:#00ffff;fill-opacity:1;stroke:none;"
+ id="rect4086-7"
+ width="2"
+ height="1"
+ x="21"
+ y="1037.3622" />
+ </g>
+</svg>
diff --git a/launcher/resources/sources/squarecreeper.svg b/launcher/resources/sources/squarecreeper.svg
new file mode 100644
index 00000000..a1b0f4d1
--- /dev/null
+++ b/launcher/resources/sources/squarecreeper.svg
@@ -0,0 +1,828 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="squarecreeper.svg">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3144">
+ <stop
+ style="stop-color:#ffbe01;stop-opacity:1;"
+ offset="0"
+ id="stop3146" />
+ <stop
+ id="stop3922"
+ offset="0.48206061"
+ style="stop-color:#ff0101;stop-opacity:1" />
+ <stop
+ id="stop3920"
+ offset="0.76562566"
+ style="stop-color:#e70000;stop-opacity:0.45098039;" />
+ <stop
+ style="stop-color:#d40000;stop-opacity:0;"
+ offset="1"
+ id="stop3148" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3917">
+ <stop
+ id="stop3919"
+ offset="0"
+ style="stop-color:#115008;stop-opacity:1;" />
+ <stop
+ style="stop-color:#39c228;stop-opacity:1;"
+ offset="0.49999952"
+ id="stop3921" />
+ <stop
+ id="stop3923"
+ offset="1"
+ style="stop-color:#c7f7c2;stop-opacity:1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3892">
+ <stop
+ style="stop-color:#1a1a1a;stop-opacity:1;"
+ offset="0"
+ id="stop3894" />
+ <stop
+ style="stop-color:#1a1a1a;stop-opacity:0;"
+ offset="1"
+ id="stop3896" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3871">
+ <stop
+ style="stop-color:#115008;stop-opacity:1;"
+ offset="0"
+ id="stop3873" />
+ <stop
+ id="stop3900"
+ offset="0.5"
+ style="stop-color:#39c228;stop-opacity:1;" />
+ <stop
+ style="stop-color:#c7f7c2;stop-opacity:1;"
+ offset="1"
+ id="stop3875" />
+ </linearGradient>
+ <filter
+ inkscape:collect="always"
+ id="filter3867">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.792"
+ id="feGaussianBlur3869" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3892"
+ id="linearGradient3898"
+ x1="16"
+ y1="31"
+ x2="16"
+ y2="26"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3871"
+ id="linearGradient5605"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,1020.3622)"
+ x1="27"
+ y1="26"
+ x2="5"
+ y2="3" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3871-3"
+ id="linearGradient5605-2"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(0,1020.3622)"
+ x1="27"
+ y1="26"
+ x2="5"
+ y2="3" />
+ <linearGradient
+ id="linearGradient3871-3">
+ <stop
+ style="stop-color:#115008;stop-opacity:1;"
+ offset="0"
+ id="stop3873-5" />
+ <stop
+ id="stop3900-7"
+ offset="0.5"
+ style="stop-color:#39c228;stop-opacity:1;" />
+ <stop
+ style="stop-color:#c7f7c2;stop-opacity:1;"
+ offset="1"
+ id="stop3875-9" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3917"
+ id="linearGradient3147"
+ x1="31"
+ y1="1052.3622"
+ x2="4"
+ y2="1024.3622"
+ gradientUnits="userSpaceOnUse"
+ spreadMethod="reflect" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3144"
+ id="radialGradient3958"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.98990735,-2.841494e-6,-1.2215289e-6,0.96907996,-6.585324,13.296415)"
+ cx="10"
+ cy="1030.3622"
+ fx="10"
+ fy="1030.3622"
+ r="3.03125" />
+ <filter
+ inkscape:collect="always"
+ id="filter4012"
+ x="-0.37037037"
+ width="1.7407407"
+ y="-0.37037037"
+ height="1.7407407">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.30864198"
+ id="feGaussianBlur4014" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter4206"
+ x="-0.44837625"
+ width="1.8967525"
+ y="-0.44837625"
+ height="1.8967525">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.37364687"
+ id="feGaussianBlur4208" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="218.7892"
+ inkscape:cy="-32.230167"
+ inkscape:document-units="px"
+ inkscape:current-layer="g4186"
+ showgrid="false"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1"
+ inkscape:snap-grids="true"
+ inkscape:snap-to-guides="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-bbox="false"
+ inkscape:snap-object-midpoints="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1020.3622)">
+ <rect
+ style="fill:#2b1100;fill-opacity:1;stroke:none"
+ id="rect3063"
+ width="22"
+ height="22"
+ x="5"
+ y="5"
+ rx="5"
+ ry="5"
+ transform="translate(0,1020.3622)" />
+ <path
+ id="path3065"
+ d="m 6,1026.3622 0,22 22,0 0,-22 z m 2,2 6,0 0,6 6,0 0,-6 6,0 0,6 -6,0 0,3 3,0 0,9 -3,0 0,-3 -6,0 0,3 -3,0 0,-9 3,0 0,-3 -6,0 z"
+ style="fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter3867)"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccccccccccccccccccc" />
+ <g
+ id="g4186"
+ transform="matrix(7.8663398,0,0,5.253808,-157.10208,-4373.7892)">
+ <rect
+ y="9"
+ x="21"
+ height="2"
+ width="2"
+ id="rect4188"
+ style="opacity:0.25104603;fill:#ff0101;fill-opacity:1;stroke:#4d4d4d;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;filter:url(#filter4206)"
+ transform="matrix(0.74984559,0,0,1.4646069,5.5088131,1015.3024)" />
+ </g>
+ <path
+ style="fill:url(#linearGradient5605);fill-opacity:1;stroke:none"
+ d="m 7,1022.3622 c -2.77,0 -5,0 -5,0 l 0,30 28,0 c 0,-10 0,-20 0,-30 z m 0,5 6,0 0,6 6,0 0,-6 6,0 0,6 -6,0 0,3 3,0 0,9 -3,0 0,-3 -6,0 0,3 -3,0 0,-9 3,0 0,-3 -6,0 z"
+ id="rect3045"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sccccsccccccccccccccccccccc" />
+ <g
+ id="g5466"
+ style="opacity:0.3"
+ transform="translate(0,-1)">
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3902"
+ d="m 3.999974,1022.3622 3,0 0,3 -3,0 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3904"
+ d="m 6.999974,1022.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3906"
+ d="m 3.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3908"
+ d="m 0.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3914"
+ d="m 3.999974,1028.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3916"
+ d="m 0.999974,1028.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3918"
+ d="m 6.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3924"
+ d="m 9.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3926"
+ d="m 12.999974,1028.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3930"
+ d="m 12.999974,1025.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3932"
+ d="m 9.999974,1022.3622 3,0 0,3 -3,0 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3936"
+ d="m 12.999974,1022.3622 3,0 0,3 -3,0 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect3999"
+ d="m 18.99998,1028.3622 0,-3 3,0 0,3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4007"
+ d="m 15.99998,1031.3622 0,-3 3,0 0,3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4009"
+ d="m 24.99998,1031.3622 0,-3 3,0 0,3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4011"
+ d="m 24.99998,1034.3622 0,-3 3,0 0,3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4013"
+ d="m 21.99998,1028.3622 0,-3 3,0 0,3 z"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4015"
+ d="m 24.99998,1028.3622 0,-3 3,0 0,3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4017"
+ d="m 15.99998,1028.3622 0,-3 3,0 0,3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4019"
+ d="m 21.99998,1025.3622 0,-3 3,0 0,3 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4023"
+ d="m 24.99998,1025.3622 0,-3 3,0 0,3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4027"
+ d="m 18.99998,1025.3622 0,-3 3,0 0,3 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4029"
+ d="m 15.99998,1025.3622 0,-3 3,0 0,3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4035"
+ d="m 15.99998,1034.3622 0,-3 3,0 0,3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4039"
+ d="m 6.99998,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4041"
+ d="m 6.99998,1037.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4043"
+ d="m 9.99998,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4049"
+ d="m 3.99998,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4053"
+ d="m 12.99998,1031.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4059"
+ d="m 3.99998,1037.3622 0,3 3,0 0,-3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4063"
+ d="m 12.99998,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4069"
+ d="m 6.99998,1040.3622 0,3 3,0 0,-3 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4071"
+ d="m 3.99998,1040.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4073"
+ d="m 6.99998,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4075"
+ d="m 3.99998,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4077"
+ d="m 3.99998,1031.3622 0,3 3,0 0,-3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4133"
+ d="m 28,1028.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4143"
+ d="m 28,1031.3622 0,3 3,0 0,-3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4155"
+ d="m 28,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4159"
+ d="m 28,1037.3622 0,3 3,0 0,-3 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4161"
+ d="m 28,1025.3622 0,3 3,0 0,-3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4175"
+ d="m 28,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4185"
+ d="m 28,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4197"
+ d="m 28,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4203"
+ d="m 28,1040.3622 0,3 3,0 0,-3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4207"
+ d="m 6.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4211"
+ d="m 9.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4213"
+ d="m 9.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4215"
+ d="m 6.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4217"
+ d="m 3.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4219"
+ d="m 12.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4221"
+ d="m 12.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4245"
+ d="m 3.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4261"
+ d="m 0.99998,1034.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4263"
+ d="m 0.99998,1031.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4267"
+ d="m 0.99998,1037.3622 0,3 3,0 0,-3 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4273"
+ d="m 0.99998,1043.3622 0,3 3,0 0,-3 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4275"
+ d="m 0.99998,1040.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4303"
+ d="m 0.99998,1049.3622 0,3 3,0 0,-3 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4305"
+ d="m 0.99998,1046.3622 0,3 3,0 0,-3 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4093"
+ d="m 12.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4099"
+ d="m 15.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4105"
+ d="m 21.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4109"
+ d="m 21.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4113"
+ d="m 18.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4115"
+ d="m 21.999974,1037.3622 3,0 0,3 -3,0 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4117"
+ d="m 21.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4119"
+ d="m 9.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4377"
+ d="m 21.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4379"
+ d="m 18.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4381"
+ d="m 15.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4383"
+ d="m 15.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4387"
+ d="m 18.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4389"
+ d="m 15.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4391"
+ d="m 21.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4393"
+ d="m 21.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4395"
+ d="m 21.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4397"
+ d="m 24.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4399"
+ d="m 27.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4401"
+ d="m 24.999974,1049.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4403"
+ d="m 27.999974,1046.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4405"
+ d="m 24.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4407"
+ d="m 24.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4409"
+ d="m 27.999974,1043.3622 3,0 0,3 -3,0 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4411"
+ d="m 27.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4417"
+ d="m 18.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4419"
+ d="m 21.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4433"
+ d="m 21.999974,1037.3622 3,0 0,3 -3,0 z"
+ style="fill:#4d4d4d;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4435"
+ d="m 21.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#e6e6e6;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4439"
+ d="m 24.999974,1037.3622 3,0 0,3 -3,0 z"
+ style="fill:#999999;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4441"
+ d="m 27.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#f2f2f2;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4443"
+ d="m 24.999974,1040.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4445"
+ d="m 27.999974,1037.3622 3,0 0,3 -3,0 z"
+ style="fill:#cccccc;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4447"
+ d="m 24.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#666666;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4449"
+ d="m 24.999974,1031.3622 3,0 0,3 -3,0 z"
+ style="fill:#b3b3b3;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4451"
+ d="m 27.999974,1034.3622 3,0 0,3 -3,0 z"
+ style="fill:#333333;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4453"
+ d="m 27.999974,1031.3622 3,0 0,3 -3,0 z"
+ style="fill:#ececec;fill-opacity:1;stroke:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="rect4455"
+ d="m 15.999974,1031.3622 3,0 0,3 -3,0 z"
+ style="fill:#808080;fill-opacity:1;stroke:none" />
+ </g>
+ <path
+ style="fill:#000000;stroke:none"
+ d="m 0,1020.3622 c 0,10.0937 0,21.0424 0,32 l 2,0 c 0,-10 0,-20 0,-30 7.6666667,0 28,0 28,0 0,9.4258 0,19.9066 0,30 l 2,0 c 0,-10.6667 0,-21.3333 0,-32 z"
+ id="rect3887"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccc" />
+ <path
+ style="opacity:0.35;fill:url(#linearGradient3147);fill-opacity:1;stroke:none"
+ d="m 1,1021.3622 c 0,9.7507 0,20.4801 0,31 l 1,0 c 0,-10 0,-20 0,-30 7.6666667,0 28,0 28,0 0,9.4258 0,19.9066 0,30 l 1,0 c 0,-10.3333 0,-20.6667 0,-31 z"
+ id="rect3098"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccc" />
+ <rect
+ style="fill:url(#linearGradient3898);fill-opacity:1;stroke:none"
+ id="rect3890"
+ width="32"
+ height="6"
+ x="0"
+ y="26"
+ transform="translate(0,1020.3622)" />
+ <rect
+ y="1008.7997"
+ x="0.24999999"
+ height="6.0625"
+ width="6.0625"
+ id="rect3931"
+ style="fill:url(#radialGradient3958);fill-opacity:1;stroke:#4d4d4d;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none" />
+ <use
+ x="0"
+ y="0"
+ xlink:href="#rect3931"
+ id="use3954"
+ transform="translate(-12.8125,-2.374983)"
+ width="32"
+ height="32" />
+ <g
+ id="g4046"
+ style="opacity:1;">
+ <rect
+ transform="matrix(1.5471436,0,0,1.5471436,-12.037158,1014.8908)"
+ style="fill:#ff0101;fill-opacity:1;stroke:#4d4d4d;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;filter:url(#filter4012)"
+ id="rect4042"
+ width="2"
+ height="2"
+ x="21"
+ y="9" />
+ <rect
+ transform="matrix(0.70378989,0,0,0.70378989,6.5166224,1023.3243)"
+ y="9"
+ x="21"
+ height="2"
+ width="2"
+ id="rect3960"
+ style="fill:#ffc501;fill-opacity:1;stroke:#4d4d4d;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;filter:url(#filter4012)" />
+ <rect
+ style="fill:#fcff01;fill-opacity:1;stroke:#4d4d4d;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;filter:url(#filter4012)"
+ id="rect4044"
+ width="2"
+ height="2"
+ x="21"
+ y="9"
+ transform="matrix(0.16522605,0,0,0.16522605,18.365027,1028.7099)" />
+ </g>
+ <use
+ x="0"
+ y="0"
+ xlink:href="#g4046"
+ id="use4096"
+ transform="translate(-12.000001,0)"
+ width="32"
+ height="32" />
+ </g>
+</svg>
diff --git a/launcher/resources/sources/steve.svg b/launcher/resources/sources/steve.svg
new file mode 100644
index 00000000..2233272c
--- /dev/null
+++ b/launcher/resources/sources/steve.svg
@@ -0,0 +1,534 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="32"
+ height="32"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="steve.svg"
+ inkscape:export-filename="/home/peterix/projects/MultiMC4/src/resources/insticons/steve128.png"
+ inkscape:export-xdpi="360"
+ inkscape:export-ydpi="360">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ id="filter3927"
+ x="-0.10864197"
+ width="1.2172839"
+ y="-0.10864198"
+ height="1.217284">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.1769547"
+ id="feGaussianBlur3929" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#484848"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4"
+ inkscape:cx="-15.767253"
+ inkscape:cy="34.171841"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1607"
+ inkscape:window-height="1030"
+ inkscape:window-x="1676"
+ inkscape:window-y="-3"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2996"
+ empspacing="4"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1020.3622)">
+ <path
+ style="fill:#002020;fill-opacity:1;stroke:none;filter:url(#filter3927)"
+ d="m 5.6875,1024.4872 c -1,0 -2,1 -2,2 l 0,22 c 0,1 1,2 2,2 l 22,0 c 1,0 2,-1 2,-2 l 0,-22 c 0,-1 -1,-2 -2,-2 z"
+ id="rect3223-4"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccc" />
+ <path
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none"
+ d="m 6,1024.3622 20,0 c 1,0 2,1 2,2 l 0,7 -3,0 0,-3 -18,0 0,3 -3,0 0,-7.005 c 0,-0.995 1,-1.995 2,-1.995 z"
+ id="rect3978"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccccc" />
+ <path
+ style="fill:#421d0a;fill-opacity:1;stroke:none"
+ d="m 10,22 3,0 0,-3 6,0 0,3 3,0 0,6 -12,0 z"
+ id="rect3981"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccccc"
+ transform="translate(0,1020.3622)" />
+ <rect
+ style="fill:#ffffff;fill-opacity:1;stroke:none"
+ id="rect3080"
+ width="8"
+ height="5.0000172"
+ x="6"
+ y="1035.3622" />
+ <rect
+ y="1035.3622"
+ x="18"
+ height="4.9999957"
+ width="8"
+ id="rect3082"
+ style="fill:#ffffff;fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#9c694c;fill-opacity:1;stroke:none"
+ d="m 7,10 18,0 0,3 3,0 0,13 c 0,1 -1,2 -2,2 l -4,0 0,-6 -3,0 0,-3 6,0 0,-3 -6,0 0,3 -6,0 0,-3 -6,0 0,3 6,0 0,3 -3,0 0,6 -4,0 C 5,28 4,27 4,26 l 0,-10 0,-3 3,0 z"
+ id="rect3941"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccccccccccccccccccccccc"
+ transform="translate(0,1020.3622)" />
+ <rect
+ y="1024.3622"
+ x="6.9999995"
+ height="3"
+ width="3"
+ id="rect3011-9"
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#2f1f0f;fill-opacity:1;stroke:none"
+ id="rect3013-0"
+ width="3"
+ height="3"
+ x="10"
+ y="1024.3622" />
+ <rect
+ style="fill:#281c0b;fill-opacity:1;stroke:none"
+ id="rect3017-9"
+ width="3"
+ height="3"
+ x="13"
+ y="1024.3622" />
+ <rect
+ y="1024.3622"
+ x="16"
+ height="3"
+ width="3"
+ id="rect3019-6"
+ style="fill:#241808;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#261a0a;fill-opacity:1;stroke:none"
+ id="rect3021-8"
+ width="3"
+ height="3"
+ x="19"
+ y="1024.3622" />
+ <rect
+ y="1024.3622"
+ x="22"
+ height="3"
+ width="3"
+ id="rect3023-6"
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none" />
+ <rect
+ y="1027.3622"
+ x="3.9999998"
+ height="3"
+ width="3"
+ id="rect3027-7"
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none"
+ id="rect3029-6"
+ width="3"
+ height="3"
+ x="6.9999995"
+ y="1027.3622" />
+ <rect
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none"
+ id="rect3033-4"
+ width="3"
+ height="3"
+ x="10"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="13"
+ height="3"
+ width="3"
+ id="rect3035-4"
+ style="fill:#332411;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#422a12;fill-opacity:1;stroke:none"
+ id="rect3037-2"
+ width="3"
+ height="3"
+ x="16"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="19"
+ height="3"
+ width="3"
+ id="rect3039-4"
+ style="fill:#3f2a15;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#2c1e0e;fill-opacity:1;stroke:none"
+ id="rect3041-2"
+ width="3"
+ height="3"
+ x="22"
+ y="1027.3622" />
+ <rect
+ y="1027.3622"
+ x="25"
+ height="3"
+ width="3"
+ id="rect3043-1"
+ style="fill:#281c0b;fill-opacity:1;stroke:none" />
+ <rect
+ y="1030.3622"
+ x="3.9999998"
+ height="3"
+ width="3"
+ id="rect3045-3"
+ style="fill:#2b1e0d;fill-opacity:1;stroke:none" />
+ <rect
+ y="1030.3622"
+ x="25"
+ height="3"
+ width="3"
+ id="rect3061-7"
+ style="fill:#342512;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#8a4c3d;fill-opacity:1;stroke:none"
+ id="rect3089"
+ width="6"
+ height="3"
+ x="13"
+ y="1042.3622" />
+ <g
+ id="g3856"
+ transform="translate(0,1.7382813e-5)">
+ <rect
+ y="1039.3622"
+ x="13"
+ height="3"
+ width="6"
+ id="rect3177-4"
+ style="fill:#6a4030;fill-opacity:1;stroke:none" />
+ <rect
+ y="1042.3622"
+ x="10"
+ height="3"
+ width="3"
+ id="rect3085"
+ style="fill:#41210c;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#41210c;fill-opacity:1;stroke:none"
+ id="rect3091"
+ width="3"
+ height="3"
+ x="19"
+ y="1042.3622" />
+ <rect
+ y="1045.3622"
+ x="13"
+ height="3"
+ width="3"
+ id="rect3093"
+ style="fill:#421d0a;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#45220e;fill-opacity:1;stroke:none"
+ id="rect3095"
+ width="3"
+ height="3"
+ x="19"
+ y="1045.3622" />
+ <rect
+ y="1045.3622"
+ x="10"
+ height="3"
+ width="3"
+ id="rect3097"
+ style="fill:#45220e;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#45220e;fill-opacity:1;stroke:none"
+ id="rect3099"
+ width="3"
+ height="3"
+ x="16"
+ y="1045.3622" />
+ </g>
+ <g
+ id="g3944"
+ transform="translate(-28,1.7382813e-5)">
+ <rect
+ y="1030.3622"
+ x="35"
+ height="3"
+ width="3"
+ id="rect3047-1"
+ style="fill:#b6896c;fill-opacity:1;stroke:none" />
+ <rect
+ y="1030.3622"
+ x="38"
+ height="3"
+ width="3"
+ id="rect3051-9"
+ style="fill:#bd8e72;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#c69680;fill-opacity:1;stroke:none"
+ id="rect3053-2"
+ width="3"
+ height="3"
+ x="41"
+ y="1030.3622" />
+ <rect
+ y="1030.3622"
+ x="44"
+ height="3"
+ width="3"
+ id="rect3055-9"
+ style="fill:#bd8b72;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#bd8e74;fill-opacity:1;stroke:none"
+ id="rect3057-7"
+ width="3"
+ height="3"
+ x="47"
+ y="1030.3622" />
+ <rect
+ y="1030.3622"
+ x="50"
+ height="3"
+ width="3"
+ id="rect3059-2"
+ style="fill:#ac765a;fill-opacity:1;stroke:none" />
+ <rect
+ y="1033.3622"
+ x="32"
+ height="3"
+ width="3"
+ id="rect3063-8"
+ style="fill:#aa7d66;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#b4846d;fill-opacity:1;stroke:none"
+ id="rect3065-6"
+ width="3"
+ height="3"
+ x="35"
+ y="1033.3622" />
+ <rect
+ style="fill:#aa7d66;fill-opacity:1;stroke:none"
+ id="rect3069-9"
+ width="3"
+ height="3"
+ x="38"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="41"
+ height="3"
+ width="3"
+ id="rect3071-7"
+ style="fill:#ad806d;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#9c725c;fill-opacity:1;stroke:none"
+ id="rect3073-1"
+ width="3"
+ height="3"
+ x="44"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="47"
+ height="3"
+ width="3"
+ id="rect3075-8"
+ style="fill:#bb8972;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#9c694c;fill-opacity:1;stroke:none"
+ id="rect3077-8"
+ width="3"
+ height="3"
+ x="50"
+ y="1033.3622" />
+ <rect
+ y="1033.3622"
+ x="53"
+ height="3"
+ width="3"
+ id="rect3079-2"
+ style="fill:#9c694c;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#b4846d;fill-opacity:1;stroke:none"
+ id="rect3153-6"
+ width="3"
+ height="3"
+ x="32"
+ y="1036.3622" />
+ <rect
+ style="fill:#b57b67;fill-opacity:1;stroke:none"
+ id="rect3159-7"
+ width="3"
+ height="3"
+ x="41"
+ y="1036.3622" />
+ <rect
+ y="1036.3622"
+ x="44"
+ height="3"
+ width="3"
+ id="rect3161-0"
+ style="fill:#bb8972;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#aa7d66;fill-opacity:1;stroke:none"
+ id="rect3167-3"
+ width="3"
+ height="3"
+ x="53"
+ y="1036.3622" />
+ <rect
+ y="1039.3622"
+ x="32"
+ height="3"
+ width="3"
+ id="rect3169-4"
+ style="fill:#9c6346;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#b37b62;fill-opacity:1;stroke:none"
+ id="rect3171-8"
+ width="3"
+ height="3"
+ x="35"
+ y="1039.3622" />
+ <rect
+ style="fill:#b78272;fill-opacity:1;stroke:none"
+ id="rect3175-0"
+ width="3"
+ height="3"
+ x="38"
+ y="1039.3622" />
+ <rect
+ y="1039.3622"
+ x="47"
+ height="3"
+ width="3"
+ id="rect3181-9"
+ style="fill:#be886c;fill-opacity:1;stroke:none" />
+ <rect
+ style="fill:#a26a47;fill-opacity:1;stroke:none"
+ id="rect3183-7"
+ width="3"
+ height="3"
+ x="50"
+ y="1039.3622" />
+ <rect
+ y="1039.3622"
+ x="53"
+ height="3"
+ width="3"
+ id="rect3185-6"
+ style="fill:#805334;fill-opacity:1;stroke:none" />
+ <rect
+ y="1042.3622"
+ x="32"
+ height="3"
+ width="3"
+ id="rect3187-1"
+ style="fill:#905e43;fill-opacity:1;stroke:none" />
+ <rect
+ y="1042.3622"
+ x="53"
+ height="3"
+ width="3"
+ id="rect3203-8"
+ style="fill:#815339;fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#6f452c;fill-opacity:1;stroke:none"
+ d="m 32,1045.3622 3,0 0,3 -1,0 c -1,0 -2,-1 -2,-2 z"
+ id="rect3205-4"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <rect
+ y="1045.3622"
+ x="35"
+ height="3"
+ width="3"
+ id="rect3207-2"
+ style="fill:#6d432a;fill-opacity:1;stroke:none" />
+ <rect
+ y="1045.3622"
+ x="50"
+ height="3"
+ width="3"
+ id="rect3219-4"
+ style="fill:#83553b;fill-opacity:1;stroke:none" />
+ <path
+ style="fill:#7a4e33;fill-opacity:1;stroke:none"
+ d="m 53,1045.3622 3,0 0,1 c 0,1 -1,2 -2,2 l -1,0 z"
+ id="rect3221-5"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <rect
+ style="fill:#965f40;fill-opacity:1;stroke:none"
+ id="rect3083"
+ width="3"
+ height="3"
+ x="35"
+ y="1042.3622" />
+ <rect
+ y="1042.3622"
+ x="50"
+ height="3"
+ width="3"
+ id="rect3101"
+ style="fill:#8f5e3e;fill-opacity:1;stroke:none" />
+ </g>
+ <rect
+ style="fill:#523d89;fill-opacity:1;stroke:none"
+ id="rect3084"
+ width="3"
+ height="3"
+ x="10"
+ y="16"
+ transform="translate(0,1020.3622)" />
+ <rect
+ style="fill:#523d89;fill-opacity:1;stroke:none"
+ id="rect3086"
+ width="3"
+ height="3"
+ x="19"
+ y="16"
+ transform="translate(0,1020.3622)" />
+ </g>
+</svg>
diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp
new file mode 100644
index 00000000..1f195f00
--- /dev/null
+++ b/launcher/screenshots/ImgurAlbumCreation.cpp
@@ -0,0 +1,88 @@
+#include "ImgurAlbumCreation.h"
+
+#include <QNetworkRequest>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QUrl>
+#include <QStringList>
+
+#include "BuildConfig.h"
+#include "Env.h"
+#include <QDebug>
+
+ImgurAlbumCreation::ImgurAlbumCreation(QList<ScreenshotPtr> screenshots) : NetAction(), m_screenshots(screenshots)
+{
+ m_url = BuildConfig.IMGUR_BASE_URL + "album.json";
+ m_status = Job_NotStarted;
+}
+
+void ImgurAlbumCreation::start()
+{
+ m_status = Job_InProgress;
+ QNetworkRequest request(m_url);
+ request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str());
+ request.setRawHeader("Accept", "application/json");
+
+ QStringList hashes;
+ for (auto shot : m_screenshots)
+ {
+ hashes.append(shot->m_imgurDeleteHash);
+ }
+
+ const QByteArray data = "deletehashes=" + hashes.join(',').toUtf8() + "&title=Minecraft%20Screenshots&privacy=hidden";
+
+ QNetworkReply *rep = ENV.qnam().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/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h
new file mode 100644
index 00000000..954637e6
--- /dev/null
+++ b/launcher/screenshots/ImgurAlbumCreation.h
@@ -0,0 +1,42 @@
+#pragma once
+#include "net/NetAction.h"
+#include "Screenshot.h"
+
+typedef std::shared_ptr<class ImgurAlbumCreation> ImgurAlbumCreationPtr;
+class 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/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp
new file mode 100644
index 00000000..7e95d5ca
--- /dev/null
+++ b/launcher/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 "BuildConfig.h"
+#include "Env.h"
+#include <QDebug>
+
+ImgurUpload::ImgurUpload(ScreenshotPtr shot) : NetAction(), m_shot(shot)
+{
+ m_url = BuildConfig.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, BuildConfig.USER_AGENT_UNCACHED);
+ request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str());
+ request.setRawHeader("Accept", "application/json");
+
+ QFile f(m_shot->m_file.absoluteFilePath());
+ 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);
+
+ QNetworkReply *rep = ENV.qnam().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_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").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/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h
new file mode 100644
index 00000000..0507d499
--- /dev/null
+++ b/launcher/screenshots/ImgurUpload.h
@@ -0,0 +1,31 @@
+#pragma once
+#include "net/NetAction.h"
+#include "Screenshot.h"
+
+typedef std::shared_ptr<class ImgurUpload> ImgurUploadPtr;
+class 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/launcher/screenshots/Screenshot.h b/launcher/screenshots/Screenshot.h
new file mode 100644
index 00000000..9db3a8a1
--- /dev/null
+++ b/launcher/screenshots/Screenshot.h
@@ -0,0 +1,20 @@
+#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;
+ QString m_imgurDeleteHash;
+};
+
+typedef std::shared_ptr<ScreenShot> ScreenshotPtr;
diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp
new file mode 100644
index 00000000..6a3c801d
--- /dev/null
+++ b/launcher/settings/INIFile.cpp
@@ -0,0 +1,163 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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 if(c == '#')
+ out += '#';
+ 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 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 (const 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.
+ int commentIndex = 0;
+ QString line = lineRaw;
+ // Search for comments until no more escaped # are available
+ while((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) {
+ if(commentIndex > 0 && line.at(commentIndex - 1) == '\\') {
+ continue;
+ }
+ line = line.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/launcher/settings/INIFile.h b/launcher/settings/INIFile.h
new file mode 100644
index 00000000..4313e829
--- /dev/null
+++ b/launcher/settings/INIFile.h
@@ -0,0 +1,36 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QVariant>
+#include <QIODevice>
+
+// Sectionless INI parser (for instance config files)
+class 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/launcher/settings/INIFile_test.cpp b/launcher/settings/INIFile_test.cpp
new file mode 100644
index 00000000..08c2155e
--- /dev/null
+++ b/launcher/settings/INIFile_test.cpp
@@ -0,0 +1,63 @@
+#include <QTest>
+#include "TestUtil.h"
+
+#include "settings/INIFile.h"
+
+class IniFileTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void initTestCase()
+ {
+
+ }
+ void cleanupTestCase()
+ {
+
+ }
+
+ void test_Escape_data()
+ {
+ QTest::addColumn<QString>("through");
+
+ QTest::newRow("unix path") << "/abc/def/ghi/jkl";
+ QTest::newRow("windows path") << "C:\\Program files\\terrible\\name\\of something\\";
+ QTest::newRow("Plain text") << "Lorem ipsum dolor sit amet.";
+ QTest::newRow("Escape sequences") << "Lorem\n\t\n\\n\\tAAZ\nipsum dolor\n\nsit amet.";
+ QTest::newRow("Escape sequences 2") << "\"\n\n\"";
+ QTest::newRow("Hashtags") << "some data#something";
+ }
+ void test_Escape()
+ {
+ QFETCH(QString, through);
+
+ QString there = INIFile::escape(through);
+ QString back = INIFile::unescape(there);
+
+ QCOMPARE(back, through);
+ }
+
+ void test_SaveLoad()
+ {
+ QString a = "a";
+ QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\#thisIsNotAComment";
+ QString filename = "test_SaveLoad.ini";
+
+ // save
+ INIFile f;
+ f.set("a", a);
+ f.set("b", b);
+ f.saveFile(filename);
+
+ // load
+ INIFile f2;
+ f2.loadFile(filename);
+ QCOMPARE(a, f2.get("a","NOT SET").toString());
+ QCOMPARE(b, f2.get("b","NOT SET").toString());
+ }
+};
+
+QTEST_GUILESS_MAIN(IniFileTest)
+
+#include "INIFile_test.moc"
diff --git a/launcher/settings/INISettingsObject.cpp b/launcher/settings/INISettingsObject.cpp
new file mode 100644
index 00000000..12513403
--- /dev/null
+++ b/launcher/settings/INISettingsObject.cpp
@@ -0,0 +1,107 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/settings/INISettingsObject.h b/launcher/settings/INISettingsObject.h
new file mode 100644
index 00000000..26cc32e5
--- /dev/null
+++ b/launcher/settings/INISettingsObject.h
@@ -0,0 +1,64 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+
+#include "settings/INIFile.h"
+
+#include "settings/SettingsObject.h"
+
+/*!
+ * \brief A settings object that stores its settings in an INIFile.
+ */
+class 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/launcher/settings/OverrideSetting.cpp b/launcher/settings/OverrideSetting.cpp
new file mode 100644
index 00000000..4396a381
--- /dev/null
+++ b/launcher/settings/OverrideSetting.cpp
@@ -0,0 +1,54 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/settings/OverrideSetting.h b/launcher/settings/OverrideSetting.h
new file mode 100644
index 00000000..9f0c98b5
--- /dev/null
+++ b/launcher/settings/OverrideSetting.h
@@ -0,0 +1,46 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/launcher/settings/PassthroughSetting.cpp b/launcher/settings/PassthroughSetting.cpp
new file mode 100644
index 00000000..8f93b251
--- /dev/null
+++ b/launcher/settings/PassthroughSetting.cpp
@@ -0,0 +1,69 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PassthroughSetting.h"
+
+PassthroughSetting::PassthroughSetting(std::shared_ptr<Setting> other, std::shared_ptr<Setting> gate)
+ : Setting(other->configKeys(), QVariant())
+{
+ Q_ASSERT(other);
+ m_other = other;
+ m_gate = gate;
+}
+
+bool PassthroughSetting::isOverriding() const
+{
+ if(!m_gate)
+ {
+ return false;
+ }
+ 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/launcher/settings/PassthroughSetting.h b/launcher/settings/PassthroughSetting.h
new file mode 100644
index 00000000..22008f83
--- /dev/null
+++ b/launcher/settings/PassthroughSetting.h
@@ -0,0 +1,45 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <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/launcher/settings/Setting.cpp b/launcher/settings/Setting.cpp
new file mode 100644
index 00000000..cfe5a7f9
--- /dev/null
+++ b/launcher/settings/Setting.cpp
@@ -0,0 +1,53 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/settings/Setting.h b/launcher/settings/Setting.h
new file mode 100644
index 00000000..9beeb35e
--- /dev/null
+++ b/launcher/settings/Setting.h
@@ -0,0 +1,117 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QVariant>
+#include <QStringList>
+#include <memory>
+
+class SettingsObject;
+
+/*!
+ *
+ */
+class 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/launcher/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp
new file mode 100644
index 00000000..8a0bc045
--- /dev/null
+++ b/launcher/settings/SettingsObject.cpp
@@ -0,0 +1,142 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h
new file mode 100644
index 00000000..3d61e707
--- /dev/null
+++ b/launcher/settings/SettingsObject.h
@@ -0,0 +1,212 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QMap>
+#include <QStringList>
+#include <QVariant>
+#include <memory>
+
+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 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/launcher/setupwizard/AnalyticsWizardPage.cpp b/launcher/setupwizard/AnalyticsWizardPage.cpp
new file mode 100644
index 00000000..4fb0bcca
--- /dev/null
+++ b/launcher/setupwizard/AnalyticsWizardPage.cpp
@@ -0,0 +1,63 @@
+#include "AnalyticsWizardPage.h"
+#include <MultiMC.h>
+
+#include <QVBoxLayout>
+#include <QTextBrowser>
+#include <QCheckBox>
+
+#include <ganalytics.h>
+#include <BuildConfig.h>
+
+AnalyticsWizardPage::AnalyticsWizardPage(QWidget *parent)
+ : BaseWizardPage(parent)
+{
+ setObjectName(QStringLiteral("analyticsPage"));
+ verticalLayout_3 = new QVBoxLayout(this);
+ verticalLayout_3->setObjectName(QStringLiteral("verticalLayout_3"));
+ textBrowser = new QTextBrowser(this);
+ textBrowser->setObjectName(QStringLiteral("textBrowser"));
+ textBrowser->setAcceptRichText(false);
+ textBrowser->setOpenExternalLinks(true);
+ verticalLayout_3->addWidget(textBrowser);
+
+ checkBox = new QCheckBox(this);
+ checkBox->setObjectName(QStringLiteral("checkBox"));
+ checkBox->setChecked(true);
+ verticalLayout_3->addWidget(checkBox);
+ retranslate();
+}
+
+AnalyticsWizardPage::~AnalyticsWizardPage()
+{
+}
+
+bool AnalyticsWizardPage::validatePage()
+{
+ auto settings = MMC->settings();
+ auto analytics = MMC->analytics();
+ auto status = checkBox->isChecked();
+ settings->set("AnalyticsSeen", analytics->version());
+ settings->set("Analytics", status);
+ return true;
+}
+
+void AnalyticsWizardPage::retranslate()
+{
+ setTitle(tr("Analytics"));
+ setSubTitle(tr("We track some anonymous statistics about users."));
+ textBrowser->setHtml(tr(
+ "<html><body>"
+ "<p>MultiMC sends anonymous usage statistics on every start of the application. This helps us decide what platforms and issues to focus on.</p>"
+ "<p>The data is processed by Google Analytics, see their <a href=\"https://support.google.com/analytics/answer/6004245?hl=en\">article on the "
+ "matter</a>.</p>"
+ "<p>The following data is collected:</p>"
+ "<ul><li>A random unique ID of the MultiMC installation.<br />It is stored in the application settings (multimc.cfg).</li>"
+ "<li>Anonymized (partial) IP address.</li>"
+ "<li>MultiMC version.</li>"
+ "<li>Operating system name, version and architecture.</li>"
+ "<li>CPU architecture (kernel architecture on linux).</li>"
+ "<li>Size of system memory.</li>"
+ "<li>Java version, architecture and memory settings.</li></ul>"
+ "<p>If we change the tracked information, you will see this page again.</p></body></html>"));
+ checkBox->setText(tr("Enable Analytics"));
+}
diff --git a/launcher/setupwizard/AnalyticsWizardPage.h b/launcher/setupwizard/AnalyticsWizardPage.h
new file mode 100644
index 00000000..c451db2c
--- /dev/null
+++ b/launcher/setupwizard/AnalyticsWizardPage.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "BaseWizardPage.h"
+
+class QVBoxLayout;
+class QTextBrowser;
+class QCheckBox;
+
+class AnalyticsWizardPage : public BaseWizardPage
+{
+ Q_OBJECT
+public:
+ explicit AnalyticsWizardPage(QWidget *parent = Q_NULLPTR);
+ virtual ~AnalyticsWizardPage();
+
+ bool validatePage() override;
+
+protected:
+ void retranslate() override;
+
+private:
+ QVBoxLayout *verticalLayout_3 = nullptr;
+ QTextBrowser *textBrowser = nullptr;
+ QCheckBox *checkBox = nullptr;
+}; \ No newline at end of file
diff --git a/launcher/setupwizard/BaseWizardPage.h b/launcher/setupwizard/BaseWizardPage.h
new file mode 100644
index 00000000..72dbecfd
--- /dev/null
+++ b/launcher/setupwizard/BaseWizardPage.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <QWizardPage>
+#include <QEvent>
+
+class BaseWizardPage : public QWizardPage
+{
+public:
+ explicit BaseWizardPage(QWidget *parent = Q_NULLPTR)
+ : QWizardPage(parent)
+ {
+ }
+ virtual ~BaseWizardPage() {};
+
+ virtual bool wantsRefreshButton()
+ {
+ return false;
+ }
+ virtual void refresh()
+ {
+ }
+
+protected:
+ virtual void retranslate() = 0;
+ void changeEvent(QEvent * event) override
+ {
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslate();
+ }
+ QWizardPage::changeEvent(event);
+ }
+};
diff --git a/launcher/setupwizard/JavaWizardPage.cpp b/launcher/setupwizard/JavaWizardPage.cpp
new file mode 100644
index 00000000..ad571c09
--- /dev/null
+++ b/launcher/setupwizard/JavaWizardPage.cpp
@@ -0,0 +1,96 @@
+#include "JavaWizardPage.h"
+#include <MultiMC.h>
+
+#include <QVBoxLayout>
+#include <QGroupBox>
+#include <QSpinBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QToolButton>
+#include <widgets/VersionSelectWidget.h>
+#include <FileSystem.h>
+
+#include <java/JavaInstall.h>
+#include <dialogs/CustomMessageBox.h>
+#include <java/JavaUtils.h>
+#include <sys.h>
+#include <QFileDialog>
+#include <JavaCommon.h>
+#include "widgets/JavaSettingsWidget.h"
+
+
+JavaWizardPage::JavaWizardPage(QWidget *parent)
+ :BaseWizardPage(parent)
+{
+ setupUi();
+}
+
+void JavaWizardPage::setupUi()
+{
+ setObjectName(QStringLiteral("javaPage"));
+ QVBoxLayout * layout = new QVBoxLayout(this);
+
+ m_java_widget = new JavaSettingsWidget(this);
+ layout->addWidget(m_java_widget);
+ setLayout(layout);
+
+ retranslate();
+}
+
+void JavaWizardPage::refresh()
+{
+ m_java_widget->refresh();
+}
+
+void JavaWizardPage::initializePage()
+{
+ m_java_widget->initialize();
+}
+
+bool JavaWizardPage::wantsRefreshButton()
+{
+ return true;
+}
+
+bool JavaWizardPage::validatePage()
+{
+ auto settings = MMC->settings();
+ auto result = m_java_widget->validate();
+ switch(result)
+ {
+ default:
+ case JavaSettingsWidget::ValidationStatus::Bad:
+ {
+ return false;
+ }
+ case JavaSettingsWidget::ValidationStatus::AllOK:
+ {
+ settings->set("JavaPath", m_java_widget->javaPath());
+ }
+ case JavaSettingsWidget::ValidationStatus::JavaBad:
+ {
+ // Memory
+ auto s = MMC->settings();
+ s->set("MinMemAlloc", m_java_widget->minHeapSize());
+ s->set("MaxMemAlloc", m_java_widget->maxHeapSize());
+ if (m_java_widget->permGenEnabled())
+ {
+ s->set("PermGen", m_java_widget->permGenSize());
+ }
+ else
+ {
+ s->reset("PermGen");
+ }
+ return true;
+ }
+ }
+}
+
+void JavaWizardPage::retranslate()
+{
+ setTitle(tr("Java"));
+ setSubTitle(tr("You do not have a working Java set up yet or it went missing.\n"
+ "Please select one of the following or browse for a java executable."));
+ m_java_widget->retranslate();
+}
diff --git a/launcher/setupwizard/JavaWizardPage.h b/launcher/setupwizard/JavaWizardPage.h
new file mode 100644
index 00000000..0d749039
--- /dev/null
+++ b/launcher/setupwizard/JavaWizardPage.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "BaseWizardPage.h"
+
+class JavaSettingsWidget;
+
+class JavaWizardPage : public BaseWizardPage
+{
+ Q_OBJECT
+public:
+ explicit JavaWizardPage(QWidget *parent = Q_NULLPTR);
+
+ virtual ~JavaWizardPage()
+ {
+ };
+
+ bool wantsRefreshButton() override;
+ void refresh() override;
+ void initializePage() override;
+ bool validatePage() override;
+
+protected: /* methods */
+ void setupUi();
+ void retranslate() override;
+
+private: /* data */
+ JavaSettingsWidget *m_java_widget = nullptr;
+};
+
diff --git a/launcher/setupwizard/LanguageWizardPage.cpp b/launcher/setupwizard/LanguageWizardPage.cpp
new file mode 100644
index 00000000..ca93c6f5
--- /dev/null
+++ b/launcher/setupwizard/LanguageWizardPage.cpp
@@ -0,0 +1,48 @@
+#include "LanguageWizardPage.h"
+#include <MultiMC.h>
+#include <translations/TranslationsModel.h>
+
+#include "widgets/LanguageSelectionWidget.h"
+#include <QVBoxLayout>
+
+LanguageWizardPage::LanguageWizardPage(QWidget *parent)
+ : BaseWizardPage(parent)
+{
+ setObjectName(QStringLiteral("languagePage"));
+ auto layout = new QVBoxLayout(this);
+ mainWidget = new LanguageSelectionWidget(this);
+ layout->setContentsMargins(0,0,0,0);
+ layout->addWidget(mainWidget);
+
+ retranslate();
+}
+
+LanguageWizardPage::~LanguageWizardPage()
+{
+}
+
+bool LanguageWizardPage::wantsRefreshButton()
+{
+ return true;
+}
+
+void LanguageWizardPage::refresh()
+{
+ auto translations = MMC->translations();
+ translations->downloadIndex();
+}
+
+bool LanguageWizardPage::validatePage()
+{
+ auto settings = MMC->settings();
+ QString key = mainWidget->getSelectedLanguageKey();
+ settings->set("Language", key);
+ return true;
+}
+
+void LanguageWizardPage::retranslate()
+{
+ setTitle(tr("Language"));
+ setSubTitle(tr("Select the language to use in MultiMC"));
+ mainWidget->retranslate();
+}
diff --git a/launcher/setupwizard/LanguageWizardPage.h b/launcher/setupwizard/LanguageWizardPage.h
new file mode 100644
index 00000000..45a0e5c0
--- /dev/null
+++ b/launcher/setupwizard/LanguageWizardPage.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "BaseWizardPage.h"
+
+class LanguageSelectionWidget;
+
+class LanguageWizardPage : public BaseWizardPage
+{
+ Q_OBJECT
+public:
+ explicit LanguageWizardPage(QWidget *parent = Q_NULLPTR);
+
+ virtual ~LanguageWizardPage();
+
+ bool wantsRefreshButton() override;
+
+ void refresh() override;
+
+ bool validatePage() override;
+
+protected:
+ void retranslate() override;
+
+private:
+ LanguageSelectionWidget *mainWidget = nullptr;
+};
diff --git a/launcher/setupwizard/SetupWizard.cpp b/launcher/setupwizard/SetupWizard.cpp
new file mode 100644
index 00000000..60a78b8d
--- /dev/null
+++ b/launcher/setupwizard/SetupWizard.cpp
@@ -0,0 +1,88 @@
+#include "SetupWizard.h"
+
+#include "LanguageWizardPage.h"
+#include "JavaWizardPage.h"
+#include "AnalyticsWizardPage.h"
+
+#include "translations/TranslationsModel.h"
+#include <MultiMC.h>
+#include <FileSystem.h>
+#include <ganalytics.h>
+
+#include <QAbstractButton>
+
+SetupWizard::SetupWizard(QWidget *parent) : QWizard(parent)
+{
+ setObjectName(QStringLiteral("SetupWizard"));
+ resize(615, 659);
+ // make it ugly everywhere to avoid variability in theming
+ setWizardStyle(QWizard::ClassicStyle);
+ setOptions(QWizard::NoCancelButton | QWizard::IndependentPages | QWizard::HaveCustomButton1);
+
+ retranslate();
+
+ connect(this, &QWizard::currentIdChanged, this, &SetupWizard::pageChanged);
+}
+
+void SetupWizard::retranslate()
+{
+ setButtonText(QWizard::NextButton, tr("&Next >"));
+ setButtonText(QWizard::BackButton, tr("< &Back"));
+ setButtonText(QWizard::FinishButton, tr("&Finish"));
+ setButtonText(QWizard::CustomButton1, tr("&Refresh"));
+ setWindowTitle(tr("MultiMC Quick Setup"));
+}
+
+BaseWizardPage * SetupWizard::getBasePage(int id)
+{
+ if(id == -1)
+ return nullptr;
+ auto pagePtr = page(id);
+ if(!pagePtr)
+ return nullptr;
+ return dynamic_cast<BaseWizardPage *>(pagePtr);
+}
+
+BaseWizardPage * SetupWizard::getCurrentBasePage()
+{
+ return getBasePage(currentId());
+}
+
+void SetupWizard::pageChanged(int id)
+{
+ auto basePagePtr = getBasePage(id);
+ if(!basePagePtr)
+ {
+ return;
+ }
+ if(basePagePtr->wantsRefreshButton())
+ {
+ setButtonLayout({QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton});
+ auto customButton = button(QWizard::CustomButton1);
+ connect(customButton, &QAbstractButton::pressed, [&](){
+ auto basePagePtr = getCurrentBasePage();
+ if(basePagePtr)
+ {
+ basePagePtr->refresh();
+ }
+ });
+ }
+ else
+ {
+ setButtonLayout({QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton});
+ }
+}
+
+
+void SetupWizard::changeEvent(QEvent *event)
+{
+ if (event->type() == QEvent::LanguageChange)
+ {
+ retranslate();
+ }
+ QWizard::changeEvent(event);
+}
+
+SetupWizard::~SetupWizard()
+{
+}
diff --git a/launcher/setupwizard/SetupWizard.h b/launcher/setupwizard/SetupWizard.h
new file mode 100644
index 00000000..9b8adb4d
--- /dev/null
+++ b/launcher/setupwizard/SetupWizard.h
@@ -0,0 +1,45 @@
+/* Copyright 2017-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWizard>
+
+namespace Ui
+{
+class SetupWizard;
+}
+
+class BaseWizardPage;
+
+class SetupWizard : public QWizard
+{
+ Q_OBJECT
+
+public: /* con/destructors */
+ explicit SetupWizard(QWidget *parent = 0);
+ virtual ~SetupWizard();
+
+ void changeEvent(QEvent * event) override;
+ BaseWizardPage *getBasePage(int id);
+ BaseWizardPage *getCurrentBasePage();
+
+private slots:
+ void pageChanged(int id);
+
+private: /* methods */
+ void retranslate();
+};
+
diff --git a/launcher/status/StatusChecker.cpp b/launcher/status/StatusChecker.cpp
new file mode 100644
index 00000000..38fc2163
--- /dev/null
+++ b/launcher/status/StatusChecker.cpp
@@ -0,0 +1,148 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "StatusChecker.h"
+
+#include <QByteArray>
+
+#include <QDebug>
+
+#include <BuildConfig.h>
+
+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(Net::Download::makeByteArray(BuildConfig.MOJANG_STATUS_URL, &dataSink));
+ 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();
+ m_statusNetJob.reset();
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(dataSink, &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/launcher/status/StatusChecker.h b/launcher/status/StatusChecker.h
new file mode 100644
index 00000000..06b3f576
--- /dev/null
+++ b/launcher/status/StatusChecker.h
@@ -0,0 +1,58 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QList>
+
+#include <net/NetJob.h>
+
+class StatusChecker : public QObject
+{
+ Q_OBJECT
+public: /* con/des */
+ StatusChecker();
+
+public: /* methods */
+ QString getLastLoadErrorMsg() const;
+ bool isLoadingStatus() const;
+ QMap<QString, QString> getStatusEntries() const;
+
+signals:
+ void statusLoading(bool loading);
+ void statusChanged(QMap<QString, QString> newStatus);
+
+public slots:
+ void reloadStatus();
+
+protected: /* methods */
+ virtual void timerEvent(QTimerEvent *);
+
+protected slots:
+ void statusDownloadFinished();
+ void statusDownloadFailed(QString reason);
+ void succeed();
+ void fail(const QString& errorMsg);
+
+protected: /* data */
+ QMap<QString, QString> m_prevEntries;
+ QMap<QString, QString> m_statusEntries;
+ NetJobPtr m_statusNetJob;
+ QString m_lastLoadError;
+ QByteArray dataSink;
+};
+
diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp
new file mode 100644
index 00000000..d0777132
--- /dev/null
+++ b/launcher/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/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h
new file mode 100644
index 00000000..6898c8a6
--- /dev/null
+++ b/launcher/tasks/SequentialTask.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "Task.h"
+
+#include <QQueue>
+#include <memory>
+
+class SequentialTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit SequentialTask(QObject *parent = 0);
+ virtual ~SequentialTask() {};
+
+ 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/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp
new file mode 100644
index 00000000..d0ac7569
--- /dev/null
+++ b/launcher/tasks/Task.cpp
@@ -0,0 +1,168 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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()
+{
+ switch(m_state)
+ {
+ case State::Inactive:
+ {
+ qDebug() << "Task" << describe() << "starting for the first time";
+ break;
+ }
+ case State::AbortedByUser:
+ {
+ qDebug() << "Task" << describe() << "restarting for after being aborted by user";
+ break;
+ }
+ case State::Failed:
+ {
+ qDebug() << "Task" << describe() << "restarting for after failing at first";
+ break;
+ }
+ case State::Succeeded:
+ {
+ qDebug() << "Task" << describe() << "restarting for after succeeding at first";
+ break;
+ }
+ case State::Running:
+ {
+ qWarning() << "MultiMC tried to start task" << describe() << "while it was already running!";
+ return;
+ }
+ }
+ // NOTE: only fall thorugh to here in end states
+ m_state = State::Running;
+ emit started();
+ executeTask();
+}
+
+void Task::emitFailed(QString reason)
+{
+ // Don't fail twice.
+ if (!isRunning())
+ {
+ qCritical() << "Task" << describe() << "failed while not running!!!!: " << reason;
+ return;
+ }
+ m_state = State::Failed;
+ m_failReason = reason;
+ qCritical() << "Task" << describe() << "failed: " << reason;
+ emit failed(reason);
+ emit finished();
+}
+
+void Task::emitAborted()
+{
+ // Don't abort twice.
+ if (!isRunning())
+ {
+ qCritical() << "Task" << describe() << "aborted while not running!!!!";
+ return;
+ }
+ m_state = State::AbortedByUser;
+ m_failReason = "Aborted.";
+ qDebug() << "Task" << describe() << "aborted.";
+ emit failed(m_failReason);
+ emit finished();
+}
+
+void Task::emitSucceeded()
+{
+ // Don't succeed twice.
+ if (!isRunning())
+ {
+ qCritical() << "Task" << describe() << "succeeded while not running!!!!";
+ return;
+ }
+ m_state = State::Succeeded;
+ qDebug() << "Task" << describe() << "succeeded";
+ emit succeeded();
+ emit finished();
+}
+
+QString Task::describe()
+{
+ QString outStr;
+ QTextStream out(&outStr);
+ out << metaObject()->className() << QChar('(');
+ auto name = objectName();
+ if(name.isEmpty())
+ {
+ out << QString("0x%1").arg((quintptr)this, 0, 16);
+ }
+ else
+ {
+ out << name;
+ }
+ out << QChar(')');
+ out.flush();
+ return outStr;
+}
+
+bool Task::isRunning() const
+{
+ return m_state == State::Running;
+}
+
+bool Task::isFinished() const
+{
+ return m_state != State::Running && m_state != State::Inactive;
+}
+
+bool Task::wasSuccessful() const
+{
+ return m_state == State::Succeeded;
+}
+
+QString Task::failReason() const
+{
+ return m_failReason;
+}
+
+void Task::logWarning(const QString& line)
+{
+ qWarning() << line;
+ m_Warnings.append(line);
+}
+
+QStringList Task::warnings() const
+{
+ return m_Warnings;
+}
diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h
new file mode 100644
index 00000000..2367f1ec
--- /dev/null
+++ b/launcher/tasks/Task.h
@@ -0,0 +1,106 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+class Task : public QObject
+{
+ Q_OBJECT
+public:
+ enum class State
+ {
+ Inactive,
+ Running,
+ Succeeded,
+ Failed,
+ AbortedByUser
+ };
+
+public:
+ explicit Task(QObject *parent = 0);
+ virtual ~Task() {};
+
+ bool isRunning() const;
+ bool isFinished() const;
+ bool wasSuccessful() 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.
+ */
+ QString failReason() const;
+
+ virtual QStringList warnings() const;
+
+ virtual bool canAbort() const { return false; }
+
+ QString getStatus()
+ {
+ return m_status;
+ }
+
+ qint64 getProgress()
+ {
+ return m_progress;
+ }
+
+ qint64 getTotalProgress()
+ {
+ return m_progressTotal;
+ }
+
+protected:
+ void logWarning(const QString & line);
+
+private:
+ QString describe();
+
+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 emitAborted();
+ virtual void emitFailed(QString reason);
+
+public slots:
+ void setStatus(const QString &status);
+ void setProgress(qint64 current, qint64 total);
+
+private:
+ State m_state = State::Inactive;
+ QStringList m_Warnings;
+ QString m_failReason = "";
+ QString m_status;
+ int m_progress = 0;
+ int m_progressTotal = 100;
+};
+
diff --git a/launcher/testdata/FileSystem-test_createShortcut-unix b/launcher/testdata/FileSystem-test_createShortcut-unix
new file mode 100755
index 00000000..1ce3a2bd
--- /dev/null
+++ b/launcher/testdata/FileSystem-test_createShortcut-unix
@@ -0,0 +1,6 @@
+[Desktop Entry]
+Type=Application
+TryExec=asdfDest
+Exec=asdfDest 'arg1' 'arg2'
+Name=asdf
+Icon=
diff --git a/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt b/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt
new file mode 100644
index 00000000..8d1c8b69
--- /dev/null
+++ b/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt
@@ -0,0 +1 @@
+
diff --git a/launcher/testdata/test_folder/pack.mcmeta b/launcher/testdata/test_folder/pack.mcmeta
new file mode 100644
index 00000000..67ee0434
--- /dev/null
+++ b/launcher/testdata/test_folder/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "pack_format": 1,
+ "description": "Some resource pack maybe"
+ }
+}
diff --git a/launcher/testdata/test_folder/pack.nfo b/launcher/testdata/test_folder/pack.nfo
new file mode 100644
index 00000000..8d1c8b69
--- /dev/null
+++ b/launcher/testdata/test_folder/pack.nfo
@@ -0,0 +1 @@
+
diff --git a/launcher/themes/BrightTheme.cpp b/launcher/themes/BrightTheme.cpp
new file mode 100644
index 00000000..b9188bdd
--- /dev/null
+++ b/launcher/themes/BrightTheme.cpp
@@ -0,0 +1,56 @@
+#include "BrightTheme.h"
+
+QString BrightTheme::id()
+{
+ return "bright";
+}
+
+QString BrightTheme::name()
+{
+ return QObject::tr("Bright");
+}
+
+bool BrightTheme::hasColorScheme()
+{
+ return true;
+}
+
+QPalette BrightTheme::colorScheme()
+{
+ QPalette brightPalette;
+ brightPalette.setColor(QPalette::Window, QColor(239,240,241));
+ brightPalette.setColor(QPalette::WindowText, QColor(49,54,59));
+ brightPalette.setColor(QPalette::Base, QColor(252,252,252));
+ brightPalette.setColor(QPalette::AlternateBase, QColor(239,240,241));
+ brightPalette.setColor(QPalette::ToolTipBase, QColor(49,54,59));
+ brightPalette.setColor(QPalette::ToolTipText, QColor(239,240,241));
+ brightPalette.setColor(QPalette::Text, QColor(49,54,59));
+ brightPalette.setColor(QPalette::Button, QColor(239,240,241));
+ brightPalette.setColor(QPalette::ButtonText, QColor(49,54,59));
+ brightPalette.setColor(QPalette::BrightText, Qt::red);
+ brightPalette.setColor(QPalette::Link, QColor(41, 128, 185));
+ brightPalette.setColor(QPalette::Highlight, QColor(61, 174, 233));
+ brightPalette.setColor(QPalette::HighlightedText, QColor(239,240,241));
+ return fadeInactive(brightPalette, fadeAmount(), fadeColor());
+}
+
+double BrightTheme::fadeAmount()
+{
+ return 0.5;
+}
+
+QColor BrightTheme::fadeColor()
+{
+ return QColor(239,240,241);
+}
+
+bool BrightTheme::hasStyleSheet()
+{
+ return false;
+}
+
+QString BrightTheme::appStyleSheet()
+{
+ return QString();
+}
+
diff --git a/launcher/themes/BrightTheme.h b/launcher/themes/BrightTheme.h
new file mode 100644
index 00000000..c61f52d5
--- /dev/null
+++ b/launcher/themes/BrightTheme.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "FusionTheme.h"
+
+class BrightTheme: public FusionTheme
+{
+public:
+ virtual ~BrightTheme() {}
+
+ QString id() override;
+ QString name() override;
+ bool hasStyleSheet() override;
+ QString appStyleSheet() override;
+ bool hasColorScheme() override;
+ QPalette colorScheme() override;
+ double fadeAmount() override;
+ QColor fadeColor() override;
+};
+
diff --git a/launcher/themes/CustomTheme.cpp b/launcher/themes/CustomTheme.cpp
new file mode 100644
index 00000000..3e3e27de
--- /dev/null
+++ b/launcher/themes/CustomTheme.cpp
@@ -0,0 +1,244 @@
+#include "CustomTheme.h"
+#include <QDir>
+#include <Json.h>
+#include <FileSystem.h>
+
+const char * themeFile = "theme.json";
+const char * styleFile = "themeStyle.css";
+
+static bool readThemeJson(const QString &path, QPalette &palette, double &fadeAmount, QColor &fadeColor, QString &name, QString &widgets)
+{
+ QFileInfo pathInfo(path);
+ if(pathInfo.exists() && pathInfo.isFile())
+ {
+ try
+ {
+ auto doc = Json::requireDocument(path, "Theme JSON file");
+ const QJsonObject root = doc.object();
+ name = Json::requireString(root, "name", "Theme name");
+ widgets = Json::requireString(root, "widgets", "Qt widget theme");
+ auto colorsRoot = Json::requireObject(root, "colors", "colors object");
+ auto readColor = [&](QString colorName) -> QColor
+ {
+ auto colorValue = Json::ensureString(colorsRoot, colorName, QString());
+ if(!colorValue.isEmpty())
+ {
+ QColor color(colorValue);
+ if(!color.isValid())
+ {
+ qWarning() << "Color value" << colorValue << "for" << colorName << "was not recognized.";
+ return QColor();
+ }
+ return color;
+ }
+ return QColor();
+ };
+ auto readAndSetColor = [&](QPalette::ColorRole role, QString colorName)
+ {
+ auto color = readColor(colorName);
+ if(color.isValid())
+ {
+ palette.setColor(role, color);
+ }
+ else
+ {
+ qDebug() << "Color value for" << colorName << "was not present.";
+ }
+ };
+
+ // palette
+ readAndSetColor(QPalette::Window, "Window");
+ readAndSetColor(QPalette::WindowText, "WindowText");
+ readAndSetColor(QPalette::Base, "Base");
+ readAndSetColor(QPalette::AlternateBase, "AlternateBase");
+ readAndSetColor(QPalette::ToolTipBase, "ToolTipBase");
+ readAndSetColor(QPalette::ToolTipText, "ToolTipText");
+ readAndSetColor(QPalette::Text, "Text");
+ readAndSetColor(QPalette::Button, "Button");
+ readAndSetColor(QPalette::ButtonText, "ButtonText");
+ readAndSetColor(QPalette::BrightText, "BrightText");
+ readAndSetColor(QPalette::Link, "Link");
+ readAndSetColor(QPalette::Highlight, "Highlight");
+ readAndSetColor(QPalette::HighlightedText, "HighlightedText");
+
+ //fade
+ fadeColor = readColor("fadeColor");
+ fadeAmount = Json::ensureDouble(colorsRoot, "fadeAmount", 0.5, "fade amount");
+
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Couldn't load theme json: " << e.cause();
+ return false;
+ }
+ }
+ else
+ {
+ qDebug() << "No theme json present.";
+ return false;
+ }
+ return true;
+}
+
+static bool writeThemeJson(const QString &path, const QPalette &palette, double fadeAmount, QColor fadeColor, QString name, QString widgets)
+{
+ QJsonObject rootObj;
+ rootObj.insert("name", name);
+ rootObj.insert("widgets", widgets);
+
+ QJsonObject colorsObj;
+ auto insertColor = [&](QPalette::ColorRole role, QString colorName)
+ {
+ colorsObj.insert(colorName, palette.color(role).name());
+ };
+
+ // palette
+ insertColor(QPalette::Window, "Window");
+ insertColor(QPalette::WindowText, "WindowText");
+ insertColor(QPalette::Base, "Base");
+ insertColor(QPalette::AlternateBase, "AlternateBase");
+ insertColor(QPalette::ToolTipBase, "ToolTipBase");
+ insertColor(QPalette::ToolTipText, "ToolTipText");
+ insertColor(QPalette::Text, "Text");
+ insertColor(QPalette::Button, "Button");
+ insertColor(QPalette::ButtonText, "ButtonText");
+ insertColor(QPalette::BrightText, "BrightText");
+ insertColor(QPalette::Link, "Link");
+ insertColor(QPalette::Highlight, "Highlight");
+ insertColor(QPalette::HighlightedText, "HighlightedText");
+
+ // fade
+ colorsObj.insert("fadeColor", fadeColor.name());
+ colorsObj.insert("fadeAmount", fadeAmount);
+
+ rootObj.insert("colors", colorsObj);
+ try
+ {
+ Json::write(rootObj, path);
+ return true;
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Failed to write theme json to" << path;
+ return false;
+ }
+}
+
+CustomTheme::CustomTheme(ITheme* baseTheme, QString folder)
+{
+ m_id = folder;
+ QString path = FS::PathCombine("themes", m_id);
+ QString pathResources = FS::PathCombine("themes", m_id, "resources");
+
+ qDebug() << "Loading theme" << m_id;
+
+ if(!FS::ensureFolderPathExists(path) || !FS::ensureFolderPathExists(pathResources))
+ {
+ qWarning() << "couldn't create folder for theme!";
+ m_palette = baseTheme->colorScheme();
+ m_styleSheet = baseTheme->appStyleSheet();
+ return;
+ }
+
+ auto themeFilePath = FS::PathCombine(path, themeFile);
+
+ m_palette = baseTheme->colorScheme();
+ if (!readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets))
+ {
+ m_name = "Custom";
+ m_palette = baseTheme->colorScheme();
+ m_fadeColor = baseTheme->fadeColor();
+ m_fadeAmount = baseTheme->fadeAmount();
+ m_widgets = baseTheme->qtTheme();
+
+ QFileInfo info(themeFilePath);
+ if(!info.exists())
+ {
+ writeThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, "Custom", m_widgets);
+ }
+ }
+ else
+ {
+ m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor);
+ }
+
+ auto cssFilePath = FS::PathCombine(path, styleFile);
+ QFileInfo info (cssFilePath);
+ if(info.isFile())
+ {
+ try
+ {
+ // TODO: validate css?
+ m_styleSheet = QString::fromUtf8(FS::read(cssFilePath));
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Couldn't load css:" << e.cause() << "from" << cssFilePath;
+ m_styleSheet = baseTheme->appStyleSheet();
+ }
+ }
+ else
+ {
+ qDebug() << "No theme css present.";
+ m_styleSheet = baseTheme->appStyleSheet();
+ try
+ {
+ FS::write(cssFilePath, m_styleSheet.toUtf8());
+ }
+ catch (const Exception &e)
+ {
+ qWarning() << "Couldn't write css:" << e.cause() << "to" << cssFilePath;
+ }
+ }
+}
+
+QStringList CustomTheme::searchPaths()
+{
+ return { FS::PathCombine("themes", m_id, "resources") };
+}
+
+
+QString CustomTheme::id()
+{
+ return m_id;
+}
+
+QString CustomTheme::name()
+{
+ return m_name;
+}
+
+bool CustomTheme::hasColorScheme()
+{
+ return true;
+}
+
+QPalette CustomTheme::colorScheme()
+{
+ return m_palette;
+}
+
+bool CustomTheme::hasStyleSheet()
+{
+ return true;
+}
+
+QString CustomTheme::appStyleSheet()
+{
+ return m_styleSheet;
+}
+
+double CustomTheme::fadeAmount()
+{
+ return m_fadeAmount;
+}
+
+QColor CustomTheme::fadeColor()
+{
+ return m_fadeColor;
+}
+
+QString CustomTheme::qtTheme()
+{
+ return m_widgets;
+}
diff --git a/launcher/themes/CustomTheme.h b/launcher/themes/CustomTheme.h
new file mode 100644
index 00000000..d216895d
--- /dev/null
+++ b/launcher/themes/CustomTheme.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "ITheme.h"
+
+class CustomTheme: public ITheme
+{
+public:
+ CustomTheme(ITheme * baseTheme, QString folder);
+ virtual ~CustomTheme() {}
+
+ QString id() override;
+ QString name() override;
+ bool hasStyleSheet() override;
+ QString appStyleSheet() override;
+ bool hasColorScheme() override;
+ QPalette colorScheme() override;
+ double fadeAmount() override;
+ QColor fadeColor() override;
+ QString qtTheme() override;
+ QStringList searchPaths() override;
+
+private: /* data */
+ QPalette m_palette;
+ QColor m_fadeColor;
+ double m_fadeAmount;
+ QString m_styleSheet;
+ QString m_name;
+ QString m_id;
+ QString m_widgets;
+};
+
diff --git a/launcher/themes/DarkTheme.cpp b/launcher/themes/DarkTheme.cpp
new file mode 100644
index 00000000..31ecd559
--- /dev/null
+++ b/launcher/themes/DarkTheme.cpp
@@ -0,0 +1,55 @@
+#include "DarkTheme.h"
+
+QString DarkTheme::id()
+{
+ return "dark";
+}
+
+QString DarkTheme::name()
+{
+ return QObject::tr("Dark");
+}
+
+bool DarkTheme::hasColorScheme()
+{
+ return true;
+}
+
+QPalette DarkTheme::colorScheme()
+{
+ QPalette darkPalette;
+ darkPalette.setColor(QPalette::Window, QColor(49,54,59));
+ darkPalette.setColor(QPalette::WindowText, Qt::white);
+ darkPalette.setColor(QPalette::Base, QColor(35,38,41));
+ darkPalette.setColor(QPalette::AlternateBase, QColor(49,54,59));
+ darkPalette.setColor(QPalette::ToolTipBase, Qt::white);
+ darkPalette.setColor(QPalette::ToolTipText, Qt::white);
+ darkPalette.setColor(QPalette::Text, Qt::white);
+ darkPalette.setColor(QPalette::Button, QColor(49,54,59));
+ darkPalette.setColor(QPalette::ButtonText, Qt::white);
+ darkPalette.setColor(QPalette::BrightText, Qt::red);
+ darkPalette.setColor(QPalette::Link, QColor(42, 130, 218));
+ darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
+ darkPalette.setColor(QPalette::HighlightedText, Qt::black);
+ return fadeInactive(darkPalette, fadeAmount(), fadeColor());
+}
+
+double DarkTheme::fadeAmount()
+{
+ return 0.5;
+}
+
+QColor DarkTheme::fadeColor()
+{
+ return QColor(49,54,59);
+}
+
+bool DarkTheme::hasStyleSheet()
+{
+ return true;
+}
+
+QString DarkTheme::appStyleSheet()
+{
+ return "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }";
+}
diff --git a/launcher/themes/DarkTheme.h b/launcher/themes/DarkTheme.h
new file mode 100644
index 00000000..9bd2f343
--- /dev/null
+++ b/launcher/themes/DarkTheme.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "FusionTheme.h"
+
+class DarkTheme: public FusionTheme
+{
+public:
+ virtual ~DarkTheme() {}
+
+ QString id() override;
+ QString name() override;
+ bool hasStyleSheet() override;
+ QString appStyleSheet() override;
+ bool hasColorScheme() override;
+ QPalette colorScheme() override;
+ double fadeAmount() override;
+ QColor fadeColor() override;
+};
diff --git a/launcher/themes/FusionTheme.cpp b/launcher/themes/FusionTheme.cpp
new file mode 100644
index 00000000..cf3286ba
--- /dev/null
+++ b/launcher/themes/FusionTheme.cpp
@@ -0,0 +1,6 @@
+#include "FusionTheme.h"
+
+QString FusionTheme::qtTheme()
+{
+ return "Fusion";
+}
diff --git a/launcher/themes/FusionTheme.h b/launcher/themes/FusionTheme.h
new file mode 100644
index 00000000..ee34245a
--- /dev/null
+++ b/launcher/themes/FusionTheme.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "ITheme.h"
+
+class FusionTheme: public ITheme
+{
+public:
+ virtual ~FusionTheme() {}
+
+ QString qtTheme() override;
+};
diff --git a/launcher/themes/ITheme.cpp b/launcher/themes/ITheme.cpp
new file mode 100644
index 00000000..bfec87e7
--- /dev/null
+++ b/launcher/themes/ITheme.cpp
@@ -0,0 +1,47 @@
+#include "ITheme.h"
+#include "rainbow.h"
+#include <QStyleFactory>
+#include <QDir>
+#include "MultiMC.h"
+
+void ITheme::apply(bool)
+{
+ QApplication::setStyle(QStyleFactory::create(qtTheme()));
+ if(hasColorScheme())
+ {
+ QApplication::setPalette(colorScheme());
+ }
+ if(hasStyleSheet())
+ {
+ MMC->setStyleSheet(appStyleSheet());
+ }
+ else
+ {
+ MMC->setStyleSheet(QString());
+ }
+ QDir::setSearchPaths("theme", searchPaths());
+}
+
+QPalette ITheme::fadeInactive(QPalette in, qreal bias, QColor color)
+{
+ auto blend = [&in, bias, color](QPalette::ColorRole role)
+ {
+ QColor from = in.color(QPalette::Active, role);
+ QColor blended = Rainbow::mix(from, color, bias);
+ in.setColor(QPalette::Disabled, role, blended);
+ };
+ blend(QPalette::Window);
+ blend(QPalette::WindowText);
+ blend(QPalette::Base);
+ blend(QPalette::AlternateBase);
+ blend(QPalette::ToolTipBase);
+ blend(QPalette::ToolTipText);
+ blend(QPalette::Text);
+ blend(QPalette::Button);
+ blend(QPalette::ButtonText);
+ blend(QPalette::BrightText);
+ blend(QPalette::Link);
+ blend(QPalette::Highlight);
+ blend(QPalette::HighlightedText);
+ return in;
+}
diff --git a/launcher/themes/ITheme.h b/launcher/themes/ITheme.h
new file mode 100644
index 00000000..c2347cf6
--- /dev/null
+++ b/launcher/themes/ITheme.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <QString>
+#include <QPalette>
+
+class QStyle;
+
+class ITheme
+{
+public:
+ virtual ~ITheme() {}
+ virtual void apply(bool initial);
+ virtual QString id() = 0;
+ virtual QString name() = 0;
+ virtual bool hasStyleSheet() = 0;
+ virtual QString appStyleSheet() = 0;
+ virtual QString qtTheme() = 0;
+ virtual bool hasColorScheme() = 0;
+ virtual QPalette colorScheme() = 0;
+ virtual QColor fadeColor() = 0;
+ virtual double fadeAmount() = 0;
+ virtual QStringList searchPaths()
+ {
+ return {};
+ }
+
+ static QPalette fadeInactive(QPalette in, qreal bias, QColor color);
+};
diff --git a/launcher/themes/SystemTheme.cpp b/launcher/themes/SystemTheme.cpp
new file mode 100644
index 00000000..49b1afaa
--- /dev/null
+++ b/launcher/themes/SystemTheme.cpp
@@ -0,0 +1,83 @@
+#include "SystemTheme.h"
+#include <QApplication>
+#include <QStyle>
+#include <QStyleFactory>
+#include <QDebug>
+
+SystemTheme::SystemTheme()
+{
+ qDebug() << "Determining System Theme...";
+ const auto & style = QApplication::style();
+ systemPalette = style->standardPalette();
+ QString lowerThemeName = style->objectName();
+ qDebug() << "System theme seems to be:" << lowerThemeName;
+ QStringList styles = QStyleFactory::keys();
+ for(auto &st: styles)
+ {
+ qDebug() << "Considering theme from theme factory:" << st.toLower();
+ if(st.toLower() == lowerThemeName)
+ {
+ systemTheme = st;
+ qDebug() << "System theme has been determined to be:" << systemTheme;
+ return;
+ }
+ }
+ // fall back to fusion if we can't find the current theme.
+ systemTheme = "Fusion";
+ qDebug() << "System theme not found, defaulted to Fusion";
+}
+
+void SystemTheme::apply(bool initial)
+{
+ // if we are applying the system theme as the first theme, just don't touch anything. it's for the better...
+ if(initial)
+ {
+ return;
+ }
+ ITheme::apply(initial);
+}
+
+QString SystemTheme::id()
+{
+ return "system";
+}
+
+QString SystemTheme::name()
+{
+ return QObject::tr("System");
+}
+
+QString SystemTheme::qtTheme()
+{
+ return systemTheme;
+}
+
+QPalette SystemTheme::colorScheme()
+{
+ return systemPalette;
+}
+
+QString SystemTheme::appStyleSheet()
+{
+ return QString();
+}
+
+double SystemTheme::fadeAmount()
+{
+ return 0.5;
+}
+
+QColor SystemTheme::fadeColor()
+{
+ return QColor(128,128,128);
+}
+
+bool SystemTheme::hasStyleSheet()
+{
+ return false;
+}
+
+bool SystemTheme::hasColorScheme()
+{
+ return true;
+}
diff --git a/launcher/themes/SystemTheme.h b/launcher/themes/SystemTheme.h
new file mode 100644
index 00000000..fe450600
--- /dev/null
+++ b/launcher/themes/SystemTheme.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "ITheme.h"
+
+class SystemTheme: public ITheme
+{
+public:
+ SystemTheme();
+ virtual ~SystemTheme() {}
+ void apply(bool initial) override;
+
+ QString id() override;
+ QString name() override;
+ QString qtTheme() override;
+ bool hasStyleSheet() override;
+ QString appStyleSheet() override;
+ bool hasColorScheme() override;
+ QPalette colorScheme() override;
+ double fadeAmount() override;
+ QColor fadeColor() override;
+private:
+ QPalette systemPalette;
+ QString systemTheme;
+};
diff --git a/launcher/tools/BaseExternalTool.cpp b/launcher/tools/BaseExternalTool.cpp
new file mode 100644
index 00000000..38d81788
--- /dev/null
+++ b/launcher/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/launcher/tools/BaseExternalTool.h b/launcher/tools/BaseExternalTool.h
new file mode 100644
index 00000000..1ebed6ae
--- /dev/null
+++ b/launcher/tools/BaseExternalTool.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <QObject>
+#include <BaseInstance.h>
+
+class BaseInstance;
+class SettingsObject;
+class QProcess;
+
+class BaseExternalTool : public QObject
+{
+ Q_OBJECT
+public:
+ explicit BaseExternalTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0);
+ virtual ~BaseExternalTool();
+
+protected:
+ InstancePtr m_instance;
+ SettingsObjectPtr globalSettings;
+};
+
+class 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 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 BaseDetachedToolFactory : public BaseExternalToolFactory
+{
+public:
+ virtual BaseDetachedTool *createDetachedTool(InstancePtr instance, QObject *parent = 0);
+};
diff --git a/launcher/tools/BaseProfiler.cpp b/launcher/tools/BaseProfiler.cpp
new file mode 100644
index 00000000..300d1a73
--- /dev/null
+++ b/launcher/tools/BaseProfiler.cpp
@@ -0,0 +1,36 @@
+#include "BaseProfiler.h"
+#include "QObjectPtr.h"
+
+#include <QProcess>
+
+BaseProfiler::BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject *parent)
+ : BaseExternalTool(settings, instance, parent)
+{
+}
+
+void BaseProfiler::beginProfiling(shared_qobject_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/launcher/tools/BaseProfiler.h b/launcher/tools/BaseProfiler.h
new file mode 100644
index 00000000..1c934aa3
--- /dev/null
+++ b/launcher/tools/BaseProfiler.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "BaseExternalTool.h"
+#include "QObjectPtr.h"
+
+class BaseInstance;
+class SettingsObject;
+class LaunchTask;
+class QProcess;
+
+class BaseProfiler : public BaseExternalTool
+{
+ Q_OBJECT
+public:
+ explicit BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0);
+
+public
+slots:
+ void beginProfiling(shared_qobject_ptr<LaunchTask> process);
+ void abortProfiling();
+
+protected:
+ QProcess *m_profilerProcess;
+
+ virtual void beginProfilingImpl(shared_qobject_ptr<LaunchTask> process) = 0;
+ virtual void abortProfilingImpl();
+
+signals:
+ void readyToLaunch(const QString &message);
+ void abortLaunch(const QString &message);
+};
+
+class BaseProfilerFactory : public BaseExternalToolFactory
+{
+public:
+ virtual BaseProfiler *createProfiler(InstancePtr instance, QObject *parent = 0);
+};
diff --git a/launcher/tools/JProfiler.cpp b/launcher/tools/JProfiler.cpp
new file mode 100644
index 00000000..1dc0d109
--- /dev/null
+++ b/launcher/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(shared_qobject_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(shared_qobject_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/launcher/tools/JProfiler.h b/launcher/tools/JProfiler.h
new file mode 100644
index 00000000..0e9a3a85
--- /dev/null
+++ b/launcher/tools/JProfiler.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "BaseProfiler.h"
+
+class 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/launcher/tools/JVisualVM.cpp b/launcher/tools/JVisualVM.cpp
new file mode 100644
index 00000000..b1acc3c0
--- /dev/null
+++ b/launcher/tools/JVisualVM.cpp
@@ -0,0 +1,104 @@
+#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(shared_qobject_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(shared_qobject_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;
+ }
+ QFileInfo finfo(path);
+ if (!finfo.isExecutable() || !finfo.fileName().contains("visualvm"))
+ {
+ *error = QObject::tr("Invalid path to JVisualVM");
+ return false;
+ }
+ return true;
+}
+
+#include "JVisualVM.moc"
diff --git a/launcher/tools/JVisualVM.h b/launcher/tools/JVisualVM.h
new file mode 100644
index 00000000..ebdea9f3
--- /dev/null
+++ b/launcher/tools/JVisualVM.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "BaseProfiler.h"
+
+class 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/launcher/tools/MCEditTool.cpp b/launcher/tools/MCEditTool.cpp
new file mode 100644
index 00000000..880327c7
--- /dev/null
+++ b/launcher/tools/MCEditTool.cpp
@@ -0,0 +1,77 @@
+#include "MCEditTool.h"
+
+#include <QDir>
+#include <QProcess>
+#include <QUrl>
+
+#include "settings/SettingsObject.h"
+#include "BaseInstance.h"
+#include "minecraft/MinecraftInstance.h"
+
+MCEditTool::MCEditTool(SettingsObjectPtr settings)
+{
+ settings->registerSetting("MCEditPath");
+ m_settings = settings;
+}
+
+void MCEditTool::setPath(QString& path)
+{
+ m_settings->set("MCEditPath", path);
+}
+
+QString MCEditTool::path() const
+{
+ return m_settings->get("MCEditPath").toString();
+}
+
+bool MCEditTool::check(const QString& toolPath, QString& error)
+{
+ if (toolPath.isEmpty())
+ {
+ error = QObject::tr("Path is empty");
+ return false;
+ }
+ const QDir dir(toolPath);
+ 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;
+}
+
+QString MCEditTool::getProgramPath()
+{
+#ifdef Q_OS_OSX
+ return path();
+#else
+ const QString mceditPath = path();
+ QDir mceditDir(mceditPath);
+#ifdef Q_OS_LINUX
+ if (mceditDir.exists("mcedit.sh"))
+ {
+ return mceditDir.absoluteFilePath("mcedit.sh");
+ }
+ else if (mceditDir.exists("mcedit.py"))
+ {
+ return mceditDir.absoluteFilePath("mcedit.py");
+ }
+ return QString();
+#elif defined(Q_OS_WIN32)
+ if (mceditDir.exists("mcedit.exe"))
+ {
+ return mceditDir.absoluteFilePath("mcedit.exe");
+ }
+ else if (mceditDir.exists("mcedit2.exe"))
+ {
+ return mceditDir.absoluteFilePath("mcedit2.exe");
+ }
+ return QString();
+#endif
+#endif
+}
diff --git a/launcher/tools/MCEditTool.h b/launcher/tools/MCEditTool.h
new file mode 100644
index 00000000..733dff86
--- /dev/null
+++ b/launcher/tools/MCEditTool.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <QString>
+#include "settings/SettingsObject.h"
+
+class MCEditTool
+{
+public:
+ MCEditTool(SettingsObjectPtr settings);
+ void setPath(QString & path);
+ QString path() const;
+ bool check(const QString &toolPath, QString &error);
+ QString getProgramPath();
+private:
+ SettingsObjectPtr m_settings;
+};
diff --git a/launcher/translations/POTranslator.cpp b/launcher/translations/POTranslator.cpp
new file mode 100644
index 00000000..1ffcb9a4
--- /dev/null
+++ b/launcher/translations/POTranslator.cpp
@@ -0,0 +1,373 @@
+#include "POTranslator.h"
+
+#include <QDebug>
+#include "FileSystem.h"
+
+struct POEntry
+{
+ QString text;
+ bool fuzzy;
+};
+
+struct POTranslatorPrivate
+{
+ QString filename;
+ QHash<QByteArray, POEntry> mapping;
+ QHash<QByteArray, POEntry> mapping_disambiguatrion;
+ bool loaded = false;
+
+ void reload();
+};
+
+class ParserArray : public QByteArray
+{
+public:
+ ParserArray(const QByteArray &in) : QByteArray(in)
+ {
+ }
+ bool chomp(const char * data, int length)
+ {
+ if(startsWith(data))
+ {
+ remove(0, length);
+ return true;
+ }
+ return false;
+ }
+ bool chompString(QByteArray & appendHere)
+ {
+ QByteArray msg;
+ bool escape = false;
+ if(size() < 2)
+ {
+ qDebug() << "String fragment is too short";
+ return false;
+ }
+ if(!startsWith('"'))
+ {
+ qDebug() << "String fragment does not start with \"";
+ return false;
+ }
+ if(!endsWith('"'))
+ {
+ qDebug() << "String fragment does not end with \", instead, there is" << at(size() - 1);
+ return false;
+ }
+ for(int i = 1; i < size() - 1; i++)
+ {
+ char c = operator[](i);
+ if(escape)
+ {
+ switch(c)
+ {
+ case 'r':
+ msg += '\r';
+ break;
+ case 'n':
+ msg += '\n';
+ break;
+ case 't':
+ msg += '\t';
+ break;
+ case 'v':
+ msg += '\v';
+ break;
+ case 'a':
+ msg += '\a';
+ break;
+ case 'b':
+ msg += '\b';
+ break;
+ case 'f':
+ msg += '\f';
+ break;
+ case '"':
+ msg += '"';
+ break;
+ case '\\':
+ msg.append('\\');
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ {
+ int octal_start = i;
+ while ((c = operator[](i)) >= '0' && c <= '7')
+ {
+ i++;
+ if (i == length() - 1)
+ {
+ qDebug() << "Something went bad while parsing an octal escape string...";
+ return false;
+ }
+ }
+ msg += mid(octal_start, i - octal_start).toUInt(0, 8);
+ break;
+ }
+ case 'x':
+ {
+ // chomp the 'x'
+ i++;
+ int hex_start = i;
+ while (isxdigit(operator[](i)))
+ {
+ i++;
+ if (i == length() - 1)
+ {
+ qDebug() << "Something went bad while parsing a hex escape string...";
+ return false;
+ }
+ }
+ msg += mid(hex_start, i - hex_start).toUInt(0, 16);
+ break;
+ }
+ default:
+ {
+ qDebug() << "Invalid escape sequence character:" << c;
+ return false;
+ }
+ }
+ escape = false;
+ }
+ else if(c == '\\')
+ {
+ escape = true;
+ }
+ else
+ {
+ msg += c;
+ }
+ }
+ if(escape)
+ {
+ qDebug() << "Unterminated escape sequence...";
+ return false;
+ }
+ appendHere += msg;
+ return true;
+ }
+};
+
+void POTranslatorPrivate::reload()
+{
+ QFile file(filename);
+ if(!file.open(QFile::OpenMode::enum_type::ReadOnly | QFile::OpenMode::enum_type::Text))
+ {
+ qDebug() << "Failed to open PO file:" << filename;
+ return;
+ }
+
+ QByteArray context;
+ QByteArray disambiguation;
+ QByteArray id;
+ QByteArray str;
+ bool fuzzy = false;
+ bool nextFuzzy = false;
+
+ enum class Mode
+ {
+ First,
+ MessageContext,
+ MessageId,
+ MessageString
+ } mode = Mode::First;
+
+ int lineNumber = 0;
+ QHash<QByteArray, POEntry> newMapping;
+ QHash<QByteArray, POEntry> newMapping_disambiguation;
+ auto endEntry = [&]() {
+ auto strStr = QString::fromUtf8(str);
+ // NOTE: PO header has empty id. We skip it.
+ if(!id.isEmpty())
+ {
+ auto normalKey = context + "|" + id;
+ newMapping.insert(normalKey, {strStr, fuzzy});
+ if(!disambiguation.isEmpty())
+ {
+ auto disambiguationKey = context + "|" + id + "@" + disambiguation;
+ newMapping_disambiguation.insert(disambiguationKey, {strStr, fuzzy});
+ }
+ }
+ context.clear();
+ disambiguation.clear();
+ id.clear();
+ str.clear();
+ fuzzy = nextFuzzy;
+ nextFuzzy = false;
+ };
+ while (!file.atEnd())
+ {
+ ParserArray line = file.readLine();
+ if(line.endsWith('\n'))
+ {
+ line.resize(line.size() - 1);
+ }
+ if(line.endsWith('\r'))
+ {
+ line.resize(line.size() - 1);
+ }
+
+ if(!line.size())
+ {
+ // NIL
+ }
+ else if(line[0] == '#')
+ {
+ if(line.contains(", fuzzy"))
+ {
+ nextFuzzy = true;
+ }
+ }
+ else if(line.startsWith('"'))
+ {
+ QByteArray temp;
+ QByteArray *out = &temp;
+
+ switch(mode)
+ {
+ case Mode::First:
+ qDebug() << "Unexpected escaped string during initial state... line:" << lineNumber;
+ return;
+ case Mode::MessageString:
+ out = &str;
+ break;
+ case Mode::MessageContext:
+ out = &context;
+ break;
+ case Mode::MessageId:
+ out = &id;
+ break;
+ }
+ if(!line.chompString(*out))
+ {
+ qDebug() << "Badly formatted string on line:" << lineNumber;
+ return;
+ }
+ }
+ else if(line.chomp("msgctxt ", 8))
+ {
+ switch(mode)
+ {
+ case Mode::First:
+ break;
+ case Mode::MessageString:
+ endEntry();
+ break;
+ case Mode::MessageContext:
+ case Mode::MessageId:
+ qDebug() << "Unexpected msgctxt line:" << lineNumber;
+ return;
+ }
+ if(line.chompString(context))
+ {
+ auto parts = context.split('|');
+ context = parts[0];
+ if(parts.size() > 1 && !parts[1].isEmpty())
+ {
+ disambiguation = parts[1];
+ }
+ mode = Mode::MessageContext;
+ }
+ }
+ else if (line.chomp("msgid ", 6))
+ {
+ switch(mode)
+ {
+ case Mode::MessageContext:
+ case Mode::First:
+ break;
+ case Mode::MessageString:
+ endEntry();
+ break;
+ case Mode::MessageId:
+ qDebug() << "Unexpected msgid line:" << lineNumber;
+ return;
+ }
+ if(line.chompString(id))
+ {
+ mode = Mode::MessageId;
+ }
+ }
+ else if (line.chomp("msgstr ", 7))
+ {
+ switch(mode)
+ {
+ case Mode::First:
+ case Mode::MessageString:
+ case Mode::MessageContext:
+ qDebug() << "Unexpected msgstr line:" << lineNumber;
+ return;
+ case Mode::MessageId:
+ break;
+ }
+ if(line.chompString(str))
+ {
+ mode = Mode::MessageString;
+ }
+ }
+ else
+ {
+ qDebug() << "I did not understand line: " << lineNumber << ":" << QString::fromUtf8(line);
+ }
+ lineNumber++;
+ }
+ endEntry();
+ mapping = std::move(newMapping);
+ mapping_disambiguatrion = std::move(newMapping_disambiguation);
+ loaded = true;
+}
+
+POTranslator::POTranslator(const QString& filename, QObject* parent) : QTranslator(parent)
+{
+ d = new POTranslatorPrivate;
+ d->filename = filename;
+ d->reload();
+}
+
+QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, int n) const
+{
+ if(disambiguation)
+ {
+ auto disambiguationKey = QByteArray(context) + "|" + QByteArray(sourceText) + "@" + QByteArray(disambiguation);
+ auto iter = d->mapping_disambiguatrion.find(disambiguationKey);
+ if(iter != d->mapping_disambiguatrion.end())
+ {
+ auto & entry = *iter;
+ if(entry.text.isEmpty())
+ {
+ qDebug() << "Translation entry has no content:" << disambiguationKey;
+ }
+ if(entry.fuzzy)
+ {
+ qDebug() << "Translation entry is fuzzy:" << disambiguationKey << "->" << entry.text;
+ }
+ return entry.text;
+ }
+ }
+ auto key = QByteArray(context) + "|" + QByteArray(sourceText);
+ auto iter = d->mapping.find(key);
+ if(iter != d->mapping.end())
+ {
+ auto & entry = *iter;
+ if(entry.text.isEmpty())
+ {
+ qDebug() << "Translation entry has no content:" << key;
+ }
+ if(entry.fuzzy)
+ {
+ qDebug() << "Translation entry is fuzzy:" << key << "->" << entry.text;
+ }
+ return entry.text;
+ }
+ return QString();
+}
+
+bool POTranslator::isEmpty() const
+{
+ return !d->loaded;
+}
diff --git a/launcher/translations/POTranslator.h b/launcher/translations/POTranslator.h
new file mode 100644
index 00000000..6d518560
--- /dev/null
+++ b/launcher/translations/POTranslator.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <QTranslator>
+
+struct POTranslatorPrivate;
+
+class POTranslator : public QTranslator
+{
+ Q_OBJECT
+public:
+ explicit POTranslator(const QString& filename, QObject * parent = nullptr);
+ QString translate(const char * context, const char * sourceText, const char * disambiguation, int n) const override;
+ bool isEmpty() const override;
+private:
+ POTranslatorPrivate * d;
+};
diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp
new file mode 100644
index 00000000..29a952b0
--- /dev/null
+++ b/launcher/translations/TranslationsModel.cpp
@@ -0,0 +1,653 @@
+#include "TranslationsModel.h"
+
+#include <QCoreApplication>
+#include <QTranslator>
+#include <QLocale>
+#include <QDir>
+#include <QLibraryInfo>
+#include <QDebug>
+#include <FileSystem.h>
+#include <net/NetJob.h>
+#include <net/ChecksumValidator.h>
+#include <Env.h>
+#include <BuildConfig.h>
+#include "Json.h"
+
+#include "POTranslator.h"
+
+const static QLatin1Literal defaultLangCode("en_US");
+
+enum class FileType
+{
+ NONE,
+ QM,
+ PO
+};
+
+struct Language
+{
+ Language()
+ {
+ updated = true;
+ }
+ Language(const QString & _key)
+ {
+ key = _key;
+ locale = QLocale(key);
+ updated = (key == defaultLangCode);
+ }
+
+ float percentTranslated() const
+ {
+ if (total == 0)
+ {
+ return 100.0f;
+ }
+ return 100.0f * float(translated) / float(total);
+ }
+
+ void setTranslationStats(unsigned _translated, unsigned _untranslated, unsigned _fuzzy)
+ {
+ translated = _translated;
+ untranslated = _untranslated;
+ fuzzy = _fuzzy;
+ total = translated + untranslated + fuzzy;
+ }
+
+ bool isOfSameNameAs(const Language& other) const
+ {
+ return key == other.key;
+ }
+
+ bool isIdenticalTo(const Language& other) const
+ {
+ return
+ (
+ key == other.key &&
+ file_name == other.file_name &&
+ file_size == other.file_size &&
+ file_sha1 == other.file_sha1 &&
+ translated == other.translated &&
+ fuzzy == other.fuzzy &&
+ total == other.fuzzy &&
+ localFileType == other.localFileType
+ );
+ }
+
+ Language & apply(Language & other)
+ {
+ if(!isOfSameNameAs(other))
+ {
+ return *this;
+ }
+ file_name = other.file_name;
+ file_size = other.file_size;
+ file_sha1 = other.file_sha1;
+ translated = other.translated;
+ fuzzy = other.fuzzy;
+ total = other.total;
+ localFileType = other.localFileType;
+ return *this;
+ }
+
+ QString key;
+ QLocale locale;
+ bool updated;
+
+ QString file_name = QString();
+ std::size_t file_size = 0;
+ QString file_sha1 = QString();
+
+ unsigned translated = 0;
+ unsigned untranslated = 0;
+ unsigned fuzzy = 0;
+ unsigned total = 0;
+
+ FileType localFileType = FileType::NONE;
+};
+
+
+
+struct TranslationsModel::Private
+{
+ QDir m_dir;
+
+ // initial state is just english
+ QVector<Language> m_languages = {Language (defaultLangCode)};
+
+ QString m_selectedLanguage = defaultLangCode;
+ std::unique_ptr<QTranslator> m_qt_translator;
+ std::unique_ptr<QTranslator> m_app_translator;
+
+ std::shared_ptr<Net::Download> m_index_task;
+ QString m_downloadingTranslation;
+ NetJobPtr m_dl_job;
+ NetJobPtr m_index_job;
+ QString m_nextDownload;
+
+ std::unique_ptr<POTranslator> m_po_translator;
+ QFileSystemWatcher *watcher;
+};
+
+TranslationsModel::TranslationsModel(QString path, QObject* parent): QAbstractListModel(parent)
+{
+ d.reset(new Private);
+ d->m_dir.setPath(path);
+ FS::ensureFolderPathExists(path);
+ reloadLocalFiles();
+
+ d->watcher = new QFileSystemWatcher(this);
+ connect(d->watcher, &QFileSystemWatcher::directoryChanged, this, &TranslationsModel::translationDirChanged);
+ d->watcher->addPath(d->m_dir.canonicalPath());
+}
+
+TranslationsModel::~TranslationsModel()
+{
+}
+
+void TranslationsModel::translationDirChanged(const QString& path)
+{
+ qDebug() << "Dir changed:" << path;
+ reloadLocalFiles();
+ selectLanguage(selectedLanguage());
+}
+
+void TranslationsModel::indexReceived()
+{
+ qDebug() << "Got translations index!";
+ d->m_index_job.reset();
+ if(d->m_selectedLanguage != defaultLangCode)
+ {
+ downloadTranslation(d->m_selectedLanguage);
+ }
+}
+
+namespace {
+void readIndex(const QString & path, QMap<QString, Language>& languages)
+{
+ QByteArray data;
+ try
+ {
+ data = FS::read(path);
+ }
+ catch (const Exception &e)
+ {
+ qCritical() << "Translations Download Failed: index file not readable";
+ return;
+ }
+
+ int index = 1;
+ try
+ {
+ auto toplevel_doc = Json::requireDocument(data);
+ auto doc = Json::requireObject(toplevel_doc);
+ auto file_type = Json::requireString(doc, "file_type");
+ if(file_type != "MMC-TRANSLATION-INDEX")
+ {
+ qCritical() << "Translations Download Failed: index file is of unknown file type" << file_type;
+ return;
+ }
+ auto version = Json::requireInteger(doc, "version");
+ if(version > 2)
+ {
+ qCritical() << "Translations Download Failed: index file is of unknown format version" << file_type;
+ return;
+ }
+ auto langObjs = Json::requireObject(doc, "languages");
+ for(auto iter = langObjs.begin(); iter != langObjs.end(); iter++)
+ {
+ Language lang(iter.key());
+
+ auto langObj = Json::requireObject(iter.value());
+ lang.setTranslationStats(
+ Json::ensureInteger(langObj, "translated", 0),
+ Json::ensureInteger(langObj, "untranslated", 0),
+ Json::ensureInteger(langObj, "fuzzy", 0)
+ );
+ lang.file_name = Json::requireString(langObj, "file");
+ lang.file_sha1 = Json::requireString(langObj, "sha1");
+ lang.file_size = Json::requireInteger(langObj, "size");
+
+ languages.insert(lang.key, lang);
+ index++;
+ }
+ }
+ catch (Json::JsonException & e)
+ {
+ qCritical() << "Translations Download Failed: index file could not be parsed as json";
+ }
+}
+}
+
+void TranslationsModel::reloadLocalFiles()
+{
+ QMap<QString, Language> languages = {{defaultLangCode, Language(defaultLangCode)}};
+
+ readIndex(d->m_dir.absoluteFilePath("index_v2.json"), languages);
+ auto entries = d->m_dir.entryInfoList({"mmc_*.qm", "*.po"}, QDir::Files | QDir::NoDotAndDotDot);
+ for(auto & entry: entries)
+ {
+ auto completeSuffix = entry.completeSuffix();
+ QString langCode;
+ FileType fileType = FileType::NONE;
+ if(completeSuffix == "qm")
+ {
+ langCode = entry.baseName().remove(0,4);
+ fileType = FileType::QM;
+ }
+ else if(completeSuffix == "po")
+ {
+ langCode = entry.baseName();
+ fileType = FileType::PO;
+ }
+ else
+ {
+ continue;
+ }
+
+ auto langIter = languages.find(langCode);
+ if(langIter != languages.end())
+ {
+ auto & language = *langIter;
+ if(int(fileType) > int(language.localFileType))
+ {
+ language.localFileType = fileType;
+ }
+ }
+ else
+ {
+ if(fileType == FileType::PO)
+ {
+ Language localFound(langCode);
+ localFound.localFileType = FileType::PO;
+ languages.insert(langCode, localFound);
+ }
+ }
+ }
+
+ // changed and removed languages
+ for(auto iter = d->m_languages.begin(); iter != d->m_languages.end();)
+ {
+ auto &language = *iter;
+ auto row = iter - d->m_languages.begin();
+
+ auto updatedLanguageIter = languages.find(language.key);
+ if(updatedLanguageIter != languages.end())
+ {
+ if(language.isIdenticalTo(*updatedLanguageIter))
+ {
+ languages.remove(language.key);
+ }
+ else
+ {
+ language.apply(*updatedLanguageIter);
+ emit dataChanged(index(row), index(row));
+ languages.remove(language.key);
+ }
+ iter++;
+ }
+ else
+ {
+ beginRemoveRows(QModelIndex(), row, row);
+ iter = d->m_languages.erase(iter);
+ endRemoveRows();
+ }
+ }
+ // added languages
+ if(languages.isEmpty())
+ {
+ return;
+ }
+ beginInsertRows(QModelIndex(), 0, d->m_languages.size() + languages.size() - 1);
+ for(auto & language: languages)
+ {
+ d->m_languages.append(language);
+ }
+ std::sort(d->m_languages.begin(), d->m_languages.end(), [](const Language& a, const Language& b) {
+ return a.key.compare(b.key) < 0;
+ });
+ endInsertRows();
+}
+
+namespace {
+enum class Column
+{
+ Language,
+ Completeness
+};
+}
+
+
+QVariant TranslationsModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ auto column = static_cast<Column>(index.column());
+
+ if (row < 0 || row >= d->m_languages.size())
+ return QVariant();
+
+ auto & lang = d->m_languages[row];
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ {
+ switch(column)
+ {
+ case Column::Language:
+ {
+ return lang.locale.nativeLanguageName();
+ }
+ case Column::Completeness:
+ {
+ QString text;
+ text.sprintf("%3.1f %%", lang.percentTranslated());
+ return text;
+ }
+ }
+ }
+ case Qt::ToolTipRole:
+ {
+ return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total").arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total));
+ }
+ case Qt::UserRole:
+ return lang.key;
+ default:
+ return QVariant();
+ }
+}
+
+QVariant TranslationsModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ auto column = static_cast<Column>(section);
+ if(role == Qt::DisplayRole)
+ {
+ switch(column)
+ {
+ case Column::Language:
+ {
+ return tr("Language");
+ }
+ case Column::Completeness:
+ {
+ return tr("Completeness");
+ }
+ }
+ }
+ else if(role == Qt::ToolTipRole)
+ {
+ switch(column)
+ {
+ case Column::Language:
+ {
+ return tr("The native language name.");
+ }
+ case Column::Completeness:
+ {
+ return tr("Completeness is the percentage of fully translated strings, not counting automatically guessed ones.");
+ }
+ }
+ }
+ return QAbstractListModel::headerData(section, orientation, role);
+}
+
+int TranslationsModel::rowCount(const QModelIndex& parent) const
+{
+ return d->m_languages.size();
+}
+
+int TranslationsModel::columnCount(const QModelIndex& parent) const
+{
+ return 2;
+}
+
+Language * TranslationsModel::findLanguage(const QString& key)
+{
+ auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language & lang)
+ {
+ return lang.key == key;
+ });
+ if(found == d->m_languages.end())
+ {
+ return nullptr;
+ }
+ else
+ {
+ return found;
+ }
+}
+
+bool TranslationsModel::selectLanguage(QString key)
+{
+ QString &langCode = key;
+ auto langPtr = findLanguage(key);
+ if(!langPtr)
+ {
+ qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
+ langCode = defaultLangCode;
+ }
+ else
+ {
+ langCode = langPtr->key;
+ }
+
+ // uninstall existing translators if there are any
+ if (d->m_app_translator)
+ {
+ QCoreApplication::removeTranslator(d->m_app_translator.get());
+ d->m_app_translator.reset();
+ }
+ if (d->m_qt_translator)
+ {
+ QCoreApplication::removeTranslator(d->m_qt_translator.get());
+ d->m_qt_translator.reset();
+ }
+
+ /*
+ * FIXME: potential source of crashes:
+ * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created.
+ * This function is not reentrant.
+ */
+ QLocale locale = QLocale(langCode);
+ QLocale::setDefault(locale);
+
+ // if it's the default UI language, finish
+ if(langCode == defaultLangCode)
+ {
+ d->m_selectedLanguage = langCode;
+ return true;
+ }
+
+ // otherwise install new translations
+ bool successful = false;
+ // FIXME: this is likely never present. FIX IT.
+ d->m_qt_translator.reset(new QTranslator());
+ if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::location(QLibraryInfo::TranslationsPath)))
+ {
+ qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "...";
+ if (!QCoreApplication::installTranslator(d->m_qt_translator.get()))
+ {
+ qCritical() << "Loading Qt Language File failed.";
+ d->m_qt_translator.reset();
+ }
+ else
+ {
+ successful = true;
+ }
+ }
+ else
+ {
+ d->m_qt_translator.reset();
+ }
+
+ if(langPtr->localFileType == FileType::PO)
+ {
+ qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
+ auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po"));
+ if(!poTranslator->isEmpty())
+ {
+ if (!QCoreApplication::installTranslator(poTranslator))
+ {
+ delete poTranslator;
+ qCritical() << "Installing Application Language File failed.";
+ }
+ else
+ {
+ d->m_app_translator.reset(poTranslator);
+ successful = true;
+ }
+ }
+ else
+ {
+ qCritical() << "Loading Application Language File failed.";
+ d->m_app_translator.reset();
+ }
+ }
+ else if(langPtr->localFileType == FileType::QM)
+ {
+ d->m_app_translator.reset(new QTranslator());
+ if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path()))
+ {
+ qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
+ if (!QCoreApplication::installTranslator(d->m_app_translator.get()))
+ {
+ qCritical() << "Installing Application Language File failed.";
+ d->m_app_translator.reset();
+ }
+ else
+ {
+ successful = true;
+ }
+ }
+ else
+ {
+ d->m_app_translator.reset();
+ }
+ }
+ else
+ {
+ d->m_app_translator.reset();
+ }
+ d->m_selectedLanguage = langCode;
+ return successful;
+}
+
+QModelIndex TranslationsModel::selectedIndex()
+{
+ auto found = findLanguage(d->m_selectedLanguage);
+ if(found)
+ {
+ // QVector iterator freely converts to pointer to contained type
+ return index(found - d->m_languages.begin(), 0, QModelIndex());
+ }
+ return QModelIndex();
+}
+
+QString TranslationsModel::selectedLanguage()
+{
+ return d->m_selectedLanguage;
+}
+
+void TranslationsModel::downloadIndex()
+{
+ if(d->m_index_job || d->m_dl_job)
+ {
+ return;
+ }
+ qDebug() << "Downloading Translations Index...";
+ d->m_index_job.reset(new NetJob("Translations Index"));
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "index_v2.json");
+ entry->setStale(true);
+ d->m_index_task = Net::Download::makeCached(QUrl("https://files.multimc.org/translations/index_v2.json"), entry);
+ d->m_index_job->addNetAction(d->m_index_task);
+ connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
+ connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived);
+ d->m_index_job->start();
+}
+
+void TranslationsModel::updateLanguage(QString key)
+{
+ if(key == defaultLangCode)
+ {
+ qWarning() << "Cannot update builtin language" << key;
+ return;
+ }
+ auto found = findLanguage(key);
+ if(!found)
+ {
+ qWarning() << "Cannot update invalid language" << key;
+ return;
+ }
+ if(!found->updated)
+ {
+ downloadTranslation(key);
+ }
+}
+
+void TranslationsModel::downloadTranslation(QString key)
+{
+ if(d->m_dl_job)
+ {
+ d->m_nextDownload = key;
+ return;
+ }
+ auto lang = findLanguage(key);
+ if(!lang)
+ {
+ qWarning() << "Will not download an unknown translation" << key;
+ return;
+ }
+
+ d->m_downloadingTranslation = key;
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "mmc_" + key + ".qm");
+ entry->setStale(true);
+
+ auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry);
+ auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash));
+ dl->m_total_progress = lang->file_size;
+
+ d->m_dl_job.reset(new NetJob("Translation for " + key));
+ d->m_dl_job->addNetAction(dl);
+
+ connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood);
+ connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed);
+
+ d->m_dl_job->start();
+}
+
+void TranslationsModel::downloadNext()
+{
+ if(!d->m_nextDownload.isEmpty())
+ {
+ downloadTranslation(d->m_nextDownload);
+ d->m_nextDownload.clear();
+ }
+}
+
+void TranslationsModel::dlFailed(QString reason)
+{
+ qCritical() << "Translations Download Failed:" << reason;
+ d->m_dl_job.reset();
+ downloadNext();
+}
+
+void TranslationsModel::dlGood()
+{
+ qDebug() << "Got translation:" << d->m_downloadingTranslation;
+
+ if(d->m_downloadingTranslation == d->m_selectedLanguage)
+ {
+ selectLanguage(d->m_selectedLanguage);
+ }
+ d->m_dl_job.reset();
+ downloadNext();
+}
+
+void TranslationsModel::indexFailed(QString reason)
+{
+ qCritical() << "Translations Index Download Failed:" << reason;
+ d->m_index_job.reset();
+}
diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h
new file mode 100644
index 00000000..3abf84e6
--- /dev/null
+++ b/launcher/translations/TranslationsModel.h
@@ -0,0 +1,64 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <memory>
+
+struct Language;
+
+class TranslationsModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ explicit TranslationsModel(QString path, QObject *parent = 0);
+ virtual ~TranslationsModel();
+
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex & parent) const override;
+
+ bool selectLanguage(QString key);
+ void updateLanguage(QString key);
+ QModelIndex selectedIndex();
+ QString selectedLanguage();
+
+ void downloadIndex();
+
+private:
+ Language *findLanguage(const QString & key);
+ void reloadLocalFiles();
+ void downloadTranslation(QString key);
+ void downloadNext();
+
+ // hide copy constructor
+ TranslationsModel(const TranslationsModel &) = delete;
+ // hide assign op
+ TranslationsModel &operator=(const TranslationsModel &) = delete;
+
+private slots:
+ void indexReceived();
+ void indexFailed(QString reason);
+ void dlFailed(QString reason);
+ void dlGood();
+ void translationDirChanged(const QString &path);
+
+
+private: /* data */
+ struct Private;
+ std::unique_ptr<Private> d;
+};
diff --git a/launcher/updater/DownloadTask.cpp b/launcher/updater/DownloadTask.cpp
new file mode 100644
index 00000000..20b26ebb
--- /dev/null
+++ b/launcher/updater/DownloadTask.cpp
@@ -0,0 +1,173 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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..."));
+
+ 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 = Net::Download::makeByteArray(newIndexUrl, &newVersionFileListData));
+
+ // 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 = Net::Download::makeByteArray(cIndexUrl, &currentVersionFileListData));
+ 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->wasSuccessful())
+ {
+ 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(newVersionFileListData, m_newVersionFileList, error))
+ {
+ qCritical() << error;
+ emitFailed(error);
+ return;
+ }
+
+ // if we have the current version info, use it.
+ if (m_currentVersionFileListDownload && m_currentVersionFileListDownload->wasSuccessful())
+ {
+ 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( currentVersionFileListData, 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);
+
+ if(netJob->size() == 1) // Translation issues... see https://github.com/MultiMC/MultiMC5/issues/1701
+ {
+ setStatus(tr("Downloading one update file."));
+ }
+ else
+ {
+ 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/launcher/updater/DownloadTask.h b/launcher/updater/DownloadTask.h
new file mode 100644
index 00000000..fc5030b4
--- /dev/null
+++ b/launcher/updater/DownloadTask.h
@@ -0,0 +1,96 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include "GoUpdate.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 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);
+ virtual ~DownloadTask() {};
+
+ /// 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;
+ QByteArray currentVersionFileListData;
+ QByteArray newVersionFileListData;
+ Net::Download::Ptr m_currentVersionFileListDownload;
+ Net::Download::Ptr 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);
+};
+
+}
diff --git a/launcher/updater/DownloadTask_test.cpp b/launcher/updater/DownloadTask_test.cpp
new file mode 100644
index 00000000..8d5375e8
--- /dev/null
+++ b/launcher/updater/DownloadTask_test.cpp
@@ -0,0 +1,195 @@
+#include <QTest>
+#include <QSignalSpy>
+
+#include "TestUtil.h"
+
+#include "updater/GoUpdate.h"
+#include "updater/DownloadTask.h"
+#include "updater/UpdateChecker.h"
+#include <FileSystem.h>
+
+using namespace GoUpdate;
+
+FileSourceList encodeBaseFile(const char *suffix)
+{
+ auto base = QDir::currentPath();
+ QUrl localFile = QUrl::fromLocalFile(base + suffix);
+ QString localUrlString = localFile.toString(QUrl::FullyEncoded);
+ auto item = FileSource("http", localUrlString);
+ return FileSourceList({item});
+}
+
+Q_DECLARE_METATYPE(VersionFileList)
+Q_DECLARE_METATYPE(Operation)
+
+QDebug operator<<(QDebug dbg, const FileSource &f)
+{
+ dbg.nospace() << "FileSource(type=" << f.type << " url=" << f.url
+ << " comp=" << f.compressionType << ")";
+ return dbg.maybeSpace();
+}
+
+QDebug operator<<(QDebug dbg, const VersionFileEntry &v)
+{
+ dbg.nospace() << "VersionFileEntry(path=" << v.path << " mode=" << v.mode
+ << " md5=" << v.md5 << " sources=" << v.sources << ")";
+ return dbg.maybeSpace();
+}
+
+QDebug operator<<(QDebug dbg, const Operation::Type &t)
+{
+ switch (t)
+ {
+ case Operation::OP_REPLACE:
+ dbg << "OP_COPY";
+ break;
+ case Operation::OP_DELETE:
+ dbg << "OP_DELETE";
+ break;
+ }
+ return dbg.maybeSpace();
+}
+
+QDebug operator<<(QDebug dbg, const Operation &u)
+{
+ dbg.nospace() << "Operation(type=" << u.type << " file=" << u.source
+ << " dest=" << u.destination << " mode=" << u.destinationMode << ")";
+ return dbg.maybeSpace();
+}
+
+class DownloadTaskTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void initTestCase()
+ {
+ }
+ void cleanupTestCase()
+ {
+ }
+
+ void test_parseVersionInfo_data()
+ {
+ QTest::addColumn<QByteArray>("data");
+ QTest::addColumn<VersionFileList>("list");
+ QTest::addColumn<QString>("error");
+ QTest::addColumn<bool>("ret");
+
+ QTest::newRow("one")
+ << MULTIMC_GET_TEST_FILE("data/1.json")
+ << (VersionFileList()
+ << VersionFileEntry{"fileOne",
+ 493,
+ encodeBaseFile("/data/fileOneA"),
+ "9eb84090956c484e32cb6c08455a667b"}
+ << VersionFileEntry{"fileTwo",
+ 644,
+ encodeBaseFile("/data/fileTwo"),
+ "38f94f54fa3eb72b0ea836538c10b043"}
+ << VersionFileEntry{"fileThree",
+ 750,
+ encodeBaseFile("/data/fileThree"),
+ "f12df554b21e320be6471d7154130e70"})
+ << QString() << true;
+ QTest::newRow("two")
+ << MULTIMC_GET_TEST_FILE("data/2.json")
+ << (VersionFileList()
+ << VersionFileEntry{"fileOne",
+ 493,
+ encodeBaseFile("/data/fileOneB"),
+ "42915a71277c9016668cce7b82c6b577"}
+ << VersionFileEntry{"fileTwo",
+ 644,
+ encodeBaseFile("/data/fileTwo"),
+ "38f94f54fa3eb72b0ea836538c10b043"})
+ << QString() << true;
+ }
+ void test_parseVersionInfo()
+ {
+ QFETCH(QByteArray, data);
+ QFETCH(VersionFileList, list);
+ QFETCH(QString, error);
+ QFETCH(bool, ret);
+
+ VersionFileList outList;
+ QString outError;
+ bool outRet = parseVersionInfo(data, outList, outError);
+ QCOMPARE(outRet, ret);
+ QCOMPARE(outList, list);
+ QCOMPARE(outError, error);
+ }
+
+ void test_processFileLists_data()
+ {
+ QTest::addColumn<QString>("tempFolder");
+ QTest::addColumn<VersionFileList>("currentVersion");
+ QTest::addColumn<VersionFileList>("newVersion");
+ QTest::addColumn<OperationList>("expectedOperations");
+
+ QTemporaryDir tempFolderObj;
+ QString tempFolder = tempFolderObj.path();
+ // update fileOne, keep fileTwo, remove fileThree
+ QTest::newRow("test 1")
+ << tempFolder << (VersionFileList()
+ << VersionFileEntry{
+ "data/fileOne", 493,
+ FileSourceList()
+ << FileSource(
+ "http", "http://host/path/fileOne-1"),
+ "9eb84090956c484e32cb6c08455a667b"}
+ << VersionFileEntry{
+ "data/fileTwo", 644,
+ FileSourceList()
+ << FileSource(
+ "http", "http://host/path/fileTwo-1"),
+ "38f94f54fa3eb72b0ea836538c10b043"}
+ << VersionFileEntry{
+ "data/fileThree", 420,
+ FileSourceList()
+ << FileSource(
+ "http", "http://host/path/fileThree-1"),
+ "f12df554b21e320be6471d7154130e70"})
+ << (VersionFileList()
+ << VersionFileEntry{
+ "data/fileOne", 493,
+ FileSourceList()
+ << FileSource("http",
+ "http://host/path/fileOne-2"),
+ "42915a71277c9016668cce7b82c6b577"}
+ << VersionFileEntry{
+ "data/fileTwo", 644,
+ FileSourceList()
+ << FileSource("http",
+ "http://host/path/fileTwo-2"),
+ "38f94f54fa3eb72b0ea836538c10b043"})
+ << (OperationList()
+ << Operation::DeleteOp("data/fileThree")
+ << Operation::CopyOp(
+ FS::PathCombine(tempFolder,
+ QString("data/fileOne").replace("/", "_")),
+ "data/fileOne", 493));
+ }
+ void test_processFileLists()
+ {
+ QFETCH(QString, tempFolder);
+ QFETCH(VersionFileList, currentVersion);
+ QFETCH(VersionFileList, newVersion);
+ QFETCH(OperationList, expectedOperations);
+
+ OperationList operations;
+
+ processFileLists(currentVersion, newVersion, QDir::currentPath(), tempFolder, new NetJob("Dummy"), operations);
+ qDebug() << (operations == expectedOperations);
+ qDebug() << operations;
+ qDebug() << expectedOperations;
+ QCOMPARE(operations, expectedOperations);
+ }
+};
+
+extern "C"
+{
+ QTEST_GUILESS_MAIN(DownloadTaskTest)
+}
+
+#include "DownloadTask_test.moc"
diff --git a/launcher/updater/GoUpdate.cpp b/launcher/updater/GoUpdate.cpp
new file mode 100644
index 00000000..6167418e
--- /dev/null
+++ b/launcher/updater/GoUpdate.cpp
@@ -0,0 +1,198 @@
+#include "GoUpdate.h"
+#include <QDebug>
+#include <QDomDocument>
+#include <QFile>
+#include <FileSystem.h>
+
+#include "net/Download.h"
+#include "net/ChecksumValidator.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();
+
+ 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 = Net::Download::makeFile(source.url, dlPath);
+ auto rawMd5 = QByteArray::fromHex(entry.md5.toLatin1());
+ download->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
+ job->addNetAction(download);
+ ops.append(Operation::CopyOp(dlPath, entry.path, entry.mode));
+ }
+ }
+ return true;
+}
+}
diff --git a/launcher/updater/GoUpdate.h b/launcher/updater/GoUpdate.h
new file mode 100644
index 00000000..8058e543
--- /dev/null
+++ b/launcher/updater/GoUpdate.h
@@ -0,0 +1,125 @@
+#pragma once
+#include <QByteArray>
+#include <net/NetJob.h>
+
+namespace GoUpdate
+{
+
+/**
+ * A temporary object exchanged between updated checker and the actual update task
+ */
+struct 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 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 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 Operation
+{
+ static Operation CopyOp(QString from, QString to, int fmode=0644)
+ {
+ return Operation{OP_REPLACE, from, to, fmode};
+ }
+ static Operation DeleteOp(QString file)
+ {
+ return Operation{OP_DELETE, QString(), file, 0644};
+ }
+
+ // FIXME: for some types, some of the other fields are irrelevant!
+ bool operator==(const Operation &u2) const
+ {
+ return type == u2.type &&
+ source == u2.source &&
+ destination == u2.destination &&
+ destinationMode == u2.destinationMode;
+ }
+
+ //! Specifies the type of operation that this is.
+ enum Type
+ {
+ OP_REPLACE,
+ OP_DELETE,
+ } type;
+
+ //! The source file, if any
+ QString source;
+
+ //! The destination file.
+ QString destination;
+
+ //! The mode to change the destination file to.
+ int destinationMode;
+};
+typedef QList<Operation> OperationList;
+
+/**
+ * Loads the file list from the given version info JSON object into the given list.
+ */
+bool 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 processFileLists
+(
+ const VersionFileList &currentVersion,
+ const VersionFileList &newVersion,
+ const QString &rootPath,
+ const QString &tempPath,
+ NetJobPtr job,
+ OperationList &ops
+);
+
+}
+Q_DECLARE_METATYPE(GoUpdate::Status)
diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp
new file mode 100644
index 00000000..be33c73c
--- /dev/null
+++ b/launcher/updater/UpdateChecker.cpp
@@ -0,0 +1,260 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "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;
+}
+
+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(Net::Download::makeByteArray(indexUrl, &indexData));
+ 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;
+ indexJob.reset();
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(indexData, &jsonError);
+ indexData.clear();
+ if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject())
+ {
+ qCritical() << "Failed to parse GoUpdate repository index. JSON error"
+ << jsonError.errorString() << "at offset" << jsonError.offset;
+ m_updateChecking = false;
+ 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;
+ m_updateChecking = false;
+ 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_chanListLoading)
+ {
+ qDebug() << "Ignoring channel list update request. Already grabbing channel list.";
+ return;
+ }
+
+ 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(Net::Download::makeByteArray(QUrl(m_channelListUrl), &chanlistData));
+ 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)
+{
+ chanListJob.reset();
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(chanlistData, &jsonError);
+ chanlistData.clear();
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ // TODO: Report errors to the user.
+ qCritical() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset;
+ m_chanListLoading = false;
+ 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;
+ m_chanListLoading = false;
+ 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/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h
new file mode 100644
index 00000000..91b6e26e
--- /dev/null
+++ b/launcher/updater/UpdateChecker.h
@@ -0,0 +1,119 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "net/NetJob.h"
+#include "GoUpdate.h"
+
+class 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;
+ QByteArray indexData;
+ NetJobPtr chanListJob;
+ QByteArray chanlistData;
+
+ 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 = false;
+
+ /*!
+ * True if the channel list has loaded.
+ * If this is false, trying to check for updates will call updateChanList first.
+ */
+ bool m_chanListLoaded = false;
+
+ /*!
+ * Set to true while the channel list is currently loading.
+ */
+ bool m_chanListLoading = false;
+
+ /*!
+ * 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 = false;
+
+ /*!
+ * 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/launcher/updater/UpdateChecker_test.cpp b/launcher/updater/UpdateChecker_test.cpp
new file mode 100644
index 00000000..5702d9c6
--- /dev/null
+++ b/launcher/updater/UpdateChecker_test.cpp
@@ -0,0 +1,147 @@
+#include <QTest>
+#include <QSignalSpy>
+
+#include "TestUtil.h"
+#include "updater/UpdateChecker.h"
+
+Q_DECLARE_METATYPE(UpdateChecker::ChannelListEntry)
+
+bool operator==(const UpdateChecker::ChannelListEntry &e1, const UpdateChecker::ChannelListEntry &e2)
+{
+ qDebug() << e1.url << "vs" << e2.url;
+ return e1.id == e2.id &&
+ e1.name == e2.name &&
+ e1.description == e2.description &&
+ e1.url == e2.url;
+}
+
+QDebug operator<<(QDebug dbg, const UpdateChecker::ChannelListEntry &c)
+{
+ dbg.nospace() << "ChannelListEntry(id=" << c.id << " name=" << c.name << " description=" << c.description << " url=" << c.url << ")";
+ return dbg.maybeSpace();
+}
+
+QString findTestDataUrl(const char *file)
+{
+ return QUrl::fromLocalFile(QFINDTESTDATA(file)).toString();
+}
+
+class UpdateCheckerTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void initTestCase()
+ {
+
+ }
+ void cleanupTestCase()
+ {
+
+ }
+
+ void tst_ChannelListParsing_data()
+ {
+ QTest::addColumn<QString>("channel");
+ QTest::addColumn<QString>("channelUrl");
+ QTest::addColumn<bool>("hasChannels");
+ QTest::addColumn<bool>("valid");
+ QTest::addColumn<QList<UpdateChecker::ChannelListEntry> >("result");
+
+ QTest::newRow("garbage")
+ << QString()
+ << findTestDataUrl("data/garbageChannels.json")
+ << false
+ << false
+ << QList<UpdateChecker::ChannelListEntry>();
+ QTest::newRow("errors")
+ << QString()
+ << findTestDataUrl("data/errorChannels.json")
+ << false
+ << true
+ << QList<UpdateChecker::ChannelListEntry>();
+ QTest::newRow("no channels")
+ << QString()
+ << findTestDataUrl("data/noChannels.json")
+ << false
+ << true
+ << QList<UpdateChecker::ChannelListEntry>();
+ QTest::newRow("one channel")
+ << QString("develop")
+ << findTestDataUrl("data/oneChannel.json")
+ << true
+ << true
+ << (QList<UpdateChecker::ChannelListEntry>() << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "http://example.org/stuff"});
+ QTest::newRow("several channels")
+ << QString("develop")
+ << findTestDataUrl("data/channels.json")
+ << true
+ << true
+ << (QList<UpdateChecker::ChannelListEntry>()
+ << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", findTestDataUrl("data")}
+ << UpdateChecker::ChannelListEntry{"stable", "Stable", "It's stable at least", findTestDataUrl("data")}
+ << UpdateChecker::ChannelListEntry{"42", "The Channel", "This is the channel that is going to answer all of your questions", "https://dent.me/tea"});
+ }
+ void tst_ChannelListParsing()
+ {
+
+ QFETCH(QString, channel);
+ QFETCH(QString, channelUrl);
+ QFETCH(bool, hasChannels);
+ QFETCH(bool, valid);
+ QFETCH(QList<UpdateChecker::ChannelListEntry>, result);
+
+ UpdateChecker checker(channelUrl, channel, 0);
+
+ QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded()));
+ QVERIFY(channelListLoadedSpy.isValid());
+
+ checker.updateChanList(false);
+
+ if (valid)
+ {
+ QVERIFY(channelListLoadedSpy.wait());
+ QCOMPARE(channelListLoadedSpy.size(), 1);
+ }
+ else
+ {
+ channelListLoadedSpy.wait();
+ QCOMPARE(channelListLoadedSpy.size(), 0);
+ }
+
+ QCOMPARE(checker.hasChannels(), hasChannels);
+ QCOMPARE(checker.getChannelList(), result);
+ }
+
+ void tst_UpdateChecking()
+ {
+ QString channel = "develop";
+ QString channelUrl = findTestDataUrl("data/channels.json");
+ int currentBuild = 2;
+
+ UpdateChecker checker(channelUrl, channel, currentBuild);
+
+ QSignalSpy updateAvailableSpy(&checker, SIGNAL(updateAvailable(GoUpdate::Status)));
+ QVERIFY(updateAvailableSpy.isValid());
+ QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded()));
+ QVERIFY(channelListLoadedSpy.isValid());
+
+ checker.updateChanList(false);
+ QVERIFY(channelListLoadedSpy.wait());
+
+ qDebug() << "CWD:" << QDir::current().absolutePath();
+ checker.m_channels[0].url = findTestDataUrl("data/");
+ checker.checkForUpdate(channel, false);
+
+ QVERIFY(updateAvailableSpy.wait());
+
+ auto status = updateAvailableSpy.first().first().value<GoUpdate::Status>();
+ QCOMPARE(checker.m_channels[0].url, status.newRepoUrl);
+ QCOMPARE(3, status.newVersionId);
+ QCOMPARE(currentBuild, status.currentVersionId);
+ }
+};
+
+QTEST_GUILESS_MAIN(UpdateCheckerTest)
+
+#include "UpdateChecker_test.moc"
diff --git a/launcher/updater/testdata/1.json b/launcher/updater/testdata/1.json
new file mode 100644
index 00000000..7af7e52d
--- /dev/null
+++ b/launcher/updater/testdata/1.json
@@ -0,0 +1,43 @@
+{
+ "ApiVersion": 0,
+ "Id": 1,
+ "Name": "1.0.1",
+ "Files": [
+ {
+ "Path": "fileOne",
+ "Sources": [
+ {
+ "SourceType": "http",
+ "Url": "@TEST_DATA_URL@/fileOneA"
+ }
+ ],
+ "Executable": true,
+ "Perms": 493,
+ "MD5": "9eb84090956c484e32cb6c08455a667b"
+ },
+ {
+ "Path": "fileTwo",
+ "Sources": [
+ {
+ "SourceType": "http",
+ "Url": "@TEST_DATA_URL@/fileTwo"
+ }
+ ],
+ "Executable": false,
+ "Perms": 644,
+ "MD5": "38f94f54fa3eb72b0ea836538c10b043"
+ },
+ {
+ "Path": "fileThree",
+ "Sources": [
+ {
+ "SourceType": "http",
+ "Url": "@TEST_DATA_URL@/fileThree"
+ }
+ ],
+ "Executable": false,
+ "Perms": "750",
+ "MD5": "f12df554b21e320be6471d7154130e70"
+ }
+ ]
+}
diff --git a/launcher/updater/testdata/2.json b/launcher/updater/testdata/2.json
new file mode 100644
index 00000000..96d430d5
--- /dev/null
+++ b/launcher/updater/testdata/2.json
@@ -0,0 +1,31 @@
+{
+ "ApiVersion": 0,
+ "Id": 1,
+ "Name": "1.0.1",
+ "Files": [
+ {
+ "Path": "fileOne",
+ "Sources": [
+ {
+ "SourceType": "http",
+ "Url": "@TEST_DATA_URL@/fileOneB"
+ }
+ ],
+ "Executable": true,
+ "Perms": 493,
+ "MD5": "42915a71277c9016668cce7b82c6b577"
+ },
+ {
+ "Path": "fileTwo",
+ "Sources": [
+ {
+ "SourceType": "http",
+ "Url": "@TEST_DATA_URL@/fileTwo"
+ }
+ ],
+ "Executable": false,
+ "Perms": 644,
+ "MD5": "38f94f54fa3eb72b0ea836538c10b043"
+ }
+ ]
+}
diff --git a/launcher/updater/testdata/channels.json b/launcher/updater/testdata/channels.json
new file mode 100644
index 00000000..5c6e42cb
--- /dev/null
+++ b/launcher/updater/testdata/channels.json
@@ -0,0 +1,23 @@
+{
+ "format_version": 0,
+ "channels": [
+ {
+ "id": "develop",
+ "name": "Develop",
+ "description": "The channel called \"develop\"",
+ "url": "@TEST_DATA_URL@"
+ },
+ {
+ "id": "stable",
+ "name": "Stable",
+ "description": "It's stable at least",
+ "url": "@TEST_DATA_URL@"
+ },
+ {
+ "id": "42",
+ "name": "The Channel",
+ "description": "This is the channel that is going to answer all of your questions",
+ "url": "https://dent.me/tea"
+ }
+ ]
+}
diff --git a/launcher/updater/testdata/errorChannels.json b/launcher/updater/testdata/errorChannels.json
new file mode 100644
index 00000000..a2cb2165
--- /dev/null
+++ b/launcher/updater/testdata/errorChannels.json
@@ -0,0 +1,23 @@
+{
+ "format_version": 0,
+ "channels": [
+ {
+ "id": "",
+ "name": "Develop",
+ "description": "The channel called \"develop\"",
+ "url": "http://example.org/stuff"
+ },
+ {
+ "id": "stable",
+ "name": "",
+ "description": "It's stable at least",
+ "url": "ftp://username@host/path/to/stuff"
+ },
+ {
+ "id": "42",
+ "name": "The Channel",
+ "description": "This is the channel that is going to answer all of your questions",
+ "url": ""
+ }
+ ]
+}
diff --git a/launcher/updater/testdata/fileOneA b/launcher/updater/testdata/fileOneA
new file mode 100644
index 00000000..f2e41136
--- /dev/null
+++ b/launcher/updater/testdata/fileOneA
@@ -0,0 +1 @@
+stuff
diff --git a/launcher/updater/testdata/fileOneB b/launcher/updater/testdata/fileOneB
new file mode 100644
index 00000000..f9aba922
--- /dev/null
+++ b/launcher/updater/testdata/fileOneB
@@ -0,0 +1,3 @@
+stuff
+
+more stuff that came in the new version
diff --git a/launcher/updater/testdata/fileThree b/launcher/updater/testdata/fileThree
new file mode 100644
index 00000000..6353ff16
--- /dev/null
+++ b/launcher/updater/testdata/fileThree
@@ -0,0 +1 @@
+this is yet another file
diff --git a/launcher/updater/testdata/fileTwo b/launcher/updater/testdata/fileTwo
new file mode 100644
index 00000000..aad9a93a
--- /dev/null
+++ b/launcher/updater/testdata/fileTwo
@@ -0,0 +1 @@
+some other stuff
diff --git a/launcher/updater/testdata/garbageChannels.json b/launcher/updater/testdata/garbageChannels.json
new file mode 100644
index 00000000..34437451
--- /dev/null
+++ b/launcher/updater/testdata/garbageChannels.json
@@ -0,0 +1,22 @@
+{
+ "format_version": 0,
+ "channels": [
+ {
+ "id": "develop",
+ "name": "Develop",
+ "description": "The channel called \"develop\"",
+aa "url": "http://example.org/stuff"
+ },
+a "id": "stable",
+ "name": "Stable",
+ "description": "It's stable at least",
+ "url": "ftp://username@host/path/to/stuff"
+ },
+ {
+ "id": "42"f
+ "name": "The Channel",
+ "description": "This is the channel that is going to answer all of your questions",
+ "url": "https://dent.me/tea"
+ }
+ ]
+}
diff --git a/launcher/updater/testdata/index.json b/launcher/updater/testdata/index.json
new file mode 100644
index 00000000..867bdcfb
--- /dev/null
+++ b/launcher/updater/testdata/index.json
@@ -0,0 +1,9 @@
+{
+ "ApiVersion": 0,
+ "Versions": [
+ { "Id": 0, "Name": "1.0.0" },
+ { "Id": 1, "Name": "1.0.1" },
+ { "Id": 2, "Name": "1.0.2" },
+ { "Id": 3, "Name": "1.0.3" }
+ ]
+}
diff --git a/launcher/updater/testdata/noChannels.json b/launcher/updater/testdata/noChannels.json
new file mode 100644
index 00000000..76988982
--- /dev/null
+++ b/launcher/updater/testdata/noChannels.json
@@ -0,0 +1,5 @@
+{
+ "format_version": 0,
+ "channels": [
+ ]
+}
diff --git a/launcher/updater/testdata/oneChannel.json b/launcher/updater/testdata/oneChannel.json
new file mode 100644
index 00000000..cc8ed255
--- /dev/null
+++ b/launcher/updater/testdata/oneChannel.json
@@ -0,0 +1,11 @@
+{
+ "format_version": 0,
+ "channels": [
+ {
+ "id": "develop",
+ "name": "Develop",
+ "description": "The channel called \"develop\"",
+ "url": "http://example.org/stuff"
+ }
+ ]
+}
diff --git a/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml b/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml
new file mode 100644
index 00000000..09c162ca
--- /dev/null
+++ b/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml
@@ -0,0 +1,17 @@
+<update version="3">
+ <install>
+ <file>
+ <source>sourceOne</source>
+ <dest>destOne</dest>
+ <mode>0777</mode>
+ </file>
+ <file>
+ <source>MultiMC.exe</source>
+ <dest>M/u/l/t/i/M/C/e/x/e</dest>
+ <mode>0644</mode>
+ </file>
+ </install>
+ <uninstall>
+ <file>toDelete.abc</file>
+ </uninstall>
+</update>
diff --git a/launcher/widgets/Common.cpp b/launcher/widgets/Common.cpp
new file mode 100644
index 00000000..f72f3596
--- /dev/null
+++ b/launcher/widgets/Common.cpp
@@ -0,0 +1,27 @@
+#include "Common.h"
+
+// Origin: Qt
+QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed)
+{
+ QStringList lines;
+ height = 0;
+ widthUsed = 0;
+ textLayout.beginLayout();
+ QString str = textLayout.text();
+ while (true)
+ {
+ QTextLine line = textLayout.createLine();
+ if (!line.isValid())
+ break;
+ if (line.textLength() == 0)
+ break;
+ line.setLineWidth(lineWidth);
+ line.setPosition(QPointF(0, height));
+ height += line.height();
+ lines.append(str.mid(line.textStart(), line.textLength()));
+ widthUsed = qMax(widthUsed, line.naturalTextWidth());
+ }
+ textLayout.endLayout();
+ return lines;
+}
diff --git a/launcher/widgets/Common.h b/launcher/widgets/Common.h
new file mode 100644
index 00000000..b3fbe1a0
--- /dev/null
+++ b/launcher/widgets/Common.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <QStringList>
+#include <QTextLayout>
+
+QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height,
+ qreal &widthUsed); \ No newline at end of file
diff --git a/launcher/widgets/CustomCommands.cpp b/launcher/widgets/CustomCommands.cpp
new file mode 100644
index 00000000..24bdc07d
--- /dev/null
+++ b/launcher/widgets/CustomCommands.cpp
@@ -0,0 +1,49 @@
+#include "CustomCommands.h"
+#include "ui_CustomCommands.h"
+
+CustomCommands::~CustomCommands()
+{
+ delete ui;
+}
+
+CustomCommands::CustomCommands(QWidget* parent):
+ QWidget(parent),
+ ui(new Ui::CustomCommands)
+{
+ ui->setupUi(this);
+}
+
+void CustomCommands::initialize(bool checkable, bool checked, const QString& prelaunch, const QString& wrapper, const QString& postexit)
+{
+ ui->customCommandsGroupBox->setCheckable(checkable);
+ if(checkable)
+ {
+ ui->customCommandsGroupBox->setChecked(checked);
+ }
+ ui->preLaunchCmdTextBox->setText(prelaunch);
+ ui->wrapperCmdTextBox->setText(wrapper);
+ ui->postExitCmdTextBox->setText(postexit);
+}
+
+
+bool CustomCommands::checked() const
+{
+ if(!ui->customCommandsGroupBox->isCheckable())
+ return true;
+ return ui->customCommandsGroupBox->isChecked();
+}
+
+QString CustomCommands::prelaunchCommand() const
+{
+ return ui->preLaunchCmdTextBox->text();
+}
+
+QString CustomCommands::wrapperCommand() const
+{
+ return ui->wrapperCmdTextBox->text();
+}
+
+QString CustomCommands::postexitCommand() const
+{
+ return ui->postExitCmdTextBox->text();
+}
diff --git a/launcher/widgets/CustomCommands.h b/launcher/widgets/CustomCommands.h
new file mode 100644
index 00000000..8db991fa
--- /dev/null
+++ b/launcher/widgets/CustomCommands.h
@@ -0,0 +1,43 @@
+/* Copyright 2018-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+namespace Ui
+{
+class CustomCommands;
+}
+
+class CustomCommands : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit CustomCommands(QWidget *parent = 0);
+ virtual ~CustomCommands();
+ void initialize(bool checkable, bool checked, const QString & prelaunch, const QString & wrapper, const QString & postexit);
+
+ bool checked() const;
+ QString prelaunchCommand() const;
+ QString wrapperCommand() const;
+ QString postexitCommand() const;
+
+private:
+ Ui::CustomCommands *ui;
+};
+
+
diff --git a/launcher/widgets/CustomCommands.ui b/launcher/widgets/CustomCommands.ui
new file mode 100644
index 00000000..25b2681b
--- /dev/null
+++ b/launcher/widgets/CustomCommands.ui
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CustomCommands</class>
+ <widget class="QWidget" name="CustomCommands">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>518</width>
+ <height>646</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="customCommandsGroupBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="title">
+ <string>Cus&amp;tom Commands</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>false</bool>
+ </property>
+ <layout class="QGridLayout" name="gridLayout_4">
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelPostExitCmd">
+ <property name="text">
+ <string>Post-exit command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="preLaunchCmdTextBox"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelPreLaunchCmd">
+ <property name="text">
+ <string>Pre-launch command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="postExitCmdTextBox"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelWrapperCmd">
+ <property name="text">
+ <string>Wrapper command:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="wrapperCmdTextBox"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="labelCustomCmdsDescription">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Pre-launch command runs before the instance launches and post-exit command runs after it exits.&lt;/p&gt;&lt;p&gt;Both will be run in MultiMC's working folder with extra environment variables:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;$INST_NAME - Name of the instance&lt;/li&gt;&lt;li&gt;$INST_ID - ID of the instance (its folder name)&lt;/li&gt;&lt;li&gt;$INST_DIR - absolute path of the instance&lt;/li&gt;&lt;li&gt;$INST_MC_DIR - absolute path of minecraft&lt;/li&gt;&lt;li&gt;$INST_JAVA - java binary used for launch&lt;/li&gt;&lt;li&gt;$INST_JAVA_ARGS - command-line parameters used for launch&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/widgets/DropLabel.cpp b/launcher/widgets/DropLabel.cpp
new file mode 100644
index 00000000..a900e57c
--- /dev/null
+++ b/launcher/widgets/DropLabel.cpp
@@ -0,0 +1,41 @@
+#include "DropLabel.h"
+
+#include <QMimeData>
+#include <QDropEvent>
+
+DropLabel::DropLabel(QWidget *parent) : QLabel(parent)
+{
+ setAcceptDrops(true);
+}
+
+void DropLabel::dragEnterEvent(QDragEnterEvent *event)
+{
+ event->acceptProposedAction();
+}
+
+void DropLabel::dragMoveEvent(QDragMoveEvent *event)
+{
+ event->acceptProposedAction();
+}
+
+void DropLabel::dragLeaveEvent(QDragLeaveEvent *event)
+{
+ event->accept();
+}
+
+void DropLabel::dropEvent(QDropEvent *event)
+{
+ const QMimeData *mimeData = event->mimeData();
+
+ if (!mimeData)
+ {
+ return;
+ }
+
+ if (mimeData->hasUrls()) {
+ auto urls = mimeData->urls();
+ emit droppedURLs(urls);
+ }
+
+ event->acceptProposedAction();
+}
diff --git a/launcher/widgets/DropLabel.h b/launcher/widgets/DropLabel.h
new file mode 100644
index 00000000..c5ca0bcc
--- /dev/null
+++ b/launcher/widgets/DropLabel.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <QLabel>
+
+class DropLabel : public QLabel
+{
+ Q_OBJECT
+
+public:
+ explicit DropLabel(QWidget *parent = nullptr);
+
+signals:
+ void droppedURLs(QList<QUrl> urls);
+
+protected:
+ void dropEvent(QDropEvent *event) override;
+ void dragEnterEvent(QDragEnterEvent *event) override;
+ void dragMoveEvent(QDragMoveEvent *event) override;
+ void dragLeaveEvent(QDragLeaveEvent *event) override;
+};
diff --git a/launcher/widgets/FocusLineEdit.cpp b/launcher/widgets/FocusLineEdit.cpp
new file mode 100644
index 00000000..b272100c
--- /dev/null
+++ b/launcher/widgets/FocusLineEdit.cpp
@@ -0,0 +1,25 @@
+#include "FocusLineEdit.h"
+#include <QDebug>
+
+FocusLineEdit::FocusLineEdit(QWidget *parent) : QLineEdit(parent)
+{
+ _selectOnMousePress = false;
+}
+
+void FocusLineEdit::focusInEvent(QFocusEvent *e)
+{
+ QLineEdit::focusInEvent(e);
+ selectAll();
+ _selectOnMousePress = true;
+}
+
+void FocusLineEdit::mousePressEvent(QMouseEvent *me)
+{
+ QLineEdit::mousePressEvent(me);
+ if (_selectOnMousePress)
+ {
+ selectAll();
+ _selectOnMousePress = false;
+ }
+ qDebug() << selectedText();
+}
diff --git a/launcher/widgets/FocusLineEdit.h b/launcher/widgets/FocusLineEdit.h
new file mode 100644
index 00000000..71b4f140
--- /dev/null
+++ b/launcher/widgets/FocusLineEdit.h
@@ -0,0 +1,17 @@
+#include <QLineEdit>
+
+class FocusLineEdit : public QLineEdit
+{
+ Q_OBJECT
+public:
+ FocusLineEdit(QWidget *parent);
+ virtual ~FocusLineEdit()
+ {
+ }
+
+protected:
+ void focusInEvent(QFocusEvent *e);
+ void mousePressEvent(QMouseEvent *me);
+
+ bool _selectOnMousePress;
+};
diff --git a/launcher/widgets/IconLabel.cpp b/launcher/widgets/IconLabel.cpp
new file mode 100644
index 00000000..bf1c2358
--- /dev/null
+++ b/launcher/widgets/IconLabel.cpp
@@ -0,0 +1,43 @@
+#include "IconLabel.h"
+
+#include <QStyle>
+#include <QStyleOption>
+#include <QLayout>
+#include <QPainter>
+#include <QRect>
+
+IconLabel::IconLabel(QWidget *parent, QIcon icon, QSize size)
+ : QWidget(parent), m_size(size), m_icon(icon)
+{
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+}
+
+QSize IconLabel::sizeHint() const
+{
+ return m_size;
+}
+
+void IconLabel::setIcon(QIcon icon)
+{
+ m_icon = icon;
+ update();
+}
+
+void IconLabel::paintEvent(QPaintEvent *)
+{
+ QPainter p(this);
+ QRect rect = contentsRect();
+ int width = rect.width();
+ int height = rect.height();
+ if(width < height)
+ {
+ rect.setHeight(width);
+ rect.translate(0, (height - width) / 2);
+ }
+ else if (width > height)
+ {
+ rect.setWidth(height);
+ rect.translate((width - height) / 2, 0);
+ }
+ m_icon.paint(&p, rect);
+}
diff --git a/launcher/widgets/IconLabel.h b/launcher/widgets/IconLabel.h
new file mode 100644
index 00000000..6d212c4c
--- /dev/null
+++ b/launcher/widgets/IconLabel.h
@@ -0,0 +1,26 @@
+#pragma once
+#include <QWidget>
+#include <QIcon>
+
+class QStyleOption;
+
+/**
+ * This is a trivial widget that paints a QIcon of the specified size.
+ */
+class IconLabel : public QWidget
+{
+ Q_OBJECT
+
+public:
+ /// Create a line separator. orientation is the orientation of the line.
+ explicit IconLabel(QWidget *parent, QIcon icon, QSize size);
+
+ virtual QSize sizeHint() const;
+ virtual void paintEvent(QPaintEvent *);
+
+ void setIcon(QIcon icon);
+
+private:
+ QSize m_size;
+ QIcon m_icon;
+};
diff --git a/launcher/widgets/InstanceCardWidget.ui b/launcher/widgets/InstanceCardWidget.ui
new file mode 100644
index 00000000..6eeeb076
--- /dev/null
+++ b/launcher/widgets/InstanceCardWidget.ui
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>InstanceCardWidget</class>
+ <widget class="QWidget" name="InstanceCardWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>473</width>
+ <height>118</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" rowspan="2">
+ <widget class="QToolButton" name="iconButton">
+ <property name="iconSize">
+ <size>
+ <width>80</width>
+ <height>80</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name:</string>
+ </property>
+ <property name="buddy">
+ <cstring>instNameTextBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLineEdit" name="instNameTextBox"/>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="groupLabel">
+ <property name="text">
+ <string>&amp;Group:</string>
+ </property>
+ <property name="buddy">
+ <cstring>groupBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QComboBox" name="groupBox">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/widgets/JavaSettingsWidget.cpp b/launcher/widgets/JavaSettingsWidget.cpp
new file mode 100644
index 00000000..7f53dc23
--- /dev/null
+++ b/launcher/widgets/JavaSettingsWidget.cpp
@@ -0,0 +1,428 @@
+#include "JavaSettingsWidget.h"
+#include <MultiMC.h>
+
+#include <java/JavaInstall.h>
+#include <dialogs/CustomMessageBox.h>
+#include <java/JavaUtils.h>
+#include <sys.h>
+
+#include <QVBoxLayout>
+#include <QGroupBox>
+#include <QSpinBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QToolButton>
+#include <widgets/VersionSelectWidget.h>
+#include <FileSystem.h>
+#include <QFileDialog>
+
+JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent)
+{
+ m_availableMemory = Sys::getSystemRam() / Sys::mebibyte;
+
+ goodIcon = MMC->getThemedIcon("status-good");
+ yellowIcon = MMC->getThemedIcon("status-yellow");
+ badIcon = MMC->getThemedIcon("status-bad");
+ setupUi();
+
+ connect(m_minMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int)));
+ connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int)));
+ connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int)));
+ connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaSettingsWidget::javaVersionSelected);
+ connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::on_javaBrowseBtn_clicked);
+ connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaSettingsWidget::javaPathEdited);
+ connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaSettingsWidget::on_javaStatusBtn_clicked);
+}
+
+void JavaSettingsWidget::setupUi()
+{
+ setObjectName(QStringLiteral("javaSettingsWidget"));
+ m_verticalLayout = new QVBoxLayout(this);
+ m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+
+ m_versionWidget = new VersionSelectWidget(this);
+ m_verticalLayout->addWidget(m_versionWidget);
+
+ m_horizontalLayout = new QHBoxLayout();
+ m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
+ m_javaPathTextBox = new QLineEdit(this);
+ m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox"));
+
+ m_horizontalLayout->addWidget(m_javaPathTextBox);
+
+ m_javaBrowseBtn = new QPushButton(this);
+ m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn"));
+
+ m_horizontalLayout->addWidget(m_javaBrowseBtn);
+
+ m_javaStatusBtn = new QToolButton(this);
+ m_javaStatusBtn->setIcon(yellowIcon);
+ m_horizontalLayout->addWidget(m_javaStatusBtn);
+
+ m_verticalLayout->addLayout(m_horizontalLayout);
+
+ m_memoryGroupBox = new QGroupBox(this);
+ m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox"));
+ m_gridLayout_2 = new QGridLayout(m_memoryGroupBox);
+ m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2"));
+
+ m_labelMinMem = new QLabel(m_memoryGroupBox);
+ m_labelMinMem->setObjectName(QStringLiteral("labelMinMem"));
+ m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1);
+
+ m_minMemSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox"));
+ m_minMemSpinBox->setSuffix(QStringLiteral(" MB"));
+ m_minMemSpinBox->setMinimum(128);
+ m_minMemSpinBox->setMaximum(m_availableMemory);
+ m_minMemSpinBox->setSingleStep(128);
+ m_labelMinMem->setBuddy(m_minMemSpinBox);
+ m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1);
+
+ m_labelMaxMem = new QLabel(m_memoryGroupBox);
+ m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem"));
+ m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1);
+
+ m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox"));
+ m_maxMemSpinBox->setSuffix(QStringLiteral(" MB"));
+ m_maxMemSpinBox->setMinimum(128);
+ m_maxMemSpinBox->setMaximum(m_availableMemory);
+ m_maxMemSpinBox->setSingleStep(128);
+ m_labelMaxMem->setBuddy(m_maxMemSpinBox);
+ m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1);
+
+ m_labelPermGen = new QLabel(m_memoryGroupBox);
+ m_labelPermGen->setObjectName(QStringLiteral("labelPermGen"));
+ m_labelPermGen->setText(QStringLiteral("PermGen:"));
+ m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1);
+ m_labelPermGen->setVisible(false);
+
+ m_permGenSpinBox = new QSpinBox(m_memoryGroupBox);
+ m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox"));
+ m_permGenSpinBox->setSuffix(QStringLiteral(" MB"));
+ m_permGenSpinBox->setMinimum(64);
+ m_permGenSpinBox->setMaximum(m_availableMemory);
+ m_permGenSpinBox->setSingleStep(8);
+ m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1);
+ m_permGenSpinBox->setVisible(false);
+
+ m_verticalLayout->addWidget(m_memoryGroupBox);
+
+ retranslate();
+}
+
+void JavaSettingsWidget::initialize()
+{
+ m_versionWidget->initialize(MMC->javalist().get());
+ m_versionWidget->setResizeOn(2);
+ auto s = MMC->settings();
+ // Memory
+ observedMinMemory = s->get("MinMemAlloc").toInt();
+ observedMaxMemory = s->get("MaxMemAlloc").toInt();
+ observedPermGenMemory = s->get("PermGen").toInt();
+ m_minMemSpinBox->setValue(observedMinMemory);
+ m_maxMemSpinBox->setValue(observedMaxMemory);
+ m_permGenSpinBox->setValue(observedPermGenMemory);
+}
+
+void JavaSettingsWidget::refresh()
+{
+ m_versionWidget->loadList();
+}
+
+JavaSettingsWidget::ValidationStatus JavaSettingsWidget::validate()
+{
+ switch(javaStatus)
+ {
+ default:
+ case JavaStatus::NotSet:
+ case JavaStatus::DoesNotExist:
+ case JavaStatus::DoesNotStart:
+ case JavaStatus::ReturnedInvalidData:
+ {
+ int button = CustomMessageBox::selectable(
+ this,
+ tr("No Java version selected"),
+ tr("You didn't select a Java version or selected something that doesn't work.\n"
+ "MultiMC will not be able to start Minecraft.\n"
+ "Do you wish to proceed without any Java?"
+ "\n\n"
+ "You can change the Java version in the settings later.\n"
+ ),
+ QMessageBox::Warning,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::NoButton
+ )->exec();
+ if(button == QMessageBox::No)
+ {
+ return ValidationStatus::Bad;
+ }
+ return ValidationStatus::JavaBad;
+ }
+ break;
+ case JavaStatus::Pending:
+ {
+ return ValidationStatus::Bad;
+ }
+ case JavaStatus::Good:
+ {
+ return ValidationStatus::AllOK;
+ }
+ }
+}
+
+QString JavaSettingsWidget::javaPath() const
+{
+ return m_javaPathTextBox->text();
+}
+
+int JavaSettingsWidget::maxHeapSize() const
+{
+ return m_maxMemSpinBox->value();
+}
+
+int JavaSettingsWidget::minHeapSize() const
+{
+ return m_minMemSpinBox->value();
+}
+
+bool JavaSettingsWidget::permGenEnabled() const
+{
+ return m_permGenSpinBox->isVisible();
+}
+
+int JavaSettingsWidget::permGenSize() const
+{
+ return m_permGenSpinBox->value();
+}
+
+void JavaSettingsWidget::memoryValueChanged(int)
+{
+ bool actuallyChanged = false;
+ int min = m_minMemSpinBox->value();
+ int max = m_maxMemSpinBox->value();
+ int permgen = m_permGenSpinBox->value();
+ QObject *obj = sender();
+ if (obj == m_minMemSpinBox && min != observedMinMemory)
+ {
+ observedMinMemory = min;
+ actuallyChanged = true;
+ if (min > max)
+ {
+ observedMaxMemory = min;
+ m_maxMemSpinBox->setValue(min);
+ }
+ }
+ else if (obj == m_maxMemSpinBox && max != observedMaxMemory)
+ {
+ observedMaxMemory = max;
+ actuallyChanged = true;
+ if (min > max)
+ {
+ observedMinMemory = max;
+ m_minMemSpinBox->setValue(max);
+ }
+ }
+ else if (obj == m_permGenSpinBox && permgen != observedPermGenMemory)
+ {
+ observedPermGenMemory = permgen;
+ actuallyChanged = true;
+ }
+ if(actuallyChanged)
+ {
+ checkJavaPathOnEdit(m_javaPathTextBox->text());
+ }
+}
+
+void JavaSettingsWidget::javaVersionSelected(BaseVersionPtr version)
+{
+ auto java = std::dynamic_pointer_cast<JavaInstall>(version);
+ if(!java)
+ {
+ return;
+ }
+ auto visible = java->id.requiresPermGen();
+ m_labelPermGen->setVisible(visible);
+ m_permGenSpinBox->setVisible(visible);
+ m_javaPathTextBox->setText(java->path);
+ checkJavaPath(java->path);
+}
+
+void JavaSettingsWidget::on_javaBrowseBtn_clicked()
+{
+ QString filter;
+#if defined Q_OS_WIN32
+ filter = "Java (javaw.exe)";
+#else
+ filter = "Java (java)";
+#endif
+ QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter);
+ if(raw_path.isEmpty())
+ {
+ return;
+ }
+ QString cooked_path = FS::NormalizePath(raw_path);
+ m_javaPathTextBox->setText(cooked_path);
+ checkJavaPath(cooked_path);
+}
+
+void JavaSettingsWidget::on_javaStatusBtn_clicked()
+{
+ QString text;
+ bool failed = false;
+ switch(javaStatus)
+ {
+ case JavaStatus::NotSet:
+ checkJavaPath(m_javaPathTextBox->text());
+ return;
+ case JavaStatus::DoesNotExist:
+ text += QObject::tr("The specified file either doesn't exist or is not a proper executable.");
+ failed = true;
+ break;
+ case JavaStatus::DoesNotStart:
+ {
+ text += QObject::tr("The specified java binary didn't start properly.<br />");
+ auto htmlError = m_result.errorLog;
+ if(!htmlError.isEmpty())
+ {
+ htmlError.replace('\n', "<br />");
+ text += QString("<font color=\"red\">%1</font>").arg(htmlError);
+ }
+ failed = true;
+ break;
+ }
+ case JavaStatus::ReturnedInvalidData:
+ {
+ text += QObject::tr("The specified java binary returned unexpected results:<br />");
+ auto htmlOut = m_result.outLog;
+ if(!htmlOut.isEmpty())
+ {
+ htmlOut.replace('\n', "<br />");
+ text += QString("<font color=\"red\">%1</font>").arg(htmlOut);
+ }
+ failed = true;
+ break;
+ }
+ case JavaStatus::Good:
+ text += QObject::tr("Java test succeeded!<br />Platform reported: %1<br />Java version "
+ "reported: %2<br />").arg(m_result.realPlatform, m_result.javaVersion.toString());
+ break;
+ case JavaStatus::Pending:
+ // TODO: abort here?
+ return;
+ }
+ CustomMessageBox::selectable(
+ this,
+ failed ? QObject::tr("Java test success") : QObject::tr("Java test failure"),
+ text,
+ failed ? QMessageBox::Critical : QMessageBox::Information
+ )->show();
+}
+
+void JavaSettingsWidget::setJavaStatus(JavaSettingsWidget::JavaStatus status)
+{
+ javaStatus = status;
+ switch(javaStatus)
+ {
+ case JavaStatus::Good:
+ m_javaStatusBtn->setIcon(goodIcon);
+ break;
+ case JavaStatus::NotSet:
+ case JavaStatus::Pending:
+ m_javaStatusBtn->setIcon(yellowIcon);
+ break;
+ default:
+ m_javaStatusBtn->setIcon(badIcon);
+ break;
+ }
+}
+
+void JavaSettingsWidget::javaPathEdited(const QString& path)
+{
+ checkJavaPathOnEdit(path);
+}
+
+void JavaSettingsWidget::checkJavaPathOnEdit(const QString& path)
+{
+ auto realPath = FS::ResolveExecutable(path);
+ QFileInfo pathInfo(realPath);
+ if (pathInfo.baseName().toLower().contains("java"))
+ {
+ checkJavaPath(path);
+ }
+ else
+ {
+ if(!m_checker)
+ {
+ setJavaStatus(JavaStatus::NotSet);
+ }
+ }
+}
+
+void JavaSettingsWidget::checkJavaPath(const QString &path)
+{
+ if(m_checker)
+ {
+ queuedCheck = path;
+ return;
+ }
+ auto realPath = FS::ResolveExecutable(path);
+ if(realPath.isNull())
+ {
+ setJavaStatus(JavaStatus::DoesNotExist);
+ return;
+ }
+ setJavaStatus(JavaStatus::Pending);
+ m_checker.reset(new JavaChecker());
+ m_checker->m_path = path;
+ m_checker->m_minMem = m_minMemSpinBox->value();
+ m_checker->m_maxMem = m_maxMemSpinBox->value();
+ if(m_permGenSpinBox->isVisible())
+ {
+ m_checker->m_permGen = m_permGenSpinBox->value();
+ }
+ connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished);
+ m_checker->performCheck();
+}
+
+void JavaSettingsWidget::checkFinished(JavaCheckResult result)
+{
+ m_result = result;
+ switch(result.validity)
+ {
+ case JavaCheckResult::Validity::Valid:
+ {
+ setJavaStatus(JavaStatus::Good);
+ break;
+ }
+ case JavaCheckResult::Validity::ReturnedInvalidData:
+ {
+ setJavaStatus(JavaStatus::ReturnedInvalidData);
+ break;
+ }
+ case JavaCheckResult::Validity::Errored:
+ {
+ setJavaStatus(JavaStatus::DoesNotStart);
+ break;
+ }
+ }
+ m_checker.reset();
+ if(!queuedCheck.isNull())
+ {
+ checkJavaPath(queuedCheck);
+ queuedCheck.clear();
+ }
+}
+
+void JavaSettingsWidget::retranslate()
+{
+ m_memoryGroupBox->setTitle(tr("Memory"));
+ m_maxMemSpinBox->setToolTip(tr("The maximum amount of memory Minecraft is allowed to use."));
+ m_labelMinMem->setText(tr("Minimum memory allocation:"));
+ m_labelMaxMem->setText(tr("Maximum memory allocation:"));
+ m_minMemSpinBox->setToolTip(tr("The amount of memory Minecraft is started with."));
+ m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes."));
+ m_javaBrowseBtn->setText(tr("Browse"));
+}
diff --git a/launcher/widgets/JavaSettingsWidget.h b/launcher/widgets/JavaSettingsWidget.h
new file mode 100644
index 00000000..0d280daf
--- /dev/null
+++ b/launcher/widgets/JavaSettingsWidget.h
@@ -0,0 +1,102 @@
+#pragma once
+#include <QWidget>
+
+#include <java/JavaChecker.h>
+#include <BaseVersion.h>
+#include <QObjectPtr.h>
+#include <QIcon>
+
+class QLineEdit;
+class VersionSelectWidget;
+class QSpinBox;
+class QPushButton;
+class QVBoxLayout;
+class QHBoxLayout;
+class QGroupBox;
+class QGridLayout;
+class QLabel;
+class QToolButton;
+
+/**
+ * This is a widget for all the Java settings dialogs and pages.
+ */
+class JavaSettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit JavaSettingsWidget(QWidget *parent);
+ virtual ~JavaSettingsWidget() {};
+
+ enum class JavaStatus
+ {
+ NotSet,
+ Pending,
+ Good,
+ DoesNotExist,
+ DoesNotStart,
+ ReturnedInvalidData
+ } javaStatus = JavaStatus::NotSet;
+
+ enum class ValidationStatus
+ {
+ Bad,
+ JavaBad,
+ AllOK
+ };
+
+ void refresh();
+ void initialize();
+ ValidationStatus validate();
+ void retranslate();
+
+ bool permGenEnabled() const;
+ int permGenSize() const;
+ int minHeapSize() const;
+ int maxHeapSize() const;
+ QString javaPath() const;
+
+
+protected slots:
+ void memoryValueChanged(int);
+ void javaPathEdited(const QString &path);
+ void javaVersionSelected(BaseVersionPtr version);
+ void on_javaBrowseBtn_clicked();
+ void on_javaStatusBtn_clicked();
+ void checkFinished(JavaCheckResult result);
+
+protected: /* methods */
+ void checkJavaPathOnEdit(const QString &path);
+ void checkJavaPath(const QString &path);
+ void setJavaStatus(JavaStatus status);
+ void setupUi();
+
+private: /* data */
+ VersionSelectWidget *m_versionWidget = nullptr;
+ QVBoxLayout *m_verticalLayout = nullptr;
+
+ QLineEdit * m_javaPathTextBox = nullptr;
+ QPushButton * m_javaBrowseBtn = nullptr;
+ QToolButton * m_javaStatusBtn = nullptr;
+ QHBoxLayout *m_horizontalLayout = nullptr;
+
+ QGroupBox *m_memoryGroupBox = nullptr;
+ QGridLayout *m_gridLayout_2 = nullptr;
+ QSpinBox *m_maxMemSpinBox = nullptr;
+ QLabel *m_labelMinMem = nullptr;
+ QLabel *m_labelMaxMem = nullptr;
+ QSpinBox *m_minMemSpinBox = nullptr;
+ QLabel *m_labelPermGen = nullptr;
+ QSpinBox *m_permGenSpinBox = nullptr;
+ QIcon goodIcon;
+ QIcon yellowIcon;
+ QIcon badIcon;
+
+ int observedMinMemory = 0;
+ int observedMaxMemory = 0;
+ int observedPermGenMemory = 0;
+ QString queuedCheck;
+ uint64_t m_availableMemory = 0ull;
+ shared_qobject_ptr<JavaChecker> m_checker;
+ JavaCheckResult m_result;
+};
diff --git a/launcher/widgets/LabeledToolButton.cpp b/launcher/widgets/LabeledToolButton.cpp
new file mode 100644
index 00000000..ab2d3278
--- /dev/null
+++ b/launcher/widgets/LabeledToolButton.cpp
@@ -0,0 +1,115 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QResizeEvent>
+#include <QStyleOption>
+#include "LabeledToolButton.h"
+#include <QApplication>
+#include <QDebug>
+
+/*
+ *
+ * Tool Button with a label on it, instead of the normal text rendering
+ *
+ */
+
+LabeledToolButton::LabeledToolButton(QWidget * parent)
+ : QToolButton(parent)
+ , m_label(new QLabel(this))
+{
+ //QToolButton::setText(" ");
+ m_label->setWordWrap(true);
+ m_label->setMouseTracking(false);
+ m_label->setAlignment(Qt::AlignCenter);
+ m_label->setTextInteractionFlags(Qt::NoTextInteraction);
+ // somehow, this makes word wrap work in the QLabel. yay.
+ //m_label->setMinimumWidth(100);
+}
+
+QString LabeledToolButton::text() const
+{
+ return m_label->text();
+}
+
+void LabeledToolButton::setText(const QString & text)
+{
+ m_label->setText(text);
+}
+
+void LabeledToolButton::setIcon(QIcon icon)
+{
+ m_icon = icon;
+ resetIcon();
+}
+
+
+/*!
+ \reimp
+*/
+QSize LabeledToolButton::sizeHint() const
+{
+ /*
+ Q_D(const QToolButton);
+ if (d->sizeHint.isValid())
+ return d->sizeHint;
+ */
+ ensurePolished();
+
+ int w = 0, h = 0;
+ QStyleOptionToolButton opt;
+ initStyleOption(&opt);
+ QSize sz =m_label->sizeHint();
+ w = sz.width();
+ h = sz.height();
+
+ opt.rect.setSize(QSize(w, h)); // PM_MenuButtonIndicator depends on the height
+ if (popupMode() == MenuButtonPopup)
+ w += style()->pixelMetric(QStyle::PM_MenuButtonIndicator, &opt, this);
+
+ QSize rawSize = style()->sizeFromContents(QStyle::CT_ToolButton, &opt, QSize(w, h), this);
+ QSize sizeHint = rawSize.expandedTo(QApplication::globalStrut());
+ return sizeHint;
+}
+
+
+
+void LabeledToolButton::resizeEvent(QResizeEvent * event)
+{
+ m_label->setGeometry(QRect(4, 4, width()-8, height()-8));
+ if(!m_icon.isNull())
+ {
+ resetIcon();
+ }
+ QWidget::resizeEvent(event);
+}
+
+void LabeledToolButton::resetIcon()
+{
+ auto iconSz = m_icon.actualSize(QSize(160, 80));
+ float w = iconSz.width();
+ float h = iconSz.height();
+ float ar = w/h;
+ // FIXME: hardcoded max size of 160x80
+ int newW = 80 * ar;
+ if(newW > 160)
+ newW = 160;
+ QSize newSz (newW, 80);
+ auto pixmap = m_icon.pixmap(newSz);
+ m_label->setPixmap(pixmap);
+ m_label->setMinimumHeight(80);
+ m_label->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred );
+}
diff --git a/launcher/widgets/LabeledToolButton.h b/launcher/widgets/LabeledToolButton.h
new file mode 100644
index 00000000..51f99e9b
--- /dev/null
+++ b/launcher/widgets/LabeledToolButton.h
@@ -0,0 +1,40 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QPushButton>
+#include <QToolButton>
+
+class QLabel;
+
+class LabeledToolButton : public QToolButton
+{
+ Q_OBJECT
+
+ QLabel * m_label;
+ QIcon m_icon;
+
+public:
+ LabeledToolButton(QWidget * parent = 0);
+
+ QString text() const;
+ void setText(const QString & text);
+ void setIcon(QIcon icon);
+ virtual QSize sizeHint() const;
+protected:
+ void resizeEvent(QResizeEvent * event);
+ void resetIcon();
+};
diff --git a/launcher/widgets/LanguageSelectionWidget.cpp b/launcher/widgets/LanguageSelectionWidget.cpp
new file mode 100644
index 00000000..8d23bdc5
--- /dev/null
+++ b/launcher/widgets/LanguageSelectionWidget.cpp
@@ -0,0 +1,66 @@
+#include "LanguageSelectionWidget.h"
+
+#include <QVBoxLayout>
+#include <QTreeView>
+#include <QHeaderView>
+#include <QLabel>
+#include "MultiMC.h"
+#include "translations/TranslationsModel.h"
+
+LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) :
+ QWidget(parent)
+{
+ verticalLayout = new QVBoxLayout(this);
+ verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ languageView = new QTreeView(this);
+ languageView->setObjectName(QStringLiteral("languageView"));
+ languageView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ languageView->setAlternatingRowColors(true);
+ languageView->setRootIsDecorated(false);
+ languageView->setItemsExpandable(false);
+ languageView->setWordWrap(true);
+ languageView->header()->setCascadingSectionResizes(true);
+ languageView->header()->setStretchLastSection(false);
+ verticalLayout->addWidget(languageView);
+ helpUsLabel = new QLabel(this);
+ helpUsLabel->setObjectName(QStringLiteral("helpUsLabel"));
+ helpUsLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
+ helpUsLabel->setOpenExternalLinks(true);
+ helpUsLabel->setWordWrap(true);
+ verticalLayout->addWidget(helpUsLabel);
+
+ auto translations = MMC->translations();
+ auto index = translations->selectedIndex();
+ languageView->setModel(translations.get());
+ languageView->setCurrentIndex(index);
+ languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
+ connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged);
+ verticalLayout->setContentsMargins(0,0,0,0);
+}
+
+QString LanguageSelectionWidget::getSelectedLanguageKey() const
+{
+ auto translations = MMC->translations();
+ return translations->data(languageView->currentIndex(), Qt::UserRole).toString();
+}
+
+void LanguageSelectionWidget::retranslate()
+{
+ QString text = tr("Don't see your language or the quality is poor?<br/><a href=\"%1\">Help us with translations!</a>")
+ .arg("https://github.com/MultiMC/MultiMC5/wiki/Translating-MultiMC");
+ helpUsLabel->setText(text);
+
+}
+
+void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous)
+{
+ if (current == previous)
+ {
+ return;
+ }
+ auto translations = MMC->translations();
+ QString key = translations->data(current, Qt::UserRole).toString();
+ translations->selectLanguage(key);
+ translations->updateLanguage(key);
+}
diff --git a/launcher/widgets/LanguageSelectionWidget.h b/launcher/widgets/LanguageSelectionWidget.h
new file mode 100644
index 00000000..e65936db
--- /dev/null
+++ b/launcher/widgets/LanguageSelectionWidget.h
@@ -0,0 +1,41 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+class QVBoxLayout;
+class QTreeView;
+class QLabel;
+
+class LanguageSelectionWidget: public QWidget
+{
+ Q_OBJECT
+public:
+ explicit LanguageSelectionWidget(QWidget *parent = 0);
+ virtual ~LanguageSelectionWidget() { };
+
+ QString getSelectedLanguageKey() const;
+ void retranslate();
+
+protected slots:
+ void languageRowChanged(const QModelIndex &current, const QModelIndex &previous);
+
+private:
+ QVBoxLayout *verticalLayout = nullptr;
+ QTreeView *languageView = nullptr;
+ QLabel *helpUsLabel = nullptr;
+};
diff --git a/launcher/widgets/LineSeparator.cpp b/launcher/widgets/LineSeparator.cpp
new file mode 100644
index 00000000..d03e6762
--- /dev/null
+++ b/launcher/widgets/LineSeparator.cpp
@@ -0,0 +1,37 @@
+#include "LineSeparator.h"
+
+#include <QStyle>
+#include <QStyleOption>
+#include <QLayout>
+#include <QPainter>
+
+void LineSeparator::initStyleOption(QStyleOption *option) const
+{
+ option->initFrom(this);
+ // in a horizontal layout, the line is vertical (and vice versa)
+ if (m_orientation == Qt::Vertical)
+ option->state |= QStyle::State_Horizontal;
+}
+
+LineSeparator::LineSeparator(QWidget *parent, Qt::Orientation orientation)
+ : QWidget(parent), m_orientation(orientation)
+{
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+}
+
+QSize LineSeparator::sizeHint() const
+{
+ QStyleOption opt;
+ initStyleOption(&opt);
+ const int extent =
+ style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, &opt, parentWidget());
+ return QSize(extent, extent);
+}
+
+void LineSeparator::paintEvent(QPaintEvent *)
+{
+ QPainter p(this);
+ QStyleOption opt;
+ initStyleOption(&opt);
+ style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p, parentWidget());
+}
diff --git a/launcher/widgets/LineSeparator.h b/launcher/widgets/LineSeparator.h
new file mode 100644
index 00000000..22927b68
--- /dev/null
+++ b/launcher/widgets/LineSeparator.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <QWidget>
+
+class QStyleOption;
+
+class LineSeparator : public QWidget
+{
+ Q_OBJECT
+
+public:
+ /// Create a line separator. orientation is the orientation of the line.
+ explicit LineSeparator(QWidget *parent, Qt::Orientation orientation = Qt::Horizontal);
+ QSize sizeHint() const;
+ void paintEvent(QPaintEvent *);
+ void initStyleOption(QStyleOption *option) const;
+private:
+ Qt::Orientation m_orientation = Qt::Horizontal;
+};
diff --git a/launcher/widgets/LogView.cpp b/launcher/widgets/LogView.cpp
new file mode 100644
index 00000000..26a2a527
--- /dev/null
+++ b/launcher/widgets/LogView.cpp
@@ -0,0 +1,144 @@
+#include "LogView.h"
+#include <QTextBlock>
+#include <QScrollBar>
+
+LogView::LogView(QWidget* parent) : QPlainTextEdit(parent)
+{
+ setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ m_defaultFormat = new QTextCharFormat(currentCharFormat());
+}
+
+LogView::~LogView()
+{
+ delete m_defaultFormat;
+}
+
+void LogView::setWordWrap(bool wrapping)
+{
+ if(wrapping)
+ {
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setLineWrapMode(QPlainTextEdit::WidgetWidth);
+ }
+ else
+ {
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ setLineWrapMode(QPlainTextEdit::NoWrap);
+ }
+}
+
+void LogView::setModel(QAbstractItemModel* model)
+{
+ if(m_model)
+ {
+ disconnect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate);
+ disconnect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted);
+ disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted);
+ disconnect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved);
+ }
+ m_model = model;
+ if(m_model)
+ {
+ connect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate);
+ connect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted);
+ connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted);
+ connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved);
+ connect(m_model, &QAbstractItemModel::destroyed, this, &LogView::modelDestroyed);
+ }
+ repopulate();
+}
+
+QAbstractItemModel * LogView::model() const
+{
+ return m_model;
+}
+
+void LogView::modelDestroyed(QObject* model)
+{
+ if(m_model == model)
+ {
+ setModel(nullptr);
+ }
+}
+
+void LogView::repopulate()
+{
+ auto doc = document();
+ doc->clear();
+ if(!m_model)
+ {
+ return;
+ }
+ rowsInserted(QModelIndex(), 0, m_model->rowCount() - 1);
+}
+
+void LogView::rowsAboutToBeInserted(const QModelIndex& parent, int first, int last)
+{
+ Q_UNUSED(parent)
+ Q_UNUSED(first)
+ Q_UNUSED(last)
+ QScrollBar *bar = verticalScrollBar();
+ int max_bar = bar->maximum();
+ int val_bar = bar->value();
+ if (m_scroll)
+ {
+ m_scroll = (max_bar - val_bar) <= 1;
+ }
+ else
+ {
+ m_scroll = val_bar == max_bar;
+ }
+}
+
+void LogView::rowsInserted(const QModelIndex& parent, int first, int last)
+{
+ for(int i = first; i <= last; i++)
+ {
+ auto idx = m_model->index(i, 0, parent);
+ auto text = m_model->data(idx, Qt::DisplayRole).toString();
+ QTextCharFormat format(*m_defaultFormat);
+ auto font = m_model->data(idx, Qt::FontRole);
+ if(font.isValid())
+ {
+ format.setFont(font.value<QFont>());
+ }
+ auto fg = m_model->data(idx, Qt::TextColorRole);
+ if(fg.isValid())
+ {
+ format.setForeground(fg.value<QColor>());
+ }
+ auto bg = m_model->data(idx, Qt::BackgroundRole);
+ if(bg.isValid())
+ {
+ format.setBackground(bg.value<QColor>());
+ }
+ auto workCursor = textCursor();
+ workCursor.movePosition(QTextCursor::End);
+ workCursor.insertText(text, format);
+ workCursor.insertBlock();
+ }
+ if(m_scroll && !m_scrolling)
+ {
+ m_scrolling = true;
+ QMetaObject::invokeMethod( this, "scrollToBottom", Qt::QueuedConnection);
+ }
+}
+
+void LogView::rowsRemoved(const QModelIndex& parent, int first, int last)
+{
+ // TODO: some day... maybe
+ Q_UNUSED(parent)
+ Q_UNUSED(first)
+ Q_UNUSED(last)
+}
+
+void LogView::scrollToBottom()
+{
+ m_scrolling = false;
+ verticalScrollBar()->setSliderPosition(verticalScrollBar()->maximum());
+}
+
+void LogView::findNext(const QString& what, bool reverse)
+{
+ find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0));
+}
diff --git a/launcher/widgets/LogView.h b/launcher/widgets/LogView.h
new file mode 100644
index 00000000..3143360a
--- /dev/null
+++ b/launcher/widgets/LogView.h
@@ -0,0 +1,36 @@
+#pragma once
+#include <QPlainTextEdit>
+#include <QAbstractItemView>
+
+class QAbstractItemModel;
+
+class LogView: public QPlainTextEdit
+{
+ Q_OBJECT
+public:
+ explicit LogView(QWidget *parent = nullptr);
+ virtual ~LogView();
+
+ virtual void setModel(QAbstractItemModel *model);
+ QAbstractItemModel *model() const;
+
+public slots:
+ void setWordWrap(bool wrapping);
+ void findNext(const QString & what, bool reverse);
+ void scrollToBottom();
+
+protected slots:
+ void repopulate();
+ // note: this supports only appending
+ void rowsInserted(const QModelIndex &parent, int first, int last);
+ void rowsAboutToBeInserted(const QModelIndex &parent, int first, int last);
+ // note: this supports only removing from front
+ void rowsRemoved(const QModelIndex &parent, int first, int last);
+ void modelDestroyed(QObject * model);
+
+protected:
+ QAbstractItemModel *m_model = nullptr;
+ QTextCharFormat *m_defaultFormat = nullptr;
+ bool m_scroll = false;
+ bool m_scrolling = false;
+};
diff --git a/launcher/widgets/MCModInfoFrame.cpp b/launcher/widgets/MCModInfoFrame.cpp
new file mode 100644
index 00000000..5b1f6230
--- /dev/null
+++ b/launcher/widgets/MCModInfoFrame.cpp
@@ -0,0 +1,167 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QMessageBox>
+#include <QtGui>
+
+#include "MCModInfoFrame.h"
+#include "ui_MCModInfoFrame.h"
+#include "dialogs/CustomMessageBox.h"
+
+void MCModInfoFrame::updateWithMod(Mod &m)
+{
+ if (m.type() == m.MOD_FOLDER)
+ {
+ clear();
+ return;
+ }
+
+ QString text = "";
+ QString name = "";
+ if (m.name().isEmpty())
+ name = m.mmc_id();
+ else
+ name = m.name();
+
+ if (m.homeurl().isEmpty())
+ text = name;
+ else
+ text = "<a href=\"" + m.homeurl() + "\">" + name + "</a>";
+ if (!m.authors().isEmpty())
+ text += " by " + m.authors().join(", ");
+
+ setModText(text);
+
+ if (m.description().isEmpty())
+ {
+ setModDescription(QString());
+ }
+ else
+ {
+ setModDescription(m.description());
+ }
+}
+
+void MCModInfoFrame::clear()
+{
+ setModText(QString());
+ setModDescription(QString());
+}
+
+MCModInfoFrame::MCModInfoFrame(QWidget *parent) :
+ QFrame(parent),
+ ui(new Ui::MCModInfoFrame)
+{
+ ui->setupUi(this);
+ ui->label_ModDescription->setHidden(true);
+ ui->label_ModText->setHidden(true);
+ updateHiddenState();
+}
+
+MCModInfoFrame::~MCModInfoFrame()
+{
+ delete ui;
+}
+
+void MCModInfoFrame::updateHiddenState()
+{
+ if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden())
+ {
+ setHidden(true);
+ }
+ else
+ {
+ setHidden(false);
+ }
+}
+
+void MCModInfoFrame::setModText(QString text)
+{
+ if(text.isEmpty())
+ {
+ ui->label_ModText->setHidden(true);
+ }
+ else
+ {
+ ui->label_ModText->setText(text);
+ ui->label_ModText->setHidden(false);
+ }
+ updateHiddenState();
+}
+
+void MCModInfoFrame::setModDescription(QString text)
+{
+ if(text.isEmpty())
+ {
+ ui->label_ModDescription->setHidden(true);
+ updateHiddenState();
+ return;
+ }
+ else
+ {
+ ui->label_ModDescription->setHidden(false);
+ updateHiddenState();
+ }
+ ui->label_ModDescription->setToolTip("");
+ QString intermediatetext = text.trimmed();
+ bool prev(false);
+ QChar rem('\n');
+ QString finaltext;
+ finaltext.reserve(intermediatetext.size());
+ foreach(const QChar& c, intermediatetext)
+ {
+ if(c == rem && prev){
+ continue;
+ }
+ prev = c == rem;
+ finaltext += c;
+ }
+ QString labeltext;
+ labeltext.reserve(300);
+ if(finaltext.length() > 290)
+ {
+ ui->label_ModDescription->setOpenExternalLinks(false);
+ ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText);
+ desc = text;
+ // This allows injecting HTML here.
+ labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
+ QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this, &MCModInfoFrame::modDescEllipsisHandler);
+ }
+ else
+ {
+ ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText);
+ labeltext.append(finaltext);
+ }
+ ui->label_ModDescription->setText(labeltext);
+}
+
+void MCModInfoFrame::modDescEllipsisHandler(const QString &link)
+{
+ if(!currentBox)
+ {
+ currentBox = CustomMessageBox::selectable(this, QString(), desc);
+ connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed);
+ currentBox->show();
+ }
+ else
+ {
+ currentBox->setText(desc);
+ }
+}
+
+void MCModInfoFrame::boxClosed(int result)
+{
+ currentBox = nullptr;
+}
diff --git a/launcher/widgets/MCModInfoFrame.h b/launcher/widgets/MCModInfoFrame.h
new file mode 100644
index 00000000..0b7ef537
--- /dev/null
+++ b/launcher/widgets/MCModInfoFrame.h
@@ -0,0 +1,52 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QFrame>
+#include "minecraft/mod/Mod.h"
+
+namespace Ui
+{
+class MCModInfoFrame;
+}
+
+class MCModInfoFrame : public QFrame
+{
+ Q_OBJECT
+
+public:
+ explicit MCModInfoFrame(QWidget *parent = 0);
+ ~MCModInfoFrame();
+
+ void setModText(QString text);
+ void setModDescription(QString text);
+
+ void updateWithMod(Mod &m);
+ void clear();
+
+public slots:
+ void modDescEllipsisHandler(const QString& link );
+ void boxClosed(int result);
+
+private:
+ void updateHiddenState();
+
+private:
+ Ui::MCModInfoFrame *ui;
+ QString desc;
+ class QMessageBox * currentBox = nullptr;
+};
+
diff --git a/launcher/widgets/MCModInfoFrame.ui b/launcher/widgets/MCModInfoFrame.ui
new file mode 100644
index 00000000..5ef33379
--- /dev/null
+++ b/launcher/widgets/MCModInfoFrame.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MCModInfoFrame</class>
+ <widget class="QFrame" name="MCModInfoFrame">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>527</width>
+ <height>113</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>120</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_ModText">
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_ModDescription">
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="openExternalLinks">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/widgets/ModListView.cpp b/launcher/widgets/ModListView.cpp
new file mode 100644
index 00000000..c8ccd292
--- /dev/null
+++ b/launcher/widgets/ModListView.cpp
@@ -0,0 +1,66 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ModListView.h"
+#include <QHeaderView>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QDrag>
+#include <QRect>
+
+ModListView::ModListView ( QWidget* parent )
+ :QTreeView ( parent )
+{
+ setAllColumnsShowFocus ( true );
+ setExpandsOnDoubleClick ( false );
+ setRootIsDecorated ( false );
+ setSortingEnabled ( true );
+ setAlternatingRowColors ( true );
+ setSelectionMode ( QAbstractItemView::ExtendedSelection );
+ setHeaderHidden ( false );
+ setSelectionBehavior(QAbstractItemView::SelectRows);
+ setVerticalScrollBarPolicy ( Qt::ScrollBarAlwaysOn );
+ setHorizontalScrollBarPolicy ( Qt::ScrollBarAsNeeded );
+ setDropIndicatorShown(true);
+ setDragEnabled(true);
+ setDragDropMode(QAbstractItemView::DropOnly);
+ viewport()->setAcceptDrops(true);
+}
+
+void ModListView::setModel ( QAbstractItemModel* model )
+{
+ QTreeView::setModel ( model );
+ auto head = header();
+ head->setStretchLastSection(false);
+ // HACK: this is true for the checkbox column of mod lists
+ auto string = model->headerData(0,head->orientation()).toString();
+ if(head->count() < 1)
+ {
+ return;
+ }
+ if(!string.size())
+ {
+ head->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ head->setSectionResizeMode(1, QHeaderView::Stretch);
+ for(int i = 2; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+ else
+ {
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ for(int i = 1; i < head->count(); i++)
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+}
diff --git a/launcher/widgets/ModListView.h b/launcher/widgets/ModListView.h
new file mode 100644
index 00000000..881e092f
--- /dev/null
+++ b/launcher/widgets/ModListView.h
@@ -0,0 +1,25 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QTreeView>
+
+class ModListView: public QTreeView
+{
+ Q_OBJECT
+public:
+ explicit ModListView ( QWidget* parent = 0 );
+ virtual void setModel ( QAbstractItemModel* model );
+};
diff --git a/launcher/widgets/PageContainer.cpp b/launcher/widgets/PageContainer.cpp
new file mode 100644
index 00000000..05a5e6b4
--- /dev/null
+++ b/launcher/widgets/PageContainer.cpp
@@ -0,0 +1,239 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PageContainer.h"
+
+#include <QStackedLayout>
+#include <QPushButton>
+#include <QSortFilterProxyModel>
+#include <QUrl>
+#include <QStyledItemDelegate>
+#include <QListView>
+#include <QLineEdit>
+#include <QLabel>
+#include <QDialogButtonBox>
+#include <QGridLayout>
+
+#include "MultiMC.h"
+#include "settings/SettingsObject.h"
+#include "widgets/IconLabel.h"
+#include "PageContainer_p.h"
+#include <MultiMC.h>
+#include <DesktopServices.h>
+
+class PageEntryFilterModel : public QSortFilterProxyModel
+{
+public:
+ explicit PageEntryFilterModel(QObject *parent = 0) : QSortFilterProxyModel(parent)
+ {
+ }
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+ {
+ const QString pattern = filterRegExp().pattern();
+ const auto model = static_cast<PageModel *>(sourceModel());
+ const auto page = model->pages().at(sourceRow);
+ if (!page->shouldDisplay())
+ return false;
+ // Regular contents check, then check page-filter.
+ return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
+ }
+};
+
+PageContainer::PageContainer(BasePageProvider *pageProvider, QString defaultId,
+ QWidget *parent)
+ : QWidget(parent)
+{
+ createUI();
+ m_model = new PageModel(this);
+ m_proxyModel = new PageEntryFilterModel(this);
+ int counter = 0;
+ auto pages = pageProvider->getPages();
+ for (auto page : pages)
+ {
+ page->stackIndex = m_pageStack->addWidget(dynamic_cast<QWidget *>(page));
+ page->listIndex = counter;
+ page->setParentContainer(this);
+ counter++;
+ }
+ m_model->setPages(pages);
+
+ m_proxyModel->setSourceModel(m_model);
+ m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+
+ m_pageList->setIconSize(QSize(pageIconSize, pageIconSize));
+ m_pageList->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_pageList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ m_pageList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ m_pageList->setModel(m_proxyModel);
+ connect(m_pageList->selectionModel(), SIGNAL(currentRowChanged(QModelIndex, QModelIndex)),
+ this, SLOT(currentChanged(QModelIndex)));
+ m_pageStack->setStackingMode(QStackedLayout::StackOne);
+ m_pageList->setFocus();
+ selectPage(defaultId);
+}
+
+bool PageContainer::selectPage(QString pageId)
+{
+ // now find what we want to have selected...
+ auto page = m_model->findPageEntryById(pageId);
+ QModelIndex index;
+ if (page)
+ {
+ index = m_proxyModel->mapFromSource(m_model->index(page->listIndex));
+ }
+ if(!index.isValid())
+ {
+ index = m_proxyModel->index(0, 0);
+ }
+ if (index.isValid())
+ {
+ m_pageList->setCurrentIndex(index);
+ return true;
+ }
+ return false;
+}
+
+void PageContainer::refreshContainer()
+{
+ m_proxyModel->invalidate();
+ if(!m_currentPage->shouldDisplay())
+ {
+ auto index = m_proxyModel->index(0, 0);
+ if(index.isValid())
+ {
+ m_pageList->setCurrentIndex(index);
+ }
+ else
+ {
+ // FIXME: unhandled corner case: what to do when there's no page to select?
+ }
+ }
+}
+
+void PageContainer::createUI()
+{
+ m_pageStack = new QStackedLayout;
+ m_pageList = new PageView;
+ m_header = new QLabel();
+ m_iconHeader = new IconLabel(this, QIcon(), QSize(24, 24));
+
+ QFont headerLabelFont = m_header->font();
+ headerLabelFont.setBold(true);
+ const int pointSize = headerLabelFont.pointSize();
+ if (pointSize > 0)
+ headerLabelFont.setPointSize(pointSize + 2);
+ m_header->setFont(headerLabelFont);
+
+ QHBoxLayout *headerHLayout = new QHBoxLayout;
+ const int leftMargin = MMC->style()->pixelMetric(QStyle::PM_LayoutLeftMargin);
+ headerHLayout->addSpacerItem(new QSpacerItem(leftMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored));
+ headerHLayout->addWidget(m_header);
+ headerHLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored));
+ headerHLayout->addWidget(m_iconHeader);
+ const int rightMargin = MMC->style()->pixelMetric(QStyle::PM_LayoutRightMargin);
+ headerHLayout->addSpacerItem(new QSpacerItem(rightMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored));
+ headerHLayout->setContentsMargins(0, 6, 0, 0);
+
+ m_pageStack->setMargin(0);
+ m_pageStack->addWidget(new QWidget(this));
+
+ m_layout = new QGridLayout;
+ m_layout->addLayout(headerHLayout, 0, 1, 1, 1);
+ m_layout->addWidget(m_pageList, 0, 0, 2, 1);
+ m_layout->addLayout(m_pageStack, 1, 1, 1, 1);
+ m_layout->setColumnStretch(1, 4);
+ m_layout->setContentsMargins(0,0,0,6);
+ setLayout(m_layout);
+}
+
+void PageContainer::addButtons(QWidget *buttons)
+{
+ m_layout->addWidget(buttons, 2, 0, 1, 2);
+}
+
+void PageContainer::addButtons(QLayout *buttons)
+{
+ m_layout->addLayout(buttons, 2, 0, 1, 2);
+}
+
+void PageContainer::showPage(int row)
+{
+ if (m_currentPage)
+ {
+ m_currentPage->closed();
+ }
+ if (row != -1)
+ {
+ m_currentPage = m_model->pages().at(row);
+ }
+ else
+ {
+ m_currentPage = nullptr;
+ }
+ if (m_currentPage)
+ {
+ m_pageStack->setCurrentIndex(m_currentPage->stackIndex);
+ m_header->setText(m_currentPage->displayName());
+ m_iconHeader->setIcon(m_currentPage->icon());
+ m_currentPage->opened();
+ }
+ else
+ {
+ m_pageStack->setCurrentIndex(0);
+ m_header->setText(QString());
+ m_iconHeader->setIcon(MMC->getThemedIcon("bug"));
+ }
+}
+
+void PageContainer::help()
+{
+ if (m_currentPage)
+ {
+ QString pageId = m_currentPage->helpPage();
+ if (pageId.isEmpty())
+ return;
+ DesktopServices::openUrl(QUrl("https://github.com/MultiMC/MultiMC5/wiki/" + pageId));
+ }
+}
+
+void PageContainer::currentChanged(const QModelIndex &current)
+{
+ showPage(current.isValid() ? m_proxyModel->mapToSource(current).row() : -1);
+}
+
+bool PageContainer::prepareToClose()
+{
+ if(!saveAll())
+ {
+ return false;
+ }
+ if (m_currentPage)
+ {
+ m_currentPage->closed();
+ }
+ return true;
+}
+
+bool PageContainer::saveAll()
+{
+ for (auto page : m_model->pages())
+ {
+ if (!page->apply())
+ return false;
+ }
+ return true;
+}
diff --git a/launcher/widgets/PageContainer.h b/launcher/widgets/PageContainer.h
new file mode 100644
index 00000000..976d34e9
--- /dev/null
+++ b/launcher/widgets/PageContainer.h
@@ -0,0 +1,89 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+#include <QModelIndex>
+
+#include "pages/BasePageProvider.h"
+#include "pages/BasePageContainer.h"
+
+class QLayout;
+class IconLabel;
+class QSortFilterProxyModel;
+class PageModel;
+class QLabel;
+class QListView;
+class QLineEdit;
+class QStackedLayout;
+class QGridLayout;
+
+class PageContainer : public QWidget, public BasePageContainer
+{
+ Q_OBJECT
+public:
+ explicit PageContainer(BasePageProvider *pageProvider, QString defaultId = QString(),
+ QWidget *parent = 0);
+ virtual ~PageContainer() {}
+
+ void addButtons(QWidget * buttons);
+ void addButtons(QLayout * buttons);
+ /*
+ * Save any unsaved state and prepare to be closed.
+ * @return true if everything can be saved, false if there is something that requires attention
+ */
+ bool prepareToClose();
+ bool saveAll();
+
+ /* request close - used by individual pages */
+ bool requestClose() override
+ {
+ if(m_container)
+ {
+ return m_container->requestClose();
+ }
+ return false;
+ }
+
+ virtual bool selectPage(QString pageId) override;
+
+ void refreshContainer() override;
+ virtual void setParentContainer(BasePageContainer * container)
+ {
+ m_container = container;
+ };
+
+private:
+ void createUI();
+
+public slots:
+ void help();
+
+private slots:
+ void currentChanged(const QModelIndex &current);
+ void showPage(int row);
+
+private:
+ BasePageContainer * m_container = nullptr;
+ BasePage * m_currentPage = 0;
+ QSortFilterProxyModel *m_proxyModel;
+ PageModel *m_model;
+ QStackedLayout *m_pageStack;
+ QListView *m_pageList;
+ QLabel *m_header;
+ IconLabel *m_iconHeader;
+ QGridLayout *m_layout;
+};
diff --git a/launcher/widgets/PageContainer_p.h b/launcher/widgets/PageContainer_p.h
new file mode 100644
index 00000000..da1a66f4
--- /dev/null
+++ b/launcher/widgets/PageContainer_p.h
@@ -0,0 +1,123 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QListView>
+#include <QStyledItemDelegate>
+#include <QEvent>
+#include <QScrollBar>
+
+class BasePage;
+const int pageIconSize = 24;
+
+class PageViewDelegate : public QStyledItemDelegate
+{
+public:
+ PageViewDelegate(QObject *parent) : QStyledItemDelegate(parent)
+ {
+ }
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
+ {
+ QSize size = QStyledItemDelegate::sizeHint(option, index);
+ size.setHeight(qMax(size.height(), 32));
+ return size;
+ }
+};
+
+class PageModel : public QAbstractListModel
+{
+public:
+ PageModel(QObject *parent = 0) : QAbstractListModel(parent)
+ {
+ QPixmap empty(pageIconSize, pageIconSize);
+ empty.fill(Qt::transparent);
+ m_emptyIcon = QIcon(empty);
+ }
+ virtual ~PageModel() {}
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const
+ {
+ return parent.isValid() ? 0 : m_pages.size();
+ }
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
+ {
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return m_pages.at(index.row())->displayName();
+ case Qt::DecorationRole:
+ {
+ QIcon icon = m_pages.at(index.row())->icon();
+ if (icon.isNull())
+ icon = m_emptyIcon;
+ // HACK: fixes icon stretching on windows. TODO: report Qt bug for this
+ return QIcon(icon.pixmap(QSize(48,48)));
+ }
+ }
+ return QVariant();
+ }
+
+ void setPages(const QList<BasePage *> &pages)
+ {
+ beginResetModel();
+ m_pages = pages;
+ endResetModel();
+ }
+ const QList<BasePage *> &pages() const
+ {
+ return m_pages;
+ }
+
+ BasePage * findPageEntryById(QString id)
+ {
+ for(auto page: m_pages)
+ {
+ if (page->id() == id)
+ return page;
+ }
+ return nullptr;
+ }
+
+ QList<BasePage *> m_pages;
+ QIcon m_emptyIcon;
+};
+
+class PageView : public QListView
+{
+public:
+ PageView(QWidget *parent = 0) : QListView(parent)
+ {
+ setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding);
+ setItemDelegate(new PageViewDelegate(this));
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ }
+
+ virtual QSize sizeHint() const
+ {
+ int width = sizeHintForColumn(0) + frameWidth() * 2 + 5;
+ if (verticalScrollBar()->isVisible())
+ width += verticalScrollBar()->width();
+ return QSize(width, 100);
+ }
+
+ virtual bool eventFilter(QObject *obj, QEvent *event)
+ {
+ if (obj == verticalScrollBar() &&
+ (event->type() == QEvent::Show || event->type() == QEvent::Hide))
+ updateGeometry();
+ return QListView::eventFilter(obj, event);
+ }
+};
diff --git a/launcher/widgets/ProgressWidget.cpp b/launcher/widgets/ProgressWidget.cpp
new file mode 100644
index 00000000..911e555d
--- /dev/null
+++ b/launcher/widgets/ProgressWidget.cpp
@@ -0,0 +1,73 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "ProgressWidget.h"
+#include <QProgressBar>
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QEventLoop>
+
+#include "tasks/Task.h"
+
+ProgressWidget::ProgressWidget(QWidget *parent)
+ : QWidget(parent)
+{
+ m_label = new QLabel(this);
+ m_label->setWordWrap(true);
+ m_bar = new QProgressBar(this);
+ m_bar->setMinimum(0);
+ m_bar->setMaximum(100);
+ QVBoxLayout *layout = new QVBoxLayout(this);
+ layout->addWidget(m_label);
+ layout->addWidget(m_bar);
+ layout->addStretch();
+ setLayout(layout);
+}
+
+void ProgressWidget::start(std::shared_ptr<Task> task)
+{
+ if (m_task)
+ {
+ disconnect(m_task.get(), 0, this, 0);
+ }
+ m_task = task;
+ connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish);
+ connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus);
+ connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress);
+ connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed);
+ if (!m_task->isRunning())
+ {
+ QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection);
+ }
+}
+bool ProgressWidget::exec(std::shared_ptr<Task> task)
+{
+ QEventLoop loop;
+ connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
+ start(task);
+ if (task->isRunning())
+ {
+ loop.exec();
+ }
+ return task->wasSuccessful();
+}
+
+void ProgressWidget::handleTaskFinish()
+{
+ if (!m_task->wasSuccessful())
+ {
+ m_label->setText(m_task->failReason());
+ }
+}
+void ProgressWidget::handleTaskStatus(const QString &status)
+{
+ m_label->setText(status);
+}
+void ProgressWidget::handleTaskProgress(qint64 current, qint64 total)
+{
+ m_bar->setMaximum(total);
+ m_bar->setValue(current);
+}
+void ProgressWidget::taskDestroyed()
+{
+ m_task = nullptr;
+}
diff --git a/launcher/widgets/ProgressWidget.h b/launcher/widgets/ProgressWidget.h
new file mode 100644
index 00000000..fa67748a
--- /dev/null
+++ b/launcher/widgets/ProgressWidget.h
@@ -0,0 +1,32 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QWidget>
+#include <memory>
+
+class Task;
+class QProgressBar;
+class QLabel;
+
+class ProgressWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ProgressWidget(QWidget *parent = nullptr);
+
+public slots:
+ void start(std::shared_ptr<Task> task);
+ bool exec(std::shared_ptr<Task> task);
+
+private slots:
+ void handleTaskFinish();
+ void handleTaskStatus(const QString &status);
+ void handleTaskProgress(qint64 current, qint64 total);
+ void taskDestroyed();
+
+private:
+ QLabel *m_label;
+ QProgressBar *m_bar;
+ std::shared_ptr<Task> m_task;
+};
diff --git a/launcher/widgets/ServerStatus.cpp b/launcher/widgets/ServerStatus.cpp
new file mode 100644
index 00000000..87c34f70
--- /dev/null
+++ b/launcher/widgets/ServerStatus.cpp
@@ -0,0 +1,179 @@
+#include "ServerStatus.h"
+#include "LineSeparator.h"
+#include "IconLabel.h"
+#include "status/StatusChecker.h"
+#include <DesktopServices.h>
+
+#include "MultiMC.h"
+
+#include <QHBoxLayout>
+#include <QFrame>
+#include <QLabel>
+#include <QMap>
+#include <QToolButton>
+#include <QAction>
+
+class ClickableLabel : public QLabel
+{
+ Q_OBJECT
+public:
+ ClickableLabel(QWidget *parent) : QLabel(parent)
+ {
+ setCursor(Qt::PointingHandCursor);
+ }
+
+ ~ClickableLabel(){};
+
+signals:
+ void clicked();
+
+protected:
+ void mousePressEvent(QMouseEvent *event)
+ {
+ emit clicked();
+ }
+};
+
+class ClickableIconLabel : public IconLabel
+{
+ Q_OBJECT
+public:
+ ClickableIconLabel(QWidget *parent, QIcon icon, QSize size) : IconLabel(parent, icon, size)
+ {
+ setCursor(Qt::PointingHandCursor);
+ }
+
+ ~ClickableIconLabel(){};
+
+signals:
+ void clicked();
+
+protected:
+ void mousePressEvent(QMouseEvent *event)
+ {
+ emit clicked();
+ }
+};
+
+ServerStatus::ServerStatus(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f)
+{
+ layout = new QHBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ goodIcon = MMC->getThemedIcon("status-good");
+ yellowIcon = MMC->getThemedIcon("status-yellow");
+ badIcon = MMC->getThemedIcon("status-bad");
+
+ addStatus("authserver.mojang.com", tr("Auth"));
+ addLine();
+ addStatus("session.minecraft.net", tr("Session"));
+ addLine();
+ addStatus("textures.minecraft.net", tr("Skins"));
+ addLine();
+ addStatus("api.mojang.com", tr("API"));
+
+ m_statusRefresh = new QToolButton(this);
+ m_statusRefresh->setCheckable(true);
+ m_statusRefresh->setToolButtonStyle(Qt::ToolButtonIconOnly);
+ m_statusRefresh->setIcon(MMC->getThemedIcon("refresh"));
+ layout->addWidget(m_statusRefresh);
+
+ setLayout(layout);
+
+ // Start status checker
+ m_statusChecker.reset(new StatusChecker());
+ {
+ auto reloader = m_statusChecker.get();
+ connect(reloader, &StatusChecker::statusChanged, this, &ServerStatus::StatusChanged);
+ connect(reloader, &StatusChecker::statusLoading, this, &ServerStatus::StatusReloading);
+ connect(m_statusRefresh, &QAbstractButton::clicked, this, &ServerStatus::reloadStatus);
+ m_statusChecker->startTimer(60000);
+ reloadStatus();
+ }
+}
+
+ServerStatus::~ServerStatus()
+{
+}
+
+void ServerStatus::reloadStatus()
+{
+ m_statusChecker->reloadStatus();
+}
+
+void ServerStatus::addLine()
+{
+ layout->addWidget(new LineSeparator(this, Qt::Vertical));
+}
+
+void ServerStatus::addStatus(QString key, QString name)
+{
+ {
+ auto label = new ClickableIconLabel(this, badIcon, QSize(16, 16));
+ label->setToolTip(key);
+ serverLabels[key] = label;
+ layout->addWidget(label);
+ connect(label,SIGNAL(clicked()),SLOT(clicked()));
+ }
+ {
+ auto label = new ClickableLabel(this);
+ label->setText(name);
+ label->setToolTip(key);
+ layout->addWidget(label);
+ connect(label,SIGNAL(clicked()),SLOT(clicked()));
+ }
+}
+
+void ServerStatus::clicked()
+{
+ DesktopServices::openUrl(QUrl("https://github.com/MultiMC/MultiMC5/wiki/Mojang-Services-Status"));
+}
+
+void ServerStatus::setStatus(QString key, int value)
+{
+ if (!serverLabels.contains(key))
+ return;
+ IconLabel *label = serverLabels[key];
+ switch(value)
+ {
+ case 0:
+ label->setIcon(goodIcon);
+ break;
+ case 1:
+ label->setIcon(yellowIcon);
+ break;
+ default:
+ case 2:
+ label->setIcon(badIcon);
+ break;
+ }
+}
+
+void ServerStatus::StatusChanged(const QMap<QString, QString> statusEntries)
+{
+ auto convertStatus = [&](QString status)->int
+ {
+ if (status == "green")
+ return 0;
+ else if (status == "yellow")
+ return 1;
+ else if (status == "red")
+ return 2;
+ return 2;
+ }
+ ;
+ auto iter = statusEntries.begin();
+ while (iter != statusEntries.end())
+ {
+ QString key = iter.key();
+ auto value = convertStatus(iter.value());
+ setStatus(key, value);
+ iter++;
+ }
+}
+
+void ServerStatus::StatusReloading(bool is_reloading)
+{
+ m_statusRefresh->setChecked(is_reloading);
+}
+
+#include "ServerStatus.moc"
diff --git a/launcher/widgets/ServerStatus.h b/launcher/widgets/ServerStatus.h
new file mode 100644
index 00000000..e1e70dfb
--- /dev/null
+++ b/launcher/widgets/ServerStatus.h
@@ -0,0 +1,40 @@
+#pragma once
+#include <QString>
+#include <QWidget>
+#include <QMap>
+#include <QIcon>
+#include <memory>
+
+class IconLabel;
+class QToolButton;
+class QHBoxLayout;
+class StatusChecker;
+
+class ServerStatus: public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ServerStatus(QWidget *parent = nullptr, Qt::WindowFlags f = 0);
+ virtual ~ServerStatus();
+
+public slots:
+ void reloadStatus();
+ void StatusChanged(const QMap<QString, QString> statuses);
+ void StatusReloading(bool is_reloading);
+
+private slots:
+ void clicked();
+
+private: /* methods */
+ void addLine();
+ void addStatus(QString key, QString name);
+ void setStatus(QString key, int value);
+private: /* data */
+ QHBoxLayout * layout = nullptr;
+ QToolButton *m_statusRefresh = nullptr;
+ QMap<QString, IconLabel *> serverLabels;
+ QIcon goodIcon;
+ QIcon yellowIcon;
+ QIcon badIcon;
+ std::shared_ptr<StatusChecker> m_statusChecker;
+};
diff --git a/launcher/widgets/VersionListView.cpp b/launcher/widgets/VersionListView.cpp
new file mode 100644
index 00000000..8424fedd
--- /dev/null
+++ b/launcher/widgets/VersionListView.cpp
@@ -0,0 +1,163 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QHeaderView>
+#include <QApplication>
+#include <QMouseEvent>
+#include <QDrag>
+#include <QPainter>
+#include "VersionListView.h"
+#include "Common.h"
+
+VersionListView::VersionListView(QWidget *parent)
+ :QTreeView ( parent )
+{
+ m_emptyString = tr("No versions are currently available.");
+}
+
+void VersionListView::rowsInserted(const QModelIndex &parent, int start, int end)
+{
+ m_itemCount += end-start+1;
+ updateEmptyViewPort();
+ QTreeView::rowsInserted(parent, start, end);
+}
+
+
+void VersionListView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
+{
+ m_itemCount -= end-start+1;
+ updateEmptyViewPort();
+ QTreeView::rowsInserted(parent, start, end);
+}
+
+void VersionListView::setModel(QAbstractItemModel *model)
+{
+ m_itemCount = model->rowCount();
+ updateEmptyViewPort();
+ QTreeView::setModel(model);
+}
+
+void VersionListView::reset()
+{
+ if(model())
+ {
+ m_itemCount = model()->rowCount();
+ }
+ else {
+ m_itemCount = 0;
+ }
+ updateEmptyViewPort();
+ QTreeView::reset();
+}
+
+void VersionListView::setEmptyString(QString emptyString)
+{
+ m_emptyString = emptyString;
+ updateEmptyViewPort();
+}
+
+void VersionListView::setEmptyErrorString(QString emptyErrorString)
+{
+ m_emptyErrorString = emptyErrorString;
+ updateEmptyViewPort();
+}
+
+void VersionListView::setEmptyMode(VersionListView::EmptyMode mode)
+{
+ m_emptyMode = mode;
+ updateEmptyViewPort();
+}
+
+void VersionListView::updateEmptyViewPort()
+{
+#ifndef QT_NO_ACCESSIBILITY
+ setAccessibleDescription(currentEmptyString());
+#endif /* !QT_NO_ACCESSIBILITY */
+
+ if(!m_itemCount)
+ {
+ viewport()->update();
+ }
+}
+
+void VersionListView::paintEvent(QPaintEvent *event)
+{
+ if(m_itemCount)
+ {
+ QTreeView::paintEvent(event);
+ }
+ else
+ {
+ paintInfoLabel(event);
+ }
+}
+
+QString VersionListView::currentEmptyString() const
+{
+ if(m_itemCount) {
+ return QString();
+ }
+ switch(m_emptyMode)
+ {
+ default:
+ case VersionListView::Empty:
+ return QString();
+ case VersionListView::String:
+ return m_emptyString;
+ case VersionListView::ErrorString:
+ return m_emptyErrorString;
+ }
+}
+
+
+void VersionListView::paintInfoLabel(QPaintEvent *event) const
+{
+ QString emptyString = currentEmptyString();
+
+ //calculate the rect for the overlay
+ QPainter painter(viewport());
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ QFont font("sans", 20);
+ font.setBold(true);
+
+ QRect bounds = viewport()->geometry();
+ bounds.moveTop(0);
+ auto innerBounds = bounds;
+ innerBounds.adjust(10, 10, -10, -10);
+
+ QColor background = QApplication::palette().color(QPalette::Foreground);
+ QColor foreground = QApplication::palette().color(QPalette::Base);
+ foreground.setAlpha(190);
+ painter.setFont(font);
+ auto fontMetrics = painter.fontMetrics();
+ auto textRect = fontMetrics.boundingRect(innerBounds, Qt::AlignHCenter | Qt::TextWordWrap, emptyString);
+ textRect.moveCenter(bounds.center());
+
+ auto wrapRect = textRect;
+ wrapRect.adjust(-10, -10, 10, 10);
+
+ //check if we are allowed to draw in our area
+ if (!event->rect().intersects(wrapRect)) {
+ return;
+ }
+
+ painter.setBrush(QBrush(background));
+ painter.setPen(foreground);
+ painter.drawRoundedRect(wrapRect, 5.0, 5.0);
+
+ painter.setPen(foreground);
+ painter.setFont(font);
+ painter.drawText(textRect, Qt::AlignHCenter | Qt::TextWordWrap, emptyString);
+}
diff --git a/launcher/widgets/VersionListView.h b/launcher/widgets/VersionListView.h
new file mode 100644
index 00000000..4153b314
--- /dev/null
+++ b/launcher/widgets/VersionListView.h
@@ -0,0 +1,56 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <QTreeView>
+
+class VersionListView : public QTreeView
+{
+ Q_OBJECT
+public:
+
+ explicit VersionListView(QWidget *parent = 0);
+ virtual void paintEvent(QPaintEvent *event) override;
+ virtual void setModel(QAbstractItemModel* model) override;
+
+ enum EmptyMode
+ {
+ Empty,
+ String,
+ ErrorString
+ };
+
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setEmptyMode(EmptyMode mode);
+
+public slots:
+ virtual void reset() override;
+
+protected slots:
+ virtual void rowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) override;
+ virtual void rowsInserted(const QModelIndex &parent, int start, int end) override;
+
+private: /* methods */
+ void paintInfoLabel(QPaintEvent *event) const;
+ void updateEmptyViewPort();
+ QString currentEmptyString() const;
+
+private: /* variables */
+ int m_itemCount = 0;
+ QString m_emptyString;
+ QString m_emptyErrorString;
+ EmptyMode m_emptyMode = Empty;
+};
diff --git a/launcher/widgets/VersionSelectWidget.cpp b/launcher/widgets/VersionSelectWidget.cpp
new file mode 100644
index 00000000..9925a6b4
--- /dev/null
+++ b/launcher/widgets/VersionSelectWidget.cpp
@@ -0,0 +1,202 @@
+#include "VersionSelectWidget.h"
+#include <QProgressBar>
+#include <QVBoxLayout>
+#include "VersionListView.h"
+#include <QHeaderView>
+#include <VersionProxyModel.h>
+#include <dialogs/CustomMessageBox.h>
+
+VersionSelectWidget::VersionSelectWidget(QWidget* parent)
+ : QWidget(parent)
+{
+ setObjectName(QStringLiteral("VersionSelectWidget"));
+ verticalLayout = new QVBoxLayout(this);
+ verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
+ verticalLayout->setContentsMargins(0, 0, 0, 0);
+
+ m_proxyModel = new VersionProxyModel(this);
+
+ listView = new VersionListView(this);
+ listView->setObjectName(QStringLiteral("listView"));
+ listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ listView->setAlternatingRowColors(true);
+ listView->setRootIsDecorated(false);
+ listView->setItemsExpandable(false);
+ listView->setWordWrap(true);
+ listView->header()->setCascadingSectionResizes(true);
+ listView->header()->setStretchLastSection(false);
+ listView->setModel(m_proxyModel);
+ verticalLayout->addWidget(listView);
+
+ sneakyProgressBar = new QProgressBar(this);
+ sneakyProgressBar->setObjectName(QStringLiteral("sneakyProgressBar"));
+ sneakyProgressBar->setFormat(QStringLiteral("%p%"));
+ verticalLayout->addWidget(sneakyProgressBar);
+ sneakyProgressBar->setHidden(true);
+ connect(listView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &VersionSelectWidget::currentRowChanged);
+
+ QMetaObject::connectSlotsByName(this);
+}
+
+void VersionSelectWidget::setCurrentVersion(const QString& version)
+{
+ m_currentVersion = version;
+ m_proxyModel->setCurrentVersion(version);
+}
+
+void VersionSelectWidget::setEmptyString(QString emptyString)
+{
+ listView->setEmptyString(emptyString);
+}
+
+void VersionSelectWidget::setEmptyErrorString(QString emptyErrorString)
+{
+ listView->setEmptyErrorString(emptyErrorString);
+}
+
+VersionSelectWidget::~VersionSelectWidget()
+{
+}
+
+void VersionSelectWidget::setResizeOn(int column)
+{
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::ResizeToContents);
+ resizeOnColumn = column;
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
+}
+
+void VersionSelectWidget::initialize(BaseVersionList *vlist)
+{
+ m_vlist = vlist;
+ m_proxyModel->setSourceModel(vlist);
+ listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch);
+
+ if (!m_vlist->isLoaded())
+ {
+ loadList();
+ }
+ else
+ {
+ if (m_proxyModel->rowCount() == 0)
+ {
+ listView->setEmptyMode(VersionListView::String);
+ }
+ preselect();
+ }
+}
+
+void VersionSelectWidget::closeEvent(QCloseEvent * event)
+{
+ QWidget::closeEvent(event);
+}
+
+void VersionSelectWidget::loadList()
+{
+ auto newTask = m_vlist->getLoadTask();
+ if (!newTask)
+ {
+ return;
+ }
+ loadTask = newTask.get();
+ connect(loadTask, &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded);
+ connect(loadTask, &Task::failed, this, &VersionSelectWidget::onTaskFailed);
+ connect(loadTask, &Task::progress, this, &VersionSelectWidget::changeProgress);
+ if(!loadTask->isRunning())
+ {
+ loadTask->start();
+ }
+ sneakyProgressBar->setHidden(false);
+}
+
+void VersionSelectWidget::onTaskSucceeded()
+{
+ if (m_proxyModel->rowCount() == 0)
+ {
+ listView->setEmptyMode(VersionListView::String);
+ }
+ sneakyProgressBar->setHidden(true);
+ preselect();
+ loadTask = nullptr;
+}
+
+void VersionSelectWidget::onTaskFailed(const QString& reason)
+{
+ CustomMessageBox::selectable(this, tr("Error"), tr("List update failed:\n%1").arg(reason), QMessageBox::Warning)->show();
+ onTaskSucceeded();
+}
+
+void VersionSelectWidget::changeProgress(qint64 current, qint64 total)
+{
+ sneakyProgressBar->setMaximum(total);
+ sneakyProgressBar->setValue(current);
+}
+
+void VersionSelectWidget::currentRowChanged(const QModelIndex& current, const QModelIndex&)
+{
+ auto variant = m_proxyModel->data(current, BaseVersionList::VersionPointerRole);
+ emit selectedVersionChanged(variant.value<BaseVersionPtr>());
+}
+
+void VersionSelectWidget::preselect()
+{
+ if(preselectedAlready)
+ return;
+ selectCurrent();
+ if(preselectedAlready)
+ return;
+ selectRecommended();
+}
+
+void VersionSelectWidget::selectCurrent()
+{
+ if(m_currentVersion.isEmpty())
+ {
+ return;
+ }
+ auto idx = m_proxyModel->getVersion(m_currentVersion);
+ if(idx.isValid())
+ {
+ preselectedAlready = true;
+ listView->selectionModel()->setCurrentIndex(idx,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ listView->scrollTo(idx, QAbstractItemView::PositionAtCenter);
+ }
+}
+
+void VersionSelectWidget::selectRecommended()
+{
+ auto idx = m_proxyModel->getRecommended();
+ if(idx.isValid())
+ {
+ preselectedAlready = true;
+ listView->selectionModel()->setCurrentIndex(idx,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
+ listView->scrollTo(idx, QAbstractItemView::PositionAtCenter);
+ }
+}
+
+bool VersionSelectWidget::hasVersions() const
+{
+ return m_proxyModel->rowCount(QModelIndex()) != 0;
+}
+
+BaseVersionPtr VersionSelectWidget::selectedVersion() const
+{
+ auto currentIndex = listView->selectionModel()->currentIndex();
+ auto variant = m_proxyModel->data(currentIndex, BaseVersionList::VersionPointerRole);
+ return variant.value<BaseVersionPtr>();
+}
+
+void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_proxyModel->setFilter(role, new ExactFilter(filter));
+}
+
+void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter)
+{
+ m_proxyModel->setFilter(role, new ContainsFilter(filter));
+}
+
+void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter *filter)
+{
+ m_proxyModel->setFilter(role, filter);
+}
diff --git a/launcher/widgets/VersionSelectWidget.h b/launcher/widgets/VersionSelectWidget.h
new file mode 100644
index 00000000..0a649408
--- /dev/null
+++ b/launcher/widgets/VersionSelectWidget.h
@@ -0,0 +1,81 @@
+/* Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QWidget>
+#include <QSortFilterProxyModel>
+#include "BaseVersionList.h"
+
+class VersionProxyModel;
+class VersionListView;
+class QVBoxLayout;
+class QProgressBar;
+class Filter;
+
+class VersionSelectWidget: public QWidget
+{
+ Q_OBJECT
+public:
+ explicit VersionSelectWidget(QWidget *parent = 0);
+ ~VersionSelectWidget();
+
+ //! loads the list if needed.
+ void initialize(BaseVersionList *vlist);
+
+ //! Starts a task that loads the list.
+ void loadList();
+
+ bool hasVersions() const;
+ BaseVersionPtr selectedVersion() const;
+ void selectRecommended();
+ void selectCurrent();
+
+ void setCurrentVersion(const QString & version);
+ void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setExactFilter(BaseVersionList::ModelRoles role, QString filter);
+ void setFilter(BaseVersionList::ModelRoles role, Filter *filter);
+ void setEmptyString(QString emptyString);
+ void setEmptyErrorString(QString emptyErrorString);
+ void setResizeOn(int column);
+
+signals:
+ void selectedVersionChanged(BaseVersionPtr version);
+
+protected:
+ virtual void closeEvent ( QCloseEvent* );
+
+private slots:
+ void onTaskSucceeded();
+ void onTaskFailed(const QString &reason);
+ void changeProgress(qint64 current, qint64 total);
+ void currentRowChanged(const QModelIndex &current, const QModelIndex &);
+
+private:
+ void preselect();
+
+private:
+ QString m_currentVersion;
+ BaseVersionList *m_vlist = nullptr;
+ VersionProxyModel *m_proxyModel = nullptr;
+ int resizeOnColumn = 0;
+ Task * loadTask;
+ bool preselectedAlready = false;
+
+private:
+ QVBoxLayout *verticalLayout = nullptr;
+ VersionListView *listView = nullptr;
+ QProgressBar *sneakyProgressBar = nullptr;
+};
diff --git a/launcher/widgets/WideBar.cpp b/launcher/widgets/WideBar.cpp
new file mode 100644
index 00000000..cbd6c617
--- /dev/null
+++ b/launcher/widgets/WideBar.cpp
@@ -0,0 +1,116 @@
+#include "WideBar.h"
+#include <QToolButton>
+#include <QMenu>
+
+class ActionButton : public QToolButton
+{
+ Q_OBJECT
+public:
+ ActionButton(QAction * action, QWidget * parent = 0) : QToolButton(parent), m_action(action) {
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ connect(action, &QAction::changed, this, &ActionButton::actionChanged);
+ connect(this, &ActionButton::clicked, action, &QAction::trigger);
+ actionChanged();
+ };
+private slots:
+ void actionChanged() {
+ setEnabled(m_action->isEnabled());
+ setChecked(m_action->isChecked());
+ setCheckable(m_action->isCheckable());
+ setText(m_action->text());
+ setIcon(m_action->icon());
+ setToolTip(m_action->toolTip());
+ setHidden(!m_action->isVisible());
+ setFocusPolicy(Qt::NoFocus);
+ }
+private:
+ QAction * m_action;
+};
+
+
+WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent)
+{
+ setFloatable(false);
+ setMovable(false);
+}
+
+WideBar::WideBar(QWidget* parent) : QToolBar(parent)
+{
+ setFloatable(false);
+ setMovable(false);
+}
+
+struct WideBar::BarEntry {
+ enum Type {
+ None,
+ Action,
+ Separator,
+ Spacer
+ } type = None;
+ QAction *qAction = nullptr;
+ QAction *wideAction = nullptr;
+};
+
+
+WideBar::~WideBar()
+{
+ for(auto *iter: m_entries) {
+ delete iter;
+ }
+}
+
+void WideBar::addAction(QAction* action)
+{
+ auto entry = new BarEntry();
+ entry->qAction = addWidget(new ActionButton(action, this));
+ entry->wideAction = action;
+ entry->type = BarEntry::Action;
+ m_entries.push_back(entry);
+}
+
+void WideBar::addSeparator()
+{
+ auto entry = new BarEntry();
+ entry->qAction = QToolBar::addSeparator();
+ entry->type = BarEntry::Separator;
+ m_entries.push_back(entry);
+}
+
+void WideBar::insertSpacer(QAction* action)
+{
+ auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) {
+ return entry->wideAction == action;
+ });
+ if(iter == m_entries.end()) {
+ return;
+ }
+ QWidget* spacer = new QWidget();
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ auto entry = new BarEntry();
+ entry->qAction = insertWidget((*iter)->qAction, spacer);
+ entry->type = BarEntry::Spacer;
+ m_entries.insert(iter, entry);
+}
+
+QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title)
+{
+ QMenu *contextMenu = new QMenu(title, parent);
+ for(auto & item: m_entries) {
+ switch(item->type) {
+ default:
+ case BarEntry::None:
+ break;
+ case BarEntry::Separator:
+ case BarEntry::Spacer:
+ contextMenu->addSeparator();
+ break;
+ case BarEntry::Action:
+ contextMenu->addAction(item->wideAction);
+ break;
+ }
+ }
+ return contextMenu;
+}
+
+#include "WideBar.moc"
diff --git a/launcher/widgets/WideBar.h b/launcher/widgets/WideBar.h
new file mode 100644
index 00000000..d1b8cbe7
--- /dev/null
+++ b/launcher/widgets/WideBar.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <QToolBar>
+#include <QAction>
+#include <QMap>
+
+class QMenu;
+
+class WideBar : public QToolBar
+{
+ Q_OBJECT
+
+public:
+ explicit WideBar(const QString &title, QWidget * parent = nullptr);
+ explicit WideBar(QWidget * parent = nullptr);
+ virtual ~WideBar();
+
+ void addAction(QAction *action);
+ void addSeparator();
+ void insertSpacer(QAction *action);
+ QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString());
+
+private:
+ struct BarEntry;
+ QList<BarEntry *> m_entries;
+};