From b6d455a02bd338e9dc0faa09d4d8177ecd8d569a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 10 Apr 2016 15:53:05 +0200 Subject: NOISSUE reorganize and document libraries --- api/logic/AbstractCommonModel.cpp | 133 +++++ api/logic/AbstractCommonModel.h | 462 ++++++++++++++++ api/logic/BaseConfigObject.cpp | 103 ++++ api/logic/BaseConfigObject.h | 50 ++ api/logic/BaseInstaller.cpp | 61 ++ api/logic/BaseInstaller.h | 46 ++ api/logic/BaseInstance.cpp | 270 +++++++++ api/logic/BaseInstance.h | 243 ++++++++ api/logic/BaseVersion.h | 59 ++ api/logic/BaseVersionList.cpp | 104 ++++ api/logic/BaseVersionList.h | 126 +++++ api/logic/CMakeLists.txt | 344 ++++++++++++ api/logic/Commandline.cpp | 483 ++++++++++++++++ api/logic/Commandline.h | 252 +++++++++ api/logic/DefaultVariable.h | 35 ++ api/logic/Env.cpp | 222 ++++++++ api/logic/Env.h | 60 ++ api/logic/Exception.h | 34 ++ api/logic/FileSystem.cpp | 436 +++++++++++++++ api/logic/FileSystem.h | 123 ++++ api/logic/GZip.cpp | 115 ++++ api/logic/GZip.h | 12 + api/logic/InstanceList.cpp | 580 +++++++++++++++++++ api/logic/InstanceList.h | 187 +++++++ api/logic/Json.cpp | 272 +++++++++ api/logic/Json.h | 249 +++++++++ api/logic/MMCStrings.cpp | 76 +++ api/logic/MMCStrings.h | 10 + api/logic/MMCZip.cpp | 491 ++++++++++++++++ api/logic/MMCZip.h | 88 +++ api/logic/NullInstance.h | 90 +++ api/logic/QObjectPtr.h | 78 +++ api/logic/RWStorage.h | 60 ++ api/logic/RecursiveFileSystemWatcher.cpp | 111 ++++ api/logic/RecursiveFileSystemWatcher.h | 63 +++ api/logic/SeparatorPrefixTree.h | 298 ++++++++++ api/logic/TypeMagic.h | 37 ++ api/logic/Version.cpp | 140 +++++ api/logic/Version.h | 110 ++++ api/logic/java/JavaChecker.cpp | 159 ++++++ api/logic/java/JavaChecker.h | 54 ++ api/logic/java/JavaCheckerJob.cpp | 45 ++ api/logic/java/JavaCheckerJob.h | 84 +++ api/logic/java/JavaInstall.cpp | 28 + api/logic/java/JavaInstall.h | 38 ++ api/logic/java/JavaInstallList.cpp | 186 +++++++ api/logic/java/JavaInstallList.h | 71 +++ api/logic/java/JavaUtils.cpp | 219 ++++++++ api/logic/java/JavaUtils.h | 43 ++ api/logic/java/JavaVersion.cpp | 112 ++++ api/logic/java/JavaVersion.h | 30 + api/logic/launch/LaunchStep.cpp | 27 + api/logic/launch/LaunchStep.h | 48 ++ api/logic/launch/LaunchTask.cpp | 228 ++++++++ api/logic/launch/LaunchTask.h | 122 ++++ api/logic/launch/LoggedProcess.cpp | 163 ++++++ api/logic/launch/LoggedProcess.h | 76 +++ api/logic/launch/MessageLevel.cpp | 36 ++ api/logic/launch/MessageLevel.h | 28 + api/logic/launch/steps/CheckJava.cpp | 92 +++ api/logic/launch/steps/CheckJava.h | 41 ++ api/logic/launch/steps/LaunchMinecraft.cpp | 155 ++++++ api/logic/launch/steps/LaunchMinecraft.h | 48 ++ api/logic/launch/steps/ModMinecraftJar.cpp | 44 ++ api/logic/launch/steps/ModMinecraftJar.h | 39 ++ api/logic/launch/steps/PostLaunchCommand.cpp | 84 +++ api/logic/launch/steps/PostLaunchCommand.h | 39 ++ api/logic/launch/steps/PreLaunchCommand.cpp | 85 +++ api/logic/launch/steps/PreLaunchCommand.h | 39 ++ api/logic/launch/steps/TextPrint.cpp | 29 + api/logic/launch/steps/TextPrint.h | 43 ++ api/logic/launch/steps/Update.cpp | 50 ++ api/logic/launch/steps/Update.h | 41 ++ api/logic/minecraft/AssetsUtils.cpp | 230 ++++++++ api/logic/minecraft/AssetsUtils.h | 48 ++ api/logic/minecraft/GradleSpecifier.h | 129 +++++ api/logic/minecraft/JarMod.h | 12 + api/logic/minecraft/Library.cpp | 239 ++++++++ api/logic/minecraft/Library.h | 184 ++++++ api/logic/minecraft/MinecraftInstance.cpp | 369 ++++++++++++ api/logic/minecraft/MinecraftInstance.h | 69 +++ api/logic/minecraft/MinecraftProfile.cpp | 610 ++++++++++++++++++++ api/logic/minecraft/MinecraftProfile.h | 200 +++++++ api/logic/minecraft/MinecraftVersion.cpp | 215 +++++++ api/logic/minecraft/MinecraftVersion.h | 119 ++++ api/logic/minecraft/MinecraftVersionList.cpp | 591 ++++++++++++++++++++ api/logic/minecraft/MinecraftVersionList.h | 72 +++ api/logic/minecraft/Mod.cpp | 377 +++++++++++++ api/logic/minecraft/Mod.h | 134 +++++ api/logic/minecraft/ModList.cpp | 616 +++++++++++++++++++++ api/logic/minecraft/ModList.h | 160 ++++++ api/logic/minecraft/MojangDownloadInfo.h | 71 +++ api/logic/minecraft/MojangVersionFormat.cpp | 381 +++++++++++++ api/logic/minecraft/MojangVersionFormat.h | 25 + api/logic/minecraft/OpSys.cpp | 42 ++ api/logic/minecraft/OpSys.h | 37 ++ api/logic/minecraft/ParseUtils.cpp | 34 ++ api/logic/minecraft/ParseUtils.h | 11 + api/logic/minecraft/ProfilePatch.h | 104 ++++ api/logic/minecraft/ProfileStrategy.h | 35 ++ api/logic/minecraft/ProfileUtils.cpp | 191 +++++++ api/logic/minecraft/ProfileUtils.h | 25 + api/logic/minecraft/Rule.cpp | 93 ++++ api/logic/minecraft/Rule.h | 101 ++++ api/logic/minecraft/VersionBuildError.h | 58 ++ api/logic/minecraft/VersionFile.cpp | 60 ++ api/logic/minecraft/VersionFile.h | 195 +++++++ api/logic/minecraft/VersionFilterData.cpp | 75 +++ api/logic/minecraft/VersionFilterData.h | 32 ++ api/logic/minecraft/World.cpp | 385 +++++++++++++ api/logic/minecraft/World.h | 83 +++ api/logic/minecraft/WorldList.cpp | 355 ++++++++++++ api/logic/minecraft/WorldList.h | 125 +++++ api/logic/minecraft/auth/AuthSession.cpp | 30 + api/logic/minecraft/auth/AuthSession.h | 51 ++ api/logic/minecraft/auth/MojangAccount.cpp | 278 ++++++++++ api/logic/minecraft/auth/MojangAccount.h | 173 ++++++ api/logic/minecraft/auth/MojangAccountList.cpp | 427 ++++++++++++++ api/logic/minecraft/auth/MojangAccountList.h | 201 +++++++ api/logic/minecraft/auth/YggdrasilTask.cpp | 255 +++++++++ api/logic/minecraft/auth/YggdrasilTask.h | 150 +++++ .../minecraft/auth/flows/AuthenticateTask.cpp | 202 +++++++ api/logic/minecraft/auth/flows/AuthenticateTask.h | 46 ++ api/logic/minecraft/auth/flows/RefreshTask.cpp | 144 +++++ api/logic/minecraft/auth/flows/RefreshTask.h | 44 ++ api/logic/minecraft/auth/flows/ValidateTask.cpp | 61 ++ api/logic/minecraft/auth/flows/ValidateTask.h | 47 ++ api/logic/minecraft/forge/ForgeInstaller.cpp | 458 +++++++++++++++ api/logic/minecraft/forge/ForgeInstaller.h | 52 ++ api/logic/minecraft/forge/ForgeVersion.cpp | 55 ++ api/logic/minecraft/forge/ForgeVersion.h | 42 ++ api/logic/minecraft/forge/ForgeVersionList.cpp | 450 +++++++++++++++ api/logic/minecraft/forge/ForgeVersionList.h | 90 +++ api/logic/minecraft/forge/ForgeXzDownload.cpp | 358 ++++++++++++ api/logic/minecraft/forge/ForgeXzDownload.h | 59 ++ api/logic/minecraft/forge/LegacyForge.cpp | 56 ++ api/logic/minecraft/forge/LegacyForge.h | 25 + api/logic/minecraft/ftb/FTBPlugin.cpp | 395 +++++++++++++ api/logic/minecraft/ftb/FTBPlugin.h | 13 + api/logic/minecraft/ftb/FTBProfileStrategy.cpp | 128 +++++ api/logic/minecraft/ftb/FTBProfileStrategy.h | 21 + api/logic/minecraft/ftb/FTBVersion.h | 32 ++ api/logic/minecraft/ftb/LegacyFTBInstance.cpp | 27 + api/logic/minecraft/ftb/LegacyFTBInstance.h | 17 + api/logic/minecraft/ftb/OneSixFTBInstance.cpp | 138 +++++ api/logic/minecraft/ftb/OneSixFTBInstance.h | 30 + api/logic/minecraft/legacy/LegacyInstance.cpp | 453 +++++++++++++++ api/logic/minecraft/legacy/LegacyInstance.h | 142 +++++ api/logic/minecraft/legacy/LegacyUpdate.cpp | 393 +++++++++++++ api/logic/minecraft/legacy/LegacyUpdate.h | 70 +++ api/logic/minecraft/legacy/LwjglVersionList.cpp | 189 +++++++ api/logic/minecraft/legacy/LwjglVersionList.h | 156 ++++++ .../minecraft/liteloader/LiteLoaderInstaller.cpp | 142 +++++ .../minecraft/liteloader/LiteLoaderInstaller.h | 39 ++ .../minecraft/liteloader/LiteLoaderVersionList.cpp | 276 +++++++++ .../minecraft/liteloader/LiteLoaderVersionList.h | 119 ++++ api/logic/minecraft/onesix/OneSixInstance.cpp | 597 ++++++++++++++++++++ api/logic/minecraft/onesix/OneSixInstance.h | 117 ++++ .../minecraft/onesix/OneSixProfileStrategy.cpp | 418 ++++++++++++++ api/logic/minecraft/onesix/OneSixProfileStrategy.h | 26 + api/logic/minecraft/onesix/OneSixUpdate.cpp | 342 ++++++++++++ api/logic/minecraft/onesix/OneSixUpdate.h | 67 +++ api/logic/minecraft/onesix/OneSixVersionFormat.cpp | 225 ++++++++ api/logic/minecraft/onesix/OneSixVersionFormat.h | 22 + api/logic/net/ByteArrayDownload.cpp | 105 ++++ api/logic/net/ByteArrayDownload.h | 48 ++ api/logic/net/CacheDownload.cpp | 192 +++++++ api/logic/net/CacheDownload.h | 63 +++ api/logic/net/HttpMetaCache.cpp | 273 +++++++++ api/logic/net/HttpMetaCache.h | 125 +++++ api/logic/net/MD5EtagDownload.cpp | 155 ++++++ api/logic/net/MD5EtagDownload.h | 52 ++ api/logic/net/NetAction.h | 96 ++++ api/logic/net/NetJob.cpp | 125 +++++ api/logic/net/NetJob.h | 117 ++++ api/logic/net/PasteUpload.cpp | 99 ++++ api/logic/net/PasteUpload.h | 50 ++ api/logic/net/URLConstants.cpp | 16 + api/logic/net/URLConstants.h | 40 ++ api/logic/news/NewsChecker.cpp | 135 +++++ api/logic/news/NewsChecker.h | 107 ++++ api/logic/news/NewsEntry.cpp | 77 +++ api/logic/news/NewsEntry.h | 65 +++ api/logic/notifications/NotificationChecker.cpp | 130 +++++ api/logic/notifications/NotificationChecker.h | 63 +++ api/logic/pathmatcher/FSTreeMatcher.h | 21 + api/logic/pathmatcher/IPathMatcher.h | 12 + api/logic/pathmatcher/MultiMatcher.h | 31 ++ api/logic/pathmatcher/RegexpMatcher.h | 42 ++ api/logic/resources/Resource.cpp | 155 ++++++ api/logic/resources/Resource.h | 132 +++++ api/logic/resources/ResourceHandler.cpp | 28 + api/logic/resources/ResourceHandler.h | 36 ++ api/logic/resources/ResourceObserver.cpp | 55 ++ api/logic/resources/ResourceObserver.h | 73 +++ api/logic/resources/ResourceProxyModel.cpp | 89 +++ api/logic/resources/ResourceProxyModel.h | 39 ++ api/logic/screenshots/ImgurAlbumCreation.cpp | 90 +++ api/logic/screenshots/ImgurAlbumCreation.h | 44 ++ api/logic/screenshots/ImgurUpload.cpp | 114 ++++ api/logic/screenshots/ImgurUpload.h | 33 ++ api/logic/screenshots/Screenshot.h | 19 + api/logic/settings/INIFile.cpp | 151 +++++ api/logic/settings/INIFile.h | 38 ++ api/logic/settings/INISettingsObject.cpp | 107 ++++ api/logic/settings/INISettingsObject.h | 66 +++ api/logic/settings/OverrideSetting.cpp | 54 ++ api/logic/settings/OverrideSetting.h | 46 ++ api/logic/settings/PassthroughSetting.cpp | 66 +++ api/logic/settings/PassthroughSetting.h | 45 ++ api/logic/settings/Setting.cpp | 53 ++ api/logic/settings/Setting.h | 119 ++++ api/logic/settings/SettingsObject.cpp | 142 +++++ api/logic/settings/SettingsObject.h | 214 +++++++ api/logic/status/StatusChecker.cpp | 153 +++++ api/logic/status/StatusChecker.h | 60 ++ api/logic/tasks/SequentialTask.cpp | 55 ++ api/logic/tasks/SequentialTask.h | 31 ++ api/logic/tasks/Task.cpp | 88 +++ api/logic/tasks/Task.h | 96 ++++ api/logic/tasks/ThreadTask.cpp | 41 ++ api/logic/tasks/ThreadTask.h | 25 + api/logic/tools/BaseExternalTool.cpp | 41 ++ api/logic/tools/BaseExternalTool.h | 60 ++ api/logic/tools/BaseProfiler.cpp | 35 ++ api/logic/tools/BaseProfiler.h | 38 ++ api/logic/tools/JProfiler.cpp | 116 ++++ api/logic/tools/JProfiler.h | 15 + api/logic/tools/JVisualVM.cpp | 103 ++++ api/logic/tools/JVisualVM.h | 15 + api/logic/tools/MCEditTool.cpp | 124 +++++ api/logic/tools/MCEditTool.h | 26 + api/logic/trans/TranslationDownloader.cpp | 53 ++ api/logic/trans/TranslationDownloader.h | 32 ++ api/logic/updater/DownloadTask.cpp | 169 ++++++ api/logic/updater/DownloadTask.h | 95 ++++ api/logic/updater/GoUpdate.cpp | 215 +++++++ api/logic/updater/GoUpdate.h | 133 +++++ api/logic/updater/UpdateChecker.cpp | 269 +++++++++ api/logic/updater/UpdateChecker.h | 121 ++++ api/logic/wonko/BaseWonkoEntity.cpp | 39 ++ api/logic/wonko/BaseWonkoEntity.h | 51 ++ api/logic/wonko/WonkoIndex.cpp | 147 +++++ api/logic/wonko/WonkoIndex.h | 68 +++ api/logic/wonko/WonkoReference.cpp | 44 ++ api/logic/wonko/WonkoReference.h | 41 ++ api/logic/wonko/WonkoUtil.cpp | 47 ++ api/logic/wonko/WonkoUtil.h | 31 ++ api/logic/wonko/WonkoVersion.cpp | 102 ++++ api/logic/wonko/WonkoVersion.h | 83 +++ api/logic/wonko/WonkoVersionList.cpp | 283 ++++++++++ api/logic/wonko/WonkoVersionList.h | 92 +++ api/logic/wonko/format/WonkoFormat.cpp | 80 +++ api/logic/wonko/format/WonkoFormat.h | 54 ++ api/logic/wonko/format/WonkoFormatV1.cpp | 156 ++++++ api/logic/wonko/format/WonkoFormatV1.h | 30 + .../wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp | 117 ++++ .../wonko/tasks/BaseWonkoEntityLocalLoadTask.h | 81 +++ .../wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp | 126 +++++ .../wonko/tasks/BaseWonkoEntityRemoteLoadTask.h | 85 +++ 260 files changed, 32792 insertions(+) create mode 100644 api/logic/AbstractCommonModel.cpp create mode 100644 api/logic/AbstractCommonModel.h create mode 100644 api/logic/BaseConfigObject.cpp create mode 100644 api/logic/BaseConfigObject.h create mode 100644 api/logic/BaseInstaller.cpp create mode 100644 api/logic/BaseInstaller.h create mode 100644 api/logic/BaseInstance.cpp create mode 100644 api/logic/BaseInstance.h create mode 100644 api/logic/BaseVersion.h create mode 100644 api/logic/BaseVersionList.cpp create mode 100644 api/logic/BaseVersionList.h create mode 100644 api/logic/CMakeLists.txt create mode 100644 api/logic/Commandline.cpp create mode 100644 api/logic/Commandline.h create mode 100644 api/logic/DefaultVariable.h create mode 100644 api/logic/Env.cpp create mode 100644 api/logic/Env.h create mode 100644 api/logic/Exception.h create mode 100644 api/logic/FileSystem.cpp create mode 100644 api/logic/FileSystem.h create mode 100644 api/logic/GZip.cpp create mode 100644 api/logic/GZip.h create mode 100644 api/logic/InstanceList.cpp create mode 100644 api/logic/InstanceList.h create mode 100644 api/logic/Json.cpp create mode 100644 api/logic/Json.h create mode 100644 api/logic/MMCStrings.cpp create mode 100644 api/logic/MMCStrings.h create mode 100644 api/logic/MMCZip.cpp create mode 100644 api/logic/MMCZip.h create mode 100644 api/logic/NullInstance.h create mode 100644 api/logic/QObjectPtr.h create mode 100644 api/logic/RWStorage.h create mode 100644 api/logic/RecursiveFileSystemWatcher.cpp create mode 100644 api/logic/RecursiveFileSystemWatcher.h create mode 100644 api/logic/SeparatorPrefixTree.h create mode 100644 api/logic/TypeMagic.h create mode 100644 api/logic/Version.cpp create mode 100644 api/logic/Version.h create mode 100644 api/logic/java/JavaChecker.cpp create mode 100644 api/logic/java/JavaChecker.h create mode 100644 api/logic/java/JavaCheckerJob.cpp create mode 100644 api/logic/java/JavaCheckerJob.h create mode 100644 api/logic/java/JavaInstall.cpp create mode 100644 api/logic/java/JavaInstall.h create mode 100644 api/logic/java/JavaInstallList.cpp create mode 100644 api/logic/java/JavaInstallList.h create mode 100644 api/logic/java/JavaUtils.cpp create mode 100644 api/logic/java/JavaUtils.h create mode 100644 api/logic/java/JavaVersion.cpp create mode 100644 api/logic/java/JavaVersion.h create mode 100644 api/logic/launch/LaunchStep.cpp create mode 100644 api/logic/launch/LaunchStep.h create mode 100644 api/logic/launch/LaunchTask.cpp create mode 100644 api/logic/launch/LaunchTask.h create mode 100644 api/logic/launch/LoggedProcess.cpp create mode 100644 api/logic/launch/LoggedProcess.h create mode 100644 api/logic/launch/MessageLevel.cpp create mode 100644 api/logic/launch/MessageLevel.h create mode 100644 api/logic/launch/steps/CheckJava.cpp create mode 100644 api/logic/launch/steps/CheckJava.h create mode 100644 api/logic/launch/steps/LaunchMinecraft.cpp create mode 100644 api/logic/launch/steps/LaunchMinecraft.h create mode 100644 api/logic/launch/steps/ModMinecraftJar.cpp create mode 100644 api/logic/launch/steps/ModMinecraftJar.h create mode 100644 api/logic/launch/steps/PostLaunchCommand.cpp create mode 100644 api/logic/launch/steps/PostLaunchCommand.h create mode 100644 api/logic/launch/steps/PreLaunchCommand.cpp create mode 100644 api/logic/launch/steps/PreLaunchCommand.h create mode 100644 api/logic/launch/steps/TextPrint.cpp create mode 100644 api/logic/launch/steps/TextPrint.h create mode 100644 api/logic/launch/steps/Update.cpp create mode 100644 api/logic/launch/steps/Update.h create mode 100644 api/logic/minecraft/AssetsUtils.cpp create mode 100644 api/logic/minecraft/AssetsUtils.h create mode 100644 api/logic/minecraft/GradleSpecifier.h create mode 100644 api/logic/minecraft/JarMod.h create mode 100644 api/logic/minecraft/Library.cpp create mode 100644 api/logic/minecraft/Library.h create mode 100644 api/logic/minecraft/MinecraftInstance.cpp create mode 100644 api/logic/minecraft/MinecraftInstance.h create mode 100644 api/logic/minecraft/MinecraftProfile.cpp create mode 100644 api/logic/minecraft/MinecraftProfile.h create mode 100644 api/logic/minecraft/MinecraftVersion.cpp create mode 100644 api/logic/minecraft/MinecraftVersion.h create mode 100644 api/logic/minecraft/MinecraftVersionList.cpp create mode 100644 api/logic/minecraft/MinecraftVersionList.h create mode 100644 api/logic/minecraft/Mod.cpp create mode 100644 api/logic/minecraft/Mod.h create mode 100644 api/logic/minecraft/ModList.cpp create mode 100644 api/logic/minecraft/ModList.h create mode 100644 api/logic/minecraft/MojangDownloadInfo.h create mode 100644 api/logic/minecraft/MojangVersionFormat.cpp create mode 100644 api/logic/minecraft/MojangVersionFormat.h create mode 100644 api/logic/minecraft/OpSys.cpp create mode 100644 api/logic/minecraft/OpSys.h create mode 100644 api/logic/minecraft/ParseUtils.cpp create mode 100644 api/logic/minecraft/ParseUtils.h create mode 100644 api/logic/minecraft/ProfilePatch.h create mode 100644 api/logic/minecraft/ProfileStrategy.h create mode 100644 api/logic/minecraft/ProfileUtils.cpp create mode 100644 api/logic/minecraft/ProfileUtils.h create mode 100644 api/logic/minecraft/Rule.cpp create mode 100644 api/logic/minecraft/Rule.h create mode 100644 api/logic/minecraft/VersionBuildError.h create mode 100644 api/logic/minecraft/VersionFile.cpp create mode 100644 api/logic/minecraft/VersionFile.h create mode 100644 api/logic/minecraft/VersionFilterData.cpp create mode 100644 api/logic/minecraft/VersionFilterData.h create mode 100644 api/logic/minecraft/World.cpp create mode 100644 api/logic/minecraft/World.h create mode 100644 api/logic/minecraft/WorldList.cpp create mode 100644 api/logic/minecraft/WorldList.h create mode 100644 api/logic/minecraft/auth/AuthSession.cpp create mode 100644 api/logic/minecraft/auth/AuthSession.h create mode 100644 api/logic/minecraft/auth/MojangAccount.cpp create mode 100644 api/logic/minecraft/auth/MojangAccount.h create mode 100644 api/logic/minecraft/auth/MojangAccountList.cpp create mode 100644 api/logic/minecraft/auth/MojangAccountList.h create mode 100644 api/logic/minecraft/auth/YggdrasilTask.cpp create mode 100644 api/logic/minecraft/auth/YggdrasilTask.h create mode 100644 api/logic/minecraft/auth/flows/AuthenticateTask.cpp create mode 100644 api/logic/minecraft/auth/flows/AuthenticateTask.h create mode 100644 api/logic/minecraft/auth/flows/RefreshTask.cpp create mode 100644 api/logic/minecraft/auth/flows/RefreshTask.h create mode 100644 api/logic/minecraft/auth/flows/ValidateTask.cpp create mode 100644 api/logic/minecraft/auth/flows/ValidateTask.h create mode 100644 api/logic/minecraft/forge/ForgeInstaller.cpp create mode 100644 api/logic/minecraft/forge/ForgeInstaller.h create mode 100644 api/logic/minecraft/forge/ForgeVersion.cpp create mode 100644 api/logic/minecraft/forge/ForgeVersion.h create mode 100644 api/logic/minecraft/forge/ForgeVersionList.cpp create mode 100644 api/logic/minecraft/forge/ForgeVersionList.h create mode 100644 api/logic/minecraft/forge/ForgeXzDownload.cpp create mode 100644 api/logic/minecraft/forge/ForgeXzDownload.h create mode 100644 api/logic/minecraft/forge/LegacyForge.cpp create mode 100644 api/logic/minecraft/forge/LegacyForge.h create mode 100644 api/logic/minecraft/ftb/FTBPlugin.cpp create mode 100644 api/logic/minecraft/ftb/FTBPlugin.h create mode 100644 api/logic/minecraft/ftb/FTBProfileStrategy.cpp create mode 100644 api/logic/minecraft/ftb/FTBProfileStrategy.h create mode 100644 api/logic/minecraft/ftb/FTBVersion.h create mode 100644 api/logic/minecraft/ftb/LegacyFTBInstance.cpp create mode 100644 api/logic/minecraft/ftb/LegacyFTBInstance.h create mode 100644 api/logic/minecraft/ftb/OneSixFTBInstance.cpp create mode 100644 api/logic/minecraft/ftb/OneSixFTBInstance.h create mode 100644 api/logic/minecraft/legacy/LegacyInstance.cpp create mode 100644 api/logic/minecraft/legacy/LegacyInstance.h create mode 100644 api/logic/minecraft/legacy/LegacyUpdate.cpp create mode 100644 api/logic/minecraft/legacy/LegacyUpdate.h create mode 100644 api/logic/minecraft/legacy/LwjglVersionList.cpp create mode 100644 api/logic/minecraft/legacy/LwjglVersionList.h create mode 100644 api/logic/minecraft/liteloader/LiteLoaderInstaller.cpp create mode 100644 api/logic/minecraft/liteloader/LiteLoaderInstaller.h create mode 100644 api/logic/minecraft/liteloader/LiteLoaderVersionList.cpp create mode 100644 api/logic/minecraft/liteloader/LiteLoaderVersionList.h create mode 100644 api/logic/minecraft/onesix/OneSixInstance.cpp create mode 100644 api/logic/minecraft/onesix/OneSixInstance.h create mode 100644 api/logic/minecraft/onesix/OneSixProfileStrategy.cpp create mode 100644 api/logic/minecraft/onesix/OneSixProfileStrategy.h create mode 100644 api/logic/minecraft/onesix/OneSixUpdate.cpp create mode 100644 api/logic/minecraft/onesix/OneSixUpdate.h create mode 100644 api/logic/minecraft/onesix/OneSixVersionFormat.cpp create mode 100644 api/logic/minecraft/onesix/OneSixVersionFormat.h create mode 100644 api/logic/net/ByteArrayDownload.cpp create mode 100644 api/logic/net/ByteArrayDownload.h create mode 100644 api/logic/net/CacheDownload.cpp create mode 100644 api/logic/net/CacheDownload.h create mode 100644 api/logic/net/HttpMetaCache.cpp create mode 100644 api/logic/net/HttpMetaCache.h create mode 100644 api/logic/net/MD5EtagDownload.cpp create mode 100644 api/logic/net/MD5EtagDownload.h create mode 100644 api/logic/net/NetAction.h create mode 100644 api/logic/net/NetJob.cpp create mode 100644 api/logic/net/NetJob.h create mode 100644 api/logic/net/PasteUpload.cpp create mode 100644 api/logic/net/PasteUpload.h create mode 100644 api/logic/net/URLConstants.cpp create mode 100644 api/logic/net/URLConstants.h create mode 100644 api/logic/news/NewsChecker.cpp create mode 100644 api/logic/news/NewsChecker.h create mode 100644 api/logic/news/NewsEntry.cpp create mode 100644 api/logic/news/NewsEntry.h create mode 100644 api/logic/notifications/NotificationChecker.cpp create mode 100644 api/logic/notifications/NotificationChecker.h create mode 100644 api/logic/pathmatcher/FSTreeMatcher.h create mode 100644 api/logic/pathmatcher/IPathMatcher.h create mode 100644 api/logic/pathmatcher/MultiMatcher.h create mode 100644 api/logic/pathmatcher/RegexpMatcher.h create mode 100644 api/logic/resources/Resource.cpp create mode 100644 api/logic/resources/Resource.h create mode 100644 api/logic/resources/ResourceHandler.cpp create mode 100644 api/logic/resources/ResourceHandler.h create mode 100644 api/logic/resources/ResourceObserver.cpp create mode 100644 api/logic/resources/ResourceObserver.h create mode 100644 api/logic/resources/ResourceProxyModel.cpp create mode 100644 api/logic/resources/ResourceProxyModel.h create mode 100644 api/logic/screenshots/ImgurAlbumCreation.cpp create mode 100644 api/logic/screenshots/ImgurAlbumCreation.h create mode 100644 api/logic/screenshots/ImgurUpload.cpp create mode 100644 api/logic/screenshots/ImgurUpload.h create mode 100644 api/logic/screenshots/Screenshot.h create mode 100644 api/logic/settings/INIFile.cpp create mode 100644 api/logic/settings/INIFile.h create mode 100644 api/logic/settings/INISettingsObject.cpp create mode 100644 api/logic/settings/INISettingsObject.h create mode 100644 api/logic/settings/OverrideSetting.cpp create mode 100644 api/logic/settings/OverrideSetting.h create mode 100644 api/logic/settings/PassthroughSetting.cpp create mode 100644 api/logic/settings/PassthroughSetting.h create mode 100644 api/logic/settings/Setting.cpp create mode 100644 api/logic/settings/Setting.h create mode 100644 api/logic/settings/SettingsObject.cpp create mode 100644 api/logic/settings/SettingsObject.h create mode 100644 api/logic/status/StatusChecker.cpp create mode 100644 api/logic/status/StatusChecker.h create mode 100644 api/logic/tasks/SequentialTask.cpp create mode 100644 api/logic/tasks/SequentialTask.h create mode 100644 api/logic/tasks/Task.cpp create mode 100644 api/logic/tasks/Task.h create mode 100644 api/logic/tasks/ThreadTask.cpp create mode 100644 api/logic/tasks/ThreadTask.h create mode 100644 api/logic/tools/BaseExternalTool.cpp create mode 100644 api/logic/tools/BaseExternalTool.h create mode 100644 api/logic/tools/BaseProfiler.cpp create mode 100644 api/logic/tools/BaseProfiler.h create mode 100644 api/logic/tools/JProfiler.cpp create mode 100644 api/logic/tools/JProfiler.h create mode 100644 api/logic/tools/JVisualVM.cpp create mode 100644 api/logic/tools/JVisualVM.h create mode 100644 api/logic/tools/MCEditTool.cpp create mode 100644 api/logic/tools/MCEditTool.h create mode 100644 api/logic/trans/TranslationDownloader.cpp create mode 100644 api/logic/trans/TranslationDownloader.h create mode 100644 api/logic/updater/DownloadTask.cpp create mode 100644 api/logic/updater/DownloadTask.h create mode 100644 api/logic/updater/GoUpdate.cpp create mode 100644 api/logic/updater/GoUpdate.h create mode 100644 api/logic/updater/UpdateChecker.cpp create mode 100644 api/logic/updater/UpdateChecker.h create mode 100644 api/logic/wonko/BaseWonkoEntity.cpp create mode 100644 api/logic/wonko/BaseWonkoEntity.h create mode 100644 api/logic/wonko/WonkoIndex.cpp create mode 100644 api/logic/wonko/WonkoIndex.h create mode 100644 api/logic/wonko/WonkoReference.cpp create mode 100644 api/logic/wonko/WonkoReference.h create mode 100644 api/logic/wonko/WonkoUtil.cpp create mode 100644 api/logic/wonko/WonkoUtil.h create mode 100644 api/logic/wonko/WonkoVersion.cpp create mode 100644 api/logic/wonko/WonkoVersion.h create mode 100644 api/logic/wonko/WonkoVersionList.cpp create mode 100644 api/logic/wonko/WonkoVersionList.h create mode 100644 api/logic/wonko/format/WonkoFormat.cpp create mode 100644 api/logic/wonko/format/WonkoFormat.h create mode 100644 api/logic/wonko/format/WonkoFormatV1.cpp create mode 100644 api/logic/wonko/format/WonkoFormatV1.h create mode 100644 api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp create mode 100644 api/logic/wonko/tasks/BaseWonkoEntityLocalLoadTask.h create mode 100644 api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp create mode 100644 api/logic/wonko/tasks/BaseWonkoEntityRemoteLoadTask.h (limited to 'api/logic') diff --git a/api/logic/AbstractCommonModel.cpp b/api/logic/AbstractCommonModel.cpp new file mode 100644 index 00000000..71d75829 --- /dev/null +++ b/api/logic/AbstractCommonModel.cpp @@ -0,0 +1,133 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AbstractCommonModel.h" + +BaseAbstractCommonModel::BaseAbstractCommonModel(const Qt::Orientation orientation, QObject *parent) + : QAbstractListModel(parent), m_orientation(orientation) +{ +} + +int BaseAbstractCommonModel::rowCount(const QModelIndex &parent) const +{ + return m_orientation == Qt::Horizontal ? entryCount() : size(); +} +int BaseAbstractCommonModel::columnCount(const QModelIndex &parent) const +{ + return m_orientation == Qt::Horizontal ? size() : entryCount(); +} +QVariant BaseAbstractCommonModel::data(const QModelIndex &index, int role) const +{ + if (!hasIndex(index.row(), index.column(), index.parent())) + { + return QVariant(); + } + const int i = m_orientation == Qt::Horizontal ? index.column() : index.row(); + const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column(); + return formatData(i, role, get(i, entry, role)); +} +QVariant BaseAbstractCommonModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != m_orientation && role == Qt::DisplayRole) + { + return entryTitle(section); + } + else + { + return QVariant(); + } +} +bool BaseAbstractCommonModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + const int i = m_orientation == Qt::Horizontal ? index.column() : index.row(); + const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column(); + const bool result = set(i, entry, role, sanetizeData(i, role, value)); + if (result) + { + emit dataChanged(index, index, QVector() << role); + } + return result; +} +Qt::ItemFlags BaseAbstractCommonModel::flags(const QModelIndex &index) const +{ + if (!hasIndex(index.row(), index.column(), index.parent())) + { + return Qt::NoItemFlags; + } + + const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column(); + if (canSet(entry)) + { + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + else + { + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; + } +} + +void BaseAbstractCommonModel::notifyAboutToAddObject(const int at) +{ + if (m_orientation == Qt::Horizontal) + { + beginInsertColumns(QModelIndex(), at, at); + } + else + { + beginInsertRows(QModelIndex(), at, at); + } +} +void BaseAbstractCommonModel::notifyObjectAdded() +{ + if (m_orientation == Qt::Horizontal) + { + endInsertColumns(); + } + else + { + endInsertRows(); + } +} +void BaseAbstractCommonModel::notifyAboutToRemoveObject(const int at) +{ + if (m_orientation == Qt::Horizontal) + { + beginRemoveColumns(QModelIndex(), at, at); + } + else + { + beginRemoveRows(QModelIndex(), at, at); + } +} +void BaseAbstractCommonModel::notifyObjectRemoved() +{ + if (m_orientation == Qt::Horizontal) + { + endRemoveColumns(); + } + else + { + endRemoveRows(); + } +} + +void BaseAbstractCommonModel::notifyBeginReset() +{ + beginResetModel(); +} +void BaseAbstractCommonModel::notifyEndReset() +{ + endResetModel(); +} diff --git a/api/logic/AbstractCommonModel.h b/api/logic/AbstractCommonModel.h new file mode 100644 index 00000000..31b86a23 --- /dev/null +++ b/api/logic/AbstractCommonModel.h @@ -0,0 +1,462 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +class BaseAbstractCommonModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit BaseAbstractCommonModel(const Qt::Orientation orientation, QObject *parent = nullptr); + + // begin QAbstractItemModel interface + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + // end QAbstractItemModel interface + + virtual int size() const = 0; + virtual int entryCount() const = 0; + + virtual QVariant formatData(const int index, int role, const QVariant &data) const { return data; } + virtual QVariant sanetizeData(const int index, int role, const QVariant &data) const { return data; } + +protected: + virtual QVariant get(const int index, const int entry, const int role) const = 0; + virtual bool set(const int index, const int entry, const int role, const QVariant &value) = 0; + virtual bool canSet(const int entry) const = 0; + virtual QString entryTitle(const int entry) const = 0; + + void notifyAboutToAddObject(const int at); + void notifyObjectAdded(); + void notifyAboutToRemoveObject(const int at); + void notifyObjectRemoved(); + void notifyBeginReset(); + void notifyEndReset(); + + const Qt::Orientation m_orientation; +}; + +template +class AbstractCommonModel : public BaseAbstractCommonModel +{ +public: + explicit AbstractCommonModel(const Qt::Orientation orientation) + : BaseAbstractCommonModel(orientation) {} + virtual ~AbstractCommonModel() {} + + int size() const override { return m_objects.size(); } + int entryCount() const override { return m_entries.size(); } + + void append(const Object &object) + { + notifyAboutToAddObject(size()); + m_objects.append(object); + notifyObjectAdded(); + } + void prepend(const Object &object) + { + notifyAboutToAddObject(0); + m_objects.prepend(object); + notifyObjectAdded(); + } + void insert(const Object &object, const int index) + { + if (index >= size()) + { + prepend(object); + } + else if (index <= 0) + { + append(object); + } + else + { + notifyAboutToAddObject(index); + m_objects.insert(index, object); + notifyObjectAdded(); + } + } + void remove(const int index) + { + notifyAboutToRemoveObject(index); + m_objects.removeAt(index); + notifyObjectRemoved(); + } + Object get(const int index) const + { + return m_objects.at(index); + } + +private: + friend class CommonModel; + QVariant get(const int index, const int entry, const int role) const override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(role)) + { + return QVariant(); + } + return m_entries[entry].second.value(role)->get(m_objects.at(index)); + } + bool set(const int index, const int entry, const int role, const QVariant &value) override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(role)) + { + return false; + } + IEntry *e = m_entries[entry].second.value(role); + if (!e->canSet()) + { + return false; + } + e->set(m_objects[index], value); + return true; + } + bool canSet(const int entry) const override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(Qt::EditRole)) + { + return false; + } + IEntry *e = m_entries[entry].second.value(Qt::EditRole); + return e->canSet(); + } + + QString entryTitle(const int entry) const override + { + return m_entries.at(entry).first; + } + +private: + struct IEntry + { + virtual ~IEntry() {} + virtual void set(Object &object, const QVariant &value) = 0; + virtual QVariant get(const Object &object) const = 0; + virtual bool canSet() const = 0; + }; + template + struct VariableEntry : public IEntry + { + typedef T (Object::*Member); + + explicit VariableEntry(Member member) + : m_member(member) {} + + void set(Object &object, const QVariant &value) override + { + object.*m_member = value.value(); + } + QVariant get(const Object &object) const override + { + return QVariant::fromValue(object.*m_member); + } + bool canSet() const override { return true; } + + private: + Member m_member; + }; + template + struct FunctionEntry : public IEntry + { + typedef T (Object::*Getter)() const; + typedef void (Object::*Setter)(T); + + explicit FunctionEntry(Getter getter, Setter setter) + : m_getter(m_getter), m_setter(m_setter) {} + + void set(Object &object, const QVariant &value) override + { + object.*m_setter(value.value()); + } + QVariant get(const Object &object) const override + { + return QVariant::fromValue(object.*m_getter()); + } + bool canSet() const override { return !!m_setter; } + + private: + Getter m_getter; + Setter m_setter; + }; + + QList m_objects; + QVector>> m_entries; + + void addEntryInternal(IEntry *e, const int entry, const int role) + { + if (m_entries.size() <= entry) + { + m_entries.resize(entry + 1); + } + m_entries[entry].second.insert(role, e); + } + +protected: + template + typename std::enable_if::value && std::is_member_function_pointer::value, void>::type + addEntry(Getter getter, Setter setter, const int entry, const int role) + { + addEntryInternal(new FunctionEntry::type>(getter, setter), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(Getter getter, const int entry, const int role) + { + addEntryInternal(new FunctionEntry::type>(getter, nullptr), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(T (Object::*member), const int entry, const int role) + { + addEntryInternal(new VariableEntry(member), entry, role); + } + + void setEntryTitle(const int entry, const QString &title) + { + m_entries[entry].first = title; + } +}; +template +class AbstractCommonModel : public BaseAbstractCommonModel +{ +public: + explicit AbstractCommonModel(const Qt::Orientation orientation) + : BaseAbstractCommonModel(orientation) {} + virtual ~AbstractCommonModel() + { + qDeleteAll(m_objects); + } + + int size() const override { return m_objects.size(); } + int entryCount() const override { return m_entries.size(); } + + void append(Object *object) + { + notifyAboutToAddObject(size()); + m_objects.append(object); + notifyObjectAdded(); + } + void prepend(Object *object) + { + notifyAboutToAddObject(0); + m_objects.prepend(object); + notifyObjectAdded(); + } + void insert(Object *object, const int index) + { + if (index >= size()) + { + prepend(object); + } + else if (index <= 0) + { + append(object); + } + else + { + notifyAboutToAddObject(index); + m_objects.insert(index, object); + notifyObjectAdded(); + } + } + void remove(const int index) + { + notifyAboutToRemoveObject(index); + m_objects.removeAt(index); + notifyObjectRemoved(); + } + Object *get(const int index) const + { + return m_objects.at(index); + } + int find(Object * const obj) const + { + return m_objects.indexOf(obj); + } + + QList getAll() const + { + return m_objects; + } + +private: + friend class CommonModel; + QVariant get(const int index, const int entry, const int role) const override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(role)) + { + return QVariant(); + } + return m_entries[entry].second.value(role)->get(m_objects.at(index)); + } + bool set(const int index, const int entry, const int role, const QVariant &value) override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(role)) + { + return false; + } + IEntry *e = m_entries[entry].second.value(role); + if (!e->canSet()) + { + return false; + } + e->set(m_objects[index], value); + return true; + } + bool canSet(const int entry) const override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(Qt::EditRole)) + { + return false; + } + IEntry *e = m_entries[entry].second.value(Qt::EditRole); + return e->canSet(); + } + + QString entryTitle(const int entry) const override + { + return m_entries.at(entry).first; + } + +private: + struct IEntry + { + virtual ~IEntry() {} + virtual void set(Object *object, const QVariant &value) = 0; + virtual QVariant get(Object *object) const = 0; + virtual bool canSet() const = 0; + }; + template + struct VariableEntry : public IEntry + { + typedef T (Object::*Member); + + explicit VariableEntry(Member member) + : m_member(member) {} + + void set(Object *object, const QVariant &value) override + { + object->*m_member = value.value(); + } + QVariant get(Object *object) const override + { + return QVariant::fromValue(object->*m_member); + } + bool canSet() const override { return true; } + + private: + Member m_member; + }; + template + struct FunctionEntry : public IEntry + { + typedef T (Object::*Getter)() const; + typedef void (Object::*Setter)(T); + + explicit FunctionEntry(Getter getter, Setter setter) + : m_getter(getter), m_setter(setter) {} + + void set(Object *object, const QVariant &value) override + { + (object->*m_setter)(value.value()); + } + QVariant get(Object *object) const override + { + return QVariant::fromValue((object->*m_getter)()); + } + bool canSet() const override { return !!m_setter; } + + private: + Getter m_getter; + Setter m_setter; + }; + template + struct LambdaEntry : public IEntry + { + using Getter = std::function; + + explicit LambdaEntry(Getter getter) + : m_getter(getter) {} + + void set(Object *object, const QVariant &value) override {} + QVariant get(Object *object) const override + { + return QVariant::fromValue(m_getter(object)); + } + bool canSet() const override { return false; } + + private: + Getter m_getter; + }; + + QList m_objects; + QVector>> m_entries; + + void addEntryInternal(IEntry *e, const int entry, const int role) + { + if (m_entries.size() <= entry) + { + m_entries.resize(entry + 1); + } + m_entries[entry].second.insert(role, e); + } + +protected: + template + typename std::enable_if::value && std::is_member_function_pointer::value, void>::type + addEntry(const int entry, const int role, Getter getter, Setter setter) + { + addEntryInternal(new FunctionEntry::type>(getter, setter), entry, role); + } + template + typename std::enable_if::Getter>::value, void>::type + addEntry(const int entry, const int role, typename FunctionEntry::Getter getter) + { + addEntryInternal(new FunctionEntry(getter, nullptr), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(const int entry, const int role, T (Object::*member)) + { + addEntryInternal(new VariableEntry(member), entry, role); + } + template + void addEntry(const int entry, const int role, typename LambdaEntry::Getter lambda) + { + addEntryInternal(new LambdaEntry(lambda), entry, role); + } + + void setEntryTitle(const int entry, const QString &title) + { + m_entries[entry].first = title; + } + + void setAll(const QList objects) + { + notifyBeginReset(); + qDeleteAll(m_objects); + m_objects = objects; + notifyEndReset(); + } +}; diff --git a/api/logic/BaseConfigObject.cpp b/api/logic/BaseConfigObject.cpp new file mode 100644 index 00000000..3040ac2e --- /dev/null +++ b/api/logic/BaseConfigObject.cpp @@ -0,0 +1,103 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseConfigObject.h" + +#include +#include +#include +#include + +#include "Exception.h" +#include "FileSystem.h" + +BaseConfigObject::BaseConfigObject(const QString &filename) + : m_filename(filename) +{ + m_saveTimer = new QTimer; + m_saveTimer->setSingleShot(true); + // cppcheck-suppress pureVirtualCall + QObject::connect(m_saveTimer, &QTimer::timeout, [this](){saveNow();}); + setSaveTimeout(250); + + m_initialReadTimer = new QTimer; + m_initialReadTimer->setSingleShot(true); + QObject::connect(m_initialReadTimer, &QTimer::timeout, [this]() + { + loadNow(); + m_initialReadTimer->deleteLater(); + m_initialReadTimer = 0; + }); + m_initialReadTimer->start(0); + + // cppcheck-suppress pureVirtualCall + m_appQuitConnection = QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this](){saveNow();}); +} +BaseConfigObject::~BaseConfigObject() +{ + delete m_saveTimer; + if (m_initialReadTimer) + { + delete m_initialReadTimer; + } + QObject::disconnect(m_appQuitConnection); +} + +void BaseConfigObject::setSaveTimeout(int msec) +{ + m_saveTimer->setInterval(msec); +} + +void BaseConfigObject::scheduleSave() +{ + m_saveTimer->stop(); + m_saveTimer->start(); +} +void BaseConfigObject::saveNow() +{ + if (m_saveTimer->isActive()) + { + m_saveTimer->stop(); + } + if (m_disableSaving) + { + return; + } + + try + { + FS::write(m_filename, doSave()); + } + catch (Exception & e) + { + qCritical() << e.cause(); + } +} +void BaseConfigObject::loadNow() +{ + if (m_saveTimer->isActive()) + { + saveNow(); + } + + try + { + doLoad(FS::read(m_filename)); + } + catch (Exception & e) + { + qWarning() << "Error loading" << m_filename << ":" << e.cause(); + } +} diff --git a/api/logic/BaseConfigObject.h b/api/logic/BaseConfigObject.h new file mode 100644 index 00000000..1c96b3d1 --- /dev/null +++ b/api/logic/BaseConfigObject.h @@ -0,0 +1,50 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +class QTimer; + +class BaseConfigObject +{ +public: + void setSaveTimeout(int msec); + +protected: + explicit BaseConfigObject(const QString &filename); + virtual ~BaseConfigObject(); + + // cppcheck-suppress pureVirtualCall + virtual QByteArray doSave() const = 0; + virtual void doLoad(const QByteArray &data) = 0; + + void setSavingDisabled(bool savingDisabled) { m_disableSaving = savingDisabled; } + + QString fileName() const { return m_filename; } + +public: + void scheduleSave(); + void saveNow(); + void loadNow(); + +private: + QTimer *m_saveTimer; + QTimer *m_initialReadTimer; + QString m_filename; + QMetaObject::Connection m_appQuitConnection; + bool m_disableSaving = false; +}; diff --git a/api/logic/BaseInstaller.cpp b/api/logic/BaseInstaller.cpp new file mode 100644 index 00000000..cb762ebd --- /dev/null +++ b/api/logic/BaseInstaller.cpp @@ -0,0 +1,61 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "BaseInstaller.h" +#include "minecraft/onesix/OneSixInstance.h" + +BaseInstaller::BaseInstaller() +{ + +} + +bool BaseInstaller::isApplied(OneSixInstance *on) +{ + return QFile::exists(filename(on->instanceRoot())); +} + +bool BaseInstaller::add(OneSixInstance *to) +{ + if (!patchesDir(to->instanceRoot()).exists()) + { + QDir(to->instanceRoot()).mkdir("patches"); + } + + if (isApplied(to)) + { + if (!remove(to)) + { + return false; + } + } + + return true; +} + +bool BaseInstaller::remove(OneSixInstance *from) +{ + return QFile::remove(filename(from->instanceRoot())); +} + +QString BaseInstaller::filename(const QString &root) const +{ + return patchesDir(root).absoluteFilePath(id() + ".json"); +} +QDir BaseInstaller::patchesDir(const QString &root) const +{ + return QDir(root + "/patches/"); +} diff --git a/api/logic/BaseInstaller.h b/api/logic/BaseInstaller.h new file mode 100644 index 00000000..a50c8cb1 --- /dev/null +++ b/api/logic/BaseInstaller.h @@ -0,0 +1,46 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "multimc_logic_export.h" + +class OneSixInstance; +class QDir; +class QString; +class QObject; +class Task; +class BaseVersion; +typedef std::shared_ptr BaseVersionPtr; + +class MULTIMC_LOGIC_EXPORT BaseInstaller +{ +public: + BaseInstaller(); + virtual ~BaseInstaller(){}; + bool isApplied(OneSixInstance *on); + + virtual bool add(OneSixInstance *to); + virtual bool remove(OneSixInstance *from); + + virtual Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) = 0; + +protected: + virtual QString id() const = 0; + QString filename(const QString &root) const; + QDir patchesDir(const QString &root) const; +}; diff --git a/api/logic/BaseInstance.cpp b/api/logic/BaseInstance.cpp new file mode 100644 index 00000000..ce55d5e4 --- /dev/null +++ b/api/logic/BaseInstance.cpp @@ -0,0 +1,270 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseInstance.h" + +#include +#include + +#include "settings/INISettingsObject.h" +#include "settings/Setting.h" +#include "settings/OverrideSetting.h" + +#include "minecraft/MinecraftVersionList.h" +#include "FileSystem.h" +#include "Commandline.h" + +BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : QObject() +{ + m_settings = settings; + m_rootDir = rootDir; + + m_settings->registerSetting("name", "Unnamed Instance"); + m_settings->registerSetting("iconKey", "default"); + m_settings->registerSetting("notes", ""); + m_settings->registerSetting("lastLaunchTime", 0); + m_settings->registerSetting("totalTimePlayed", 0); + + // Custom Commands + auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); + m_settings->registerOverride(globalSettings->getSetting("PreLaunchCommand"), commandSetting); + m_settings->registerOverride(globalSettings->getSetting("WrapperCommand"), commandSetting); + m_settings->registerOverride(globalSettings->getSetting("PostExitCommand"), commandSetting); + + // Console + auto consoleSetting = m_settings->registerSetting("OverrideConsole", false); + m_settings->registerOverride(globalSettings->getSetting("ShowConsole"), consoleSetting); + m_settings->registerOverride(globalSettings->getSetting("AutoCloseConsole"), consoleSetting); + m_settings->registerOverride(globalSettings->getSetting("LogPrePostOutput"), consoleSetting); +} + +QString BaseInstance::getPreLaunchCommand() +{ + return settings()->get("PreLaunchCommand").toString(); +} + +QString BaseInstance::getWrapperCommand() +{ + return settings()->get("WrapperCommand").toString(); +} + +QString BaseInstance::getPostExitCommand() +{ + return settings()->get("PostExitCommand").toString(); +} + +void BaseInstance::iconUpdated(QString key) +{ + if(iconKey() == key) + { + emit propertiesChanged(this); + } +} + +void BaseInstance::nuke() +{ + FS::deletePath(instanceRoot()); + emit nuked(this); +} + +QString BaseInstance::id() const +{ + return QFileInfo(instanceRoot()).fileName(); +} + +bool BaseInstance::isRunning() const +{ + return m_isRunning; +} + +void BaseInstance::setRunning(bool running) +{ + if(running && !m_isRunning) + { + m_timeStarted = QDateTime::currentDateTime(); + } + else if(!running && m_isRunning) + { + qint64 current = settings()->get("totalTimePlayed").toLongLong(); + QDateTime timeEnded = QDateTime::currentDateTime(); + settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded)); + emit propertiesChanged(this); + } + m_isRunning = running; +} + +int64_t BaseInstance::totalTimePlayed() const +{ + qint64 current = settings()->get("totalTimePlayed").toLongLong(); + if(m_isRunning) + { + QDateTime timeNow = QDateTime::currentDateTime(); + return current + m_timeStarted.secsTo(timeNow); + } + return current; +} + +void BaseInstance::resetTimePlayed() +{ + settings()->reset("totalTimePlayed"); +} + +QString BaseInstance::instanceType() const +{ + return m_settings->get("InstanceType").toString(); +} + +QString BaseInstance::instanceRoot() const +{ + return m_rootDir; +} + +InstancePtr BaseInstance::getSharedPtr() +{ + return shared_from_this(); +} + +SettingsObjectPtr BaseInstance::settings() const +{ + return m_settings; +} + +BaseInstance::InstanceFlags BaseInstance::flags() const +{ + return m_flags; +} + +void BaseInstance::setFlags(const InstanceFlags &flags) +{ + if (flags != m_flags) + { + m_flags = flags; + emit flagsChanged(); + emit propertiesChanged(this); + } +} + +void BaseInstance::setFlag(const BaseInstance::InstanceFlag flag) +{ + // nothing to set? + if(flag & m_flags) + return; + m_flags |= flag; + emit flagsChanged(); + emit propertiesChanged(this); +} + +void BaseInstance::unsetFlag(const BaseInstance::InstanceFlag flag) +{ + // nothing to unset? + if(!(flag & m_flags)) + return; + m_flags &= ~flag; + emit flagsChanged(); + emit propertiesChanged(this); +} + +bool BaseInstance::canLaunch() const +{ + return !(flags() & VersionBrokenFlag); +} + +bool BaseInstance::reload() +{ + return m_settings->reload(); +} + +qint64 BaseInstance::lastLaunch() const +{ + return m_settings->get("lastLaunchTime").value(); +} + +void BaseInstance::setLastLaunch(qint64 val) +{ + //FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("lastLaunchTime", val); + emit propertiesChanged(this); +} + +void BaseInstance::setGroupInitial(QString val) +{ + if(m_group == val) + { + return; + } + m_group = val; + emit propertiesChanged(this); +} + +void BaseInstance::setGroupPost(QString val) +{ + if(m_group == val) + { + return; + } + setGroupInitial(val); + emit groupChanged(); +} + +QString BaseInstance::group() const +{ + return m_group; +} + +void BaseInstance::setNotes(QString val) +{ + //FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("notes", val); +} + +QString BaseInstance::notes() const +{ + return m_settings->get("notes").toString(); +} + +void BaseInstance::setIconKey(QString val) +{ + //FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("iconKey", val); + emit propertiesChanged(this); +} + +QString BaseInstance::iconKey() const +{ + return m_settings->get("iconKey").toString(); +} + +void BaseInstance::setName(QString val) +{ + //FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("name", val); + emit propertiesChanged(this); +} + +QString BaseInstance::name() const +{ + return m_settings->get("name").toString(); +} + +QString BaseInstance::windowTitle() const +{ + return "MultiMC: " + name(); +} + +QStringList BaseInstance::extraArguments() const +{ + return Commandline::splitArgs(settings()->get("JvmArgs").toString()); +} diff --git a/api/logic/BaseInstance.h b/api/logic/BaseInstance.h new file mode 100644 index 00000000..5e587c48 --- /dev/null +++ b/api/logic/BaseInstance.h @@ -0,0 +1,243 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "settings/SettingsObject.h" + +#include "settings/INIFile.h" +#include "BaseVersionList.h" +#include "minecraft/auth/MojangAccount.h" +#include "launch/MessageLevel.h" +#include "pathmatcher/IPathMatcher.h" + +#include "multimc_logic_export.h" + +class QDir; +class Task; +class LaunchTask; +class BaseInstance; + +// pointer for lazy people +typedef std::shared_ptr InstancePtr; + +/*! + * \brief Base class for instances. + * This class implements many functions that are common between instances and + * provides a standard interface for all instances. + * + * To create a new instance type, create a new class inheriting from this class + * and implement the pure virtual functions. + */ +class MULTIMC_LOGIC_EXPORT BaseInstance : public QObject, public std::enable_shared_from_this +{ + Q_OBJECT +protected: + /// no-touchy! + BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + +public: + /// virtual destructor to make sure the destruction is COMPLETE + virtual ~BaseInstance() {}; + + virtual void copy(const QDir &newDir) {} + + virtual void init() = 0; + + /// nuke thoroughly - deletes the instance contents, notifies the list/model which is + /// responsible of cleaning up the husk + void nuke(); + + /// The instance's ID. The ID SHALL be determined by MMC internally. The ID IS guaranteed to + /// be unique. + virtual QString id() const; + + void setRunning(bool running); + bool isRunning() const; + int64_t totalTimePlayed() const; + void resetTimePlayed(); + + /// get the type of this instance + QString instanceType() const; + + /// Path to the instance's root directory. + QString instanceRoot() const; + + QString name() const; + void setName(QString val); + + /// Value used for instance window titles + QString windowTitle() const; + + QString iconKey() const; + void setIconKey(QString val); + + QString notes() const; + void setNotes(QString val); + + QString group() const; + void setGroupInitial(QString val); + void setGroupPost(QString val); + + QString getPreLaunchCommand(); + QString getPostExitCommand(); + QString getWrapperCommand(); + + /// guess log level from a line of game log + virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) + { + return level; + }; + + virtual QStringList extraArguments() const; + + virtual QString intendedVersionId() const = 0; + virtual bool setIntendedVersionId(QString version) = 0; + + /*! + * The instance's current version. + * This value represents the instance's current version. If this value is + * different from the intendedVersion, the instance should be updated. + * \warning Don't change this value unless you know what you're doing. + */ + virtual QString currentVersionId() const = 0; + + /*! + * Whether or not 'the game' should be downloaded when the instance is launched. + */ + virtual bool shouldUpdate() const = 0; + virtual void setShouldUpdate(bool val) = 0; + + /// Traits. Normally inside the version, depends on instance implementation. + virtual QSet traits() = 0; + + /** + * Gets the time that the instance was last launched. + * Stored in milliseconds since epoch. + */ + qint64 lastLaunch() const; + /// Sets the last launched time to 'val' milliseconds since epoch + void setLastLaunch(qint64 val = QDateTime::currentMSecsSinceEpoch()); + + InstancePtr getSharedPtr(); + + /*! + * \brief Gets a pointer to this instance's version list. + * \return A pointer to the available version list for this instance. + */ + virtual std::shared_ptr versionList() const = 0; + + /*! + * \brief Gets this instance's settings object. + * This settings object stores instance-specific settings. + * \return A pointer to this instance's settings object. + */ + virtual SettingsObjectPtr settings() const; + + /// returns a valid update task + virtual std::shared_ptr createUpdateTask() = 0; + + /// returns a valid launcher (task container) + virtual std::shared_ptr createLaunchTask(AuthSessionPtr account) = 0; + + /*! + * Returns a task that should be done right before launch + * This task should do any extra preparations needed + */ + virtual std::shared_ptr createJarModdingTask() = 0; + + /*! + * Create envrironment variables for running the instance + */ + virtual QProcessEnvironment createEnvironment() = 0; + + /*! + * Returns a matcher that can maps relative paths within the instance to whether they are 'log files' + */ + virtual IPathMatcher::Ptr getLogFileMatcher() = 0; + + /*! + * Returns the root folder to use for looking up log files + */ + virtual QString getLogFileRoot() = 0; + + /*! + * does any necessary cleanups after the instance finishes. also runs before\ + * TODO: turn into a task that can run asynchronously + */ + virtual void cleanupAfterRun() = 0; + + virtual QString getStatusbarDescription() = 0; + + /// FIXME: this really should be elsewhere... + virtual QString instanceConfigFolder() const = 0; + + /// get variables this instance exports + virtual QMap getVariables() const = 0; + + virtual QString typeName() const = 0; + + enum InstanceFlag + { + VersionBrokenFlag = 0x01, + UpdateAvailable = 0x02 + }; + Q_DECLARE_FLAGS(InstanceFlags, InstanceFlag) + InstanceFlags flags() const; + void setFlags(const InstanceFlags &flags); + void setFlag(const InstanceFlag flag); + void unsetFlag(const InstanceFlag flag); + + bool canLaunch() const; + virtual bool canExport() const = 0; + + virtual bool reload(); + +signals: + /*! + * \brief Signal emitted when properties relevant to the instance view change + */ + void propertiesChanged(BaseInstance *inst); + /*! + * \brief Signal emitted when groups are affected in any way + */ + void groupChanged(); + /*! + * \brief The instance just got nuked. Hurray! + */ + void nuked(BaseInstance *inst); + + void flagsChanged(); + +protected slots: + void iconUpdated(QString key); + +protected: + QString m_rootDir; + QString m_group; + SettingsObjectPtr m_settings; + InstanceFlags m_flags; + bool m_isRunning = false; + QDateTime m_timeStarted; +}; + +Q_DECLARE_METATYPE(std::shared_ptr) +Q_DECLARE_METATYPE(BaseInstance::InstanceFlag) +Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags) diff --git a/api/logic/BaseVersion.h b/api/logic/BaseVersion.h new file mode 100644 index 00000000..80767518 --- /dev/null +++ b/api/logic/BaseVersion.h @@ -0,0 +1,59 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +/*! + * 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 BaseVersionPtr; + +Q_DECLARE_METATYPE(BaseVersionPtr) diff --git a/api/logic/BaseVersionList.cpp b/api/logic/BaseVersionList.cpp new file mode 100644 index 00000000..b34f318c --- /dev/null +++ b/api/logic/BaseVersionList.cpp @@ -0,0 +1,104 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseVersionList.h" +#include "BaseVersion.h" + +BaseVersionList::BaseVersionList(QObject *parent) : QAbstractListModel(parent) +{ +} + +BaseVersionPtr BaseVersionList::findVersion(const QString &descriptor) +{ + for (int i = 0; i < count(); i++) + { + if (at(i)->descriptor() == descriptor) + return at(i); + } + return BaseVersionPtr(); +} + +BaseVersionPtr BaseVersionList::getLatestStable() const +{ + if (count() <= 0) + return BaseVersionPtr(); + else + return at(0); +} + +BaseVersionPtr BaseVersionList::getRecommended() const +{ + return getLatestStable(); +} + +QVariant BaseVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + BaseVersionPtr version = at(index.row()); + + switch (role) + { + case VersionPointerRole: + return qVariantFromValue(version); + + case VersionRole: + return version->name(); + + case VersionIdRole: + return version->descriptor(); + + case TypeRole: + return version->typeString(); + + default: + return QVariant(); + } +} + +BaseVersionList::RoleList BaseVersionList::providesRoles() const +{ + return {VersionPointerRole, VersionRole, VersionIdRole, TypeRole}; +} + +int BaseVersionList::rowCount(const QModelIndex &parent) const +{ + // Return count + return count(); +} + +int BaseVersionList::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QHash BaseVersionList::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles.insert(VersionRole, "version"); + roles.insert(VersionIdRole, "versionId"); + roles.insert(ParentGameVersionRole, "parentGameVersion"); + roles.insert(RecommendedRole, "recommended"); + roles.insert(LatestRole, "latest"); + roles.insert(TypeRole, "type"); + roles.insert(BranchRole, "branch"); + roles.insert(PathRole, "path"); + roles.insert(ArchitectureRole, "architecture"); + return roles; +} diff --git a/api/logic/BaseVersionList.h b/api/logic/BaseVersionList.h new file mode 100644 index 00000000..73d2ee1f --- /dev/null +++ b/api/logic/BaseVersionList.h @@ -0,0 +1,126 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "BaseVersion.h" +#include "tasks/Task.h" +#include "multimc_logic_export.h" + +/*! + * \brief Class that each instance type's version list derives from. + * Version lists are the lists that keep track of the available game versions + * for that instance. This list will not be loaded on startup. It will be loaded + * when the list's load function is called. Before using the version list, you + * should check to see if it has been loaded yet and if not, load the list. + * + * Note that this class also inherits from QAbstractListModel. Methods from that + * class determine how this version list shows up in a list view. Said methods + * all have a default implementation, but they can be overridden by plugins to + * change the behavior of the list. + */ +class MULTIMC_LOGIC_EXPORT BaseVersionList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + VersionPointerRole = Qt::UserRole, + VersionRole, + VersionIdRole, + ParentGameVersionRole, + RecommendedRole, + LatestRole, + TypeRole, + BranchRole, + PathRole, + ArchitectureRole, + SortRole + }; + typedef QList RoleList; + + explicit BaseVersionList(QObject *parent = 0); + + /*! + * \brief Gets a task that will reload the version list. + * Simply execute the task to load the list. + * The task returned by this function should reset the model when it's done. + * \return A pointer to a task that reloads the version list. + */ + virtual Task *getLoadTask() = 0; + + //! Checks whether or not the list is loaded. If this returns false, the list should be + //loaded. + virtual bool isLoaded() = 0; + + //! Gets the version at the given index. + virtual const BaseVersionPtr at(int i) const = 0; + + //! Returns the number of versions in the list. + virtual int count() const = 0; + + //////// List Model Functions //////// + virtual QVariant data(const QModelIndex &index, int role) const; + virtual int rowCount(const QModelIndex &parent) const; + virtual int columnCount(const QModelIndex &parent) const; + virtual QHash roleNames() const override; + + //! which roles are provided by this version list? + virtual RoleList providesRoles() const; + + /*! + * \brief Finds a version by its descriptor. + * \param The descriptor of the version to find. + * \return A const pointer to the version with the given descriptor. NULL if + * one doesn't exist. + */ + virtual BaseVersionPtr findVersion(const QString &descriptor); + + /*! + * \brief Gets the latest stable version from this list + */ + virtual BaseVersionPtr getLatestStable() const; + + /*! + * \brief Gets the recommended version from this list + * If the list doesn't support recommended versions, this works exactly as getLatestStable + */ + virtual BaseVersionPtr getRecommended() const; + + /*! + * Sorts the version list. + */ + virtual void sortVersions() = 0; + +protected +slots: + /*! + * Updates this list with the given list of versions. + * This is done by copying each version in the given list and inserting it + * into this one. + * We need to do this so that we can set the parents of the versions are set to this + * version list. This can't be done in the load task, because the versions the load + * task creates are on the load task's thread and Qt won't allow their parents + * to be set to something created on another thread. + * To get around that problem, we invoke this method on the GUI thread, which + * then copies the versions and sets their parents correctly. + * \param versions List of versions whose parents should be set. + */ + virtual void updateListData(QList versions) = 0; +}; diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt new file mode 100644 index 00000000..317627d5 --- /dev/null +++ b/api/logic/CMakeLists.txt @@ -0,0 +1,344 @@ +project(MultiMC_logic) + +set(LOGIC_SOURCES + # LOGIC - Base classes and infrastructure + BaseInstaller.h + BaseInstaller.cpp + BaseVersionList.h + BaseVersionList.cpp + InstanceList.h + InstanceList.cpp + BaseVersion.h + BaseInstance.h + BaseInstance.cpp + NullInstance.h + MMCZip.h + MMCZip.cpp + MMCStrings.h + MMCStrings.cpp + BaseConfigObject.h + BaseConfigObject.cpp + AbstractCommonModel.h + AbstractCommonModel.cpp + TypeMagic.h + + # Prefix tree where node names are strings between separators + SeparatorPrefixTree.h + + # WARNING: globals live here + Env.h + Env.cpp + + # JSON parsing helpers + Json.h + Json.cpp + + FileSystem.h + FileSystem.cpp + + Exception.h + + # RW lock protected map + RWStorage.h + + # A variable that has an implicit default value and keeps track of changes + DefaultVariable.h + + # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms + QObjectPtr.h + + # Resources + resources/Resource.cpp + resources/Resource.h + resources/ResourceHandler.cpp + resources/ResourceHandler.h + resources/ResourceObserver.cpp + resources/ResourceObserver.h + resources/ResourceProxyModel.h + resources/ResourceProxyModel.cpp + + # Path matchers + pathmatcher/FSTreeMatcher.h + pathmatcher/IPathMatcher.h + pathmatcher/MultiMatcher.h + pathmatcher/RegexpMatcher.h + + # Compression support + GZip.h + GZip.cpp + + # Command line parameter parsing + Commandline.h + Commandline.cpp + + # Version number string support + Version.h + Version.cpp + + # network stuffs + net/NetAction.h + net/MD5EtagDownload.h + net/MD5EtagDownload.cpp + net/ByteArrayDownload.h + net/ByteArrayDownload.cpp + net/CacheDownload.h + net/CacheDownload.cpp + net/NetJob.h + net/NetJob.cpp + net/HttpMetaCache.h + net/HttpMetaCache.cpp + net/PasteUpload.h + net/PasteUpload.cpp + net/URLConstants.h + net/URLConstants.cpp + + # Yggdrasil login stuff + minecraft/auth/AuthSession.h + minecraft/auth/AuthSession.cpp + minecraft/auth/MojangAccountList.h + minecraft/auth/MojangAccountList.cpp + minecraft/auth/MojangAccount.h + minecraft/auth/MojangAccount.cpp + minecraft/auth/YggdrasilTask.h + minecraft/auth/YggdrasilTask.cpp + minecraft/auth/flows/AuthenticateTask.h + minecraft/auth/flows/AuthenticateTask.cpp + minecraft/auth/flows/RefreshTask.cpp + minecraft/auth/flows/RefreshTask.cpp + minecraft/auth/flows/ValidateTask.h + minecraft/auth/flows/ValidateTask.cpp + + # Game launch logic + launch/steps/CheckJava.cpp + launch/steps/CheckJava.h + launch/steps/LaunchMinecraft.cpp + launch/steps/LaunchMinecraft.h + launch/steps/ModMinecraftJar.cpp + launch/steps/ModMinecraftJar.h + launch/steps/PostLaunchCommand.cpp + launch/steps/PostLaunchCommand.h + launch/steps/PreLaunchCommand.cpp + launch/steps/PreLaunchCommand.h + launch/steps/TextPrint.cpp + launch/steps/TextPrint.h + launch/steps/Update.cpp + launch/steps/Update.h + launch/LaunchStep.cpp + launch/LaunchStep.h + launch/LaunchTask.cpp + launch/LaunchTask.h + launch/LoggedProcess.cpp + launch/LoggedProcess.h + launch/MessageLevel.cpp + launch/MessageLevel.h + + # Update system + updater/GoUpdate.h + updater/GoUpdate.cpp + updater/UpdateChecker.h + updater/UpdateChecker.cpp + updater/DownloadTask.h + updater/DownloadTask.cpp + + # Notifications - short warning messages + notifications/NotificationChecker.h + notifications/NotificationChecker.cpp + + # News System + news/NewsChecker.h + news/NewsChecker.cpp + news/NewsEntry.h + news/NewsEntry.cpp + + # Status system + status/StatusChecker.h + status/StatusChecker.cpp + + # Minecraft support + minecraft/onesix/OneSixUpdate.h + minecraft/onesix/OneSixUpdate.cpp + minecraft/onesix/OneSixInstance.h + minecraft/onesix/OneSixInstance.cpp + minecraft/onesix/OneSixProfileStrategy.cpp + minecraft/onesix/OneSixProfileStrategy.h + minecraft/onesix/OneSixVersionFormat.cpp + minecraft/onesix/OneSixVersionFormat.h + minecraft/legacy/LegacyUpdate.h + minecraft/legacy/LegacyUpdate.cpp + minecraft/legacy/LegacyInstance.h + minecraft/legacy/LegacyInstance.cpp + minecraft/legacy/LwjglVersionList.h + minecraft/legacy/LwjglVersionList.cpp + minecraft/GradleSpecifier.h + minecraft/MinecraftProfile.cpp + minecraft/MinecraftProfile.h + minecraft/MojangVersionFormat.cpp + minecraft/MojangVersionFormat.h + minecraft/JarMod.h + minecraft/MinecraftInstance.cpp + minecraft/MinecraftInstance.h + minecraft/MinecraftVersion.cpp + minecraft/MinecraftVersion.h + minecraft/MinecraftVersionList.cpp + minecraft/MinecraftVersionList.h + minecraft/Rule.cpp + minecraft/Rule.h + minecraft/OpSys.cpp + minecraft/OpSys.h + minecraft/ParseUtils.cpp + minecraft/ParseUtils.h + minecraft/ProfileUtils.cpp + minecraft/ProfileUtils.h + minecraft/ProfileStrategy.h + minecraft/Library.cpp + minecraft/Library.h + minecraft/MojangDownloadInfo.h + minecraft/VersionBuildError.h + minecraft/VersionFile.cpp + minecraft/VersionFile.h + minecraft/ProfilePatch.h + minecraft/VersionFilterData.h + minecraft/VersionFilterData.cpp + minecraft/Mod.h + minecraft/Mod.cpp + minecraft/ModList.h + minecraft/ModList.cpp + minecraft/World.h + minecraft/World.cpp + minecraft/WorldList.h + minecraft/WorldList.cpp + + # FTB + minecraft/ftb/OneSixFTBInstance.h + minecraft/ftb/OneSixFTBInstance.cpp + minecraft/ftb/LegacyFTBInstance.h + minecraft/ftb/LegacyFTBInstance.cpp + minecraft/ftb/FTBProfileStrategy.h + minecraft/ftb/FTBProfileStrategy.cpp + minecraft/ftb/FTBPlugin.h + minecraft/ftb/FTBPlugin.cpp + + # A Recursive file system watcher + RecursiveFileSystemWatcher.h + RecursiveFileSystemWatcher.cpp + + # the screenshots feature + screenshots/Screenshot.h + screenshots/ImgurUpload.h + screenshots/ImgurUpload.cpp + screenshots/ImgurAlbumCreation.h + screenshots/ImgurAlbumCreation.cpp + + # Tasks + tasks/Task.h + tasks/Task.cpp + tasks/ThreadTask.h + tasks/ThreadTask.cpp + tasks/SequentialTask.h + tasks/SequentialTask.cpp + + # Settings + settings/INIFile.cpp + settings/INIFile.h + settings/INISettingsObject.cpp + settings/INISettingsObject.h + settings/OverrideSetting.cpp + settings/OverrideSetting.h + settings/PassthroughSetting.cpp + settings/PassthroughSetting.h + settings/Setting.cpp + settings/Setting.h + settings/SettingsObject.cpp + settings/SettingsObject.h + + # Java related code + java/JavaChecker.h + java/JavaChecker.cpp + java/JavaCheckerJob.h + java/JavaCheckerJob.cpp + java/JavaInstall.h + java/JavaInstall.cpp + java/JavaInstallList.h + java/JavaInstallList.cpp + java/JavaUtils.h + java/JavaUtils.cpp + java/JavaVersion.h + java/JavaVersion.cpp + + # Assets + minecraft/AssetsUtils.h + minecraft/AssetsUtils.cpp + + # Forge and all things forge related + minecraft/forge/ForgeVersion.h + minecraft/forge/ForgeVersion.cpp + minecraft/forge/ForgeVersionList.h + minecraft/forge/ForgeVersionList.cpp + minecraft/forge/ForgeXzDownload.h + minecraft/forge/ForgeXzDownload.cpp + minecraft/forge/LegacyForge.h + minecraft/forge/LegacyForge.cpp + minecraft/forge/ForgeInstaller.h + minecraft/forge/ForgeInstaller.cpp + + # Liteloader and related things + minecraft/liteloader/LiteLoaderInstaller.h + minecraft/liteloader/LiteLoaderInstaller.cpp + minecraft/liteloader/LiteLoaderVersionList.h + minecraft/liteloader/LiteLoaderVersionList.cpp + + # Translations + trans/TranslationDownloader.h + trans/TranslationDownloader.cpp + + # Tools + tools/BaseExternalTool.cpp + tools/BaseExternalTool.h + tools/BaseProfiler.cpp + tools/BaseProfiler.h + tools/JProfiler.cpp + tools/JProfiler.h + tools/JVisualVM.cpp + tools/JVisualVM.h + tools/MCEditTool.cpp + tools/MCEditTool.h + + # Wonko + wonko/tasks/BaseWonkoEntityRemoteLoadTask.cpp + wonko/tasks/BaseWonkoEntityRemoteLoadTask.h + wonko/tasks/BaseWonkoEntityLocalLoadTask.cpp + wonko/tasks/BaseWonkoEntityLocalLoadTask.h + wonko/format/WonkoFormatV1.cpp + wonko/format/WonkoFormatV1.h + wonko/format/WonkoFormat.cpp + wonko/format/WonkoFormat.h + wonko/BaseWonkoEntity.cpp + wonko/BaseWonkoEntity.h + wonko/WonkoVersionList.cpp + wonko/WonkoVersionList.h + wonko/WonkoVersion.cpp + wonko/WonkoVersion.h + wonko/WonkoIndex.cpp + wonko/WonkoIndex.h + wonko/WonkoUtil.cpp + wonko/WonkoUtil.h + wonko/WonkoReference.cpp + wonko/WonkoReference.h +) +################################ COMPILE ################################ + +# we need zlib +find_package(ZLIB REQUIRED) + +add_library(MultiMC_logic SHARED ${LOGIC_SOURCES}) +set_target_properties(MultiMC_logic PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1) + +generate_export_header(MultiMC_logic) + +# Link +target_link_libraries(MultiMC_logic xz-embedded unpack200 ${QUAZIP_LIBRARIES} nbt++ ${ZLIB_LIBRARIES}) +qt5_use_modules(MultiMC_logic Core Xml Network Concurrent) +add_dependencies(MultiMC_logic QuaZIP) + +# Mark and export headers +target_include_directories(MultiMC_logic PUBLIC "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}" PRIVATE "${ZLIB_INCLUDE_DIRS}") diff --git a/api/logic/Commandline.cpp b/api/logic/Commandline.cpp new file mode 100644 index 00000000..9a8ddbf1 --- /dev/null +++ b/api/logic/Commandline.cpp @@ -0,0 +1,483 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Commandline.h" + +/** + * @file libutil/src/cmdutils.cpp + */ + +namespace Commandline +{ + +// commandline splitter +QStringList splitArgs(QString args) +{ + QStringList argv; + QString current; + bool escape = false; + QChar inquotes; + for (int i = 0; i < args.length(); i++) + { + QChar cchar = args.at(i); + + // \ escaped + if (escape) + { + current += cchar; + escape = false; + // in "quotes" + } + else if (!inquotes.isNull()) + { + if (cchar == 0x5C) + escape = true; + else if (cchar == inquotes) + inquotes = 0; + else + current += cchar; + // otherwise + } + else + { + if (cchar == 0x20) + { + if (!current.isEmpty()) + { + argv << current; + current.clear(); + } + } + else if (cchar == 0x22 || cchar == 0x27) + inquotes = cchar; + else + current += cchar; + } + } + if (!current.isEmpty()) + argv << current; + return argv; +} + +Parser::Parser(FlagStyle::Enum flagStyle, ArgumentStyle::Enum argStyle) +{ + m_flagStyle = flagStyle; + m_argStyle = argStyle; +} + +// styles setter/getter +void Parser::setArgumentStyle(ArgumentStyle::Enum style) +{ + m_argStyle = style; +} +ArgumentStyle::Enum Parser::argumentStyle() +{ + return m_argStyle; +} + +void Parser::setFlagStyle(FlagStyle::Enum style) +{ + m_flagStyle = style; +} +FlagStyle::Enum Parser::flagStyle() +{ + return m_flagStyle; +} + +// setup methods +void Parser::addSwitch(QString name, bool def) +{ + if (m_params.contains(name)) + throw "Name not unique"; + + OptionDef *param = new OptionDef; + param->type = otSwitch; + param->name = name; + param->metavar = QString("<%1>").arg(name); + param->def = def; + + m_options[name] = param; + m_params[name] = (CommonDef *)param; + m_optionList.append(param); +} + +void Parser::addOption(QString name, QVariant def) +{ + if (m_params.contains(name)) + throw "Name not unique"; + + OptionDef *param = new OptionDef; + param->type = otOption; + param->name = name; + param->metavar = QString("<%1>").arg(name); + param->def = def; + + m_options[name] = param; + m_params[name] = (CommonDef *)param; + m_optionList.append(param); +} + +void Parser::addArgument(QString name, bool required, QVariant def) +{ + if (m_params.contains(name)) + throw "Name not unique"; + + PositionalDef *param = new PositionalDef; + param->name = name; + param->def = def; + param->required = required; + param->metavar = name; + + m_positionals.append(param); + m_params[name] = (CommonDef *)param; +} + +void Parser::addDocumentation(QString name, QString doc, QString metavar) +{ + if (!m_params.contains(name)) + throw "Name does not exist"; + + CommonDef *param = m_params[name]; + param->doc = doc; + if (!metavar.isNull()) + param->metavar = metavar; +} + +void Parser::addShortOpt(QString name, QChar flag) +{ + if (!m_params.contains(name)) + throw "Name does not exist"; + if (!m_options.contains(name)) + throw "Name is not an Option or Swtich"; + + OptionDef *param = m_options[name]; + m_flags[flag] = param; + param->flag = flag; +} + +// help methods +QString Parser::compileHelp(QString progName, int helpIndent, bool useFlags) +{ + QStringList help; + help << compileUsage(progName, useFlags) << "\r\n"; + + // positionals + if (!m_positionals.isEmpty()) + { + help << "\r\n"; + help << "Positional arguments:\r\n"; + QListIterator 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 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 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 it2(m_positionals); + while (it2.hasNext()) + { + PositionalDef *param = it2.next(); + usage << " " << (param->required ? "<" : "["); + usage << param->metavar; + usage << (param->required ? ">" : "]"); + } + + return usage.join(""); +} + +// parsing +QHash Parser::parse(QStringList argv) +{ + QHash map; + + QStringListIterator it(argv); + QString programName = it.next(); + + QString optionPrefix; + QString flagPrefix; + QListIterator 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 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 it(m_optionList); + while (it.hasNext()) + { + OptionDef *option = it.next(); + it.remove(); + delete option; + } + + QMutableListIterator it2(m_positionals); + while (it2.hasNext()) + { + PositionalDef *arg = it2.next(); + it2.remove(); + delete arg; + } +} + +// Destructor +Parser::~Parser() +{ + clear(); +} + +// getPrefix +void Parser::getPrefix(QString &opt, QString &flag) +{ + if (m_flagStyle == FlagStyle::Windows) + opt = flag = "/"; + else if (m_flagStyle == FlagStyle::Unix) + opt = flag = "-"; + // else if (m_flagStyle == FlagStyle::GNU) + else + { + opt = "--"; + flag = "-"; + } +} + +// ParsingError +ParsingError::ParsingError(const QString &what) : std::runtime_error(what.toStdString()) +{ +} +} \ No newline at end of file diff --git a/api/logic/Commandline.h b/api/logic/Commandline.h new file mode 100644 index 00000000..bee02bad --- /dev/null +++ b/api/logic/Commandline.h @@ -0,0 +1,252 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 +#include + +#include +#include +#include +#include + +#include "multimc_logic_export.h" + +/** + * @file libutil/include/cmdutils.h + * @brief commandline parsing and processing utilities + */ + +namespace Commandline +{ + +/** + * @brief split a string into argv items like a shell would do + * @param args the argument string + * @return a QStringList containing all arguments + */ +MULTIMC_LOGIC_EXPORT QStringList splitArgs(QString args); + +/** + * @brief The FlagStyle enum + * Specifies how flags are decorated + */ + +namespace FlagStyle +{ +enum Enum +{ + GNU, /**< --option and -o (GNU Style) */ + Unix, /**< -option and -o (Unix Style) */ + Windows, /**< /option and /o (Windows Style) */ +#ifdef Q_OS_WIN32 + Default = Windows +#else + Default = GNU +#endif +}; +} + +/** + * @brief The ArgumentStyle enum + */ +namespace ArgumentStyle +{ +enum Enum +{ + Space, /**< --option=value */ + Equals, /**< --option value */ + SpaceAndEquals, /**< --option[= ]value */ +#ifdef Q_OS_WIN32 + Default = Equals +#else + Default = SpaceAndEquals +#endif +}; +} + +/** + * @brief The ParsingError class + */ +class MULTIMC_LOGIC_EXPORT ParsingError : public std::runtime_error +{ +public: + ParsingError(const QString &what); +}; + +/** + * @brief The Parser class + */ +class MULTIMC_LOGIC_EXPORT Parser +{ +public: + /** + * @brief Parser constructor + * @param flagStyle the FlagStyle to use in this Parser + * @param argStyle the ArgumentStyle to use in this Parser + */ + Parser(FlagStyle::Enum flagStyle = FlagStyle::Default, + ArgumentStyle::Enum argStyle = ArgumentStyle::Default); + + /** + * @brief set the flag style + * @param style + */ + void setFlagStyle(FlagStyle::Enum style); + + /** + * @brief get the flag style + * @return + */ + FlagStyle::Enum flagStyle(); + + /** + * @brief set the argument style + * @param style + */ + void setArgumentStyle(ArgumentStyle::Enum style); + + /** + * @brief get the argument style + * @return + */ + ArgumentStyle::Enum argumentStyle(); + + /** + * @brief define a boolean switch + * @param name the parameter name + * @param def the default value + */ + void addSwitch(QString name, bool def = false); + + /** + * @brief define an option that takes an additional argument + * @param name the parameter name + * @param def the default value + */ + void addOption(QString name, QVariant def = QVariant()); + + /** + * @brief define a positional argument + * @param name the parameter name + * @param required wether this argument is required + * @param def the default value + */ + void addArgument(QString name, bool required = true, QVariant def = QVariant()); + + /** + * @brief adds a flag to an existing parameter + * @param name the (existing) parameter name + * @param flag the flag character + * @see addSwitch addArgument addOption + * Note: any one parameter can only have one flag + */ + void addShortOpt(QString name, QChar flag); + + /** + * @brief adds documentation to a Parameter + * @param name the parameter name + * @param metavar a string to be displayed as placeholder for the value + * @param doc a QString containing the documentation + * Note: on positional arguments, metavar replaces the name as displayed. + * on options , metavar replaces the value placeholder + */ + void addDocumentation(QString name, QString doc, QString metavar = QString()); + + /** + * @brief generate a help message + * @param progName the program name to use in the help message + * @param helpIndent how much the parameter documentation should be indented + * @param flagsInUsage whether we should use flags instead of options in the usage + * @return a help message + */ + QString compileHelp(QString progName, int helpIndent = 22, bool flagsInUsage = true); + + /** + * @brief generate a short usage message + * @param progName the program name to use in the usage message + * @param useFlags whether we should use flags instead of options + * @return a usage message + */ + QString compileUsage(QString progName, bool useFlags = true); + + /** + * @brief parse + * @param argv a QStringList containing the program ARGV + * @return a QHash mapping argument names to their values + */ + QHash 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 m_options; + QHash m_flags; + QHash m_params; + QList m_positionals; + QList m_optionList; + + void getPrefix(QString &opt, QString &flag); +}; +} diff --git a/api/logic/DefaultVariable.h b/api/logic/DefaultVariable.h new file mode 100644 index 00000000..38d7ecc2 --- /dev/null +++ b/api/logic/DefaultVariable.h @@ -0,0 +1,35 @@ +#pragma once + +template +class DefaultVariable +{ +public: + DefaultVariable(const T & value) + { + defaultValue = value; + } + DefaultVariable & operator =(const T & value) + { + currentValue = value; + is_default = currentValue == defaultValue; + is_explicit = true; + return *this; + } + operator const T &() const + { + return is_default ? defaultValue : currentValue; + } + bool isDefault() const + { + return is_default; + } + bool isExplicit() const + { + return is_explicit; + } +private: + T currentValue; + T defaultValue; + bool is_default = true; + bool is_explicit = false; +}; diff --git a/api/logic/Env.cpp b/api/logic/Env.cpp new file mode 100644 index 00000000..cc0c5981 --- /dev/null +++ b/api/logic/Env.cpp @@ -0,0 +1,222 @@ +#include "Env.h" +#include "net/HttpMetaCache.h" +#include "BaseVersion.h" +#include "BaseVersionList.h" +#include +#include +#include +#include +#include "tasks/Task.h" +#include "wonko/WonkoIndex.h" +#include + +/* + * The *NEW* global rat nest of an object. Handle with care. + */ + +Env::Env() +{ + m_qnam = std::make_shared(); +} + +void Env::destroy() +{ + m_metacache.reset(); + m_qnam.reset(); + m_versionLists.clear(); +} + +Env& Env::Env::getInstance() +{ + static Env instance; + return instance; +} + +std::shared_ptr< HttpMetaCache > Env::metacache() +{ + Q_ASSERT(m_metacache != nullptr); + return m_metacache; +} + +std::shared_ptr< QNetworkAccessManager > Env::qnam() +{ + return m_qnam; +} + +/* +class NullVersion : public BaseVersion +{ + Q_OBJECT +public: + virtual QString name() + { + return "null"; + } + virtual QString descriptor() + { + return "null"; + } + virtual QString typeString() const + { + return "Null"; + } +}; + +class NullTask: public Task +{ + Q_OBJECT +public: + virtual void executeTask() + { + emitFailed(tr("Nothing to do.")); + } +}; + +class NullVersionList: public BaseVersionList +{ + Q_OBJECT +public: + virtual const BaseVersionPtr at(int i) const + { + return std::make_shared(); + } + virtual int count() const + { + return 0; + }; + virtual Task* getLoadTask() + { + return new NullTask; + } + virtual bool isLoaded() + { + return false; + } + virtual void sort() + { + } + virtual void updateListData(QList< BaseVersionPtr >) + { + } +}; +*/ + +BaseVersionPtr Env::getVersion(QString component, QString version) +{ + auto list = getVersionList(component); + if(!list) + { + return nullptr; + } + return list->findVersion(version); +} + +std::shared_ptr< BaseVersionList > Env::getVersionList(QString component) +{ + auto iter = m_versionLists.find(component); + if(iter != m_versionLists.end()) + { + return *iter; + } + //return std::make_shared(); + return nullptr; +} + +void Env::registerVersionList(QString name, std::shared_ptr< BaseVersionList > vlist) +{ + m_versionLists[name] = vlist; +} + +std::shared_ptr Env::wonkoIndex() +{ + if (!m_wonkoIndex) + { + m_wonkoIndex = std::make_shared(); + } + return m_wonkoIndex; +} + + +void Env::initHttpMetaCache() +{ + m_metacache.reset(new HttpMetaCache("metacache")); + m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath()); + m_metacache->addBase("asset_objects", QDir("assets/objects").absolutePath()); + m_metacache->addBase("versions", QDir("versions").absolutePath()); + m_metacache->addBase("libraries", QDir("libraries").absolutePath()); + m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath()); + m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath()); + m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath()); + m_metacache->addBase("general", QDir("cache").absolutePath()); + m_metacache->addBase("skins", QDir("accounts/skins").absolutePath()); + m_metacache->addBase("root", QDir::currentPath()); + m_metacache->addBase("translations", QDir("translations").absolutePath()); + m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); + m_metacache->addBase("wonko", QDir("cache/wonko").absolutePath()); + m_metacache->Load(); +} + +void Env::updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password) +{ + // Set the application proxy settings. + if (proxyTypeStr == "SOCKS5") + { + QNetworkProxy::setApplicationProxy( + QNetworkProxy(QNetworkProxy::Socks5Proxy, addr, port, user, password)); + } + else if (proxyTypeStr == "HTTP") + { + QNetworkProxy::setApplicationProxy( + QNetworkProxy(QNetworkProxy::HttpProxy, addr, port, user, password)); + } + else if (proxyTypeStr == "None") + { + // If we have no proxy set, set no proxy and return. + QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::NoProxy)); + } + else + { + // If we have "Default" selected, set Qt to use the system proxy settings. + QNetworkProxyFactory::setUseSystemConfiguration(true); + } + + qDebug() << "Detecting proxy settings..."; + QNetworkProxy proxy = QNetworkProxy::applicationProxy(); + if (m_qnam.get()) + m_qnam->setProxy(proxy); + QString proxyDesc; + if (proxy.type() == QNetworkProxy::NoProxy) + { + qDebug() << "Using no proxy is an option!"; + return; + } + switch (proxy.type()) + { + case QNetworkProxy::DefaultProxy: + proxyDesc = "Default proxy: "; + break; + case QNetworkProxy::Socks5Proxy: + proxyDesc = "Socks5 proxy: "; + break; + case QNetworkProxy::HttpProxy: + proxyDesc = "HTTP proxy: "; + break; + case QNetworkProxy::HttpCachingProxy: + proxyDesc = "HTTP caching: "; + break; + case QNetworkProxy::FtpCachingProxy: + proxyDesc = "FTP caching: "; + break; + default: + proxyDesc = "DERP proxy: "; + break; + } + proxyDesc += QString("%3@%1:%2 pass %4") + .arg(proxy.hostName()) + .arg(proxy.port()) + .arg(proxy.user()) + .arg(proxy.password()); + qDebug() << proxyDesc; +} + +#include "Env.moc" diff --git a/api/logic/Env.h b/api/logic/Env.h new file mode 100644 index 00000000..4d8945d7 --- /dev/null +++ b/api/logic/Env.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +#include "multimc_logic_export.h" + +class QNetworkAccessManager; +class HttpMetaCache; +class BaseVersionList; +class BaseVersion; +class WonkoIndex; + +#if defined(ENV) + #undef ENV +#endif +#define ENV (Env::getInstance()) + +class MULTIMC_LOGIC_EXPORT Env +{ + friend class MultiMC; +private: + Env(); +public: + static Env& getInstance(); + + // call when Qt stuff is being torn down + void destroy(); + + std::shared_ptr qnam(); + + std::shared_ptr metacache(); + + /// init the cache. FIXME: possible future hook point + void initHttpMetaCache(); + + /// Updates the application proxy settings from the settings object. + void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password); + + /// get a version list by name + std::shared_ptr getVersionList(QString component); + + /// get a version by list name and version name + std::shared_ptr getVersion(QString component, QString version); + + void registerVersionList(QString name, std::shared_ptr vlist); + + std::shared_ptr wonkoIndex(); + + QString wonkoRootUrl() const { return m_wonkoRootUrl; } + void setWonkoRootUrl(const QString &url) { m_wonkoRootUrl = url; } + +protected: + std::shared_ptr m_qnam; + std::shared_ptr m_metacache; + QMap> m_versionLists; + std::shared_ptr m_wonkoIndex; + QString m_wonkoRootUrl; +}; diff --git a/api/logic/Exception.h b/api/logic/Exception.h new file mode 100644 index 00000000..30c7aa45 --- /dev/null +++ b/api/logic/Exception.h @@ -0,0 +1,34 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include +#include +#include + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT Exception : public std::exception +{ +public: + Exception(const QString &message) : std::exception(), m_message(message) + { + qCritical() << "Exception:" << message; + } + Exception(const Exception &other) + : std::exception(), m_message(other.cause()) + { + } + virtual ~Exception() noexcept {} + const char *what() const noexcept + { + return m_message.toLatin1().constData(); + } + QString cause() const + { + return m_message; + } + +private: + QString m_message; +}; diff --git a/api/logic/FileSystem.cpp b/api/logic/FileSystem.cpp new file mode 100644 index 00000000..049f1e38 --- /dev/null +++ b/api/logic/FileSystem.cpp @@ -0,0 +1,436 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "FileSystem.h" + +#include +#include +#include +#include +#include +#include + +namespace FS { + +void ensureExists(const QDir &dir) +{ + if (!QDir().mkpath(dir.absolutePath())) + { + throw FileSystemException("Unable to create directory " + dir.dirName() + " (" + + dir.absolutePath() + ")"); + } +} + +void write(const QString &filename, const QByteArray &data) +{ + ensureExists(QFileInfo(filename).dir()); + QSaveFile file(filename); + if (!file.open(QSaveFile::WriteOnly)) + { + throw FileSystemException("Couldn't open " + filename + " for writing: " + + file.errorString()); + } + if (data.size() != file.write(data)) + { + throw FileSystemException("Error writing data to " + filename + ": " + + file.errorString()); + } + if (!file.commit()) + { + throw FileSystemException("Error while committing data to " + filename + ": " + + file.errorString()); + } +} + +QByteArray read(const QString &filename) +{ + QFile file(filename); + if (!file.open(QFile::ReadOnly)) + { + throw FileSystemException("Unable to open " + filename + " for reading: " + + file.errorString()); + } + const qint64 size = file.size(); + QByteArray data(int(size), 0); + const qint64 ret = file.read(data.data(), size); + if (ret == -1 || ret != size) + { + throw FileSystemException("Error reading data from " + filename + ": " + + file.errorString()); + } + return data; +} + +bool ensureFilePathExists(QString filenamepath) +{ + QFileInfo a(filenamepath); + QDir dir; + QString ensuredPath = a.path(); + bool success = dir.mkpath(ensuredPath); + return success; +} + +bool ensureFolderPathExists(QString foldernamepath) +{ + QFileInfo a(foldernamepath); + QDir dir; + QString ensuredPath = a.filePath(); + bool success = dir.mkpath(ensuredPath); + return success; +} + +bool copy::operator()(const QString &offset) +{ + //NOTE always deep copy on windows. the alternatives are too messy. + #if defined Q_OS_WIN32 + m_followSymlinks = true; + #endif + + auto src = PathCombine(m_src.absolutePath(), offset); + auto dst = PathCombine(m_dst.absolutePath(), offset); + + QFileInfo currentSrc(src); + if (!currentSrc.exists()) + return false; + + if(!m_followSymlinks && currentSrc.isSymLink()) + { + qDebug() << "creating symlink" << src << " - " << dst; + if (!ensureFilePathExists(dst)) + { + qWarning() << "Cannot create path!"; + return false; + } + return QFile::link(currentSrc.symLinkTarget(), dst); + } + else if(currentSrc.isFile()) + { + qDebug() << "copying file" << src << " - " << dst; + if (!ensureFilePathExists(dst)) + { + qWarning() << "Cannot create path!"; + return false; + } + return QFile::copy(src, dst); + } + else if(currentSrc.isDir()) + { + qDebug() << "recursing" << offset; + if (!ensureFolderPathExists(dst)) + { + qWarning() << "Cannot create path!"; + return false; + } + QDir currentDir(src); + for(auto & f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) + { + auto inner_offset = PathCombine(offset, f); + // ignore and skip stuff that matches the blacklist. + if(m_blacklist && m_blacklist->matches(inner_offset)) + { + continue; + } + if(!operator()(inner_offset)) + { + return false; + } + } + } + else + { + qCritical() << "Copy ERROR: Unknown filesystem object:" << src; + return false; + } + return true; +} + + +#if defined Q_OS_WIN32 +#include +#include +#endif +bool deletePath(QString path) +{ + bool OK = true; + QDir dir(path); + + if (!dir.exists()) + { + return OK; + } + auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, + QDir::DirsFirst); + + for(auto & info: allEntries) + { +#if defined Q_OS_WIN32 + QString nativePath = QDir::toNativeSeparators(info.absoluteFilePath()); + auto wString = nativePath.toStdWString(); + DWORD dwAttrs = GetFileAttributesW(wString.c_str()); + // Windows: check for junctions, reparse points and other nasty things of that sort + if(dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) + { + if (info.isFile()) + { + OK &= QFile::remove(info.absoluteFilePath()); + } + else if (info.isDir()) + { + OK &= dir.rmdir(info.absoluteFilePath()); + } + } +#else + // We do not trust Qt with reparse points, but do trust it with unix symlinks. + if(info.isSymLink()) + { + OK &= QFile::remove(info.absoluteFilePath()); + } +#endif + else if (info.isDir()) + { + OK &= deletePath(info.absoluteFilePath()); + } + else if (info.isFile()) + { + OK &= QFile::remove(info.absoluteFilePath()); + } + else + { + OK = false; + qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath(); + } + } + OK &= dir.rmdir(dir.absolutePath()); + return OK; +} + + +QString PathCombine(QString path1, QString path2) +{ + if(!path1.size()) + return path2; + if(!path2.size()) + return path1; + return QDir::cleanPath(path1 + QDir::separator() + path2); +} + +QString PathCombine(QString path1, QString path2, QString path3) +{ + return PathCombine(PathCombine(path1, path2), path3); +} + +QString AbsolutePath(QString path) +{ + return QFileInfo(path).absolutePath(); +} + +QString ResolveExecutable(QString path) +{ + if (path.isEmpty()) + { + return QString(); + } + if(!path.contains('/')) + { + path = QStandardPaths::findExecutable(path); + } + QFileInfo pathInfo(path); + if(!pathInfo.exists() || !pathInfo.isExecutable()) + { + return QString(); + } + return pathInfo.absoluteFilePath(); +} + +/** + * Normalize path + * + * Any paths inside the current directory will be normalized to relative paths (to current) + * Other paths will be made absolute + */ +QString NormalizePath(QString path) +{ + QDir a = QDir::currentPath(); + QString currentAbsolute = a.absolutePath(); + + QDir b(path); + QString newAbsolute = b.absolutePath(); + + if (newAbsolute.startsWith(currentAbsolute)) + { + return a.relativeFilePath(newAbsolute); + } + else + { + return newAbsolute; + } +} + +QString badFilenameChars = "\"\\/?<>:*|!"; + +QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) +{ + for (int i = 0; i < string.length(); i++) + { + if (badFilenameChars.contains(string[i])) + { + string[i] = replaceWith; + } + } + return string; +} + +QString DirNameFromString(QString string, QString inDir) +{ + int num = 0; + QString baseName = RemoveInvalidFilenameChars(string, '-'); + QString dirName; + do + { + if(num == 0) + { + dirName = baseName; + } + else + { + dirName = baseName + QString::number(num);; + } + + // If it's over 9000 + if (num > 9000) + return ""; + num++; + } while (QFileInfo(PathCombine(inDir, dirName)).exists()); + return dirName; +} + +// Does the directory path contain any '!'? If yes, return true, otherwise false. +// (This is a problem for Java) +bool checkProblemticPathJava(QDir folder) +{ + QString pathfoldername = folder.absolutePath(); + return pathfoldername.contains("!", Qt::CaseInsensitive); +} + +#include +#include +#include + +// Win32 crap +#if defined Q_OS_WIN + +#include +#include +#include +#include +#include +#include +#include + +bool called_coinit = false; + +HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args) +{ + HRESULT hres; + + if (!called_coinit) + { + hres = CoInitialize(NULL); + called_coinit = true; + + if (!SUCCEEDED(hres)) + { + qWarning("Failed to initialize COM. Error 0x%08X", hres); + return hres; + } + } + + IShellLink *link; + hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, + (LPVOID *)&link); + + if (SUCCEEDED(hres)) + { + IPersistFile *persistFile; + + link->SetPath(targetPath); + link->SetArguments(args); + + hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile); + if (SUCCEEDED(hres)) + { + WCHAR wstr[MAX_PATH]; + + MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH); + + hres = persistFile->Save(wstr, TRUE); + persistFile->Release(); + } + link->Release(); + } + return hres; +} + +#endif + +QString getDesktopDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +} + +// Cross-platform Shortcut creation +bool createShortCut(QString location, QString dest, QStringList args, QString name, + QString icon) +{ +#if defined Q_OS_LINUX + location = PathCombine(location, name + ".desktop"); + + QFile f(location); + f.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream stream(&f); + + QString argstring; + if (!args.empty()) + argstring = " '" + args.join("' '") + "'"; + + stream << "[Desktop Entry]" + << "\n"; + stream << "Type=Application" + << "\n"; + stream << "TryExec=" << dest.toLocal8Bit() << "\n"; + stream << "Exec=" << dest.toLocal8Bit() << argstring.toLocal8Bit() << "\n"; + stream << "Name=" << name.toLocal8Bit() << "\n"; + stream << "Icon=" << icon.toLocal8Bit() << "\n"; + + stream.flush(); + f.close(); + + f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | + QFileDevice::ExeOther); + + return true; +#elif defined Q_OS_WIN + // TODO: Fix + // QFile file(PathCombine(location, name + ".lnk")); + // WCHAR *file_w; + // WCHAR *dest_w; + // WCHAR *args_w; + // file.fileName().toWCharArray(file_w); + // dest.toWCharArray(dest_w); + + // QString argStr; + // for (int i = 0; i < args.count(); i++) + // { + // argStr.append(args[i]); + // argStr.append(" "); + // } + // argStr.toWCharArray(args_w); + + // return SUCCEEDED(CreateLink(file_w, dest_w, args_w)); + return false; +#else + qWarning("Desktop Shortcuts not supported on your platform!"); + return false; +#endif +} +} diff --git a/api/logic/FileSystem.h b/api/logic/FileSystem.h new file mode 100644 index 00000000..80637f90 --- /dev/null +++ b/api/logic/FileSystem.h @@ -0,0 +1,123 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include "Exception.h" +#include "pathmatcher/IPathMatcher.h" + +#include "multimc_logic_export.h" +#include +#include + +namespace FS +{ + +class MULTIMC_LOGIC_EXPORT FileSystemException : public ::Exception +{ +public: + FileSystemException(const QString &message) : Exception(message) {} +}; + +/** + * write data to a file safely + */ +MULTIMC_LOGIC_EXPORT void write(const QString &filename, const QByteArray &data); + +/** + * read data from a file safely\ + */ +MULTIMC_LOGIC_EXPORT QByteArray read(const QString &filename); + +/** + * Creates all the folders in a path for the specified path + * last segment of the path is treated as a file name and is ignored! + */ +MULTIMC_LOGIC_EXPORT bool ensureFilePathExists(QString filenamepath); + +/** + * Creates all the folders in a path for the specified path + * last segment of the path is treated as a folder name and is created! + */ +MULTIMC_LOGIC_EXPORT bool ensureFolderPathExists(QString filenamepath); + +class MULTIMC_LOGIC_EXPORT copy +{ +public: + copy(const copy&) = delete; + copy(const QString & src, const QString & dst) + { + m_src = src; + m_dst = dst; + } + copy & followSymlinks(const bool follow) + { + m_followSymlinks = follow; + return *this; + } + copy & blacklist(const IPathMatcher * filter) + { + m_blacklist = filter; + return *this; + } + bool operator()() + { + return operator()(QString()); + } + +private: + bool operator()(const QString &offset); + +private: + bool m_followSymlinks = true; + const IPathMatcher * m_blacklist = nullptr; + QDir m_src; + QDir m_dst; +}; + +/** + * Delete a folder recursively + */ +MULTIMC_LOGIC_EXPORT bool deletePath(QString path); + +MULTIMC_LOGIC_EXPORT QString PathCombine(QString path1, QString path2); +MULTIMC_LOGIC_EXPORT QString PathCombine(QString path1, QString path2, QString path3); + +MULTIMC_LOGIC_EXPORT QString AbsolutePath(QString path); + +/** + * Resolve an executable + * + * Will resolve: + * single executable (by name) + * relative path + * absolute path + * + * @return absolute path to executable or null string + */ +MULTIMC_LOGIC_EXPORT QString ResolveExecutable(QString path); + +/** + * Normalize path + * + * Any paths inside the current directory will be normalized to relative paths (to current) + * Other paths will be made absolute + * + * Returns false if the path logic somehow filed (and normalizedPath in invalid) + */ +MULTIMC_LOGIC_EXPORT QString NormalizePath(QString path); + +MULTIMC_LOGIC_EXPORT QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); + +MULTIMC_LOGIC_EXPORT QString DirNameFromString(QString string, QString inDir = "."); + +/// Checks if the a given Path contains "!" +MULTIMC_LOGIC_EXPORT bool checkProblemticPathJava(QDir folder); + +// Get the Directory representing the User's Desktop +MULTIMC_LOGIC_EXPORT QString getDesktopDir(); + +// Create a shortcut at *location*, pointing to *dest* called with the arguments *args* +// call it *name* and assign it the icon *icon* +// return true if operation succeeded +MULTIMC_LOGIC_EXPORT bool createShortCut(QString location, QString dest, QStringList args, QString name, QString iconLocation); +} diff --git a/api/logic/GZip.cpp b/api/logic/GZip.cpp new file mode 100644 index 00000000..38605df6 --- /dev/null +++ b/api/logic/GZip.cpp @@ -0,0 +1,115 @@ +#include "GZip.h" +#include +#include + +bool GZip::unzip(const QByteArray &compressedBytes, QByteArray &uncompressedBytes) +{ + if (compressedBytes.size() == 0) + { + uncompressedBytes = compressedBytes; + return true; + } + + unsigned uncompLength = compressedBytes.size(); + uncompressedBytes.clear(); + uncompressedBytes.resize(uncompLength); + + z_stream strm; + memset(&strm, 0, sizeof(strm)); + strm.next_in = (Bytef *)compressedBytes.data(); + strm.avail_in = compressedBytes.size(); + + bool done = false; + + if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK) + { + return false; + } + + int err = Z_OK; + + while (!done) + { + // If our output buffer is too small + if (strm.total_out >= uncompLength) + { + uncompressedBytes.resize(uncompLength * 2); + uncompLength *= 2; + } + + strm.next_out = (Bytef *)(uncompressedBytes.data() + strm.total_out); + strm.avail_out = uncompLength - strm.total_out; + + // Inflate another chunk. + err = inflate(&strm, Z_SYNC_FLUSH); + if (err == Z_STREAM_END) + done = true; + else if (err != Z_OK) + { + break; + } + } + + if (inflateEnd(&strm) != Z_OK || !done) + { + return false; + } + + uncompressedBytes.resize(strm.total_out); + return true; +} + +bool GZip::zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes) +{ + if (uncompressedBytes.size() == 0) + { + compressedBytes = uncompressedBytes; + return true; + } + + unsigned compLength = std::min(uncompressedBytes.size(), 16); + compressedBytes.clear(); + compressedBytes.resize(compLength); + + z_stream zs; + memset(&zs, 0, sizeof(zs)); + + if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (16 + MAX_WBITS), 8, Z_DEFAULT_STRATEGY) != Z_OK) + { + return false; + } + + zs.next_in = (Bytef*)uncompressedBytes.data(); + zs.avail_in = uncompressedBytes.size(); + + int ret; + compressedBytes.resize(uncompressedBytes.size()); + + unsigned offset = 0; + unsigned temp = 0; + do + { + auto remaining = compressedBytes.size() - offset; + if(remaining < 1) + { + compressedBytes.resize(compressedBytes.size() * 2); + } + zs.next_out = (Bytef *) (compressedBytes.data() + offset); + temp = zs.avail_out = compressedBytes.size() - offset; + ret = deflate(&zs, Z_FINISH); + offset += temp - zs.avail_out; + } while (ret == Z_OK); + + compressedBytes.resize(offset); + + if (deflateEnd(&zs) != Z_OK) + { + return false; + } + + if (ret != Z_STREAM_END) + { + return false; + } + return true; +} \ No newline at end of file diff --git a/api/logic/GZip.h b/api/logic/GZip.h new file mode 100644 index 00000000..6993a222 --- /dev/null +++ b/api/logic/GZip.h @@ -0,0 +1,12 @@ +#pragma once +#include + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT GZip +{ +public: + static bool unzip(const QByteArray &compressedBytes, QByteArray &uncompressedBytes); + static bool zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes); +}; + diff --git a/api/logic/InstanceList.cpp b/api/logic/InstanceList.cpp new file mode 100644 index 00000000..783df660 --- /dev/null +++ b/api/logic/InstanceList.cpp @@ -0,0 +1,580 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "InstanceList.h" +#include "BaseInstance.h" + +//FIXME: this really doesn't belong *here* +#include "minecraft/onesix/OneSixInstance.h" +#include "minecraft/legacy/LegacyInstance.h" +#include "minecraft/ftb/FTBPlugin.h" +#include "minecraft/MinecraftVersion.h" +#include "settings/INISettingsObject.h" +#include "NullInstance.h" +#include "FileSystem.h" +#include "pathmatcher/RegexpMatcher.h" + +const static int GROUP_FILE_FORMAT_VERSION = 1; + +InstanceList::InstanceList(SettingsObjectPtr globalSettings, const QString &instDir, QObject *parent) + : QAbstractListModel(parent), m_instDir(instDir) +{ + m_globalSettings = globalSettings; + if (!QDir::current().exists(m_instDir)) + { + QDir::current().mkpath(m_instDir); + } +} + +InstanceList::~InstanceList() +{ +} + +int InstanceList::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_instances.count(); +} + +QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent); + if (row < 0 || row >= m_instances.size()) + return QModelIndex(); + return createIndex(row, column, (void *)m_instances.at(row).get()); +} + +QVariant InstanceList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + { + return QVariant(); + } + BaseInstance *pdata = static_cast(index.internalPointer()); + switch (role) + { + case InstancePointerRole: + { + QVariant v = qVariantFromValue((void *)pdata); + return v; + } + case InstanceIDRole: + { + return pdata->id(); + } + case Qt::DisplayRole: + { + return pdata->name(); + } + case Qt::ToolTipRole: + { + return pdata->instanceRoot(); + } + case Qt::DecorationRole: + { + return pdata->iconKey(); + } + // HACK: see GroupView.h in gui! + case GroupRole: + { + return pdata->group(); + } + default: + break; + } + return QVariant(); +} + +Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags f; + if (index.isValid()) + { + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable); + } + return f; +} + +void InstanceList::groupChanged() +{ + // save the groups. save all of them. + saveGroupList(); +} + +QStringList InstanceList::getGroups() +{ + return m_groups.toList(); +} + +void InstanceList::suspendGroupSaving() +{ + suspendedGroupSave = true; +} + +void InstanceList::resumeGroupSaving() +{ + if(suspendedGroupSave) + { + suspendedGroupSave = false; + if(queuedGroupSave) + { + saveGroupList(); + } + } +} + +void InstanceList::deleteGroup(const QString& name) +{ + for(auto & instance: m_instances) + { + auto instGroupName = instance->group(); + if(instGroupName == name) + { + instance->setGroupPost(QString()); + } + } +} + +void InstanceList::saveGroupList() +{ + if(suspendedGroupSave) + { + queuedGroupSave = true; + return; + } + + QString groupFileName = m_instDir + "/instgroups.json"; + QMap> groupMap; + for (auto instance : m_instances) + { + QString id = instance->id(); + QString group = instance->group(); + if (group.isEmpty()) + continue; + + // keep a list/set of groups for choosing + m_groups.insert(group); + + if (!groupMap.count(group)) + { + QSet set; + set.insert(id); + groupMap[group] = set; + } + else + { + QSet &set = groupMap[group]; + set.insert(id); + } + } + QJsonObject toplevel; + toplevel.insert("formatVersion", QJsonValue(QString("1"))); + QJsonObject groupsArr; + for (auto iter = groupMap.begin(); iter != groupMap.end(); iter++) + { + auto list = iter.value(); + auto name = iter.key(); + QJsonObject groupObj; + QJsonArray instanceArr; + groupObj.insert("hidden", QJsonValue(QString("false"))); + for (auto item : list) + { + instanceArr.append(QJsonValue(item)); + } + groupObj.insert("instances", instanceArr); + groupsArr.insert(name, groupObj); + } + toplevel.insert("groups", groupsArr); + QJsonDocument doc(toplevel); + try + { + FS::write(groupFileName, doc.toJson()); + } + catch(FS::FileSystemException & e) + { + qCritical() << "Failed to write instance group file :" << e.cause(); + } +} + +void InstanceList::loadGroupList(QMap &groupMap) +{ + QString groupFileName = m_instDir + "/instgroups.json"; + + // if there's no group file, fail + if (!QFileInfo(groupFileName).exists()) + return; + + QByteArray jsonData; + try + { + jsonData = FS::read(groupFileName); + } + catch (FS::FileSystemException & e) + { + qCritical() << "Failed to read instance group file :" << e.cause(); + return; + } + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); + + // if the json was bad, fail + if (error.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse instance group file: %1 at offset %2") + .arg(error.errorString(), QString::number(error.offset)) + .toUtf8(); + return; + } + + // if the root of the json wasn't an object, fail + if (!jsonDoc.isObject()) + { + qWarning() << "Invalid group file. Root entry should be an object."; + return; + } + + QJsonObject rootObj = jsonDoc.object(); + + // Make sure the format version matches, otherwise fail. + if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION) + return; + + // Get the groups. if it's not an object, fail + if (!rootObj.value("groups").isObject()) + { + qWarning() << "Invalid group list JSON: 'groups' should be an object."; + return; + } + + // Iterate through all the groups. + QJsonObject groupMapping = rootObj.value("groups").toObject(); + for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) + { + QString groupName = iter.key(); + + // If not an object, complain and skip to the next one. + if (!iter.value().isObject()) + { + qWarning() << QString("Group '%1' in the group list should " + "be an object.") + .arg(groupName) + .toUtf8(); + continue; + } + + QJsonObject groupObj = iter.value().toObject(); + if (!groupObj.value("instances").isArray()) + { + qWarning() << QString("Group '%1' in the group list is invalid. " + "It should contain an array " + "called 'instances'.") + .arg(groupName) + .toUtf8(); + continue; + } + + // keep a list/set of groups for choosing + m_groups.insert(groupName); + + // Iterate through the list of instances in the group. + QJsonArray instancesArray = groupObj.value("instances").toArray(); + + for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); + iter2++) + { + groupMap[(*iter2).toString()] = groupName; + } + } +} + +InstanceList::InstListError InstanceList::loadList() +{ + // load the instance groups + QMap groupMap; + loadGroupList(groupMap); + + QList tempList; + { + QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable, + QDirIterator::FollowSymlinks); + while (iter.hasNext()) + { + QString subDir = iter.next(); + if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) + continue; + qDebug() << "Loading MultiMC instance from " << subDir; + InstancePtr instPtr; + auto error = loadInstance(instPtr, subDir); + if(!continueProcessInstance(instPtr, error, subDir, groupMap)) + continue; + tempList.append(instPtr); + } + } + + // FIXME: generalize + FTBPlugin::loadInstances(m_globalSettings, groupMap, tempList); + + beginResetModel(); + m_instances.clear(); + for(auto inst: tempList) + { + inst->setParent(this); + connect(inst.get(), SIGNAL(propertiesChanged(BaseInstance *)), this, + SLOT(propertiesChanged(BaseInstance *))); + connect(inst.get(), SIGNAL(groupChanged()), this, SLOT(groupChanged())); + connect(inst.get(), SIGNAL(nuked(BaseInstance *)), this, + SLOT(instanceNuked(BaseInstance *))); + m_instances.append(inst); + } + endResetModel(); + emit dataIsInvalid(); + return NoError; +} + +/// Clear all instances. Triggers notifications. +void InstanceList::clear() +{ + beginResetModel(); + saveGroupList(); + m_instances.clear(); + endResetModel(); + emit dataIsInvalid(); +} + +void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) +{ + m_instDir = value.toString(); + loadList(); +} + +/// Add an instance. Triggers notifications, returns the new index +int InstanceList::add(InstancePtr t) +{ + beginInsertRows(QModelIndex(), m_instances.size(), m_instances.size()); + m_instances.append(t); + t->setParent(this); + connect(t.get(), SIGNAL(propertiesChanged(BaseInstance *)), this, + SLOT(propertiesChanged(BaseInstance *))); + connect(t.get(), SIGNAL(groupChanged()), this, SLOT(groupChanged())); + connect(t.get(), SIGNAL(nuked(BaseInstance *)), this, SLOT(instanceNuked(BaseInstance *))); + endInsertRows(); + return count() - 1; +} + +InstancePtr InstanceList::getInstanceById(QString instId) const +{ + if(instId.isEmpty()) + return InstancePtr(); + for(auto & inst: m_instances) + { + if (inst->id() == instId) + { + return inst; + } + } + return InstancePtr(); +} + +QModelIndex InstanceList::getInstanceIndexById(const QString &id) const +{ + return index(getInstIndex(getInstanceById(id).get())); +} + +int InstanceList::getInstIndex(BaseInstance *inst) const +{ + int count = m_instances.count(); + for (int i = 0; i < count; i++) + { + if (inst == m_instances[i].get()) + { + return i; + } + } + return -1; +} + +bool InstanceList::continueProcessInstance(InstancePtr instPtr, const int error, + const QDir &dir, QMap &groupMap) +{ + if (error != InstanceList::NoLoadError && error != InstanceList::NotAnInstance) + { + QString errorMsg = QString("Failed to load instance %1: ") + .arg(QFileInfo(dir.absolutePath()).baseName()) + .toUtf8(); + + switch (error) + { + default: + errorMsg += QString("Unknown instance loader error %1").arg(error); + break; + } + qCritical() << errorMsg.toUtf8(); + return false; + } + else if (!instPtr) + { + qCritical() << QString("Error loading instance %1. Instance loader returned null.") + .arg(QFileInfo(dir.absolutePath()).baseName()) + .toUtf8(); + return false; + } + else + { + auto iter = groupMap.find(instPtr->id()); + if (iter != groupMap.end()) + { + instPtr->setGroupInitial((*iter)); + } + qDebug() << "Loaded instance " << instPtr->name() << " from " << dir.absolutePath(); + return true; + } +} + +InstanceList::InstLoadError +InstanceList::loadInstance(InstancePtr &inst, const QString &instDir) +{ + auto instanceSettings = std::make_shared(FS::PathCombine(instDir, "instance.cfg")); + + instanceSettings->registerSetting("InstanceType", "Legacy"); + + QString inst_type = instanceSettings->get("InstanceType").toString(); + + // FIXME: replace with a map lookup, where instance classes register their types + if (inst_type == "OneSix" || inst_type == "Nostalgia") + { + inst.reset(new OneSixInstance(m_globalSettings, instanceSettings, instDir)); + } + else if (inst_type == "Legacy") + { + inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instDir)); + } + else + { + inst.reset(new NullInstance(m_globalSettings, instanceSettings, instDir)); + } + inst->init(); + return NoLoadError; +} + +InstanceList::InstCreateError +InstanceList::createInstance(InstancePtr &inst, BaseVersionPtr version, const QString &instDir) +{ + QDir rootDir(instDir); + + qDebug() << instDir.toUtf8(); + if (!rootDir.exists() && !rootDir.mkpath(".")) + { + qCritical() << "Can't create instance folder" << instDir; + return InstanceList::CantCreateDir; + } + + if (!version) + { + qCritical() << "Can't create instance for non-existing MC version"; + return InstanceList::NoSuchVersion; + } + + auto instanceSettings = std::make_shared(FS::PathCombine(instDir, "instance.cfg")); + instanceSettings->registerSetting("InstanceType", "Legacy"); + + auto minecraftVersion = std::dynamic_pointer_cast(version); + if(minecraftVersion) + { + auto mcVer = std::dynamic_pointer_cast(version); + instanceSettings->set("InstanceType", "OneSix"); + inst.reset(new OneSixInstance(m_globalSettings, instanceSettings, instDir)); + inst->setIntendedVersionId(version->descriptor()); + inst->init(); + return InstanceList::NoCreateError; + } + return InstanceList::NoSuchVersion; +} + +InstanceList::InstCreateError +InstanceList::copyInstance(InstancePtr &newInstance, InstancePtr &oldInstance, const QString &instDir, bool copySaves) +{ + QDir rootDir(instDir); + std::unique_ptr matcher; + if(!copySaves) + { + auto matcherReal = new RegexpMatcher("[.]?minecraft/saves"); + matcherReal->caseSensitive(false); + matcher.reset(matcherReal); + } + + qDebug() << instDir.toUtf8(); + FS::copy folderCopy(oldInstance->instanceRoot(), instDir); + folderCopy.followSymlinks(false).blacklist(matcher.get()); + if (!folderCopy()) + { + FS::deletePath(instDir); + return InstanceList::CantCreateDir; + } + + INISettingsObject settings_obj(FS::PathCombine(instDir, "instance.cfg")); + settings_obj.registerSetting("InstanceType", "Legacy"); + QString inst_type = settings_obj.get("InstanceType").toString(); + + oldInstance->copy(instDir); + + auto error = loadInstance(newInstance, instDir); + + switch (error) + { + case NoLoadError: + return NoCreateError; + case NotAnInstance: + rootDir.removeRecursively(); + return CantCreateDir; + default: + case UnknownLoadError: + rootDir.removeRecursively(); + return UnknownCreateError; + } +} + +void InstanceList::instanceNuked(BaseInstance *inst) +{ + int i = getInstIndex(inst); + if (i != -1) + { + beginRemoveRows(QModelIndex(), i, i); + m_instances.removeAt(i); + endRemoveRows(); + } +} + +void InstanceList::propertiesChanged(BaseInstance *inst) +{ + int i = getInstIndex(inst); + if (i != -1) + { + emit dataChanged(index(i), index(i)); + } +} diff --git a/api/logic/InstanceList.h b/api/logic/InstanceList.h new file mode 100644 index 00000000..074cca7c --- /dev/null +++ b/api/logic/InstanceList.h @@ -0,0 +1,187 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "BaseInstance.h" + +#include "multimc_logic_export.h" + +class BaseInstance; +class QDir; + +class MULTIMC_LOGIC_EXPORT InstanceList : public QAbstractListModel +{ + Q_OBJECT +private: + void loadGroupList(QMap &groupList); + void suspendGroupSaving(); + void resumeGroupSaving(); + +public slots: + void saveGroupList(); + +public: + explicit InstanceList(SettingsObjectPtr globalSettings, const QString &instDir, QObject *parent = 0); + virtual ~InstanceList(); + +public: + QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + + enum AdditionalRoles + { + GroupRole = Qt::UserRole, + InstancePointerRole = 0x34B1CB48, ///< Return pointer to real instance + InstanceIDRole = 0x34B1CB49 ///< Return id if the instance + }; + /*! + * \brief Error codes returned by functions in the InstanceList class. + * NoError Indicates that no error occurred. + * UnknownError indicates that an unspecified error occurred. + */ + enum InstListError + { + NoError = 0, + UnknownError + }; + + enum InstLoadError + { + NoLoadError = 0, + UnknownLoadError, + NotAnInstance + }; + + enum InstCreateError + { + NoCreateError = 0, + NoSuchVersion, + UnknownCreateError, + InstExists, + CantCreateDir + }; + + QString instDir() const + { + return m_instDir; + } + + /*! + * \brief Get the instance at index + */ + InstancePtr at(int i) const + { + return m_instances.at(i); + } + ; + + /*! + * \brief Get the count of loaded instances + */ + int count() const + { + return m_instances.count(); + } + ; + + /// Clear all instances. Triggers notifications. + void clear(); + + /// Add an instance. Triggers notifications, returns the new index + int add(InstancePtr t); + + /// Get an instance by ID + InstancePtr getInstanceById(QString id) const; + + QModelIndex getInstanceIndexById(const QString &id) const; + + // FIXME: instead of iterating through all instances and forming a set, keep the set around + QStringList getGroups(); + + void deleteGroup(const QString & name); + + /*! + * \brief Creates a stub instance + * + * \param inst Pointer to store the created instance in. + * \param version Game version to use for the instance + * \param instDir The new instance's directory. + * \return An InstCreateError error code. + * - InstExists if the given instance directory is already an instance. + * - CantCreateDir if the given instance directory cannot be created. + */ + InstCreateError createInstance(InstancePtr &inst, BaseVersionPtr version, + const QString &instDir); + + /*! + * \brief Creates a copy of an existing instance with a new name + * + * \param newInstance Pointer to store the created instance in. + * \param oldInstance The instance to copy + * \param instDir The new instance's directory. + * \return An InstCreateError error code. + * - InstExists if the given instance directory is already an instance. + * - CantCreateDir if the given instance directory cannot be created. + */ + InstCreateError copyInstance(InstancePtr &newInstance, InstancePtr &oldInstance, + const QString &instDir, bool copySaves); + + /*! + * \brief Loads an instance from the given directory. + * Checks the instance's INI file to figure out what the instance's type is first. + * \param inst Pointer to store the loaded instance in. + * \param instDir The instance's directory. + * \return An InstLoadError error code. + * - NotAnInstance if the given instance directory isn't a valid instance. + */ + InstLoadError loadInstance(InstancePtr &inst, const QString &instDir); + +signals: + void dataIsInvalid(); + +public slots: + void on_InstFolderChanged(const Setting &setting, QVariant value); + + /*! + * \brief Loads the instance list. Triggers notifications. + */ + InstListError loadList(); + +private slots: + void propertiesChanged(BaseInstance *inst); + void instanceNuked(BaseInstance *inst); + void groupChanged(); + +private: + int getInstIndex(BaseInstance *inst) const; + +public: + static bool continueProcessInstance(InstancePtr instPtr, const int error, const QDir &dir, QMap &groupMap); + +protected: + QString m_instDir; + QList m_instances; + QSet m_groups; + SettingsObjectPtr m_globalSettings; + bool suspendedGroupSave = false; + bool queuedGroupSave = false; +}; diff --git a/api/logic/Json.cpp b/api/logic/Json.cpp new file mode 100644 index 00000000..f2cbc8a3 --- /dev/null +++ b/api/logic/Json.cpp @@ -0,0 +1,272 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "Json.h" + +#include + +#include "FileSystem.h" +#include + +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(const QUrl &url) +{ + return QJsonValue(url.toString(QUrl::FullyEncoded)); +} +template<> +QJsonValue toJson(const QByteArray &data) +{ + return QJsonValue(QString::fromLatin1(data.toHex())); +} +template<> +QJsonValue toJson(const QDateTime &datetime) +{ + return QJsonValue(datetime.toString(Qt::ISODate)); +} +template<> +QJsonValue toJson(const QDir &dir) +{ + return QDir::current().relativeFilePath(dir.absolutePath()); +} +template<> +QJsonValue toJson(const QUuid &uuid) +{ + return uuid.toString(); +} +template<> +QJsonValue toJson(const QVariant &variant) +{ + return QJsonValue::fromVariant(variant); +} + + +template<> QByteArray requireIsType(const QJsonValue &value, const QString &what) +{ + const QString string = ensureIsType(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(const QJsonValue &value, const QString &what) +{ + if (!value.isArray()) + { + throw JsonException(what + " is not an array"); + } + return value.toArray(); +} + + +template<> QString requireIsType(const QJsonValue &value, const QString &what) +{ + if (!value.isString()) + { + throw JsonException(what + " is not a string"); + } + return value.toString(); +} + +template<> bool requireIsType(const QJsonValue &value, const QString &what) +{ + if (!value.isBool()) + { + throw JsonException(what + " is not a bool"); + } + return value.toBool(); +} + +template<> double requireIsType(const QJsonValue &value, const QString &what) +{ + if (!value.isDouble()) + { + throw JsonException(what + " is not a double"); + } + return value.toDouble(); +} + +template<> int requireIsType(const QJsonValue &value, const QString &what) +{ + const double doubl = requireIsType(value, what); + if (fmod(doubl, 1) != 0) + { + throw JsonException(what + " is not an integer"); + } + return int(doubl); +} + +template<> QDateTime requireIsType(const QJsonValue &value, const QString &what) +{ + const QString string = requireIsType(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(const QJsonValue &value, const QString &what) +{ + const QString string = ensureIsType(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(const QJsonValue &value, const QString &what) +{ + const QString string = requireIsType(value, what); + // FIXME: does not handle invalid characters! + return QDir::current().absoluteFilePath(string); +} + +template<> QUuid requireIsType(const QJsonValue &value, const QString &what) +{ + const QString string = requireIsType(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(const QJsonValue &value, const QString &what) +{ + if (!value.isObject()) + { + throw JsonException(what + " is not an object"); + } + return value.toObject(); +} + +template<> QVariant requireIsType(const QJsonValue &value, const QString &what) +{ + if (value.isNull() || value.isUndefined()) + { + throw JsonException(what + " is null or undefined"); + } + return value.toVariant(); +} + +template<> QJsonValue requireIsType(const QJsonValue &value, const QString &what) +{ + if (value.isNull() || value.isUndefined()) + { + throw JsonException(what + " is null or undefined"); + } + return value; +} + +} diff --git a/api/logic/Json.h b/api/logic/Json.h new file mode 100644 index 00000000..2cb60f0e --- /dev/null +++ b/api/logic/Json.h @@ -0,0 +1,249 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Exception.h" + +namespace Json +{ +class MULTIMC_LOGIC_EXPORT JsonException : public ::Exception +{ +public: + JsonException(const QString &message) : Exception(message) {} +}; + +/// @throw FileSystemException +void write(const QJsonDocument &doc, const QString &filename); +/// @throw FileSystemException +void write(const QJsonObject &object, const QString &filename); +/// @throw FileSystemException +void write(const QJsonArray &array, const QString &filename); + +QByteArray toBinary(const QJsonObject &obj); +QByteArray toBinary(const QJsonArray &array); +QByteArray toText(const QJsonObject &obj); +QByteArray toText(const QJsonArray &array); + +/// @throw JsonException +MULTIMC_LOGIC_EXPORT QJsonDocument requireDocument(const QByteArray &data, const QString &what = "Document"); +/// @throw JsonException +MULTIMC_LOGIC_EXPORT QJsonDocument requireDocument(const QString &filename, const QString &what = "Document"); +/// @throw JsonException +MULTIMC_LOGIC_EXPORT QJsonObject requireObject(const QJsonDocument &doc, const QString &what = "Document"); +/// @throw JsonException +MULTIMC_LOGIC_EXPORT QJsonArray requireArray(const QJsonDocument &doc, const QString &what = "Document"); + +/////////////////// WRITING //////////////////// + +void writeString(QJsonObject & to, const QString &key, const QString &value); +void writeStringList(QJsonObject & to, const QString &key, const QStringList &values); + +template +QJsonValue toJson(const T &t) +{ + return QJsonValue(t); +} +template<> +QJsonValue toJson(const QUrl &url); +template<> +QJsonValue toJson(const QByteArray &data); +template<> +QJsonValue toJson(const QDateTime &datetime); +template<> +QJsonValue toJson(const QDir &dir); +template<> +QJsonValue toJson(const QUuid &uuid); +template<> +QJsonValue toJson(const QVariant &variant); + +template +QJsonArray toJsonArray(const QList &container) +{ + QJsonArray array; + for (const T item : container) + { + array.append(toJson(item)); + } + return array; +} + +////////////////// READING //////////////////// + +/// @throw JsonException +template +T requireIsType(const QJsonValue &value, const QString &what = "Value"); + +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT double requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT bool requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT int requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QJsonObject requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QJsonArray requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QJsonValue requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QByteArray requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QDateTime requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QVariant requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QString requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QUuid requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QDir requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> MULTIMC_LOGIC_EXPORT QUrl requireIsType(const QJsonValue &value, const QString &what); + +// the following functions are higher level functions, that make use of the above functions for +// type conversion +template +T ensureIsType(const QJsonValue &value, const T default_ = T(), const QString &what = "Value") +{ + if (value.isUndefined() || value.isNull()) + { + return default_; + } + try + { + return requireIsType(value, what); + } + catch (JsonException &) + { + return default_; + } +} + +/// @throw JsonException +template +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(parent.value(key), localWhat); +} + +template +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(parent.value(key), default_, localWhat); +} + +template +QVector requireIsArrayOf(const QJsonDocument &doc) +{ + const QJsonArray array = requireArray(doc); + QVector out; + for (const QJsonValue val : array) + { + out.append(requireIsType(val, "Document")); + } + return out; +} + +template +QVector ensureIsArrayOf(const QJsonValue &value, const QString &what = "Value") +{ + const QJsonArray array = ensureIsType(value, QJsonArray(), what); + QVector out; + for (const QJsonValue val : array) + { + out.append(requireIsType(val, what)); + } + return out; +} + +template +QVector ensureIsArrayOf(const QJsonValue &value, const QVector default_, const QString &what = "Value") +{ + if (value.isUndefined()) + { + return default_; + } + return ensureIsArrayOf(value, what); +} + +/// @throw JsonException +template +QVector 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(parent.value(key), localWhat); +} + +template +QVector ensureIsArrayOf(const QJsonObject &parent, const QString &key, + const QVector &default_ = QVector(), const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + return default_; + } + return ensureIsArrayOf(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(value, what); \ + } \ + inline TYPE ensure##NAME(const QJsonValue &value, const TYPE default_ = TYPE(), const QString &what = "Value") \ + { \ + return ensureIsType(value, default_, what); \ + } \ + inline TYPE require##NAME(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__") \ + { \ + return requireIsType(parent, key, what); \ + } \ + inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const TYPE default_ = TYPE(), const QString &what = "__placeholder") \ + { \ + return ensureIsType(parent, key, default_, what); \ + } + +JSON_HELPERFUNCTIONS(Array, QJsonArray) +JSON_HELPERFUNCTIONS(Object, QJsonObject) +JSON_HELPERFUNCTIONS(JsonValue, QJsonValue) +JSON_HELPERFUNCTIONS(String, QString) +JSON_HELPERFUNCTIONS(Boolean, bool) +JSON_HELPERFUNCTIONS(Double, double) +JSON_HELPERFUNCTIONS(Integer, int) +JSON_HELPERFUNCTIONS(DateTime, QDateTime) +JSON_HELPERFUNCTIONS(Url, QUrl) +JSON_HELPERFUNCTIONS(ByteArray, QByteArray) +JSON_HELPERFUNCTIONS(Dir, QDir) +JSON_HELPERFUNCTIONS(Uuid, QUuid) +JSON_HELPERFUNCTIONS(Variant, QVariant) + +#undef JSON_HELPERFUNCTIONS + +} +using JSONValidationError = Json::JsonException; diff --git a/api/logic/MMCStrings.cpp b/api/logic/MMCStrings.cpp new file mode 100644 index 00000000..c50d596e --- /dev/null +++ b/api/logic/MMCStrings.cpp @@ -0,0 +1,76 @@ +#include "MMCStrings.h" + +/// TAKEN FROM Qt, because it doesn't expose it intelligently +static inline QChar getNextChar(const QString &s, int location) +{ + return (location < s.length()) ? s.at(location) : QChar(); +} + +/// TAKEN FROM Qt, because it doesn't expose it intelligently +int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) +{ + for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) + { + // skip spaces, tabs and 0's + QChar c1 = getNextChar(s1, l1); + while (c1.isSpace()) + c1 = getNextChar(s1, ++l1); + QChar c2 = getNextChar(s2, l2); + while (c2.isSpace()) + c2 = getNextChar(s2, ++l2); + + if (c1.isDigit() && c2.isDigit()) + { + while (c1.digitValue() == 0) + c1 = getNextChar(s1, ++l1); + while (c2.digitValue() == 0) + c2 = getNextChar(s2, ++l2); + + int lookAheadLocation1 = l1; + int lookAheadLocation2 = l2; + int currentReturnValue = 0; + // find the last digit, setting currentReturnValue as we go if it isn't equal + for (QChar lookAhead1 = c1, lookAhead2 = c2; + (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); + lookAhead1 = getNextChar(s1, ++lookAheadLocation1), + lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) + { + bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); + bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); + if (!is1ADigit && !is2ADigit) + break; + if (!is1ADigit) + return -1; + if (!is2ADigit) + return 1; + if (currentReturnValue == 0) + { + if (lookAhead1 < lookAhead2) + { + currentReturnValue = -1; + } + else if (lookAhead1 > lookAhead2) + { + currentReturnValue = 1; + } + } + } + if (currentReturnValue != 0) + return currentReturnValue; + } + if (cs == Qt::CaseInsensitive) + { + if (!c1.isLower()) + c1 = c1.toLower(); + if (!c2.isLower()) + c2 = c2.toLower(); + } + int r = QString::localeAwareCompare(c1, c2); + if (r < 0) + return -1; + if (r > 0) + return 1; + } + // The two strings are the same (02 == 2) so fall back to the normal sort + return QString::compare(s1, s2, cs); +} diff --git a/api/logic/MMCStrings.h b/api/logic/MMCStrings.h new file mode 100644 index 00000000..5606b909 --- /dev/null +++ b/api/logic/MMCStrings.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include "multimc_logic_export.h" + +namespace Strings +{ + int MULTIMC_LOGIC_EXPORT naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs); +} diff --git a/api/logic/MMCZip.cpp b/api/logic/MMCZip.cpp new file mode 100644 index 00000000..0f35bc70 --- /dev/null +++ b/api/logic/MMCZip.cpp @@ -0,0 +1,491 @@ +/* +Copyright (C) 2010 Roberto Pompermaier +Copyright (C) 2005-2014 Sergey A. Tachenov + +Parts of this file were part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)MMCZip.h files for details. Basically it's the zlib license. +*/ + +#include +#include +#include +#include "MMCZip.h" +#include "FileSystem.h" + +#include + +bool copyData(QIODevice &inFile, QIODevice &outFile) +{ + while (!inFile.atEnd()) + { + char buf[4096]; + qint64 readLen = inFile.read(buf, 4096); + if (readLen <= 0) + return false; + if (outFile.write(buf, readLen) != readLen) + return false; + } + return true; +} + +QStringList MMCZip::extractDir(QString fileCompressed, QString dir) +{ + return JlCompress::extractDir(fileCompressed, dir); +} + +bool compressFile(QuaZip *zip, QString fileName, QString fileDest) +{ + if (!zip) + { + return false; + } + if (zip->getMode() != QuaZip::mdCreate && zip->getMode() != QuaZip::mdAppend && + zip->getMode() != QuaZip::mdAdd) + { + return false; + } + + QFile inFile; + inFile.setFileName(fileName); + if (!inFile.open(QIODevice::ReadOnly)) + { + return false; + } + + QuaZipFile outFile(zip); + if (!outFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileDest, inFile.fileName()))) + { + return false; + } + + if (!copyData(inFile, outFile) || outFile.getZipError() != UNZ_OK) + { + return false; + } + + outFile.close(); + if (outFile.getZipError() != UNZ_OK) + { + return false; + } + inFile.close(); + + return true; +} + +bool MMCZip::compressSubDir(QuaZip* zip, QString dir, QString origDir, QSet& added, QString prefix, const SeparatorPrefixTree <'/'> * blacklist) +{ + if (!zip) return false; + if (zip->getMode()!=QuaZip::mdCreate && zip->getMode()!=QuaZip::mdAppend && zip->getMode()!=QuaZip::mdAdd) + { + return false; + } + + QDir directory(dir); + if (!directory.exists()) + { + return false; + } + + QDir origDirectory(origDir); + if (dir != origDir) + { + QString internalDirName = origDirectory.relativeFilePath(dir); + if(!blacklist || !blacklist->covers(internalDirName)) + { + QuaZipFile dirZipFile(zip); + auto dirPrefix = FS::PathCombine(prefix, origDirectory.relativeFilePath(dir)) + "/"; + if (!dirZipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(dirPrefix, dir), 0, 0, 0)) + { + return false; + } + dirZipFile.close(); + } + } + + QFileInfoList files = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); + for (auto file: files) + { + if(!file.isDir()) + { + continue; + } + if(!compressSubDir(zip,file.absoluteFilePath(),origDir, added, prefix, blacklist)) + { + return false; + } + } + + files = directory.entryInfoList(QDir::Files); + for (auto file: files) + { + if(!file.isFile()) + { + continue; + } + + if(file.absoluteFilePath()==zip->getZipName()) + { + continue; + } + + QString filename = origDirectory.relativeFilePath(file.absoluteFilePath()); + if(blacklist && blacklist->covers(filename)) + { + continue; + } + if(prefix.size()) + { + filename = FS::PathCombine(prefix, filename); + } + added.insert(filename); + if (!compressFile(zip,file.absoluteFilePath(),filename)) + { + return false; + } + } + + return true; +} + +bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, + std::function filter) +{ + QuaZip modZip(from.filePath()); + modZip.open(QuaZip::mdUnzip); + + QuaZipFile fileInsideMod(&modZip); + QuaZipFile zipOutFile(into); + for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) + { + QString filename = modZip.getCurrentFileName(); + if (!filter(filename)) + { + qDebug() << "Skipping file " << filename << " from " + << from.fileName() << " - filtered"; + continue; + } + if (contained.contains(filename)) + { + qDebug() << "Skipping already contained file " << filename << " from " + << from.fileName(); + continue; + } + contained.insert(filename); + + if (!fileInsideMod.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open " << filename << " from " << from.fileName(); + return false; + } + + QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); + + if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) + { + qCritical() << "Failed to open " << filename << " in the jar"; + fileInsideMod.close(); + return false; + } + if (!copyData(fileInsideMod, zipOutFile)) + { + zipOutFile.close(); + fileInsideMod.close(); + qCritical() << "Failed to copy data of " << filename << " into the jar"; + return false; + } + zipOutFile.close(); + fileInsideMod.close(); + } + return true; +} + +bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& 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 addedFiles; + + // Modify the jar + QListIterator i(mods); + i.toBack(); + while (i.hasPrevious()) + { + const Mod &mod = i.previous(); + // do not merge disabled mods. + if (!mod.enabled()) + continue; + if (mod.type() == Mod::MOD_ZIPFILE) + { + if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles, noFilter)) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + } + else if (mod.type() == Mod::MOD_SINGLEFILE) + { + auto filename = mod.filename(); + if (!compressFile(&zipOut, filename.absoluteFilePath(), + filename.fileName())) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + addedFiles.insert(filename.fileName()); + } + else if (mod.type() == Mod::MOD_FOLDER) + { + auto filename = mod.filename(); + QString what_to_zip = filename.absoluteFilePath(); + QDir dir(what_to_zip); + dir.cdUp(); + QString parent_dir = dir.absolutePath(); + if (!compressSubDir(&zipOut, what_to_zip, parent_dir, addedFiles)) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + qDebug() << "Adding folder " << filename.fileName() << " from " + << filename.absoluteFilePath(); + } + } + + if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, metaInfFilter)) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to insert minecraft.jar contents."; + return false; + } + + // Recompress the jar + zipOut.close(); + if (zipOut.getZipError() != 0) + { + QFile::remove(targetJarPath); + qCritical() << "Failed to finalize minecraft.jar!"; + return false; + } + return true; +} + +bool MMCZip::noFilter(QString) +{ + return true; +} + +bool MMCZip::metaInfFilter(QString key) +{ + if(key.contains("META-INF")) + { + return false; + } + return true; +} + +bool MMCZip::compressDir(QString zipFile, QString dir, QString prefix, const SeparatorPrefixTree <'/'> * blacklist) +{ + QuaZip zip(zipFile); + QDir().mkpath(QFileInfo(zipFile).absolutePath()); + if(!zip.open(QuaZip::mdCreate)) + { + QFile::remove(zipFile); + return false; + } + + QSet added; + if (!compressSubDir(&zip, dir, dir, added, prefix, blacklist)) + { + QFile::remove(zipFile); + return false; + } + zip.close(); + if(zip.getZipError()!=0) + { + QFile::remove(zipFile); + return false; + } + return true; +} + +QString MMCZip::findFileInZip(QuaZip * zip, const QString & what, const QString &root) +{ + QuaZipDir rootDir(zip, root); + for(auto fileName: rootDir.entryList(QDir::Files)) + { + if(fileName == what) + return root; + } + for(auto fileName: rootDir.entryList(QDir::Dirs)) + { + QString result = findFileInZip(zip, what, root + fileName); + if(!result.isEmpty()) + { + return result; + } + } + return QString(); +} + +bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root) +{ + QuaZipDir rootDir(zip, root); + for(auto fileName: rootDir.entryList(QDir::Files)) + { + if(fileName == what) + { + result.append(root); + return true; + } + } + for(auto fileName: rootDir.entryList(QDir::Dirs)) + { + findFilesInZip(zip, what, result, root + fileName); + } + return !result.isEmpty(); +} + +bool removeFile(QStringList listFile) +{ + bool ret = true; + for (int i = 0; i < listFile.count(); i++) + { + ret &= QFile::remove(listFile.at(i)); + } + return ret; +} + +bool MMCZip::extractFile(QuaZip *zip, const QString &fileName, const QString &fileDest) +{ + if(!zip) + return false; + + if (zip->getMode() != QuaZip::mdUnzip) + return false; + + if (!fileName.isEmpty()) + zip->setCurrentFile(fileName); + + QuaZipFile inFile(zip); + if (!inFile.open(QIODevice::ReadOnly) || inFile.getZipError() != UNZ_OK) + return false; + + // Controllo esistenza cartella file risultato + QDir curDir; + if (fileDest.endsWith('/')) + { + if (!curDir.mkpath(fileDest)) + { + return false; + } + } + else + { + if (!curDir.mkpath(QFileInfo(fileDest).absolutePath())) + { + return false; + } + } + + QuaZipFileInfo64 info; + if (!zip->getCurrentFileInfo(&info)) + return false; + + QFile::Permissions srcPerm = info.getPermissions(); + if (fileDest.endsWith('/') && QFileInfo(fileDest).isDir()) + { + if (srcPerm != 0) + { + QFile(fileDest).setPermissions(srcPerm); + } + return true; + } + + QFile outFile; + outFile.setFileName(fileDest); + if (!outFile.open(QIODevice::WriteOnly)) + return false; + + if (!copyData(inFile, outFile) || inFile.getZipError() != UNZ_OK) + { + outFile.close(); + removeFile(QStringList(fileDest)); + return false; + } + outFile.close(); + + inFile.close(); + if (inFile.getZipError() != UNZ_OK) + { + removeFile(QStringList(fileDest)); + return false; + } + + if (srcPerm != 0) + { + outFile.setPermissions(srcPerm); + } + return true; +} + +QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +{ + QDir directory(target); + QStringList extracted; + if (!zip->goToFirstFile()) + { + return QStringList(); + } + do + { + QString name = zip->getCurrentFileName(); + if(!name.startsWith(subdir)) + { + continue; + } + name.remove(0, subdir.size()); + QString absFilePath = directory.absoluteFilePath(name); + if(name.isEmpty()) + { + absFilePath += "/"; + } + if (!extractFile(zip, "", absFilePath)) + { + removeFile(extracted); + return QStringList(); + } + extracted.append(absFilePath); + } while (zip->goToNextFile()); + return extracted; +} diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h new file mode 100644 index 00000000..f350e668 --- /dev/null +++ b/api/logic/MMCZip.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include "minecraft/Mod.h" +#include "SeparatorPrefixTree.h" +#include + +#include "multimc_logic_export.h" + +class QuaZip; + +namespace MMCZip +{ + /** + * Compress a subdirectory. + * \param parentZip Opened zip containing the parent directory. + * \param dir The full path to the directory to pack. + * \param parentDir The full path to the directory corresponding to the root of the ZIP. + * \param recursive Whether to pack sub-directories as well or only files. + * \return true if success, false otherwise. + */ + bool MULTIMC_LOGIC_EXPORT compressSubDir(QuaZip *zip, QString dir, QString origDir, QSet &added, + QString prefix = QString(), const SeparatorPrefixTree <'/'> * blacklist = nullptr); + + /** + * Compress a whole directory. + * \param fileCompressed The name of the archive. + * \param dir The directory to compress. + * \param recursive Whether to pack the subdirectories as well, or just regular files. + * \return true if success, false otherwise. + */ + bool MULTIMC_LOGIC_EXPORT compressDir(QString zipFile, QString dir, QString prefix = QString(), const SeparatorPrefixTree <'/'> * blacklist = nullptr); + + /// filter function for @mergeZipFiles - passthrough + bool MULTIMC_LOGIC_EXPORT noFilter(QString key); + + /// filter function for @mergeZipFiles - ignores METAINF + bool MULTIMC_LOGIC_EXPORT metaInfFilter(QString key); + + /** + * Merge two zip files, using a filter function + */ + bool MULTIMC_LOGIC_EXPORT mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, std::function filter); + + /** + * take a source jar, add mods to it, resulting in target jar + */ + bool MULTIMC_LOGIC_EXPORT createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); + + /** + * Extract a whole archive. + * + * \param fileCompressed The name of the archive. + * \param dir The directory to extract to, the current directory if + * left empty. + * \return The list of the full paths of the files extracted, empty on failure. + */ + QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir = QString()); + + /** + * Find a single file in archive by file name (not path) + * + * \return the path prefix where the file is + */ + QString MULTIMC_LOGIC_EXPORT findFileInZip(QuaZip * zip, const QString & what, const QString &root = QString()); + + /** + * Find a multiple files of the same name in archive by file name + * If a file is found in a path, no deeper paths are searched + * + * \return true if anything was found + */ + bool MULTIMC_LOGIC_EXPORT findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString()); + + /** + * Extract a single file to a destination + * + * \return true if it succeeds + */ + bool MULTIMC_LOGIC_EXPORT extractFile(QuaZip *zip, const QString &fileName, const QString &fileDest); + + /** + * Extract a subdirectory from an archive + */ + QStringList MULTIMC_LOGIC_EXPORT extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); +} diff --git a/api/logic/NullInstance.h b/api/logic/NullInstance.h new file mode 100644 index 00000000..fbb2d985 --- /dev/null +++ b/api/logic/NullInstance.h @@ -0,0 +1,90 @@ +#pragma once +#include "BaseInstance.h" + +class NullInstance: public BaseInstance +{ +public: + NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) + :BaseInstance(globalSettings, settings, rootDir) + { + setFlag(BaseInstance::VersionBrokenFlag); + } + virtual ~NullInstance() {}; + virtual bool setIntendedVersionId(QString) override + { + return false; + } + virtual void cleanupAfterRun() override + { + } + virtual QString currentVersionId() const override + { + return "Null"; + }; + virtual QString intendedVersionId() const override + { + return "Null"; + }; + virtual void init() override + { + }; + virtual QString getStatusbarDescription() override + { + return tr("Unknown instance type"); + }; + virtual bool shouldUpdate() const override + { + return false; + }; + virtual QSet< QString > traits() override + { + return {}; + }; + virtual QString instanceConfigFolder() const override + { + return instanceRoot(); + }; + virtual std::shared_ptr createLaunchTask(AuthSessionPtr) override + { + return nullptr; + } + virtual std::shared_ptr< Task > createUpdateTask() override + { + return nullptr; + } + virtual std::shared_ptr createJarModdingTask() override + { + return nullptr; + } + virtual void setShouldUpdate(bool) override + { + }; + virtual std::shared_ptr< BaseVersionList > versionList() const override + { + return nullptr; + }; + virtual QProcessEnvironment createEnvironment() override + { + return QProcessEnvironment(); + } + virtual QMap getVariables() const override + { + return QMap(); + } + virtual IPathMatcher::Ptr getLogFileMatcher() override + { + return nullptr; + } + virtual QString getLogFileRoot() override + { + return instanceRoot(); + } + virtual QString typeName() const override + { + return "Null"; + } + bool canExport() const override + { + return false; + } +}; diff --git a/api/logic/QObjectPtr.h b/api/logic/QObjectPtr.h new file mode 100644 index 00000000..b81b3234 --- /dev/null +++ b/api/logic/QObjectPtr.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +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 using unique_qobject_ptr = std::unique_ptr; + +/** + * A shared pointer class with shared pointer semantics intended for derivates of QObject + * Calls deleteLater() instead of destroying the contained object immediately + */ +template +class shared_qobject_ptr +{ +public: + shared_qobject_ptr(){} + shared_qobject_ptr(T * wrap) + { + reset(wrap); + } + shared_qobject_ptr(const shared_qobject_ptr& other) + { + m_ptr = other.m_ptr; + } + template + shared_qobject_ptr(const shared_qobject_ptr &other) + { + m_ptr = other.unwrap(); + } + +public: + void reset(T * wrap) + { + using namespace std::placeholders; + m_ptr.reset(wrap, std::bind(&QObject::deleteLater, _1)); + } + void reset() + { + m_ptr.reset(); + } + T * get() const + { + return m_ptr.get(); + } + T * operator->() const + { + return m_ptr.get(); + } + T & operator*() const + { + return *m_ptr.get(); + } + operator bool() const + { + return m_ptr.get() != nullptr; + } + const std::shared_ptr unwrap() const + { + return m_ptr; + } + +private: + std::shared_ptr m_ptr; +}; diff --git a/api/logic/RWStorage.h b/api/logic/RWStorage.h new file mode 100644 index 00000000..b1598ca4 --- /dev/null +++ b/api/logic/RWStorage.h @@ -0,0 +1,60 @@ +#pragma once +template +class RWStorage +{ +public: + void add(K key, V value) + { + QWriteLocker l(&lock); + cache[key] = value; + stale_entries.remove(key); + } + V get(K key) + { + QReadLocker l(&lock); + if(cache.contains(key)) + { + return cache[key]; + } + else return V(); + } + bool get(K key, V& value) + { + QReadLocker l(&lock); + if(cache.contains(key)) + { + value = cache[key]; + return true; + } + else return false; + } + bool has(K key) + { + QReadLocker l(&lock); + return cache.contains(key); + } + bool stale(K key) + { + QReadLocker l(&lock); + if(!cache.contains(key)) + return true; + return stale_entries.contains(key); + } + void setStale(K key) + { + QReadLocker l(&lock); + if(cache.contains(key)) + { + stale_entries.insert(key); + } + } + void clear() + { + QWriteLocker l(&lock); + cache.clear(); + } +private: + QReadWriteLock lock; + QMap cache; + QSet stale_entries; +}; \ No newline at end of file diff --git a/api/logic/RecursiveFileSystemWatcher.cpp b/api/logic/RecursiveFileSystemWatcher.cpp new file mode 100644 index 00000000..59c3f0f0 --- /dev/null +++ b/api/logic/RecursiveFileSystemWatcher.cpp @@ -0,0 +1,111 @@ +#include "RecursiveFileSystemWatcher.h" + +#include +#include + +RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject *parent) + : QObject(parent), m_watcher(new QFileSystemWatcher(this)) +{ + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, + &RecursiveFileSystemWatcher::fileChange); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, + &RecursiveFileSystemWatcher::directoryChange); +} + +void RecursiveFileSystemWatcher::setRootDir(const QDir &root) +{ + bool wasEnabled = m_isEnabled; + disable(); + m_root = root; + setFiles(scanRecursive(m_root)); + if (wasEnabled) + { + enable(); + } +} +void RecursiveFileSystemWatcher::setWatchFiles(const bool watchFiles) +{ + bool wasEnabled = m_isEnabled; + disable(); + m_watchFiles = watchFiles; + if (wasEnabled) + { + enable(); + } +} + +void RecursiveFileSystemWatcher::enable() +{ + if (m_isEnabled) + { + return; + } + Q_ASSERT(m_root != QDir::root()); + addFilesToWatcherRecursive(m_root); + m_isEnabled = true; +} +void RecursiveFileSystemWatcher::disable() +{ + if (!m_isEnabled) + { + return; + } + m_isEnabled = false; + m_watcher->removePaths(m_watcher->files()); + m_watcher->removePaths(m_watcher->directories()); +} + +void RecursiveFileSystemWatcher::setFiles(const QStringList &files) +{ + if (files != m_files) + { + m_files = files; + emit filesChanged(); + } +} + +void RecursiveFileSystemWatcher::addFilesToWatcherRecursive(const QDir &dir) +{ + m_watcher->addPath(dir.absolutePath()); + for (const QString &directory : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) + { + addFilesToWatcherRecursive(dir.absoluteFilePath(directory)); + } + if (m_watchFiles) + { + for (const QFileInfo &info : dir.entryInfoList(QDir::Files)) + { + m_watcher->addPath(info.absoluteFilePath()); + } + } +} +QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir &directory) +{ + QStringList ret; + if(!m_matcher) + { + return {}; + } + for (const QString &dir : directory.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden)) + { + ret.append(scanRecursive(directory.absoluteFilePath(dir))); + } + for (const QString &file : directory.entryList(QDir::Files | QDir::Hidden)) + { + auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); + if (m_matcher->matches(relPath)) + { + ret.append(relPath); + } + } + return ret; +} + +void RecursiveFileSystemWatcher::fileChange(const QString &path) +{ + emit fileChanged(path); +} +void RecursiveFileSystemWatcher::directoryChange(const QString &path) +{ + setFiles(scanRecursive(m_root)); +} diff --git a/api/logic/RecursiveFileSystemWatcher.h b/api/logic/RecursiveFileSystemWatcher.h new file mode 100644 index 00000000..07bce0b9 --- /dev/null +++ b/api/logic/RecursiveFileSystemWatcher.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include "pathmatcher/IPathMatcher.h" + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT RecursiveFileSystemWatcher : public QObject +{ + Q_OBJECT +public: + RecursiveFileSystemWatcher(QObject *parent); + + void setRootDir(const QDir &root); + QDir rootDir() const + { + return m_root; + } + + // WARNING: setting this to true may be bad for performance + void setWatchFiles(const bool watchFiles); + bool watchFiles() const + { + return m_watchFiles; + } + + void setMatcher(IPathMatcher::Ptr matcher) + { + m_matcher = matcher; + } + + QStringList files() const + { + return m_files; + } + +signals: + void filesChanged(); + void fileChanged(const QString &path); + +public slots: + void enable(); + void disable(); + +private: + QDir m_root; + bool m_watchFiles = false; + bool m_isEnabled = false; + IPathMatcher::Ptr m_matcher; + + QFileSystemWatcher *m_watcher; + + QStringList m_files; + void setFiles(const QStringList &files); + + void addFilesToWatcherRecursive(const QDir &dir); + QStringList scanRecursive(const QDir &dir); + +private slots: + void fileChange(const QString &path); + void directoryChange(const QString &path); +}; diff --git a/api/logic/SeparatorPrefixTree.h b/api/logic/SeparatorPrefixTree.h new file mode 100644 index 00000000..fd149af0 --- /dev/null +++ b/api/logic/SeparatorPrefixTree.h @@ -0,0 +1,298 @@ +#pragma once +#include +#include +#include + +template +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> children; + bool m_contained = false; +}; diff --git a/api/logic/TypeMagic.h b/api/logic/TypeMagic.h new file mode 100644 index 00000000..fa9d12a9 --- /dev/null +++ b/api/logic/TypeMagic.h @@ -0,0 +1,37 @@ +#pragma once + +namespace TypeMagic +{ +/** "Cleans" the given type T by stripping references (&) and cv-qualifiers (const, volatile) from it + * const int => int + * QString & => QString + * const unsigned long long & => unsigned long long + * + * Usage: + * using Cleaned = Detail::CleanType; + * static_assert(std::is_same, "Cleaned == int"); + */ +// the order of remove_cv and remove_reference matters! +template +using CleanType = typename std::remove_cv::type>::type; + +/// For functors (structs with operator()), including lambdas, which in **most** cases are functors +/// "Calls" Function or Function +template struct Function : public Function {}; +/// For function pointers (&function), including static members (&Class::member) +template struct Function : public Function {}; +/// Default specialization used by others. +template struct Function +{ + using ReturnType = Ret; + using Argument = Arg; +}; +/// For member functions. Also used by the lambda overload if the lambda captures [this] +template struct Function : public Function {}; +template struct Function : public Function {}; +/// Overload for references +template struct Function : public Function {}; +/// Overload for rvalues +template struct Function : public Function {}; +// for more info: https://functionalcpp.wordpress.com/2013/08/05/function-traits/ +} diff --git a/api/logic/Version.cpp b/api/logic/Version.cpp new file mode 100644 index 00000000..3c4727ad --- /dev/null +++ b/api/logic/Version.cpp @@ -0,0 +1,140 @@ +#include "Version.h" + +#include +#include +#include +#include + +Version::Version(const QString &str) : m_string(str) +{ + parse(); +} + +bool Version::operator<(const Version &other) const +{ + const int size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) + { + const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec2 = + (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + if (sec1 != sec2) + { + return sec1 < sec2; + } + } + + return false; +} +bool Version::operator<=(const Version &other) const +{ + return *this < other || *this == other; +} +bool Version::operator>(const Version &other) const +{ + const int size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) + { + const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec2 = + (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + if (sec1 != sec2) + { + return sec1 > sec2; + } + } + + return false; +} +bool Version::operator>=(const Version &other) const +{ + return *this > other || *this == other; +} +bool Version::operator==(const Version &other) const +{ + const int size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) + { + const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec2 = + (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + if (sec1 != sec2) + { + return false; + } + } + + return true; +} +bool Version::operator!=(const Version &other) const +{ + return !operator==(other); +} + +void Version::parse() +{ + m_sections.clear(); + + QStringList parts = m_string.split('.'); + + for (const auto part : parts) + { + m_sections.append(Section(part)); + } +} + +bool versionIsInInterval(const QString &version, const QString &interval) +{ + return versionIsInInterval(Version(version), interval); +} +bool versionIsInInterval(const Version &version, const QString &interval) +{ + if (interval.isEmpty() || version.toString() == interval) + { + return true; + } + + // Interval notation is used + QRegularExpression exp( + "(?[\\[\\]\\(\\)])(?.*?)(,(?.*?))?(?[\\[\\]\\(\\)]),?"); + QRegularExpressionMatch match = exp.match(interval); + if (match.hasMatch()) + { + const QChar start = match.captured("start").at(0); + const QChar end = match.captured("end").at(0); + const QString bottom = match.captured("bottom"); + const QString top = match.captured("top"); + + // check if in range (bottom) + if (!bottom.isEmpty()) + { + const auto bottomVersion = Version(bottom); + if ((start == '[') && !(version >= bottomVersion)) + { + return false; + } + else if ((start == '(') && !(version > bottomVersion)) + { + return false; + } + } + + // check if in range (top) + if (!top.isEmpty()) + { + const auto topVersion = Version(top); + if ((end == ']') && !(version <= topVersion)) + { + return false; + } + else if ((end == ')') && !(version < topVersion)) + { + return false; + } + } + + return true; + } + + return false; +} diff --git a/api/logic/Version.h b/api/logic/Version.h new file mode 100644 index 00000000..b5946ced --- /dev/null +++ b/api/logic/Version.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include + +#include "multimc_logic_export.h" + +class QUrl; + +struct MULTIMC_LOGIC_EXPORT Version +{ + Version(const QString &str); + Version() {} + + bool operator<(const Version &other) const; + bool operator<=(const Version &other) const; + bool operator>(const Version &other) const; + bool operator>=(const Version &other) const; + bool operator==(const Version &other) const; + bool operator!=(const Version &other) const; + + QString toString() const + { + return m_string; + } + +private: + QString m_string; + struct Section + { + explicit Section(const QString &fullString) + { + m_fullString = fullString; + int cutoff = m_fullString.size(); + for(int i = 0; i < m_fullString.size(); i++) + { + if(!m_fullString[i].isDigit()) + { + cutoff = i; + break; + } + } + auto numPart = m_fullString.leftRef(cutoff); + if(numPart.size()) + { + numValid = true; + m_numPart = numPart.toInt(); + } + auto stringPart = m_fullString.midRef(cutoff); + if(stringPart.size()) + { + m_stringPart = stringPart.toString(); + } + } + explicit Section() {} + bool numValid = false; + int m_numPart = 0; + QString m_stringPart; + QString m_fullString; + + inline bool operator!=(const Section &other) const + { + if(numValid && other.numValid) + { + return m_numPart != other.m_numPart || m_stringPart != other.m_stringPart; + } + else + { + return m_fullString != other.m_fullString; + } + } + inline bool operator<(const Section &other) const + { + if(numValid && other.numValid) + { + if(m_numPart < other.m_numPart) + return true; + if(m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) + return true; + return false; + } + else + { + return m_fullString < other.m_fullString; + } + } + inline bool operator>(const Section &other) const + { + if(numValid && other.numValid) + { + if(m_numPart > other.m_numPart) + return true; + if(m_numPart == other.m_numPart && m_stringPart > other.m_stringPart) + return true; + return false; + } + else + { + return m_fullString > other.m_fullString; + } + } + }; + QList
m_sections; + + void parse(); +}; + +MULTIMC_LOGIC_EXPORT bool versionIsInInterval(const QString &version, const QString &interval); +MULTIMC_LOGIC_EXPORT bool versionIsInInterval(const Version &version, const QString &interval); + diff --git a/api/logic/java/JavaChecker.cpp b/api/logic/java/JavaChecker.cpp new file mode 100644 index 00000000..54d552a9 --- /dev/null +++ b/api/logic/java/JavaChecker.cpp @@ -0,0 +1,159 @@ +#include "JavaChecker.h" +#include +#include +#include +#include +#include +#include +#include + +JavaChecker::JavaChecker(QObject *parent) : QObject(parent) +{ +} + +void JavaChecker::performCheck() +{ + QString checkerJar = FS::PathCombine(QCoreApplication::applicationDirPath(), "jars", "JavaCheck.jar"); + + QStringList args; + + process.reset(new QProcess()); + if(m_args.size()) + { + auto extraArgs = Commandline::splitArgs(m_args); + args.append(extraArgs); + } + if(m_minMem != 0) + { + args << QString("-Xms%1m").arg(m_minMem); + } + if(m_maxMem != 0) + { + args << QString("-Xmx%1m").arg(m_maxMem); + } + if(m_permGen != 64) + { + args << QString("-XX:PermSize=%1m").arg(m_permGen); + } + + args.append({"-jar", checkerJar}); + process->setArguments(args); + process->setProgram(m_path); + process->setProcessChannelMode(QProcess::SeparateChannels); + qDebug() << "Running java checker: " + m_path + args.join(" ");; + + connect(process.get(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus))); + connect(process.get(), SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError))); + connect(process.get(), SIGNAL(readyReadStandardOutput()), this, SLOT(stdoutReady())); + connect(process.get(), SIGNAL(readyReadStandardError()), this, SLOT(stderrReady())); + connect(&killTimer, SIGNAL(timeout()), SLOT(timeout())); + killTimer.setSingleShot(true); + killTimer.start(15000); + process->start(); +} + +void JavaChecker::stdoutReady() +{ + QByteArray data = process->readAllStandardOutput(); + QString added = QString::fromLocal8Bit(data); + added.remove('\r'); + m_stdout += added; +} + +void JavaChecker::stderrReady() +{ + QByteArray data = process->readAllStandardError(); + QString added = QString::fromLocal8Bit(data); + added.remove('\r'); + m_stderr += added; +} + +void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) +{ + killTimer.stop(); + QProcessPtr _process; + _process.swap(process); + + JavaCheckResult result; + { + result.path = m_path; + result.id = m_id; + } + result.errorLog = m_stderr; + qDebug() << "STDOUT" << m_stdout; + qWarning() << "STDERR" << m_stderr; + qDebug() << "Java checker finished with status " << status << " exit code " << exitcode; + + if (status == QProcess::CrashExit || exitcode == 1) + { + qDebug() << "Java checker failed!"; + emit checkFinished(result); + return; + } + + bool success = true; + + QMap results; + QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts); + for(QString line : lines) + { + line = line.trimmed(); + + auto parts = line.split('=', QString::SkipEmptyParts); + if(parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) + { + success = false; + } + else + { + results.insert(parts[0], parts[1]); + } + } + + if(!results.contains("os.arch") || !results.contains("java.version") || !success) + { + qDebug() << "Java checker failed - couldn't extract required information."; + emit checkFinished(result); + return; + } + + auto os_arch = results["os.arch"]; + auto java_version = results["java.version"]; + bool is_64 = os_arch == "x86_64" || os_arch == "amd64"; + + + result.valid = true; + result.is_64bit = is_64; + result.mojangPlatform = is_64 ? "64" : "32"; + result.realPlatform = os_arch; + result.javaVersion = java_version; + qDebug() << "Java checker succeeded."; + emit checkFinished(result); +} + +void JavaChecker::error(QProcess::ProcessError err) +{ + if(err == QProcess::FailedToStart) + { + killTimer.stop(); + qDebug() << "Java checker has failed to start."; + JavaCheckResult result; + { + result.path = m_path; + result.id = m_id; + } + + emit checkFinished(result); + return; + } +} + +void JavaChecker::timeout() +{ + // NO MERCY. NO ABUSE. + if(process) + { + qDebug() << "Java checker has been killed by timeout."; + process->kill(); + } +} diff --git a/api/logic/java/JavaChecker.h b/api/logic/java/JavaChecker.h new file mode 100644 index 00000000..650e7ce3 --- /dev/null +++ b/api/logic/java/JavaChecker.h @@ -0,0 +1,54 @@ +#pragma once +#include +#include +#include + +#include "multimc_logic_export.h" + +#include "JavaVersion.h" + +class JavaChecker; + +struct MULTIMC_LOGIC_EXPORT JavaCheckResult +{ + QString path; + QString mojangPlatform; + QString realPlatform; + JavaVersion javaVersion; + QString errorLog; + bool valid = false; + bool is_64bit = false; + int id; +}; + +typedef std::shared_ptr QProcessPtr; +typedef std::shared_ptr JavaCheckerPtr; +class MULTIMC_LOGIC_EXPORT JavaChecker : public QObject +{ + Q_OBJECT +public: + explicit JavaChecker(QObject *parent = 0); + void performCheck(); + + QString m_path; + QString m_args; + int m_id = 0; + int m_minMem = 0; + int m_maxMem = 0; + int m_permGen = 64; + +signals: + void checkFinished(JavaCheckResult result); +private: + QProcessPtr process; + QTimer killTimer; + QString m_stdout; + QString m_stderr; +public +slots: + void timeout(); + void finished(int exitcode, QProcess::ExitStatus); + void error(QProcess::ProcessError); + void stdoutReady(); + void stderrReady(); +}; diff --git a/api/logic/java/JavaCheckerJob.cpp b/api/logic/java/JavaCheckerJob.cpp new file mode 100644 index 00000000..0b040e43 --- /dev/null +++ b/api/logic/java/JavaCheckerJob.cpp @@ -0,0 +1,45 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "JavaCheckerJob.h" + +#include + +void JavaCheckerJob::partFinished(JavaCheckResult result) +{ + num_finished++; + qDebug() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/" + << javacheckers.size(); + emit progress(num_finished, javacheckers.size()); + + javaresults.replace(result.id, result); + + if (num_finished == javacheckers.size()) + { + emit finished(javaresults); + } +} + +void JavaCheckerJob::executeTask() +{ + qDebug() << m_job_name.toLocal8Bit() << " started."; + m_running = true; + for (auto iter : javacheckers) + { + javaresults.append(JavaCheckResult()); + connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult))); + iter->performCheck(); + } +} diff --git a/api/logic/java/JavaCheckerJob.h b/api/logic/java/JavaCheckerJob.h new file mode 100644 index 00000000..aca0d02e --- /dev/null +++ b/api/logic/java/JavaCheckerJob.h @@ -0,0 +1,84 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "JavaChecker.h" +#include "tasks/Task.h" + +class JavaCheckerJob; +typedef std::shared_ptr JavaCheckerJobPtr; + +class JavaCheckerJob : public Task +{ + Q_OBJECT +public: + explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name) {}; + + bool addJavaCheckerAction(JavaCheckerPtr base) + { + javacheckers.append(base); + total_progress++; + // if this is already running, the action needs to be started right away! + if (isRunning()) + { + setProgress(current_progress, total_progress); + connect(base.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult))); + + base->performCheck(); + } + return true; + } + + JavaCheckerPtr operator[](int index) + { + return javacheckers[index]; + } + ; + JavaCheckerPtr first() + { + if (javacheckers.size()) + return javacheckers[0]; + return JavaCheckerPtr(); + } + int size() const + { + return javacheckers.size(); + } + virtual bool isRunning() const override + { + return m_running; + } + +signals: + void started(); + void finished(QList); + +private slots: + void partFinished(JavaCheckResult result); + +protected: + virtual void executeTask() override; + +private: + QString m_job_name; + QList javacheckers; + QList javaresults; + qint64 current_progress = 0; + qint64 total_progress = 0; + int num_finished = 0; + bool m_running = false; +}; diff --git a/api/logic/java/JavaInstall.cpp b/api/logic/java/JavaInstall.cpp new file mode 100644 index 00000000..bb262b6e --- /dev/null +++ b/api/logic/java/JavaInstall.cpp @@ -0,0 +1,28 @@ +#include "JavaInstall.h" +#include + +bool JavaInstall::operator<(const JavaInstall &rhs) +{ + auto archCompare = Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); + if(archCompare != 0) + return archCompare < 0; + if(id < rhs.id) + { + return true; + } + if(id > rhs.id) + { + return false; + } + return Strings::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; +} + +bool JavaInstall::operator==(const JavaInstall &rhs) +{ + return arch == rhs.arch && id == rhs.id && path == rhs.path; +} + +bool JavaInstall::operator>(const JavaInstall &rhs) +{ + return (!operator<(rhs)) && (!operator==(rhs)); +} diff --git a/api/logic/java/JavaInstall.h b/api/logic/java/JavaInstall.h new file mode 100644 index 00000000..882c7386 --- /dev/null +++ b/api/logic/java/JavaInstall.h @@ -0,0 +1,38 @@ +#pragma once + +#include "BaseVersion.h" +#include "JavaVersion.h" + +struct JavaInstall : public BaseVersion +{ + JavaInstall(){} + JavaInstall(QString id, QString arch, QString path) + : id(id), arch(arch), path(path) + { + } + virtual QString descriptor() + { + return id.toString(); + } + + virtual QString name() + { + return id.toString(); + } + + virtual QString typeString() const + { + return arch; + } + + bool operator<(const JavaInstall & rhs); + bool operator==(const JavaInstall & rhs); + bool operator>(const JavaInstall & rhs); + + JavaVersion id; + QString arch; + QString path; + bool recommended = false; +}; + +typedef std::shared_ptr JavaInstallPtr; diff --git a/api/logic/java/JavaInstallList.cpp b/api/logic/java/JavaInstallList.cpp new file mode 100644 index 00000000..c0729227 --- /dev/null +++ b/api/logic/java/JavaInstallList.cpp @@ -0,0 +1,186 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#include "java/JavaInstallList.h" +#include "java/JavaCheckerJob.h" +#include "java/JavaUtils.h" +#include "MMCStrings.h" +#include "minecraft/VersionFilterData.h" + +JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent) +{ +} + +Task *JavaInstallList::getLoadTask() +{ + return new JavaListLoadTask(this); +} + +const BaseVersionPtr JavaInstallList::at(int i) const +{ + return m_vlist.at(i); +} + +bool JavaInstallList::isLoaded() +{ + return m_loaded; +} + +int JavaInstallList::count() const +{ + return m_vlist.count(); +} + +QVariant JavaInstallList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = std::dynamic_pointer_cast(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 versions) +{ + beginResetModel(); + m_vlist = versions; + m_loaded = true; + sortVersions(); + if(m_vlist.size()) + { + auto best = std::dynamic_pointer_cast(m_vlist[0]); + best->recommended = true; + } + endResetModel(); +} + +bool sortJavas(BaseVersionPtr left, BaseVersionPtr right) +{ + auto rleft = std::dynamic_pointer_cast(left); + auto rright = std::dynamic_pointer_cast(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 candidate_paths = ju.FindJavaPaths(); + + m_job = std::shared_ptr(new JavaCheckerJob("Java detection")); + connect(m_job.get(), SIGNAL(finished(QList)), this, SLOT(javaCheckerFinished(QList))); + 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 results) +{ + QList candidates; + + qDebug() << "Found the following valid Java installations:"; + for(JavaCheckResult result : results) + { + if(result.valid) + { + JavaInstallPtr javaVersion(new JavaInstall()); + + javaVersion->id = result.javaVersion; + javaVersion->arch = result.mojangPlatform; + javaVersion->path = result.path; + candidates.append(javaVersion); + + qDebug() << " " << javaVersion->id.toString() << javaVersion->arch << javaVersion->path; + } + } + + QList javas_bvp; + for (auto java : candidates) + { + //qDebug() << java->id << java->arch << " at " << java->path; + BaseVersionPtr bp_java = std::dynamic_pointer_cast(java); + + if (bp_java) + { + javas_bvp.append(java); + } + } + + m_list->updateListData(javas_bvp); + emitSucceeded(); +} diff --git a/api/logic/java/JavaInstallList.h b/api/logic/java/JavaInstallList.h new file mode 100644 index 00000000..cf0e5784 --- /dev/null +++ b/api/logic/java/JavaInstallList.h @@ -0,0 +1,71 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "BaseVersionList.h" +#include "tasks/Task.h" + +#include "JavaCheckerJob.h" +#include "JavaInstall.h" + +#include "multimc_logic_export.h" + +class JavaListLoadTask; + +class MULTIMC_LOGIC_EXPORT JavaInstallList : public BaseVersionList +{ + Q_OBJECT +public: + explicit JavaInstallList(QObject *parent = 0); + + virtual Task *getLoadTask() override; + virtual bool isLoaded() override; + virtual const BaseVersionPtr at(int i) const override; + virtual int count() const override; + virtual void sortVersions() override; + + virtual QVariant data(const QModelIndex &index, int role) const override; + virtual RoleList providesRoles() const override; + +public slots: + virtual void updateListData(QList versions) override; + +protected: + QList m_vlist; + + bool m_loaded = false; +}; + +class JavaListLoadTask : public Task +{ + Q_OBJECT + +public: + explicit JavaListLoadTask(JavaInstallList *vlist); + ~JavaListLoadTask(); + + virtual void executeTask(); +public slots: + void javaCheckerFinished(QList results); + +protected: + std::shared_ptr m_job; + JavaInstallList *m_list; + JavaInstall *m_currentRecommended; +}; diff --git a/api/logic/java/JavaUtils.cpp b/api/logic/java/JavaUtils.cpp new file mode 100644 index 00000000..88996e9f --- /dev/null +++ b/api/logic/java/JavaUtils.cpp @@ -0,0 +1,219 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +#include +#include "java/JavaUtils.h" +#include "java/JavaCheckerJob.h" +#include "java/JavaInstallList.h" +#include "FileSystem.h" + +JavaUtils::JavaUtils() +{ +} + +JavaInstallPtr JavaUtils::MakeJavaPtr(QString path, QString id, QString arch) +{ + JavaInstallPtr javaVersion(new JavaInstall()); + + javaVersion->id = id; + javaVersion->arch = arch; + javaVersion->path = path; + + return javaVersion; +} + +JavaInstallPtr JavaUtils::GetDefaultJava() +{ + JavaInstallPtr javaVersion(new JavaInstall()); + + javaVersion->id = "java"; + javaVersion->arch = "unknown"; + javaVersion->path = "java"; + + return javaVersion; +} + +#if defined(Q_OS_WIN32) +QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName) +{ + QList javas; + + QString archType = "unknown"; + if (keyType == KEY_WOW64_64KEY) + archType = "64"; + else if (keyType == KEY_WOW64_32KEY) + archType = "32"; + + HKEY jreKey; + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, keyName.toStdString().c_str(), 0, + KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) == ERROR_SUCCESS) + { + // Read the current type version from the registry. + // This will be used to find any key that contains the JavaHome value. + char *value = new char[0]; + DWORD valueSz = 0; + if (RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz) == + ERROR_MORE_DATA) + { + value = new char[valueSz]; + RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz); + } + + QString recommended = value; + + TCHAR subKeyName[255]; + DWORD subKeyNameSize, numSubKeys, retCode; + + // Get the number of subkeys + RegQueryInfoKey(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, + NULL, NULL); + + // Iterate until RegEnumKeyEx fails + if (numSubKeys > 0) + { + for (int i = 0; i < numSubKeys; i++) + { + subKeyNameSize = 255; + retCode = RegEnumKeyEx(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, + NULL); + if (retCode == ERROR_SUCCESS) + { + // Now open the registry key for the version that we just got. + QString newKeyName = keyName + "\\" + subKeyName; + + HKEY newKey; + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, newKeyName.toStdString().c_str(), 0, + KEY_READ | KEY_WOW64_64KEY, &newKey) == ERROR_SUCCESS) + { + // Read the JavaHome value to find where Java is installed. + value = new char[0]; + valueSz = 0; + if (RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value, + &valueSz) == ERROR_MORE_DATA) + { + value = new char[valueSz]; + RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value, + &valueSz); + + // Now, we construct the version object and add it to the list. + JavaInstallPtr javaVersion(new JavaInstall()); + + javaVersion->id = subKeyName; + javaVersion->arch = archType; + javaVersion->path = + QDir(FS::PathCombine(value, "bin")).absoluteFilePath("javaw.exe"); + javas.append(javaVersion); + } + + RegCloseKey(newKey); + } + } + } + } + + RegCloseKey(jreKey); + } + + return javas; +} + +QList JavaUtils::FindJavaPaths() +{ + QList java_candidates; + + QList JRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + QList JDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); + QList JRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + QList JDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); + + java_candidates.append(JRE64s); + java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); + java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); + java_candidates.append(JDK64s); + java_candidates.append(JRE32s); + java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); + java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); + java_candidates.append(JDK32s); + java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); + + QList 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 JavaUtils::FindJavaPaths() +{ + QList 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 JavaUtils::FindJavaPaths() +{ + qDebug() << "Linux Java detection incomplete - defaulting to \"java\""; + + QList javas; + javas.append(this->GetDefaultJava()->path); + javas.append("/opt/java/bin/java"); + javas.append("/usr/bin/java"); + + return javas; +} +#else +QList JavaUtils::FindJavaPaths() +{ + qDebug() << "Unknown operating system build - defaulting to \"java\""; + + QList javas; + javas.append(this->GetDefaultJava()->path); + + return javas; +} +#endif diff --git a/api/logic/java/JavaUtils.h b/api/logic/java/JavaUtils.h new file mode 100644 index 00000000..3fb88341 --- /dev/null +++ b/api/logic/java/JavaUtils.h @@ -0,0 +1,43 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "JavaCheckerJob.h" +#include "JavaChecker.h" +#include "JavaInstallList.h" + +#ifdef Q_OS_WIN +#include +#endif + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT JavaUtils : public QObject +{ + Q_OBJECT +public: + JavaUtils(); + + JavaInstallPtr MakeJavaPtr(QString path, QString id = "unknown", QString arch = "unknown"); + QList FindJavaPaths(); + JavaInstallPtr GetDefaultJava(); + +#ifdef Q_OS_WIN + QList FindJavaFromRegistryKey(DWORD keyType, QString keyName); +#endif +}; diff --git a/api/logic/java/JavaVersion.cpp b/api/logic/java/JavaVersion.cpp new file mode 100644 index 00000000..84fc48a4 --- /dev/null +++ b/api/logic/java/JavaVersion.cpp @@ -0,0 +1,112 @@ +#include "JavaVersion.h" +#include + +#include +#include + +JavaVersion & JavaVersion::operator=(const QString & javaVersionString) +{ + string = javaVersionString; + + auto getCapturedInteger = [](const QRegularExpressionMatch & match, const QString &what) -> int + { + auto str = match.captured(what); + if(str.isEmpty()) + { + return 0; + } + return str.toInt(); + }; + + QRegularExpression pattern; + if(javaVersionString.startsWith("1.")) + { + pattern = QRegularExpression ("1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); + } + else + { + pattern = QRegularExpression("(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); + } + + auto match = pattern.match(string); + parseable = match.hasMatch(); + major = getCapturedInteger(match, "major"); + minor = getCapturedInteger(match, "minor"); + security = getCapturedInteger(match, "security"); + prerelease = match.captured("prerelease"); + return *this; +} + +JavaVersion::JavaVersion(const QString &rhs) +{ + operator=(rhs); +} + +QString JavaVersion::toString() +{ + return string; +} + +bool JavaVersion::requiresPermGen() +{ + if(parseable) + { + return major < 8; + } + return true; +} + +bool JavaVersion::operator<(const JavaVersion &rhs) +{ + if(parseable && rhs.parseable) + { + if(major < rhs.major) + return true; + if(major > rhs.major) + return false; + if(minor < rhs.minor) + return true; + if(minor > rhs.minor) + return false; + if(security < rhs.security) + return true; + if(security > rhs.security) + return false; + + // everything else being equal, consider prerelease status + bool thisPre = !prerelease.isEmpty(); + bool rhsPre = !rhs.prerelease.isEmpty(); + if(thisPre && !rhsPre) + { + // this is a prerelease and the other one isn't -> lesser + return true; + } + else if(!thisPre && rhsPre) + { + // this isn't a prerelease and the other one is -> greater + return false; + } + else if(thisPre && rhsPre) + { + // both are prereleases - use natural compare... + return Strings::naturalCompare(prerelease, rhs.prerelease, Qt::CaseSensitive) < 0; + } + // neither is prerelease, so they are the same -> this cannot be less than rhs + return false; + } + else return Strings::naturalCompare(string, rhs.string, Qt::CaseSensitive) < 0; +} + +bool JavaVersion::operator==(const JavaVersion &rhs) +{ + if(parseable && rhs.parseable) + { + return major == rhs.major && minor == rhs.minor && security == rhs.security && prerelease == rhs.prerelease; + } + return string == rhs.string; +} + +bool JavaVersion::operator>(const JavaVersion &rhs) +{ + return (!operator<(rhs)) && (!operator==(rhs)); +} diff --git a/api/logic/java/JavaVersion.h b/api/logic/java/JavaVersion.h new file mode 100644 index 00000000..f9a733d3 --- /dev/null +++ b/api/logic/java/JavaVersion.h @@ -0,0 +1,30 @@ +#pragma once + +#include "multimc_logic_export.h" +#include + +class MULTIMC_LOGIC_EXPORT JavaVersion +{ + friend class JavaVersionTest; +public: + JavaVersion() {}; + JavaVersion(const QString & rhs); + + JavaVersion & operator=(const QString & rhs); + + bool operator<(const JavaVersion & rhs); + bool operator==(const JavaVersion & rhs); + bool operator>(const JavaVersion & rhs); + + bool requiresPermGen(); + + QString toString(); + +private: + QString string; + int major = 0; + int minor = 0; + int security = 0; + bool parseable = false; + QString prerelease; +}; diff --git a/api/logic/launch/LaunchStep.cpp b/api/logic/launch/LaunchStep.cpp new file mode 100644 index 00000000..3078043b --- /dev/null +++ b/api/logic/launch/LaunchStep.cpp @@ -0,0 +1,27 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LaunchStep.h" +#include "LaunchTask.h" + +void LaunchStep::bind(LaunchTask *parent) +{ + m_parent = parent; + connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch); + connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine); + connect(this, &LaunchStep::logLines, parent, &LaunchTask::onLogLines); + connect(this, &LaunchStep::finished, parent, &LaunchTask::onStepFinished); + connect(this, &LaunchStep::progressReportingRequest, parent, &LaunchTask::onProgressReportingRequested); +} diff --git a/api/logic/launch/LaunchStep.h b/api/logic/launch/LaunchStep.h new file mode 100644 index 00000000..ea472c0d --- /dev/null +++ b/api/logic/launch/LaunchStep.h @@ -0,0 +1,48 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tasks/Task.h" +#include "MessageLevel.h" + +#include + +class LaunchTask; +class LaunchStep: public Task +{ + Q_OBJECT +public: /* methods */ + explicit LaunchStep(LaunchTask *parent):Task(nullptr), m_parent(parent) + { + bind(parent); + }; + virtual ~LaunchStep() {}; + +protected: /* methods */ + virtual void bind(LaunchTask *parent); + +signals: + void logLines(QStringList lines, MessageLevel::Enum level); + void logLine(QString line, MessageLevel::Enum level); + void readyForLaunch(); + void progressReportingRequest(); + +public slots: + virtual void proceed() {}; + +protected: /* data */ + LaunchTask *m_parent; +}; \ No newline at end of file diff --git a/api/logic/launch/LaunchTask.cpp b/api/logic/launch/LaunchTask.cpp new file mode 100644 index 00000000..5b7ff182 --- /dev/null +++ b/api/logic/launch/LaunchTask.cpp @@ -0,0 +1,228 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 +#include +#include +#include +#include +#include +#include + +void LaunchTask::init() +{ + m_instance->setRunning(true); +} + +std::shared_ptr LaunchTask::create(InstancePtr inst) +{ + std::shared_ptr proc(new LaunchTask(inst)); + proc->init(); + return proc; +} + +LaunchTask::LaunchTask(InstancePtr instance): m_instance(instance) +{ +} + +void LaunchTask::appendStep(std::shared_ptr step) +{ + m_steps.append(step); +} + +void LaunchTask::prependStep(std::shared_ptr step) +{ + m_steps.prepend(step); +} + +void LaunchTask::executeTask() +{ + if(!m_steps.size()) + { + state = LaunchTask::Finished; + emitSucceeded(); + } + state = LaunchTask::Running; + onStepFinished(); +} + +void LaunchTask::onReadyForLaunch() +{ + state = LaunchTask::Waiting; + emit readyForLaunch(); +} + +void LaunchTask::onStepFinished() +{ + // initial -> just start the first step + if(currentStep == -1) + { + currentStep ++; + m_steps[currentStep]->start(); + return; + } + + auto step = m_steps[currentStep]; + if(step->successful()) + { + // end? + if(currentStep == m_steps.size() - 1) + { + emitSucceeded(); + } + else + { + currentStep ++; + step = m_steps[currentStep]; + step->start(); + } + } + else + { + emitFailed(step->failReason()); + } +} + +void LaunchTask::onProgressReportingRequested() +{ + state = LaunchTask::Waiting; + emit requestProgress(m_steps[currentStep].get()); +} + +void LaunchTask::setCensorFilter(QMap filter) +{ + m_censorFilter = filter; +} + +QString LaunchTask::censorPrivateInfo(QString in) +{ + auto iter = m_censorFilter.begin(); + while (iter != m_censorFilter.end()) + { + in.replace(iter.key(), iter.value()); + iter++; + } + return in; +} + +void LaunchTask::proceed() +{ + if(state != LaunchTask::Waiting) + { + return; + } + m_steps[currentStep]->proceed(); +} + +bool LaunchTask::abort() +{ + switch(state) + { + case LaunchTask::Aborted: + case LaunchTask::Failed: + case LaunchTask::Finished: + return true; + case LaunchTask::NotStarted: + { + state = LaunchTask::Aborted; + emitFailed("Aborted"); + return true; + } + case LaunchTask::Running: + case LaunchTask::Waiting: + { + auto step = m_steps[currentStep]; + if(!step->canAbort()) + { + return false; + } + if(step->abort()) + { + state = LaunchTask::Aborted; + return true; + } + } + default: + break; + } + return false; +} + +void LaunchTask::onLogLines(const QStringList &lines, MessageLevel::Enum defaultLevel) +{ + for (auto & line: lines) + { + onLogLine(line, defaultLevel); + } +} + +void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) +{ + // if the launcher part set a log level, use it + auto innerLevel = MessageLevel::fromLine(line); + if(innerLevel != MessageLevel::Unknown) + { + level = innerLevel; + } + + // If the level is still undetermined, guess level + if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) + { + level = m_instance->guessLevel(line, level); + } + + // censor private user info + line = censorPrivateInfo(line); + + emit log(line, level); +} + +void LaunchTask::emitSucceeded() +{ + m_instance->cleanupAfterRun(); + m_instance->setRunning(false); + Task::emitSucceeded(); +} + +void LaunchTask::emitFailed(QString reason) +{ + m_instance->cleanupAfterRun(); + m_instance->setRunning(false); + Task::emitFailed(reason); +} + +QString LaunchTask::substituteVariables(const QString &cmd) const +{ + QString out = cmd; + auto variables = m_instance->getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + out.replace("$" + it.key(), it.value()); + } + auto env = QProcessEnvironment::systemEnvironment(); + for (auto var : env.keys()) + { + out.replace("$" + var, env.value(var)); + } + return out; +} + diff --git a/api/logic/launch/LaunchTask.h b/api/logic/launch/LaunchTask.h new file mode 100644 index 00000000..447445ca --- /dev/null +++ b/api/logic/launch/LaunchTask.h @@ -0,0 +1,122 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 +#include "BaseInstance.h" +#include "MessageLevel.h" +#include "LoggedProcess.h" +#include "LaunchStep.h" + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT LaunchTask: public Task +{ + Q_OBJECT +protected: + explicit LaunchTask(InstancePtr instance); + void init(); + +public: + enum State + { + NotStarted, + Running, + Waiting, + Failed, + Aborted, + Finished + }; + +public: /* methods */ + static std::shared_ptr create(InstancePtr inst); + virtual ~LaunchTask() {}; + + void appendStep(std::shared_ptr step); + void prependStep(std::shared_ptr step); + void setCensorFilter(QMap filter); + + InstancePtr instance() + { + return m_instance; + } + + void setPid(qint64 pid) + { + m_pid = pid; + } + + qint64 pid() + { + return m_pid; + } + + /** + * @brief prepare the process for launch (for multi-stage launch) + */ + virtual void executeTask() override; + + /** + * @brief launch the armed instance + */ + void proceed(); + + /** + * @brief abort launch + */ + virtual bool abort() override; + +public: + QString substituteVariables(const QString &cmd) const; + QString censorPrivateInfo(QString in); + +protected: /* methods */ + virtual void emitFailed(QString reason) override; + virtual void emitSucceeded() override; + +signals: + /** + * @brief emitted when the launch preparations are done + */ + void readyForLaunch(); + + void requestProgress(Task *task); + + void requestLogging(); + + /** + * @brief emitted when we want to log something + * @param text the text to log + * @param level the level to log at + */ + void log(QString text, MessageLevel::Enum level = MessageLevel::MultiMC); + +public slots: + void onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel = MessageLevel::MultiMC); + void onLogLine(QString line, MessageLevel::Enum defaultLevel = MessageLevel::MultiMC); + void onReadyForLaunch(); + void onStepFinished(); + void onProgressReportingRequested(); + +protected: /* data */ + InstancePtr m_instance; + QList > m_steps; + QMap m_censorFilter; + int currentStep = -1; + State state = NotStarted; + qint64 m_pid = -1; +}; diff --git a/api/logic/launch/LoggedProcess.cpp b/api/logic/launch/LoggedProcess.cpp new file mode 100644 index 00000000..88ca40aa --- /dev/null +++ b/api/logic/launch/LoggedProcess.cpp @@ -0,0 +1,163 @@ +#include "LoggedProcess.h" +#include "MessageLevel.h" +#include + +LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent) +{ + // QProcess has a strange interface... let's map a lot of those into a few. + connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); + connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); + connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(on_exit(int,QProcess::ExitStatus))); + connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError))); + connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); +} + +QStringList reprocess(const QByteArray & data, QString & leftover) +{ + QString str = leftover + QString::fromLocal8Bit(data); + + str.remove('\r'); + QStringList lines = str.split("\n"); + leftover = lines.takeLast(); + return lines; +} + +void LoggedProcess::on_stdErr() +{ + auto lines = reprocess(readAllStandardError(), m_err_leftover); + emit log(lines, MessageLevel::StdErr); +} + +void LoggedProcess::on_stdOut() +{ + auto lines = reprocess(readAllStandardOutput(), m_out_leftover); + emit log(lines, MessageLevel::StdOut); +} + +void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status) +{ + // save the exit code + m_exit_code = exit_code; + + // Flush console window + if (!m_err_leftover.isEmpty()) + { + emit log({m_err_leftover}, MessageLevel::StdErr); + m_err_leftover.clear(); + } + if (!m_out_leftover.isEmpty()) + { + emit log({m_err_leftover}, MessageLevel::StdOut); + m_out_leftover.clear(); + } + + // based on state, send signals + if (!m_is_aborting) + { + if (status == QProcess::NormalExit) + { + //: Message displayed on instance exit + emit log({tr("Process exited with code %1.").arg(exit_code)}, MessageLevel::MultiMC); + changeState(LoggedProcess::Finished); + } + else + { + //: Message displayed on instance crashed + if(exit_code == -1) + emit log({tr("Process crashed.")}, MessageLevel::MultiMC); + else + emit log({tr("Process crashed with exitcode %1.").arg(exit_code)}, MessageLevel::MultiMC); + changeState(LoggedProcess::Crashed); + } + } + else + { + //: Message displayed after the instance exits due to kill request + emit log({tr("Process was killed by user.")}, MessageLevel::Error); + changeState(LoggedProcess::Aborted); + } +} + +void LoggedProcess::on_error(QProcess::ProcessError error) +{ + switch(error) + { + case QProcess::FailedToStart: + { + emit log({tr("The process failed to start.")}, MessageLevel::Fatal); + changeState(LoggedProcess::FailedToStart); + break; + } + // we'll just ignore those... never needed them + case QProcess::Crashed: + case QProcess::ReadError: + case QProcess::Timedout: + case QProcess::UnknownError: + case QProcess::WriteError: + break; + } +} + +void LoggedProcess::kill() +{ + m_is_aborting = true; + QProcess::kill(); +} + +int LoggedProcess::exitCode() const +{ + return m_exit_code; +} + +void LoggedProcess::changeState(LoggedProcess::State state) +{ + if(state == m_state) + return; + m_state = state; + emit stateChanged(m_state); +} + +LoggedProcess::State LoggedProcess::state() const +{ + return m_state; +} + +void LoggedProcess::on_stateChange(QProcess::ProcessState state) +{ + switch(state) + { + case QProcess::NotRunning: + break; // let's not - there are too many that handle this already. + case QProcess::Starting: + { + if(m_state != LoggedProcess::NotRunning) + { + qWarning() << "Wrong state change for process from state" << m_state << "to" << (int) LoggedProcess::Starting; + } + changeState(LoggedProcess::Starting); + return; + } + case QProcess::Running: + { + if(m_state != LoggedProcess::Starting) + { + qWarning() << "Wrong state change for process from state" << m_state << "to" << (int) LoggedProcess::Running; + } + changeState(LoggedProcess::Running); + return; + } + } +} + +#if defined Q_OS_WIN32 +#include +#endif + +qint64 LoggedProcess::processId() const +{ +#ifdef Q_OS_WIN + return pid() ? pid()->dwProcessId : 0; +#else + return pid(); +#endif +} diff --git a/api/logic/launch/LoggedProcess.h b/api/logic/launch/LoggedProcess.h new file mode 100644 index 00000000..baa53d79 --- /dev/null +++ b/api/logic/launch/LoggedProcess.h @@ -0,0 +1,76 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "MessageLevel.h" + +/* + * This is a basic process. + * It has line-based logging support and hides some of the nasty bits. + */ +class LoggedProcess : public QProcess +{ +Q_OBJECT +public: + enum State + { + NotRunning, + Starting, + FailedToStart, + Running, + Finished, + Crashed, + Aborted + }; + +public: + explicit LoggedProcess(QObject* parent = 0); + virtual ~LoggedProcess() {}; + + State state() const; + int exitCode() const; + qint64 processId() const; + +signals: + void log(QStringList lines, MessageLevel::Enum level); + void stateChanged(LoggedProcess::State state); + +public slots: + /** + * @brief kill the process - equivalent to kill -9 + */ + void kill(); + + +private slots: + void on_stdErr(); + void on_stdOut(); + void on_exit(int exit_code, QProcess::ExitStatus status); + void on_error(QProcess::ProcessError error); + void on_stateChange(QProcess::ProcessState); + +private: + void changeState(LoggedProcess::State state); + +private: + QString m_err_leftover; + QString m_out_leftover; + bool m_killed = false; + State m_state = NotRunning; + int m_exit_code = 0; + bool m_is_aborting = false; +}; diff --git a/api/logic/launch/MessageLevel.cpp b/api/logic/launch/MessageLevel.cpp new file mode 100644 index 00000000..a5191290 --- /dev/null +++ b/api/logic/launch/MessageLevel.cpp @@ -0,0 +1,36 @@ +#include "MessageLevel.h" + +MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) +{ + if (levelName == "MultiMC") + return MessageLevel::MultiMC; + else if (levelName == "Debug") + return MessageLevel::Debug; + else if (levelName == "Info") + return MessageLevel::Info; + else if (levelName == "Message") + return MessageLevel::Message; + else if (levelName == "Warning") + return MessageLevel::Warning; + else if (levelName == "Error") + return MessageLevel::Error; + else if (levelName == "Fatal") + return MessageLevel::Fatal; + // Skip PrePost, it's not exposed to !![]! + // Also skip StdErr and StdOut + else + return MessageLevel::Unknown; +} + +MessageLevel::Enum MessageLevel::fromLine(QString &line) +{ + // Level prefix + int endmark = line.indexOf("]!"); + if (line.startsWith("!![") && endmark != -1) + { + auto level = MessageLevel::getLevel(line.left(endmark).mid(3)); + line = line.mid(endmark + 2); + return level; + } + return MessageLevel::Unknown; +} diff --git a/api/logic/launch/MessageLevel.h b/api/logic/launch/MessageLevel.h new file mode 100644 index 00000000..0128148d --- /dev/null +++ b/api/logic/launch/MessageLevel.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +/** + * @brief the MessageLevel Enum + * defines what level a log message is + */ +namespace MessageLevel +{ +enum Enum +{ + Unknown, /**< No idea what this is or where it came from */ + StdOut, /**< Undetermined stderr messages */ + StdErr, /**< Undetermined stdout messages */ + MultiMC, /**< MultiMC Messages */ + Debug, /**< Debug Messages */ + Info, /**< Info Messages */ + Message, /**< Standard Messages */ + Warning, /**< Warnings */ + Error, /**< Errors */ + Fatal, /**< Fatal Errors */ +}; +MessageLevel::Enum getLevel(const QString &levelName); + +/* Get message level from a line. Line is modified if it was successful. */ +MessageLevel::Enum fromLine(QString &line); +} diff --git a/api/logic/launch/steps/CheckJava.cpp b/api/logic/launch/steps/CheckJava.cpp new file mode 100644 index 00000000..a4eaa307 --- /dev/null +++ b/api/logic/launch/steps/CheckJava.cpp @@ -0,0 +1,92 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CheckJava.h" +#include +#include +#include +#include + +void CheckJava::executeTask() +{ + auto instance = m_parent->instance(); + auto settings = instance->settings(); + m_javaPath = FS::ResolveExecutable(settings->get("JavaPath").toString()); + bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool(); + + auto realJavaPath = QStandardPaths::findExecutable(m_javaPath); + if (realJavaPath.isEmpty()) + { + if (perInstance) + { + emit logLine( + tr("The java binary \"%1\" couldn't be found. Please fix the java path " + "override in the instance's settings or disable it.").arg(m_javaPath), + MessageLevel::Warning); + } + else + { + emit logLine(tr("The java binary \"%1\" couldn't be found. Please set up java in " + "the settings.").arg(m_javaPath), + MessageLevel::Warning); + } + emitFailed(tr("Java path is not valid.")); + return; + } + else + { + emit logLine("Java path is:\n" + m_javaPath + "\n\n", MessageLevel::MultiMC); + } + + QFileInfo javaInfo(realJavaPath); + qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); + auto storedUnixTime = settings->get("JavaTimestamp").toLongLong(); + m_javaUnixTime = javaUnixTime; + // if they are not the same, check! + if (javaUnixTime != storedUnixTime) + { + m_JavaChecker = std::make_shared(); + QString errorLog; + QString version; + emit logLine(tr("Checking Java version..."), MessageLevel::MultiMC); + connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, + &CheckJava::checkJavaFinished); + m_JavaChecker->m_path = realJavaPath; + m_JavaChecker->performCheck(); + return; + } + emitSucceeded(); +} + +void CheckJava::checkJavaFinished(JavaCheckResult result) +{ + if (!result.valid) + { + // Error message displayed if java can't start + emit logLine(tr("Could not start java:"), MessageLevel::Error); + emit logLines(result.errorLog.split('\n'), MessageLevel::Error); + emit logLine("\nCheck your MultiMC Java settings.", MessageLevel::MultiMC); + emitFailed(tr("Could not start java!")); + } + else + { + auto instance = m_parent->instance(); + emit logLine(tr("Java version is %1!\n").arg(result.javaVersion.toString()), + MessageLevel::MultiMC); + instance->settings()->set("JavaVersion", result.javaVersion.toString()); + instance->settings()->set("JavaTimestamp", m_javaUnixTime); + emitSucceeded(); + } +} diff --git a/api/logic/launch/steps/CheckJava.h b/api/logic/launch/steps/CheckJava.h new file mode 100644 index 00000000..b63dd4f4 --- /dev/null +++ b/api/logic/launch/steps/CheckJava.h @@ -0,0 +1,41 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +class CheckJava: public LaunchStep +{ + Q_OBJECT +public: + explicit CheckJava(LaunchTask *parent) :LaunchStep(parent){}; + virtual ~CheckJava() {}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +private slots: + void checkJavaFinished(JavaCheckResult result); + +private: + QString m_javaPath; + qlonglong m_javaUnixTime; + JavaCheckerPtr m_JavaChecker; +}; diff --git a/api/logic/launch/steps/LaunchMinecraft.cpp b/api/logic/launch/steps/LaunchMinecraft.cpp new file mode 100644 index 00000000..9b8cc0fb --- /dev/null +++ b/api/logic/launch/steps/LaunchMinecraft.cpp @@ -0,0 +1,155 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LaunchMinecraft.h" +#include +#include +#include +#include + +LaunchMinecraft::LaunchMinecraft(LaunchTask *parent) : LaunchStep(parent) +{ + connect(&m_process, &LoggedProcess::log, this, &LaunchMinecraft::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &LaunchMinecraft::on_state); +} + +void LaunchMinecraft::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); + + m_launchScript = minecraftInstance->createLaunchScript(m_session); + + QStringList args = minecraftInstance->javaArguments(); + + // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' is created. + if(!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->minecraftRoot(), "server-resource-packs"))) + { + emit logLine(tr("Couldn't create the 'server-resource-packs' folder"), MessageLevel::Error); + } + + QString allArgs = args.join(", "); + emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::MultiMC); + + auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); + + m_process.setProcessEnvironment(instance->createEnvironment()); + + QString wrapperCommand = instance->getWrapperCommand(); + if(!wrapperCommand.isEmpty()) + { + auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); + if (realWrapperCommand.isEmpty()) + { + QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(reason); + return; + } + emit logLine("Wrapper command is:\n" + wrapperCommand + "\n\n", MessageLevel::MultiMC); + args.prepend(javaPath); + m_process.start(wrapperCommand, args); + } + else + { + m_process.start(javaPath, args); + } +} + +void LaunchMinecraft::on_state(LoggedProcess::State state) +{ + switch(state) + { + case LoggedProcess::FailedToStart: + { + //: Error message displayed if instace can't start + QString reason = tr("Could not launch minecraft!"); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(reason); + return; + } + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + + { + m_parent->setPid(-1); + emitFailed("Game crashed."); + return; + } + case LoggedProcess::Finished: + { + m_parent->setPid(-1); + // if the exit code wasn't 0, report this as a crash + auto exitCode = m_process.exitCode(); + if(exitCode != 0) + { + emitFailed("Game crashed."); + return; + } + //FIXME: make this work again + // m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(exitCode)); + // run post-exit + emitSucceeded(); + break; + } + case LoggedProcess::Running: + emit logLine(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); + m_parent->setPid(m_process.processId()); + m_parent->instance()->setLastLaunch(); + // send the launch script to the launcher part + m_process.write(m_launchScript.toUtf8()); + qDebug() << m_launchScript; + + mayProceed = true; + emit readyForLaunch(); + break; + default: + break; + } +} + +void LaunchMinecraft::setWorkingDirectory(const QString &wd) +{ + m_process.setWorkingDirectory(wd); +} + +void LaunchMinecraft::proceed() +{ + if(mayProceed) + { + QString launchString("launch\n"); + m_process.write(launchString.toUtf8()); + mayProceed = false; + } +} + +bool LaunchMinecraft::abort() +{ + if(mayProceed) + { + mayProceed = false; + QString launchString("abort\n"); + m_process.write(launchString.toUtf8()); + } + else + { + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + } + return true; +} diff --git a/api/logic/launch/steps/LaunchMinecraft.h b/api/logic/launch/steps/LaunchMinecraft.h new file mode 100644 index 00000000..6b9f7919 --- /dev/null +++ b/api/logic/launch/steps/LaunchMinecraft.h @@ -0,0 +1,48 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +class LaunchMinecraft: public LaunchStep +{ + Q_OBJECT +public: + explicit LaunchMinecraft(LaunchTask *parent); + virtual void executeTask(); + virtual bool abort(); + virtual void proceed(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString &wd); + void setAuthSession(AuthSessionPtr session) + { + m_session = session; + } +private slots: + void on_state(LoggedProcess::State state); + +private: + LoggedProcess m_process; + QString m_command; + QString m_launchScript; + AuthSessionPtr m_session; + bool mayProceed = false; +}; diff --git a/api/logic/launch/steps/ModMinecraftJar.cpp b/api/logic/launch/steps/ModMinecraftJar.cpp new file mode 100644 index 00000000..fce2d70a --- /dev/null +++ b/api/logic/launch/steps/ModMinecraftJar.cpp @@ -0,0 +1,44 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModMinecraftJar.h" +#include +#include + +void ModMinecraftJar::executeTask() +{ + m_jarModTask = m_parent->instance()->createJarModdingTask(); + if(m_jarModTask) + { + connect(m_jarModTask.get(), SIGNAL(finished()), this, SLOT(jarModdingFinished())); + m_jarModTask->start(); + return; + } + emitSucceeded(); +} + +void ModMinecraftJar::jarModdingFinished() +{ + if(m_jarModTask->successful()) + { + emitSucceeded(); + } + else + { + QString reason = tr("jar modding failed because: %1.\n\n").arg(m_jarModTask->failReason()); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(reason); + } +} diff --git a/api/logic/launch/steps/ModMinecraftJar.h b/api/logic/launch/steps/ModMinecraftJar.h new file mode 100644 index 00000000..b35dfafa --- /dev/null +++ b/api/logic/launch/steps/ModMinecraftJar.h @@ -0,0 +1,39 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +// FIXME: temporary wrapper for existing task. +class ModMinecraftJar: public LaunchStep +{ + Q_OBJECT +public: + explicit ModMinecraftJar(LaunchTask *parent) : LaunchStep(parent) {}; + virtual ~ModMinecraftJar(){}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +private slots: + void jarModdingFinished(); + +private: + std::shared_ptr m_jarModTask; +}; diff --git a/api/logic/launch/steps/PostLaunchCommand.cpp b/api/logic/launch/steps/PostLaunchCommand.cpp new file mode 100644 index 00000000..29a45f1b --- /dev/null +++ b/api/logic/launch/steps/PostLaunchCommand.cpp @@ -0,0 +1,84 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PostLaunchCommand.h" +#include + +PostLaunchCommand::PostLaunchCommand(LaunchTask *parent) : LaunchStep(parent) +{ + auto instance = m_parent->instance(); + m_command = instance->getPostExitCommand(); + m_process.setProcessEnvironment(instance->createEnvironment()); + connect(&m_process, &LoggedProcess::log, this, &PostLaunchCommand::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &PostLaunchCommand::on_state); +} + +void PostLaunchCommand::executeTask() +{ + QString postlaunch_cmd = m_parent->substituteVariables(m_command); + emit logLine(tr("Running Post-Launch command: %1").arg(postlaunch_cmd), MessageLevel::MultiMC); + m_process.start(postlaunch_cmd); +} + +void PostLaunchCommand::on_state(LoggedProcess::State state) +{ + auto getError = [&]() + { + return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); + }; + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + auto error = getError(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + return; + } + case LoggedProcess::Finished: + { + if(m_process.exitCode() != 0) + { + auto error = getError(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + } + else + { + emit logLine(tr("Post-Launch command ran successfully.\n\n"), MessageLevel::MultiMC); + emitSucceeded(); + } + } + default: + break; + } +} + +void PostLaunchCommand::setWorkingDirectory(const QString &wd) +{ + m_process.setWorkingDirectory(wd); +} + +bool PostLaunchCommand::abort() +{ + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + return true; +} diff --git a/api/logic/launch/steps/PostLaunchCommand.h b/api/logic/launch/steps/PostLaunchCommand.h new file mode 100644 index 00000000..4d5b0a52 --- /dev/null +++ b/api/logic/launch/steps/PostLaunchCommand.h @@ -0,0 +1,39 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class PostLaunchCommand: public LaunchStep +{ + Q_OBJECT +public: + explicit PostLaunchCommand(LaunchTask *parent); + virtual void executeTask(); + virtual bool abort(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString &wd); +private slots: + void on_state(LoggedProcess::State state); + +private: + LoggedProcess m_process; + QString m_command; +}; diff --git a/api/logic/launch/steps/PreLaunchCommand.cpp b/api/logic/launch/steps/PreLaunchCommand.cpp new file mode 100644 index 00000000..47197a82 --- /dev/null +++ b/api/logic/launch/steps/PreLaunchCommand.cpp @@ -0,0 +1,85 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PreLaunchCommand.h" +#include + +PreLaunchCommand::PreLaunchCommand(LaunchTask *parent) : LaunchStep(parent) +{ + auto instance = m_parent->instance(); + m_command = instance->getPreLaunchCommand(); + m_process.setProcessEnvironment(instance->createEnvironment()); + connect(&m_process, &LoggedProcess::log, this, &PreLaunchCommand::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &PreLaunchCommand::on_state); +} + +void PreLaunchCommand::executeTask() +{ + //FIXME: where to put this? + QString prelaunch_cmd = m_parent->substituteVariables(m_command); + emit logLine(tr("Running Pre-Launch command: %1").arg(prelaunch_cmd), MessageLevel::MultiMC); + m_process.start(prelaunch_cmd); +} + +void PreLaunchCommand::on_state(LoggedProcess::State state) +{ + auto getError = [&]() + { + return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); + }; + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + auto error = getError(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + return; + } + case LoggedProcess::Finished: + { + if(m_process.exitCode() != 0) + { + auto error = getError(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + } + else + { + emit logLine(tr("Pre-Launch command ran successfully.\n\n"), MessageLevel::MultiMC); + emitSucceeded(); + } + } + default: + break; + } +} + +void PreLaunchCommand::setWorkingDirectory(const QString &wd) +{ + m_process.setWorkingDirectory(wd); +} + +bool PreLaunchCommand::abort() +{ + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + return true; +} diff --git a/api/logic/launch/steps/PreLaunchCommand.h b/api/logic/launch/steps/PreLaunchCommand.h new file mode 100644 index 00000000..077bdfca --- /dev/null +++ b/api/logic/launch/steps/PreLaunchCommand.h @@ -0,0 +1,39 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class PreLaunchCommand: public LaunchStep +{ + Q_OBJECT +public: + explicit PreLaunchCommand(LaunchTask *parent); + virtual void executeTask(); + virtual bool abort(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString &wd); +private slots: + void on_state(LoggedProcess::State state); + +private: + LoggedProcess m_process; + QString m_command; +}; diff --git a/api/logic/launch/steps/TextPrint.cpp b/api/logic/launch/steps/TextPrint.cpp new file mode 100644 index 00000000..f307b1fd --- /dev/null +++ b/api/logic/launch/steps/TextPrint.cpp @@ -0,0 +1,29 @@ +#include "TextPrint.h" + +TextPrint::TextPrint(LaunchTask * parent, const QStringList &lines, MessageLevel::Enum level) : LaunchStep(parent) +{ + m_lines = lines; + m_level = level; +} +TextPrint::TextPrint(LaunchTask *parent, const QString &line, MessageLevel::Enum level) : LaunchStep(parent) +{ + m_lines.append(line); + m_level = level; +} + +void TextPrint::executeTask() +{ + emit logLines(m_lines, m_level); + emitSucceeded(); +} + +bool TextPrint::canAbort() const +{ + return true; +} + +bool TextPrint::abort() +{ + emitFailed("Aborted."); + return true; +} diff --git a/api/logic/launch/steps/TextPrint.h b/api/logic/launch/steps/TextPrint.h new file mode 100644 index 00000000..fdd9014a --- /dev/null +++ b/api/logic/launch/steps/TextPrint.h @@ -0,0 +1,43 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "multimc_logic_export.h" + +/* + * FIXME: maybe do not export + */ + +class MULTIMC_LOGIC_EXPORT TextPrint: public LaunchStep +{ + Q_OBJECT +public: + explicit TextPrint(LaunchTask *parent, const QStringList &lines, MessageLevel::Enum level); + explicit TextPrint(LaunchTask *parent, const QString &line, MessageLevel::Enum level); + virtual ~TextPrint(){}; + + virtual void executeTask(); + virtual bool canAbort() const; + virtual bool abort(); + +private: + QStringList m_lines; + MessageLevel::Enum m_level; +}; diff --git a/api/logic/launch/steps/Update.cpp b/api/logic/launch/steps/Update.cpp new file mode 100644 index 00000000..4901f001 --- /dev/null +++ b/api/logic/launch/steps/Update.cpp @@ -0,0 +1,50 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Update.h" +#include + +void Update::executeTask() +{ + m_updateTask = m_parent->instance()->createUpdateTask(); + if(m_updateTask) + { + connect(m_updateTask.get(), SIGNAL(finished()), this, SLOT(updateFinished())); + connect(m_updateTask.get(), &Task::progress, this, &Task::setProgress); + connect(m_updateTask.get(), &Task::status, this, &Task::setStatus); + emit progressReportingRequest(); + return; + } + emitSucceeded(); +} + +void Update::proceed() +{ + m_updateTask->start(); +} + +void Update::updateFinished() +{ + if(m_updateTask->successful()) + { + emitSucceeded(); + } + else + { + QString reason = tr("Instance update failed because: %1.\n\n").arg(m_updateTask->failReason()); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(reason); + } +} diff --git a/api/logic/launch/steps/Update.h b/api/logic/launch/steps/Update.h new file mode 100644 index 00000000..14928253 --- /dev/null +++ b/api/logic/launch/steps/Update.h @@ -0,0 +1,41 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +// FIXME: stupid. should be defined by the instance type? or even completely abstracted away... +class Update: public LaunchStep +{ + Q_OBJECT +public: + explicit Update(LaunchTask *parent):LaunchStep(parent) {}; + virtual ~Update() {}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } + virtual void proceed(); +private slots: + void updateFinished(); + +private: + std::shared_ptr m_updateTask; +}; diff --git a/api/logic/minecraft/AssetsUtils.cpp b/api/logic/minecraft/AssetsUtils.cpp new file mode 100644 index 00000000..7a525abe --- /dev/null +++ b/api/logic/minecraft/AssetsUtils.cpp @@ -0,0 +1,230 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AssetsUtils.h" +#include "FileSystem.h" +#include "net/MD5EtagDownload.h" + +namespace AssetsUtils +{ + +/* + * Returns true on success, with index populated + * index is undefined otherwise + */ +bool loadAssetsIndexJson(QString assetsId, QString path, AssetsIndex *index) +{ + /* + { + "objects": { + "icons/icon_16x16.png": { + "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", + "size": 3665 + }, + ... + } + } + } + */ + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to read assets index file" << path; + return false; + } + index->id = assetsId; + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << "Failed to parse assets index file:" << parseError.errorString() + << "at offset " << QString::number(parseError.offset); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid assets index JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + QJsonValue isVirtual = root.value("virtual"); + if (!isVirtual.isUndefined()) + { + index->isVirtual = isVirtual.toBool(false); + } + + QJsonValue objects = root.value("objects"); + QVariantMap map = objects.toVariant().toMap(); + + for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) + { + // qDebug() << iter.key(); + + QVariant variant = iter.value(); + QVariantMap nested_objects = variant.toMap(); + + AssetObject object; + + for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); + nested_iter != nested_objects.end(); ++nested_iter) + { + // qDebug() << nested_iter.key() << nested_iter.value().toString(); + QString key = nested_iter.key(); + QVariant value = nested_iter.value(); + + if (key == "hash") + { + object.hash = value.toString(); + } + else if (key == "size") + { + object.size = value.toDouble(); + } + } + + index->objects.insert(iter.key(), object); + } + + return true; +} + +QDir reconstructAssets(QString assetsId) +{ + QDir assetsDir = QDir("assets/"); + QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); + QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); + QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); + + QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets"; + return virtualRoot; + } + + qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() + << objectDir.path() << virtualDir.path() << virtualRoot.path(); + + AssetsIndex index; + bool loadAssetsIndex = AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, &index); + + if (loadAssetsIndex && index.isVirtual) + { + qDebug() << "Reconstructing virtual assets folder at" << virtualRoot.path(); + + for (QString map : index.objects.keys()) + { + AssetObject asset_object = index.objects.value(map); + QString target_path = FS::PathCombine(virtualRoot.path(), map); + QFile target(target_path); + + QString tlk = asset_object.hash.left(2); + + QString original_path = FS::PathCombine(objectDir.path(), tlk, asset_object.hash); + QFile original(original_path); + if (!original.exists()) + continue; + if (!target.exists()) + { + QFileInfo info(target_path); + QDir target_dir = info.dir(); + // qDebug() << target_dir; + if (!target_dir.exists()) + QDir("").mkpath(target_dir.path()); + + bool couldCopy = original.copy(target_path); + qDebug() << " Copying" << original_path << "to" << target_path + << QString::number(couldCopy); // << original.errorString(); + } + } + + // TODO: Write last used time to virtualRoot/.lastused + } + + return virtualRoot; +} + +} + +NetActionPtr AssetObject::getDownloadAction() +{ + QFileInfo objectFile(getLocalPath()); + if ((!objectFile.isFile()) || (objectFile.size() != size)) + { + auto objectDL = MD5EtagDownload::make(getUrl(), objectFile.filePath()); + objectDL->m_total_progress = size; + return objectDL; + } + return nullptr; +} + +QString AssetObject::getLocalPath() +{ + return "assets/objects/" + getRelPath(); +} + +QUrl AssetObject::getUrl() +{ + return QUrl("http://resources.download.minecraft.net/" + getRelPath()); +} + +QString AssetObject::getRelPath() +{ + return hash.left(2) + "/" + hash; +} + +NetJobPtr AssetsIndex::getDownloadJob() +{ + auto job = new NetJob(QObject::tr("Assets for %1").arg(id)); + for (auto &object : objects.values()) + { + auto dl = object.getDownloadAction(); + if(dl) + { + job->addNetAction(dl); + } + } + if(job->size()) + return job; + return nullptr; +} diff --git a/api/logic/minecraft/AssetsUtils.h b/api/logic/minecraft/AssetsUtils.h new file mode 100644 index 00000000..90251c2d --- /dev/null +++ b/api/logic/minecraft/AssetsUtils.h @@ -0,0 +1,48 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#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 objects; + bool isVirtual = false; +}; + +namespace AssetsUtils +{ +bool loadAssetsIndexJson(QString id, QString file, AssetsIndex* index); +/// Reconstruct a virtual assets folder for the given assets ID and return the folder +QDir reconstructAssets(QString assetsId); +} diff --git a/api/logic/minecraft/GradleSpecifier.h b/api/logic/minecraft/GradleSpecifier.h new file mode 100644 index 00000000..18308537 --- /dev/null +++ b/api/logic/minecraft/GradleSpecifier.h @@ -0,0 +1,129 @@ +#pragma once + +#include +#include +#include "DefaultVariable.h" + +struct GradleSpecifier +{ + GradleSpecifier() + { + m_valid = false; + } + GradleSpecifier(QString value) + { + operator=(value); + } + GradleSpecifier & operator =(const QString & value) + { + /* + org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar + DEBUG 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" + DEBUG 1 "org.gradle.test.classifiers" + DEBUG 2 "service" + DEBUG 3 "1.0" + DEBUG 4 ":jdk15" + DEBUG 5 "jdk15" + DEBUG 6 "@jar" + DEBUG 7 "jar" + */ + QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(:([^:@]+))?" "(@([^:@]+))?"); + m_valid = matcher.exactMatch(value); + auto elements = matcher.capturedTexts(); + m_groupId = elements[1]; + m_artifactId = elements[2]; + m_version = elements[3]; + m_classifier = elements[5]; + if(!elements[7].isEmpty()) + { + m_extension = elements[7]; + } + return *this; + } + operator QString() const + { + if(!m_valid) + return "INVALID"; + QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; + if(!m_classifier.isEmpty()) + { + retval += ":" + m_classifier; + } + if(m_extension.isExplicit()) + { + retval += "@" + m_extension; + } + return retval; + } + QString toPath() const + { + if(!m_valid) + return "INVALID"; + QString path = m_groupId; + path.replace('.', '/'); + path += '/' + m_artifactId + '/' + m_version + '/' + m_artifactId + '-' + m_version; + if(!m_classifier.isEmpty()) + { + path += "-" + m_classifier; + } + path += "." + m_extension; + return path; + } + inline bool valid() const + { + return m_valid; + } + inline QString version() const + { + return m_version; + } + inline QString groupId() const + { + return m_groupId; + } + inline QString artifactId() const + { + return m_artifactId; + } + inline void setClassifier(const QString & classifier) + { + m_classifier = classifier; + } + inline QString classifier() const + { + return m_classifier; + } + inline QString extension() const + { + return m_extension; + } + inline QString artifactPrefix() const + { + return m_groupId + ":" + m_artifactId; + } + bool matchName(const GradleSpecifier & other) const + { + return other.artifactId() == artifactId() && other.groupId() == groupId(); + } + bool operator==(const GradleSpecifier & other) const + { + if(m_groupId != other.m_groupId) + return false; + if(m_artifactId != other.m_artifactId) + return false; + if(m_version != other.m_version) + return false; + if(m_classifier != other.m_classifier) + return false; + if(m_extension != other.m_extension) + return false; + return true; + } +private: + QString m_groupId; + QString m_artifactId; + QString m_version; + QString m_classifier; + DefaultVariable m_extension = DefaultVariable("jar"); + bool m_valid = false; +}; diff --git a/api/logic/minecraft/JarMod.h b/api/logic/minecraft/JarMod.h new file mode 100644 index 00000000..42d05da9 --- /dev/null +++ b/api/logic/minecraft/JarMod.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include +class Jarmod; +typedef std::shared_ptr JarmodPtr; +class Jarmod +{ +public: /* data */ + QString name; + QString originalName; +}; diff --git a/api/logic/minecraft/Library.cpp b/api/logic/minecraft/Library.cpp new file mode 100644 index 00000000..922db84e --- /dev/null +++ b/api/logic/minecraft/Library.cpp @@ -0,0 +1,239 @@ +#include "Library.h" +#include +#include +#include +#include + +void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& native, QStringList& native32, QStringList& native64) const +{ + auto actualPath = [&](QString relPath) + { + QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); + return out.absoluteFilePath(); + }; + if(m_mojangDownloads) + { + if(m_mojangDownloads->artifact) + { + auto artifact = m_mojangDownloads->artifact; + jar += actualPath(artifact->path); + } + if(!isNative()) + return; + if(m_nativeClassifiers.contains(system)) + { + auto nativeClassifier = m_nativeClassifiers[system]; + if(nativeClassifier.contains("${arch}")) + { + auto nat32Classifier = nativeClassifier; + nat32Classifier.replace("${arch}", "32"); + auto nat64Classifier = nativeClassifier; + nat64Classifier.replace("${arch}", "64"); + auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); + if(nat32info) + native32 += actualPath(nat32info->path); + auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); + if(nat64info) + native64 += actualPath(nat64info->path); + } + else + { + native += actualPath(m_mojangDownloads->getDownloadInfo(nativeClassifier)->path); + } + } + } + else + { + QString raw_storage = storageSuffix(system); + if(isNative()) + { + if (raw_storage.contains("${arch}")) + { + auto nat32Storage = raw_storage; + nat32Storage.replace("${arch}", "32"); + auto nat64Storage = raw_storage; + nat64Storage.replace("${arch}", "64"); + native32 += actualPath(nat32Storage); + native64 += actualPath(nat64Storage); + } + else + { + native += actualPath(raw_storage); + } + } + else + { + jar += actualPath(raw_storage); + } + } +} + +QList Library::getDownloads(OpSys system, HttpMetaCache * cache, QStringList &failedFiles) const +{ + QList out; + bool isLocal = (hint() == "local"); + bool isForge = (hint() == "forge-pack-xz"); + + auto add_download = [&](QString storage, QString dl) + { + auto entry = cache->resolveEntry("libraries", storage); + if (!entry->isStale()) + return true; + if(isLocal) + { + QFileInfo fileinfo(entry->getFullPath()); + if(!fileinfo.exists()) + { + failedFiles.append(entry->getFullPath()); + return false; + } + return true; + } + if (isForge) + { + out.append(ForgeXzDownload::make(storage, entry)); + } + else + { + out.append(CacheDownload::make(dl, entry)); + } + return true; + }; + + if(m_mojangDownloads) + { + if(m_mojangDownloads->artifact) + { + auto artifact = m_mojangDownloads->artifact; + add_download(artifact->path, artifact->url); + } + if(m_nativeClassifiers.contains(system)) + { + auto nativeClassifier = m_nativeClassifiers[system]; + if(nativeClassifier.contains("${arch}")) + { + auto nat32Classifier = nativeClassifier; + nat32Classifier.replace("${arch}", "32"); + auto nat64Classifier = nativeClassifier; + nat64Classifier.replace("${arch}", "64"); + auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); + if(nat32info) + add_download(nat32info->path, nat32info->url); + auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); + if(nat64info) + add_download(nat64info->path, nat64info->url); + } + else + { + auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier); + if(info) + { + add_download(info->path, info->url); + } + } + } + } + else + { + QString raw_storage = storageSuffix(system); + auto raw_dl = [&](){ + if (!m_absoluteURL.isEmpty()) + { + return m_absoluteURL; + } + + if (m_repositoryURL.isEmpty()) + { + return QString("https://" + URLConstants::LIBRARY_BASE) + raw_storage; + } + + if(m_repositoryURL.endsWith('/')) + { + return m_repositoryURL + raw_storage; + } + else + { + return m_repositoryURL + QChar('/') + raw_storage; + } + }(); + if (raw_storage.contains("${arch}")) + { + QString cooked_storage = raw_storage; + QString cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32")); + cooked_storage = raw_storage; + cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64")); + } + else + { + add_download(raw_storage, raw_dl); + } + } + return out; +} + +bool Library::isActive() const +{ + bool result = true; + if (m_rules.empty()) + { + result = true; + } + else + { + RuleAction ruleResult = Disallow; + for (auto rule : m_rules) + { + RuleAction temp = rule->apply(this); + if (temp != Defer) + ruleResult = temp; + } + result = result && (ruleResult == Allow); + } + if (isNative()) + { + result = result && m_nativeClassifiers.contains(currentSystem); + } + return result; +} + +void Library::setStoragePrefix(QString prefix) +{ + m_storagePrefix = prefix; +} + +QString Library::defaultStoragePrefix() +{ + return "libraries/"; +} + +QString Library::storagePrefix() const +{ + if(m_storagePrefix.isEmpty()) + { + return defaultStoragePrefix(); + } + return m_storagePrefix; +} + +QString Library::storageSuffix(OpSys system) const +{ + // non-native? use only the gradle specifier + if (!isNative()) + { + return m_name.toPath(); + } + + // otherwise native, override classifiers. Mojang HACK! + GradleSpecifier nativeSpec = m_name; + if (m_nativeClassifiers.contains(system)) + { + nativeSpec.setClassifier(m_nativeClassifiers[system]); + } + else + { + nativeSpec.setClassifier("INVALID"); + } + return nativeSpec.toPath(); +} diff --git a/api/logic/minecraft/Library.h b/api/logic/minecraft/Library.h new file mode 100644 index 00000000..fdce93f3 --- /dev/null +++ b/api/logic/minecraft/Library.h @@ -0,0 +1,184 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Rule.h" +#include "minecraft/OpSys.h" +#include "GradleSpecifier.h" +#include "net/URLConstants.h" +#include "MojangDownloadInfo.h" + +#include "multimc_logic_export.h" + +class Library; + +typedef std::shared_ptr LibraryPtr; + +class MULTIMC_LOGIC_EXPORT Library +{ + friend class OneSixVersionFormat; + friend class MojangVersionFormat; + friend class LibraryTest; +public: + Library() + { + } + Library(const QString &name) + { + m_name = name; + } + /// limited copy without some data. TODO: why? + static LibraryPtr limitedCopy(LibraryPtr base) + { + auto newlib = std::make_shared(); + newlib->m_name = base->m_name; + newlib->m_repositoryURL = base->m_repositoryURL; + newlib->m_hint = base->m_hint; + newlib->m_absoluteURL = base->m_absoluteURL; + newlib->m_extractExcludes = base->m_extractExcludes; + newlib->m_nativeClassifiers = base->m_nativeClassifiers; + newlib->m_rules = base->m_rules; + newlib->m_storagePrefix = base->m_storagePrefix; + newlib->m_mojangDownloads = base->m_mojangDownloads; + return newlib; + } + +public: /* methods */ + /// Returns the raw name field + const GradleSpecifier & rawName() const + { + return m_name; + } + + void setRawName(const GradleSpecifier & spec) + { + m_name = spec; + } + + void setClassifier(const QString & spec) + { + m_name.setClassifier(spec); + } + + /// returns the full group and artifact prefix + QString artifactPrefix() const + { + return m_name.artifactPrefix(); + } + + /// get the artifact ID + QString artifactId() const + { + return m_name.artifactId(); + } + + /// get the artifact version + QString version() const + { + return m_name.version(); + } + + /// Returns true if the library is native + bool isNative() const + { + return m_nativeClassifiers.size() != 0; + } + + void setStoragePrefix(QString prefix = QString()); + + /// Set the url base for downloads + void setRepositoryURL(const QString &base_url) + { + m_repositoryURL = base_url; + } + + void getApplicableFiles(OpSys system, QStringList & jar, QStringList & native, QStringList & native32, QStringList & native64) const; + + void setAbsoluteUrl(const QString &absolute_url) + { + m_absoluteURL = absolute_url; + } + + void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info) + { + m_mojangDownloads = info; + } + + void setHint(const QString &hint) + { + m_hint = hint; + } + + /// Set the load rules + void setRules(QList> rules) + { + m_rules = rules; + } + + /// Returns true if the library should be loaded (or extracted, in case of natives) + bool isActive() const; + + // Get a list of downloads for this library + QList getDownloads(OpSys system, class HttpMetaCache * cache, QStringList &failedFiles) const; + +private: /* methods */ + /// the default storage prefix used by MultiMC + static QString defaultStoragePrefix(); + + /// Get the prefix - root of the storage to be used + QString storagePrefix() const; + + /// Get the relative path where the library should be saved + QString storageSuffix(OpSys system) const; + + QString hint() const + { + return m_hint; + } + +protected: /* data */ + /// the basic gradle dependency specifier. + GradleSpecifier m_name; + + /// DEPRECATED URL prefix of the maven repo where the file can be downloaded + QString m_repositoryURL; + + /// DEPRECATED: MultiMC-specific absolute URL. takes precedence over the implicit maven repo URL, if defined + QString m_absoluteURL; + + /** + * MultiMC-specific type hint - modifies how the library is treated + */ + QString m_hint; + + /** + * storage - by default the local libraries folder in multimc, but could be elsewhere + * MultiMC specific, because of FTB. + */ + QString m_storagePrefix; + + /// true if the library had an extract/excludes section (even empty) + bool m_hasExcludes = false; + + /// a list of files that shouldn't be extracted from the library + QStringList m_extractExcludes; + + /// native suffixes per OS + QMap m_nativeClassifiers; + + /// true if the library had a rules section (even empty) + bool applyRules = false; + + /// rules associated with the library + QList> m_rules; + + /// MOJANG: container with Mojang style download info + MojangLibraryDownloadInfo::Ptr m_mojangDownloads; +}; diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp new file mode 100644 index 00000000..405ccd26 --- /dev/null +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -0,0 +1,369 @@ +#include "MinecraftInstance.h" +#include +#include "settings/SettingsObject.h" +#include "Env.h" +#include "minecraft/MinecraftVersionList.h" +#include +#include +#include +#include +#include + +#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 a, std::shared_ptr 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 m_a; + std::shared_ptr 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("JavaOrLocationOverride", javaOverride, locationOverride); + auto javaOrArgs = std::make_shared("JavaOrArgsOverride", javaOverride, argsOverride); + + m_settings->registerOverride(globalSettings->getSetting("JavaPath"), javaOrLocation); + m_settings->registerOverride(globalSettings->getSetting("JvmArgs"), javaOrArgs); + + // special! + m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"), javaOrLocation); + m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"), javaOrLocation); + + // Window Size + auto windowSetting = m_settings->registerSetting("OverrideWindow", false); + m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"), windowSetting); + m_settings->registerOverride(globalSettings->getSetting("MinecraftWinWidth"), windowSetting); + m_settings->registerOverride(globalSettings->getSetting("MinecraftWinHeight"), windowSetting); + + // Memory + auto memorySetting = m_settings->registerSetting("OverrideMemory", false); + m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"), memorySetting); + m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"), memorySetting); + m_settings->registerOverride(globalSettings->getSetting("PermGen"), memorySetting); +} + +QString MinecraftInstance::minecraftRoot() const +{ + QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); + + if (dotMCDir.exists() && !mcDir.exists()) + return dotMCDir.filePath(); + else + return mcDir.filePath(); +} + +std::shared_ptr< BaseVersionList > MinecraftInstance::versionList() const +{ + return ENV.getVersionList("net.minecraft"); +} + +QStringList MinecraftInstance::javaArguments() const +{ + QStringList args; + + // custom args go first. we want to override them if we have our own here. + args.append(extraArguments()); + + // OSX dock icon and name +#ifdef Q_OS_MAC + args << "-Xdock:icon=icon.png"; + args << QString("-Xdock:name=\"%1\"").arg(windowTitle()); +#endif + + // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767 +#ifdef Q_OS_WIN32 + args << QString("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_" + "minecraft.exe.heapdump"); +#endif + + args << QString("-Xms%1m").arg(settings()->get("MinMemAlloc").toInt()); + args << QString("-Xmx%1m").arg(settings()->get("MaxMemAlloc").toInt()); + + // No PermGen in newer java. + JavaVersion javaVersion(settings()->get("JavaVersion").toString()); + if(javaVersion.requiresPermGen()) + { + auto permgen = settings()->get("PermGen").toInt(); + if (permgen != 64) + { + args << QString("-XX:PermSize=%1m").arg(permgen); + } + } + + args << "-Duser.language=en"; + args << "-jar" << FS::PathCombine(QCoreApplication::applicationDirPath(), "jars", "NewLaunch.jar"); + + return args; +} + +QMap MinecraftInstance::getVariables() const +{ + QMap out; + out.insert("INST_NAME", name()); + out.insert("INST_ID", id()); + out.insert("INST_DIR", QDir(instanceRoot()).absolutePath()); + out.insert("INST_MC_DIR", QDir(minecraftRoot()).absolutePath()); + out.insert("INST_JAVA", settings()->get("JavaPath").toString()); + out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); + return out; +} + +static QString processLD_LIBRARY_PATH(const QString & LD_LIBRARY_PATH) +{ + QDir mmcBin(QCoreApplication::applicationDirPath()); + auto items = LD_LIBRARY_PATH.split(':'); + QStringList final; + for(auto & item: items) + { + QDir test(item); + if(test == mmcBin) + { + qDebug() << "Env:LD_LIBRARY_PATH ignoring path" << item; + continue; + } + final.append(item); + } + return final.join(':'); +} + +QProcessEnvironment MinecraftInstance::createEnvironment() +{ + // prepare the process environment + QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment(); + QProcessEnvironment env; + + QStringList ignored = + { + "JAVA_ARGS", + "CLASSPATH", + "CONFIGPATH", + "JAVA_HOME", + "JRE_HOME", + "_JAVA_OPTIONS", + "JAVA_OPTIONS", + "JAVA_TOOL_OPTIONS" + }; + for(auto key: rawenv.keys()) + { + auto value = rawenv.value(key); + // filter out dangerous java crap + if(ignored.contains(key)) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } + // filter MultiMC-related things + if(key.startsWith("QT_")) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } +#ifdef Q_OS_LINUX + // Do not pass LD_* variables to java. They were intended for MultiMC + if(key.startsWith("LD_")) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } + // Strip IBus + // IBus is a Linux IME framework. For some reason, it breaks MC? + if (key == "XMODIFIERS" && value.contains(IBUS)) + { + QString save = value; + value.replace(IBUS, ""); + qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value; + } + if(key == "GAME_PRELOAD") + { + env.insert("LD_PRELOAD", value); + continue; + } + if(key == "GAME_LIBRARY_PATH") + { + env.insert("LD_LIBRARY_PATH", processLD_LIBRARY_PATH(value)); + continue; + } +#endif + qDebug() << "Env: " << key << value; + env.insert(key, value); + } +#ifdef Q_OS_LINUX + // HACK: Workaround for QTBUG42500 + if(!env.contains("LD_LIBRARY_PATH")) + { + env.insert("LD_LIBRARY_PATH", ""); + } +#endif + + // export some infos + auto variables = getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + env.insert(it.key(), it.value()); + } + return env; +} + +QMap MinecraftInstance::createCensorFilterFromSession(AuthSessionPtr session) +{ + if(!session) + { + return QMap(); + } + auto & sessionRef = *session.get(); + QMap filter; + auto addToFilter = [&filter](QString key, QString value) + { + if(key.trimmed().size()) + { + filter[key] = value; + } + }; + if (sessionRef.session != "-") + { + addToFilter(sessionRef.session, tr("")); + } + addToFilter(sessionRef.access_token, tr("")); + addToFilter(sessionRef.client_token, tr("")); + addToFilter(sessionRef.uuid, tr("")); + addToFilter(sessionRef.player_name, tr("")); + + auto i = sessionRef.u.properties.begin(); + while (i != sessionRef.u.properties.end()) + { + addToFilter(i.value(), "<" + i.key().toUpper() + ">"); + ++i; + } + return filter; +} + +MessageLevel::Enum MinecraftInstance::guessLevel(const QString &line, MessageLevel::Enum level) +{ + QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + 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(); + combined->add(std::make_shared(".*\\.log(\\.[0-9]*)?(\\.gz)?$")); + combined->add(std::make_shared("crash-.*\\.txt")); + combined->add(std::make_shared("IDMap dump.*\\.txt$")); + combined->add(std::make_shared("ModLoader\\.txt(\\..*)?$")); + return combined; +} + +QString MinecraftInstance::getLogFileRoot() +{ + return minecraftRoot(); +} + +QString MinecraftInstance::prettifyTimeDuration(int64_t duration) +{ + int seconds = (int) (duration % 60); + duration /= 60; + int minutes = (int) (duration % 60); + duration /= 60; + int hours = (int) (duration % 24); + int days = (int) (duration / 24); + if((hours == 0)&&(days == 0)) + { + return tr("%1m %2s").arg(minutes).arg(seconds); + } + if (days == 0) + { + return tr("%1h %2m").arg(hours).arg(minutes); + } + return tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes); +} + +QString MinecraftInstance::getStatusbarDescription() +{ + QStringList traits; + if (flags() & VersionBrokenFlag) + { + traits.append(tr("broken")); + } + + QString description; + description.append(tr("Minecraft %1 (%2)").arg(intendedVersionId()).arg(typeName())); + if(totalTimePlayed() > 0) + { + description.append(tr(", played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); + } + /* + if(traits.size()) + { + description.append(QString(" (%1)").arg(traits.join(", "))); + } + */ + return description; +} + +#include "MinecraftInstance.moc" diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h new file mode 100644 index 00000000..cd3a8d90 --- /dev/null +++ b/api/logic/minecraft/MinecraftInstance.h @@ -0,0 +1,69 @@ +#pragma once +#include "BaseInstance.h" +#include "minecraft/Mod.h" +#include + +#include "multimc_logic_export.h" + +class ModList; +class WorldList; + +class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance +{ +public: + MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + virtual ~MinecraftInstance() {}; + + /// Path to the instance's minecraft directory. + QString minecraftRoot() const; + + ////// Mod Lists ////// + virtual std::shared_ptr resourcePackList() const + { + return nullptr; + } + virtual std::shared_ptr texturePackList() const + { + return nullptr; + } + virtual std::shared_ptr worldList() const + { + return nullptr; + } + /// get all jar mods applicable to this instance's jar + virtual QList getJarMods() const + { + return QList(); + } + + /// get the launch script to be used with this + virtual QString createLaunchScript(AuthSessionPtr session) = 0; + + //FIXME: nuke? + virtual std::shared_ptr versionList() const override; + + /// get arguments passed to java + QStringList javaArguments() const; + + /// get variables for launch command variable substitution/environment + virtual QMap getVariables() const override; + + /// create an environment for launching processes + virtual QProcessEnvironment createEnvironment() override; + + /// guess log level from a line of minecraft log + virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) override; + + virtual IPathMatcher::Ptr getLogFileMatcher() override; + + virtual QString getLogFileRoot() override; + + virtual QString getStatusbarDescription() override; + +protected: + QMap createCensorFilterFromSession(AuthSessionPtr session); +private: + QString prettifyTimeDuration(int64_t duration); +}; + +typedef std::shared_ptr MinecraftInstancePtr; diff --git a/api/logic/minecraft/MinecraftProfile.cpp b/api/logic/minecraft/MinecraftProfile.cpp new file mode 100644 index 00000000..70d0cee4 --- /dev/null +++ b/api/logic/minecraft/MinecraftProfile.cpp @@ -0,0 +1,610 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "minecraft/MinecraftProfile.h" +#include "ProfileUtils.h" +#include "ProfileStrategy.h" +#include "Exception.h" + +MinecraftProfile::MinecraftProfile(ProfileStrategy *strategy) + : QAbstractListModel() +{ + setStrategy(strategy); + clear(); +} + +void MinecraftProfile::setStrategy(ProfileStrategy* strategy) +{ + Q_ASSERT(strategy != nullptr); + + if(m_strategy != nullptr) + { + delete m_strategy; + m_strategy = nullptr; + } + m_strategy = strategy; + m_strategy->profile = this; +} + +ProfileStrategy* MinecraftProfile::strategy() +{ + return m_strategy; +} + +void MinecraftProfile::reload() +{ + beginResetModel(); + m_strategy->load(); + reapplyPatches(); + endResetModel(); +} + +void MinecraftProfile::clear() +{ + m_minecraftVersion.clear(); + m_minecraftVersionType.clear(); + m_minecraftAssets.reset(); + m_minecraftArguments.clear(); + m_tweakers.clear(); + m_mainClass.clear(); + m_appletClass.clear(); + m_libraries.clear(); + m_traits.clear(); + m_jarMods.clear(); + mojangDownloads.clear(); + m_problemSeverity = ProblemSeverity::PROBLEM_NONE; +} + +void MinecraftProfile::clearPatches() +{ + beginResetModel(); + m_patches.clear(); + endResetModel(); +} + +void MinecraftProfile::appendPatch(ProfilePatchPtr patch) +{ + int index = m_patches.size(); + beginInsertRows(QModelIndex(), index, index); + m_patches.append(patch); + endInsertRows(); +} + +bool MinecraftProfile::remove(const int index) +{ + auto patch = versionPatch(index); + if (!patch->isRemovable()) + { + qDebug() << "Patch" << patch->getID() << "is non-removable"; + return false; + } + + if(!m_strategy->removePatch(patch)) + { + qCritical() << "Patch" << patch->getID() << "could not be removed"; + return false; + } + + beginRemoveRows(QModelIndex(), index, index); + m_patches.removeAt(index); + endRemoveRows(); + reapplyPatches(); + saveCurrentOrder(); + return true; +} + +bool MinecraftProfile::remove(const QString id) +{ + int i = 0; + for (auto patch : m_patches) + { + if (patch->getID() == id) + { + return remove(i); + } + i++; + } + return false; +} + +bool MinecraftProfile::customize(int index) +{ + auto patch = versionPatch(index); + if (!patch->isCustomizable()) + { + qDebug() << "Patch" << patch->getID() << "is not customizable"; + return false; + } + if(!m_strategy->customizePatch(patch)) + { + qCritical() << "Patch" << patch->getID() << "could not be customized"; + return false; + } + reapplyPatches(); + saveCurrentOrder(); + // FIXME: maybe later in unstable + // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + return true; +} + +bool MinecraftProfile::revertToBase(int index) +{ + auto patch = versionPatch(index); + if (!patch->isRevertible()) + { + qDebug() << "Patch" << patch->getID() << "is not revertible"; + return false; + } + if(!m_strategy->revertPatch(patch)) + { + qCritical() << "Patch" << patch->getID() << "could not be reverted"; + return false; + } + reapplyPatches(); + saveCurrentOrder(); + // FIXME: maybe later in unstable + // emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + return true; +} + +ProfilePatchPtr MinecraftProfile::versionPatch(const QString &id) +{ + for (auto file : m_patches) + { + if (file->getID() == id) + { + return file; + } + } + return nullptr; +} + +ProfilePatchPtr MinecraftProfile::versionPatch(int index) +{ + if(index < 0 || index >= m_patches.size()) + return nullptr; + return m_patches[index]; +} + +bool MinecraftProfile::isVanilla() +{ + for(auto patchptr: m_patches) + { + if(patchptr->isCustom()) + return false; + } + return true; +} + +bool MinecraftProfile::revertToVanilla() +{ + // remove patches, if present + auto VersionPatchesCopy = m_patches; + for(auto & it: VersionPatchesCopy) + { + if (!it->isCustom()) + { + continue; + } + if(it->isRevertible() || it->isRemovable()) + { + if(!remove(it->getID())) + { + qWarning() << "Couldn't remove" << it->getID() << "from profile!"; + reapplyPatches(); + saveCurrentOrder(); + return false; + } + } + } + reapplyPatches(); + saveCurrentOrder(); + return true; +} + +QVariant MinecraftProfile::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= m_patches.size()) + return QVariant(); + + auto patch = m_patches.at(row); + + if (role == Qt::DisplayRole) + { + switch (column) + { + case 0: + return m_patches.at(row)->getName(); + case 1: + { + if(patch->isCustom()) + { + return QString("%1 (Custom)").arg(patch->getVersion()); + } + else + { + return patch->getVersion(); + } + } + default: + return QVariant(); + } + } + if(role == Qt::DecorationRole) + { + switch(column) + { + case 0: + { + auto severity = patch->getProblemSeverity(); + switch (severity) + { + case PROBLEM_WARNING: + return "warning"; + case PROBLEM_ERROR: + return "error"; + default: + return QVariant(); + } + } + default: + { + return QVariant(); + } + } + } + return QVariant(); +} +QVariant MinecraftProfile::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) + { + if (role == Qt::DisplayRole) + { + switch (section) + { + case 0: + return tr("Name"); + case 1: + return tr("Version"); + default: + return QVariant(); + } + } + } + return QVariant(); +} +Qt::ItemFlags MinecraftProfile::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +int MinecraftProfile::rowCount(const QModelIndex &parent) const +{ + return m_patches.size(); +} + +int MinecraftProfile::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +void MinecraftProfile::saveCurrentOrder() const +{ + ProfileUtils::PatchOrder order; + for(auto item: m_patches) + { + if(!item->isMoveable()) + continue; + order.append(item->getID()); + } + m_strategy->saveOrder(order); +} + +void MinecraftProfile::move(const int index, const MoveDirection direction) +{ + int theirIndex; + if (direction == MoveUp) + { + theirIndex = index - 1; + } + else + { + theirIndex = index + 1; + } + + if (index < 0 || index >= m_patches.size()) + return; + if (theirIndex >= rowCount()) + theirIndex = rowCount() - 1; + if (theirIndex == -1) + theirIndex = rowCount() - 1; + if (index == theirIndex) + return; + int togap = theirIndex > index ? theirIndex + 1 : theirIndex; + + auto from = versionPatch(index); + auto to = versionPatch(theirIndex); + + if (!from || !to || !to->isMoveable() || !from->isMoveable()) + { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); + m_patches.swap(index, theirIndex); + endMoveRows(); + reapplyPatches(); + saveCurrentOrder(); +} +void MinecraftProfile::resetOrder() +{ + m_strategy->resetOrder(); + reload(); +} + +bool MinecraftProfile::reapplyPatches() +{ + try + { + clear(); + for(auto file: m_patches) + { + file->applyTo(this); + } + } + catch (Exception & error) + { + clear(); + qWarning() << "Couldn't apply profile patches because: " << error.cause(); + return false; + } + return true; +} + +static void applyString(const QString & from, QString & to) +{ + if(from.isEmpty()) + return; + to = from; +} + +void MinecraftProfile::applyMinecraftVersion(const QString& id) +{ + applyString(id, this->m_minecraftVersion); +} + +void MinecraftProfile::applyAppletClass(const QString& appletClass) +{ + applyString(appletClass, this->m_appletClass); +} + +void MinecraftProfile::applyMainClass(const QString& mainClass) +{ + applyString(mainClass, this->m_mainClass); +} + +void MinecraftProfile::applyMinecraftArguments(const QString& minecraftArguments) +{ + applyString(minecraftArguments, this->m_minecraftArguments); +} + +void MinecraftProfile::applyMinecraftVersionType(const QString& type) +{ + applyString(type, this->m_minecraftVersionType); +} + +void MinecraftProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets) +{ + if(assets) + { + m_minecraftAssets = assets; + } +} + +void MinecraftProfile::applyMojangDownload(const QString &key, MojangDownloadInfo::Ptr download) +{ + if(download) + { + mojangDownloads[key] = download; + } + else + { + mojangDownloads.remove(key); + } +} + +void MinecraftProfile::applyTraits(const QSet& traits) +{ + this->m_traits.unite(traits); +} + +void MinecraftProfile::applyTweakers(const QStringList& tweakers) +{ + // FIXME: check for dupes? + // FIXME: does order matter? + for (auto tweaker : tweakers) + { + this->m_tweakers += tweaker; + } +} + +void MinecraftProfile::applyJarMods(const QList& jarMods) +{ + this->m_jarMods.append(jarMods); +} + +static int findLibraryByName(QList haystack, const GradleSpecifier &needle) +{ + int retval = -1; + for (int i = 0; i < haystack.size(); ++i) + { + if (haystack.at(i)->rawName().matchName(needle)) + { + // only one is allowed. + if (retval != -1) + return -1; + retval = i; + } + } + return retval; +} + +void MinecraftProfile::applyLibrary(LibraryPtr library) +{ + if(!library->isActive()) + { + return; + } + // find the library by name. + const int index = findLibraryByName(m_libraries, library->rawName()); + // library not found? just add it. + if (index < 0) + { + m_libraries.append(Library::limitedCopy(library)); + return; + } + auto existingLibrary = m_libraries.at(index); + // if we are higher it means we should update + if (Version(library->version()) > Version(existingLibrary->version())) + { + auto libraryCopy = Library::limitedCopy(library); + m_libraries.replace(index, libraryCopy); + } +} + +void MinecraftProfile::applyProblemSeverity(ProblemSeverity severity) +{ + if (m_problemSeverity < severity) + { + m_problemSeverity = severity; + } +} + + +QString MinecraftProfile::getMinecraftVersion() const +{ + return m_minecraftVersion; +} + +QString MinecraftProfile::getAppletClass() const +{ + return m_appletClass; +} + +QString MinecraftProfile::getMainClass() const +{ + return m_mainClass; +} + +const QSet &MinecraftProfile::getTraits() const +{ + return m_traits; +} + +const QStringList & MinecraftProfile::getTweakers() const +{ + return m_tweakers; +} + +bool MinecraftProfile::hasTrait(const QString& trait) const +{ + return m_traits.contains(trait); +} + +ProblemSeverity MinecraftProfile::getProblemSeverity() const +{ + return m_problemSeverity; +} + +QString MinecraftProfile::getMinecraftVersionType() const +{ + return m_minecraftVersionType; +} + +std::shared_ptr MinecraftProfile::getMinecraftAssets() const +{ + if(!m_minecraftAssets) + { + return std::make_shared("legacy"); + } + return m_minecraftAssets; +} + +QString MinecraftProfile::getMinecraftArguments() const +{ + return m_minecraftArguments; +} + +const QList & MinecraftProfile::getJarMods() const +{ + return m_jarMods; +} + +const QList & MinecraftProfile::getLibraries() const +{ + return m_libraries; +} + +QString MinecraftProfile::getMainJarUrl() const +{ + auto iter = mojangDownloads.find("client"); + if(iter != mojangDownloads.end()) + { + // current + return iter.value()->url; + } + else + { + // legacy fallback + return URLConstants::getLegacyJarUrl(getMinecraftVersion()); + } +} + +void MinecraftProfile::installJarMods(QStringList selectedFiles) +{ + m_strategy->installJarMods(selectedFiles); +} + +/* + * TODO: get rid of this. Get rid of all order numbers. + */ +int MinecraftProfile::getFreeOrderNumber() +{ + int largest = 100; + // yes, I do realize this is dumb. The order thing itself is dumb. and to be removed next. + for(auto thing: m_patches) + { + int order = thing->getOrder(); + if(order > largest) + largest = order; + } + return largest + 1; +} diff --git a/api/logic/minecraft/MinecraftProfile.h b/api/logic/minecraft/MinecraftProfile.h new file mode 100644 index 00000000..ca9288ad --- /dev/null +++ b/api/logic/minecraft/MinecraftProfile.h @@ -0,0 +1,200 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +#include "Library.h" +#include "VersionFile.h" +#include "JarMod.h" +#include "MojangDownloadInfo.h" + +#include "multimc_logic_export.h" + +class ProfileStrategy; +class OneSixInstance; + + +class MULTIMC_LOGIC_EXPORT MinecraftProfile : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit MinecraftProfile(ProfileStrategy *strategy); + + void setStrategy(ProfileStrategy * strategy); + ProfileStrategy *strategy(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex &parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + + /// is this version unchanged by the user? + bool isVanilla(); + + /// remove any customizations on top of whatever 'vanilla' means + bool revertToVanilla(); + + /// install more jar mods + void installJarMods(QStringList selectedFiles); + + /// DEPRECATED, remove ASAP + int getFreeOrderNumber(); + + enum MoveDirection { MoveUp, MoveDown }; + /// move patch file # up or down the list + void move(const int index, const MoveDirection direction); + + /// remove patch file # - including files/records + bool remove(const int index); + + /// remove patch file by id - including files/records + bool remove(const QString id); + + bool customize(int index); + + bool revertToBase(int index); + + void resetOrder(); + + /// reload all profile patches from storage, clear the profile and apply the patches + void reload(); + + /// clear the profile + void clear(); + + /// apply the patches. Catches all the errors and returns true/false for success/failure + bool reapplyPatches(); + +public: /* application of profile variables from patches */ + void applyMinecraftVersion(const QString& id); + void applyMainClass(const QString& mainClass); + void applyAppletClass(const QString& appletClass); + void applyMinecraftArguments(const QString& minecraftArguments); + void applyMinecraftVersionType(const QString& type); + void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets); + void applyTraits(const QSet &traits); + void applyTweakers(const QStringList &tweakers); + void applyJarMods(const QList &jarMods); + void applyLibrary(LibraryPtr library); + void applyProblemSeverity(ProblemSeverity severity); + void applyMojangDownload(const QString & key, MojangDownloadInfo::Ptr download); + +public: /* getters for profile variables */ + QString getMinecraftVersion() const; + QString getMainClass() const; + QString getAppletClass() const; + QString getMinecraftVersionType() const; + MojangAssetIndexInfo::Ptr getMinecraftAssets() const; + QString getMinecraftArguments() const; + const QSet & getTraits() const; + const QStringList & getTweakers() const; + const QList & getJarMods() const; + const QList & getLibraries() const; + QString getMainJarUrl() const; + bool hasTrait(const QString & trait) const; + ProblemSeverity getProblemSeverity() const; + +public: + /// get the profile patch by id + ProfilePatchPtr versionPatch(const QString &id); + + /// get the profile patch by index + ProfilePatchPtr versionPatch(int index); + + /// save the current patch order + void saveCurrentOrder() const; + + /// Remove all the patches + void clearPatches(); + + /// Add the patch object to the internal list of patches + void appendPatch(ProfilePatchPtr patch); + +private: /* data */ + /// the version of Minecraft - jar to use + QString m_minecraftVersion; + + /// Release type - "release" or "snapshot" + QString m_minecraftVersionType; + + /// Assets type - "legacy" or a version ID + MojangAssetIndexInfo::Ptr m_minecraftAssets; + + // Mojang: list of 'downloads' - client jar, server jar, windows server exe, maybe more. + QMap > mojangDownloads; + + /** + * arguments that should be used for launching minecraft + * + * ex: "--username ${auth_player_name} --session ${auth_session} + * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}" + */ + QString m_minecraftArguments; + + /// A list of all tweaker classes + QStringList m_tweakers; + + /// The main class to load first + QString m_mainClass; + + /// The applet class, for some very old minecraft releases + QString m_appletClass; + + /// the list of libraries + QList m_libraries; + + /// traits, collected from all the version files (version files can only add) + QSet m_traits; + + /// A list of jar mods. version files can add those. + QList m_jarMods; + + ProblemSeverity m_problemSeverity = PROBLEM_NONE; + + /* + FIXME: add support for those rules here? Looks like a pile of quick hacks to me though. + + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx", + "version": "^10\\.5\\.\\d$" + } + } + ], + "incompatibilityReason": "There is a bug in LWJGL which makes it incompatible with OSX + 10.5.8. Please go to New Profile and use 1.5.2 for now. Sorry!" + } + */ + // QList rules; + + /// list of attached profile patches + QList m_patches; + + /// strategy used for profile operations + ProfileStrategy *m_strategy = nullptr; +}; diff --git a/api/logic/minecraft/MinecraftVersion.cpp b/api/logic/minecraft/MinecraftVersion.cpp new file mode 100644 index 00000000..1e1d273c --- /dev/null +++ b/api/logic/minecraft/MinecraftVersion.cpp @@ -0,0 +1,215 @@ +#include "MinecraftVersion.h" +#include "MinecraftProfile.h" +#include "VersionBuildError.h" +#include "ProfileUtils.h" +#include "settings/SettingsObject.h" +#include "minecraft/VersionFilterData.h" + +bool MinecraftVersion::usesLegacyLauncher() +{ + return getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; +} + + +QString MinecraftVersion::descriptor() +{ + return m_version; +} + +QString MinecraftVersion::name() +{ + return m_version; +} + +QString MinecraftVersion::typeString() const +{ + if(m_type == "snapshot") + { + return QObject::tr("Snapshot"); + } + else if (m_type == "release") + { + return QObject::tr("Regular release"); + } + else if (m_type == "old_alpha") + { + return QObject::tr("Alpha"); + } + else if (m_type == "old_beta") + { + return QObject::tr("Beta"); + } + else + { + return QString(); + } +} + +VersionSource MinecraftVersion::getVersionSource() +{ + return m_versionSource; +} + +bool MinecraftVersion::hasJarMods() +{ + return false; +} + +bool MinecraftVersion::isMinecraftVersion() +{ + return true; +} + +void MinecraftVersion::applyFileTo(MinecraftProfile *profile) +{ + if(m_versionSource == Local && getVersionFile()) + { + getVersionFile()->applyTo(profile); + } + else + { + throw VersionIncomplete(QObject::tr("Can't apply incomplete/builtin Minecraft version %1").arg(m_version)); + } +} + +QString MinecraftVersion::getUrl() const +{ + // legacy fallback + if(m_versionFileURL.isEmpty()) + { + return QString("http://") + URLConstants::AWS_DOWNLOAD_VERSIONS + m_version + "/" + m_version + ".json"; + } + // current + return m_versionFileURL; +} + +VersionFilePtr MinecraftVersion::getVersionFile() +{ + QFileInfo versionFile(QString("versions/%1/%1.dat").arg(m_version)); + m_problems.clear(); + if(!versionFile.exists()) + { + if(m_loadedVersionFile) + { + m_loadedVersionFile.reset(); + } + addProblem(PROBLEM_WARNING, QObject::tr("The patch file doesn't exist locally. It's possible it just needs to be downloaded.")); + } + else + { + try + { + if(versionFile.lastModified() != m_loadedVersionFileTimestamp) + { + auto loadedVersionFile = ProfileUtils::parseBinaryJsonFile(versionFile); + loadedVersionFile->name = "Minecraft"; + loadedVersionFile->setCustomizable(true); + m_loadedVersionFileTimestamp = versionFile.lastModified(); + m_loadedVersionFile = loadedVersionFile; + } + } + catch(Exception e) + { + m_loadedVersionFile.reset(); + addProblem(PROBLEM_ERROR, QObject::tr("The patch file couldn't be read:\n%1").arg(e.cause())); + } + } + return m_loadedVersionFile; +} + +bool MinecraftVersion::isCustomizable() +{ + switch(m_versionSource) + { + case Local: + case Remote: + // locally cached file, or a remote file that we can acquire can be customized + return true; + default: + // Everything else is undefined and therefore not customizable. + return false; + } + return false; +} + +const QList &MinecraftVersion::getProblems() +{ + if(getVersionFile()) + { + return getVersionFile()->getProblems(); + } + return ProfilePatch::getProblems(); +} + +ProblemSeverity MinecraftVersion::getProblemSeverity() +{ + if(getVersionFile()) + { + return getVersionFile()->getProblemSeverity(); + } + return ProfilePatch::getProblemSeverity(); +} + +void MinecraftVersion::applyTo(MinecraftProfile *profile) +{ + // do we have this one cached? + if (m_versionSource == Local) + { + applyFileTo(profile); + return; + } + throw VersionIncomplete(QObject::tr("Minecraft version %1 could not be applied: version files are missing.").arg(m_version)); +} + +int MinecraftVersion::getOrder() +{ + return order; +} + +void MinecraftVersion::setOrder(int order) +{ + this->order = order; +} + +QList MinecraftVersion::getJarMods() +{ + return QList(); +} + +QString MinecraftVersion::getName() +{ + return "Minecraft"; +} +QString MinecraftVersion::getVersion() +{ + return m_version; +} +QString MinecraftVersion::getID() +{ + return "net.minecraft"; +} +QString MinecraftVersion::getFilename() +{ + return QString(); +} +QDateTime MinecraftVersion::getReleaseDateTime() +{ + return m_releaseTime; +} + + +bool MinecraftVersion::needsUpdate() +{ + return m_versionSource == Remote || hasUpdate(); +} + +bool MinecraftVersion::hasUpdate() +{ + return m_versionSource == Remote || (m_versionSource == Local && upstreamUpdate); +} + +bool MinecraftVersion::isCustom() +{ + // if we add any other source types, this will evaluate to false for them. + return m_versionSource != Local && m_versionSource != Remote; +} diff --git a/api/logic/minecraft/MinecraftVersion.h b/api/logic/minecraft/MinecraftVersion.h new file mode 100644 index 00000000..b21427d9 --- /dev/null +++ b/api/logic/minecraft/MinecraftVersion.h @@ -0,0 +1,119 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "BaseVersion.h" +#include "ProfilePatch.h" +#include "VersionFile.h" + +#include "multimc_logic_export.h" + +class MinecraftProfile; +class MinecraftVersion; +typedef std::shared_ptr MinecraftVersionPtr; + +class MULTIMC_LOGIC_EXPORT MinecraftVersion : public BaseVersion, public ProfilePatch +{ +friend class MinecraftVersionList; + +public: /* methods */ + // FIXME: nuke this. + bool usesLegacyLauncher(); + + virtual QString descriptor() override; + virtual QString name() override; + virtual QString typeString() const override; + virtual bool hasJarMods() override; + virtual bool isMinecraftVersion() override; + virtual void applyTo(MinecraftProfile *profile) override; + virtual int getOrder() override; + virtual void setOrder(int order) override; + virtual QList getJarMods() override; + virtual QString getID() override; + virtual QString getVersion() override; + virtual QString getName() override; + virtual QString getFilename() override; + QDateTime getReleaseDateTime() override; + VersionSource getVersionSource() override; + + bool needsUpdate(); + bool hasUpdate(); + virtual bool isCustom() override; + virtual bool isMoveable() override + { + return false; + } + virtual bool isCustomizable() override; + virtual bool isRemovable() override + { + return false; + } + virtual bool isRevertible() override + { + return false; + } + virtual bool isEditable() override + { + return false; + } + virtual bool isVersionChangeable() override + { + return true; + } + + virtual VersionFilePtr getVersionFile() override; + + // virtual QJsonDocument toJson(bool saveOrder) override; + + QString getUrl() const; + + virtual const QList &getProblems() override; + virtual ProblemSeverity getProblemSeverity() override; + +private: /* methods */ + void applyFileTo(MinecraftProfile *profile); + +protected: /* data */ + VersionSource m_versionSource = Remote; + + /// The URL that this version will be downloaded from. + QString m_versionFileURL; + + /// the human readable version name + QString m_version; + + /// The type of this release + QString m_type; + + /// the time this version was actually released by Mojang + QDateTime m_releaseTime; + + /// the time this version was last updated by Mojang + QDateTime m_updateTime; + + /// order of this file... default = -2 + int order = -2; + + /// an update available from Mojang + MinecraftVersionPtr upstreamUpdate; + + QDateTime m_loadedVersionFileTimestamp; + mutable VersionFilePtr m_loadedVersionFile; +}; diff --git a/api/logic/minecraft/MinecraftVersionList.cpp b/api/logic/minecraft/MinecraftVersionList.cpp new file mode 100644 index 00000000..a5cc3a39 --- /dev/null +++ b/api/logic/minecraft/MinecraftVersionList.cpp @@ -0,0 +1,591 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "Json.h" +#include +#include + +#include "Env.h" +#include "Exception.h" + +#include "MinecraftVersionList.h" +#include "net/URLConstants.h" + +#include "ParseUtils.h" +#include "ProfileUtils.h" +#include "VersionFilterData.h" +#include "onesix/OneSixVersionFormat.h" +#include "MojangVersionFormat.h" +#include + +static const char * localVersionCache = "versions/versions.dat"; + +class MCVListLoadTask : public Task +{ + Q_OBJECT + +public: + explicit MCVListLoadTask(MinecraftVersionList *vlist); + virtual ~MCVListLoadTask() override{}; + + virtual void executeTask() override; + +protected +slots: + void list_downloaded(); + +protected: + QNetworkReply *vlistReply; + MinecraftVersionList *m_list; + MinecraftVersion *m_currentStable; +}; + +class MCVListVersionUpdateTask : public Task +{ + Q_OBJECT + +public: + explicit MCVListVersionUpdateTask(MinecraftVersionList *vlist, std::shared_ptr updatedVersion); + virtual ~MCVListVersionUpdateTask() override{}; + virtual void executeTask() override; + +protected +slots: + void json_downloaded(); + +protected: + NetJobPtr specificVersionDownloadJob; + std::shared_ptr updatedVersion; + MinecraftVersionList *m_list; +}; + +class ListLoadError : public Exception +{ +public: + ListLoadError(QString cause) : Exception(cause) {}; + virtual ~ListLoadError() noexcept + { + } +}; + +MinecraftVersionList::MinecraftVersionList(QObject *parent) : BaseVersionList(parent) +{ + loadCachedList(); +} + +Task *MinecraftVersionList::getLoadTask() +{ + return new MCVListLoadTask(this); +} + +bool MinecraftVersionList::isLoaded() +{ + return m_loaded; +} + +const BaseVersionPtr MinecraftVersionList::at(int i) const +{ + return m_vlist.at(i); +} + +int MinecraftVersionList::count() const +{ + return m_vlist.count(); +} + +static bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second) +{ + auto left = std::dynamic_pointer_cast(first); + auto right = std::dynamic_pointer_cast(second); + return left->getReleaseDateTime() > right->getReleaseDateTime(); +} + +void MinecraftVersionList::sortInternal() +{ + qSort(m_vlist.begin(), m_vlist.end(), cmpVersions); +} + +void MinecraftVersionList::loadCachedList() +{ + QFile localIndex(localVersionCache); + if (!localIndex.exists()) + { + return; + } + if (!localIndex.open(QIODevice::ReadOnly)) + { + // FIXME: this is actually a very bad thing! How do we deal with this? + qCritical() << "The minecraft version cache can't be read."; + return; + } + auto data = localIndex.readAll(); + try + { + localIndex.close(); + QJsonDocument jsonDoc = QJsonDocument::fromBinaryData(data); + if (jsonDoc.isNull()) + { + throw ListLoadError(tr("Error reading the version list.")); + } + loadList(jsonDoc, Local); + } + catch (Exception &e) + { + // the cache has gone bad for some reason... flush it. + qCritical() << "The minecraft version cache is corrupted. Flushing cache."; + localIndex.remove(); + return; + } + m_hasLocalIndex = true; +} + +void MinecraftVersionList::loadList(QJsonDocument jsonDoc, VersionSource source) +{ + qDebug() << "Loading" << ((source == Remote) ? "remote" : "local") << "version list."; + + if (!jsonDoc.isObject()) + { + throw ListLoadError(tr("Error parsing version list JSON: jsonDoc is not an object")); + } + + QJsonObject root = jsonDoc.object(); + + try + { + QJsonObject latest = Json::requireObject(root.value("latest")); + m_latestReleaseID = Json::requireString(latest.value("release")); + m_latestSnapshotID = Json::requireString(latest.value("snapshot")); + } + catch (Exception &err) + { + qCritical() + << tr("Error parsing version list JSON: couldn't determine latest versions"); + } + + // Now, get the array of versions. + if (!root.value("versions").isArray()) + { + throw ListLoadError(tr("Error parsing version list JSON: version list object is " + "missing 'versions' array")); + } + QJsonArray versions = root.value("versions").toArray(); + + QList tempList; + for (auto version : versions) + { + // Load the version info. + if (!version.isObject()) + { + qCritical() << "Error while parsing version list : invalid JSON structure"; + continue; + } + + QJsonObject versionObj = version.toObject(); + QString versionID = versionObj.value("id").toString(""); + if (versionID.isEmpty()) + { + qCritical() << "Error while parsing version : version ID is missing"; + continue; + } + + if (g_VersionFilterData.legacyBlacklist.contains(versionID)) + { + qWarning() << "Blacklisted legacy version ignored: " << versionID; + continue; + } + + // Now, we construct the version object and add it to the list. + std::shared_ptr mcVersion(new MinecraftVersion()); + mcVersion->m_version = versionID; + + mcVersion->m_releaseTime = timeFromS3Time(versionObj.value("releaseTime").toString("")); + mcVersion->m_updateTime = timeFromS3Time(versionObj.value("time").toString("")); + + // depends on where we load the version from -- network request or local file? + mcVersion->m_versionSource = source; + mcVersion->m_versionFileURL = versionObj.value("url").toString(""); + QString versionTypeStr = versionObj.value("type").toString(""); + if (versionTypeStr.isEmpty()) + { + qCritical() << "Ignoring" << versionID + << "because it doesn't have the version type set."; + continue; + } + // OneSix or Legacy. use filter to determine type + if (versionTypeStr == "release") + { + } + else if (versionTypeStr == "snapshot") // It's a snapshot... yay + { + } + else if (versionTypeStr == "old_alpha") + { + } + else if (versionTypeStr == "old_beta") + { + } + else + { + qCritical() << "Ignoring" << versionID + << "because it has an invalid version type."; + continue; + } + mcVersion->m_type = versionTypeStr; + qDebug() << "Loaded version" << versionID << "from" + << ((source == Remote) ? "remote" : "local") << "version list."; + tempList.append(mcVersion); + } + updateListData(tempList); + if(source == Remote) + { + m_loaded = true; + } +} + +void MinecraftVersionList::sortVersions() +{ + beginResetModel(); + sortInternal(); + endResetModel(); +} + +QVariant MinecraftVersionList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = std::dynamic_pointer_cast(m_vlist[index.row()]); + switch (role) + { + case VersionPointerRole: + return qVariantFromValue(m_vlist[index.row()]); + + case VersionRole: + return version->name(); + + case VersionIdRole: + return version->descriptor(); + + case RecommendedRole: + return version->descriptor() == m_latestReleaseID; + + case LatestRole: + { + if(version->descriptor() != m_latestSnapshotID) + return false; + MinecraftVersionPtr latestRelease = std::dynamic_pointer_cast(getLatestStable()); + /* + if(latestRelease && latestRelease->m_releaseTime > version->m_releaseTime) + { + return false; + } + */ + return true; + } + + case TypeRole: + return version->typeString(); + + default: + return QVariant(); + } +} + +BaseVersionList::RoleList MinecraftVersionList::providesRoles() const +{ + return {VersionPointerRole, VersionRole, VersionIdRole, RecommendedRole, LatestRole, TypeRole}; +} + +BaseVersionPtr MinecraftVersionList::getLatestStable() const +{ + if(m_lookup.contains(m_latestReleaseID)) + return m_lookup[m_latestReleaseID]; + return BaseVersionPtr(); +} + +BaseVersionPtr MinecraftVersionList::getRecommended() const +{ + return getLatestStable(); +} + +void MinecraftVersionList::updateListData(QList versions) +{ + beginResetModel(); + for (auto version : versions) + { + auto descr = version->descriptor(); + + if (!m_lookup.contains(descr)) + { + m_lookup[version->descriptor()] = version; + m_vlist.append(version); + continue; + } + auto orig = std::dynamic_pointer_cast(m_lookup[descr]); + auto added = std::dynamic_pointer_cast(version); + // updateListData is called after Mojang list loads. those can be local or remote + // remote comes always after local + // any other options are ignored + if (orig->m_versionSource != Local || added->m_versionSource != Remote) + { + continue; + } + // alright, it's an update. put it inside the original, for further processing. + orig->upstreamUpdate = added; + } + sortInternal(); + endResetModel(); +} + +MCVListLoadTask::MCVListLoadTask(MinecraftVersionList *vlist) +{ + m_list = vlist; + m_currentStable = NULL; + vlistReply = nullptr; +} + +void MCVListLoadTask::executeTask() +{ + setStatus(tr("Loading instance version list...")); + auto worker = ENV.qnam(); + vlistReply = worker->get(QNetworkRequest(QUrl("https://launchermeta.mojang.com/mc/game/version_manifest.json"))); + connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded())); +} + +void MCVListLoadTask::list_downloaded() +{ + if (vlistReply->error() != QNetworkReply::NoError) + { + vlistReply->deleteLater(); + emitFailed("Failed to load Minecraft main version list" + vlistReply->errorString()); + return; + } + + auto data = vlistReply->readAll(); + vlistReply->deleteLater(); + try + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + throw ListLoadError( + tr("Error parsing version list JSON: %1").arg(jsonError.errorString())); + } + m_list->loadList(jsonDoc, Remote); + } + catch (Exception &e) + { + emitFailed(e.cause()); + return; + } + + emitSucceeded(); + return; +} + +MCVListVersionUpdateTask::MCVListVersionUpdateTask(MinecraftVersionList *vlist, std::shared_ptr updatedVersion) + : Task() +{ + m_list = vlist; + this->updatedVersion = updatedVersion; +} + +void MCVListVersionUpdateTask::executeTask() +{ + auto job = new NetJob("Version index"); + job->addNetAction(ByteArrayDownload::make(QUrl(updatedVersion->getUrl()))); + specificVersionDownloadJob.reset(job); + connect(specificVersionDownloadJob.get(), SIGNAL(succeeded()), SLOT(json_downloaded())); + connect(specificVersionDownloadJob.get(), SIGNAL(failed(QString)), SIGNAL(failed(QString))); + connect(specificVersionDownloadJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + specificVersionDownloadJob->start(); +} + +void MCVListVersionUpdateTask::json_downloaded() +{ + NetActionPtr DlJob = specificVersionDownloadJob->first(); + auto data = std::dynamic_pointer_cast(DlJob)->m_data; + specificVersionDownloadJob.reset(); + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed(tr("The download version file is not valid.")); + return; + } + VersionFilePtr file; + try + { + file = MojangVersionFormat::versionFileFromJson(jsonDoc, "net.minecraft.json"); + } + catch (Exception &e) + { + emitFailed(tr("Couldn't process version file: %1").arg(e.cause())); + return; + } + + // Strip LWJGL from the version file. We use our own. + ProfileUtils::removeLwjglFromPatch(file); + + file->fileId = "net.minecraft"; + + // now dump the file to disk + auto doc = OneSixVersionFormat::versionFileToJson(file, false); + auto newdata = doc.toBinaryData(); + auto id = updatedVersion->descriptor(); + QString targetPath = "versions/" + id + "/" + id + ".dat"; + FS::ensureFilePathExists(targetPath); + QSaveFile vfile1(targetPath); + if (!vfile1.open(QIODevice::Truncate | QIODevice::WriteOnly)) + { + emitFailed(tr("Can't open %1 for writing.").arg(targetPath)); + return; + } + qint64 actual = 0; + if ((actual = vfile1.write(newdata)) != newdata.size()) + { + emitFailed(tr("Failed to write into %1. Written %2 out of %3.") + .arg(targetPath) + .arg(actual) + .arg(newdata.size())); + return; + } + if (!vfile1.commit()) + { + emitFailed(tr("Can't commit changes to %1").arg(targetPath)); + return; + } + + m_list->finalizeUpdate(id); + emitSucceeded(); +} + +std::shared_ptr MinecraftVersionList::createUpdateTask(QString version) +{ + auto iter = m_lookup.find(version); + if(iter == m_lookup.end()) + return nullptr; + + auto mcversion = std::dynamic_pointer_cast(*iter); + if(!mcversion) + { + return nullptr; + } + + return std::shared_ptr(new MCVListVersionUpdateTask(this, mcversion)); +} + +void MinecraftVersionList::saveCachedList() +{ + // FIXME: throw. + if (!FS::ensureFilePathExists(localVersionCache)) + return; + QSaveFile tfile(localVersionCache); + if (!tfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) + return; + QJsonObject toplevel; + QJsonArray entriesArr; + for (auto version : m_vlist) + { + auto mcversion = std::dynamic_pointer_cast(version); + // do not save the remote versions. + if (mcversion->m_versionSource != Local) + continue; + QJsonObject entryObj; + + entryObj.insert("id", mcversion->descriptor()); + entryObj.insert("version", mcversion->descriptor()); + entryObj.insert("time", timeToS3Time(mcversion->m_updateTime)); + entryObj.insert("releaseTime", timeToS3Time(mcversion->m_releaseTime)); + entryObj.insert("url", mcversion->m_versionFileURL); + entryObj.insert("type", mcversion->m_type); + entriesArr.append(entryObj); + } + toplevel.insert("versions", entriesArr); + + { + bool someLatest = false; + QJsonObject latestObj; + if(!m_latestReleaseID.isNull()) + { + latestObj.insert("release", m_latestReleaseID); + someLatest = true; + } + if(!m_latestSnapshotID.isNull()) + { + latestObj.insert("snapshot", m_latestSnapshotID); + someLatest = true; + } + if(someLatest) + { + toplevel.insert("latest", latestObj); + } + } + + QJsonDocument doc(toplevel); + QByteArray jsonData = doc.toBinaryData(); + qint64 result = tfile.write(jsonData); + if (result == -1) + return; + if (result != jsonData.size()) + return; + tfile.commit(); +} + +void MinecraftVersionList::finalizeUpdate(QString version) +{ + int idx = -1; + for (int i = 0; i < m_vlist.size(); i++) + { + if (version == m_vlist[i]->descriptor()) + { + idx = i; + break; + } + } + if (idx == -1) + { + return; + } + + auto updatedVersion = std::dynamic_pointer_cast(m_vlist[idx]); + + // if we have an update for the version, replace it, make the update local + if (updatedVersion->upstreamUpdate) + { + auto updatedWith = updatedVersion->upstreamUpdate; + updatedWith->m_versionSource = Local; + m_vlist[idx] = updatedWith; + m_lookup[version] = updatedWith; + } + else + { + // otherwise, just set the version as local; + updatedVersion->m_versionSource = Local; + } + + dataChanged(index(idx), index(idx)); + + saveCachedList(); +} + +#include "MinecraftVersionList.moc" diff --git a/api/logic/minecraft/MinecraftVersionList.h b/api/logic/minecraft/MinecraftVersionList.h new file mode 100644 index 00000000..0fca02a7 --- /dev/null +++ b/api/logic/minecraft/MinecraftVersionList.h @@ -0,0 +1,72 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "BaseVersionList.h" +#include "tasks/Task.h" +#include "minecraft/MinecraftVersion.h" +#include + +#include "multimc_logic_export.h" + +class MCVListLoadTask; +class MCVListVersionUpdateTask; + +class MULTIMC_LOGIC_EXPORT MinecraftVersionList : public BaseVersionList +{ + Q_OBJECT +private: + void sortInternal(); + void loadList(QJsonDocument jsonDoc, VersionSource source); + void loadCachedList(); + void saveCachedList(); + void finalizeUpdate(QString version); +public: + friend class MCVListLoadTask; + friend class MCVListVersionUpdateTask; + + explicit MinecraftVersionList(QObject *parent = 0); + + std::shared_ptr createUpdateTask(QString version); + + virtual Task *getLoadTask() override; + virtual bool isLoaded() override; + virtual const BaseVersionPtr at(int i) const override; + virtual int count() const override; + virtual void sortVersions() override; + virtual QVariant data(const QModelIndex & index, int role) const override; + virtual RoleList providesRoles() const override; + + virtual BaseVersionPtr getLatestStable() const override; + virtual BaseVersionPtr getRecommended() const override; + +protected: + QList m_vlist; + QMap m_lookup; + + bool m_loaded = false; + bool m_hasLocalIndex = false; + QString m_latestReleaseID = "INVALID"; + QString m_latestSnapshotID = "INVALID"; + +protected +slots: + virtual void updateListData(QList versions) override; +}; diff --git a/api/logic/minecraft/Mod.cpp b/api/logic/minecraft/Mod.cpp new file mode 100644 index 00000000..9b9f76f9 --- /dev/null +++ b/api/logic/minecraft/Mod.cpp @@ -0,0 +1,377 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Mod.h" +#include "settings/INIFile.h" +#include +#include + +Mod::Mod(const QFileInfo &file) +{ + repath(file); +} + +void Mod::repath(const QFileInfo &file) +{ + m_file = file; + QString name_base = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + + if (m_file.isDir()) + { + m_type = MOD_FOLDER; + m_name = name_base; + m_mmc_id = name_base; + } + else if (m_file.isFile()) + { + if (name_base.endsWith(".disabled")) + { + m_enabled = false; + name_base.chop(9); + } + else + { + m_enabled = true; + } + m_mmc_id = name_base; + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) + { + m_type = MOD_ZIPFILE; + name_base.chop(4); + } + else if (name_base.endsWith(".litemod")) + { + m_type = MOD_LITEMOD; + name_base.chop(8); + } + else + { + m_type = MOD_SINGLEFILE; + } + m_name = name_base; + } + + if (m_type == MOD_ZIPFILE) + { + QuaZip zip(m_file.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + + zip.close(); + } + else if (m_type == MOD_FOLDER) + { + QFileInfo mcmod_info(FS::PathCombine(m_file.filePath(), "mcmod.info")); + if (mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return; + ReadMCModInfo(data); + } + } + else if (m_type == MOD_LITEMOD) + { + QuaZip zip(m_file.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadLiteModInfo(file.readAll()); + file.close(); + } + zip.close(); + } +} + +// NEW format +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 + +// OLD format: +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc +void Mod::ReadMCModInfo(QByteArray contents) +{ + auto getInfoFromArray = [&](QJsonArray arr)->void + { + if (!arr.at(0).isObject()) + return; + auto firstObj = arr.at(0).toObject(); + m_mod_id = firstObj.value("modid").toString(); + m_name = firstObj.value("name").toString(); + m_version = firstObj.value("version").toString(); + m_homeurl = firstObj.value("url").toString(); + m_updateurl = firstObj.value("updateUrl").toString(); + m_homeurl = m_homeurl.trimmed(); + if(!m_homeurl.isEmpty()) + { + // fix up url. + if (!m_homeurl.startsWith("http://") && !m_homeurl.startsWith("https://") && + !m_homeurl.startsWith("ftp://")) + { + m_homeurl.prepend("http://"); + } + } + m_description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) + authors = firstObj.value("authors").toArray(); + + if (authors.size() == 0) + m_authors = ""; + else if (authors.size() >= 1) + { + m_authors = authors.at(0).toString(); + for (int i = 1; i < authors.size(); i++) + { + m_authors += ", " + authors.at(i).toString(); + } + } + m_credits = firstObj.value("credits").toString(); + return; + } + ; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if(val.isUndefined()) + val = jsonDoc.object().value("modListVersion"); + int version = val.toDouble(); + if (version != 2) + { + qCritical() << "BAD stuff happened to mod json:"; + qCritical() << contents; + return; + } + auto arrVal = jsonDoc.object().value("modlist"); + if(arrVal.isUndefined()) + arrVal = jsonDoc.object().value("modList"); + if (arrVal.isArray()) + { + getInfoFromArray(arrVal.toArray()); + } + } +} + +void Mod::ReadForgeInfo(QByteArray contents) +{ + // Read the data + m_name = "Minecraft Forge"; + m_mod_id = "Forge"; + m_homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + m_version = major + "." + minor + "." + revision + "." + build; +} + +void Mod::ReadLiteModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) + { + m_mod_id = m_name = object.value("name").toString(); + } + if (object.contains("version")) + { + m_version = object.value("version").toString(""); + } + else + { + m_version = object.value("revision").toString(""); + } + m_mcversion = object.value("mcversion").toString(); + m_authors = object.value("author").toString(); + m_description = object.value("description").toString(); + m_homeurl = object.value("url").toString(); +} + +bool Mod::replace(Mod &with) +{ + if (!destroy()) + return false; + bool success = false; + auto t = with.type(); + + if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE || t == MOD_LITEMOD) + { + qDebug() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath(); + success = QFile::copy(with.m_file.filePath(), m_file.filePath()); + } + if (t == MOD_FOLDER) + { + success = FS::copy(with.m_file.filePath(), m_file.path())(); + } + if (success) + { + m_name = with.m_name; + m_mmc_id = with.m_mmc_id; + m_mod_id = with.m_mod_id; + m_version = with.m_version; + m_mcversion = with.m_mcversion; + m_description = with.m_description; + m_authors = with.m_authors; + m_credits = with.m_credits; + m_homeurl = with.m_homeurl; + m_type = with.m_type; + m_file.refresh(); + } + return success; +} + +bool Mod::destroy() +{ + if (m_type == MOD_FOLDER) + { + QDir d(m_file.filePath()); + if (d.removeRecursively()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + else if (m_type == MOD_SINGLEFILE || m_type == MOD_ZIPFILE || m_type == MOD_LITEMOD) + { + QFile f(m_file.filePath()); + if (f.remove()) + { + m_type = MOD_UNKNOWN; + return true; + } + return false; + } + return true; +} + +QString Mod::version() const +{ + switch (type()) + { + case MOD_ZIPFILE: + case MOD_LITEMOD: + return m_version; + case MOD_FOLDER: + return "Folder"; + case MOD_SINGLEFILE: + return "File"; + default: + return "VOID"; + } +} + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) + { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } + else + { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + m_file = QFileInfo(path); + m_enabled = value; + return true; +} +bool Mod::operator==(const Mod &other) const +{ + return mmc_id() == other.mmc_id(); +} +bool Mod::strongCompare(const Mod &other) const +{ + return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type(); +} diff --git a/api/logic/minecraft/Mod.h b/api/logic/minecraft/Mod.h new file mode 100644 index 00000000..19f4c740 --- /dev/null +++ b/api/logic/minecraft/Mod.h @@ -0,0 +1,134 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +class Mod +{ +public: + enum ModType + { + MOD_UNKNOWN, //!< Indicates an unspecified mod type. + MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. + MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). + MOD_FOLDER, //!< The mod is in a folder on the filesystem. + MOD_LITEMOD, //!< The mod is a litemod + }; + + Mod(const QFileInfo &file); + + QFileInfo filename() const + { + return m_file; + } + QString mmc_id() const + { + return m_mmc_id; + } + QString mod_id() const + { + return m_mod_id; + } + ModType type() const + { + return m_type; + } + QString mcversion() const + { + return m_mcversion; + } + ; + bool valid() + { + return m_type != MOD_UNKNOWN; + } + QString name() const + { + if(m_name.trimmed().isEmpty()) + { + return m_mmc_id; + } + return m_name; + } + + QString version() const; + + QString homeurl() const + { + return m_homeurl; + } + + QString description() const + { + return m_description; + } + + QString authors() const + { + return m_authors; + } + + QString credits() const + { + return m_credits; + } + + bool enabled() const + { + return m_enabled; + } + + bool enable(bool value); + + // delete all the files of this mod + bool destroy(); + // replace this mod with a copy of the other + bool replace(Mod &with); + // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) + void repath(const QFileInfo &file); + + // WEAK compare operator - used for replacing mods + bool operator==(const Mod &other) const; + bool strongCompare(const Mod &other) const; + +private: + void ReadMCModInfo(QByteArray contents); + void ReadForgeInfo(QByteArray contents); + void ReadLiteModInfo(QByteArray contents); + +protected: + + // FIXME: what do do with those? HMM... + /* + void ReadModInfoData(QString info); + void ReadForgeInfoData(QString infoFileData); + */ + + QFileInfo m_file; + QString m_mmc_id; + QString m_mod_id; + bool m_enabled = true; + QString m_name; + QString m_version; + QString m_mcversion; + QString m_homeurl; + QString m_updateurl; + QString m_description; + QString m_authors; + QString m_credits; + + ModType m_type; +}; diff --git a/api/logic/minecraft/ModList.cpp b/api/logic/minecraft/ModList.cpp new file mode 100644 index 00000000..d9ed4886 --- /dev/null +++ b/api/logic/minecraft/ModList.cpp @@ -0,0 +1,616 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModList.h" +#include +#include +#include +#include +#include +#include +#include + +ModList::ModList(const QString &dir, const QString &list_file) + : QAbstractListModel(), m_dir(dir), m_list_file(list_file) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | + QDir::NoSymLinks); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_list_id = QUuid::createUuid().toString(); + m_watcher = new QFileSystemWatcher(this); + is_watching = false; + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, + SLOT(directoryChanged(QString))); +} + +void ModList::startWatching() +{ + update(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void ModList::stopWatching() +{ + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +void ModList::internalSort(QList &what) +{ + auto predicate = [](const Mod &left, const Mod &right) + { + if (left.name() == right.name()) + { + return left.mmc_id().localeAwareCompare(right.mmc_id()) < 0; + } + return left.name().localeAwareCompare(right.name()) < 0; + }; + std::sort(what.begin(), what.end(), predicate); +} + +bool ModList::update() +{ + if (!isValid()) + return false; + + QList orderedMods; + QList newMods; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + bool orderOrStateChanged = false; + + // first, process the ordered items (if any) + OrderList listOrder = readListFile(); + for (auto item : listOrder) + { + QFileInfo infoEnabled(m_dir.filePath(item.id)); + QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled")); + int idxEnabled = folderContents.indexOf(infoEnabled); + int idxDisabled = folderContents.indexOf(infoDisabled); + bool isEnabled; + // if both enabled and disabled versions are present, it's a special case... + if (idxEnabled >= 0 && idxDisabled >= 0) + { + // we only process the one we actually have in the order file. + // and exactly as we have it. + // THIS IS A CORNER CASE + isEnabled = item.enabled; + } + else + { + // only one is present. + // we pick the one that we found. + // we assume the mod was enabled/disabled by external means + isEnabled = idxEnabled >= 0; + } + int idx = isEnabled ? idxEnabled : idxDisabled; + QFileInfo &info = isEnabled ? infoEnabled : infoDisabled; + // if the file from the index file exists + if (idx != -1) + { + // remove from the actual folder contents list + folderContents.takeAt(idx); + // append the new mod + orderedMods.append(Mod(info)); + if (isEnabled != item.enabled) + orderOrStateChanged = true; + } + else + { + orderOrStateChanged = true; + } + } + // if there are any untracked files... + if (folderContents.size()) + { + // the order surely changed! + for (auto entry : folderContents) + { + newMods.append(Mod(entry)); + } + internalSort(newMods); + orderedMods.append(newMods); + orderOrStateChanged = true; + } + // otherwise, if we were already tracking some mods + else if (mods.size()) + { + // if the number doesn't match, order changed. + if (mods.size() != orderedMods.size()) + orderOrStateChanged = true; + // if it does match, compare the mods themselves + else + for (int i = 0; i < mods.size(); i++) + { + if (!mods[i].strongCompare(orderedMods[i])) + { + orderOrStateChanged = true; + break; + } + } + } + beginResetModel(); + mods.swap(orderedMods); + endResetModel(); + if (orderOrStateChanged && !m_list_file.isEmpty()) + { + qDebug() << "Mod list " << m_list_file << " changed!"; + saveListFile(); + emit changed(); + } + return true; +} + +void ModList::directoryChanged(QString path) +{ + update(); +} + +ModList::OrderList ModList::readListFile() +{ + OrderList itemList; + if (m_list_file.isNull() || m_list_file.isEmpty()) + return itemList; + + QFile textFile(m_list_file); + if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text)) + return OrderList(); + + QTextStream textStream; + textStream.setAutoDetectUnicode(true); + textStream.setDevice(&textFile); + while (true) + { + QString line = textStream.readLine(); + if (line.isNull() || line.isEmpty()) + break; + else + { + OrderItem it; + it.enabled = !line.endsWith(".disabled"); + if (!it.enabled) + { + line.chop(9); + } + it.id = line; + itemList.append(it); + } + } + textFile.close(); + return itemList; +} + +bool ModList::saveListFile() +{ + if (m_list_file.isNull() || m_list_file.isEmpty()) + return false; + QFile textFile(m_list_file); + if (!textFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) + return false; + QTextStream textStream; + textStream.setGenerateByteOrderMark(true); + textStream.setCodec("UTF-8"); + textStream.setDevice(&textFile); + for (auto mod : mods) + { + textStream << mod.mmc_id(); + if (!mod.enabled()) + textStream << ".disabled"; + textStream << endl; + } + textFile.close(); + return false; +} + +bool ModList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool ModList::installMod(const QString &filename, int index) +{ + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + QFileInfo fileinfo(FS::NormalizePath(filename)); + + qDebug() << "installing: " << fileinfo.absoluteFilePath(); + + if (!fileinfo.exists() || !fileinfo.isReadable() || index < 0) + { + return false; + } + Mod m(fileinfo); + if (!m.valid()) + return false; + + // if it's already there, replace the original mod (in place) + int idx = mods.indexOf(m); + if (idx != -1) + { + int idx2 = mods.indexOf(m, idx + 1); + if (idx2 != -1) + return false; + if (mods[idx].replace(m)) + { + + auto left = this->index(index); + auto right = this->index(index, columnCount(QModelIndex()) - 1); + emit dataChanged(left, right); + saveListFile(); + update(); + return true; + } + return false; + } + + auto type = m.type(); + if (type == Mod::MOD_UNKNOWN) + return false; + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) + { + QString newpath = FS::PathCombine(m_dir.path(), fileinfo.fileName()); + if (!QFile::copy(fileinfo.filePath(), newpath)) + return false; + m.repath(newpath); + beginInsertRows(QModelIndex(), index, index); + mods.insert(index, m); + endInsertRows(); + saveListFile(); + update(); + return true; + } + else if (type == Mod::MOD_FOLDER) + { + + QString from = fileinfo.filePath(); + QString to = FS::PathCombine(m_dir.path(), fileinfo.fileName()); + if (!FS::copy(from, to)()) + return false; + m.repath(to); + beginInsertRows(QModelIndex(), index, index); + mods.insert(index, m); + endInsertRows(); + saveListFile(); + update(); + return true; + } + return false; +} + +bool ModList::deleteMod(int index) +{ + if (index >= mods.size() || index < 0) + return false; + Mod &m = mods[index]; + if (m.destroy()) + { + beginRemoveRows(QModelIndex(), index, index); + mods.removeAt(index); + endRemoveRows(); + saveListFile(); + emit changed(); + return true; + } + return false; +} + +bool ModList::deleteMods(int first, int last) +{ + for (int i = first; i <= last; i++) + { + Mod &m = mods[i]; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + mods.erase(mods.begin() + first, mods.begin() + last + 1); + endRemoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModTo(int from, int to) +{ + if (from < 0 || from >= mods.size()) + return false; + if (to >= rowCount()) + to = rowCount() - 1; + if (to == -1) + to = rowCount() - 1; + if (from == to) + return false; + int togap = to > from ? to + 1 : to; + beginMoveRows(QModelIndex(), from, from, QModelIndex(), togap); + mods.move(from, to); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModUp(int from) +{ + if (from > 0) + return moveModTo(from, from - 1); + return false; +} + +bool ModList::moveModsUp(int first, int last) +{ + if (first == 0) + return false; + + beginMoveRows(QModelIndex(), first, last, QModelIndex(), first - 1); + mods.move(first - 1, last); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +bool ModList::moveModDown(int from) +{ + if (from < 0) + return false; + if (from < mods.size() - 1) + return moveModTo(from, from + 1); + return false; +} + +bool ModList::moveModsDown(int first, int last) +{ + if (last == mods.size() - 1) + return false; + + beginMoveRows(QModelIndex(), first, last, QModelIndex(), last + 2); + mods.move(last + 1, first); + endMoveRows(); + saveListFile(); + emit changed(); + return true; +} + +int ModList::columnCount(const QModelIndex &parent) const +{ + return 3; +} + +QVariant ModList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= mods.size()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return mods[row].name(); + case VersionColumn: + return mods[row].version(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + auto &mod = mods[index.row()]; + if (mod.enable(!mod.enabled())) + { + emit dataChanged(index, index); + return true; + } + } + return false; +} + +QVariant ModList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags ModList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | + defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +QStringList ModList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + types << "text/plain"; + return types; +} + +Qt::DropActions ModList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::DropActions ModList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +QMimeData *ModList::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *data = new QMimeData(); + + if (indexes.size() == 0) + return data; + + auto idx = indexes[0]; + int row = idx.row(); + if (row < 0 || row >= mods.size()) + return data; + + QStringList params; + params << m_list_id << QString::number(row); + data->setText(params.join('|')); + return data; +} + +bool ModList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + if (parent.isValid()) + { + row = parent.row(); + column = parent.column(); + } + + if (row > rowCount()) + row = rowCount(); + if (row == -1) + row = rowCount(); + if (column == -1) + column = 0; + qDebug() << "Drop row: " << row << " column: " << column; + + // files dropped from outside? + if (data->hasUrls()) + { + bool was_watching = is_watching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + installMod(filename, row); + // if there is no ordering, re-sort the list + if (m_list_file.isEmpty()) + { + beginResetModel(); + internalSort(mods); + endResetModel(); + } + } + if (was_watching) + startWatching(); + return true; + } + else if (data->hasText()) + { + QString sourcestr = data->text(); + auto list = sourcestr.split('|'); + if (list.size() != 2) + return false; + QString remoteId = list[0]; + int remoteIndex = list[1].toInt(); + qDebug() << "move: " << sourcestr; + // no moving of things between two lists + if (remoteId != m_list_id) + return false; + // no point moving to the same place... + if (row == remoteIndex) + return false; + // otherwise, move the mod :D + moveModTo(remoteIndex, row); + return true; + } + return false; +} diff --git a/api/logic/minecraft/ModList.h b/api/logic/minecraft/ModList.h new file mode 100644 index 00000000..05ada8ee --- /dev/null +++ b/api/logic/minecraft/ModList.h @@ -0,0 +1,160 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "minecraft/Mod.h" + +#include "multimc_logic_export.h" + +class LegacyInstance; +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class MULTIMC_LOGIC_EXPORT ModList : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + VersionColumn + }; + ModList(const QString &dir, const QString &list_file = QString()); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole); + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return size(); + } + ; + virtual QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex &parent) const; + + size_t size() const + { + return mods.size(); + } + ; + bool empty() const + { + return size() == 0; + } + Mod &operator[](size_t index) + { + return mods[index]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /** + * Adds the given mod to the list at the given index - if the list supports custom ordering + */ + virtual bool installMod(const QString & filename, int index = 0); + + /// Deletes the mod at the given index. + virtual bool deleteMod(int index); + + /// Deletes all the selected mods + virtual bool deleteMods(int first, int last); + + /** + * move the mod at index to the position N + * 0 is the beginning of the list, length() is the end of the list. + */ + virtual bool moveModTo(int from, int to); + + /** + * move the mod at index one position upwards + */ + virtual bool moveModUp(int from); + virtual bool moveModsUp(int first, int last); + + /** + * move the mod at index one position downwards + */ + virtual bool moveModDown(int from); + virtual bool moveModsDown(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + /// get data for drag action + virtual QMimeData *mimeData(const QModelIndexList &indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent); + /// what drag actions do we support? + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() + { + return m_dir; + } + + const QList & allMods() + { + return mods; + } + +private: + void internalSort(QList & what); + struct OrderItem + { + QString id; + bool enabled = false; + }; + typedef QList OrderList; + OrderList readListFile(); + bool saveListFile(); +private +slots: + void directoryChanged(QString path); + +signals: + void changed(); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching; + QDir m_dir; + QString m_list_file; + QString m_list_id; + QList mods; +}; diff --git a/api/logic/minecraft/MojangDownloadInfo.h b/api/logic/minecraft/MojangDownloadInfo.h new file mode 100644 index 00000000..1f3306e0 --- /dev/null +++ b/api/logic/minecraft/MojangDownloadInfo.h @@ -0,0 +1,71 @@ +#pragma once +#include +#include +#include + +struct MojangDownloadInfo +{ + // types + typedef std::shared_ptr 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 Ptr; + + // methods + MojangDownloadInfo *getDownloadInfo(QString classifier) + { + if (classifier.isNull()) + { + return artifact.get(); + } + + return classifiers[classifier].get(); + } + + // data + MojangDownloadInfo::Ptr artifact; + QMap classifiers; +}; + + + +struct MojangAssetIndexInfo : public MojangDownloadInfo +{ + // types + typedef std::shared_ptr Ptr; + + // methods + MojangAssetIndexInfo() + { + } + + MojangAssetIndexInfo(QString id) + { + this->id = id; + url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id + ".json"; + known = false; + } + + // data + int totalSize; + QString id; + bool known = true; +}; diff --git a/api/logic/minecraft/MojangVersionFormat.cpp b/api/logic/minecraft/MojangVersionFormat.cpp new file mode 100644 index 00000000..34129c9e --- /dev/null +++ b/api/logic/minecraft/MojangVersionFormat.cpp @@ -0,0 +1,381 @@ +#include "MojangVersionFormat.h" +#include "onesix/OneSixVersionFormat.h" +#include "MinecraftVersion.h" +#include "VersionBuildError.h" +#include "MojangDownloadInfo.h" + +#include "Json.h" +using namespace Json; +#include "ParseUtils.h" + +static const int CURRENT_MINIMUM_LAUNCHER_VERSION = 18; + +static MojangAssetIndexInfo::Ptr assetIndexFromJson (const QJsonObject &obj); +static MojangDownloadInfo::Ptr downloadInfoFromJson (const QJsonObject &obj); +static MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson (const QJsonObject &libObj); +static QJsonObject assetIndexToJson (MojangAssetIndexInfo::Ptr assetidxinfo); +static QJsonObject libDownloadInfoToJson (MojangLibraryDownloadInfo::Ptr libinfo); +static QJsonObject downloadInfoToJson (MojangDownloadInfo::Ptr info); + +namespace Bits +{ +static void readString(const QJsonObject &root, const QString &key, QString &variable) +{ + if (root.contains(key)) + { + variable = requireString(root.value(key)); + } +} + +static void readDownloadInfo(MojangDownloadInfo::Ptr out, const QJsonObject &obj) +{ + // optional, not used + readString(obj, "path", out->path); + // required! + out->sha1 = requireString(obj, "sha1"); + out->url = requireString(obj, "url"); + out->size = requireInteger(obj, "size"); +} + +static void readAssetIndex(MojangAssetIndexInfo::Ptr out, const QJsonObject &obj) +{ + out->totalSize = requireInteger(obj, "totalSize"); + out->id = requireString(obj, "id"); + // out->known = true; +} +} + +MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject &obj) +{ + auto out = std::make_shared(); + Bits::readDownloadInfo(out, obj); + return out; +} + +MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject &obj) +{ + auto out = std::make_shared(); + 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(); + auto dlObj = requireObject(libObj.value("downloads")); + if(dlObj.contains("artifact")) + { + out->artifact = downloadInfoFromJson(requireObject(dlObj, "artifact")); + } + if(dlObj.contains("classifiers")) + { + auto classifiersObj = requireObject(dlObj, "classifiers"); + for(auto iter = classifiersObj.begin(); iter != classifiersObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireObject(iter.value()); + out->classifiers[classifier] = downloadInfoFromJson(classifierObj); + } + } + return out; +} + +QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) +{ + QJsonObject out; + if(libinfo->artifact) + { + out.insert("artifact", downloadInfoToJson(libinfo->artifact)); + } + if(libinfo->classifiers.size()) + { + QJsonObject classifiersOut; + for(auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) + { + classifiersOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("classifiers", classifiersOut); + } + return out; +} + +QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr info) +{ + QJsonObject out; + if(!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + out.insert("totalSize", info->totalSize); + out.insert("id", info->id); + return out; +} + +void MojangVersionFormat::readVersionProperties(const QJsonObject &in, VersionFile *out) +{ + Bits::readString(in, "id", out->minecraftVersion); + Bits::readString(in, "mainClass", out->mainClass); + Bits::readString(in, "minecraftArguments", out->minecraftArguments); + if(out->minecraftArguments.isEmpty()) + { + QString processArguments; + Bits::readString(in, "processArguments", processArguments); + QString toCompare = processArguments.toLower(); + if (toCompare == "legacy") + { + out->minecraftArguments = " ${auth_player_name} ${auth_session}"; + } + else if (toCompare == "username_session") + { + out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}"; + } + else if (toCompare == "username_session_version") + { + out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}"; + } + else if (!toCompare.isEmpty()) + { + out->addProblem(PROBLEM_ERROR, QObject::tr("processArguments is set to unknown value '%1'").arg(processArguments)); + } + } + Bits::readString(in, "type", out->type); + + Bits::readString(in, "assets", out->assets); + if(in.contains("assetIndex")) + { + out->mojangAssetIndex = assetIndexFromJson(requireObject(in, "assetIndex")); + } + else if (!out->assets.isNull()) + { + out->mojangAssetIndex = std::make_shared(out->assets); + } + + out->m_releaseTime = timeFromS3Time(in.value("releaseTime").toString("")); + out->m_updateTime = timeFromS3Time(in.value("time").toString("")); + + if (in.contains("minimumLauncherVersion")) + { + out->minimumLauncherVersion = requireInteger(in.value("minimumLauncherVersion")); + if (out->minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION) + { + out->addProblem( + PROBLEM_WARNING, + QObject::tr("The 'minimumLauncherVersion' value of this version (%1) is higher than supported by MultiMC (%2). It might not work properly!") + .arg(out->minimumLauncherVersion) + .arg(CURRENT_MINIMUM_LAUNCHER_VERSION)); + } + } + if(in.contains("downloads")) + { + auto downloadsObj = requireObject(in, "downloads"); + for(auto iter = downloadsObj.begin(); iter != downloadsObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireObject(iter.value()); + out->mojangDownloads[classifier] = downloadInfoFromJson(classifierObj); + } + } +} + +VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument &doc, const QString &filename) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + readVersionProperties(root, out.get()); + + out->name = "Minecraft"; + out->fileId = "net.minecraft"; + out->version = out->minecraftVersion; + out->filename = filename; + + + if (root.contains("libraries")) + { + for (auto libVal : requireArray(root.value("libraries"))) + { + auto libObj = requireObject(libVal); + + auto lib = MojangVersionFormat::libraryFromJson(libObj, filename); + out->libraries.append(lib); + } + } + return out; +} + +void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObject& out) +{ + writeString(out, "id", in->minecraftVersion); + writeString(out, "mainClass", in->mainClass); + writeString(out, "minecraftArguments", in->minecraftArguments); + writeString(out, "type", in->type); + if(!in->m_releaseTime.isNull()) + { + writeString(out, "releaseTime", timeToS3Time(in->m_releaseTime)); + } + if(!in->m_updateTime.isNull()) + { + writeString(out, "time", timeToS3Time(in->m_updateTime)); + } + if(in->minimumLauncherVersion != -1) + { + out.insert("minimumLauncherVersion", in->minimumLauncherVersion); + } + writeString(out, "assets", in->assets); + if(in->mojangAssetIndex && in->mojangAssetIndex->known) + { + out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); + } + if(in->mojangDownloads.size()) + { + QJsonObject downloadsOut; + for(auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) + { + downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("downloads", downloadsOut); + } +} + +QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch) +{ + QJsonObject root; + writeVersionProperties(patch.get(), root); + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value: patch->libraries) + { + array.append(MojangVersionFormat::libraryToJson(value.get())); + } + root.insert("libraries", array); + } + + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +{ + LibraryPtr out(new Library()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); + } + out->m_name = libObj.value("name").toString(); + + Bits::readString(libObj, "url", out->m_repositoryURL); + if (libObj.contains("extract")) + { + out->m_hasExcludes = true; + auto extractObj = requireObject(libObj.value("extract")); + for (auto excludeVal : requireArray(extractObj.value("exclude"))) + { + out->m_extractExcludes.append(requireString(excludeVal)); + } + } + if (libObj.contains("natives")) + { + QJsonObject nativesObj = requireObject(libObj.value("natives")); + for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) + { + if (!it.value().isString()) + { + qWarning() << filename << "contains an invalid native (skipping)"; + } + OpSys opSys = OpSys_fromString(it.key()); + if (opSys != Os_Other) + { + out->m_nativeClassifiers[opSys] = it.value().toString(); + } + } + } + if (libObj.contains("rules")) + { + out->applyRules = true; + out->m_rules = rulesFromJsonV4(libObj); + } + if (libObj.contains("downloads")) + { + out->m_mojangDownloads = libDownloadInfoFromJson(libObj); + } + return out; +} + +QJsonObject MojangVersionFormat::libraryToJson(Library *library) +{ + QJsonObject libRoot; + libRoot.insert("name", (QString)library->m_name); + if (!library->m_repositoryURL.isEmpty()) + { + libRoot.insert("url", library->m_repositoryURL); + } + if (library->isNative()) + { + QJsonObject nativeList; + auto iter = library->m_nativeClassifiers.begin(); + while (iter != library->m_nativeClassifiers.end()) + { + nativeList.insert(OpSys_toString(iter.key()), iter.value()); + iter++; + } + libRoot.insert("natives", nativeList); + if (library->m_extractExcludes.size()) + { + QJsonArray excludes; + QJsonObject extract; + for (auto exclude : library->m_extractExcludes) + { + excludes.append(exclude); + } + extract.insert("exclude", excludes); + libRoot.insert("extract", extract); + } + } + if (library->m_rules.size()) + { + QJsonArray allRules; + for (auto &rule : library->m_rules) + { + QJsonObject ruleObj = rule->toJson(); + allRules.append(ruleObj); + } + libRoot.insert("rules", allRules); + } + if(library->m_mojangDownloads) + { + auto downloadsObj = libDownloadInfoToJson(library->m_mojangDownloads); + libRoot.insert("downloads", downloadsObj); + } + return libRoot; +} diff --git a/api/logic/minecraft/MojangVersionFormat.h b/api/logic/minecraft/MojangVersionFormat.h new file mode 100644 index 00000000..4e141088 --- /dev/null +++ b/api/logic/minecraft/MojangVersionFormat.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT MojangVersionFormat +{ +friend class OneSixVersionFormat; +protected: + // does not include libraries + static void readVersionProperties(const QJsonObject& in, VersionFile* out); + // does not include libraries + static void writeVersionProperties(const VersionFile* in, QJsonObject& out); +public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument &doc, const QString &filename); + static QJsonDocument versionFileToJson(const VersionFilePtr &patch); + + // libraries + static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static QJsonObject libraryToJson(Library *library); +}; diff --git a/api/logic/minecraft/OpSys.cpp b/api/logic/minecraft/OpSys.cpp new file mode 100644 index 00000000..4c2a236d --- /dev/null +++ b/api/logic/minecraft/OpSys.cpp @@ -0,0 +1,42 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OpSys.h" + +OpSys OpSys_fromString(QString name) +{ + if (name == "linux") + return Os_Linux; + if (name == "windows") + return Os_Windows; + if (name == "osx") + return Os_OSX; + return Os_Other; +} + +QString OpSys_toString(OpSys name) +{ + switch (name) + { + case Os_Linux: + return "linux"; + case Os_OSX: + return "osx"; + case Os_Windows: + return "windows"; + default: + return "other"; + } +} \ No newline at end of file diff --git a/api/logic/minecraft/OpSys.h b/api/logic/minecraft/OpSys.h new file mode 100644 index 00000000..9ebea3de --- /dev/null +++ b/api/logic/minecraft/OpSys.h @@ -0,0 +1,37 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +enum OpSys +{ + Os_Windows, + Os_Linux, + Os_OSX, + Os_Other +}; + +OpSys OpSys_fromString(QString); +QString OpSys_toString(OpSys); + +#ifdef Q_OS_WIN32 +#define currentSystem Os_Windows +#else +#ifdef Q_OS_MAC +#define currentSystem Os_OSX +#else +#define currentSystem Os_Linux +#endif +#endif \ No newline at end of file diff --git a/api/logic/minecraft/ParseUtils.cpp b/api/logic/minecraft/ParseUtils.cpp new file mode 100644 index 00000000..ca188432 --- /dev/null +++ b/api/logic/minecraft/ParseUtils.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "ParseUtils.h" +#include +#include + +QDateTime timeFromS3Time(QString str) +{ + return QDateTime::fromString(str, Qt::ISODate); +} + +QString timeToS3Time(QDateTime time) +{ + // this all because Qt can't format timestamps right. + int offsetRaw = time.offsetFromUtc(); + bool negative = offsetRaw < 0; + int offsetAbs = std::abs(offsetRaw); + + int offsetSeconds = offsetAbs % 60; + offsetAbs -= offsetSeconds; + + int offsetMinutes = offsetAbs % 3600; + offsetAbs -= offsetMinutes; + offsetMinutes /= 60; + + int offsetHours = offsetAbs / 3600; + + QString raw = time.toString("yyyy-MM-ddTHH:mm:ss"); + raw += (negative ? QChar('-') : QChar('+')); + raw += QString("%1").arg(offsetHours, 2, 10, QChar('0')); + raw += ":"; + raw += QString("%1").arg(offsetMinutes, 2, 10, QChar('0')); + return raw; +} diff --git a/api/logic/minecraft/ParseUtils.h b/api/logic/minecraft/ParseUtils.h new file mode 100644 index 00000000..2b367a10 --- /dev/null +++ b/api/logic/minecraft/ParseUtils.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +#include "multimc_logic_export.h" + +/// take the timestamp used by S3 and turn it into QDateTime +MULTIMC_LOGIC_EXPORT QDateTime timeFromS3Time(QString str); + +/// take a timestamp and convert it into an S3 timestamp +MULTIMC_LOGIC_EXPORT QString timeToS3Time(QDateTime); diff --git a/api/logic/minecraft/ProfilePatch.h b/api/logic/minecraft/ProfilePatch.h new file mode 100644 index 00000000..f0c65360 --- /dev/null +++ b/api/logic/minecraft/ProfilePatch.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include +#include "JarMod.h" + +class MinecraftProfile; + +enum ProblemSeverity +{ + PROBLEM_NONE, + PROBLEM_WARNING, + PROBLEM_ERROR +}; + +/// where is a version from? +enum VersionSource +{ + Local, //!< version loaded from a file in the cache. + Remote, //!< incomplete version on a remote server. +}; + +class PatchProblem +{ +public: + PatchProblem(ProblemSeverity severity, const QString & description) + { + m_severity = severity; + m_description = description; + } + const QString & getDescription() const + { + return m_description; + } + const ProblemSeverity getSeverity() const + { + return m_severity; + } +private: + ProblemSeverity m_severity; + QString m_description; +}; + +class ProfilePatch : public std::enable_shared_from_this +{ +public: + virtual ~ProfilePatch(){}; + virtual void applyTo(MinecraftProfile *profile) = 0; + + virtual bool isMinecraftVersion() = 0; + virtual bool hasJarMods() = 0; + virtual QList getJarMods() = 0; + + virtual bool isMoveable() = 0; + virtual bool isCustomizable() = 0; + virtual bool isRevertible() = 0; + virtual bool isRemovable() = 0; + virtual bool isCustom() = 0; + virtual bool isEditable() = 0; + virtual bool isVersionChangeable() = 0; + + virtual void setOrder(int order) = 0; + virtual int getOrder() = 0; + + virtual QString getID() = 0; + virtual QString getName() = 0; + virtual QString getVersion() = 0; + virtual QDateTime getReleaseDateTime() = 0; + + virtual QString getFilename() = 0; + + virtual VersionSource getVersionSource() = 0; + + virtual std::shared_ptr getVersionFile() = 0; + + virtual const QList& getProblems() + { + return m_problems; + } + virtual void addProblem(ProblemSeverity severity, const QString &description) + { + if(severity > m_problemSeverity) + { + m_problemSeverity = severity; + } + m_problems.append(PatchProblem(severity, description)); + } + virtual ProblemSeverity getProblemSeverity() + { + return m_problemSeverity; + } + virtual bool hasFailed() + { + return getProblemSeverity() == PROBLEM_ERROR; + } + +protected: + QList m_problems; + ProblemSeverity m_problemSeverity = PROBLEM_NONE; +}; + +typedef std::shared_ptr ProfilePatchPtr; diff --git a/api/logic/minecraft/ProfileStrategy.h b/api/logic/minecraft/ProfileStrategy.h new file mode 100644 index 00000000..b4dfc4b3 --- /dev/null +++ b/api/logic/minecraft/ProfileStrategy.h @@ -0,0 +1,35 @@ +#pragma once + +#include "ProfileUtils.h" + +class MinecraftProfile; + +class ProfileStrategy +{ + friend class MinecraftProfile; +public: + virtual ~ProfileStrategy(){}; + + /// load the patch files into the profile + virtual void load() = 0; + + /// reset the order of patches + virtual bool resetOrder() = 0; + + /// save the order of patches, given the order + virtual bool saveOrder(ProfileUtils::PatchOrder order) = 0; + + /// install a list of jar mods into the instance + virtual bool installJarMods(QStringList filepaths) = 0; + + /// remove any files or records that constitute the version patch + virtual bool removePatch(ProfilePatchPtr jarMod) = 0; + + /// make the patch custom, if possible + virtual bool customizePatch(ProfilePatchPtr patch) = 0; + + /// revert the custom patch to 'vanilla', if possible + virtual bool revertPatch(ProfilePatchPtr patch) = 0; +protected: + MinecraftProfile *profile; +}; diff --git a/api/logic/minecraft/ProfileUtils.cpp b/api/logic/minecraft/ProfileUtils.cpp new file mode 100644 index 00000000..ef9b3b28 --- /dev/null +++ b/api/logic/minecraft/ProfileUtils.cpp @@ -0,0 +1,191 @@ +#include "ProfileUtils.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/onesix/OneSixVersionFormat.h" +#include "Json.h" +#include + +#include +#include +#include +#include + +namespace ProfileUtils +{ + +static const int currentOrderFileVersion = 1; + +bool writeOverrideOrders(QString path, const PatchOrder &order) +{ + QJsonObject obj; + obj.insert("version", currentOrderFileVersion); + QJsonArray orderArray; + for(auto str: order) + { + orderArray.append(str); + } + obj.insert("order", orderArray); + QSaveFile orderFile(path); + if (!orderFile.open(QFile::WriteOnly)) + { + qCritical() << "Couldn't open" << orderFile.fileName() + << "for writing:" << orderFile.errorString(); + return false; + } + auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); + if(orderFile.write(data) != data.size()) + { + qCritical() << "Couldn't write all the data into" << orderFile.fileName() + << "because:" << orderFile.errorString(); + return false; + } + if(!orderFile.commit()) + { + qCritical() << "Couldn't save" << orderFile.fileName() + << "because:" << orderFile.errorString(); + } + return true; +} + +bool readOverrideOrders(QString path, PatchOrder &order) +{ + QFile orderFile(path); + if (!orderFile.exists()) + { + qWarning() << "Order file doesn't exist. Ignoring."; + return false; + } + if (!orderFile.open(QFile::ReadOnly)) + { + qCritical() << "Couldn't open" << orderFile.fileName() + << " for reading:" << orderFile.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireInteger(obj.value("version")); + if (version != currentOrderFileVersion) + { + throw JSONValidationError(QObject::tr("Invalid order file version, expected %1") + .arg(currentOrderFileVersion)); + } + auto orderArray = Json::requireArray(obj.value("order")); + for(auto item: orderArray) + { + order.append(Json::requireString(item)); + } + } + catch (JSONValidationError &err) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; + qWarning() << "Ignoring overriden order"; + order.clear(); + return false; + } + return true; +} + +static VersionFilePtr createErrorVersionFile(QString fileId, QString filepath, QString error) +{ + auto outError = std::make_shared(); + outError->fileId = outError->name = fileId; + outError->filename = filepath; + outError->addProblem(PROBLEM_ERROR, error); + return outError; +} + +static VersionFilePtr guardedParseJson(const QJsonDocument & doc,const QString &fileId,const QString &filepath,const bool &requireOrder) +{ + try + { + return OneSixVersionFormat::versionFileFromJson(doc, filepath, requireOrder); + } + catch (Exception & e) + { + return createErrorVersionFile(fileId, filepath, e.cause()); + } +} + +VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + QJsonParseError error; + auto data = file.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + file.close(); + if (error.error != QJsonParseError::NoError) + { + int line = 1; + int column = 0; + for(int i = 0; i < error.offset; i++) + { + if(data[i] == '\n') + { + line++; + column = 0; + continue; + } + column++; + } + auto errorStr = QObject::tr("Unable to process the version file %1: %2 at line %3 column %4.") + .arg(fileInfo.fileName(), error.errorString()) + .arg(line).arg(column); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); +} + +VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + QJsonDocument doc = QJsonDocument::fromBinaryData(file.readAll()); + file.close(); + if (doc.isNull()) + { + file.remove(); + throw JSONValidationError(QObject::tr("Unable to process the version file %1.").arg(fileInfo.fileName())); + } + return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), false); +} + +void removeLwjglFromPatch(VersionFilePtr patch) +{ + auto filter = [](QList& libs) + { + QList filteredLibs; + for (auto lib : libs) + { + if (!g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) + { + filteredLibs.append(lib); + } + } + libs = filteredLibs; + }; + filter(patch->libraries); +} +} diff --git a/api/logic/minecraft/ProfileUtils.h b/api/logic/minecraft/ProfileUtils.h new file mode 100644 index 00000000..267fd42b --- /dev/null +++ b/api/logic/minecraft/ProfileUtils.h @@ -0,0 +1,25 @@ +#pragma once +#include "Library.h" +#include "VersionFile.h" + +namespace ProfileUtils +{ +typedef QStringList PatchOrder; + +/// Read and parse a OneSix format order file +bool readOverrideOrders(QString path, PatchOrder &order); + +/// Write a OneSix format order file +bool writeOverrideOrders(QString path, const PatchOrder &order); + + +/// Parse a version file in JSON format +VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder); + +/// Parse a version file in binary JSON format +VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo); + +/// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. +void removeLwjglFromPatch(VersionFilePtr patch); + +} diff --git a/api/logic/minecraft/Rule.cpp b/api/logic/minecraft/Rule.cpp new file mode 100644 index 00000000..c8ba297b --- /dev/null +++ b/api/logic/minecraft/Rule.cpp @@ -0,0 +1,93 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "Rule.h" + +RuleAction RuleAction_fromString(QString name) +{ + if (name == "allow") + return Allow; + if (name == "disallow") + return Disallow; + return Defer; +} + +QList> rulesFromJsonV4(const QJsonObject &objectWithRules) +{ + QList> rules; + auto rulesVal = objectWithRules.value("rules"); + if (!rulesVal.isArray()) + return rules; + + QJsonArray ruleList = rulesVal.toArray(); + for (auto ruleVal : ruleList) + { + std::shared_ptr rule; + if (!ruleVal.isObject()) + continue; + auto ruleObj = ruleVal.toObject(); + auto actionVal = ruleObj.value("action"); + if (!actionVal.isString()) + continue; + auto action = RuleAction_fromString(actionVal.toString()); + if (action == Defer) + continue; + + auto osVal = ruleObj.value("os"); + if (!osVal.isObject()) + { + // add a new implicit action rule + rules.append(ImplicitRule::create(action)); + continue; + } + + auto osObj = osVal.toObject(); + auto osNameVal = osObj.value("name"); + if (!osNameVal.isString()) + continue; + OpSys requiredOs = OpSys_fromString(osNameVal.toString()); + QString versionRegex = osObj.value("version").toString(); + // add a new OS rule + rules.append(OsRule::create(action, requiredOs, versionRegex)); + } + return rules; +} + +QJsonObject ImplicitRule::toJson() +{ + QJsonObject ruleObj; + ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); + return ruleObj; +} + +QJsonObject OsRule::toJson() +{ + QJsonObject ruleObj; + ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); + QJsonObject osObj; + { + osObj.insert("name", OpSys_toString(m_system)); + if(!m_version_regexp.isEmpty()) + { + osObj.insert("version", m_version_regexp); + } + } + ruleObj.insert("os", osObj); + return ruleObj; +} + diff --git a/api/logic/minecraft/Rule.h b/api/logic/minecraft/Rule.h new file mode 100644 index 00000000..c8bf6eaa --- /dev/null +++ b/api/logic/minecraft/Rule.h @@ -0,0 +1,101 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include "OpSys.h" + +class Library; +class Rule; + +enum RuleAction +{ + Allow, + Disallow, + Defer +}; + +QList> 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 create(RuleAction result, OpSys system, + QString version_regexp) + { + return std::shared_ptr(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 create(RuleAction result) + { + return std::shared_ptr(new ImplicitRule(result)); + } +}; diff --git a/api/logic/minecraft/VersionBuildError.h b/api/logic/minecraft/VersionBuildError.h new file mode 100644 index 00000000..fda453e5 --- /dev/null +++ b/api/logic/minecraft/VersionBuildError.h @@ -0,0 +1,58 @@ +#include "Exception.h" + +class VersionBuildError : public Exception +{ +public: + explicit VersionBuildError(QString cause) : Exception(cause) {} + virtual ~VersionBuildError() noexcept + { + } +}; + +/** + * the base version file was meant for a newer version of the vanilla launcher than we support + */ +class LauncherVersionError : public VersionBuildError +{ +public: + LauncherVersionError(int actual, int supported) + : VersionBuildError(QObject::tr( + "The base version file of this instance was meant for a newer (%1) " + "version of the vanilla launcher than this version of MultiMC supports (%2).") + .arg(actual) + .arg(supported)) {}; + virtual ~LauncherVersionError() noexcept + { + } +}; + +/** + * some patch was intended for a different version of minecraft + */ +class MinecraftVersionMismatch : public VersionBuildError +{ +public: + MinecraftVersionMismatch(QString fileId, QString mcVersion, QString parentMcVersion) + : VersionBuildError(QObject::tr("The patch %1 is for a different version of Minecraft " + "(%2) than that of the instance (%3).") + .arg(fileId) + .arg(mcVersion) + .arg(parentMcVersion)) {}; + virtual ~MinecraftVersionMismatch() noexcept + { + } +}; + +/** + * files required for the version are not (yet?) present + */ +class VersionIncomplete : public VersionBuildError +{ +public: + VersionIncomplete(QString missingPatch) + : VersionBuildError(QObject::tr("Version is incomplete: missing %1.") + .arg(missingPatch)) {}; + virtual ~VersionIncomplete() noexcept + { + } +}; diff --git a/api/logic/minecraft/VersionFile.cpp b/api/logic/minecraft/VersionFile.cpp new file mode 100644 index 00000000..573c4cb4 --- /dev/null +++ b/api/logic/minecraft/VersionFile.cpp @@ -0,0 +1,60 @@ +#include +#include + +#include + +#include "minecraft/VersionFile.h" +#include "minecraft/Library.h" +#include "minecraft/MinecraftProfile.h" +#include "minecraft/JarMod.h" +#include "ParseUtils.h" + +#include "VersionBuildError.h" +#include + +bool VersionFile::isMinecraftVersion() +{ + return fileId == "net.minecraft"; +} + +bool VersionFile::hasJarMods() +{ + return !jarMods.isEmpty(); +} + +void VersionFile::applyTo(MinecraftProfile *profile) +{ + auto theirVersion = profile->getMinecraftVersion(); + if (!theirVersion.isNull() && !dependsOnMinecraftVersion.isNull()) + { + if (QRegExp(dependsOnMinecraftVersion, Qt::CaseInsensitive, QRegExp::Wildcard).indexIn(theirVersion) == -1) + { + throw MinecraftVersionMismatch(fileId, dependsOnMinecraftVersion, theirVersion); + } + } + profile->applyMinecraftVersion(minecraftVersion); + profile->applyMainClass(mainClass); + profile->applyAppletClass(appletClass); + profile->applyMinecraftArguments(minecraftArguments); + if (isMinecraftVersion()) + { + profile->applyMinecraftVersionType(type); + } + profile->applyMinecraftAssets(mojangAssetIndex); + profile->applyTweakers(addTweakers); + + profile->applyJarMods(jarMods); + profile->applyTraits(traits); + + for (auto library : libraries) + { + profile->applyLibrary(library); + } + profile->applyProblemSeverity(getProblemSeverity()); + auto iter = mojangDownloads.begin(); + while(iter != mojangDownloads.end()) + { + profile->applyMojangDownload(iter.key(), iter.value()); + iter++; + } +} diff --git a/api/logic/minecraft/VersionFile.h b/api/logic/minecraft/VersionFile.h new file mode 100644 index 00000000..1b692f0f --- /dev/null +++ b/api/logic/minecraft/VersionFile.h @@ -0,0 +1,195 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include "minecraft/OpSys.h" +#include "minecraft/Rule.h" +#include "ProfilePatch.h" +#include "Library.h" +#include "JarMod.h" + +class MinecraftProfile; +class VersionFile; +struct MojangDownloadInfo; +struct MojangAssetIndexInfo; + +typedef std::shared_ptr VersionFilePtr; +class VersionFile : public ProfilePatch +{ + friend class MojangVersionFormat; + friend class OneSixVersionFormat; +public: /* methods */ + virtual void applyTo(MinecraftProfile *profile) override; + virtual bool isMinecraftVersion() override; + virtual bool hasJarMods() override; + virtual int getOrder() override + { + return order; + } + virtual void setOrder(int order) override + { + this->order = order; + } + virtual QList getJarMods() override + { + return jarMods; + } + virtual QString getID() override + { + return fileId; + } + virtual QString getName() override + { + return name; + } + virtual QString getVersion() override + { + return version; + } + virtual QString getFilename() override + { + return filename; + } + virtual QDateTime getReleaseDateTime() override + { + return m_releaseTime; + } + VersionSource getVersionSource() override + { + return Local; + } + + std::shared_ptr getVersionFile() override + { + return std::dynamic_pointer_cast(shared_from_this()); + } + + virtual bool isCustom() override + { + return !m_isVanilla; + }; + virtual bool isCustomizable() override + { + return m_isCustomizable; + } + virtual bool isRemovable() override + { + return m_isRemovable; + } + virtual bool isRevertible() override + { + return m_isRevertible; + } + virtual bool isMoveable() override + { + return m_isMovable; + } + virtual bool isEditable() override + { + return isCustom(); + } + virtual bool isVersionChangeable() override + { + return false; + } + + void setVanilla (bool state) + { + m_isVanilla = state; + } + void setRemovable (bool state) + { + m_isRemovable = state; + } + void setRevertible (bool state) + { + m_isRevertible = state; + } + void setCustomizable (bool state) + { + m_isCustomizable = state; + } + void setMovable (bool state) + { + m_isMovable = state; + } + + +public: /* data */ + /// MultiMC: order hint for this version file if no explicit order is set + int order = 0; + + // Flags for UI and version file manipulation in general + bool m_isVanilla = false; + bool m_isRemovable = false; + bool m_isRevertible = false; + bool m_isCustomizable = false; + bool m_isMovable = false; + + /// MultiMC: filename of the file this was loaded from + QString filename; + + /// MultiMC: human readable name of this package + QString name; + + /// MultiMC: package ID of this package + QString fileId; + + /// MultiMC: version of this package + QString version; + + /// MultiMC: dependency on a Minecraft version + QString dependsOnMinecraftVersion; + + /// Mojang: used to version the Mojang version format + int minimumLauncherVersion = -1; + + /// Mojang: version of Minecraft this is + QString minecraftVersion; + + /// Mojang: class to launch Minecraft with + QString mainClass; + + /// MultiMC: DEPRECATED class to launch legacy Minecraft with (ambed in a custom window) + QString appletClass; + + /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) + QString minecraftArguments; + + /// Mojang: type of the Minecraft version + QString type; + + /// Mojang: the time this version was actually released by Mojang + QDateTime m_releaseTime; + + /// Mojang: the time this version was last updated by Mojang + QDateTime m_updateTime; + + /// Mojang: DEPRECATED asset group to be used with Minecraft + QString assets; + + /// MultiMC: list of tweaker mod arguments for launchwrapper + QStringList addTweakers; + + /// Mojang: list of libraries to add to the version + QList libraries; + + /// MultiMC: list of attached traits of this version file - used to enable features + QSet traits; + + /// MultiMC: list of jar mods added to this version + QList jarMods; + +public: + // Mojang: list of 'downloads' - client jar, server jar, windows server exe, maybe more. + QMap > mojangDownloads; + + // Mojang: extended asset index download information + std::shared_ptr mojangAssetIndex; +}; + + diff --git a/api/logic/minecraft/VersionFilterData.cpp b/api/logic/minecraft/VersionFilterData.cpp new file mode 100644 index 00000000..0c4a6e3d --- /dev/null +++ b/api/logic/minecraft/VersionFilterData.cpp @@ -0,0 +1,75 @@ +#include "VersionFilterData.h" +#include "ParseUtils.h" + +VersionFilterData g_VersionFilterData = VersionFilterData(); + +VersionFilterData::VersionFilterData() +{ + // 1.3.* + auto libs13 = + QList{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}}; + + fmlLibsMapping["1.3.2"] = libs13; + + // 1.4.* + auto libs14 = QList{ + {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}, + {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", false}}; + + fmlLibsMapping["1.4"] = libs14; + fmlLibsMapping["1.4.1"] = libs14; + fmlLibsMapping["1.4.2"] = libs14; + fmlLibsMapping["1.4.3"] = libs14; + fmlLibsMapping["1.4.4"] = libs14; + fmlLibsMapping["1.4.5"] = libs14; + fmlLibsMapping["1.4.6"] = libs14; + fmlLibsMapping["1.4.7"] = libs14; + + // 1.5 + fmlLibsMapping["1.5"] = QList{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, + {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", false}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + + // 1.5.1 + fmlLibsMapping["1.5.1"] = QList{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, + {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", false}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + + // 1.5.2 + fmlLibsMapping["1.5.2"] = QList{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, + {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", false}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + + // don't use installers for those. + forgeInstallerBlacklist = QSet({"1.5.2"}); + // these won't show up in version lists because they are extremely bad and dangerous + legacyBlacklist = QSet({"rd-160052"}); + /* + * nothing older than this will be accepted from Mojang servers + * (these versions need to be tested by us first) + */ + legacyCutoffDate = timeFromS3Time("2013-06-25T15:08:56+02:00"); + lwjglWhitelist = + QSet{"net.java.jinput:jinput", "net.java.jinput:jinput-platform", + "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl", + "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform"}; + + // Version list magic + recommendedMinecraftVersion = "1.7.10"; +} diff --git a/api/logic/minecraft/VersionFilterData.h b/api/logic/minecraft/VersionFilterData.h new file mode 100644 index 00000000..f7d4ebe7 --- /dev/null +++ b/api/logic/minecraft/VersionFilterData.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include +#include + +#include "multimc_logic_export.h" + +struct FMLlib +{ + QString filename; + QString checksum; + bool ours; +}; + +struct VersionFilterData +{ + VersionFilterData(); + // mapping between minecraft versions and FML libraries required + QMap> fmlLibsMapping; + // set of minecraft versions for which using forge installers is blacklisted + QSet forgeInstallerBlacklist; + // set of 'legacy' versions that will not show up in the version lists. + QSet legacyBlacklist; + // no new versions below this date will be accepted from Mojang servers + QDateTime legacyCutoffDate; + // Libraries that belong to LWJGL + QSet lwjglWhitelist; + // Currently recommended minecraft version + QString recommendedMinecraftVersion; +}; +extern VersionFilterData MULTIMC_LOGIC_EXPORT g_VersionFilterData; diff --git a/api/logic/minecraft/World.cpp b/api/logic/minecraft/World.cpp new file mode 100644 index 00000000..6081a8ec --- /dev/null +++ b/api/logic/minecraft/World.cpp @@ -0,0 +1,385 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include "World.h" + +#include "GZip.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +std::unique_ptr parseLevelDat(QByteArray data) +{ + QByteArray output; + if(!GZip::unzip(data, output)) + { + return nullptr; + } + std::istringstream foo(std::string(output.constData(), output.size())); + auto pair = nbt::io::read_compound(foo); + + if(pair.first != "") + return nullptr; + + if(pair.second == nullptr) + return nullptr; + + return std::move(pair.second); +} + +QByteArray serializeLevelDat(nbt::tag_compound * levelInfo) +{ + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val( s.str().data(), (int) s.str().size() ); + return val; +} + +QString getLevelDatFromFS(const QFileInfo &file) +{ + QDir worldDir(file.filePath()); + if(!file.isDir() || !worldDir.exists("level.dat")) + { + return QString(); + } + return worldDir.absoluteFilePath("level.dat"); +} + +QByteArray getLevelDatDataFromFS(const QFileInfo &file) +{ + auto fullFilePath = getLevelDatFromFS(file); + if(fullFilePath.isNull()) + { + return QByteArray(); + } + QFile f(fullFilePath); + if(!f.open(QIODevice::ReadOnly)) + { + return QByteArray(); + } + return f.readAll(); +} + +bool putLevelDatDataToFS(const QFileInfo &file, QByteArray & data) +{ + auto fullFilePath = getLevelDatFromFS(file); + if(fullFilePath.isNull()) + { + return false; + } + QSaveFile f(fullFilePath); + if(!f.open(QIODevice::WriteOnly)) + { + return false; + } + QByteArray compressed; + if(!GZip::zip(data, compressed)) + { + return false; + } + if(f.write(compressed) != compressed.size()) + { + f.cancelWriting(); + return false; + } + return f.commit(); +} + +World::World(const QFileInfo &file) +{ + repath(file); +} + +void World::repath(const QFileInfo &file) +{ + m_containerFile = file; + m_folderName = file.fileName(); + if(file.isFile() && file.suffix() == "zip") + { + readFromZip(file); + } + else if(file.isDir()) + { + readFromFS(file); + } +} + +void World::readFromFS(const QFileInfo &file) +{ + auto bytes = getLevelDatDataFromFS(file); + if(bytes.isEmpty()) + { + is_valid = false; + return; + } + loadFromLevelDat(bytes); + levelDatTime = file.lastModified(); +} + +void World::readFromZip(const QFileInfo &file) +{ + QuaZip zip(file.absoluteFilePath()); + is_valid = zip.open(QuaZip::mdUnzip); + if (!is_valid) + { + return; + } + auto location = MMCZip::findFileInZip(&zip, "level.dat"); + is_valid = !location.isEmpty(); + if (!is_valid) + { + return; + } + m_containerOffsetPath = location; + QuaZipFile zippedFile(&zip); + // read the install profile + is_valid = zip.setCurrentFile(location + "level.dat"); + if (!is_valid) + { + return; + } + is_valid = zippedFile.open(QIODevice::ReadOnly); + QuaZipFileInfo64 levelDatInfo; + zippedFile.getFileInfo(&levelDatInfo); + auto modTime = levelDatInfo.getNTFSmTime(); + if(!modTime.isValid()) + { + modTime = levelDatInfo.dateTime; + } + levelDatTime = modTime; + if (!is_valid) + { + return; + } + loadFromLevelDat(zippedFile.readAll()); + zippedFile.close(); +} + +bool World::install(const QString &to, const QString &name) +{ + auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to)); + if(!FS::ensureFolderPathExists(finalPath)) + { + return false; + } + bool ok = false; + if(m_containerFile.isFile()) + { + QuaZip zip(m_containerFile.absoluteFilePath()); + if (!zip.open(QuaZip::mdUnzip)) + { + return false; + } + ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath).isEmpty(); + } + else if(m_containerFile.isDir()) + { + QString from = m_containerFile.filePath(); + ok = FS::copy(from, finalPath)(); + } + + if(ok && !name.isEmpty() && m_actualName != name) + { + World newWorld(finalPath); + if(newWorld.isValid()) + { + newWorld.rename(name); + } + } + return ok; +} + +bool World::rename(const QString &newName) +{ + if(m_containerFile.isFile()) + { + return false; + } + + auto data = getLevelDatDataFromFS(m_containerFile); + if(data.isEmpty()) + { + return false; + } + + auto worldData = parseLevelDat(data); + if(!worldData) + { + return false; + } + auto &val = worldData->at("Data"); + if(val.get_type() != nbt::tag_type::Compound) + { + return false; + } + auto &dataCompound = val.as(); + dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data())); + data = serializeLevelDat(worldData.get()); + + putLevelDatDataToFS(m_containerFile, data); + + m_actualName = newName; + + QDir parentDir(m_containerFile.absoluteFilePath()); + parentDir.cdUp(); + QFile container(m_containerFile.absoluteFilePath()); + auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath()); + container.rename(parentDir.absoluteFilePath(dirName)); + + return true; +} + +static QString read_string (nbt::value& parent, const char * name, const QString & fallback = QString()) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::String) + { + return fallback; + } + auto & tag_str = namedValue.as(); + return QString::fromStdString(tag_str.get()); + } + catch(std::out_of_range e) + { + // fallback for old world formats + qWarning() << "String NBT tag" << name << "could not be found. Defaulting to" << fallback; + return fallback; + } + catch(std::bad_cast e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to string. Defaulting to" << fallback; + return fallback; + } +}; + +static int64_t read_long (nbt::value& parent, const char * name, const int64_t & fallback = 0) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::Long) + { + return fallback; + } + auto & tag_str = namedValue.as(); + return tag_str.get(); + } + catch(std::out_of_range e) + { + // fallback for old world formats + qWarning() << "Long NBT tag" << name << "could not be found. Defaulting to" << fallback; + return fallback; + } + catch(std::bad_cast e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to long. Defaulting to" << fallback; + return fallback; + } +}; + +void World::loadFromLevelDat(QByteArray data) +{ + try + { + auto levelData = parseLevelDat(data); + if(!levelData) + { + is_valid = false; + return; + } + + auto &val = levelData->at("Data"); + is_valid = val.get_type() == nbt::tag_type::Compound; + if(!is_valid) + return; + + m_actualName = read_string(val, "LevelName", m_folderName); + + + int64_t temp = read_long(val, "LastPlayed", 0); + if(temp == 0) + { + m_lastPlayed = levelDatTime; + } + else + { + m_lastPlayed = QDateTime::fromMSecsSinceEpoch(temp); + } + + m_randomSeed = read_long(val, "RandomSeed", 0); + + qDebug() << "World Name:" << m_actualName; + qDebug() << "Last Played:" << m_lastPlayed.toString(); + qDebug() << "Seed:" << m_randomSeed; + } + catch (nbt::io::input_error e) + { + qWarning() << "Unable to load" << m_folderName << ":" << e.what(); + is_valid = false; + return; + } +} + +bool World::replace(World &with) +{ + if (!destroy()) + return false; + bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())(); + if (success) + { + m_folderName = with.m_folderName; + m_containerFile.refresh(); + } + return success; +} + +bool World::destroy() +{ + if(!is_valid) return false; + if (m_containerFile.isDir()) + { + QDir d(m_containerFile.filePath()); + return d.removeRecursively(); + } + else if(m_containerFile.isFile()) + { + QFile file(m_containerFile.absoluteFilePath()); + return file.remove(); + } + return true; +} + +bool World::operator==(const World &other) const +{ + return is_valid == other.is_valid && folderName() == other.folderName(); +} +bool World::strongCompare(const World &other) const +{ + return is_valid == other.is_valid && folderName() == other.folderName(); +} diff --git a/api/logic/minecraft/World.h b/api/logic/minecraft/World.h new file mode 100644 index 00000000..3cde5ea4 --- /dev/null +++ b/api/logic/minecraft/World.h @@ -0,0 +1,83 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +#include "multimc_logic_export.h" + +class MULTIMC_LOGIC_EXPORT World +{ +public: + World(const QFileInfo &file); + QString folderName() const + { + return m_folderName; + } + QString name() const + { + return m_actualName; + } + QDateTime lastPlayed() const + { + return m_lastPlayed; + } + int64_t seed() const + { + return m_randomSeed; + } + bool isValid() const + { + return is_valid; + } + bool isOnFS() const + { + return m_containerFile.isDir(); + } + QFileInfo container() const + { + return m_containerFile; + } + // delete all the files of this world + bool destroy(); + // replace this world with a copy of the other + bool replace(World &with); + // change the world's filesystem path (used by world lists for *MAGIC* purposes) + void repath(const QFileInfo &file); + + bool rename(const QString &to); + bool install(const QString &to, const QString &name= QString()); + + // WEAK compare operator - used for replacing worlds + bool operator==(const World &other) const; + bool strongCompare(const World &other) const; + +private: + void readFromZip(const QFileInfo &file); + void readFromFS(const QFileInfo &file); + void loadFromLevelDat(QByteArray data); + +protected: + + QFileInfo m_containerFile; + QString m_containerOffsetPath; + QString m_folderName; + QString m_actualName; + QDateTime levelDatTime; + QDateTime m_lastPlayed; + int64_t m_randomSeed = 0; + bool is_valid = false; +}; diff --git a/api/logic/minecraft/WorldList.cpp b/api/logic/minecraft/WorldList.cpp new file mode 100644 index 00000000..42c8a3e6 --- /dev/null +++ b/api/logic/minecraft/WorldList.cpp @@ -0,0 +1,355 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WorldList.h" +#include +#include +#include +#include +#include +#include +#include + +WorldList::WorldList(const QString &dir) + : QAbstractListModel(), m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | + QDir::NoSymLinks); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + is_watching = false; + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, + SLOT(directoryChanged(QString))); +} + +void WorldList::startWatching() +{ + update(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void WorldList::stopWatching() +{ + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool WorldList::update() +{ + if (!isValid()) + return false; + + QList newWorlds; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) + { + if(!entry.isDir()) + continue; + + World w(entry); + if(w.isValid()) + { + newWorlds.append(w); + } + } + beginResetModel(); + worlds.swap(newWorlds); + endResetModel(); + return true; +} + +void WorldList::directoryChanged(QString path) +{ + update(); +} + +bool WorldList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool WorldList::deleteWorld(int index) +{ + if (index >= worlds.size() || index < 0) + return false; + World &m = worlds[index]; + if (m.destroy()) + { + beginRemoveRows(QModelIndex(), index, index); + worlds.removeAt(index); + endRemoveRows(); + emit changed(); + return true; + } + return false; +} + +bool WorldList::deleteWorlds(int first, int last) +{ + for (int i = first; i <= last; i++) + { + World &m = worlds[i]; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); + endRemoveRows(); + emit changed(); + return true; +} + +int WorldList::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +QVariant WorldList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= worlds.size()) + return QVariant(); + + auto & world = worlds[row]; + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return world.name(); + + case LastPlayedColumn: + return world.lastPlayed(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + { + return world.folderName(); + } + case ObjectRole: + { + return QVariant::fromValue((void *)&world); + } + case FolderRole: + { + return QDir::toNativeSeparators(dir().absoluteFilePath(world.folderName())); + } + case SeedRole: + { + return qVariantFromValue(world.seed()); + } + case NameRole: + { + return world.name(); + } + case LastPlayedRole: + { + return world.lastPlayed(); + } + default: + return QVariant(); + } +} + +QVariant WorldList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case NameColumn: + return tr("Name"); + case LastPlayedColumn: + return tr("Last Played"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return tr("The name of the world."); + case LastPlayedColumn: + return tr("Date and time the world was last played."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +QStringList WorldList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +class WorldMimeData : public QMimeData +{ +Q_OBJECT + +public: + WorldMimeData(QList worlds) + { + m_worlds = worlds; + + } + QStringList formats() const + { + return QMimeData::formats() << "text/uri-list"; + } + +protected: + QVariant retrieveData(const QString &mimetype, QVariant::Type type) const + { + QList 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(this)->setUrls(urls); + return QMimeData::retrieveData(mimetype, type); + } +private: + QList m_worlds; +}; + +QMimeData *WorldList::mimeData(const QModelIndexList &indexes) const +{ + if (indexes.size() == 0) + return new QMimeData(); + + QList worlds; + for(auto idx : indexes) + { + if(idx.column() != 0) + continue; + int row = idx.row(); + if (row < 0 || row >= this->worlds.size()) + continue; + worlds.append(this->worlds[row]); + } + if(!worlds.size()) + { + return new QMimeData(); + } + return new WorldMimeData(worlds); +} + +Qt::ItemFlags WorldList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | + defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions WorldList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +Qt::DropActions WorldList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +void WorldList::installWorld(QFileInfo filename) +{ + qDebug() << "installing: " << filename.absoluteFilePath(); + World w(filename); + if(!w.isValid()) + { + return; + } + w.install(m_dir.absolutePath()); +} + +bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + // files dropped from outside? + if (data->hasUrls()) + { + bool was_watching = is_watching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + + QFileInfo worldInfo(filename); + + if(!m_dir.entryInfoList().contains(worldInfo)) + { + installWorld(worldInfo); + } + } + if (was_watching) + startWatching(); + return true; + } + return false; +} + +#include "WorldList.moc" diff --git a/api/logic/minecraft/WorldList.h b/api/logic/minecraft/WorldList.h new file mode 100644 index 00000000..34b30e9c --- /dev/null +++ b/api/logic/minecraft/WorldList.h @@ -0,0 +1,125 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "minecraft/World.h" + +#include "multimc_logic_export.h" + +class QFileSystemWatcher; + +class MULTIMC_LOGIC_EXPORT WorldList : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + NameColumn, + LastPlayedColumn + }; + + enum Roles + { + ObjectRole = Qt::UserRole + 1, + FolderRole, + SeedRole, + NameRole, + LastPlayedRole + }; + + WorldList(const QString &dir); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return size(); + }; + virtual QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex &parent) const; + + size_t size() const + { + return worlds.size(); + }; + bool empty() const + { + return size() == 0; + } + World &operator[](size_t index) + { + return worlds[index]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /// Install a world from location + void installWorld(QFileInfo filename); + + /// Deletes the mod at the given index. + virtual bool deleteWorld(int index); + + /// Deletes all the selected mods + virtual bool deleteWorlds(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + /// get data for drag action + virtual QMimeData *mimeData(const QModelIndexList &indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); + /// what drag actions do we support? + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() const + { + return m_dir; + } + + const QList &allWorlds() const + { + return worlds; + } + +private slots: + void directoryChanged(QString path); + +signals: + void changed(); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching; + QDir m_dir; + QList worlds; +}; diff --git a/api/logic/minecraft/auth/AuthSession.cpp b/api/logic/minecraft/auth/AuthSession.cpp new file mode 100644 index 00000000..8758bfbd --- /dev/null +++ b/api/logic/minecraft/auth/AuthSession.cpp @@ -0,0 +1,30 @@ +#include "AuthSession.h" +#include +#include +#include +#include + +QString AuthSession::serializeUserProperties() +{ + QJsonObject userAttrs; + for (auto key : u.properties.keys()) + { + auto array = QJsonArray::fromStringList(u.properties.values(key)); + userAttrs.insert(key, array); + } + QJsonDocument value(userAttrs); + return value.toJson(QJsonDocument::Compact); + +} + +bool AuthSession::MakeOffline(QString offline_playername) +{ + if (status != PlayableOffline && status != PlayableOnline) + { + return false; + } + session = "-"; + player_name = offline_playername; + status = PlayableOffline; + return true; +} diff --git a/api/logic/minecraft/auth/AuthSession.h b/api/logic/minecraft/auth/AuthSession.h new file mode 100644 index 00000000..dede90a9 --- /dev/null +++ b/api/logic/minecraft/auth/AuthSession.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +#include "multimc_logic_export.h" + +struct User +{ + QString id; + QMultiMap properties; +}; + +struct MULTIMC_LOGIC_EXPORT AuthSession +{ + bool MakeOffline(QString offline_playername); + + QString serializeUserProperties(); + + enum Status + { + Undetermined, + RequiresPassword, + PlayableOffline, + PlayableOnline + } status = Undetermined; + + User u; + + // client token + QString client_token; + // account user name + QString username; + // combined session ID + QString session; + // volatile auth token + QString access_token; + // profile name + QString player_name; + // profile ID + QString uuid; + // 'legacy' or 'mojang', depending on account type + QString user_type; + // Did the auth server reply? + bool auth_server_online = false; + // Did the user request online mode? + bool wants_online = true; +}; + +typedef std::shared_ptr AuthSessionPtr; diff --git a/api/logic/minecraft/auth/MojangAccount.cpp b/api/logic/minecraft/auth/MojangAccount.cpp new file mode 100644 index 00000000..69a24c09 --- /dev/null +++ b/api/logic/minecraft/auth/MojangAccount.cpp @@ -0,0 +1,278 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 +#include +#include +#include +#include +#include + +#include + +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 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 MojangAccount::login(AuthSessionPtr session, + QString password) +{ + Q_ASSERT(m_currentTask.get() == nullptr); + + // take care of the true offline status + if (accountStatus() == NotVerified && password.isEmpty()) + { + if (session) + { + session->status = AuthSession::RequiresPassword; + fillSession(session); + } + return nullptr; + } + + if (password.isEmpty()) + { + m_currentTask.reset(new RefreshTask(this)); + } + else + { + m_currentTask.reset(new AuthenticateTask(this, password)); + } + m_currentTask->assignSession(session); + + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + return m_currentTask; +} + +void MojangAccount::authSucceeded() +{ + auto session = m_currentTask->getAssignedSession(); + if (session) + { + session->status = + session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline; + fillSession(session); + session->auth_server_online = true; + } + m_currentTask.reset(); + emit changed(); +} + +void MojangAccount::authFailed(QString reason) +{ + auto session = m_currentTask->getAssignedSession(); + // This is emitted when the yggdrasil tasks time out or are cancelled. + // -> we treat the error as no-op + if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT) + { + if (session) + { + session->status = accountStatus() == Verified ? AuthSession::PlayableOffline + : AuthSession::RequiresPassword; + session->auth_server_online = false; + fillSession(session); + } + } + else + { + m_accessToken = QString(); + emit changed(); + if (session) + { + session->status = AuthSession::RequiresPassword; + session->auth_server_online = true; + fillSession(session); + } + } + m_currentTask.reset(); +} + +void MojangAccount::fillSession(AuthSessionPtr session) +{ + // the user name. you have to have an user name + session->username = m_username; + // volatile auth token + session->access_token = m_accessToken; + // the semi-permanent client token + session->client_token = m_clientToken; + if (currentProfile()) + { + // profile name + session->player_name = currentProfile()->name; + // profile ID + session->uuid = currentProfile()->id; + // 'legacy' or 'mojang', depending on account type + session->user_type = currentProfile()->legacy ? "legacy" : "mojang"; + if (!session->access_token.isEmpty()) + { + session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id; + } + else + { + session->session = "-"; + } + } + else + { + session->player_name = "Player"; + session->session = "-"; + } + session->u = user(); +} diff --git a/api/logic/minecraft/auth/MojangAccount.h b/api/logic/minecraft/auth/MojangAccount.h new file mode 100644 index 00000000..2de0c19c --- /dev/null +++ b/api/logic/minecraft/auth/MojangAccount.h @@ -0,0 +1,173 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include "AuthSession.h" + +#include "multimc_logic_export.h" + +class Task; +class YggdrasilTask; +class MojangAccount; + +typedef std::shared_ptr MojangAccountPtr; +Q_DECLARE_METATYPE(MojangAccountPtr) + +/** + * A profile within someone's Mojang account. + * + * Currently, the profile system has not been implemented by Mojang yet, + * but we might as well add some things for it in MultiMC right now so + * we don't have to rip the code to pieces to add it later. + */ +struct AccountProfile +{ + QString id; + QString name; + bool legacy; +}; + +enum AccountStatus +{ + NotVerified, + Verified +}; + +/** + * Object that stores information about a certain Mojang account. + * + * Said information may include things such as that account's username, client token, and access + * token if the user chose to stay logged in. + */ +class MULTIMC_LOGIC_EXPORT MojangAccount : public QObject +{ + Q_OBJECT +public: /* construction */ + //! Do not copy accounts. ever. + explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete; + + //! Default constructor + explicit MojangAccount(QObject *parent = 0) : QObject(parent) {}; + + //! Creates an empty account for the specified user name. + static MojangAccountPtr createFromUsername(const QString &username); + + //! Loads a MojangAccount from the given JSON object. + static MojangAccountPtr loadFromJson(const QJsonObject &json); + + //! Saves a MojangAccount to a JSON object and returns it. + QJsonObject saveToJson() const; + +public: /* manipulation */ + /** + * Sets the currently selected profile to the profile with the given ID string. + * If profileId is not in the list of available profiles, the function will simply return + * false. + */ + bool setCurrentProfile(const QString &profileId); + + /** + * Attempt to login. Empty password means we use the token. + * If the attempt fails because we already are performing some task, it returns false. + */ + std::shared_ptr login(AuthSessionPtr session, + QString password = QString()); + +public: /* queries */ + const QString &username() const + { + return m_username; + } + + const QString &clientToken() const + { + return m_clientToken; + } + + const QString &accessToken() const + { + return m_accessToken; + } + + const QList &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 m_profiles; + + // the user structure, whatever it is. + User m_user; + + // current task we are executing here + std::shared_ptr m_currentTask; + +private +slots: + void authSucceeded(); + void authFailed(QString reason); + +private: + void fillSession(AuthSessionPtr session); + +public: + friend class YggdrasilTask; + friend class AuthenticateTask; + friend class ValidateTask; + friend class RefreshTask; +}; diff --git a/api/logic/minecraft/auth/MojangAccountList.cpp b/api/logic/minecraft/auth/MojangAccountList.cpp new file mode 100644 index 00000000..26cbc81a --- /dev/null +++ b/api/logic/minecraft/auth/MojangAccountList.cpp @@ -0,0 +1,427 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MojangAccountList.h" +#include "MojangAccount.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#define ACCOUNT_LIST_FORMAT_VERSION 2 + +MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent) +{ +} + +MojangAccountPtr MojangAccountList::findAccount(const QString &username) const +{ + for (int i = 0; i < count(); i++) + { + MojangAccountPtr account = at(i); + if (account->username() == username) + return account; + } + return nullptr; +} + +const MojangAccountPtr MojangAccountList::at(int i) const +{ + return MojangAccountPtr(m_accounts.at(i)); +} + +void MojangAccountList::addAccount(const MojangAccountPtr account) +{ + beginResetModel(); + connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); + m_accounts.append(account); + endResetModel(); + onListChanged(); +} + +void MojangAccountList::removeAccount(const QString &username) +{ + beginResetModel(); + for (auto account : m_accounts) + { + if (account->username() == username) + { + m_accounts.removeOne(account); + return; + } + } + endResetModel(); + onListChanged(); +} + +void MojangAccountList::removeAccount(QModelIndex index) +{ + beginResetModel(); + m_accounts.removeAt(index.row()); + endResetModel(); + onListChanged(); +} + +MojangAccountPtr MojangAccountList::activeAccount() const +{ + return m_activeAccount; +} + +void MojangAccountList::setActiveAccount(const QString &username) +{ + beginResetModel(); + if (username.isEmpty()) + { + m_activeAccount = nullptr; + } + else + { + for (MojangAccountPtr account : m_accounts) + { + if (account->username() == username) + m_activeAccount = account; + } + } + endResetModel(); + onActiveChanged(); +} + +void MojangAccountList::accountChanged() +{ + // the list changed. there is no doubt. + onListChanged(); +} + +void MojangAccountList::onListChanged() +{ + if (m_autosave) + // TODO: Alert the user if this fails. + saveList(); + + emit listChanged(); +} + +void MojangAccountList::onActiveChanged() +{ + if (m_autosave) + saveList(); + + emit activeAccountChanged(); +} + +int MojangAccountList::count() const +{ + return m_accounts.count(); +} + +QVariant MojangAccountList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + MojangAccountPtr account = at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case NameColumn: + return account->username(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return account->username(); + + case PointerRole: + return qVariantFromValue(account); + + case Qt::CheckStateRole: + switch (index.column()) + { + case ActiveColumn: + return account == m_activeAccount; + } + + default: + return QVariant(); + } +} + +QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return tr("Active?"); + + case NameColumn: + return tr("Name"); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return tr("The name of the version."); + + default: + return QVariant(); + } + + default: + return QVariant(); + } +} + +int MojangAccountList::rowCount(const QModelIndex &parent) const +{ + // Return count + return count(); +} + +int MojangAccountList::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return Qt::NoItemFlags; + } + + return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if(role == Qt::CheckStateRole) + { + if(value == Qt::Checked) + { + MojangAccountPtr account = this->at(index.row()); + this->setActiveAccount(account->username()); + } + } + + emit dataChanged(index, index); + return true; +} + +void MojangAccountList::updateListData(QList versions) +{ + beginResetModel(); + m_accounts = versions; + endResetModel(); +} + +bool MojangAccountList::loadList(const QString &filePath) +{ + QString path = filePath; + if (path.isEmpty()) + path = m_listFilePath; + if (path.isEmpty()) + { + qCritical() << "Can't load Mojang account list. No file path given and no default set."; + return false; + } + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8(); + return false; + } + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse account list file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid account list JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Make sure the format version matches. + if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION) + { + QString newName = "accounts-old.json"; + qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to" + << newName; + + // Attempt to rename the old version. + file.rename(newName); + return false; + } + + // Now, load the accounts array. + beginResetModel(); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MojangAccountPtr account = MojangAccount::loadFromJson(accountObj); + if (account.get() != nullptr) + { + connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); + m_accounts.append(account); + } + else + { + qWarning() << "Failed to load an account."; + } + } + // Load the active account. + m_activeAccount = findAccount(root.value("activeAccount").toString("")); + endResetModel(); + return true; +} + +bool MojangAccountList::saveList(const QString &filePath) +{ + QString path(filePath); + if (path.isEmpty()) + path = m_listFilePath; + if (path.isEmpty()) + { + qCritical() << "Can't save Mojang account list. No file path given and no default set."; + return false; + } + + // make sure the parent folder exists + if(!FS::ensureFilePathExists(path)) + return false; + + // make sure the file wasn't overwritten with a folder before (fixes a bug) + QFileInfo finfo(path); + if(finfo.isDir()) + { + QDir badDir(path); + badDir.removeRecursively(); + } + + qDebug() << "Writing account list to" << path; + + qDebug() << "Building JSON data structure."; + // Build the JSON document to write to the list file. + QJsonObject root; + + root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION); + + // Build a list of accounts. + qDebug() << "Building account array."; + QJsonArray accounts; + for (MojangAccountPtr account : m_accounts) + { + QJsonObject accountObj = account->saveToJson(); + accounts.append(accountObj); + } + + // Insert the account list into the root object. + root.insert("accounts", accounts); + + if(m_activeAccount) + { + // Save the active account. + root.insert("activeAccount", m_activeAccount->username()); + } + + // Create a JSON document object to convert our JSON to bytes. + QJsonDocument doc(root); + + // Now that we're done building the JSON object, we can write it to the file. + qDebug() << "Writing account list to file."; + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::WriteOnly)) + { + qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8(); + return false; + } + + // Write the JSON to the file. + file.write(doc.toJson()); + file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser); + file.close(); + + qDebug() << "Saved account list to" << path; + + return true; +} + +void MojangAccountList::setListFilePath(QString path, bool autosave) +{ + m_listFilePath = path; + m_autosave = autosave; +} + +bool MojangAccountList::anyAccountIsValid() +{ + for(auto account:m_accounts) + { + if(account->accountStatus() != NotVerified) + return true; + } + return false; +} diff --git a/api/logic/minecraft/auth/MojangAccountList.h b/api/logic/minecraft/auth/MojangAccountList.h new file mode 100644 index 00000000..c40fa6a3 --- /dev/null +++ b/api/logic/minecraft/auth/MojangAccountList.h @@ -0,0 +1,201 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "MojangAccount.h" + +#include +#include +#include +#include + +#include "multimc_logic_export.h" + +/*! + * \brief List of available Mojang accounts. + * This should be loaded in the background by MultiMC on startup. + * + * This class also inherits from QAbstractListModel. Methods from that + * class determine how this list shows up in a list view. Said methods + * all have a default implementation, but they can be overridden by subclasses to + * change the behavior of the list. + */ +class MULTIMC_LOGIC_EXPORT MojangAccountList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + PointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + // TODO: Add icon column. + + // First column - Active? + ActiveColumn = 0, + + // Second column - Name + NameColumn, + }; + + explicit MojangAccountList(QObject *parent = 0); + + //! Gets the account at the given index. + virtual const MojangAccountPtr at(int i) const; + + //! Returns the number of accounts in the list. + virtual int count() const; + + //////// List Model Functions //////// + virtual QVariant data(const QModelIndex &index, int role) const; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; + virtual int rowCount(const QModelIndex &parent) const; + virtual int columnCount(const QModelIndex &parent) const; + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role); + + /*! + * Adds a the given Mojang account to the account list. + */ + virtual void addAccount(const MojangAccountPtr account); + + /*! + * Removes the mojang account with the given username from the account list. + */ + virtual void removeAccount(const QString &username); + + /*! + * Removes the account at the given QModelIndex. + */ + virtual void removeAccount(QModelIndex index); + + /*! + * \brief Finds an account by its username. + * \param The username of the account to find. + * \return A const pointer to the account with the given username. NULL if + * one doesn't exist. + */ + virtual MojangAccountPtr findAccount(const QString &username) const; + + /*! + * Sets the default path to save the list file to. + * If autosave is true, this list will automatically save to the given path whenever it changes. + * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately + * after calling this function to ensure an autosaved change doesn't overwrite the list you intended + * to load. + */ + virtual void setListFilePath(QString path, bool autosave = false); + + /*! + * \brief Loads the account list from the given file path. + * If the given file is an empty string (default), will load from the default account list file. + * \return True if successful, otherwise false. + */ + virtual bool loadList(const QString &file = ""); + + /*! + * \brief Saves the account list to the given file. + * If the given file is an empty string (default), will save from the default account list file. + * \return True if successful, otherwise false. + */ + virtual bool saveList(const QString &file = ""); + + /*! + * \brief Gets a pointer to the account that the user has selected as their "active" account. + * Which account is active can be overridden on a per-instance basis, but this will return the one that + * is set as active globally. + * \return The currently active MojangAccount. If there isn't an active account, returns a null pointer. + */ + virtual MojangAccountPtr activeAccount() const; + + /*! + * Sets the given account as the current active account. + * If the username given is an empty string, sets the active account to nothing. + */ + virtual void setActiveAccount(const QString &username); + + /*! + * Returns true if any of the account is at least Validated + */ + bool anyAccountIsValid(); + +signals: + /*! + * Signal emitted to indicate that the account list has changed. + * This will also fire if the value of an element in the list changes (will be implemented + * later). + */ + void listChanged(); + + /*! + * Signal emitted to indicate that the active account has changed. + */ + void activeAccountChanged(); + +public +slots: + /** + * This is called when one of the accounts changes and the list needs to be updated + */ + void accountChanged(); + +protected: + /*! + * Called whenever the list changes. + * This emits the listChanged() signal and autosaves the list (if autosave is enabled). + */ + void onListChanged(); + + /*! + * Called whenever the active account changes. + * Emits the activeAccountChanged() signal and autosaves the list if enabled. + */ + void onActiveChanged(); + + QList 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 versions); +}; diff --git a/api/logic/minecraft/auth/YggdrasilTask.cpp b/api/logic/minecraft/auth/YggdrasilTask.cpp new file mode 100644 index 00000000..c6971c9f --- /dev/null +++ b/api/logic/minecraft/auth/YggdrasilTask.cpp @@ -0,0 +1,255 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "YggdrasilTask.h" +#include "MojangAccount.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent) + : Task(parent), m_account(account) +{ + changeState(STATE_CREATED); +} + +void YggdrasilTask::executeTask() +{ + changeState(STATE_SENDING_REQUEST); + + // Get the content of the request we're going to send to the server. + QJsonDocument doc(getRequestContent()); + + auto worker = ENV.qnam(); + QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint()); + QNetworkRequest netRequest(reqUrl); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QByteArray requestData = doc.toJson(); + m_netReply = worker->post(netRequest, requestData); + connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply); + connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors); + timeout_keeper.setSingleShot(true); + timeout_keeper.start(timeout_max); + counter.setSingleShot(false); + counter.start(time_step); + progress(0, timeout_max); + connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout); + connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat); +} + +void YggdrasilTask::refreshTimers(qint64, qint64) +{ + timeout_keeper.stop(); + timeout_keeper.start(timeout_max); + progress(count = 0, timeout_max); +} +void YggdrasilTask::heartbeat() +{ + count += time_step; + progress(count, timeout_max); +} + +bool YggdrasilTask::abort() +{ + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = YggdrasilTask::BY_USER; + m_netReply->abort(); + return true; +} + +void YggdrasilTask::abortByTimeout() +{ + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = YggdrasilTask::BY_TIMEOUT; + m_netReply->abort(); +} + +void YggdrasilTask::sslErrors(QList 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("SSL Handshake failed.
There might be a few causes for it:
" + "
    " + "
  • You use Windows XP and need to update " + "your root certificates
  • " + "
  • Some device on your network is interfering with SSL traffic. In that case, " + "you have bigger worries than Minecraft not starting.
  • " + "
  • Possibly something else. Check the MultiMC log file for details
  • " + "
")); + return; + // used for invalid credentials and similar errors. Fall through. + case QNetworkReply::ContentOperationNotPermittedError: + break; + default: + changeState(STATE_FAILED_SOFT, + tr("Authentication operation failed due to a network error: %1 (%2)") + .arg(m_netReply->errorString()).arg(m_netReply->error())); + return; + } + + // Try to parse the response regardless of the response code. + // Sometimes the auth server will give more information and an error code. + QJsonParseError jsonError; + QByteArray replyData = m_netReply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); + // Check the response code. + int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) + { + // If the response code was 200, then there shouldn't be an error. Make sure + // anyways. + // Also, sometimes an empty reply indicates success. If there was no data received, + // pass an empty json object to the processResponse function. + if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) + { + processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); + return; + } + else + { + changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response " + "JSON response: %1 at offset %2.") + .arg(jsonError.errorString()) + .arg(jsonError.offset)); + qCritical() << replyData; + } + return; + } + + // If the response code was not 200, then Yggdrasil may have given us information + // about the error. + // If we can parse the response, then get information from it. Otherwise just say + // there was an unknown error. + if (jsonError.error == QJsonParseError::NoError) + { + // We were able to parse the server's response. Woo! + // Call processError. If a subclass has overridden it then they'll handle their + // stuff there. + qDebug() << "The request failed, but the server gave us an error message. " + "Processing error."; + processError(doc.object()); + } + else + { + // The server didn't say anything regarding the error. Give the user an unknown + // error. + qDebug() + << "The request failed and the server gave no error message. Unknown error."; + changeState(STATE_FAILED_SOFT, + tr("An unknown error occurred when trying to communicate with the " + "authentication server: %1").arg(m_netReply->errorString())); + } +} + +void YggdrasilTask::processError(QJsonObject responseData) +{ + QJsonValue errorVal = responseData.value("error"); + QJsonValue errorMessageValue = responseData.value("errorMessage"); + QJsonValue causeVal = responseData.value("cause"); + + if (errorVal.isString() && errorMessageValue.isString()) + { + m_error = std::shared_ptr(new Error{ + errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")}); + changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose); + } + else + { + // Error is not in standard format. Don't set m_error and return unknown error. + changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); + } +} + +QString YggdrasilTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_CREATED: + return "Waiting..."; + case STATE_SENDING_REQUEST: + return tr("Sending request to auth servers..."); + case STATE_PROCESSING_RESPONSE: + return tr("Processing response from servers..."); + case STATE_SUCCEEDED: + return tr("Authentication task succeeded."); + case STATE_FAILED_SOFT: + return tr("Failed to contact the authentication server."); + case STATE_FAILED_HARD: + return tr("Failed to authenticate."); + default: + return tr("..."); + } +} + +void YggdrasilTask::changeState(YggdrasilTask::State newState, QString reason) +{ + m_state = newState; + setStatus(getStateMessage()); + if (newState == STATE_SUCCEEDED) + { + emitSucceeded(); + } + else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT) + { + emitFailed(reason); + } +} + +YggdrasilTask::State YggdrasilTask::state() +{ + return m_state; +} diff --git a/api/logic/minecraft/auth/YggdrasilTask.h b/api/logic/minecraft/auth/YggdrasilTask.h new file mode 100644 index 00000000..c84cfc06 --- /dev/null +++ b/api/logic/minecraft/auth/YggdrasilTask.h @@ -0,0 +1,150 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "MojangAccount.h" + +class QNetworkReply; + +/** + * A Yggdrasil task is a task that performs an operation on a given mojang account. + */ +class YggdrasilTask : public Task +{ + Q_OBJECT +public: + explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0); + + /** + * assign a session to this task. the session will be filled with required infomration + * upon completion + */ + void assignSession(AuthSessionPtr session) + { + m_session = session; + } + + /// get the assigned session for filling with information. + AuthSessionPtr getAssignedSession() + { + return m_session; + } + + /** + * Class describing a Yggdrasil error response. + */ + struct Error + { + QString m_errorMessageShort; + QString m_errorMessageVerbose; + QString m_cause; + }; + + enum AbortedBy + { + BY_NOTHING, + BY_USER, + BY_TIMEOUT + } m_aborted = BY_NOTHING; + + /** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ + enum State + { + STATE_CREATED, + STATE_SENDING_REQUEST, + STATE_PROCESSING_RESPONSE, + STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated + STATE_FAILED_HARD, //!< hard failure. auth is invalid + STATE_SUCCEEDED + } m_state = STATE_CREATED; + +protected: + + virtual void executeTask() override; + + /** + * Gets the JSON object that will be sent to the authentication server. + * Should be overridden by subclasses. + */ + virtual QJsonObject getRequestContent() const = 0; + + /** + * Gets the endpoint to POST to. + * No leading slash. + */ + virtual QString getEndpoint() const = 0; + + /** + * Processes the response received from the server. + * If an error occurred, this should emit a failed signal and return false. + * If Yggdrasil gave an error response, it should call setError() first, and then return false. + * Otherwise, it should return true. + * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with + * an empty QJsonObject. + */ + virtual void processResponse(QJsonObject responseData) = 0; + + /** + * Processes an error response received from the server. + * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error. + * \returns a QString error message that will be passed to emitFailed. + */ + virtual void processError(QJsonObject responseData); + + /** + * Returns the state message for the given state. + * Used to set the status message for the task. + * Should be overridden by subclasses that want to change messages for a given state. + */ + virtual QString getStateMessage() const; + +protected +slots: + void processReply(); + void refreshTimers(qint64, qint64); + void heartbeat(); + void sslErrors(QList); + + 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 m_error; + QTimer timeout_keeper; + QTimer counter; + int count = 0; // num msec since time reset + + const int timeout_max = 30000; + const int time_step = 50; + + AuthSessionPtr m_session; +}; diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.cpp b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp new file mode 100644 index 00000000..8d136f0b --- /dev/null +++ b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp @@ -0,0 +1,202 @@ + +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AuthenticateTask.h" +#include "../MojangAccount.h" + +#include +#include +#include +#include + +#include +#include + +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 loadedProfiles; + for (auto iter : availableProfiles) + { + QJsonObject profile = iter.toObject(); + // Profiles are easy, we just need their ID and name. + QString id = profile.value("id").toString(""); + QString name = profile.value("name").toString(""); + bool legacy = profile.value("legacy").toBool(false); + + if (id.isEmpty() || name.isEmpty()) + { + // This should never happen, but we might as well + // warn about it if it does so we can debug it easily. + // You never know when Mojang might do something truly derpy. + qWarning() << "Found entry in available profiles list with missing ID or name " + "field. Ignoring it."; + } + + // Now, add a new AccountProfile entry to the list. + loadedProfiles.append({id, name, legacy}); + } + // Put the list of profiles we loaded into the MojangAccount object. + m_account->m_profiles = loadedProfiles; + + // Finally, we set the current profile to the correct value. This is pretty simple. + // We do need to make sure that the current profile that the server gave us + // is actually in the available profiles list. + // If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know). + qDebug() << "Setting current profile."; + QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); + QString currentProfileId = currentProfile.value("id").toString(""); + if (currentProfileId.isEmpty()) + { + changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium.")); + return; + } + if (!m_account->setCurrentProfile(currentProfileId)) + { + changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list.")); + return; + } + + // this is what the vanilla launcher passes to the userProperties launch param + if (responseData.contains("user")) + { + User u; + auto obj = responseData.value("user").toObject(); + u.id = obj.value("id").toString(); + auto propArray = obj.value("properties").toArray(); + for (auto prop : propArray) + { + auto propTuple = prop.toObject(); + auto name = propTuple.value("name").toString(); + auto value = propTuple.value("value").toString(); + u.properties.insert(name, value); + } + m_account->m_user = u; + } + + // We've made it through the minefield of possible errors. Return true to indicate that + // we've succeeded. + qDebug() << "Finished reading authentication response."; + changeState(STATE_SUCCEEDED); +} + +QString AuthenticateTask::getEndpoint() const +{ + return "authenticate"; +} + +QString AuthenticateTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_SENDING_REQUEST: + return tr("Authenticating: Sending request..."); + case STATE_PROCESSING_RESPONSE: + return tr("Authenticating: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/api/logic/minecraft/auth/flows/AuthenticateTask.h b/api/logic/minecraft/auth/flows/AuthenticateTask.h new file mode 100644 index 00000000..398fab98 --- /dev/null +++ b/api/logic/minecraft/auth/flows/AuthenticateTask.h @@ -0,0 +1,46 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../YggdrasilTask.h" + +#include +#include +#include + +/** + * The authenticate task takes a MojangAccount with no access token and password and attempts to + * authenticate with Mojang's servers. + * If successful, it will set the MojangAccount's access token. + */ +class AuthenticateTask : public YggdrasilTask +{ + Q_OBJECT +public: + AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; + +private: + QString m_password; +}; diff --git a/api/logic/minecraft/auth/flows/RefreshTask.cpp b/api/logic/minecraft/auth/flows/RefreshTask.cpp new file mode 100644 index 00000000..a0fb2e48 --- /dev/null +++ b/api/logic/minecraft/auth/flows/RefreshTask.cpp @@ -0,0 +1,144 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "RefreshTask.h" +#include "../MojangAccount.h" + +#include +#include +#include +#include + +#include + +RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account) +{ +} + +QJsonObject RefreshTask::getRequestContent() const +{ + /* + * { + * "clientToken": "client identifier" + * "accessToken": "current access token to be refreshed" + * "selectedProfile": // specifying this causes errors + * { + * "id": "profile ID" + * "name": "profile name" + * } + * "requestUser": true/false // request the user structure + * } + */ + QJsonObject req; + req.insert("clientToken", m_account->m_clientToken); + req.insert("accessToken", m_account->m_accessToken); + /* + { + auto currentProfile = m_account->currentProfile(); + QJsonObject profile; + profile.insert("id", currentProfile->id()); + profile.insert("name", currentProfile->name()); + req.insert("selectedProfile", profile); + } + */ + req.insert("requestUser", true); + + return req; +} + +void RefreshTask::processResponse(QJsonObject responseData) +{ + // Read the response data. We need to get the client token, access token, and the selected + // profile. + qDebug() << "Processing authentication response."; + + // qDebug() << responseData; + // If we already have a client token, make sure the one the server gave us matches our + // existing one. + QString clientToken = responseData.value("clientToken").toString(""); + if (clientToken.isEmpty()) + { + // Fail if the server gave us an empty client token + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); + return; + } + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) + { + changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); + return; + } + + // Now, we set the access token. + qDebug() << "Getting new access token."; + QString accessToken = responseData.value("accessToken").toString(""); + if (accessToken.isEmpty()) + { + // Fail if the server didn't give us an access token. + changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); + return; + } + + // we validate that the server responded right. (our current profile = returned current + // profile) + QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); + QString currentProfileId = currentProfile.value("id").toString(""); + if (m_account->currentProfile()->id != currentProfileId) + { + changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected.")); + return; + } + + // this is what the vanilla launcher passes to the userProperties launch param + if (responseData.contains("user")) + { + User u; + auto obj = responseData.value("user").toObject(); + u.id = obj.value("id").toString(); + auto propArray = obj.value("properties").toArray(); + for (auto prop : propArray) + { + auto propTuple = prop.toObject(); + auto name = propTuple.value("name").toString(); + auto value = propTuple.value("value").toString(); + u.properties.insert(name, value); + } + m_account->m_user = u; + } + + // We've made it through the minefield of possible errors. Return true to indicate that + // we've succeeded. + qDebug() << "Finished reading refresh response."; + // Reset the access token. + m_account->m_accessToken = accessToken; + changeState(STATE_SUCCEEDED); +} + +QString RefreshTask::getEndpoint() const +{ + return "refresh"; +} + +QString RefreshTask::getStateMessage() const +{ + switch (m_state) + { + case STATE_SENDING_REQUEST: + return tr("Refreshing login token..."); + case STATE_PROCESSING_RESPONSE: + return tr("Refreshing login token: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/api/logic/minecraft/auth/flows/RefreshTask.h b/api/logic/minecraft/auth/flows/RefreshTask.h new file mode 100644 index 00000000..17714b4f --- /dev/null +++ b/api/logic/minecraft/auth/flows/RefreshTask.h @@ -0,0 +1,44 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../YggdrasilTask.h" + +#include +#include +#include + +/** + * The authenticate task takes a MojangAccount with a possibly timed-out access token + * and attempts to authenticate with Mojang's servers. + * If successful, it will set the new access token. The token is considered validated. + */ +class RefreshTask : public YggdrasilTask +{ + Q_OBJECT +public: + RefreshTask(MojangAccount * account); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; +}; + diff --git a/api/logic/minecraft/auth/flows/ValidateTask.cpp b/api/logic/minecraft/auth/flows/ValidateTask.cpp new file mode 100644 index 00000000..4deceb6a --- /dev/null +++ b/api/logic/minecraft/auth/flows/ValidateTask.cpp @@ -0,0 +1,61 @@ + +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ValidateTask.h" +#include "../MojangAccount.h" + +#include +#include +#include +#include + +#include + +ValidateTask::ValidateTask(MojangAccount * account, QObject *parent) + : YggdrasilTask(account, parent) +{ +} + +QJsonObject ValidateTask::getRequestContent() const +{ + QJsonObject req; + req.insert("accessToken", m_account->m_accessToken); + return req; +} + +void ValidateTask::processResponse(QJsonObject responseData) +{ + // Assume that if processError wasn't called, then the request was successful. + changeState(YggdrasilTask::STATE_SUCCEEDED); +} + +QString ValidateTask::getEndpoint() const +{ + return "validate"; +} + +QString ValidateTask::getStateMessage() const +{ + switch (m_state) + { + case YggdrasilTask::STATE_SENDING_REQUEST: + return tr("Validating access token: Sending request..."); + case YggdrasilTask::STATE_PROCESSING_RESPONSE: + return tr("Validating access token: Processing response..."); + default: + return YggdrasilTask::getStateMessage(); + } +} diff --git a/api/logic/minecraft/auth/flows/ValidateTask.h b/api/logic/minecraft/auth/flows/ValidateTask.h new file mode 100644 index 00000000..77d628a0 --- /dev/null +++ b/api/logic/minecraft/auth/flows/ValidateTask.h @@ -0,0 +1,47 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME: + */ + +#pragma once + +#include "../YggdrasilTask.h" + +#include +#include +#include + +/** + * The validate task takes a MojangAccount and checks to make sure its access token is valid. + */ +class ValidateTask : public YggdrasilTask +{ + Q_OBJECT +public: + ValidateTask(MojangAccount *account, QObject *parent = 0); + +protected: + virtual QJsonObject getRequestContent() const override; + + virtual QString getEndpoint() const override; + + virtual void processResponse(QJsonObject responseData) override; + + virtual QString getStateMessage() const override; + +private: +}; diff --git a/api/logic/minecraft/forge/ForgeInstaller.cpp b/api/logic/minecraft/forge/ForgeInstaller.cpp new file mode 100644 index 00000000..353328ab --- /dev/null +++ b/api/logic/minecraft/forge/ForgeInstaller.cpp @@ -0,0 +1,458 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ForgeInstaller.h" +#include "ForgeVersionList.h" + +#include "minecraft/MinecraftProfile.h" +#include "minecraft/GradleSpecifier.h" +#include "net/HttpMetaCache.h" +#include "tasks/Task.h" +#include "minecraft/onesix/OneSixInstance.h" +#include +#include "minecraft/VersionFilterData.h" +#include "minecraft/MinecraftVersion.h" +#include "Env.h" +#include "Exception.h" +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +ForgeInstaller::ForgeInstaller() : BaseInstaller() +{ +} + +void ForgeInstaller::prepare(const QString &filename, const QString &universalUrl) +{ + VersionFilePtr newVersion; + m_universal_url = universalUrl; + + QuaZip zip(filename); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + // read the install profile + if (!zip.setCurrentFile("install_profile.json")) + return; + + QJsonParseError jsonError; + if (!file.open(QIODevice::ReadOnly)) + return; + QJsonDocument jsonDoc = QJsonDocument::fromJson(file.readAll(), &jsonError); + file.close(); + if (jsonError.error != QJsonParseError::NoError) + return; + + if (!jsonDoc.isObject()) + return; + + QJsonObject root = jsonDoc.object(); + + auto installVal = root.value("install"); + auto versionInfoVal = root.value("versionInfo"); + if (!installVal.isObject() || !versionInfoVal.isObject()) + return; + + try + { + newVersion = OneSixVersionFormat::versionFileFromJson(QJsonDocument(versionInfoVal.toObject()), QString(), false); + } + catch(Exception &err) + { + qWarning() << "Forge: Fatal error while parsing version file:" << err.what(); + return; + } + + for(auto problem: newVersion->getProblems()) + { + qWarning() << "Forge: Problem found: " << problem.getDescription(); + } + if(newVersion->getProblemSeverity() == ProblemSeverity::PROBLEM_ERROR) + { + qWarning() << "Forge: Errors found while parsing version file"; + return; + } + + QJsonObject installObj = installVal.toObject(); + QString libraryName = installObj.value("path").toString(); + internalPath = installObj.value("filePath").toString(); + m_forgeVersionString = installObj.value("version").toString().remove("Forge", Qt::CaseInsensitive).trimmed(); + + // where do we put the library? decode the mojang path + GradleSpecifier lib(libraryName); + + auto cacheentry = ENV.metacache()->resolveEntry("libraries", lib.toPath()); + finalPath = "libraries/" + lib.toPath(); + if (!FS::ensureFilePathExists(finalPath)) + return; + + if (!zip.setCurrentFile(internalPath)) + return; + if (!file.open(QIODevice::ReadOnly)) + return; + { + QByteArray data = file.readAll(); + // extract file + QSaveFile extraction(finalPath); + if (!extraction.open(QIODevice::WriteOnly)) + return; + if (extraction.write(data) != data.size()) + return; + if (!extraction.commit()) + return; + QCryptographicHash md5sum(QCryptographicHash::Md5); + md5sum.addData(data); + + cacheentry->setStale(false); + cacheentry->setMD5Sum(md5sum.result().toHex().constData()); + ENV.metacache()->updateEntry(cacheentry); + } + file.close(); + + m_forge_json = newVersion; +} + +bool ForgeInstaller::add(OneSixInstance *to) +{ + if (!BaseInstaller::add(to)) + { + return false; + } + + if (!m_forge_json) + { + return false; + } + + // A blacklist + QSet blacklist{"authlib", "realms"}; + QList xzlist{"org.scala-lang", "com.typesafe"}; + + // get the minecraft version from the instance + VersionFilePtr minecraft; + auto minecraftPatch = to->getMinecraftProfile()->versionPatch("net.minecraft"); + if(minecraftPatch) + { + minecraft = std::dynamic_pointer_cast(minecraftPatch); + if(!minecraft) + { + auto mcWrap = std::dynamic_pointer_cast(minecraftPatch); + if(mcWrap) + { + minecraft = mcWrap->getVersionFile(); + } + } + } + + // for each library in the version we are adding (except for the blacklisted) + QMutableListIterator iter(m_forge_json->libraries); + while (iter.hasNext()) + { + auto library = iter.next(); + QString libName = library->artifactId(); + QString libVersion = library->version(); + QString rawName = library->rawName(); + + // ignore lwjgl libraries. + if (g_VersionFilterData.lwjglWhitelist.contains(library->artifactPrefix())) + { + iter.remove(); + continue; + } + // ignore other blacklisted (realms, authlib) + if (blacklist.contains(libName)) + { + iter.remove(); + continue; + } + // if minecraft version was found, ignore everything that is already in the minecraft version + if(minecraft) + { + bool found = false; + for (auto & lib: minecraft->libraries) + { + if(library->artifactPrefix() == lib->artifactPrefix() && library->version() == lib->version()) + { + found = true; + break; + } + } + if (found) + continue; + } + + // if this is the actual forge lib, set an absolute url for the download + if (m_forge_version->type == ForgeVersion::Gradle) + { + if (libName == "forge") + { + library->setClassifier("universal"); + } + else if (libName == "minecraftforge") + { + QString forgeCoord("net.minecraftforge:forge:%1:universal"); + // using insane form of the MC version... + QString longVersion = m_forge_version->mcver + "-" + m_forge_version->jobbuildver; + GradleSpecifier spec(forgeCoord.arg(longVersion)); + library->setRawName(spec); + } + } + else + { + if (libName.contains("minecraftforge")) + { + library->setAbsoluteUrl(m_universal_url); + } + } + + // mark bad libraries based on the xzlist above + for (auto entry : xzlist) + { + qDebug() << "Testing " << rawName << " : " << entry; + if (rawName.startsWith(entry)) + { + library->setHint("forge-pack-xz"); + break; + } + } + } + QString &args = m_forge_json->minecraftArguments; + QStringList tweakers; + { + QRegularExpression expression("--tweakClass ([a-zA-Z0-9\\.]*)"); + QRegularExpressionMatch match = expression.match(args); + while (match.hasMatch()) + { + tweakers.append(match.captured(1)); + args.remove(match.capturedStart(), match.capturedLength()); + match = expression.match(args); + } + if(tweakers.size()) + { + args.operator=(args.trimmed()); + m_forge_json->addTweakers = tweakers; + } + } + if(minecraft && args == minecraft->minecraftArguments) + { + args.clear(); + } + + m_forge_json->name = "Forge"; + m_forge_json->fileId = id(); + m_forge_json->version = m_forgeVersionString; + m_forge_json->dependsOnMinecraftVersion = to->intendedVersionId(); + m_forge_json->order = 5; + + // reset some things we do not want to be passed along. + m_forge_json->m_releaseTime = QDateTime(); + m_forge_json->m_updateTime = QDateTime(); + m_forge_json->minimumLauncherVersion = -1; + m_forge_json->type.clear(); + m_forge_json->minecraftArguments.clear(); + m_forge_json->minecraftVersion.clear(); + + QSaveFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(m_forge_json, true).toJson()); + file.commit(); + + return true; +} + +bool ForgeInstaller::addLegacy(OneSixInstance *to) +{ + if (!BaseInstaller::add(to)) + { + return false; + } + auto entry = ENV.metacache()->resolveEntry("minecraftforge", m_forge_version->filename()); + finalPath = FS::PathCombine(to->jarModsDir(), m_forge_version->filename()); + if (!FS::ensureFilePathExists(finalPath)) + { + return false; + } + if (!QFile::copy(entry->getFullPath(), finalPath)) + { + return false; + } + QJsonObject obj; + obj.insert("order", 5); + { + QJsonArray jarmodsPlus; + { + QJsonObject libObj; + libObj.insert("name", m_forge_version->universal_filename); + jarmodsPlus.append(libObj); + } + obj.insert("+jarMods", jarmodsPlus); + } + + obj.insert("name", QString("Forge")); + obj.insert("fileId", id()); + obj.insert("version", m_forge_version->jobbuildver); + obj.insert("mcVersion", to->intendedVersionId()); + if (g_VersionFilterData.fmlLibsMapping.contains(m_forge_version->mcver)) + { + QJsonArray traitsPlus; + traitsPlus.append(QString("legacyFML")); + obj.insert("+traits", traitsPlus); + } + auto fullversion = to->getMinecraftProfile(); + fullversion->remove("net.minecraftforge"); + + QFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(QJsonDocument(obj).toJson()); + file.close(); + return true; +} + +class ForgeInstallTask : public Task +{ + Q_OBJECT +public: + ForgeInstallTask(ForgeInstaller *installer, OneSixInstance *instance, + BaseVersionPtr version, QObject *parent = 0) + : Task(parent), m_installer(installer), m_instance(instance), m_version(version) + { + } + +protected: + void executeTask() override + { + setStatus(tr("Installing Forge...")); + ForgeVersionPtr forgeVersion = std::dynamic_pointer_cast(m_version); + if (!forgeVersion) + { + emitFailed(tr("Unknown error occured")); + return; + } + prepare(forgeVersion); + } + void prepare(ForgeVersionPtr forgeVersion) + { + auto entry = ENV.metacache()->resolveEntry("minecraftforge", forgeVersion->filename()); + auto installFunction = [this, entry, forgeVersion]() + { + if (!install(entry, forgeVersion)) + { + qCritical() << "Failure installing Forge"; + emitFailed(tr("Failure to install Forge")); + } + else + { + reload(); + } + }; + + /* + * HACK IF the local non-stale file is too small, mark is as stale + * + * This fixes some problems with bad files acquired because of unhandled HTTP redirects + * in old versions of MultiMC. + */ + if (!entry->isStale()) + { + QFileInfo localFile(entry->getFullPath()); + if (localFile.size() <= 0x4000) + { + entry->setStale(true); + } + } + + if (entry->isStale()) + { + NetJob *fjob = new NetJob("Forge download"); + fjob->addNetAction(CacheDownload::make(forgeVersion->url(), entry)); + connect(fjob, &NetJob::progress, this, &Task::setProgress); + connect(fjob, &NetJob::status, this, &Task::setStatus); + connect(fjob, &NetJob::failed, [this](QString reason) + { emitFailed(tr("Failure to download Forge:\n%1").arg(reason)); }); + connect(fjob, &NetJob::succeeded, installFunction); + fjob->start(); + } + else + { + installFunction(); + } + } + bool install(const std::shared_ptr &entry, const ForgeVersionPtr &forgeVersion) + { + if (forgeVersion->usesInstaller()) + { + QString forgePath = entry->getFullPath(); + m_installer->prepare(forgePath, forgeVersion->universal_url); + return m_installer->add(m_instance); + } + else + return m_installer->addLegacy(m_instance); + } + void reload() + { + try + { + m_instance->reloadProfile(); + emitSucceeded(); + } + catch (Exception &e) + { + emitFailed(e.cause()); + } + catch (...) + { + emitFailed(tr("Failed to load the version description file for reasons unknown.")); + } + } + +private: + ForgeInstaller *m_installer; + OneSixInstance *m_instance; + BaseVersionPtr m_version; +}; + +Task *ForgeInstaller::createInstallTask(OneSixInstance *instance, + BaseVersionPtr version, QObject *parent) +{ + if (!version) + { + return nullptr; + } + m_forge_version = std::dynamic_pointer_cast(version); + return new ForgeInstallTask(this, instance, version, parent); +} + +#include "ForgeInstaller.moc" diff --git a/api/logic/minecraft/forge/ForgeInstaller.h b/api/logic/minecraft/forge/ForgeInstaller.h new file mode 100644 index 00000000..499a6fb3 --- /dev/null +++ b/api/logic/minecraft/forge/ForgeInstaller.h @@ -0,0 +1,52 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseInstaller.h" + +#include +#include + +#include "multimc_logic_export.h" + +class VersionFile; +class ForgeInstallTask; +struct ForgeVersion; + +class MULTIMC_LOGIC_EXPORT ForgeInstaller : public BaseInstaller +{ + friend class ForgeInstallTask; +public: + ForgeInstaller(); + virtual ~ForgeInstaller(){} + virtual Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override; + virtual QString id() const override { return "net.minecraftforge"; } + +protected: + void prepare(const QString &filename, const QString &universalUrl); + bool add(OneSixInstance *to) override; + bool addLegacy(OneSixInstance *to); + +private: + // the parsed version json, read from the installer + std::shared_ptr m_forge_json; + // the actual forge version + std::shared_ptr m_forge_version; + QString internalPath; + QString finalPath; + QString m_forgeVersionString; + QString m_universal_url; +}; diff --git a/api/logic/minecraft/forge/ForgeVersion.cpp b/api/logic/minecraft/forge/ForgeVersion.cpp new file mode 100644 index 00000000..b859a28c --- /dev/null +++ b/api/logic/minecraft/forge/ForgeVersion.cpp @@ -0,0 +1,55 @@ +#include "ForgeVersion.h" +#include "minecraft/VersionFilterData.h" +#include + +QString ForgeVersion::name() +{ + return "Forge " + jobbuildver; +} + +QString ForgeVersion::descriptor() +{ + return universal_filename; +} + +QString ForgeVersion::typeString() const +{ + if (is_recommended) + return QObject::tr("Recommended"); + return QString(); +} + +bool ForgeVersion::operator<(BaseVersion &a) +{ + ForgeVersion *pa = dynamic_cast(&a); + if (!pa) + return true; + return m_buildnr < pa->m_buildnr; +} + +bool ForgeVersion::operator>(BaseVersion &a) +{ + ForgeVersion *pa = dynamic_cast(&a); + if (!pa) + return false; + return m_buildnr > pa->m_buildnr; +} + +bool ForgeVersion::usesInstaller() +{ + if(installer_url.isEmpty()) + return false; + if(g_VersionFilterData.forgeInstallerBlacklist.contains(mcver)) + return false; + return true; +} + +QString ForgeVersion::filename() +{ + return usesInstaller() ? installer_filename : universal_filename; +} + +QString ForgeVersion::url() +{ + return usesInstaller() ? installer_url : universal_url; +} diff --git a/api/logic/minecraft/forge/ForgeVersion.h b/api/logic/minecraft/forge/ForgeVersion.h new file mode 100644 index 00000000..e77d32f1 --- /dev/null +++ b/api/logic/minecraft/forge/ForgeVersion.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include "BaseVersion.h" + +struct ForgeVersion; +typedef std::shared_ptr ForgeVersionPtr; + +struct ForgeVersion : public BaseVersion +{ + virtual QString descriptor() override; + virtual QString name() override; + virtual QString typeString() const override; + virtual bool operator<(BaseVersion &a) override; + virtual bool operator>(BaseVersion &a) override; + + QString filename(); + QString url(); + + enum + { + Invalid, + Legacy, + Gradle + } type = Invalid; + + bool usesInstaller(); + + int m_buildnr = 0; + QString branch; + QString universal_url; + QString changelog_url; + QString installer_url; + QString jobbuildver; + QString mcver; + QString mcver_sane; + QString universal_filename; + QString installer_filename; + bool is_recommended = false; +}; + +Q_DECLARE_METATYPE(ForgeVersionPtr) diff --git a/api/logic/minecraft/forge/ForgeVersionList.cpp b/api/logic/minecraft/forge/ForgeVersionList.cpp new file mode 100644 index 00000000..de185e5f --- /dev/null +++ b/api/logic/minecraft/forge/ForgeVersionList.cpp @@ -0,0 +1,450 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ForgeVersionList.h" +#include "ForgeVersion.h" + +#include "net/NetJob.h" +#include "net/URLConstants.h" +#include "Env.h" + +#include +#include +#include + +#include + +ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent) +{ +} + +Task *ForgeVersionList::getLoadTask() +{ + return new ForgeListLoadTask(this); +} + +bool ForgeVersionList::isLoaded() +{ + return m_loaded; +} + +const BaseVersionPtr ForgeVersionList::at(int i) const +{ + return m_vlist.at(i); +} + +int ForgeVersionList::count() const +{ + return m_vlist.count(); +} + +int ForgeVersionList::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ForgeVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = std::dynamic_pointer_cast(m_vlist[index.row()]); + switch (role) + { + case VersionPointerRole: + return qVariantFromValue(m_vlist[index.row()]); + + case VersionRole: + return version->name(); + + case VersionIdRole: + return version->descriptor(); + + case ParentGameVersionRole: + return version->mcver_sane; + + case RecommendedRole: + return version->is_recommended; + + case BranchRole: + return version->branch; + + default: + return QVariant(); + } +} + +BaseVersionList::RoleList ForgeVersionList::providesRoles() const +{ + return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole, BranchRole}; +} + +BaseVersionPtr ForgeVersionList::getLatestStable() const +{ + return BaseVersionPtr(); +} + +void ForgeVersionList::updateListData(QList versions) +{ + beginResetModel(); + m_vlist = versions; + m_loaded = true; + endResetModel(); + // NOW SORT!! + // sort(); +} + +void ForgeVersionList::sortVersions() +{ + // NO-OP for now +} + +ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task() +{ + m_list = vlist; +} + +void ForgeListLoadTask::executeTask() +{ + setStatus(tr("Fetching Forge version lists...")); + auto job = new NetJob("Version index"); + // we do not care if the version is stale or not. + auto forgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "list.json"); + auto gradleForgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "json"); + + // verify by poking the server. + forgeListEntry->setStale(true); + gradleForgeListEntry->setStale(true); + + job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL), + forgeListEntry)); + job->addNetAction(gradleListDownload = CacheDownload::make( + QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry)); + + connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed())); + connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed())); + + listJob.reset(job); + connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded())); + connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + listJob->start(); +} + +bool ForgeListLoadTask::abort() +{ + return listJob->abort(); +} + +bool ForgeListLoadTask::parseForgeList(QList &out) +{ + QByteArray data; + { + auto dlJob = listDownload; + auto filename = std::dynamic_pointer_cast(dlJob)->getTargetFilepath(); + QFile listFile(filename); + if (!listFile.open(QIODevice::ReadOnly)) + { + return false; + } + data = listFile.readAll(); + dlJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing version list JSON:" + jsonError.errorString()); + return false; + } + + if (!jsonDoc.isObject()) + { + emitFailed("Error parsing version list JSON: JSON root is not an object"); + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Now, get the array of versions. + if (!root.value("builds").isArray()) + { + emitFailed( + "Error parsing version list JSON: version list object is missing 'builds' array"); + return false; + } + QJsonArray builds = root.value("builds").toArray(); + + for (int i = 0; i < builds.count(); i++) + { + // Load the version info. + if (!builds[i].isObject()) + { + // FIXME: log this somewhere + continue; + } + QJsonObject obj = builds[i].toObject(); + int build_nr = obj.value("build").toDouble(0); + if (!build_nr) + continue; + QJsonArray files = obj.value("files").toArray(); + QString url, jobbuildver, mcver, buildtype, universal_filename; + QString changelog_url, installer_url; + QString installer_filename; + bool valid = false; + for (int j = 0; j < files.count(); j++) + { + if (!files[j].isObject()) + { + continue; + } + QJsonObject file = files[j].toObject(); + buildtype = file.value("buildtype").toString(); + if ((buildtype == "client" || buildtype == "universal") && !valid) + { + mcver = file.value("mcver").toString(); + url = file.value("url").toString(); + jobbuildver = file.value("jobbuildver").toString(); + int lastSlash = url.lastIndexOf('/'); + universal_filename = url.mid(lastSlash + 1); + valid = true; + } + else if (buildtype == "changelog") + { + QString ext = file.value("ext").toString(); + if (ext.isEmpty()) + { + continue; + } + changelog_url = file.value("url").toString(); + } + else if (buildtype == "installer") + { + installer_url = file.value("url").toString(); + int lastSlash = installer_url.lastIndexOf('/'); + installer_filename = installer_url.mid(lastSlash + 1); + } + } + if (valid) + { + // Now, we construct the version object and add it to the list. + std::shared_ptr fVersion(new ForgeVersion()); + fVersion->universal_url = url; + fVersion->changelog_url = changelog_url; + fVersion->installer_url = installer_url; + fVersion->jobbuildver = jobbuildver; + fVersion->mcver = fVersion->mcver_sane = mcver; + fVersion->installer_filename = installer_filename; + fVersion->universal_filename = universal_filename; + fVersion->m_buildnr = build_nr; + fVersion->type = ForgeVersion::Legacy; + out.append(fVersion); + } + } + + return true; +} + +bool ForgeListLoadTask::parseForgeGradleList(QList &out) +{ + QMap> lookup; + QByteArray data; + { + auto dlJob = gradleListDownload; + auto filename = std::dynamic_pointer_cast(dlJob)->getTargetFilepath(); + QFile listFile(filename); + if (!listFile.open(QIODevice::ReadOnly)) + { + return false; + } + data = listFile.readAll(); + dlJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString()); + return false; + } + + if (!jsonDoc.isObject()) + { + emitFailed("Error parsing gradle version list JSON: JSON root is not an object"); + return false; + } + + QJsonObject root = jsonDoc.object(); + + // we probably could hard code these, but it might still be worth doing it this way + const QString webpath = root.value("webpath").toString(); + const QString artifact = root.value("artifact").toString(); + + QJsonObject numbers = root.value("number").toObject(); + for (auto it = numbers.begin(); it != numbers.end(); ++it) + { + QJsonObject number = it.value().toObject(); + std::shared_ptr fVersion(new ForgeVersion()); + fVersion->m_buildnr = number.value("build").toDouble(); + if(fVersion->m_buildnr >= 953 && fVersion->m_buildnr <= 965) + { + qDebug() << fVersion->m_buildnr; + } + fVersion->jobbuildver = number.value("version").toString(); + fVersion->branch = number.value("branch").toString(""); + fVersion->mcver = number.value("mcversion").toString(); + fVersion->universal_filename = ""; + fVersion->installer_filename = ""; + // HACK: here, we fix the minecraft version used by forge. + // HACK: this will inevitably break (later) + // FIXME: replace with a dictionary + fVersion->mcver_sane = fVersion->mcver; + fVersion->mcver_sane.replace("_pre", "-pre"); + + QString universal_filename, installer_filename; + QJsonArray files = number.value("files").toArray(); + for (auto fIt = files.begin(); fIt != files.end(); ++fIt) + { + // TODO with gradle we also get checksums, use them + QJsonArray file = (*fIt).toArray(); + if (file.size() < 3) + { + continue; + } + + QString extension = file.at(0).toString(); + QString part = file.at(1).toString(); + QString checksum = file.at(2).toString(); + + // insane form of mcver is used here + QString longVersion = fVersion->mcver + "-" + fVersion->jobbuildver; + if (!fVersion->branch.isEmpty()) + { + longVersion = longVersion + "-" + fVersion->branch; + } + QString filename = artifact + "-" + longVersion + "-" + part + "." + extension; + + QString url = QString("%1/%2/%3") + .arg(webpath) + .arg(longVersion) + .arg(filename); + + if (part == "installer") + { + fVersion->installer_url = url; + installer_filename = filename; + } + else if (part == "universal") + { + fVersion->universal_url = url; + universal_filename = filename; + } + else if (part == "changelog") + { + fVersion->changelog_url = url; + } + } + if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty()) + { + continue; + } + fVersion->universal_filename = universal_filename; + fVersion->installer_filename = installer_filename; + fVersion->type = ForgeVersion::Gradle; + out.append(fVersion); + lookup[fVersion->m_buildnr] = fVersion; + } + QJsonObject promos = root.value("promos").toObject(); + for (auto it = promos.begin(); it != promos.end(); ++it) + { + QString key = it.key(); + int build = it.value().toInt(); + QRegularExpression regexp("^(?[0-9]+(.[0-9]+)*)-(?