diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..717fbb6851 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,51 @@ + + +System Information +----------------------------- +MultiMC version: + +Operating System: + +Summary of the issue or suggestion: +---------------------------------------------- + + +What should happen: +------------------------------ + + +Steps to reproduce the issue (Add more if needed): +------------------------------------------------------------- +1. + +2. + +3. + +Suspected cause: +--------------------------- + + +Logs/Screenshots: +---------------------------- +[//]: # (Please refer to https://github.com/Ponywka/MultiMC5-with-offline/wiki/Log-Upload for instructions on how to attach your logs.) + + +Additional Info: +--------------------------- diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7e894785ba..5b8d858e1c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,6 @@ name: Bug Report description: File a bug report labels: [bug, needs-triage] -issue_body: false body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml index ab2449a0fe..88bf66cf60 100644 --- a/.github/ISSUE_TEMPLATE/suggestion.yml +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -1,7 +1,6 @@ name: Suggestion description: Make a suggestion labels: [idea, needs-triage] -issue_body: true body: - type: markdown attributes: @@ -34,8 +33,6 @@ body: options: - label: I have searched the issue tracker and did not find an issue describing my suggestion, especially not one that has been rejected. required: true -- type: markdown +- type: textarea attributes: - value: | - ### You may use the editor below to elaborate further. -# The issue_body: true up there makes the standard WYSIWYG editor for issues show up down here. + label: You may use the editor below to elaborate further. diff --git a/.github/workflows/lin64.yml b/.github/workflows/lin64.yml new file mode 100644 index 0000000000..2aa60fcc4c --- /dev/null +++ b/.github/workflows/lin64.yml @@ -0,0 +1,30 @@ +name: lin64 + +on: [push, pull_request] + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + - uses: jurplel/install-qt-action@v2 + with: + version: 5.11.3 + + - name: Sub projects + run: git submodule init && git submodule update + + - name: CMake + run: cmake -DCMAKE_INSTALL_PREFIX=./install . + + - name: Build + run: mkdir install && make -j8 install + + - uses: actions/upload-artifact@v2 + with: + name: lin64 + path: install/ diff --git a/.gitignore b/.gitignore index 496c382e3f..6b716252f6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ CMakeLists.txt.user.* /.settings /.idea cmake-build-*/ +Debug # Build dirs build @@ -29,3 +30,6 @@ tags #OSX Stuff .DS_Store + +branding +secrets diff --git a/BUILD.md b/BUILD.md index 4360fddea4..c1efb96972 100644 --- a/BUILD.md +++ b/BUILD.md @@ -12,7 +12,7 @@ Build Instructions # Note MultiMC is a portable application and is not supposed to be installed into any system folders. -That would be anything outside your home folder. Before runing `make install`, make sure +That would be anything outside your home folder. Before running `make install`, make sure you set the install path to something you have write access to. Never build this under an administrator/root level account. Don't use `sudo`. It won't work and it's not supposed to work. @@ -22,7 +22,7 @@ an administrator/root level account. Don't use `sudo`. It won't work and it's no Clone the source code using git and grab all the submodules: ``` -git clone git@github.com:MultiMC/MultiMC5.git +git clone git@github.com:Ponywka/MultiMC5-with-offline.git git submodule init git submodule update ``` @@ -50,7 +50,7 @@ mkdir ~/MultiMC && cd ~/MultiMC mkdir build mkdir install # clone the complete source -git clone --recursive https://github.com/MultiMC/MultiMC5.git src +git clone --recursive https://github.com/Ponywka/MultiMC5-with-offline.git src # configure the project cd build cmake -DCMAKE_INSTALL_PREFIX=../install ../src @@ -165,7 +165,7 @@ zlib1.dll **These build instructions worked for me (Drayshak) on a fresh Windows 8 x64 Professional install. If they don't work for you, let us know on IRC ([Esper/#MultiMC](http://webchat.esper.net/?nick=&channels=MultiMC))!** ### Compile from command line on Windows 1. If you installed Qt with the web installer, there should be a shortcut called `Qt 5.4 for Desktop (MinGW 4.9 32-bit)` in the Start menu on Windows 7 and 10. Best way to find it is to search for it. Do note you cannot just use cmd.exe, you have to use the shortcut, otherwise the proper MinGW software will not be on the PATH. -2. Once that is open, change into your user directory, and clone MultiMC by doing `git clone --recursive https://github.com/MultiMC/MultiMC5.git`, and change directory to the folder you cloned to. +2. Once that is open, change into your user directory, and clone MultiMC by doing `git clone --recursive https://github.com/Ponywka/MultiMC5-with-offline.git`, and change directory to the folder you cloned to. 3. Make a build directory, and change directory to the directory and do `cmake -G "MinGW Makefiles" -DCMAKE_INSTALL_PREFIX=C:\Path\that\makes\sense\for\you`. By default, it will install to C:\Program Files (x86), which you might not want, if you want a local installation. If you want to install it to that directory, make sure to run the command window as administrator. 3. Do `mingw32-make -jX`, where X is the number of cores your CPU has plus one. 4. Now to wait for it to compile. This could take some time. Hopefully it compiles properly. @@ -185,7 +185,10 @@ zlib1.dll Pick an installation path - this is where the final `.app` will be constructed when you run `make install`. Supply it as the `CMAKE_INSTALL_PREFIX` argument during CMake configuration. ``` -git clone --recursive https://github.com/MultiMC/MultiMC5.git +git clone https://github.com/Ponywka/MultiMC5-with-offline.git +cd MultiMC5 +git submodule init +git submodule update cd MultiMC5 mkdir build cd build diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c93e7a9c0..44028f7656 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ set(MultiMC_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fetc ######## Set version numbers ######## set(MultiMC_VERSION_MAJOR 0) set(MultiMC_VERSION_MINOR 6) -set(MultiMC_VERSION_HOTFIX 12) +set(MultiMC_VERSION_HOTFIX 13) # Build number set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") @@ -64,7 +64,7 @@ set(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number. set(MultiMC_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used by the notification system and to display in the about dialog.") # Channel list URL -set(MultiMC_CHANLIST_URL "" CACHE STRING "URL for the channel list.") +set(MultiMC_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") # Notification URL set(MultiMC_NOTIFICATION_URL "" CACHE STRING "URL for checking for notifications.") @@ -75,9 +75,24 @@ set(MultiMC_META_URL "https://meta.multimc.org/v1/" CACHE STRING "URL to fetch M # paste.ee API key set(MultiMC_PASTE_EE_API_KEY "utLvciUouSURFzfjPxLBf5W4ISsUX4pwBDF7N1AfZ" CACHE STRING "API key you can get from paste.ee when you register an account") +# Imgur API Client ID +set(MultiMC_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can get from Imgur when you register an application") + # Google analytics ID set(MultiMC_ANALYTICS_ID "UA-87731965-2" CACHE STRING "ID you can get from Google analytics") +# Bug tracker URL +set(MultiMC_BUG_TRACKER_URL "" CACHE STRING "URL for the bug tracker.") + +# Discord URL +set(MultiMC_DISCORD_URL "" CACHE STRING "URL for the Discord guild.") + +# Subreddit URL +set(MultiMC_SUBREDDIT_URL "" CACHE STRING "URL for the subreddit.") + + +option(MultiMC_EMBED_SECRETS "Determines whether to embed secrets. Secrets are separate and non-public." OFF) + #### Check the current Git commit and branch include(GetGitRevisionDescription) get_git_head_revision(MultiMC_GIT_REFSPEC MultiMC_GIT_COMMIT) @@ -163,7 +178,7 @@ if(MultiMC_LAYOUT_REAL STREQUAL "mac-bundle") set(INSTALL_BUNDLE "full") # Add the icon - install(FILES application/resources/MultiMC.icns DESTINATION ${RESOURCES_DEST_DIR}) + install(FILES launcher/resources/MultiMC.icns DESTINATION ${RESOURCES_DEST_DIR}) elseif(MultiMC_LAYOUT_REAL STREQUAL "lin-bundle") set(BINARY_DEST_DIR "bin") @@ -186,7 +201,7 @@ elseif(MultiMC_LAYOUT_REAL STREQUAL "lin-bundle") SET(MultiMC_BINARY_RPATH "$ORIGIN/") # Install basic runner script - install(PROGRAMS application/package/linux/MultiMC DESTINATION ${BUNDLE_DEST_DIR}) + install(PROGRAMS launcher/package/linux/MultiMC DESTINATION ${BUNDLE_DEST_DIR}) elseif(MultiMC_LAYOUT_REAL STREQUAL "lin-nodeps") set(BINARY_DEST_DIR "bin") @@ -203,7 +218,7 @@ elseif(MultiMC_LAYOUT_REAL STREQUAL "lin-nodeps") SET(MultiMC_BINARY_RPATH "$ORIGIN/") # Install basic runner script - install(PROGRAMS application/package/linux/MultiMC DESTINATION ${BUNDLE_DEST_DIR}) + install(PROGRAMS launcher/package/linux/MultiMC DESTINATION ${BUNDLE_DEST_DIR}) elseif(MultiMC_LAYOUT_REAL STREQUAL "lin-system") set(MultiMC_APP_BINARY_NAME "multimc" CACHE STRING "Name of the MultiMC binary") @@ -265,12 +280,19 @@ add_subdirectory(libraries/iconfix) # fork of Qt's QIcon loader add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions add_subdirectory(libraries/classparser) # google analytics library add_subdirectory(libraries/optional-bare) +add_subdirectory(libraries/tomlc99) # toml parser +add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much ############################### Built Artifacts ############################### add_subdirectory(buildconfig) -add_subdirectory(api/logic) -add_subdirectory(api/gui) + +if(MultiMC_EMBED_SECRETS) + add_subdirectory(secrets) +else() + add_subdirectory(notsecrets) +endif() + # NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. -add_subdirectory(application) +add_subdirectory(launcher) diff --git a/COPYING.md b/COPYING.md index c0c986066a..4c19bbc22f 100644 --- a/COPYING.md +++ b/COPYING.md @@ -251,3 +251,54 @@ FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# tomlc99 + + MIT License + + Copyright (c) 2017 CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +# O2 (Katabasis fork) + + Copyright (c) 2012, Akos Polster + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index ede8f88f02..0133e58a78 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,28 @@ -

- MultiMC logo -

+**This is a "cracked" version of a popular Minecraft launcher that lets you play the game without a Mojang account.** +This software is not related to MultiMC developers and provided without any warranty. Please don't bomb MultiMC developers if something gets wrong using this launcher. + +Offline mode based by this code: +https://github.com/MultiMC/MultiMC5/commit/6ede3c13b2bcda315e65dd78f2bfd729bc8b699b + +Rewrited for use license and offline accounts at the same time + +Details about the original launcher below: MultiMC 5 ========= -MultiMC is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. It also allows you to easily install and remove mods by simply dragging and dropping. Here are the current [features](https://github.com/MultiMC/MultiMC5/wiki#features) of MultiMC. - +MultiMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity. ## Development -The project uses C++ and Qt5 as the language and base framework. This might seem odd in the Minecraft community, but allows using 25MB of RAM, where other tools use an excessive amount of resources for no reason. +If you want to contribute, talk to us on [Discord](https://discord.gg/multimc) first. -We can do more, with less, on worse hardware and leave more resources for the game while keeping the launcher running and providing extra features. +While blindly submitting PRs is definitely possible, they're not necessarily going to get accepted. -If you want to contribute, either talk to us on [Discord](https://discord.gg/multimc), [IRC](http://webchat.esper.net/?nick=&channels=MultiMC)(esper.net/#MultiMC) or pick up some item from the github issues [workflowy](https://github.com/MultiMC/MultiMC5/issues) - there is always plenty of ideas around. +<<<<<<< HEAD +We aren't looking for flashy features, but expanding upon the existing feature set without distruption or endangering future viability of the project is OK. +======= +If you want to contribute, either talk to us on [Discord](https://discord.gg/0k2zsXGNHs0fE4Wm), [IRC](http://webchat.esper.net/?nick=&channels=MultiMC)(esper.net/#MultiMC) or pick up some item from the github issues [workflowy](https://github.com/Ponywka/MultiMC5-with-offline/issues) - there is always plenty of ideas around. +>>>>>>> 8f2bcfc5 (Offline patch) ### Building If you want to build MultiMC yourself, check [BUILD.md](BUILD.md) for build instructions. @@ -21,19 +30,21 @@ If you want to build MultiMC yourself, check [BUILD.md](BUILD.md) for build inst ### Code formatting Just follow the existing formatting. -In general: -* Indent with 4 space unless it's in a submodule -* Keep lists (of arguments, parameters, initializators...) as lists, not paragraphs. +In general, in order of importance: +* Make sure your IDE is not messing up line endings or whitespace and avoid using linters. * Prefer readability over dogma. +* Keep to the existing formatting. +* Indent with 4 space unless it's in a submodule. +* Keep lists (of arguments, parameters, initializers...) as lists, not paragraphs. It should either read from top to bottom, or left to right. Not both. ## Translations -Translations can be done [on crowdin](https://translate.multimc.org). +Translations can be done [on crowdin](https://translate.multimc.org). Please avoid making direct pull requests to the translations repository. -## Forking/Redistributing +## Forking/Redistributing/Custom builds policy We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license. -Part of the reason for using the Apache license is we don't want people using the "MultiMC" name when redistributing the project. This means people must take the time to go through the source code and remove all references to "MultiMC", including but not limited to the project icon and the title of windows, (no *MultiMC-fork* in the title). +Part of the reason for using the Apache license is that we don't want people using the "MultiMC" name when redistributing the project. This means people must take the time to go through the source code and remove all references to "MultiMC", including but not limited to the project icon and the title of windows, (no *MultiMC-fork* in the title). Apache covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. However, it should be abundantly clear that the project is a fork *without* implying that you have our blessing. diff --git a/api/gui/CMakeLists.txt b/api/gui/CMakeLists.txt deleted file mode 100644 index ad116a43da..0000000000 --- a/api/gui/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -project(MultiMC_gui LANGUAGES CXX) - -set(GUI_SOURCES - DesktopServices.h - DesktopServices.cpp - - # Icons - icons/MMCIcon.h - icons/MMCIcon.cpp - icons/IconList.h - icons/IconList.cpp - - SkinUtils.cpp - SkinUtils.h -) -################################ COMPILE ################################ - -add_library(MultiMC_gui SHARED ${GUI_SOURCES}) -set_target_properties(MultiMC_gui PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1) - -generate_export_header(MultiMC_gui) - -# Link -target_link_libraries(MultiMC_gui MultiMC_iconfix MultiMC_logic Qt5::Gui) - -# Mark and export headers -target_include_directories(MultiMC_gui PUBLIC "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}") - -# Install it -install( - TARGETS MultiMC_gui - RUNTIME DESTINATION ${LIBRARY_DEST_DIR} - LIBRARY DESTINATION ${LIBRARY_DEST_DIR} -) \ No newline at end of file diff --git a/api/gui/DesktopServices.h b/api/gui/DesktopServices.h deleted file mode 100644 index 606fa52cbf..0000000000 --- a/api/gui/DesktopServices.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include "multimc_gui_export.h" - -/** - * This wraps around QDesktopServices and adds workarounds where needed - * Use this instead of QDesktopServices! - */ -namespace DesktopServices -{ - /** - * Open a file in whatever application is applicable - */ - MULTIMC_GUI_EXPORT bool openFile(const QString &path); - - /** - * Open a file in the specified application - */ - MULTIMC_GUI_EXPORT bool openFile(const QString &application, const QString &path, const QString & workingDirectory = QString(), qint64 *pid = 0); - - /** - * Run an application - */ - MULTIMC_GUI_EXPORT bool run(const QString &application,const QStringList &args, const QString & workingDirectory = QString(), qint64 *pid = 0); - - /** - * Open a directory - */ - MULTIMC_GUI_EXPORT bool openDirectory(const QString &path, bool ensureExists = false); - - /** - * Open the URL, most likely in a browser. Maybe. - */ - MULTIMC_GUI_EXPORT bool openUrl(const QUrl &url); -} diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt deleted file mode 100644 index 3193d813ab..0000000000 --- a/api/logic/CMakeLists.txt +++ /dev/null @@ -1,552 +0,0 @@ -project(MultiMC_logic) - -include (UnitTest) - -set(CORE_SOURCES - # LOGIC - Base classes and infrastructure - BaseInstaller.h - BaseInstaller.cpp - BaseVersionList.h - BaseVersionList.cpp - InstanceList.h - InstanceList.cpp - InstanceTask.h - InstanceTask.cpp - LoggedProcess.h - LoggedProcess.cpp - MessageLevel.cpp - MessageLevel.h - BaseVersion.h - BaseInstance.h - BaseInstance.cpp - NullInstance.h - MMCZip.h - MMCZip.cpp - MMCStrings.h - MMCStrings.cpp - - # Basic instance manipulation tasks (derived from InstanceTask) - InstanceCreationTask.h - InstanceCreationTask.cpp - InstanceCopyTask.h - InstanceCopyTask.cpp - InstanceImportTask.h - InstanceImportTask.cpp - - # Use tracking separate from memory management - Usable.h - - # Prefix tree where node names are strings between separators - SeparatorPrefixTree.h - - # WARNING: globals live here - Env.h - Env.cpp - - # String filters - Filter.h - Filter.cpp - - # JSON parsing helpers - Json.h - Json.cpp - - FileSystem.h - FileSystem.cpp - - Exception.h - - # RW lock protected map - RWStorage.h - - # A variable that has an implicit default value and keeps track of changes - DefaultVariable.h - - # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms - QObjectPtr.h - - # Compression support - GZip.h - GZip.cpp - - # Command line parameter parsing - Commandline.h - Commandline.cpp - - # Version number string support - Version.h - Version.cpp - - # A Recursive file system watcher - RecursiveFileSystemWatcher.h - RecursiveFileSystemWatcher.cpp -) - -add_unit_test(FileSystem - SOURCES FileSystem_test.cpp - LIBS MultiMC_logic - DATA testdata - ) - -add_unit_test(GZip - SOURCES GZip_test.cpp - LIBS MultiMC_logic - ) - -set(PATHMATCHER_SOURCES - # Path matchers - pathmatcher/FSTreeMatcher.h - pathmatcher/IPathMatcher.h - pathmatcher/MultiMatcher.h - pathmatcher/RegexpMatcher.h -) - -set(NET_SOURCES - # network stuffs - net/ByteArraySink.h - net/ChecksumValidator.h - net/Download.cpp - net/Download.h - net/FileSink.cpp - net/FileSink.h - net/HttpMetaCache.cpp - net/HttpMetaCache.h - net/MetaCacheSink.cpp - net/MetaCacheSink.h - net/NetAction.h - net/NetJob.cpp - net/NetJob.h - net/PasteUpload.cpp - net/PasteUpload.h - net/Sink.h - net/Validator.h -) - -# Game launch logic -set(LAUNCH_SOURCES - launch/steps/PostLaunchCommand.cpp - launch/steps/PostLaunchCommand.h - launch/steps/PreLaunchCommand.cpp - launch/steps/PreLaunchCommand.h - launch/steps/TextPrint.cpp - launch/steps/TextPrint.h - launch/steps/Update.cpp - launch/steps/Update.h - launch/LaunchStep.cpp - launch/LaunchStep.h - launch/LaunchTask.cpp - launch/LaunchTask.h - launch/LogModel.cpp - launch/LogModel.h -) - -# Old update system -set(UPDATE_SOURCES - updater/GoUpdate.h - updater/GoUpdate.cpp - updater/UpdateChecker.h - updater/UpdateChecker.cpp - updater/DownloadTask.h - updater/DownloadTask.cpp -) - -add_unit_test(UpdateChecker - SOURCES updater/UpdateChecker_test.cpp - LIBS MultiMC_logic - DATA updater/testdata - ) - -add_unit_test(DownloadTask - SOURCES updater/DownloadTask_test.cpp - LIBS MultiMC_logic - DATA updater/testdata - ) - -# Rarely used notifications -set(NOTIFICATIONS_SOURCES - # Notifications - short warning messages - notifications/NotificationChecker.h - notifications/NotificationChecker.cpp -) - -# Backend for the news bar... there's usually no news. -set(NEWS_SOURCES - # News System - news/NewsChecker.h - news/NewsChecker.cpp - news/NewsEntry.h - news/NewsEntry.cpp -) - -# Icon interface -set(ICONS_SOURCES - # Icons System and related code - icons/IIconList.h - icons/IIconList.cpp - icons/IconUtils.h - icons/IconUtils.cpp -) - -# Minecraft services status checker -set(STATUS_SOURCES - # Status system - status/StatusChecker.h - status/StatusChecker.cpp -) - -# Support for Minecraft instances and launch -set(MINECRAFT_SOURCES - # Minecraft support - minecraft/auth/AuthSession.h - minecraft/auth/AuthSession.cpp - minecraft/auth/MojangAccountList.h - minecraft/auth/MojangAccountList.cpp - minecraft/auth/MojangAccount.h - minecraft/auth/MojangAccount.cpp - minecraft/auth/YggdrasilTask.h - minecraft/auth/YggdrasilTask.cpp - minecraft/auth/flows/AuthenticateTask.h - minecraft/auth/flows/AuthenticateTask.cpp - minecraft/auth/flows/RefreshTask.cpp - minecraft/auth/flows/RefreshTask.cpp - minecraft/auth/flows/ValidateTask.h - minecraft/auth/flows/ValidateTask.cpp - - minecraft/gameoptions/GameOptions.h - minecraft/gameoptions/GameOptions.cpp - - minecraft/update/AssetUpdateTask.h - minecraft/update/AssetUpdateTask.cpp - minecraft/update/FMLLibrariesTask.cpp - minecraft/update/FMLLibrariesTask.h - minecraft/update/FoldersTask.cpp - minecraft/update/FoldersTask.h - minecraft/update/LibrariesTask.cpp - minecraft/update/LibrariesTask.h - - minecraft/launch/ClaimAccount.cpp - minecraft/launch/ClaimAccount.h - minecraft/launch/CreateGameFolders.cpp - minecraft/launch/CreateGameFolders.h - minecraft/launch/ModMinecraftJar.cpp - minecraft/launch/ModMinecraftJar.h - minecraft/launch/DirectJavaLaunch.cpp - minecraft/launch/DirectJavaLaunch.h - minecraft/launch/ExtractNatives.cpp - minecraft/launch/ExtractNatives.h - minecraft/launch/LauncherPartLaunch.cpp - minecraft/launch/LauncherPartLaunch.h - minecraft/launch/PrintInstanceInfo.cpp - minecraft/launch/PrintInstanceInfo.h - minecraft/launch/ReconstructAssets.cpp - minecraft/launch/ReconstructAssets.h - minecraft/launch/ScanModFolders.cpp - minecraft/launch/ScanModFolders.h - - minecraft/legacy/LegacyModList.h - minecraft/legacy/LegacyModList.cpp - minecraft/legacy/LegacyInstance.h - minecraft/legacy/LegacyInstance.cpp - minecraft/legacy/LegacyUpgradeTask.h - minecraft/legacy/LegacyUpgradeTask.cpp - - minecraft/GradleSpecifier.h - minecraft/MinecraftInstance.cpp - minecraft/MinecraftInstance.h - minecraft/LaunchProfile.cpp - minecraft/LaunchProfile.h - minecraft/Component.cpp - minecraft/Component.h - minecraft/PackProfile.cpp - minecraft/PackProfile.h - minecraft/ComponentUpdateTask.cpp - minecraft/ComponentUpdateTask.h - minecraft/MinecraftLoadAndCheck.h - minecraft/MinecraftLoadAndCheck.cpp - minecraft/MinecraftUpdate.h - minecraft/MinecraftUpdate.cpp - minecraft/MojangVersionFormat.cpp - minecraft/MojangVersionFormat.h - minecraft/Rule.cpp - minecraft/Rule.h - minecraft/OneSixVersionFormat.cpp - minecraft/OneSixVersionFormat.h - minecraft/OpSys.cpp - minecraft/OpSys.h - minecraft/ParseUtils.cpp - minecraft/ParseUtils.h - minecraft/ProfileUtils.cpp - minecraft/ProfileUtils.h - minecraft/Library.cpp - minecraft/Library.h - minecraft/MojangDownloadInfo.h - minecraft/VersionFile.cpp - minecraft/VersionFile.h - minecraft/VersionFilterData.h - minecraft/VersionFilterData.cpp - minecraft/World.h - minecraft/World.cpp - minecraft/WorldList.h - minecraft/WorldList.cpp - - minecraft/mod/Mod.h - minecraft/mod/Mod.cpp - minecraft/mod/ModDetails.h - minecraft/mod/ModFolderModel.h - minecraft/mod/ModFolderModel.cpp - minecraft/mod/ModFolderLoadTask.h - minecraft/mod/ModFolderLoadTask.cpp - minecraft/mod/LocalModParseTask.h - minecraft/mod/LocalModParseTask.cpp - - # Assets - minecraft/AssetsUtils.h - minecraft/AssetsUtils.cpp - - # Minecraft services - minecraft/services/SkinUpload.cpp - minecraft/services/SkinUpload.h - minecraft/services/SkinDelete.cpp - minecraft/services/SkinDelete.h - - mojang/PackageManifest.h - mojang/PackageManifest.cpp - ) - -add_unit_test(GradleSpecifier - SOURCES minecraft/GradleSpecifier_test.cpp - LIBS MultiMC_logic - ) - -add_executable(PackageManifest - mojang/PackageManifest_test.cpp -) -target_link_libraries(PackageManifest - MultiMC_logic - Qt5::Test -) -target_include_directories(PackageManifest - PRIVATE ../../cmake/UnitTest/ -) -add_test( - NAME PackageManifest - COMMAND PackageManifest - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) - -add_unit_test(MojangVersionFormat - SOURCES minecraft/MojangVersionFormat_test.cpp - LIBS MultiMC_logic - DATA minecraft/testdata - ) - -add_unit_test(Library - SOURCES minecraft/Library_test.cpp - LIBS MultiMC_logic - ) - -# FIXME: shares data with FileSystem test -add_unit_test(ModFolderModel - SOURCES minecraft/mod/ModFolderModel_test.cpp - DATA testdata - LIBS MultiMC_logic - ) - -add_unit_test(ParseUtils - SOURCES minecraft/ParseUtils_test.cpp - LIBS MultiMC_logic - ) - -# the screenshots feature -set(SCREENSHOTS_SOURCES - screenshots/Screenshot.h - screenshots/ImgurUpload.h - screenshots/ImgurUpload.cpp - screenshots/ImgurAlbumCreation.h - screenshots/ImgurAlbumCreation.cpp -) - -set(TASKS_SOURCES - # Tasks - tasks/Task.h - tasks/Task.cpp - tasks/SequentialTask.h - tasks/SequentialTask.cpp -) - -set(SETTINGS_SOURCES - # Settings - settings/INIFile.cpp - settings/INIFile.h - settings/INISettingsObject.cpp - settings/INISettingsObject.h - settings/OverrideSetting.cpp - settings/OverrideSetting.h - settings/PassthroughSetting.cpp - settings/PassthroughSetting.h - settings/Setting.cpp - settings/Setting.h - settings/SettingsObject.cpp - settings/SettingsObject.h -) - -add_unit_test(INIFile - SOURCES settings/INIFile_test.cpp - LIBS MultiMC_logic - ) - -set(JAVA_SOURCES - # Java related code - java/launch/CheckJava.cpp - java/launch/CheckJava.h - java/JavaChecker.h - java/JavaChecker.cpp - java/JavaCheckerJob.h - java/JavaCheckerJob.cpp - java/JavaInstall.h - java/JavaInstall.cpp - java/JavaInstallList.h - java/JavaInstallList.cpp - java/JavaUtils.h - java/JavaUtils.cpp - java/JavaVersion.h - java/JavaVersion.cpp -) - -add_unit_test(JavaVersion - SOURCES java/JavaVersion_test.cpp - LIBS MultiMC_logic - ) - -set(TRANSLATIONS_SOURCES - translations/TranslationsModel.h - translations/TranslationsModel.cpp - translations/POTranslator.h - translations/POTranslator.cpp -) - -set(TOOLS_SOURCES - # Tools - tools/BaseExternalTool.cpp - tools/BaseExternalTool.h - tools/BaseProfiler.cpp - tools/BaseProfiler.h - tools/JProfiler.cpp - tools/JProfiler.h - tools/JVisualVM.cpp - tools/JVisualVM.h - tools/MCEditTool.cpp - tools/MCEditTool.h -) - -set(META_SOURCES - # Metadata sources - meta/JsonFormat.cpp - meta/JsonFormat.h - meta/BaseEntity.cpp - meta/BaseEntity.h - meta/VersionList.cpp - meta/VersionList.h - meta/Version.cpp - meta/Version.h - meta/Index.cpp - meta/Index.h -) - -set(FTB_SOURCES - modplatform/legacy_ftb/PackFetchTask.h - modplatform/legacy_ftb/PackFetchTask.cpp - modplatform/legacy_ftb/PackInstallTask.h - modplatform/legacy_ftb/PackInstallTask.cpp - modplatform/legacy_ftb/PrivatePackManager.h - modplatform/legacy_ftb/PrivatePackManager.cpp - - modplatform/legacy_ftb/PackHelpers.h -) - -set(FLAME_SOURCES - # Flame - modplatform/flame/PackManifest.h - modplatform/flame/PackManifest.cpp - modplatform/flame/FileResolvingTask.h - modplatform/flame/FileResolvingTask.cpp -) - -set(MODPACKSCH_SOURCES - modplatform/modpacksch/FTBPackInstallTask.h - modplatform/modpacksch/FTBPackInstallTask.cpp - modplatform/modpacksch/FTBPackManifest.h - modplatform/modpacksch/FTBPackManifest.cpp -) - -set(TECHNIC_SOURCES - modplatform/technic/SingleZipPackInstallTask.h - modplatform/technic/SingleZipPackInstallTask.cpp - modplatform/technic/SolderPackInstallTask.h - modplatform/technic/SolderPackInstallTask.cpp - modplatform/technic/TechnicPackProcessor.h - modplatform/technic/TechnicPackProcessor.cpp -) - -set(ATLAUNCHER_SOURCES - modplatform/atlauncher/ATLPackIndex.cpp - modplatform/atlauncher/ATLPackIndex.h - modplatform/atlauncher/ATLPackInstallTask.cpp - modplatform/atlauncher/ATLPackInstallTask.h - modplatform/atlauncher/ATLPackManifest.cpp - modplatform/atlauncher/ATLPackManifest.h -) - -add_unit_test(Index - SOURCES meta/Index_test.cpp - LIBS MultiMC_logic - ) - -################################ COMPILE ################################ - -# we need zlib -find_package(ZLIB REQUIRED) - -set(LOGIC_SOURCES - ${CORE_SOURCES} - ${PATHMATCHER_SOURCES} - ${NET_SOURCES} - ${LAUNCH_SOURCES} - ${UPDATE_SOURCES} - ${NOTIFICATIONS_SOURCES} - ${NEWS_SOURCES} - ${STATUS_SOURCES} - ${MINECRAFT_SOURCES} - ${SCREENSHOTS_SOURCES} - ${TASKS_SOURCES} - ${SETTINGS_SOURCES} - ${JAVA_SOURCES} - ${TRANSLATIONS_SOURCES} - ${TOOLS_SOURCES} - ${META_SOURCES} - ${ICONS_SOURCES} - ${FTB_SOURCES} - ${FLAME_SOURCES} - ${MODPACKSCH_SOURCES} - ${TECHNIC_SOURCES} - ${ATLAUNCHER_SOURCES} -) - -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 systeminfo MultiMC_quazip MultiMC_classparser ${NBT_NAME} ${ZLIB_LIBRARIES} optional-bare BuildConfig) -target_link_libraries(MultiMC_logic Qt5::Core Qt5::Xml Qt5::Network Qt5::Concurrent) - -# Mark and export headers -target_include_directories(MultiMC_logic PUBLIC "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}" PRIVATE "${ZLIB_INCLUDE_DIRS}") - -# Install it -install( - TARGETS MultiMC_logic - RUNTIME DESTINATION ${LIBRARY_DEST_DIR} - LIBRARY DESTINATION ${LIBRARY_DEST_DIR} -) diff --git a/api/logic/MMCStrings.h b/api/logic/MMCStrings.h deleted file mode 100644 index 493ba3d2fc..0000000000 --- a/api/logic/MMCStrings.h +++ /dev/null @@ -1,10 +0,0 @@ -#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/minecraft/auth/MojangAccount.cpp b/api/logic/minecraft/auth/MojangAccount.cpp deleted file mode 100644 index f5853fe335..0000000000 --- a/api/logic/minecraft/auth/MojangAccount.cpp +++ /dev/null @@ -1,315 +0,0 @@ -/* Copyright 2013-2021 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(accountStatus() == Verified && !session->wants_online) - { - session->status = AuthSession::PlayableOffline; - session->auth_server_online = false; - fillSession(session); - return nullptr; - } - else - { - if (password.isEmpty()) - { - m_currentTask.reset(new RefreshTask(this)); - } - else - { - m_currentTask.reset(new AuthenticateTask(this, password)); - } - m_currentTask->assignSession(session); - - connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); - connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); - } - return m_currentTask; -} - -void MojangAccount::authSucceeded() -{ - auto session = m_currentTask->getAssignedSession(); - if (session) - { - session->status = - session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline; - fillSession(session); - session->auth_server_online = true; - } - m_currentTask.reset(); - emit changed(); -} - -void MojangAccount::authFailed(QString reason) -{ - auto session = m_currentTask->getAssignedSession(); - // This is emitted when the yggdrasil tasks time out or are cancelled. - // -> we treat the error as no-op - if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT) - { - if (session) - { - session->status = accountStatus() == Verified ? AuthSession::PlayableOffline - : AuthSession::RequiresPassword; - session->auth_server_online = false; - fillSession(session); - } - } - else - { - m_accessToken = QString(); - emit changed(); - if (session) - { - session->status = AuthSession::RequiresPassword; - session->auth_server_online = true; - fillSession(session); - } - } - m_currentTask.reset(); -} - -void MojangAccount::fillSession(AuthSessionPtr session) -{ - // the user name. you have to have an user name - session->username = m_username; - // volatile auth token - session->access_token = m_accessToken; - // the semi-permanent client token - session->client_token = m_clientToken; - if (currentProfile()) - { - // profile name - session->player_name = currentProfile()->name; - // profile ID - session->uuid = currentProfile()->id; - // 'legacy' or 'mojang', depending on account type - session->user_type = currentProfile()->legacy ? "legacy" : "mojang"; - if (!session->access_token.isEmpty()) - { - session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id; - } - else - { - session->session = "-"; - } - } - else - { - session->player_name = "Player"; - session->session = "-"; - } - session->u = user(); - session->m_accountPtr = shared_from_this(); -} - -void MojangAccount::decrementUses() -{ - Usable::decrementUses(); - if(!isInUse()) - { - emit changed(); - qWarning() << "Account" << m_username << "is no longer in use."; - } -} - -void MojangAccount::incrementUses() -{ - bool wasInUse = isInUse(); - Usable::incrementUses(); - if(!wasInUse) - { - emit changed(); - qWarning() << "Account" << m_username << "is now in use."; - } -} - -void MojangAccount::invalidateClientToken() -{ - m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); - emit changed(); -} diff --git a/api/logic/minecraft/auth/MojangAccountList.h b/api/logic/minecraft/auth/MojangAccountList.h deleted file mode 100644 index cc3a61a277..0000000000 --- a/api/logic/minecraft/auth/MojangAccountList.h +++ /dev/null @@ -1,201 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "MojangAccount.h" - -#include -#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 deleted file mode 100644 index 0857b46bd3..0000000000 --- a/api/logic/minecraft/auth/YggdrasilTask.cpp +++ /dev/null @@ -1,255 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "YggdrasilTask.h" -#include "MojangAccount.h" - -#include -#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()); - - QUrl reqUrl(BuildConfig.AUTH_BASE + getEndpoint()); - QNetworkRequest netRequest(reqUrl); - netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - QByteArray requestData = doc.toJson(); - m_netReply = ENV.qnam().post(netRequest, requestData); - connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply); - connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers); - connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers); - connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors); - timeout_keeper.setSingleShot(true); - timeout_keeper.start(timeout_max); - counter.setSingleShot(false); - counter.start(time_step); - progress(0, timeout_max); - connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout); - connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat); -} - -void YggdrasilTask::refreshTimers(qint64, qint64) -{ - timeout_keeper.stop(); - timeout_keeper.start(timeout_max); - progress(count = 0, timeout_max); -} -void YggdrasilTask::heartbeat() -{ - count += time_step; - progress(count, timeout_max); -} - -bool YggdrasilTask::abort() -{ - progress(timeout_max, timeout_max); - // TODO: actually use this in a meaningful way - m_aborted = YggdrasilTask::BY_USER; - m_netReply->abort(); - return true; -} - -void YggdrasilTask::abortByTimeout() -{ - progress(timeout_max, timeout_max); - // TODO: actually use this in a meaningful way - m_aborted = YggdrasilTask::BY_TIMEOUT; - m_netReply->abort(); -} - -void YggdrasilTask::sslErrors(QList 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::ContentAccessDenied: - case QNetworkReply::ContentOperationNotPermittedError: - break; - default: - changeState(STATE_FAILED_SOFT, - tr("Authentication operation failed due to a network error: %1 (%2)") - .arg(m_netReply->errorString()).arg(m_netReply->error())); - return; - } - - // Try to parse the response regardless of the response code. - // Sometimes the auth server will give more information and an error code. - QJsonParseError jsonError; - QByteArray replyData = m_netReply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); - // Check the response code. - int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (responseCode == 200) - { - // If the response code was 200, then there shouldn't be an error. Make sure - // anyways. - // Also, sometimes an empty reply indicates success. If there was no data received, - // pass an empty json object to the processResponse function. - if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) - { - processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); - return; - } - else - { - changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response " - "JSON response: %1 at offset %2.") - .arg(jsonError.errorString()) - .arg(jsonError.offset)); - qCritical() << replyData; - } - return; - } - - // If the response code was not 200, then Yggdrasil may have given us information - // about the error. - // If we can parse the response, then get information from it. Otherwise just say - // there was an unknown error. - if (jsonError.error == QJsonParseError::NoError) - { - // We were able to parse the server's response. Woo! - // Call processError. If a subclass has overridden it then they'll handle their - // stuff there. - qDebug() << "The request failed, but the server gave us an error message. " - "Processing error."; - processError(doc.object()); - } - else - { - // The server didn't say anything regarding the error. Give the user an unknown - // error. - qDebug() - << "The request failed and the server gave no error message. Unknown error."; - changeState(STATE_FAILED_SOFT, - tr("An unknown error occurred when trying to communicate with the " - "authentication server: %1").arg(m_netReply->errorString())); - } -} - -void YggdrasilTask::processError(QJsonObject responseData) -{ - QJsonValue errorVal = responseData.value("error"); - QJsonValue errorMessageValue = responseData.value("errorMessage"); - QJsonValue causeVal = responseData.value("cause"); - - if (errorVal.isString() && errorMessageValue.isString()) - { - m_error = std::shared_ptr(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/flows/AuthenticateTask.cpp b/api/logic/minecraft/auth/flows/AuthenticateTask.cpp deleted file mode 100644 index 2e8dc8595b..0000000000 --- a/api/logic/minecraft/auth/flows/AuthenticateTask.cpp +++ /dev/null @@ -1,202 +0,0 @@ - -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "AuthenticateTask.h" -#include "../MojangAccount.h" - -#include -#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 deleted file mode 100644 index 4c14eec7a9..0000000000 --- a/api/logic/minecraft/auth/flows/AuthenticateTask.h +++ /dev/null @@ -1,46 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "../YggdrasilTask.h" - -#include -#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 deleted file mode 100644 index ecba178da5..0000000000 --- a/api/logic/minecraft/auth/flows/RefreshTask.cpp +++ /dev/null @@ -1,144 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "RefreshTask.h" -#include "../MojangAccount.h" - -#include -#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 deleted file mode 100644 index f0840ddaf3..0000000000 --- a/api/logic/minecraft/auth/flows/RefreshTask.h +++ /dev/null @@ -1,44 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "../YggdrasilTask.h" - -#include -#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 deleted file mode 100644 index 6b3f0a65ca..0000000000 --- a/api/logic/minecraft/auth/flows/ValidateTask.cpp +++ /dev/null @@ -1,61 +0,0 @@ - -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "ValidateTask.h" -#include "../MojangAccount.h" - -#include -#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/application/CMakeLists.txt b/application/CMakeLists.txt deleted file mode 100644 index a81327e340..0000000000 --- a/application/CMakeLists.txt +++ /dev/null @@ -1,414 +0,0 @@ -project(application) - -################################ FILES ################################ - -######## Sources and headers ######## -SET(MULTIMC_SOURCES - # Application base - main.cpp - MultiMC.h - MultiMC.cpp - UpdateController.cpp - UpdateController.h - - # GUI - general utilities - GuiUtil.h - GuiUtil.cpp - ColumnResizer.h - ColumnResizer.cpp - InstanceProxyModel.h - InstanceProxyModel.cpp - VersionProxyModel.h - VersionProxyModel.cpp - ColorCache.h - ColorCache.cpp - HoeDown.h - - # Super secret! - KonamiCode.h - KonamiCode.cpp - - # GUI - windows - MainWindow.h - MainWindow.cpp - InstanceWindow.h - InstanceWindow.cpp - - # GUI - setup wizard - setupwizard/SetupWizard.h - setupwizard/SetupWizard.cpp - setupwizard/AnalyticsWizardPage.cpp - setupwizard/AnalyticsWizardPage.h - setupwizard/BaseWizardPage.h - setupwizard/JavaWizardPage.cpp - setupwizard/JavaWizardPage.h - setupwizard/LanguageWizardPage.cpp - setupwizard/LanguageWizardPage.h - - # GUI - themes - themes/FusionTheme.cpp - themes/FusionTheme.h - themes/BrightTheme.cpp - themes/BrightTheme.h - themes/CustomTheme.cpp - themes/CustomTheme.h - themes/DarkTheme.cpp - themes/DarkTheme.h - themes/ITheme.cpp - themes/ITheme.h - themes/SystemTheme.cpp - themes/SystemTheme.h - - # Processes - LaunchController.h - LaunchController.cpp - - # page provider for instances - InstancePageProvider.h - - # Common java checking UI - JavaCommon.h - JavaCommon.cpp - - # GUI - paged dialog base - pages/BasePage.h - pages/BasePageContainer.h - pages/BasePageProvider.h - - # GUI - instance pages - pages/instance/GameOptionsPage.cpp - pages/instance/GameOptionsPage.h - pages/instance/VersionPage.cpp - pages/instance/VersionPage.h - pages/instance/TexturePackPage.h - pages/instance/ResourcePackPage.h - pages/instance/ModFolderPage.cpp - pages/instance/ModFolderPage.h - pages/instance/NotesPage.cpp - pages/instance/NotesPage.h - pages/instance/LogPage.cpp - pages/instance/LogPage.h - pages/instance/InstanceSettingsPage.cpp - pages/instance/InstanceSettingsPage.h - pages/instance/ScreenshotsPage.cpp - pages/instance/ScreenshotsPage.h - pages/instance/OtherLogsPage.cpp - pages/instance/OtherLogsPage.h - pages/instance/ServersPage.cpp - pages/instance/ServersPage.h - pages/instance/LegacyUpgradePage.cpp - pages/instance/LegacyUpgradePage.h - pages/instance/WorldListPage.cpp - pages/instance/WorldListPage.h - - # GUI - global settings pages - pages/global/AccountListPage.cpp - pages/global/AccountListPage.h - pages/global/CustomCommandsPage.cpp - pages/global/CustomCommandsPage.h - pages/global/ExternalToolsPage.cpp - pages/global/ExternalToolsPage.h - pages/global/JavaPage.cpp - pages/global/JavaPage.h - pages/global/LanguagePage.cpp - pages/global/LanguagePage.h - pages/global/MinecraftPage.cpp - pages/global/MinecraftPage.h - pages/global/MultiMCPage.cpp - pages/global/MultiMCPage.h - pages/global/ProxyPage.cpp - pages/global/ProxyPage.h - pages/global/PasteEEPage.cpp - pages/global/PasteEEPage.h - - # GUI - platform pages - pages/modplatform/VanillaPage.cpp - pages/modplatform/VanillaPage.h - - pages/modplatform/atlauncher/AtlModel.cpp - pages/modplatform/atlauncher/AtlModel.h - pages/modplatform/atlauncher/AtlFilterModel.cpp - pages/modplatform/atlauncher/AtlFilterModel.h - pages/modplatform/atlauncher/AtlPage.cpp - pages/modplatform/atlauncher/AtlPage.h - pages/modplatform/atlauncher/AtlPage.h - - pages/modplatform/ftb/FtbFilterModel.cpp - pages/modplatform/ftb/FtbFilterModel.h - pages/modplatform/ftb/FtbListModel.cpp - pages/modplatform/ftb/FtbListModel.h - pages/modplatform/ftb/FtbPage.cpp - pages/modplatform/ftb/FtbPage.h - - pages/modplatform/legacy_ftb/Page.cpp - pages/modplatform/legacy_ftb/Page.h - pages/modplatform/legacy_ftb/ListModel.h - pages/modplatform/legacy_ftb/ListModel.cpp - - pages/modplatform/twitch/TwitchData.h - pages/modplatform/twitch/TwitchModel.cpp - pages/modplatform/twitch/TwitchModel.h - pages/modplatform/twitch/TwitchPage.cpp - pages/modplatform/twitch/TwitchPage.h - - pages/modplatform/technic/TechnicModel.cpp - pages/modplatform/technic/TechnicModel.h - pages/modplatform/technic/TechnicPage.cpp - pages/modplatform/technic/TechnicPage.h - - pages/modplatform/ImportPage.cpp - pages/modplatform/ImportPage.h - - # GUI - dialogs - dialogs/AboutDialog.cpp - dialogs/AboutDialog.h - dialogs/ProfileSelectDialog.cpp - dialogs/ProfileSelectDialog.h - dialogs/CopyInstanceDialog.cpp - dialogs/CopyInstanceDialog.h - dialogs/CustomMessageBox.cpp - dialogs/CustomMessageBox.h - dialogs/EditAccountDialog.cpp - dialogs/EditAccountDialog.h - dialogs/ExportInstanceDialog.cpp - dialogs/ExportInstanceDialog.h - dialogs/IconPickerDialog.cpp - dialogs/IconPickerDialog.h - dialogs/LoginDialog.cpp - dialogs/LoginDialog.h - dialogs/NewComponentDialog.cpp - dialogs/NewComponentDialog.h - dialogs/NewInstanceDialog.cpp - dialogs/NewInstanceDialog.h - dialogs/NotificationDialog.cpp - dialogs/NotificationDialog.h - pagedialog/PageDialog.cpp - pagedialog/PageDialog.h - dialogs/ProgressDialog.cpp - dialogs/ProgressDialog.h - dialogs/UpdateDialog.cpp - dialogs/UpdateDialog.h - dialogs/VersionSelectDialog.cpp - dialogs/VersionSelectDialog.h - dialogs/SkinUploadDialog.cpp - dialogs/SkinUploadDialog.h - - - # GUI - widgets - widgets/Common.cpp - widgets/Common.h - widgets/CustomCommands.cpp - widgets/CustomCommands.h - widgets/DropLabel.cpp - widgets/DropLabel.h - widgets/FocusLineEdit.cpp - widgets/FocusLineEdit.h - widgets/IconLabel.cpp - widgets/IconLabel.h - widgets/JavaSettingsWidget.cpp - widgets/JavaSettingsWidget.h - widgets/LabeledToolButton.cpp - widgets/LabeledToolButton.h - widgets/LanguageSelectionWidget.cpp - widgets/LanguageSelectionWidget.h - widgets/LineSeparator.cpp - widgets/LineSeparator.h - widgets/LogView.cpp - widgets/LogView.h - widgets/MCModInfoFrame.cpp - widgets/MCModInfoFrame.h - widgets/ModListView.cpp - widgets/ModListView.h - widgets/PageContainer.cpp - widgets/PageContainer.h - widgets/PageContainer_p.h - widgets/ServerStatus.cpp - widgets/ServerStatus.h - widgets/VersionListView.cpp - widgets/VersionListView.h - widgets/VersionSelectWidget.cpp - widgets/VersionSelectWidget.h - widgets/ProgressWidget.h - widgets/ProgressWidget.cpp - widgets/WideBar.h - widgets/WideBar.cpp - - # GUI - instance group view - groupview/GroupedProxyModel.cpp - groupview/GroupedProxyModel.h - groupview/AccessibleGroupView.cpp - groupview/AccessibleGroupView.h - groupview/AccessibleGroupView_p.h - groupview/GroupView.cpp - groupview/GroupView.h - groupview/InstanceDelegate.cpp - groupview/InstanceDelegate.h - groupview/VisualGroup.cpp - groupview/VisualGroup.h - ) - -######## UIs ######## -SET(MULTIMC_UIS - # Instance pages - pages/instance/GameOptionsPage.ui - pages/instance/VersionPage.ui - pages/instance/ModFolderPage.ui - pages/instance/LogPage.ui - pages/instance/InstanceSettingsPage.ui - pages/instance/NotesPage.ui - pages/instance/ScreenshotsPage.ui - pages/instance/OtherLogsPage.ui - pages/instance/LegacyUpgradePage.ui - pages/instance/ServersPage.ui - pages/instance/WorldListPage.ui - - # Global settings pages - pages/global/AccountListPage.ui - pages/global/ExternalToolsPage.ui - pages/global/JavaPage.ui - pages/global/MinecraftPage.ui - pages/global/MultiMCPage.ui - pages/global/ProxyPage.ui - pages/global/PasteEEPage.ui - - # Platform pages - pages/modplatform/VanillaPage.ui - pages/modplatform/atlauncher/AtlPage.ui - pages/modplatform/ftb/FtbPage.ui - pages/modplatform/legacy_ftb/Page.ui - pages/modplatform/twitch/TwitchPage.ui - pages/modplatform/technic/TechnicPage.ui - pages/modplatform/ImportPage.ui - - # Dialogs - dialogs/CopyInstanceDialog.ui - dialogs/NewComponentDialog.ui - dialogs/NewInstanceDialog.ui - dialogs/AboutDialog.ui - dialogs/ProgressDialog.ui - dialogs/IconPickerDialog.ui - dialogs/ProfileSelectDialog.ui - dialogs/EditAccountDialog.ui - dialogs/ExportInstanceDialog.ui - dialogs/LoginDialog.ui - dialogs/UpdateDialog.ui - dialogs/NotificationDialog.ui - dialogs/SkinUploadDialog.ui - - # Widgets/other - widgets/CustomCommands.ui - widgets/MCModInfoFrame.ui -) - -set(MULTIMC_QRCS - resources/backgrounds/backgrounds.qrc - resources/multimc/multimc.qrc - resources/pe_dark/pe_dark.qrc - resources/pe_light/pe_light.qrc - resources/pe_colored/pe_colored.qrc - resources/pe_blue/pe_blue.qrc - resources/OSX/OSX.qrc - resources/iOS/iOS.qrc - resources/flat/flat.qrc - resources/documents/documents.qrc -) - -######## Windows resource files ######## -if(WIN32) - set(MULTIMC_RCS resources/multimc.rc) -endif() - -# Qt 5 stuff -qt5_wrap_ui(MULTIMC_UI ${MULTIMC_UIS}) -qt5_add_resources(MULTIMC_RESOURCES ${MULTIMC_QRCS}) - -# Add executable -add_executable(MultiMC MACOSX_BUNDLE WIN32 ${MULTIMC_SOURCES} ${MULTIMC_UI} ${MULTIMC_RESOURCES} ${MULTIMC_RCS}) -target_link_libraries(MultiMC MultiMC_gui ${QUAZIP_LIBRARIES} hoedown MultiMC_rainbow LocalPeer ganalytics) -if(DEFINED MultiMC_APP_BINARY_NAME) - set_target_properties(MultiMC PROPERTIES OUTPUT_NAME "${MultiMC_APP_BINARY_NAME}") -endif() -if(DEFINED MultiMC_BINARY_RPATH) - SET_TARGET_PROPERTIES(MultiMC PROPERTIES INSTALL_RPATH "${MultiMC_BINARY_RPATH}") -endif() -if(DEFINED MultiMC_APP_BINARY_DEFS) - target_compile_definitions(MultiMC PRIVATE ${MultiMC_APP_BINARY_DEFS}) -endif() - -install(TARGETS MultiMC - BUNDLE DESTINATION ${BUNDLE_DEST_DIR} COMPONENT Runtime - LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime - RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime -) - -#### The MultiMC bundle mess! #### -# Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. -# NOTE: it seems that this absolutely has to be here, and nowhere else. -if(INSTALL_BUNDLE STREQUAL "full") - # Add qt.conf - this makes Qt stop looking for things outside the bundle - install( - CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" - COMPONENT Runtime - ) - # Bundle plugins - if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") - # Image formats - install( - DIRECTORY "${QT_PLUGINS_DIR}/imageformats" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "tga|tiff|mng|webp" EXCLUDE - ) - # Icon engines - install( - DIRECTORY "${QT_PLUGINS_DIR}/iconengines" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "fontawesome" EXCLUDE - ) - # Platform plugins - install( - DIRECTORY "${QT_PLUGINS_DIR}/platforms" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "minimal|linuxfb|offscreen" EXCLUDE - ) - else() - # Image formats - install( - DIRECTORY "${QT_PLUGINS_DIR}/imageformats" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "tga|tiff|mng|webp" EXCLUDE - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - # Icon engines - install( - DIRECTORY "${QT_PLUGINS_DIR}/iconengines" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "fontawesome" EXCLUDE - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - # Platform plugins - install( - DIRECTORY "${QT_PLUGINS_DIR}/platforms" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "minimal|linuxfb|offscreen" EXCLUDE - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" - @ONLY - ) - install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" COMPONENT Runtime) -endif() diff --git a/application/LaunchController.cpp b/application/LaunchController.cpp deleted file mode 100644 index bebc3db1b8..0000000000 --- a/application/LaunchController.cpp +++ /dev/null @@ -1,311 +0,0 @@ -#include "LaunchController.h" -#include "MainWindow.h" -#include -#include "MultiMC.h" -#include "dialogs/CustomMessageBox.h" -#include "dialogs/ProfileSelectDialog.h" -#include "dialogs/ProgressDialog.h" -#include "dialogs/EditAccountDialog.h" -#include "InstanceWindow.h" -#include "BuildConfig.h" -#include "JavaCommon.h" -#include -#include -#include -#include -#include -#include - -LaunchController::LaunchController(QObject *parent) : Task(parent) -{ -} - -void LaunchController::executeTask() -{ - if (!m_instance) - { - emitFailed(tr("No instance specified!")); - return; - } - - login(); -} - -// FIXME: minecraft specific -void LaunchController::login() -{ - JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); - - // Find an account to use. - std::shared_ptr accounts = MMC->accounts(); - MojangAccountPtr account = accounts->activeAccount(); - if (accounts->count() <= 0) - { - // Tell the user they need to log in at least one account in order to play. - auto reply = CustomMessageBox::selectable( - m_parentWidget, tr("No Accounts"), - tr("In order to play Minecraft, you must have at least one Mojang or Minecraft " - "account logged in to MultiMC." - "Would you like to open the account manager to add an account now?"), - QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec(); - - if (reply == QMessageBox::Yes) - { - // Open the account manager. - MMC->ShowGlobalSettings(m_parentWidget, "accounts"); - } - } - else if (account.get() == nullptr) - { - // If no default account is set, ask the user which one to use. - ProfileSelectDialog selectDialog(tr("Which profile would you like to use?"), - ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget); - - selectDialog.exec(); - - // Launch the instance with the selected account. - account = selectDialog.selectedAccount(); - - // If the user said to use the account as default, do that. - if (selectDialog.useAsGlobalDefault() && account.get() != nullptr) - accounts->setActiveAccount(account->username()); - } - - // if no account is selected, we bail - if (!account.get()) - { - emitFailed(tr("No account selected for launch.")); - return; - } - - // we try empty password first :) - QString password; - // we loop until the user succeeds in logging in or gives up - bool tryagain = true; - // the failure. the default failure. - const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again.

This could be caused by a password change."); - QString failReason = needLoginAgain; - - while (tryagain) - { - m_session = std::make_shared(); - m_session->wants_online = m_online; - auto task = account->login(m_session, password); - if (task) - { - // We'll need to validate the access token to make sure the account - // is still logged in. - ProgressDialog progDialog(m_parentWidget); - if (m_online) - { - progDialog.setSkipButton(true, tr("Play Offline")); - } - progDialog.execWithTask(task.get()); - if (!task->wasSuccessful()) - { - auto failReasonNew = task->failReason(); - if(failReasonNew == "Invalid token.") - { - account->invalidateClientToken(); - failReason = needLoginAgain; - } - else failReason = failReasonNew; - } - } - switch (m_session->status) - { - case AuthSession::Undetermined: - { - qCritical() << "Received undetermined session status during login. Bye."; - tryagain = false; - emitFailed(tr("Received undetermined session status during login.")); - break; - } - case AuthSession::RequiresPassword: - { - EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField); - auto username = m_session->username; - auto chopN = [](QString toChop, int N) -> QString - { - if(toChop.size() > N) - { - auto left = toChop.left(N); - left += QString("\u25CF").repeated(toChop.size() - N); - return left; - } - return toChop; - }; - - if(username.contains('@')) - { - auto parts = username.split('@'); - auto mailbox = chopN(parts[0],3); - QString domain = chopN(parts[1], 3); - username = mailbox + '@' + domain; - } - passDialog.setUsername(username); - if (passDialog.exec() == QDialog::Accepted) - { - password = passDialog.password(); - } - else - { - tryagain = false; - } - break; - } - case AuthSession::PlayableOffline: - { - // we ask the user for a player name - bool ok = false; - QString usedname = m_session->player_name; - QString name = QInputDialog::getText(m_parentWidget, tr("Player name"), - tr("Choose your offline mode player name."), - QLineEdit::Normal, m_session->player_name, &ok); - if (!ok) - { - tryagain = false; - break; - } - if (name.length()) - { - usedname = name; - } - m_session->MakeOffline(usedname); - // offline flavored game from here :3 - } - case AuthSession::PlayableOnline: - { - launchInstance(); - tryagain = false; - return; - } - } - } - emitFailed(tr("Failed to launch.")); -} - -void LaunchController::launchInstance() -{ - Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); - Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); - - if(!m_instance->reloadSettings()) - { - QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); - emitFailed(tr("Couldn't load the instance profile.")); - return; - } - - m_launcher = m_instance->createLaunchTask(m_session); - if (!m_launcher) - { - emitFailed(tr("Couldn't instantiate a launcher.")); - return; - } - - auto console = qobject_cast(m_parentWidget); - auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); - if(!console && showConsole) - { - MMC->showInstanceWindow(m_instance); - } - connect(m_launcher.get(), &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); - connect(m_launcher.get(), &LaunchTask::succeeded, this, &LaunchController::onSucceeded); - connect(m_launcher.get(), &LaunchTask::failed, this, &LaunchController::onFailed); - connect(m_launcher.get(), &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); - - - m_launcher->prependStep(new TextPrint(m_launcher.get(), "MultiMC version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::MultiMC)); - m_launcher->start(); -} - -void LaunchController::readyForLaunch() -{ - if (!m_profiler) - { - m_launcher->proceed(); - return; - } - - QString error; - if (!m_profiler->check(&error)) - { - m_launcher->abort(); - QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't start profiler: %1").arg(error)); - emitFailed("Profiler startup failed!"); - return; - } - BaseProfiler *profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this); - - connect(profilerInstance, &BaseProfiler::readyToLaunch, [this](const QString & message) - { - QMessageBox msg; - msg.setText(tr("The game launch is delayed until you press the " - "button. This is the right time to setup the profiler, as the " - "profiler server is running now.\n\n%1").arg(message)); - msg.setWindowTitle(tr("Waiting.")); - msg.setIcon(QMessageBox::Information); - msg.addButton(tr("Launch"), QMessageBox::AcceptRole); - msg.setModal(true); - msg.exec(); - m_launcher->proceed(); - }); - connect(profilerInstance, &BaseProfiler::abortLaunch, [this](const QString & message) - { - QMessageBox msg; - msg.setText(tr("Couldn't start the profiler: %1").arg(message)); - msg.setWindowTitle(tr("Error")); - msg.setIcon(QMessageBox::Critical); - msg.addButton(QMessageBox::Ok); - msg.setModal(true); - msg.exec(); - m_launcher->abort(); - emitFailed("Profiler startup failed!"); - }); - profilerInstance->beginProfiling(m_launcher); -} - -void LaunchController::onSucceeded() -{ - emitSucceeded(); -} - -void LaunchController::onFailed(QString reason) -{ - if(m_instance->settings()->get("ShowConsoleOnError").toBool()) - { - MMC->showInstanceWindow(m_instance, "console"); - } - emitFailed(reason); -} - -void LaunchController::onProgressRequested(Task* task) -{ - ProgressDialog progDialog(m_parentWidget); - progDialog.setSkipButton(true, tr("Abort")); - m_launcher->proceed(); - progDialog.execWithTask(task); -} - -bool LaunchController::abort() -{ - if(!m_launcher) - { - return true; - } - if(!m_launcher->canAbort()) - { - return false; - } - auto response = CustomMessageBox::selectable( - m_parentWidget, tr("Kill Minecraft?"), - tr("This can cause the instance to get corrupted and should only be used if Minecraft " - "is frozen for some reason"), - QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)->exec(); - if (response == QMessageBox::Yes) - { - return m_launcher->abort(); - } - return false; -} diff --git a/application/dialogs/SkinUploadDialog.ui b/application/dialogs/SkinUploadDialog.ui deleted file mode 100644 index 6f5307e343..0000000000 --- a/application/dialogs/SkinUploadDialog.ui +++ /dev/null @@ -1,85 +0,0 @@ - - - SkinUploadDialog - - - - 0 - 0 - 413 - 300 - - - - Skin Upload - - - - - - Skin File - - - - - - - - - - 0 - 0 - - - - - 28 - 16777215 - - - - ... - - - - - - - - - - Player Model - - - - - - Steve Model - - - true - - - - - - - Alex Model - - - - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/application/pages/modplatform/twitch/TwitchPage.cpp b/application/pages/modplatform/twitch/TwitchPage.cpp deleted file mode 100644 index 1e9f9dbbf8..0000000000 --- a/application/pages/modplatform/twitch/TwitchPage.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "TwitchPage.h" -#include "ui_TwitchPage.h" - -#include "MultiMC.h" -#include "dialogs/NewInstanceDialog.h" -#include -#include "TwitchModel.h" -#include - -TwitchPage::TwitchPage(NewInstanceDialog* dialog, QWidget *parent) - : QWidget(parent), ui(new Ui::TwitchPage), dialog(dialog) -{ - ui->setupUi(this); - connect(ui->searchButton, &QPushButton::clicked, this, &TwitchPage::triggerSearch); - ui->searchEdit->installEventFilter(this); - model = new Twitch::ListModel(this); - ui->packView->setModel(model); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TwitchPage::onSelectionChanged); -} - -TwitchPage::~TwitchPage() -{ - delete ui; -} - -bool TwitchPage::eventFilter(QObject* watched, QEvent* event) -{ - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(event); - if (keyEvent->key() == Qt::Key_Return) { - triggerSearch(); - keyEvent->accept(); - return true; - } - } - return QWidget::eventFilter(watched, event); -} - -bool TwitchPage::shouldDisplay() const -{ - return true; -} - -void TwitchPage::openedImpl() -{ - suggestCurrent(); -} - -void TwitchPage::triggerSearch() -{ - model->searchWithTerm(ui->searchEdit->text()); -} - -void TwitchPage::onSelectionChanged(QModelIndex first, QModelIndex second) -{ - if(!first.isValid()) - { - if(isOpened) - { - dialog->setSuggestedPack(); - } - ui->frame->clear(); - return; - } - - current = model->data(first, Qt::UserRole).value(); - QString text = ""; - QString name = current.name; - - if (current.websiteUrl.isEmpty()) - text = name; - else - text = "" + name + ""; - if (!current.authors.empty()) { - auto authorToStr = [](Twitch::ModpackAuthor & author) { - if(author.url.isEmpty()) { - return author.name; - } - return QString("%2").arg(author.url, author.name); - }; - QStringList authorStrs; - for(auto & author: current.authors) { - authorStrs.push_back(authorToStr(author)); - } - text += tr(" by ") + authorStrs.join(", "); - } - - ui->frame->setModText(text); - ui->frame->setModDescription(current.description); - suggestCurrent(); -} - -void TwitchPage::suggestCurrent() -{ - if(!isOpened) - { - return; - } - if(current.broken) - { - dialog->setSuggestedPack(); - } - - dialog->setSuggestedPack(current.name, new InstanceImportTask(current.latestFile.downloadUrl)); - QString editedLogoName; - editedLogoName = "twitch_" + current.logoName.section(".", 0, 0); - model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) - { - dialog->setSuggestedIconFromFile(logo, editedLogoName); - }); -} diff --git a/application/pages/modplatform/twitch/TwitchPage.ui b/application/pages/modplatform/twitch/TwitchPage.ui deleted file mode 100644 index c78d8ce024..0000000000 --- a/application/pages/modplatform/twitch/TwitchPage.ui +++ /dev/null @@ -1,73 +0,0 @@ - - - TwitchPage - - - - 0 - 0 - 875 - 745 - - - - - - - - - - Search - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - - - - - - - - - MCModInfoFrame - QFrame -
widgets/MCModInfoFrame.h
- 1 -
-
- - searchEdit - searchButton - packView - - - -
diff --git a/application/resources/MultiMC.ico b/application/resources/MultiMC.ico deleted file mode 100644 index 1846964e96..0000000000 Binary files a/application/resources/MultiMC.ico and /dev/null differ diff --git a/application/resources/multimc/16x16/patreon.png b/application/resources/multimc/16x16/patreon.png deleted file mode 100644 index cde2b326d1..0000000000 Binary files a/application/resources/multimc/16x16/patreon.png and /dev/null differ diff --git a/application/resources/multimc/22x22/patreon.png b/application/resources/multimc/22x22/patreon.png deleted file mode 100644 index b6235ad2df..0000000000 Binary files a/application/resources/multimc/22x22/patreon.png and /dev/null differ diff --git a/application/resources/multimc/24x24/patreon.png b/application/resources/multimc/24x24/patreon.png deleted file mode 100644 index c1da080f83..0000000000 Binary files a/application/resources/multimc/24x24/patreon.png and /dev/null differ diff --git a/application/resources/multimc/32x32/instances/brick.png b/application/resources/multimc/32x32/instances/brick.png deleted file mode 100644 index 0b534366bd..0000000000 Binary files a/application/resources/multimc/32x32/instances/brick.png and /dev/null differ diff --git a/application/resources/multimc/32x32/instances/diamond.png b/application/resources/multimc/32x32/instances/diamond.png deleted file mode 100644 index 376ab901aa..0000000000 Binary files a/application/resources/multimc/32x32/instances/diamond.png and /dev/null differ diff --git a/application/resources/multimc/32x32/instances/gold.png b/application/resources/multimc/32x32/instances/gold.png deleted file mode 100644 index 9bedda168a..0000000000 Binary files a/application/resources/multimc/32x32/instances/gold.png and /dev/null differ diff --git a/application/resources/multimc/32x32/instances/iron.png b/application/resources/multimc/32x32/instances/iron.png deleted file mode 100644 index 28960782ee..0000000000 Binary files a/application/resources/multimc/32x32/instances/iron.png and /dev/null differ diff --git a/application/resources/multimc/32x32/instances/planks.png b/application/resources/multimc/32x32/instances/planks.png deleted file mode 100644 index 7fcf8467c7..0000000000 Binary files a/application/resources/multimc/32x32/instances/planks.png and /dev/null differ diff --git a/application/resources/multimc/32x32/instances/stone.png b/application/resources/multimc/32x32/instances/stone.png deleted file mode 100644 index 34f9a751e4..0000000000 Binary files a/application/resources/multimc/32x32/instances/stone.png and /dev/null differ diff --git a/application/resources/multimc/32x32/patreon.png b/application/resources/multimc/32x32/patreon.png deleted file mode 100644 index f5ae8a5e4e..0000000000 Binary files a/application/resources/multimc/32x32/patreon.png and /dev/null differ diff --git a/application/resources/multimc/48x48/patreon.png b/application/resources/multimc/48x48/patreon.png deleted file mode 100644 index 2708a85a65..0000000000 Binary files a/application/resources/multimc/48x48/patreon.png and /dev/null differ diff --git a/application/resources/multimc/64x64/patreon.png b/application/resources/multimc/64x64/patreon.png deleted file mode 100644 index 7b4814ec6d..0000000000 Binary files a/application/resources/multimc/64x64/patreon.png and /dev/null differ diff --git a/application/resources/multimc/scalable/twitch.svg b/application/resources/multimc/scalable/twitch.svg deleted file mode 100644 index 8099938040..0000000000 --- a/application/resources/multimc/scalable/twitch.svg +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - image/svg+xml - - Glitch - - - - - - - - Glitch - - diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 86ea83ee37..d9f4d1f1c3 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -12,14 +12,14 @@ Config::Config() VERSION_BUILD = @MultiMC_VERSION_BUILD@; BUILD_PLATFORM = "@MultiMC_BUILD_PLATFORM@"; - CHANLIST_URL = "@MultiMC_CHANLIST_URL@"; + UPDATER_BASE = "@MultiMC_UPDATER_BASE@"; ANALYTICS_ID = "@MultiMC_ANALYTICS_ID@"; NOTIFICATION_URL = "@MultiMC_NOTIFICATION_URL@"; FULL_VERSION_STR = "@MultiMC_VERSION_MAJOR@.@MultiMC_VERSION_MINOR@.@MultiMC_VERSION_BUILD@"; GIT_COMMIT = "@MultiMC_GIT_COMMIT@"; GIT_REFSPEC = "@MultiMC_GIT_REFSPEC@"; - if(GIT_REFSPEC.startsWith("refs/heads/") && !CHANLIST_URL.isEmpty() && VERSION_BUILD >= 0) + if(GIT_REFSPEC.startsWith("refs/heads/") && !UPDATER_BASE.isEmpty() && !BUILD_PLATFORM.isEmpty() && VERSION_BUILD >= 0) { VERSION_CHANNEL = GIT_REFSPEC; VERSION_CHANNEL.remove("refs/heads/"); @@ -33,7 +33,12 @@ Config::Config() VERSION_STR = "@MultiMC_VERSION_STRING@"; NEWS_RSS_URL = "@MultiMC_NEWS_RSS_URL@"; PASTE_EE_KEY = "@MultiMC_PASTE_EE_API_KEY@"; + IMGUR_CLIENT_ID = "@MultiMC_IMGUR_CLIENT_ID@"; META_URL = "@MultiMC_META_URL@"; + + BUG_TRACKER_URL = "@MultiMC_BUG_TRACKER_URL@"; + DISCORD_URL = "@MultiMC_DISCORD_URL@"; + SUBREDDIT_URL = "@MultiMC_SUBREDDIT_URL@"; } QString Config::printableVersionString() const diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 02a9297a9f..6a35d1b352 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -29,7 +29,12 @@ class Config QString BUILD_PLATFORM; /// URL for the updater's channel - QString CHANLIST_URL; + QString UPDATER_BASE; + + /// User-Agent to use. + QString USER_AGENT = "MultiMC/5.0"; + /// User-Agent to use for uncached requests. + QString USER_AGENT_UNCACHED = "MultiMC/5.0 (Uncached)"; /// Google analytics ID QString ANALYTICS_ID; @@ -60,19 +65,26 @@ class Config */ QString PASTE_EE_KEY; + /** + * Client ID you can get from Imgur when you register an application + */ + QString IMGUR_CLIENT_ID; + /** * MultiMC Metadata repository URL prefix */ QString META_URL; + QString BUG_TRACKER_URL; + QString DISCORD_URL; + QString SUBREDDIT_URL; + QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; - QString SKINS_BASE = "https://crafatar.com/skins/"; QString AUTH_BASE = "https://authserver.mojang.com/"; QString MOJANG_STATUS_URL = "https://status.mojang.com/check"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; - QString FMLLIBS_OUR_BASE_URL = "https://files.multimc.org/fmllibs/"; - QString FMLLIBS_FORGE_BASE_URL = "https://files.minecraftforge.net/fmllibs/"; + QString FMLLIBS_BASE_URL = "https://files.multimc.org/fmllibs/"; QString TRANSLATIONS_BASE_URL = "https://files.multimc.org/translations/"; QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; diff --git a/changelog.md b/changelog.md index 31b99a6b45..97454d7f62 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,135 @@ -# MultiMC 0.6.12 +# MultiMC 0.6.13 + +This release brings initial support for Microsoft accounts, along with a nice pile of modpack platform support changes and improved Java runtime detection. + +Java runtimes still need an overhaul, so we're staying on the 0.6 version for a little longer. + +Next release should also tackle the current Forge 1.17.x issues in a systematic way. + +### Microsoft accounts + +This is the first release with Microsoft accounts in. + +Implementation is loosely based on documentation available from [wiki.vg](https://wiki.vg/Microsoft_Authentication_Scheme) with some notable changes: + +- More complete implementation including getting and displaying GamerTags [(see XR-046)](https://docs.microsoft.com/en-us/gaming/gdk/_content/gc/policies/pc/live-policies-pc#xr-046-display-name-and-gamerpic-). + +- Using the OAuth Device Flow instead of closely integrating with a browser engine. + + MultiMC asks you to open a Microsoft login web page and put in a code that lets MultiMC authenticate. + + This lets you authenticate on a completely separate device like your phone, leaving code we ship and the computer you may not even trust out of the picture. + +As part of this, the skin fetching no longer uses a third party service and instead gets skins directly from Mojang. + +Capes can also be selected in MultiMC now. With how many people will now get one for migrating their accounts, it only makes sense. + +### macOS update + +Because of issues with the Microsoft accounts, we now have two builds on macOS: + +- The old build with Qt 5.6 that does not work with Microsoft accounts, but can run on macOS older than 10.13. + +- A new build with Qt 5.15.2 that does work with Microsoft accounts, can use the new macOS dark theme and highlight colors, but requires at least macOS 10.13. + +MultiMC will update to the 5.15.2 builds when it detects that this is possible. **It may look like it is updating twice, just let it do its thing.** + +Similar approach got attempted on Windows, aiming to fix various display scaling and theming issues, but it ran into too many problems and will be attempted later, with more caution. + +### Modpack platforms + +In general, the modpack platform pages have been made more consistent with each other (GH-3118, GH-3720, GH-3731). + +- FTB improvements: + + - Modpack file downloads are now checked with checksums and cached. + + - GH-1949: Allow Legacy FTB and FTB pack downloads to be aborted. + +- CurseForge improvements: + + - CurseForge modpack platform is now presented as CurseForge, not Twitch. + + - UI has been updated to match other platforms + + - Added sorting + + - GH-3667: Added version selection + + - GH-3611: Added ability to install beta versions + + - GH-3633: When a CurseForge pack is available for multiple Minecraft versions, we assume the latest one. + +- ATLauncher improvements: + + - Handling latest/custom/recommended mod loader versions. + + - Fabric loader packs should now work. + + - GH-3764: Only client mods are installed now for ATL packs. + + - Improved error handling + + - Optional mods are supported. + + - GH-1949: Allow ATLauncher pack downloads to be aborted + + +- Fixed bugs in FTB platform search. + +### Other changes + +- Forge installation is disabled on Minecraft 1.17+ because of incompatible/unresolved changes on the Forge side. + + We're going to aim for fixing it in time for 1.18. Thankfully, 1.17 is more of a in-between release, so go play some 1.16.x packs! + +- GH-2529: On macOS, MultiMC will ask to move all the instance data to a new `Data` folder in order to fix long load times caused by macOS checking all files. + +- Detection of a large amount of various Java runtime flavors have been added. + +- It is now possible to join servers when starting an instance: + + - From command line via the `--launch` and `--server` arguments. + + - Or by setting this up in the instance settings page. + + This may not work correctly in some cases, because it is a rarely used feature and modders do not test with it. + +- MultiMC now prints resolved IP addresses of Minecraft services into the game log for diagnostic purposes. + +- Updated instance icons based on Minecraft textures. + +- Forge `mods.toml` files are now used for displaying mods in the UI. + +- Datapack button is now disabled when no world is selected. + +- MultiMC warns about GLFW and OpenAL workarounds being enabled in the game log. + +- Languages in the translations list are now sorted by their two/three letter key + +- GH-3450: Displaying and recording gameplay time is now optional and can be turned off. + +- GH-3930: MultiMC can now track the gameplay time of the last session. + +- GH-3033: The version pages of instances now have a filter bar. + +- GH-2971: UI descriptions of texture and resource packs no longer mention mods. + +- Quick and dirty minimum Java runtime versions checks have been added. This needs to be expanded in the future. + +### Technical changes + +- The codebase continues to move towards being debranded and harder to build as 'MultiMC' for third parties. + +# Previous releases + +## MultiMC 0.6.12 After roughly one year of maintenance and development work by various contributors, we're just calling it a good time to release. What got added since the last time? Quite a bit! But in general, this is more of a spring cleaning before the major changes that we need to make come in. -### Modpack platforms +#### Modpack platforms We've added a whole bunch of new modpack platforms to pick from right into the new instance dialog. If you run into any unusual issues with the imported packs, report them on the bug tracker. @@ -18,7 +143,7 @@ We've added a whole bunch of new modpack platforms to pick from right into the n - GH-405: Added a ATLauncher pack browser -### Other changes +#### Other changes - Added the option to not use OpenAL and/or GLFW libraries bundled with the game. @@ -46,7 +171,7 @@ We've added a whole bunch of new modpack platforms to pick from right into the n - GH-3602: Pre-launch commands could fail on first launch of the instance because the .minecraft folder has not been created yet. -### Technical changes +#### Technical changes - GH-3234: At build time, the meta URL can be changed. @@ -58,8 +183,6 @@ We've added a whole bunch of new modpack platforms to pick from right into the n - Compatibility with unusual build environments has been increased -# Previous releases - ## MultiMC 0.6.11 This adds Forge 1.13+ support using [ForgeWrapper](https://github.com/ZekerZhayard/ForgeWrapper) by ZekerZhayard. diff --git a/doc/multimc.1.txt b/doc/multimc.1.txt new file mode 100644 index 0000000000..c2d938804b --- /dev/null +++ b/doc/multimc.1.txt @@ -0,0 +1,62 @@ +MULTIMC(1) +========== +:doctype: manpage + + +NAME +---- +multimc - a launcher and instance manager for Minecraft. + + +SYNOPSIS +-------- +*multimc* ['OPTIONS'] + + +DESCRIPTION +----------- +MultiMC is a custom launcher for Minecraft that allows you to easily manage +multiple installations of Minecraft at once. It also allows you to easily +install and remove mods by simply dragging and dropping. +Here are the current features of MultiMC. + +OPTIONS +------- +*-d, --dir*='DIRECTORY':: + Use 'DIRECTORY' as the MultiMC root. + +*-l, --launch*='INSTANCE_ID':: + Launch the instance specified by 'INSTANCE_ID'. + +*--alive*:: + Write a small 'live.check' file after MultiMC starts. + +*-h, --help*:: + Display help text and exit. + +*-v, --version*:: + Display program version and exit. + +EXIT STATUS +----------- +*0*:: + Success + +*1*:: + Failure (syntax or usage error; configuration error; unexpected error). + +BUGS +---- + + +RESOURCES +--------- +GitHub: + +Main website: + +AUTHORS +------- +peterix + +// vim: syntax=asciidoc diff --git a/api/logic/BaseInstaller.cpp b/launcher/BaseInstaller.cpp similarity index 100% rename from api/logic/BaseInstaller.cpp rename to launcher/BaseInstaller.cpp diff --git a/api/logic/BaseInstaller.h b/launcher/BaseInstaller.h similarity index 94% rename from api/logic/BaseInstaller.h rename to launcher/BaseInstaller.h index 3e40b3554b..b2e6a14d65 100644 --- a/api/logic/BaseInstaller.h +++ b/launcher/BaseInstaller.h @@ -17,8 +17,6 @@ #include -#include "multimc_logic_export.h" - class MinecraftInstance; class QDir; class QString; @@ -27,7 +25,7 @@ class Task; class BaseVersion; typedef std::shared_ptr BaseVersionPtr; -class MULTIMC_LOGIC_EXPORT BaseInstaller +class BaseInstaller { public: BaseInstaller(); diff --git a/api/logic/BaseInstance.cpp b/launcher/BaseInstance.cpp similarity index 92% rename from api/logic/BaseInstance.cpp rename to launcher/BaseInstance.cpp index d08ceb2b86..46b458271b 100644 --- a/api/logic/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -37,6 +37,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("notes", ""); m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); + m_settings->registerSetting("lastTimePlayed", 0); // Custom Commands auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); @@ -134,15 +135,24 @@ void BaseInstance::setRunning(bool running) m_isRunning = running; + if(!m_settings->get("RecordGameTime").toBool()) + { + emit runningStatusChanged(running); + return; + } + if(running) { m_timeStarted = QDateTime::currentDateTime(); } else { - qint64 current = settings()->get("totalTimePlayed").toLongLong(); QDateTime timeEnded = QDateTime::currentDateTime(); + + qint64 current = settings()->get("totalTimePlayed").toLongLong(); settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded)); + settings()->set("lastTimePlayed", m_timeStarted.secsTo(timeEnded)); + emit propertiesChanged(this); } @@ -160,9 +170,20 @@ int64_t BaseInstance::totalTimePlayed() const return current; } +int64_t BaseInstance::lastTimePlayed() const +{ + if(m_isRunning) + { + QDateTime timeNow = QDateTime::currentDateTime(); + return m_timeStarted.secsTo(timeNow); + } + return settings()->get("lastTimePlayed").toLongLong(); +} + void BaseInstance::resetTimePlayed() { settings()->reset("totalTimePlayed"); + settings()->reset("lastTimePlayed"); } QString BaseInstance::instanceType() const diff --git a/api/logic/BaseInstance.h b/launcher/BaseInstance.h similarity index 93% rename from api/logic/BaseInstance.h rename to launcher/BaseInstance.h index bbeabb4179..8c08dc05c3 100644 --- a/api/logic/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -26,13 +26,13 @@ #include "settings/INIFile.h" #include "BaseVersionList.h" -#include "minecraft/auth/MojangAccount.h" +#include "minecraft/auth/MinecraftAccount.h" #include "MessageLevel.h" #include "pathmatcher/IPathMatcher.h" #include "net/Mode.h" -#include "multimc_logic_export.h" +#include "minecraft/launch/MinecraftServerTarget.h" class QDir; class Task; @@ -50,7 +50,7 @@ typedef std::shared_ptr InstancePtr; * 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 +class BaseInstance : public QObject, public std::enable_shared_from_this { Q_OBJECT protected: @@ -85,6 +85,7 @@ class MULTIMC_LOGIC_EXPORT BaseInstance : public QObject, public std::enable_sha void setRunning(bool running); bool isRunning() const; int64_t totalTimePlayed() const; + int64_t lastTimePlayed() const; void resetTimePlayed(); /// get the type of this instance @@ -145,7 +146,8 @@ class MULTIMC_LOGIC_EXPORT BaseInstance : public QObject, public std::enable_sha virtual shared_qobject_ptr createUpdateTask(Net::Mode mode) = 0; /// returns a valid launcher (task container) - virtual shared_qobject_ptr createLaunchTask(AuthSessionPtr account) = 0; + virtual shared_qobject_ptr createLaunchTask( + AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) = 0; /// returns the current launch task (if any) shared_qobject_ptr getLaunchTask(); @@ -221,9 +223,9 @@ class MULTIMC_LOGIC_EXPORT BaseInstance : public QObject, public std::enable_sha bool reloadSettings(); /** - * 'print' a verbose desription of the instance into a QStringList + * 'print' a verbose description of the instance into a QStringList */ - virtual QStringList verboseDescription(AuthSessionPtr session) = 0; + virtual QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) = 0; Status currentStatus() const; diff --git a/api/logic/BaseVersion.h b/launcher/BaseVersion.h similarity index 100% rename from api/logic/BaseVersion.h rename to launcher/BaseVersion.h diff --git a/api/logic/BaseVersionList.cpp b/launcher/BaseVersionList.cpp similarity index 100% rename from api/logic/BaseVersionList.cpp rename to launcher/BaseVersionList.cpp diff --git a/api/logic/BaseVersionList.h b/launcher/BaseVersionList.h similarity index 97% rename from api/logic/BaseVersionList.h rename to launcher/BaseVersionList.h index 29e21bdb11..ce7abce195 100644 --- a/api/logic/BaseVersionList.h +++ b/launcher/BaseVersionList.h @@ -21,7 +21,6 @@ #include "BaseVersion.h" #include "tasks/Task.h" -#include "multimc_logic_export.h" #include "QObjectPtr.h" /*! @@ -36,7 +35,7 @@ * 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 +class BaseVersionList : public QAbstractListModel { Q_OBJECT public: diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt new file mode 100644 index 0000000000..c29ee3e1ab --- /dev/null +++ b/launcher/CMakeLists.txt @@ -0,0 +1,1043 @@ +project(application) + +################################ FILES ################################ + +######## Sources and headers ######## + +include (UnitTest) + +set(CORE_SOURCES + # LOGIC - Base classes and infrastructure + BaseInstaller.h + BaseInstaller.cpp + BaseVersionList.h + BaseVersionList.cpp + InstanceList.h + InstanceList.cpp + InstanceTask.h + InstanceTask.cpp + LoggedProcess.h + LoggedProcess.cpp + MessageLevel.cpp + MessageLevel.h + BaseVersion.h + BaseInstance.h + BaseInstance.cpp + NullInstance.h + MMCZip.h + MMCZip.cpp + MMCStrings.h + MMCStrings.cpp + + # Basic instance manipulation tasks (derived from InstanceTask) + InstanceCreationTask.h + InstanceCreationTask.cpp + InstanceCopyTask.h + InstanceCopyTask.cpp + InstanceImportTask.h + InstanceImportTask.cpp + + # Use tracking separate from memory management + Usable.h + + # Prefix tree where node names are strings between separators + SeparatorPrefixTree.h + + # WARNING: globals live here + Env.h + Env.cpp + + # String filters + Filter.h + Filter.cpp + + # JSON parsing helpers + Json.h + Json.cpp + + FileSystem.h + FileSystem.cpp + + Exception.h + + # RW lock protected map + RWStorage.h + + # A variable that has an implicit default value and keeps track of changes + DefaultVariable.h + + # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms + QObjectPtr.h + + # Compression support + GZip.h + GZip.cpp + + # Command line parameter parsing + Commandline.h + Commandline.cpp + + # Version number string support + Version.h + Version.cpp + + # A Recursive file system watcher + RecursiveFileSystemWatcher.h + RecursiveFileSystemWatcher.cpp +) + +add_unit_test(FileSystem + SOURCES FileSystem_test.cpp + LIBS MultiMC_logic + DATA testdata + ) + +add_unit_test(GZip + SOURCES GZip_test.cpp + LIBS MultiMC_logic + ) + +set(PATHMATCHER_SOURCES + # Path matchers + pathmatcher/FSTreeMatcher.h + pathmatcher/IPathMatcher.h + pathmatcher/MultiMatcher.h + pathmatcher/RegexpMatcher.h +) + +set(NET_SOURCES + # network stuffs + net/ByteArraySink.h + net/ChecksumValidator.h + net/Download.cpp + net/Download.h + net/FileSink.cpp + net/FileSink.h + net/HttpMetaCache.cpp + net/HttpMetaCache.h + net/MetaCacheSink.cpp + net/MetaCacheSink.h + net/NetAction.h + net/NetJob.cpp + net/NetJob.h + net/PasteUpload.cpp + net/PasteUpload.h + net/Sink.h + net/Validator.h +) + +# Game launch logic +set(LAUNCH_SOURCES + launch/steps/LookupServerAddress.cpp + launch/steps/LookupServerAddress.h + launch/steps/PostLaunchCommand.cpp + launch/steps/PostLaunchCommand.h + launch/steps/PreLaunchCommand.cpp + launch/steps/PreLaunchCommand.h + launch/steps/TextPrint.cpp + launch/steps/TextPrint.h + launch/steps/Update.cpp + launch/steps/Update.h + launch/LaunchStep.cpp + launch/LaunchStep.h + launch/LaunchTask.cpp + launch/LaunchTask.h + launch/LogModel.cpp + launch/LogModel.h +) + +# Old update system +set(UPDATE_SOURCES + updater/GoUpdate.h + updater/GoUpdate.cpp + updater/UpdateChecker.h + updater/UpdateChecker.cpp + updater/DownloadTask.h + updater/DownloadTask.cpp +) + +add_unit_test(UpdateChecker + SOURCES updater/UpdateChecker_test.cpp + LIBS MultiMC_logic + DATA updater/testdata + ) + +add_unit_test(DownloadTask + SOURCES updater/DownloadTask_test.cpp + LIBS MultiMC_logic + DATA updater/testdata + ) + +# Rarely used notifications +set(NOTIFICATIONS_SOURCES + # Notifications - short warning messages + notifications/NotificationChecker.h + notifications/NotificationChecker.cpp +) + +# Backend for the news bar... there's usually no news. +set(NEWS_SOURCES + # News System + news/NewsChecker.h + news/NewsChecker.cpp + news/NewsEntry.h + news/NewsEntry.cpp +) + +# Icon interface +set(ICONS_SOURCES + # Icons System and related code + icons/IIconList.h + icons/IIconList.cpp + icons/IconUtils.h + icons/IconUtils.cpp +) + +# Minecraft services status checker +set(STATUS_SOURCES + # Status system + status/StatusChecker.h + status/StatusChecker.cpp +) + +# Support for Minecraft instances and launch +set(MINECRAFT_SOURCES + # Minecraft support + minecraft/auth/AccountData.h + minecraft/auth/AccountData.cpp + minecraft/auth/AccountTask.h + minecraft/auth/AccountTask.cpp + minecraft/auth/AuthSession.h + minecraft/auth/AuthSession.cpp + minecraft/auth/AccountList.h + minecraft/auth/AccountList.cpp + minecraft/auth/MinecraftAccount.h + minecraft/auth/MinecraftAccount.cpp + minecraft/auth/flows/AuthContext.h + minecraft/auth/flows/AuthContext.cpp + minecraft/auth/flows/AuthRequest.h + minecraft/auth/flows/AuthRequest.cpp + + minecraft/auth/flows/MSAInteractive.h + minecraft/auth/flows/MSAInteractive.cpp + minecraft/auth/flows/MSASilent.h + minecraft/auth/flows/MSASilent.cpp + + minecraft/auth/flows/MojangLogin.h + minecraft/auth/flows/MojangLogin.cpp + minecraft/auth/flows/MojangRefresh.h + minecraft/auth/flows/MojangRefresh.cpp + + minecraft/auth/flows/Yggdrasil.h + minecraft/auth/flows/Yggdrasil.cpp + + minecraft/gameoptions/GameOptions.h + minecraft/gameoptions/GameOptions.cpp + + minecraft/update/AssetUpdateTask.h + minecraft/update/AssetUpdateTask.cpp + minecraft/update/FMLLibrariesTask.cpp + minecraft/update/FMLLibrariesTask.h + minecraft/update/FoldersTask.cpp + minecraft/update/FoldersTask.h + minecraft/update/LibrariesTask.cpp + minecraft/update/LibrariesTask.h + + minecraft/launch/ClaimAccount.cpp + minecraft/launch/ClaimAccount.h + minecraft/launch/CreateGameFolders.cpp + minecraft/launch/CreateGameFolders.h + minecraft/launch/ModMinecraftJar.cpp + minecraft/launch/ModMinecraftJar.h + minecraft/launch/DirectJavaLaunch.cpp + minecraft/launch/DirectJavaLaunch.h + minecraft/launch/ExtractNatives.cpp + minecraft/launch/ExtractNatives.h + minecraft/launch/LauncherPartLaunch.cpp + minecraft/launch/LauncherPartLaunch.h + minecraft/launch/MinecraftServerTarget.cpp + minecraft/launch/MinecraftServerTarget.h + minecraft/launch/PrintInstanceInfo.cpp + minecraft/launch/PrintInstanceInfo.h + minecraft/launch/ReconstructAssets.cpp + minecraft/launch/ReconstructAssets.h + minecraft/launch/ScanModFolders.cpp + minecraft/launch/ScanModFolders.h + minecraft/launch/VerifyJavaInstall.cpp + minecraft/launch/VerifyJavaInstall.h + + minecraft/legacy/LegacyModList.h + minecraft/legacy/LegacyModList.cpp + minecraft/legacy/LegacyInstance.h + minecraft/legacy/LegacyInstance.cpp + minecraft/legacy/LegacyUpgradeTask.h + minecraft/legacy/LegacyUpgradeTask.cpp + + minecraft/GradleSpecifier.h + minecraft/MinecraftInstance.cpp + minecraft/MinecraftInstance.h + minecraft/LaunchProfile.cpp + minecraft/LaunchProfile.h + minecraft/Component.cpp + minecraft/Component.h + minecraft/PackProfile.cpp + minecraft/PackProfile.h + minecraft/ComponentUpdateTask.cpp + minecraft/ComponentUpdateTask.h + minecraft/MinecraftLoadAndCheck.h + minecraft/MinecraftLoadAndCheck.cpp + minecraft/MinecraftUpdate.h + minecraft/MinecraftUpdate.cpp + minecraft/MojangVersionFormat.cpp + minecraft/MojangVersionFormat.h + minecraft/Rule.cpp + minecraft/Rule.h + minecraft/OneSixVersionFormat.cpp + minecraft/OneSixVersionFormat.h + minecraft/OpSys.cpp + minecraft/OpSys.h + minecraft/ParseUtils.cpp + minecraft/ParseUtils.h + minecraft/ProfileUtils.cpp + minecraft/ProfileUtils.h + minecraft/Library.cpp + minecraft/Library.h + minecraft/MojangDownloadInfo.h + minecraft/VersionFile.cpp + minecraft/VersionFile.h + minecraft/VersionFilterData.h + minecraft/VersionFilterData.cpp + minecraft/World.h + minecraft/World.cpp + minecraft/WorldList.h + minecraft/WorldList.cpp + + minecraft/mod/Mod.h + minecraft/mod/Mod.cpp + minecraft/mod/ModDetails.h + minecraft/mod/ModFolderModel.h + minecraft/mod/ModFolderModel.cpp + minecraft/mod/ModFolderLoadTask.h + minecraft/mod/ModFolderLoadTask.cpp + minecraft/mod/LocalModParseTask.h + minecraft/mod/LocalModParseTask.cpp + minecraft/mod/ResourcePackFolderModel.h + minecraft/mod/ResourcePackFolderModel.cpp + minecraft/mod/TexturePackFolderModel.h + minecraft/mod/TexturePackFolderModel.cpp + + # Assets + minecraft/AssetsUtils.h + minecraft/AssetsUtils.cpp + + # Minecraft services + minecraft/services/CapeChange.cpp + minecraft/services/CapeChange.h + minecraft/services/SkinUpload.cpp + minecraft/services/SkinUpload.h + minecraft/services/SkinDelete.cpp + minecraft/services/SkinDelete.h + + mojang/PackageManifest.h + mojang/PackageManifest.cpp + ) + +add_unit_test(GradleSpecifier + SOURCES minecraft/GradleSpecifier_test.cpp + LIBS MultiMC_logic + ) + +add_executable(PackageManifest + mojang/PackageManifest_test.cpp +) +target_link_libraries(PackageManifest + MultiMC_logic + Qt5::Test +) +target_include_directories(PackageManifest + PRIVATE ../cmake/UnitTest/ +) +add_test( + NAME PackageManifest + COMMAND PackageManifest + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_unit_test(MojangVersionFormat + SOURCES minecraft/MojangVersionFormat_test.cpp + LIBS MultiMC_logic + DATA minecraft/testdata + ) + +add_unit_test(Library + SOURCES minecraft/Library_test.cpp + LIBS MultiMC_logic + ) + +# FIXME: shares data with FileSystem test +add_unit_test(ModFolderModel + SOURCES minecraft/mod/ModFolderModel_test.cpp + DATA testdata + LIBS MultiMC_logic + ) + +add_unit_test(ParseUtils + SOURCES minecraft/ParseUtils_test.cpp + LIBS MultiMC_logic + ) + +# the screenshots feature +set(SCREENSHOTS_SOURCES + screenshots/Screenshot.h + screenshots/ImgurUpload.h + screenshots/ImgurUpload.cpp + screenshots/ImgurAlbumCreation.h + screenshots/ImgurAlbumCreation.cpp +) + +set(TASKS_SOURCES + # Tasks + tasks/Task.h + tasks/Task.cpp + tasks/SequentialTask.h + tasks/SequentialTask.cpp +) + +set(SETTINGS_SOURCES + # Settings + settings/INIFile.cpp + settings/INIFile.h + settings/INISettingsObject.cpp + settings/INISettingsObject.h + settings/OverrideSetting.cpp + settings/OverrideSetting.h + settings/PassthroughSetting.cpp + settings/PassthroughSetting.h + settings/Setting.cpp + settings/Setting.h + settings/SettingsObject.cpp + settings/SettingsObject.h +) + +add_unit_test(INIFile + SOURCES settings/INIFile_test.cpp + LIBS MultiMC_logic + ) + +set(JAVA_SOURCES + # Java related code + java/launch/CheckJava.cpp + java/launch/CheckJava.h + java/JavaChecker.h + java/JavaChecker.cpp + java/JavaCheckerJob.h + java/JavaCheckerJob.cpp + java/JavaInstall.h + java/JavaInstall.cpp + java/JavaInstallList.h + java/JavaInstallList.cpp + java/JavaUtils.h + java/JavaUtils.cpp + java/JavaVersion.h + java/JavaVersion.cpp +) + +add_unit_test(JavaVersion + SOURCES java/JavaVersion_test.cpp + LIBS MultiMC_logic + ) + +set(TRANSLATIONS_SOURCES + translations/TranslationsModel.h + translations/TranslationsModel.cpp + translations/POTranslator.h + translations/POTranslator.cpp +) + +set(TOOLS_SOURCES + # Tools + tools/BaseExternalTool.cpp + tools/BaseExternalTool.h + tools/BaseProfiler.cpp + tools/BaseProfiler.h + tools/JProfiler.cpp + tools/JProfiler.h + tools/JVisualVM.cpp + tools/JVisualVM.h + tools/MCEditTool.cpp + tools/MCEditTool.h +) + +set(META_SOURCES + # Metadata sources + meta/JsonFormat.cpp + meta/JsonFormat.h + meta/BaseEntity.cpp + meta/BaseEntity.h + meta/VersionList.cpp + meta/VersionList.h + meta/Version.cpp + meta/Version.h + meta/Index.cpp + meta/Index.h +) + +set(FTB_SOURCES + modplatform/legacy_ftb/PackFetchTask.h + modplatform/legacy_ftb/PackFetchTask.cpp + modplatform/legacy_ftb/PackInstallTask.h + modplatform/legacy_ftb/PackInstallTask.cpp + modplatform/legacy_ftb/PrivatePackManager.h + modplatform/legacy_ftb/PrivatePackManager.cpp + + modplatform/legacy_ftb/PackHelpers.h +) + +set(FLAME_SOURCES + # Flame + modplatform/flame/FlamePackIndex.cpp + modplatform/flame/FlamePackIndex.h + modplatform/flame/PackManifest.h + modplatform/flame/PackManifest.cpp + modplatform/flame/FileResolvingTask.h + modplatform/flame/FileResolvingTask.cpp +) + +set(MODPACKSCH_SOURCES + modplatform/modpacksch/FTBPackInstallTask.h + modplatform/modpacksch/FTBPackInstallTask.cpp + modplatform/modpacksch/FTBPackManifest.h + modplatform/modpacksch/FTBPackManifest.cpp +) + +set(TECHNIC_SOURCES + modplatform/technic/SingleZipPackInstallTask.h + modplatform/technic/SingleZipPackInstallTask.cpp + modplatform/technic/SolderPackInstallTask.h + modplatform/technic/SolderPackInstallTask.cpp + modplatform/technic/TechnicPackProcessor.h + modplatform/technic/TechnicPackProcessor.cpp +) + +set(ATLAUNCHER_SOURCES + modplatform/atlauncher/ATLPackIndex.cpp + modplatform/atlauncher/ATLPackIndex.h + modplatform/atlauncher/ATLPackInstallTask.cpp + modplatform/atlauncher/ATLPackInstallTask.h + modplatform/atlauncher/ATLPackManifest.cpp + modplatform/atlauncher/ATLPackManifest.h +) + +add_unit_test(Index + SOURCES meta/Index_test.cpp + LIBS MultiMC_logic + ) + +################################ COMPILE ################################ + +# we need zlib +find_package(ZLIB REQUIRED) + +set(LOGIC_SOURCES + ${CORE_SOURCES} + ${PATHMATCHER_SOURCES} + ${NET_SOURCES} + ${LAUNCH_SOURCES} + ${UPDATE_SOURCES} + ${NOTIFICATIONS_SOURCES} + ${NEWS_SOURCES} + ${STATUS_SOURCES} + ${MINECRAFT_SOURCES} + ${SCREENSHOTS_SOURCES} + ${TASKS_SOURCES} + ${SETTINGS_SOURCES} + ${JAVA_SOURCES} + ${TRANSLATIONS_SOURCES} + ${TOOLS_SOURCES} + ${META_SOURCES} + ${ICONS_SOURCES} + ${FTB_SOURCES} + ${FLAME_SOURCES} + ${MODPACKSCH_SOURCES} + ${TECHNIC_SOURCES} + ${ATLAUNCHER_SOURCES} +) + +SET(MULTIMC_SOURCES + # Application base + MultiMC.h + MultiMC.cpp + UpdateController.cpp + UpdateController.h + + # GUI - general utilities + DesktopServices.h + DesktopServices.cpp + GuiUtil.h + GuiUtil.cpp + ColumnResizer.h + ColumnResizer.cpp + InstanceProxyModel.h + InstanceProxyModel.cpp + VersionProxyModel.h + VersionProxyModel.cpp + ColorCache.h + ColorCache.cpp + HoeDown.h + + # Super secret! + KonamiCode.h + KonamiCode.cpp + + # Icons + icons/MMCIcon.h + icons/MMCIcon.cpp + icons/IconList.h + icons/IconList.cpp + + # GUI - windows + MainWindow.h + MainWindow.cpp + InstanceWindow.h + InstanceWindow.cpp + + # FIXME: maybe find a better home for this. + SkinUtils.cpp + SkinUtils.h + + # GUI - setup wizard + setupwizard/SetupWizard.h + setupwizard/SetupWizard.cpp + setupwizard/AnalyticsWizardPage.cpp + setupwizard/AnalyticsWizardPage.h + setupwizard/BaseWizardPage.h + setupwizard/JavaWizardPage.cpp + setupwizard/JavaWizardPage.h + setupwizard/LanguageWizardPage.cpp + setupwizard/LanguageWizardPage.h + + # GUI - themes + themes/FusionTheme.cpp + themes/FusionTheme.h + themes/BrightTheme.cpp + themes/BrightTheme.h + themes/CustomTheme.cpp + themes/CustomTheme.h + themes/DarkTheme.cpp + themes/DarkTheme.h + themes/ITheme.cpp + themes/ITheme.h + themes/SystemTheme.cpp + themes/SystemTheme.h + + # Processes + LaunchController.h + LaunchController.cpp + + # page provider for instances + InstancePageProvider.h + + # Common java checking UI + JavaCommon.h + JavaCommon.cpp + + # GUI - paged dialog base + pages/BasePage.h + pages/BasePageContainer.h + pages/BasePageProvider.h + + # GUI - instance pages + pages/instance/GameOptionsPage.cpp + pages/instance/GameOptionsPage.h + pages/instance/VersionPage.cpp + pages/instance/VersionPage.h + pages/instance/TexturePackPage.h + pages/instance/ResourcePackPage.h + pages/instance/ModFolderPage.cpp + pages/instance/ModFolderPage.h + pages/instance/NotesPage.cpp + pages/instance/NotesPage.h + pages/instance/LogPage.cpp + pages/instance/LogPage.h + pages/instance/InstanceSettingsPage.cpp + pages/instance/InstanceSettingsPage.h + pages/instance/ScreenshotsPage.cpp + pages/instance/ScreenshotsPage.h + pages/instance/OtherLogsPage.cpp + pages/instance/OtherLogsPage.h + pages/instance/ServersPage.cpp + pages/instance/ServersPage.h + pages/instance/LegacyUpgradePage.cpp + pages/instance/LegacyUpgradePage.h + pages/instance/WorldListPage.cpp + pages/instance/WorldListPage.h + + # GUI - global settings pages + pages/global/AccountListPage.cpp + pages/global/AccountListPage.h + pages/global/CustomCommandsPage.cpp + pages/global/CustomCommandsPage.h + pages/global/ExternalToolsPage.cpp + pages/global/ExternalToolsPage.h + pages/global/JavaPage.cpp + pages/global/JavaPage.h + pages/global/LanguagePage.cpp + pages/global/LanguagePage.h + pages/global/MinecraftPage.cpp + pages/global/MinecraftPage.h + pages/global/MultiMCPage.cpp + pages/global/MultiMCPage.h + pages/global/ProxyPage.cpp + pages/global/ProxyPage.h + pages/global/PasteEEPage.cpp + pages/global/PasteEEPage.h + + # GUI - platform pages + pages/modplatform/VanillaPage.cpp + pages/modplatform/VanillaPage.h + + pages/modplatform/atlauncher/AtlFilterModel.cpp + pages/modplatform/atlauncher/AtlFilterModel.h + pages/modplatform/atlauncher/AtlListModel.cpp + pages/modplatform/atlauncher/AtlListModel.h + pages/modplatform/atlauncher/AtlOptionalModDialog.cpp + pages/modplatform/atlauncher/AtlOptionalModDialog.h + pages/modplatform/atlauncher/AtlPage.cpp + pages/modplatform/atlauncher/AtlPage.h + + pages/modplatform/ftb/FtbFilterModel.cpp + pages/modplatform/ftb/FtbFilterModel.h + pages/modplatform/ftb/FtbListModel.cpp + pages/modplatform/ftb/FtbListModel.h + pages/modplatform/ftb/FtbPage.cpp + pages/modplatform/ftb/FtbPage.h + + pages/modplatform/legacy_ftb/Page.cpp + pages/modplatform/legacy_ftb/Page.h + pages/modplatform/legacy_ftb/ListModel.h + pages/modplatform/legacy_ftb/ListModel.cpp + + pages/modplatform/flame/FlameModel.cpp + pages/modplatform/flame/FlameModel.h + pages/modplatform/flame/FlamePage.cpp + pages/modplatform/flame/FlamePage.h + + pages/modplatform/technic/TechnicModel.cpp + pages/modplatform/technic/TechnicModel.h + pages/modplatform/technic/TechnicPage.cpp + pages/modplatform/technic/TechnicPage.h + + pages/modplatform/ImportPage.cpp + pages/modplatform/ImportPage.h + + # GUI - dialogs + dialogs/AboutDialog.cpp + dialogs/AboutDialog.h + dialogs/ProfileSelectDialog.cpp + dialogs/ProfileSelectDialog.h + dialogs/CopyInstanceDialog.cpp + dialogs/CopyInstanceDialog.h + dialogs/CustomMessageBox.cpp + dialogs/CustomMessageBox.h + dialogs/EditAccountDialog.cpp + dialogs/EditAccountDialog.h + dialogs/ExportInstanceDialog.cpp + dialogs/ExportInstanceDialog.h + dialogs/IconPickerDialog.cpp + dialogs/IconPickerDialog.h + dialogs/LoginDialog.cpp + dialogs/LoginDialog.h + dialogs/MSALoginDialog.cpp + dialogs/MSALoginDialog.h + dialogs/NewComponentDialog.cpp + dialogs/NewComponentDialog.h + dialogs/NewInstanceDialog.cpp + dialogs/NewInstanceDialog.h + dialogs/NotificationDialog.cpp + dialogs/NotificationDialog.h + pagedialog/PageDialog.cpp + pagedialog/PageDialog.h + dialogs/ProgressDialog.cpp + dialogs/ProgressDialog.h + dialogs/UpdateDialog.cpp + dialogs/UpdateDialog.h + dialogs/VersionSelectDialog.cpp + dialogs/VersionSelectDialog.h + dialogs/SkinUploadDialog.cpp + dialogs/SkinUploadDialog.h + + + # GUI - widgets + widgets/Common.cpp + widgets/Common.h + widgets/CustomCommands.cpp + widgets/CustomCommands.h + widgets/DropLabel.cpp + widgets/DropLabel.h + widgets/FocusLineEdit.cpp + widgets/FocusLineEdit.h + widgets/IconLabel.cpp + widgets/IconLabel.h + widgets/JavaSettingsWidget.cpp + widgets/JavaSettingsWidget.h + widgets/LabeledToolButton.cpp + widgets/LabeledToolButton.h + widgets/LanguageSelectionWidget.cpp + widgets/LanguageSelectionWidget.h + widgets/LineSeparator.cpp + widgets/LineSeparator.h + widgets/LogView.cpp + widgets/LogView.h + widgets/MCModInfoFrame.cpp + widgets/MCModInfoFrame.h + widgets/ModListView.cpp + widgets/ModListView.h + widgets/PageContainer.cpp + widgets/PageContainer.h + widgets/PageContainer_p.h + widgets/ServerStatus.cpp + widgets/ServerStatus.h + widgets/VersionListView.cpp + widgets/VersionListView.h + widgets/VersionSelectWidget.cpp + widgets/VersionSelectWidget.h + widgets/ProgressWidget.h + widgets/ProgressWidget.cpp + widgets/WideBar.h + widgets/WideBar.cpp + + # GUI - instance group view + groupview/GroupedProxyModel.cpp + groupview/GroupedProxyModel.h + groupview/AccessibleGroupView.cpp + groupview/AccessibleGroupView.h + groupview/AccessibleGroupView_p.h + groupview/GroupView.cpp + groupview/GroupView.h + groupview/InstanceDelegate.cpp + groupview/InstanceDelegate.h + groupview/VisualGroup.cpp + groupview/VisualGroup.h + ) + +######## UIs ######## +SET(MULTIMC_UIS + # Instance pages + pages/instance/GameOptionsPage.ui + pages/instance/VersionPage.ui + pages/instance/ModFolderPage.ui + pages/instance/LogPage.ui + pages/instance/InstanceSettingsPage.ui + pages/instance/NotesPage.ui + pages/instance/ScreenshotsPage.ui + pages/instance/OtherLogsPage.ui + pages/instance/LegacyUpgradePage.ui + pages/instance/ServersPage.ui + pages/instance/WorldListPage.ui + + # Global settings pages + pages/global/AccountListPage.ui + pages/global/ExternalToolsPage.ui + pages/global/JavaPage.ui + pages/global/MinecraftPage.ui + pages/global/MultiMCPage.ui + pages/global/ProxyPage.ui + pages/global/PasteEEPage.ui + + # Platform pages + pages/modplatform/VanillaPage.ui + pages/modplatform/atlauncher/AtlPage.ui + pages/modplatform/ftb/FtbPage.ui + pages/modplatform/legacy_ftb/Page.ui + pages/modplatform/flame/FlamePage.ui + pages/modplatform/technic/TechnicPage.ui + pages/modplatform/ImportPage.ui + + # Platform Dialogs + pages/modplatform/atlauncher/AtlOptionalModDialog.ui + + # Dialogs + dialogs/CopyInstanceDialog.ui + dialogs/NewComponentDialog.ui + dialogs/NewInstanceDialog.ui + dialogs/AboutDialog.ui + dialogs/ProgressDialog.ui + dialogs/IconPickerDialog.ui + dialogs/ProfileSelectDialog.ui + dialogs/EditAccountDialog.ui + dialogs/ExportInstanceDialog.ui + dialogs/LoginDialog.ui + dialogs/MSALoginDialog.ui + dialogs/UpdateDialog.ui + dialogs/NotificationDialog.ui + dialogs/SkinUploadDialog.ui + + # Widgets/other + widgets/CustomCommands.ui + widgets/MCModInfoFrame.ui +) + +set(MULTIMC_QRCS + resources/backgrounds/backgrounds.qrc + resources/multimc/multimc.qrc + resources/pe_dark/pe_dark.qrc + resources/pe_light/pe_light.qrc + resources/pe_colored/pe_colored.qrc + resources/pe_blue/pe_blue.qrc + resources/OSX/OSX.qrc + resources/iOS/iOS.qrc + resources/flat/flat.qrc + resources/documents/documents.qrc +) + +######## Windows resource files ######## +if(WIN32) + set(MULTIMC_RCS resources/multimc.rc) +endif() + +# Qt 5 stuff +qt5_wrap_ui(MULTIMC_UI ${MULTIMC_UIS}) +qt5_add_resources(MULTIMC_RESOURCES ${MULTIMC_QRCS}) + +# Add executable +add_library(MultiMC_logic STATIC ${LOGIC_SOURCES} ${MULTIMC_SOURCES} ${MULTIMC_UI} ${MULTIMC_RESOURCES}) +target_link_libraries(MultiMC_logic + systeminfo + MultiMC_quazip + MultiMC_classparser + ${NBT_NAME} + ${ZLIB_LIBRARIES} + optional-bare + tomlc99 + BuildConfig + Katabasis +) +target_link_libraries(MultiMC_logic + Qt5::Core + Qt5::Xml + Qt5::Network + Qt5::Concurrent + Qt5::Gui +) +target_link_libraries(MultiMC_logic + MultiMC_iconfix + ${QUAZIP_LIBRARIES} + hoedown + MultiMC_rainbow + LocalPeer + ganalytics +) + +add_executable(MultiMC MACOSX_BUNDLE WIN32 main.cpp ${MULTIMC_RCS}) +target_link_libraries(MultiMC MultiMC_logic) + +if(DEFINED MultiMC_APP_BINARY_NAME) + set_target_properties(MultiMC PROPERTIES OUTPUT_NAME "${MultiMC_APP_BINARY_NAME}") +endif() +if(DEFINED MultiMC_BINARY_RPATH) + SET_TARGET_PROPERTIES(MultiMC PROPERTIES INSTALL_RPATH "${MultiMC_BINARY_RPATH}") +endif() + +if(DEFINED MultiMC_APP_BINARY_DEFS) + target_compile_definitions(MultiMC PRIVATE ${MultiMC_APP_BINARY_DEFS}) + target_compile_definitions(MultiMC_logic PRIVATE ${MultiMC_APP_BINARY_DEFS}) +endif() + +install(TARGETS MultiMC + BUNDLE DESTINATION ${BUNDLE_DEST_DIR} COMPONENT Runtime + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime + RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime +) + +target_link_libraries(MultiMC_logic secrets) + +#### The MultiMC bundle mess! #### +# Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. +# NOTE: it seems that this absolutely has to be here, and nowhere else. +if(INSTALL_BUNDLE STREQUAL "full") + # Add qt.conf - this makes Qt stop looking for things outside the bundle + install( + CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" + COMPONENT Runtime + ) + # Bundle plugins + if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + # Image formats + install( + DIRECTORY "${QT_PLUGINS_DIR}/imageformats" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "tga|tiff|mng|webp" EXCLUDE + ) + # Icon engines + install( + DIRECTORY "${QT_PLUGINS_DIR}/iconengines" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "fontawesome" EXCLUDE + ) + # Platform plugins + install( + DIRECTORY "${QT_PLUGINS_DIR}/platforms" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "minimal|linuxfb|offscreen" EXCLUDE + ) + # Style plugins + if(EXISTS "${QT_PLUGINS_DIR}/styles") + install( + DIRECTORY "${QT_PLUGINS_DIR}/styles" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + endif() + else() + # Image formats + install( + DIRECTORY "${QT_PLUGINS_DIR}/imageformats" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "tga|tiff|mng|webp" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Icon engines + install( + DIRECTORY "${QT_PLUGINS_DIR}/iconengines" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "fontawesome" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Platform plugins + install( + DIRECTORY "${QT_PLUGINS_DIR}/platforms" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "minimal|linuxfb|offscreen" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Style plugins + if(EXISTS "${QT_PLUGINS_DIR}/styles") + install( + DIRECTORY "${QT_PLUGINS_DIR}/styles" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + endif() + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" + @ONLY + ) + install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" COMPONENT Runtime) +endif() diff --git a/application/ColorCache.cpp b/launcher/ColorCache.cpp similarity index 100% rename from application/ColorCache.cpp rename to launcher/ColorCache.cpp diff --git a/application/ColorCache.h b/launcher/ColorCache.h similarity index 100% rename from application/ColorCache.h rename to launcher/ColorCache.h diff --git a/application/ColumnResizer.cpp b/launcher/ColumnResizer.cpp similarity index 100% rename from application/ColumnResizer.cpp rename to launcher/ColumnResizer.cpp diff --git a/application/ColumnResizer.h b/launcher/ColumnResizer.h similarity index 100% rename from application/ColumnResizer.h rename to launcher/ColumnResizer.h diff --git a/api/logic/Commandline.cpp b/launcher/Commandline.cpp similarity index 100% rename from api/logic/Commandline.cpp rename to launcher/Commandline.cpp diff --git a/api/logic/Commandline.h b/launcher/Commandline.h similarity index 96% rename from api/logic/Commandline.h rename to launcher/Commandline.h index 09c1707e56..a4e7aa6110 100644 --- a/api/logic/Commandline.h +++ b/launcher/Commandline.h @@ -25,8 +25,6 @@ #include #include -#include "multimc_logic_export.h" - /** * @file libutil/include/cmdutils.h * @brief commandline parsing and processing utilities @@ -40,7 +38,7 @@ namespace Commandline * @param args the argument string * @return a QStringList containing all arguments */ -MULTIMC_LOGIC_EXPORT QStringList splitArgs(QString args); +QStringList splitArgs(QString args); /** * @brief The FlagStyle enum @@ -83,7 +81,7 @@ enum Enum /** * @brief The ParsingError class */ -class MULTIMC_LOGIC_EXPORT ParsingError : public std::runtime_error +class ParsingError : public std::runtime_error { public: ParsingError(const QString &what); @@ -92,7 +90,7 @@ class MULTIMC_LOGIC_EXPORT ParsingError : public std::runtime_error /** * @brief The Parser class */ -class MULTIMC_LOGIC_EXPORT Parser +class Parser { public: /** diff --git a/api/logic/DefaultVariable.h b/launcher/DefaultVariable.h similarity index 100% rename from api/logic/DefaultVariable.h rename to launcher/DefaultVariable.h diff --git a/api/gui/DesktopServices.cpp b/launcher/DesktopServices.cpp similarity index 100% rename from api/gui/DesktopServices.cpp rename to launcher/DesktopServices.cpp diff --git a/launcher/DesktopServices.h b/launcher/DesktopServices.h new file mode 100644 index 0000000000..1c081da412 --- /dev/null +++ b/launcher/DesktopServices.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +/** + * This wraps around QDesktopServices and adds workarounds where needed + * Use this instead of QDesktopServices! + */ +namespace DesktopServices +{ + /** + * Open a file in whatever application is applicable + */ + bool openFile(const QString &path); + + /** + * Open a file in the specified application + */ + bool openFile(const QString &application, const QString &path, const QString & workingDirectory = QString(), qint64 *pid = 0); + + /** + * Run an application + */ + bool run(const QString &application,const QStringList &args, const QString & workingDirectory = QString(), qint64 *pid = 0); + + /** + * Open a directory + */ + bool openDirectory(const QString &path, bool ensureExists = false); + + /** + * Open the URL, most likely in a browser. Maybe. + */ + bool openUrl(const QUrl &url); +} diff --git a/api/logic/Env.cpp b/launcher/Env.cpp similarity index 97% rename from api/logic/Env.cpp rename to launcher/Env.cpp index 42a1cff7af..abf9f58cc2 100644 --- a/api/logic/Env.cpp +++ b/launcher/Env.cpp @@ -100,8 +100,7 @@ void Env::initHttpMetaCache() m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); - m_metacache->addBase("TwitchPacks", QDir("cache/TwitchPacks").absolutePath()); - m_metacache->addBase("skins", QDir("accounts/skins").absolutePath()); + m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); diff --git a/api/logic/Env.h b/launcher/Env.h similarity index 95% rename from api/logic/Env.h rename to launcher/Env.h index 8b9b827e58..7d1a8bc9ae 100644 --- a/api/logic/Env.h +++ b/launcher/Env.h @@ -5,8 +5,6 @@ #include #include -#include "multimc_logic_export.h" - #include "QObjectPtr.h" class QNetworkAccessManager; @@ -25,7 +23,7 @@ class Index; #define ENV (Env::getInstance()) -class MULTIMC_LOGIC_EXPORT Env +class Env { friend class MultiMC; private: diff --git a/api/logic/Exception.h b/launcher/Exception.h similarity index 86% rename from api/logic/Exception.h rename to launcher/Exception.h index 9400b3f86a..fe0b86b5f8 100644 --- a/api/logic/Exception.h +++ b/launcher/Exception.h @@ -6,9 +6,7 @@ #include #include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT Exception : public std::exception +class Exception : public std::exception { public: Exception(const QString &message) : std::exception(), m_message(message) diff --git a/api/logic/ExponentialSeries.h b/launcher/ExponentialSeries.h similarity index 100% rename from api/logic/ExponentialSeries.h rename to launcher/ExponentialSeries.h diff --git a/api/logic/FileSystem.cpp b/launcher/FileSystem.cpp similarity index 100% rename from api/logic/FileSystem.cpp rename to launcher/FileSystem.cpp diff --git a/api/logic/FileSystem.h b/launcher/FileSystem.h similarity index 58% rename from api/logic/FileSystem.h rename to launcher/FileSystem.h index 55ec6a58f5..8f6e8b4898 100644 --- a/api/logic/FileSystem.h +++ b/launcher/FileSystem.h @@ -5,14 +5,13 @@ #include "Exception.h" #include "pathmatcher/IPathMatcher.h" -#include "multimc_logic_export.h" #include #include namespace FS { -class MULTIMC_LOGIC_EXPORT FileSystemException : public ::Exception +class FileSystemException : public ::Exception { public: FileSystemException(const QString &message) : Exception(message) {} @@ -21,31 +20,31 @@ class MULTIMC_LOGIC_EXPORT FileSystemException : public ::Exception /** * write data to a file safely */ -MULTIMC_LOGIC_EXPORT void write(const QString &filename, const QByteArray &data); +void write(const QString &filename, const QByteArray &data); /** * read data from a file safely\ */ -MULTIMC_LOGIC_EXPORT QByteArray read(const QString &filename); +QByteArray read(const QString &filename); /** * Update the last changed timestamp of an existing file */ -MULTIMC_LOGIC_EXPORT bool updateTimestamp(const QString & filename); +bool updateTimestamp(const QString & filename); /** * Creates all the folders in a path for the specified path * last segment of the path is treated as a file name and is ignored! */ -MULTIMC_LOGIC_EXPORT bool ensureFilePathExists(QString filenamepath); +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); +bool ensureFolderPathExists(QString filenamepath); -class MULTIMC_LOGIC_EXPORT copy +class copy { public: copy(const QString & src, const QString & dst) @@ -81,13 +80,13 @@ class MULTIMC_LOGIC_EXPORT copy /** * Delete a folder recursively */ -MULTIMC_LOGIC_EXPORT bool deletePath(QString path); +bool deletePath(QString path); -MULTIMC_LOGIC_EXPORT QString PathCombine(const QString &path1, const QString &path2); -MULTIMC_LOGIC_EXPORT QString PathCombine(const QString &path1, const QString &path2, const QString &path3); -MULTIMC_LOGIC_EXPORT QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4); +QString PathCombine(const QString &path1, const QString &path2); +QString PathCombine(const QString &path1, const QString &path2, const QString &path3); +QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4); -MULTIMC_LOGIC_EXPORT QString AbsolutePath(QString path); +QString AbsolutePath(QString path); /** * Resolve an executable @@ -99,7 +98,7 @@ MULTIMC_LOGIC_EXPORT QString AbsolutePath(QString path); * * @return absolute path to executable or null string */ -MULTIMC_LOGIC_EXPORT QString ResolveExecutable(QString path); +QString ResolveExecutable(QString path); /** * Normalize path @@ -109,20 +108,20 @@ MULTIMC_LOGIC_EXPORT QString ResolveExecutable(QString path); * * Returns false if the path logic somehow filed (and normalizedPath in invalid) */ -MULTIMC_LOGIC_EXPORT QString NormalizePath(QString path); +QString NormalizePath(QString path); -MULTIMC_LOGIC_EXPORT QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); +QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); -MULTIMC_LOGIC_EXPORT QString DirNameFromString(QString string, QString inDir = "."); +QString DirNameFromString(QString string, QString inDir = "."); /// Checks if the a given Path contains "!" -MULTIMC_LOGIC_EXPORT bool checkProblemticPathJava(QDir folder); +bool checkProblemticPathJava(QDir folder); // Get the Directory representing the User's Desktop -MULTIMC_LOGIC_EXPORT QString getDesktopDir(); +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); +bool createShortCut(QString location, QString dest, QStringList args, QString name, QString iconLocation); } diff --git a/api/logic/FileSystem_test.cpp b/launcher/FileSystem_test.cpp similarity index 100% rename from api/logic/FileSystem_test.cpp rename to launcher/FileSystem_test.cpp diff --git a/api/logic/Filter.cpp b/launcher/Filter.cpp similarity index 100% rename from api/logic/Filter.cpp rename to launcher/Filter.cpp diff --git a/api/logic/Filter.h b/launcher/Filter.h similarity index 74% rename from api/logic/Filter.h rename to launcher/Filter.h index 1ba48e482a..b55067acb1 100644 --- a/api/logic/Filter.h +++ b/launcher/Filter.h @@ -3,16 +3,14 @@ #include #include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT Filter +class Filter { public: virtual ~Filter(); virtual bool accepts(const QString & value) = 0; }; -class MULTIMC_LOGIC_EXPORT ContainsFilter: public Filter +class ContainsFilter: public Filter { public: ContainsFilter(const QString &pattern); @@ -22,7 +20,7 @@ class MULTIMC_LOGIC_EXPORT ContainsFilter: public Filter QString pattern; }; -class MULTIMC_LOGIC_EXPORT ExactFilter: public Filter +class ExactFilter: public Filter { public: ExactFilter(const QString &pattern); @@ -32,7 +30,7 @@ class MULTIMC_LOGIC_EXPORT ExactFilter: public Filter QString pattern; }; -class MULTIMC_LOGIC_EXPORT RegexpFilter: public Filter +class RegexpFilter: public Filter { public: RegexpFilter(const QString ®exp, bool invert); diff --git a/api/logic/GZip.cpp b/launcher/GZip.cpp similarity index 100% rename from api/logic/GZip.cpp rename to launcher/GZip.cpp diff --git a/api/logic/GZip.h b/launcher/GZip.h similarity index 77% rename from api/logic/GZip.h rename to launcher/GZip.h index c7eddbb300..7d4b1c3323 100644 --- a/api/logic/GZip.h +++ b/launcher/GZip.h @@ -1,9 +1,7 @@ #pragma once #include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT GZip +class GZip { public: static bool unzip(const QByteArray &compressedBytes, QByteArray &uncompressedBytes); diff --git a/api/logic/GZip_test.cpp b/launcher/GZip_test.cpp similarity index 100% rename from api/logic/GZip_test.cpp rename to launcher/GZip_test.cpp diff --git a/application/GuiUtil.cpp b/launcher/GuiUtil.cpp similarity index 100% rename from application/GuiUtil.cpp rename to launcher/GuiUtil.cpp diff --git a/application/GuiUtil.h b/launcher/GuiUtil.h similarity index 100% rename from application/GuiUtil.h rename to launcher/GuiUtil.h diff --git a/application/HoeDown.h b/launcher/HoeDown.h similarity index 100% rename from application/HoeDown.h rename to launcher/HoeDown.h diff --git a/api/logic/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp similarity index 100% rename from api/logic/InstanceCopyTask.cpp rename to launcher/InstanceCopyTask.cpp diff --git a/api/logic/InstanceCopyTask.h b/launcher/InstanceCopyTask.h similarity index 87% rename from api/logic/InstanceCopyTask.h rename to launcher/InstanceCopyTask.h index 6465e92d8f..8290173265 100644 --- a/api/logic/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -1,7 +1,6 @@ #pragma once #include "tasks/Task.h" -#include "multimc_logic_export.h" #include "net/NetJob.h" #include #include @@ -11,7 +10,7 @@ #include "BaseInstance.h" #include "InstanceTask.h" -class MULTIMC_LOGIC_EXPORT InstanceCopyTask : public InstanceTask +class InstanceCopyTask : public InstanceTask { Q_OBJECT public: diff --git a/api/logic/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp similarity index 100% rename from api/logic/InstanceCreationTask.cpp rename to launcher/InstanceCreationTask.cpp diff --git a/api/logic/InstanceCreationTask.h b/launcher/InstanceCreationTask.h similarity index 78% rename from api/logic/InstanceCreationTask.h rename to launcher/InstanceCreationTask.h index 154a854f8c..549971164b 100644 --- a/api/logic/InstanceCreationTask.h +++ b/launcher/InstanceCreationTask.h @@ -1,14 +1,13 @@ #pragma once #include "tasks/Task.h" -#include "multimc_logic_export.h" #include "net/NetJob.h" #include #include "settings/SettingsObject.h" #include "BaseVersion.h" #include "InstanceTask.h" -class MULTIMC_LOGIC_EXPORT InstanceCreationTask : public InstanceTask +class InstanceCreationTask : public InstanceTask { Q_OBJECT public: diff --git a/api/logic/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp similarity index 98% rename from api/logic/InstanceImportTask.cpp rename to launcher/InstanceImportTask.cpp index fe2cdd7533..3eac4d5794 100644 --- a/api/logic/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -238,6 +238,7 @@ void InstanceImportTask::processFlame() } QString forgeVersion; + QString fabricVersion; for(auto &loader: pack.minecraft.modLoaders) { auto id = loader.id; @@ -247,6 +248,12 @@ void InstanceImportTask::processFlame() forgeVersion = id; continue; } + if(id.startsWith("fabric-")) + { + id.remove("fabric-"); + fabricVersion = id; + continue; + } logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); } @@ -281,6 +288,10 @@ void InstanceImportTask::processFlame() } components->setComponentVersion("net.minecraftforge", forgeVersion); } + if(!fabricVersion.isEmpty()) + { + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); + } if (m_instIcon != "default") { instance.setIconKey(m_instIcon); diff --git a/api/logic/InstanceImportTask.h b/launcher/InstanceImportTask.h similarity index 94% rename from api/logic/InstanceImportTask.h rename to launcher/InstanceImportTask.h index 7291324d3c..72ae6851a8 100644 --- a/api/logic/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -16,7 +16,6 @@ #pragma once #include "InstanceTask.h" -#include "multimc_logic_export.h" #include "net/NetJob.h" #include #include @@ -32,7 +31,7 @@ namespace Flame class FileResolvingTask; } -class MULTIMC_LOGIC_EXPORT InstanceImportTask : public InstanceTask +class InstanceImportTask : public InstanceTask { Q_OBJECT public: diff --git a/api/logic/InstanceList.cpp b/launcher/InstanceList.cpp similarity index 98% rename from api/logic/InstanceList.cpp rename to launcher/InstanceList.cpp index 02fae6ac53..cb38853b55 100644 --- a/api/logic/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -387,9 +387,19 @@ InstanceList::InstListError InstanceList::loadList() add(newList); } m_dirty = false; + updateTotalPlayTime(); return NoError; } +void InstanceList::updateTotalPlayTime() +{ + totalPlayTime = 0; + for(auto const& itr : m_instances) + { + totalPlayTime += itr.get()->totalTimePlayed(); + } +} + void InstanceList::saveNow() { for(auto & item: m_instances) @@ -475,6 +485,7 @@ void InstanceList::propertiesChanged(BaseInstance *inst) if (i != -1) { emit dataChanged(index(i), index(i)); + updateTotalPlayTime(); } } @@ -848,4 +859,9 @@ bool InstanceList::destroyStagingPath(const QString& keyPath) return FS::deletePath(keyPath); } -#include "InstanceList.moc" \ No newline at end of file +int InstanceList::getTotalPlayTime() { + updateTotalPlayTime(); + return totalPlayTime; +} + +#include "InstanceList.moc" diff --git a/api/logic/InstanceList.h b/launcher/InstanceList.h similarity index 97% rename from api/logic/InstanceList.h rename to launcher/InstanceList.h index 8215cb665e..4d2dc1f60d 100644 --- a/api/logic/InstanceList.h +++ b/launcher/InstanceList.h @@ -22,8 +22,6 @@ #include "BaseInstance.h" -#include "multimc_logic_export.h" - #include "QObjectPtr.h" class QFileSystemWatcher; @@ -49,7 +47,7 @@ enum class GroupsState }; -class MULTIMC_LOGIC_EXPORT InstanceList : public QAbstractListModel +class InstanceList : public QAbstractListModel { Q_OBJECT @@ -128,6 +126,8 @@ class MULTIMC_LOGIC_EXPORT InstanceList : public QAbstractListModel */ bool destroyStagingPath(const QString & keyPath); + int getTotalPlayTime(); + signals: void dataIsInvalid(); void instancesChanged(); @@ -145,6 +145,7 @@ private slots: private: int getInstIndex(BaseInstance *inst) const; + void updateTotalPlayTime(); void suspendWatch(); void resumeWatch(); void add(const QList &list); @@ -155,6 +156,7 @@ private slots: private: int m_watchLevel = 0; + int totalPlayTime = 0; bool m_dirty = false; QList m_instances; QSet m_groupNameCache; diff --git a/application/InstancePageProvider.h b/launcher/InstancePageProvider.h similarity index 98% rename from application/InstancePageProvider.h rename to launcher/InstancePageProvider.h index dde36aefb9..3cb723c403 100644 --- a/application/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -46,7 +46,7 @@ class InstancePageProvider : public QObject, public BasePageProvider values.append(new TexturePackPage(onesix.get())); values.append(new NotesPage(onesix.get())); values.append(new WorldListPage(onesix.get(), onesix->worldList())); - values.append(new ServersPage(onesix.get())); + values.append(new ServersPage(onesix)); // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix.get())); diff --git a/application/InstanceProxyModel.cpp b/launcher/InstanceProxyModel.cpp similarity index 100% rename from application/InstanceProxyModel.cpp rename to launcher/InstanceProxyModel.cpp diff --git a/application/InstanceProxyModel.h b/launcher/InstanceProxyModel.h similarity index 100% rename from application/InstanceProxyModel.h rename to launcher/InstanceProxyModel.h diff --git a/api/logic/InstanceTask.cpp b/launcher/InstanceTask.cpp similarity index 100% rename from api/logic/InstanceTask.cpp rename to launcher/InstanceTask.cpp diff --git a/api/logic/InstanceTask.h b/launcher/InstanceTask.h similarity index 91% rename from api/logic/InstanceTask.h rename to launcher/InstanceTask.h index c5f6c7fd36..82e23f1114 100644 --- a/api/logic/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -1,10 +1,9 @@ #pragma once #include "tasks/Task.h" -#include "multimc_logic_export.h" #include "settings/SettingsObject.h" -class MULTIMC_LOGIC_EXPORT InstanceTask : public Task +class InstanceTask : public Task { Q_OBJECT public: diff --git a/application/InstanceWindow.cpp b/launcher/InstanceWindow.cpp similarity index 100% rename from application/InstanceWindow.cpp rename to launcher/InstanceWindow.cpp diff --git a/application/InstanceWindow.h b/launcher/InstanceWindow.h similarity index 100% rename from application/InstanceWindow.h rename to launcher/InstanceWindow.h diff --git a/application/JavaCommon.cpp b/launcher/JavaCommon.cpp similarity index 100% rename from application/JavaCommon.cpp rename to launcher/JavaCommon.cpp diff --git a/application/JavaCommon.h b/launcher/JavaCommon.h similarity index 100% rename from application/JavaCommon.h rename to launcher/JavaCommon.h diff --git a/api/logic/Json.cpp b/launcher/Json.cpp similarity index 100% rename from api/logic/Json.cpp rename to launcher/Json.cpp diff --git a/api/logic/Json.h b/launcher/Json.h similarity index 72% rename from api/logic/Json.h rename to launcher/Json.h index 34ff6fe2b3..f2e68f0c28 100644 --- a/api/logic/Json.h +++ b/launcher/Json.h @@ -16,32 +16,32 @@ namespace Json { -class MULTIMC_LOGIC_EXPORT JsonException : public ::Exception +class JsonException : public ::Exception { public: JsonException(const QString &message) : Exception(message) {} }; /// @throw FileSystemException -MULTIMC_LOGIC_EXPORT void write(const QJsonDocument &doc, const QString &filename); +void write(const QJsonDocument &doc, const QString &filename); /// @throw FileSystemException -MULTIMC_LOGIC_EXPORT void write(const QJsonObject &object, const QString &filename); +void write(const QJsonObject &object, const QString &filename); /// @throw FileSystemException -MULTIMC_LOGIC_EXPORT void write(const QJsonArray &array, const QString &filename); +void write(const QJsonArray &array, const QString &filename); -MULTIMC_LOGIC_EXPORT QByteArray toBinary(const QJsonObject &obj); -MULTIMC_LOGIC_EXPORT QByteArray toBinary(const QJsonArray &array); -MULTIMC_LOGIC_EXPORT QByteArray toText(const QJsonObject &obj); -MULTIMC_LOGIC_EXPORT QByteArray toText(const QJsonArray &array); +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"); +QJsonDocument requireDocument(const QByteArray &data, const QString &what = "Document"); /// @throw JsonException -MULTIMC_LOGIC_EXPORT QJsonDocument requireDocument(const QString &filename, const QString &what = "Document"); +QJsonDocument requireDocument(const QString &filename, const QString &what = "Document"); /// @throw JsonException -MULTIMC_LOGIC_EXPORT QJsonObject requireObject(const QJsonDocument &doc, const QString &what = "Document"); +QJsonObject requireObject(const QJsonDocument &doc, const QString &what = "Document"); /// @throw JsonException -MULTIMC_LOGIC_EXPORT QJsonArray requireArray(const QJsonDocument &doc, const QString &what = "Document"); +QJsonArray requireArray(const QJsonDocument &doc, const QString &what = "Document"); /////////////////// WRITING //////////////////// @@ -84,31 +84,31 @@ template T requireIsType(const QJsonValue &value, const QString &what = "Value"); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT double requireIsType(const QJsonValue &value, const QString &what); +template<> double requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT bool requireIsType(const QJsonValue &value, const QString &what); +template<> bool requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT int requireIsType(const QJsonValue &value, const QString &what); +template<> int requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QJsonObject requireIsType(const QJsonValue &value, const QString &what); +template<> QJsonObject requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QJsonArray requireIsType(const QJsonValue &value, const QString &what); +template<> QJsonArray requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QJsonValue requireIsType(const QJsonValue &value, const QString &what); +template<> QJsonValue requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QByteArray requireIsType(const QJsonValue &value, const QString &what); +template<> QByteArray requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QDateTime requireIsType(const QJsonValue &value, const QString &what); +template<> QDateTime requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QVariant requireIsType(const QJsonValue &value, const QString &what); +template<> QVariant requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QString requireIsType(const QJsonValue &value, const QString &what); +template<> QString requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QUuid requireIsType(const QJsonValue &value, const QString &what); +template<> QUuid requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QDir requireIsType(const QJsonValue &value, const QString &what); +template<> QDir requireIsType(const QJsonValue &value, const QString &what); /// @throw JsonException -template<> MULTIMC_LOGIC_EXPORT QUrl requireIsType(const QJsonValue &value, const QString &what); +template<> 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 diff --git a/application/KonamiCode.cpp b/launcher/KonamiCode.cpp similarity index 92% rename from application/KonamiCode.cpp rename to launcher/KonamiCode.cpp index 4c5af83713..46a2a0b2e7 100644 --- a/application/KonamiCode.cpp +++ b/launcher/KonamiCode.cpp @@ -35,7 +35,7 @@ void KonamiCode::input(QEvent* event) { m_progress = 0; } - if(m_progress == konamiCode.size()) + if(m_progress == static_cast(konamiCode.size())) { m_progress = 0; emit triggered(); diff --git a/application/KonamiCode.h b/launcher/KonamiCode.h similarity index 100% rename from application/KonamiCode.h rename to launcher/KonamiCode.h diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp new file mode 100644 index 0000000000..833bfc59bf --- /dev/null +++ b/launcher/LaunchController.cpp @@ -0,0 +1,399 @@ +#include "LaunchController.h" +#include "MainWindow.h" +#include +#include "MultiMC.h" +#include "dialogs/CustomMessageBox.h" +#include "dialogs/ProfileSelectDialog.h" +#include "dialogs/ProgressDialog.h" +#include "dialogs/EditAccountDialog.h" +#include "InstanceWindow.h" +#include "BuildConfig.h" +#include "JavaCommon.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LaunchController::LaunchController(QObject *parent) : Task(parent) +{ +} + +void LaunchController::executeTask() +{ + if (!m_instance) + { + emitFailed(tr("No instance specified!")); + return; + } + + login(); +} + +// FIXME: minecraft specific +void LaunchController::login() { + JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); + + // Find an account to use. + std::shared_ptr accounts = MMC->accounts(); + if (accounts->count() <= 0) + { + // Tell the user they need to log in at least one account in order to play. + auto reply = CustomMessageBox::selectable( + m_parentWidget, + tr("No Accounts"), + tr("In order to play Minecraft, you must have at least one Mojang or Minecraft " + "account logged in to MultiMC." + "Would you like to open the account manager to add an account now?"), + QMessageBox::Information, + QMessageBox::Yes | QMessageBox::No + )->exec(); + + if (reply == QMessageBox::Yes) + { + // Open the account manager. + MMC->ShowGlobalSettings(m_parentWidget, "accounts"); + } + } + + MinecraftAccountPtr account = accounts->activeAccount(); + if (account.get() == nullptr) + { + // If no default account is set, ask the user which one to use. + ProfileSelectDialog selectDialog( + tr("Which account would you like to use?"), + ProfileSelectDialog::GlobalDefaultCheckbox, + m_parentWidget + ); + + selectDialog.exec(); + + // Launch the instance with the selected account. + account = selectDialog.selectedAccount(); + + // If the user said to use the account as default, do that. + if (selectDialog.useAsGlobalDefault() && account.get() != nullptr) { + accounts->setActiveAccount(account->profileId()); + } + } + + // if no account is selected, we bail + if (!account.get()) + { + emitFailed(tr("No account selected for launch.")); + return; + } + + if(account->accessToken() != "ff64ff64ff64ff64ff64ff64ff64ff64") { + // Online + // we try empty password first :) + QString password; + // we loop until the user succeeds in logging in or gives up + bool tryagain = true; + // the failure. the default failure. + const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again.

This could be caused by a password change."); + QString failReason = needLoginAgain; + + while (tryagain) + { + m_session = std::make_shared(); + m_session->wants_online = m_online; + auto task = account->login(m_session, password); + // We'll need to validate the access token to make sure the account + // is still logged in. + ProgressDialog progDialog(m_parentWidget); + if (task) + { + progDialog.setSkipButton(true, tr("Play Offline")); + } + progDialog.execWithTask(task.get()); + if (!task->wasSuccessful()) + { + auto failReasonNew = task->failReason(); + if(failReasonNew == "Invalid token." || failReasonNew == "Invalid Signature") + { + // account->invalidateClientToken(); + failReason = needLoginAgain; + } + else failReason = failReasonNew; + } + } + switch (m_session->status) + { + case AuthSession::Undetermined: { + qCritical() << "Received undetermined session status during login. Bye."; + tryagain = false; + emitFailed(tr("Received undetermined session status during login.")); + return; + } + case AuthSession::RequiresPassword: { + // FIXME: this needs to understand MSA + EditAccountDialog passDialog(failReason, m_parentWidget, EditAccountDialog::PasswordField); + auto username = m_session->username; + auto chopN = [](QString toChop, int N) -> QString + { + if(toChop.size() > N) + { + auto left = toChop.left(N); + left += QString("\u25CF").repeated(toChop.size() - N); + return left; + } + return toChop; + }; + + if(username.contains('@')) + { + auto parts = username.split('@'); + auto mailbox = chopN(parts[0],3); + QString domain = chopN(parts[1], 3); + username = mailbox + '@' + domain; + } + passDialog.setUsername(username); + if (passDialog.exec() == QDialog::Accepted) + { + password = passDialog.password(); + } + else + { + tryagain = false; + emitFailed(tr("Received undetermined session status during login.")); + } + break; + } + case AuthSession::RequiresOAuth: { + auto errorString = tr("Microsoft account has expired and needs to be logged into manually again."); + QMessageBox::warning( + nullptr, + tr("Microsoft Account refresh failed"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + tryagain = false; + emitFailed(errorString); + return; + } + case AuthSession::GoneOrMigrated: { + auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to."); + QMessageBox::warning( + nullptr, + tr("Account gone"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + tryagain = false; + emitFailed(errorString); + return; + } + case AuthSession::PlayableOffline: { + // we ask the user for a player name + bool ok = false; + QString usedname = m_session->player_name; + QString name = QInputDialog::getText( + m_parentWidget, + tr("Player name"), + tr("Choose your offline mode player name."), + QLineEdit::Normal, + m_session->player_name, + &ok + ); + if (!ok) + { + tryagain = false; + break; + } + if (name.length()) + { + usedname = name; + } + m_session->MakeOffline(usedname); + // offline flavored game from here :3 + } + case AuthSession::PlayableOnline: + { + launchInstance(); + tryagain = false; + return; + } + } + emitFailed(tr("Failed to launch.")); + }else{ + // Offline + m_session = std::make_shared(); + m_session->client_token = account->accountData()->clientToken(); + m_session->access_token = account->accessToken(); + m_session->uuid = account->accountData()->minecraftProfile.id; + m_session->status = AuthSession::PlayableOffline; + m_session->MakeOffline(account->accountData()->userName()); + launchInstance(); + } +} + +void LaunchController::launchInstance() +{ + Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); + Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); + + if(!m_instance->reloadSettings()) + { + QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); + emitFailed(tr("Couldn't load the instance profile.")); + return; + } + + m_launcher = m_instance->createLaunchTask(m_session, m_serverToJoin); + if (!m_launcher) + { + emitFailed(tr("Couldn't instantiate a launcher.")); + return; + } + + auto console = qobject_cast(m_parentWidget); + auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); + if(!console && showConsole) + { + MMC->showInstanceWindow(m_instance); + } + connect(m_launcher.get(), &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); + connect(m_launcher.get(), &LaunchTask::succeeded, this, &LaunchController::onSucceeded); + connect(m_launcher.get(), &LaunchTask::failed, this, &LaunchController::onFailed); + connect(m_launcher.get(), &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); + + // Prepend Online and Auth Status + QString online_mode; + if(m_session->wants_online) { + online_mode = "online"; + + // Prepend Server Status + QStringList servers = {"authserver.mojang.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com"}; + QString resolved_servers = ""; + QHostInfo host_info; + + for(QString server : servers) { + host_info = QHostInfo::fromName(server); + resolved_servers = resolved_servers + server + " resolves to:\n ["; + if(!host_info.addresses().isEmpty()) { + for(QHostAddress address : host_info.addresses()) { + resolved_servers = resolved_servers + address.toString(); + if(!host_info.addresses().endsWith(address)) { + resolved_servers = resolved_servers + ", "; + } + } + } else { + resolved_servers = resolved_servers + "N/A"; + } + resolved_servers = resolved_servers + "]\n\n"; + } + m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::MultiMC)); + } else { + online_mode = "offline"; + } + + QString auth_server_status; + if(m_session->auth_server_online) { + auth_server_status = "online"; + } else { + auth_server_status = "offline"; + } + + m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\nAuthentication server is " + auth_server_status + "\n", MessageLevel::MultiMC)); + + // Prepend Version + m_launcher->prependStep(new TextPrint(m_launcher.get(), "MultiMC version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::MultiMC)); + m_launcher->start(); +} + +void LaunchController::readyForLaunch() +{ + if (!m_profiler) + { + m_launcher->proceed(); + return; + } + + QString error; + if (!m_profiler->check(&error)) + { + m_launcher->abort(); + QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't start profiler: %1").arg(error)); + emitFailed("Profiler startup failed!"); + return; + } + BaseProfiler *profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this); + + connect(profilerInstance, &BaseProfiler::readyToLaunch, [this](const QString & message) + { + QMessageBox msg; + msg.setText(tr("The game launch is delayed until you press the " + "button. This is the right time to setup the profiler, as the " + "profiler server is running now.\n\n%1").arg(message)); + msg.setWindowTitle(tr("Waiting.")); + msg.setIcon(QMessageBox::Information); + msg.addButton(tr("Launch"), QMessageBox::AcceptRole); + msg.setModal(true); + msg.exec(); + m_launcher->proceed(); + }); + connect(profilerInstance, &BaseProfiler::abortLaunch, [this](const QString & message) + { + QMessageBox msg; + msg.setText(tr("Couldn't start the profiler: %1").arg(message)); + msg.setWindowTitle(tr("Error")); + msg.setIcon(QMessageBox::Critical); + msg.addButton(QMessageBox::Ok); + msg.setModal(true); + msg.exec(); + m_launcher->abort(); + emitFailed("Profiler startup failed!"); + }); + profilerInstance->beginProfiling(m_launcher); +} + +void LaunchController::onSucceeded() +{ + emitSucceeded(); +} + +void LaunchController::onFailed(QString reason) +{ + if(m_instance->settings()->get("ShowConsoleOnError").toBool()) + { + MMC->showInstanceWindow(m_instance, "console"); + } + emitFailed(reason); +} + +void LaunchController::onProgressRequested(Task* task) +{ + ProgressDialog progDialog(m_parentWidget); + progDialog.setSkipButton(true, tr("Abort")); + m_launcher->proceed(); + progDialog.execWithTask(task); +} + +bool LaunchController::abort() +{ + if(!m_launcher) + { + return true; + } + if(!m_launcher->canAbort()) + { + return false; + } + auto response = CustomMessageBox::selectable( + m_parentWidget, tr("Kill Minecraft?"), + tr("This can cause the instance to get corrupted and should only be used if Minecraft " + "is frozen for some reason"), + QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)->exec(); + if (response == QMessageBox::Yes) + { + return m_launcher->abort(); + } + return false; +} diff --git a/application/LaunchController.h b/launcher/LaunchController.h similarity index 84% rename from application/LaunchController.h rename to launcher/LaunchController.h index 1d879028d9..5f177e0065 100644 --- a/application/LaunchController.h +++ b/launcher/LaunchController.h @@ -3,6 +3,8 @@ #include #include +#include "minecraft/launch/MinecraftServerTarget.h" + class InstanceWindow; class LaunchController: public Task { @@ -33,6 +35,10 @@ class LaunchController: public Task { m_parentWidget = widget; } + void setServerToJoin(MinecraftServerTargetPtr serverToJoin) + { + m_serverToJoin = std::move(serverToJoin); + } QString id() { return m_instance->id(); @@ -58,4 +64,5 @@ private slots: InstanceWindow *m_console = nullptr; AuthSessionPtr m_session; shared_qobject_ptr m_launcher; + MinecraftServerTargetPtr m_serverToJoin; }; diff --git a/api/logic/LoggedProcess.cpp b/launcher/LoggedProcess.cpp similarity index 100% rename from api/logic/LoggedProcess.cpp rename to launcher/LoggedProcess.cpp diff --git a/api/logic/LoggedProcess.h b/launcher/LoggedProcess.h similarity index 95% rename from api/logic/LoggedProcess.h rename to launcher/LoggedProcess.h index 327cdc6ac7..e52b8a7ba9 100644 --- a/api/logic/LoggedProcess.h +++ b/launcher/LoggedProcess.h @@ -17,13 +17,12 @@ #include #include "MessageLevel.h" -#include "multimc_logic_export.h" /* * This is a basic process. * It has line-based logging support and hides some of the nasty bits. */ -class MULTIMC_LOGIC_EXPORT LoggedProcess : public QProcess +class LoggedProcess : public QProcess { Q_OBJECT public: diff --git a/api/logic/MMCStrings.cpp b/launcher/MMCStrings.cpp similarity index 100% rename from api/logic/MMCStrings.cpp rename to launcher/MMCStrings.cpp diff --git a/launcher/MMCStrings.h b/launcher/MMCStrings.h new file mode 100644 index 0000000000..48052a00e1 --- /dev/null +++ b/launcher/MMCStrings.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace Strings +{ + int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs); +} diff --git a/api/logic/MMCZip.cpp b/launcher/MMCZip.cpp similarity index 100% rename from api/logic/MMCZip.cpp rename to launcher/MMCZip.cpp diff --git a/api/logic/MMCZip.h b/launcher/MMCZip.h similarity index 69% rename from api/logic/MMCZip.h rename to launcher/MMCZip.h index 98d9cd5bdd..9c47fa11f2 100644 --- a/api/logic/MMCZip.h +++ b/launcher/MMCZip.h @@ -21,8 +21,6 @@ #include "minecraft/mod/Mod.h" #include -#include "multimc_logic_export.h" - #include #include @@ -32,20 +30,20 @@ namespace MMCZip /** * Merge two zip files, using a filter function */ - bool MULTIMC_LOGIC_EXPORT mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, + bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, const JlCompress::FilterFunction filter = nullptr); /** * take a source jar, add mods to it, resulting in target jar */ - bool MULTIMC_LOGIC_EXPORT createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); + bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); /** * Find a single file in archive by file name (not path) * * \return the path prefix where the file is */ - QString MULTIMC_LOGIC_EXPORT findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString("")); + QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString("")); /** * Find a multiple files of the same name in archive by file name @@ -53,14 +51,14 @@ namespace MMCZip * * \return true if anything was found */ - bool MULTIMC_LOGIC_EXPORT findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString()); + bool findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString()); /** * Extract a subdirectory from an archive */ - nonstd::optional MULTIMC_LOGIC_EXPORT extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + nonstd::optional extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); - bool MULTIMC_LOGIC_EXPORT extractRelFile(QuaZip *zip, const QString & file, const QString &target); + bool extractRelFile(QuaZip *zip, const QString & file, const QString &target); /** * Extract a whole archive. @@ -69,7 +67,7 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - nonstd::optional MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir); + nonstd::optional extractDir(QString fileCompressed, QString dir); /** * Extract a subdirectory from an archive @@ -79,7 +77,7 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - nonstd::optional MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString subdir, QString dir); + nonstd::optional extractDir(QString fileCompressed, QString subdir, QString dir); /** * Extract a single file from an archive into a directory @@ -89,6 +87,6 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return true for success or false for failure */ - bool MULTIMC_LOGIC_EXPORT extractFile(QString fileCompressed, QString file, QString dir); + bool extractFile(QString fileCompressed, QString file, QString dir); } diff --git a/application/MainWindow.cpp b/launcher/MainWindow.cpp similarity index 92% rename from application/MainWindow.cpp rename to launcher/MainWindow.cpp index 1286007d50..182b22e99f 100644 --- a/application/MainWindow.cpp +++ b/launcher/MainWindow.cpp @@ -54,7 +54,7 @@ #include #include #include -#include +#include #include #include #include @@ -90,6 +90,20 @@ #include "KonamiCode.h" #include +namespace { +QString profileInUseFilter(const QString & profile, bool used) +{ + if(used) + { + return QObject::tr("%1 (in use)").arg(profile); + } + else + { + return profile; + } +} +} + // WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code template class Translated @@ -306,29 +320,35 @@ class MainWindow::Ui helpMenu = new QMenu(MainWindow); helpMenu->setToolTipsVisible(true); - actionReportBug = TranslatedAction(MainWindow); - actionReportBug->setObjectName(QStringLiteral("actionReportBug")); - actionReportBug->setIcon(MMC->getThemedIcon("bug")); - actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a Bug")); - actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with MultiMC.")); - all_actions.append(&actionReportBug); - helpMenu->addAction(actionReportBug); - - actionDISCORD = TranslatedAction(MainWindow); - actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); - actionDISCORD->setIcon(MMC->getThemedIcon("discord")); - actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord")); - actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open MultiMC discord voice chat.")); - all_actions.append(&actionDISCORD); - helpMenu->addAction(actionDISCORD); - - actionREDDIT = TranslatedAction(MainWindow); - actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); - actionREDDIT->setIcon(MMC->getThemedIcon("reddit-alien")); - actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Reddit")); - actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open MultiMC subreddit.")); - all_actions.append(&actionREDDIT); - helpMenu->addAction(actionREDDIT); + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { + actionReportBug = TranslatedAction(MainWindow); + actionReportBug->setObjectName(QStringLiteral("actionReportBug")); + actionReportBug->setIcon(MMC->getThemedIcon("bug")); + actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a Bug")); + actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with MultiMC.")); + all_actions.append(&actionReportBug); + helpMenu->addAction(actionReportBug); + } + + if (!BuildConfig.DISCORD_URL.isEmpty()) { + actionDISCORD = TranslatedAction(MainWindow); + actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); + actionDISCORD->setIcon(MMC->getThemedIcon("discord")); + actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord")); + actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open MultiMC discord voice chat.")); + all_actions.append(&actionDISCORD); + helpMenu->addAction(actionDISCORD); + } + + if (!BuildConfig.SUBREDDIT_URL.isEmpty()) { + actionREDDIT = TranslatedAction(MainWindow); + actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); + actionREDDIT->setIcon(MMC->getThemedIcon("reddit-alien")); + actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Reddit")); + actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open MultiMC subreddit.")); + all_actions.append(&actionREDDIT); + helpMenu->addAction(actionREDDIT); + } actionAbout = TranslatedAction(MainWindow); actionAbout->setObjectName(QStringLiteral("actionAbout")); @@ -718,8 +738,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow connect(MMC, &MultiMC::globalSettingsClosed, this, &MainWindow::globalSettingsClosed); m_statusLeft = new QLabel(tr("No instance selected"), this); + m_statusCenter = new QLabel(tr("Total playtime: 0s."), this); m_statusRight = new ServerStatus(this); statusBar()->addPermanentWidget(m_statusLeft, 1); + statusBar()->addPermanentWidget(m_statusCenter, 1); statusBar()->addPermanentWidget(m_statusRight, 0); // Add "manage accounts" button, right align @@ -732,7 +754,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow repopulateAccountsMenu(); accountMenuButton = new QToolButton(this); - accountMenuButton->setText(tr("Profiles")); accountMenuButton->setMenu(accountMenu); accountMenuButton->setPopupMode(QToolButton::InstantPopup); accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); @@ -746,49 +767,27 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow // Update the menu when the active account changes. // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Template hell sucks... - connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this] - { - activeAccountChanged(); - }); - connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this] - { - repopulateAccountsMenu(); - }); + connect( + MMC->accounts().get(), + &AccountList::activeAccountChanged, + [this] { + activeAccountChanged(); + } + ); + connect( + MMC->accounts().get(), + &AccountList::listChanged, + [this] + { + repopulateAccountsMenu(); + } + ); // Show initial account activeAccountChanged(); - auto accounts = MMC->accounts(); - - QList skin_dls; - for (int i = 0; i < accounts->count(); i++) - { - auto account = accounts->at(i); - if (!account) - { - qWarning() << "Null account at index" << i; - continue; - } - for (auto profile : account->profiles()) - { - auto meta = Env::getInstance().metacache()->resolveEntry("skins", profile.id + ".png"); - auto action = Net::Download::makeCached(QUrl(BuildConfig.SKINS_BASE + profile.id + ".png"), meta); - skin_dls.append(action); - meta->setStale(true); - } - } - if (!skin_dls.isEmpty()) - { - auto job = new NetJob("Startup player skins download"); - connect(job, &NetJob::succeeded, this, &MainWindow::skinJobFinished); - connect(job, &NetJob::failed, this, &MainWindow::skinJobFinished); - for (auto action : skin_dls) - { - job->addNetAction(action); - } - skin_download_job.reset(job); - job->start(); - } + // TODO: refresh accounts here? + // auto accounts = MMC->accounts(); // load the news { @@ -831,6 +830,29 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow // removing this looks stupid view->setFocus(); + + retranslateUi(); +} + +void MainWindow::retranslateUi() +{ + std::shared_ptr accounts = MMC->accounts(); + MinecraftAccountPtr active_account = accounts->activeAccount(); + if(active_account) { + auto profileLabel = profileInUseFilter(active_account->profileName(), active_account->isInUse()); + accountMenuButton->setText(profileLabel); + } + else { + accountMenuButton->setText(tr("Profiles")); + } + + if (m_selectedInstance) { + m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); + } else { + m_statusLeft->setText(tr("No instance selected")); + } + + ui->retranslateUi(this); } MainWindow::~MainWindow() @@ -850,12 +872,6 @@ void MainWindow::konamiTriggered() qDebug() << "Super Secret Mode ACTIVATED!"; } -void MainWindow::skinJobFinished() -{ - activeAccountChanged(); - skin_download_job.reset(); -} - void MainWindow::showInstanceContextMenu(const QPoint &pos) { QList actions; @@ -996,34 +1012,21 @@ void MainWindow::updateToolsMenu() ui->actionLaunchInstanceOffline->setMenu(launchOfflineMenu); } -QString profileInUseFilter(const QString & profile, bool used) -{ - if(used) - { - return profile + QObject::tr(" (in use)"); - } - else - { - return profile; - } -} - void MainWindow::repopulateAccountsMenu() { accountMenu->clear(); - std::shared_ptr accounts = MMC->accounts(); - MojangAccountPtr active_account = accounts->activeAccount(); + std::shared_ptr accounts = MMC->accounts(); + MinecraftAccountPtr active_account = accounts->activeAccount(); - QString active_username = ""; + QString active_profileId = ""; if (active_account != nullptr) { - active_username = active_account->username(); - const AccountProfile *profile = active_account->currentProfile(); + active_profileId = active_account->profileId(); // this can be called before accountMenuButton exists - if (profile != nullptr && accountMenuButton) + if (accountMenuButton) { - auto profileLabel = profileInUseFilter(profile->name, active_account->isInUse()); + auto profileLabel = profileInUseFilter(active_account->profileName(), active_account->isInUse()); accountMenuButton->setText(profileLabel); } } @@ -1039,22 +1042,19 @@ void MainWindow::repopulateAccountsMenu() // TODO: Nicer way to iterate? for (int i = 0; i < accounts->count(); i++) { - MojangAccountPtr account = accounts->at(i); - for (auto profile : account->profiles()) + MinecraftAccountPtr account = accounts->at(i); + auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); + QAction *action = new QAction(profileLabel, this); + action->setData(account->profileId()); + action->setCheckable(true); + if (active_profileId == account->profileId()) { - auto profileLabel = profileInUseFilter(profile.name, account->isInUse()); - QAction *action = new QAction(profileLabel, this); - action->setData(account->username()); - action->setCheckable(true); - if (active_username == account->username()) - { - action->setChecked(true); - } - - action->setIcon(SkinUtils::getFaceFromCache(profile.id)); - accountMenu->addAction(action); - connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + action->setChecked(true); } + + action->setIcon(account->getFace()); + accountMenu->addAction(action); + connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); } } @@ -1064,8 +1064,7 @@ void MainWindow::repopulateAccountsMenu() action->setCheckable(true); action->setIcon(MMC->getThemedIcon("noaccount")); action->setData(""); - if (active_username.isEmpty()) - { + if (active_profileId.isEmpty()) { action->setChecked(true); } @@ -1112,18 +1111,15 @@ void MainWindow::activeAccountChanged() { repopulateAccountsMenu(); - MojangAccountPtr account = MMC->accounts()->activeAccount(); + MinecraftAccountPtr account = MMC->accounts()->activeAccount(); - if (account != nullptr && account->username() != "") + // FIXME: this needs adjustment for MSA + if (account != nullptr && account->profileName() != "") { - const AccountProfile *profile = account->currentProfile(); - if (profile != nullptr) - { - auto profileLabel = profileInUseFilter(profile->name, account->isInUse()); - accountMenuButton->setIcon(SkinUtils::getFaceFromCache(profile->id)); - accountMenuButton->setText(profileLabel); - return; - } + auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); + accountMenuButton->setText(profileLabel); + accountMenuButton->setIcon(account->getFace()); + return; } // Set the icon to the "no account" icon. @@ -1307,7 +1303,6 @@ void MainWindow::setCatBackground(bool enabled) { QDateTime now = QDateTime::currentDateTime(); QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); - ; QString cat = (non_stupid_abs(now.daysTo(xmas)) <= 4) ? "catmas" : "kitteh"; view->setStyleSheet(QString(R"( GroupView @@ -1455,12 +1450,12 @@ void MainWindow::droppedURLs(QList urls) void MainWindow::on_actionREDDIT_triggered() { - DesktopServices::openUrl(QUrl("https://www.reddit.com/r/MultiMC/")); + DesktopServices::openUrl(QUrl(BuildConfig.SUBREDDIT_URL)); } void MainWindow::on_actionDISCORD_triggered() { - DesktopServices::openUrl(QUrl("https://discord.gg/multimc")); + DesktopServices::openUrl(QUrl(BuildConfig.DISCORD_URL)); } void MainWindow::on_actionChangeInstIcon_triggered() @@ -1506,6 +1501,7 @@ void MainWindow::setSelectedInstanceById(const QString &id) { QModelIndex selectionIndex = proxymodel->mapFromSource(index); view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect); + updateStatusCenter(); } } @@ -1638,7 +1634,7 @@ void MainWindow::on_actionManageAccounts_triggered() void MainWindow::on_actionReportBug_triggered() { - DesktopServices::openUrl(QUrl("https://github.com/MultiMC/MultiMC5/issues")); + DesktopServices::openUrl(QUrl(BuildConfig.BUG_TRACKER_URL)); } void MainWindow::on_actionPatreon_triggered() @@ -1745,7 +1741,7 @@ void MainWindow::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { - ui->retranslateUi(this); + retranslateUi(); } QMainWindow::changeEvent(event); } @@ -1834,6 +1830,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); ui->renameButton->setText(m_selectedInstance->name()); m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); + updateStatusCenter(); updateInstanceToolIcon(m_selectedInstance->iconKey()); updateToolsMenu(); @@ -1912,3 +1909,18 @@ void MainWindow::checkInstancePathForProblems() warning.exec(); } } + +void MainWindow::updateStatusCenter() +{ + int timeplayed = MMC->instances()->getTotalPlayTime(); + int minutesTotal = timeplayed / 60; + int seconds = timeplayed % 60; + int minutes = minutesTotal % 60; + int hours = minutesTotal / 60; + if(hours != 0) + m_statusCenter->setText(tr("Total playtime: %1h %2m %3s").arg(hours).arg(minutes).arg(seconds)); + else if(minutes != 0) + m_statusCenter->setText(tr("Total playtime: %1m %2s").arg(minutes).arg(seconds)); + else if(seconds != 0) + m_statusCenter->setText(tr("Total playtime: %1s").arg(seconds)); +} diff --git a/application/MainWindow.h b/launcher/MainWindow.h similarity index 97% rename from application/MainWindow.h rename to launcher/MainWindow.h index 3d4114de9f..67dec8cfd4 100644 --- a/application/MainWindow.h +++ b/launcher/MainWindow.h @@ -22,7 +22,7 @@ #include #include "BaseInstance.h" -#include "minecraft/auth/MojangAccount.h" +#include "minecraft/auth/MinecraftAccount.h" #include "net/NetJob.h" #include "updater/GoUpdate.h" @@ -149,8 +149,6 @@ private slots: void updateToolsMenu(); - void skinJobFinished(); - void instanceActivated(QModelIndex); void instanceChanged(const QModelIndex ¤t, const QModelIndex &previous); @@ -187,11 +185,14 @@ private slots: void globalSettingsClosed(); private: + void retranslateUi(); + void addInstance(QString url = QString()); void activateInstance(InstancePtr instance); void setCatBackground(bool enabled); void updateInstanceToolIcon(QString new_icon); void setSelectedInstanceById(const QString &id); + void updateStatusCenter(); void runModalTask(Task *task); void instanceFromInstanceTask(InstanceTask *task); @@ -205,12 +206,12 @@ private slots: InstanceProxyModel *proxymodel = nullptr; QToolButton *newsLabel = nullptr; QLabel *m_statusLeft = nullptr; + QLabel *m_statusCenter = nullptr; ServerStatus *m_statusRight = nullptr; QMenu *accountMenu = nullptr; QToolButton *accountMenuButton = nullptr; KonamiCode * secretEventFilter = nullptr; - unique_qobject_ptr skin_download_job; unique_qobject_ptr m_newsChecker; unique_qobject_ptr m_notificationChecker; diff --git a/api/logic/MessageLevel.cpp b/launcher/MessageLevel.cpp similarity index 100% rename from api/logic/MessageLevel.cpp rename to launcher/MessageLevel.cpp diff --git a/api/logic/MessageLevel.h b/launcher/MessageLevel.h similarity index 100% rename from api/logic/MessageLevel.h rename to launcher/MessageLevel.h diff --git a/application/MultiMC.cpp b/launcher/MultiMC.cpp similarity index 84% rename from application/MultiMC.cpp rename to launcher/MultiMC.cpp index 22946e08c2..c532ce8264 100644 --- a/application/MultiMC.cpp +++ b/launcher/MultiMC.cpp @@ -42,7 +42,7 @@ #include "dialogs/CustomMessageBox.h" #include "InstanceList.h" -#include +#include #include "icons/IconList.h" #include "net/HttpMetaCache.h" #include "Env.h" @@ -91,7 +91,8 @@ using namespace Commandline; "This usually fixes the problem and you can move the application elsewhere afterwards.\n"\ "\n" -static void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) +namespace { +void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { const char *levels = "DWCFIS"; const QString format("%1 %2 %3\n"); @@ -111,6 +112,47 @@ static void appDebugOutput(QtMsgType type, const QMessageLogContext &context, co fflush(stderr); } +QString getIdealPlatform(QString currentPlatform) { + auto info = Sys::getKernelInfo(); + switch(info.kernelType) { + case Sys::KernelType::Darwin: { + if(info.kernelMajor >= 17) { + // macOS 10.13 or newer + return "osx64-5.15.2"; + } + else { + // macOS 10.12 or older + return "osx64"; + } + } + case Sys::KernelType::Windows: { + // FIXME: 5.15.2 is not stable on Windows, due to a large number of completely unpredictable and hard to reproduce issues + break; +/* + if(info.kernelMajor == 6 && info.kernelMinor >= 1) { + // Windows 7 + return "win32-5.15.2"; + } + else if (info.kernelMajor > 6) { + // Above Windows 7 + return "win32-5.15.2"; + } + else { + // Below Windows 7 + return "win32"; + } +*/ + } + case Sys::KernelType::Undetermined: + case Sys::KernelType::Linux: { + break; + } + } + return currentPlatform; +} + +} + MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 @@ -191,6 +233,11 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) parser.addOption("launch"); parser.addShortOpt("launch", 'l'); parser.addDocumentation("launch", "Launch the specified instance (by instance ID)"); + // --server + parser.addOption("server"); + parser.addShortOpt("server", 's'); + parser.addDocumentation("server", "Join the specified server on launch " + "(only valid in combination with --launch)"); // --alive parser.addSwitch("alive"); parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after MultiMC starts"); @@ -232,6 +279,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) } } m_instanceIdToLaunch = args["launch"].toString(); + m_serverToJoin = args["server"].toString(); m_liveCheck = args["alive"].toBool(); m_zipToImport = args["import"].toUrl(); @@ -256,6 +304,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) xdgDataHome = QDir::homePath() + QLatin1String("/.local/share"); dataPath = xdgDataHome + "/multimc"; adjustedBy += "XDG standard " + dataPath; +#elif defined(Q_OS_MAC) + QDir foo(FS::PathCombine(applicationDirPath(), "../../Data")); + dataPath = foo.absolutePath(); + adjustedBy += "Fallback to special Mac location " + dataPath; #else dataPath = applicationDirPath(); adjustedBy += "Fallback to binary path " + dataPath; @@ -293,6 +345,70 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) return; } + if(m_instanceIdToLaunch.isEmpty() && !m_serverToJoin.isEmpty()) + { + std::cerr << "--server can only be used in combination with --launch!" << std::endl; + m_status = MultiMC::Failed; + return; + } + +#if defined(Q_OS_MAC) + // move user data to new location if on macOS and it still exists in Contents/MacOS + QDir fi(applicationDirPath()); + QString originalData = fi.absolutePath(); + // if the config file exists in Contents/MacOS, then user data is still there and needs to moved + if (QFileInfo::exists(FS::PathCombine(originalData, "multimc.cfg"))) + { + if (!QFileInfo::exists(FS::PathCombine(originalData, "dontmovemacdata"))) + { + QMessageBox::StandardButton askMoveDialogue; + askMoveDialogue = QMessageBox::question(nullptr, "MultiMC 5", "Would you like to move application data to a new data location? It will improve MultiMC's performance, but if you switch to older versions it will look like instances have disappeared. If you select no, you can migrate later in settings. You should select yes unless you're commonly switching between different versions of MultiMC (eg. develop and stable).", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + if (askMoveDialogue == QMessageBox::Yes) + { + qDebug() << "On macOS and found config file in old location, moving user data..."; + QDir dir; + QStringList dataFiles { + "*.log", // MultiMC-@.log + "accounts.json", + "accounts", + "assets", + "cache", + "icons", + "instances", + "libraries", + "meta", + "metacache", + "mods", + "multimc.cfg", + "themes", + "translations" + }; + QDirIterator files(originalData, dataFiles); + while (files.hasNext()) { + QString filePath(files.next()); + QString fileName(files.fileName()); + if (!dir.rename(filePath, FS::PathCombine(dataPath, fileName))) + { + qWarning() << "Failed to move " << fileName; + } + } + } + else + { + dataPath = originalData; + QDir::setCurrent(dataPath); + QFile file(originalData + "/dontmovemacdata"); + file.open(QIODevice::WriteOnly); + } + } + else + { + dataPath = originalData; + QDir::setCurrent(dataPath); + } + } +#endif + /* * Establish the mechanism for communication with an already running MultiMC that uses the same data path. * If there is one, tell it what the user actually wanted to do and exit. @@ -318,7 +434,15 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) } else { - m_peerInstance->sendMessage("launch " + m_instanceIdToLaunch, timeout); + if(!m_serverToJoin.isEmpty()) + { + m_peerInstance->sendMessage( + "launch-with-server " + m_instanceIdToLaunch + " " + m_serverToJoin, timeout); + } + else + { + m_peerInstance->sendMessage("launch " + m_instanceIdToLaunch, timeout); + } } m_status = MultiMC::Succeeded; return; @@ -399,6 +523,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) { qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; } + if(!m_serverToJoin.isEmpty()) + { + qDebug() << "Address of server to join :" << m_serverToJoin; + } qDebug() << "<> Paths set."; } @@ -514,6 +642,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("UseNativeGLFW", false); + // Game time + m_settings->registerSetting("ShowGameTime", true); + m_settings->registerSetting("RecordGameTime", true); + // Minecraft launch method m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); @@ -588,7 +720,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) // initialize the updater if(BuildConfig.UPDATER_ENABLED) { - m_updateChecker.reset(new UpdateChecker(BuildConfig.CHANLIST_URL, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD)); + auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM); + auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json"; + qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl; + m_updateChecker.reset(new UpdateChecker(channelUrl, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD)); qDebug() << "<> Updater started."; } @@ -655,7 +790,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) // and accounts { - m_accounts.reset(new MojangAccountList(this)); + m_accounts.reset(new AccountList(this)); qDebug() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); @@ -840,8 +975,19 @@ void MultiMC::performMainStartupAction() auto inst = instances()->getInstanceById(m_instanceIdToLaunch); if(inst) { - qDebug() << "<> Instance launching:" << m_instanceIdToLaunch; - launch(inst, true, nullptr); + MinecraftServerTargetPtr serverToJoin = nullptr; + + if(!m_serverToJoin.isEmpty()) + { + serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin))); + qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching with server" << m_serverToJoin; + } + else + { + qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; + } + + launch(inst, true, nullptr, serverToJoin); return; } } @@ -923,6 +1069,31 @@ void MultiMC::messageReceived(const QString& message) launch(inst, true, nullptr); } } + else if(command == "launch-with-server") + { + QString instanceID = message.section(' ', 1, 1); + QString serverToJoin = message.section(' ', 2, 2); + if(instanceID.isEmpty()) + { + qWarning() << "Received" << command << "message without an instance ID."; + return; + } + if(serverToJoin.isEmpty()) + { + qWarning() << "Received" << command << "message without a server to join."; + return; + } + auto inst = instances()->getInstanceById(instanceID); + if(inst) + { + launch( + inst, + true, + nullptr, + std::make_shared(MinecraftServerTarget::parse(serverToJoin)) + ); + } + } else { qWarning() << "Received invalid message" << message; @@ -1010,8 +1181,12 @@ bool MultiMC::openJsonEditor(const QString &filename) } } -bool MultiMC::launch(InstancePtr instance, bool online, BaseProfilerFactory *profiler) -{ +bool MultiMC::launch( + InstancePtr instance, + bool online, + BaseProfilerFactory *profiler, + MinecraftServerTargetPtr serverToJoin +) { if(m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; @@ -1032,6 +1207,7 @@ bool MultiMC::launch(InstancePtr instance, bool online, BaseProfilerFactory *pro controller->setInstance(instance); controller->setOnline(online); controller->setProfiler(profiler); + controller->setServerToJoin(serverToJoin); if(window) { controller->setParentWidget(window); diff --git a/application/MultiMC.h b/launcher/MultiMC.h similarity index 93% rename from application/MultiMC.h rename to launcher/MultiMC.h index e6588a1470..59fd7345f0 100644 --- a/application/MultiMC.h +++ b/launcher/MultiMC.h @@ -11,6 +11,8 @@ #include +#include "minecraft/launch/MinecraftServerTarget.h" + class LaunchController; class LocalPeer; class InstanceWindow; @@ -22,7 +24,7 @@ class QFile; class HttpMetaCache; class SettingsObject; class InstanceList; -class MojangAccountList; +class AccountList; class IconList; class QNetworkAccessManager; class JavaInstallList; @@ -109,7 +111,7 @@ class MultiMC : public QApplication return m_mcedit.get(); } - std::shared_ptr accounts() const + std::shared_ptr accounts() const { return m_accounts; } @@ -150,7 +152,12 @@ class MultiMC : public QApplication void globalSettingsClosed(); public slots: - bool launch(InstancePtr instance, bool online = true, BaseProfilerFactory *profiler = nullptr); + bool launch( + InstancePtr instance, + bool online = true, + BaseProfilerFactory *profiler = nullptr, + MinecraftServerTargetPtr serverToJoin = nullptr + ); bool kill(InstancePtr instance); private slots: @@ -181,7 +188,7 @@ private slots: FolderInstanceProvider * m_instanceFolder = nullptr; std::shared_ptr m_icons; std::shared_ptr m_updateChecker; - std::shared_ptr m_accounts; + std::shared_ptr m_accounts; std::shared_ptr m_javalist; std::shared_ptr m_translations; std::shared_ptr m_globalSettingsProvider; @@ -221,6 +228,7 @@ private slots: SetupWizard * m_setupWizard = nullptr; public: QString m_instanceIdToLaunch; + QString m_serverToJoin; bool m_liveCheck = false; QUrl m_zipToImport; std::unique_ptr logFile; diff --git a/api/logic/NullInstance.h b/launcher/NullInstance.h similarity index 91% rename from api/logic/NullInstance.h rename to launcher/NullInstance.h index e9ba1a13c0..94ed6c3a9c 100644 --- a/api/logic/NullInstance.h +++ b/launcher/NullInstance.h @@ -27,7 +27,7 @@ class NullInstance: public BaseInstance { return instanceRoot(); }; - shared_qobject_ptr createLaunchTask(AuthSessionPtr) override + shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override { return nullptr; } @@ -67,7 +67,7 @@ class NullInstance: public BaseInstance { return false; } - QStringList verboseDescription(AuthSessionPtr session) override + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override { QStringList out; out << "Null instance - placeholder."; diff --git a/api/logic/ProblemProvider.h b/launcher/ProblemProvider.h similarity index 86% rename from api/logic/ProblemProvider.h rename to launcher/ProblemProvider.h index 33c9d3649c..cd4745fa14 100644 --- a/api/logic/ProblemProvider.h +++ b/launcher/ProblemProvider.h @@ -1,7 +1,5 @@ #pragma once -#include "multimc_logic_export.h" - enum class ProblemSeverity { None, @@ -15,7 +13,7 @@ struct PatchProblem QString m_description; }; -class MULTIMC_LOGIC_EXPORT ProblemProvider +class ProblemProvider { public: virtual ~ProblemProvider() {}; @@ -23,7 +21,7 @@ class MULTIMC_LOGIC_EXPORT ProblemProvider virtual ProblemSeverity getProblemSeverity() const = 0; }; -class MULTIMC_LOGIC_EXPORT ProblemContainer : public ProblemProvider +class ProblemContainer : public ProblemProvider { public: const QList getProblems() const override diff --git a/api/logic/QObjectPtr.h b/launcher/QObjectPtr.h similarity index 100% rename from api/logic/QObjectPtr.h rename to launcher/QObjectPtr.h diff --git a/api/logic/RWStorage.h b/launcher/RWStorage.h similarity index 97% rename from api/logic/RWStorage.h rename to launcher/RWStorage.h index 5d7923676e..3028388e25 100644 --- a/api/logic/RWStorage.h +++ b/launcher/RWStorage.h @@ -1,6 +1,9 @@ #pragma once #include #include +#include +#include + template class RWStorage { diff --git a/api/logic/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp similarity index 100% rename from api/logic/RecursiveFileSystemWatcher.cpp rename to launcher/RecursiveFileSystemWatcher.cpp diff --git a/api/logic/RecursiveFileSystemWatcher.h b/launcher/RecursiveFileSystemWatcher.h similarity index 91% rename from api/logic/RecursiveFileSystemWatcher.h rename to launcher/RecursiveFileSystemWatcher.h index c9c39f49ce..cc837d6034 100644 --- a/api/logic/RecursiveFileSystemWatcher.h +++ b/launcher/RecursiveFileSystemWatcher.h @@ -4,9 +4,7 @@ #include #include "pathmatcher/IPathMatcher.h" -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT RecursiveFileSystemWatcher : public QObject +class RecursiveFileSystemWatcher : public QObject { Q_OBJECT public: diff --git a/api/logic/SeparatorPrefixTree.h b/launcher/SeparatorPrefixTree.h similarity index 100% rename from api/logic/SeparatorPrefixTree.h rename to launcher/SeparatorPrefixTree.h diff --git a/api/gui/SkinUtils.cpp b/launcher/SkinUtils.cpp similarity index 91% rename from api/gui/SkinUtils.cpp rename to launcher/SkinUtils.cpp index ec969889f8..a196173eb7 100644 --- a/api/gui/SkinUtils.cpp +++ b/launcher/SkinUtils.cpp @@ -30,9 +30,7 @@ namespace SkinUtils */ QPixmap getFaceFromCache(QString username, int height, int width) { - QFile fskin(ENV.metacache() - ->resolveEntry("skins", username + ".png") - ->getFullPath()); + QFile fskin(ENV.metacache()->resolveEntry("skins", username + ".png")->getFullPath()); if (fskin.exists()) { diff --git a/api/gui/SkinUtils.h b/launcher/SkinUtils.h similarity index 84% rename from api/gui/SkinUtils.h rename to launcher/SkinUtils.h index b44f422836..c1f437ab0b 100644 --- a/api/gui/SkinUtils.h +++ b/launcher/SkinUtils.h @@ -17,9 +17,7 @@ #include -#include "multimc_gui_export.h" - namespace SkinUtils { -QPixmap MULTIMC_GUI_EXPORT getFaceFromCache(QString id, int height = 64, int width = 64); +QPixmap getFaceFromCache(QString id, int height = 64, int width = 64); } diff --git a/application/UpdateController.cpp b/launcher/UpdateController.cpp similarity index 100% rename from application/UpdateController.cpp rename to launcher/UpdateController.cpp diff --git a/application/UpdateController.h b/launcher/UpdateController.h similarity index 100% rename from application/UpdateController.h rename to launcher/UpdateController.h diff --git a/api/logic/Usable.h b/launcher/Usable.h similarity index 100% rename from api/logic/Usable.h rename to launcher/Usable.h diff --git a/api/logic/Version.cpp b/launcher/Version.cpp similarity index 100% rename from api/logic/Version.cpp rename to launcher/Version.cpp diff --git a/api/logic/Version.h b/launcher/Version.h similarity index 97% rename from api/logic/Version.h rename to launcher/Version.h index c5d9308150..9fe12d6dad 100644 --- a/api/logic/Version.h +++ b/launcher/Version.h @@ -3,11 +3,9 @@ #include #include -#include "multimc_logic_export.h" - class QUrl; -class MULTIMC_LOGIC_EXPORT Version +class Version { public: Version(const QString &str); diff --git a/application/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp similarity index 100% rename from application/VersionProxyModel.cpp rename to launcher/VersionProxyModel.cpp diff --git a/application/VersionProxyModel.h b/launcher/VersionProxyModel.h similarity index 100% rename from application/VersionProxyModel.h rename to launcher/VersionProxyModel.h diff --git a/api/logic/Version_test.cpp b/launcher/Version_test.cpp similarity index 100% rename from api/logic/Version_test.cpp rename to launcher/Version_test.cpp diff --git a/api/logic/WatchLock.h b/launcher/WatchLock.h similarity index 100% rename from api/logic/WatchLock.h rename to launcher/WatchLock.h diff --git a/application/dialogs/AboutDialog.cpp b/launcher/dialogs/AboutDialog.cpp similarity index 100% rename from application/dialogs/AboutDialog.cpp rename to launcher/dialogs/AboutDialog.cpp diff --git a/application/dialogs/AboutDialog.h b/launcher/dialogs/AboutDialog.h similarity index 100% rename from application/dialogs/AboutDialog.h rename to launcher/dialogs/AboutDialog.h diff --git a/application/dialogs/AboutDialog.ui b/launcher/dialogs/AboutDialog.ui similarity index 100% rename from application/dialogs/AboutDialog.ui rename to launcher/dialogs/AboutDialog.ui diff --git a/application/dialogs/CopyInstanceDialog.cpp b/launcher/dialogs/CopyInstanceDialog.cpp similarity index 100% rename from application/dialogs/CopyInstanceDialog.cpp rename to launcher/dialogs/CopyInstanceDialog.cpp diff --git a/application/dialogs/CopyInstanceDialog.h b/launcher/dialogs/CopyInstanceDialog.h similarity index 100% rename from application/dialogs/CopyInstanceDialog.h rename to launcher/dialogs/CopyInstanceDialog.h diff --git a/application/dialogs/CopyInstanceDialog.ui b/launcher/dialogs/CopyInstanceDialog.ui similarity index 100% rename from application/dialogs/CopyInstanceDialog.ui rename to launcher/dialogs/CopyInstanceDialog.ui diff --git a/application/dialogs/CustomMessageBox.cpp b/launcher/dialogs/CustomMessageBox.cpp similarity index 100% rename from application/dialogs/CustomMessageBox.cpp rename to launcher/dialogs/CustomMessageBox.cpp diff --git a/application/dialogs/CustomMessageBox.h b/launcher/dialogs/CustomMessageBox.h similarity index 100% rename from application/dialogs/CustomMessageBox.h rename to launcher/dialogs/CustomMessageBox.h diff --git a/application/dialogs/EditAccountDialog.cpp b/launcher/dialogs/EditAccountDialog.cpp similarity index 100% rename from application/dialogs/EditAccountDialog.cpp rename to launcher/dialogs/EditAccountDialog.cpp diff --git a/application/dialogs/EditAccountDialog.h b/launcher/dialogs/EditAccountDialog.h similarity index 100% rename from application/dialogs/EditAccountDialog.h rename to launcher/dialogs/EditAccountDialog.h diff --git a/application/dialogs/EditAccountDialog.ui b/launcher/dialogs/EditAccountDialog.ui similarity index 100% rename from application/dialogs/EditAccountDialog.ui rename to launcher/dialogs/EditAccountDialog.ui diff --git a/application/dialogs/ExportInstanceDialog.cpp b/launcher/dialogs/ExportInstanceDialog.cpp similarity index 100% rename from application/dialogs/ExportInstanceDialog.cpp rename to launcher/dialogs/ExportInstanceDialog.cpp diff --git a/application/dialogs/ExportInstanceDialog.h b/launcher/dialogs/ExportInstanceDialog.h similarity index 100% rename from application/dialogs/ExportInstanceDialog.h rename to launcher/dialogs/ExportInstanceDialog.h diff --git a/application/dialogs/ExportInstanceDialog.ui b/launcher/dialogs/ExportInstanceDialog.ui similarity index 100% rename from application/dialogs/ExportInstanceDialog.ui rename to launcher/dialogs/ExportInstanceDialog.ui diff --git a/application/dialogs/IconPickerDialog.cpp b/launcher/dialogs/IconPickerDialog.cpp similarity index 100% rename from application/dialogs/IconPickerDialog.cpp rename to launcher/dialogs/IconPickerDialog.cpp diff --git a/application/dialogs/IconPickerDialog.h b/launcher/dialogs/IconPickerDialog.h similarity index 100% rename from application/dialogs/IconPickerDialog.h rename to launcher/dialogs/IconPickerDialog.h diff --git a/application/dialogs/IconPickerDialog.ui b/launcher/dialogs/IconPickerDialog.ui similarity index 100% rename from application/dialogs/IconPickerDialog.ui rename to launcher/dialogs/IconPickerDialog.ui diff --git a/application/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp similarity index 64% rename from application/dialogs/LoginDialog.cpp rename to launcher/dialogs/LoginDialog.cpp index 32f8a48fd8..befd87a510 100644 --- a/application/dialogs/LoginDialog.cpp +++ b/launcher/dialogs/LoginDialog.cpp @@ -16,7 +16,7 @@ #include "LoginDialog.h" #include "ui_LoginDialog.h" -#include "minecraft/auth/YggdrasilTask.h" +#include "minecraft/auth/AccountTask.h" #include @@ -41,15 +41,22 @@ void LoginDialog::accept() setUserInputsEnabled(false); ui->progressBar->setVisible(true); - // Setup the login task and start it - m_account = MojangAccount::createFromUsername(ui->userTextBox->text()); - m_loginTask = m_account->login(nullptr, ui->passTextBox->text()); - connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); - connect(m_loginTask.get(), &Task::succeeded, this, - &LoginDialog::onTaskSucceeded); - connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus); - connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress); - m_loginTask->start(); + if(!ui->passTextBox->text().isEmpty()){ + // Online mode + // Setup the login task and start it + m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text()); + m_loginTask = m_account->login(nullptr, ui->passTextBox->text()); + connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, + &LoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress); + m_loginTask->start(); + }else{ + // Offline mode + m_account = MinecraftAccount::createFromUsernameOffline(ui->userTextBox->text()); + QDialog::accept(); + } } void LoginDialog::setUserInputsEnabled(bool enable) @@ -63,18 +70,28 @@ void LoginDialog::setUserInputsEnabled(bool enable) void LoginDialog::on_userTextBox_textEdited(const QString &newText) { ui->buttonBox->button(QDialogButtonBox::Ok) - ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty()); + ->setEnabled(!newText.isEmpty()); } void LoginDialog::on_passTextBox_textEdited(const QString &newText) { ui->buttonBox->button(QDialogButtonBox::Ok) - ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty()); + ->setEnabled(!ui->userTextBox->text().isEmpty()); } void LoginDialog::onTaskFailed(const QString &reason) { // Set message - ui->label->setText("" + reason + ""); + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "
"; + } + else { + processed += "
"; + } + } + ui->label->setText(processed); // Re-enable user-interaction setUserInputsEnabled(true); @@ -98,7 +115,7 @@ void LoginDialog::onTaskProgress(qint64 current, qint64 total) } // Public interface -MojangAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg) +MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg) { LoginDialog dlg(parent); dlg.ui->label->setText(msg); diff --git a/application/dialogs/LoginDialog.h b/launcher/dialogs/LoginDialog.h similarity index 89% rename from application/dialogs/LoginDialog.h rename to launcher/dialogs/LoginDialog.h index 16bdddfbf2..134636407d 100644 --- a/application/dialogs/LoginDialog.h +++ b/launcher/dialogs/LoginDialog.h @@ -18,7 +18,7 @@ #include #include -#include "minecraft/auth/MojangAccount.h" +#include "minecraft/auth/MinecraftAccount.h" namespace Ui { @@ -32,7 +32,7 @@ class LoginDialog : public QDialog public: ~LoginDialog(); - static MojangAccountPtr newAccount(QWidget *parent, QString message); + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); private: explicit LoginDialog(QWidget *parent = 0); @@ -53,6 +53,6 @@ protected private: Ui::LoginDialog *ui; - MojangAccountPtr m_account; + MinecraftAccountPtr m_account; std::shared_ptr m_loginTask; }; diff --git a/application/dialogs/LoginDialog.ui b/launcher/dialogs/LoginDialog.ui similarity index 80% rename from application/dialogs/LoginDialog.ui rename to launcher/dialogs/LoginDialog.ui index d92fbae391..60c866ccaa 100644 --- a/application/dialogs/LoginDialog.ui +++ b/launcher/dialogs/LoginDialog.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 162 + 421 + 198 @@ -33,6 +33,22 @@ + + + + + 75 + true + + + + Note: For offline mode use "Email" field for username and "Password" field leave blank + + + Qt::RichText + + + diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp new file mode 100644 index 0000000000..15c04061f4 --- /dev/null +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -0,0 +1,141 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MSALoginDialog.h" +#include "ui_MSALoginDialog.h" + +#include "minecraft/auth/AccountTask.h" + +#include +#include + +MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + // ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +int MSALoginDialog::exec() { + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + // Setup the login task and start it + m_account = MinecraftAccount::createBlankMSA(); + m_loginTask = m_account->loginMSA(nullptr); + connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress); + connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode); + connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode); + connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick); + m_loginTask->start(); + + return QDialog::exec(); +} + + +MSALoginDialog::~MSALoginDialog() +{ + delete ui; +} + +void MSALoginDialog::externalLoginTick() { + m_externalLoginElapsed++; + ui->progressBar->setValue(m_externalLoginElapsed); + ui->progressBar->repaint(); + + if(m_externalLoginElapsed >= m_externalLoginTimeout) { + m_externalLoginTimer.stop(); + } +} + + +void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) { + m_externalLoginElapsed = 0; + m_externalLoginTimeout = expiresIn; + + m_externalLoginTimer.setInterval(1000); + m_externalLoginTimer.setSingleShot(false); + m_externalLoginTimer.start(); + + ui->progressBar->setMaximum(expiresIn); + ui->progressBar->setValue(m_externalLoginElapsed); + + QString urlString = uri.toString(); + QString linkString = QString("%2").arg(urlString, urlString); + ui->label->setText(tr("

Please open up %1 in a browser and put in the code %2 to proceed with login.

").arg(linkString, code)); +} + +void MSALoginDialog::hideVerificationUriAndCode() { + m_externalLoginTimer.stop(); +} + +void MSALoginDialog::setUserInputsEnabled(bool enable) +{ + ui->buttonBox->setEnabled(enable); +} + +void MSALoginDialog::onTaskFailed(const QString &reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "
"; + } + else { + processed += "
"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void MSALoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void MSALoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void MSALoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg) +{ + MSALoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return 0; +} diff --git a/launcher/dialogs/MSALoginDialog.h b/launcher/dialogs/MSALoginDialog.h new file mode 100644 index 0000000000..3d26a0dd6e --- /dev/null +++ b/launcher/dialogs/MSALoginDialog.h @@ -0,0 +1,63 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" + +namespace Ui +{ +class MSALoginDialog; +} + +class MSALoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~MSALoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + int exec() override; + +private: + explicit MSALoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + + void externalLoginTick(); + +private: + Ui::MSALoginDialog *ui; + MinecraftAccountPtr m_account; + std::shared_ptr m_loginTask; + QTimer m_externalLoginTimer; + int m_externalLoginElapsed = 0; + int m_externalLoginTimeout = 0; +}; + diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui new file mode 100644 index 0000000000..78cbfb269f --- /dev/null +++ b/launcher/dialogs/MSALoginDialog.ui @@ -0,0 +1,65 @@ + + + MSALoginDialog + + + + 0 + 0 + 491 + 143 + + + + + 0 + 0 + + + + Add Microsoft Account + + + + + + Message label placeholder. + +aaaaa + + + Qt::RichText + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 24 + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + diff --git a/application/dialogs/NewComponentDialog.cpp b/launcher/dialogs/NewComponentDialog.cpp similarity index 100% rename from application/dialogs/NewComponentDialog.cpp rename to launcher/dialogs/NewComponentDialog.cpp diff --git a/application/dialogs/NewComponentDialog.h b/launcher/dialogs/NewComponentDialog.h similarity index 100% rename from application/dialogs/NewComponentDialog.h rename to launcher/dialogs/NewComponentDialog.h diff --git a/application/dialogs/NewComponentDialog.ui b/launcher/dialogs/NewComponentDialog.ui similarity index 100% rename from application/dialogs/NewComponentDialog.ui rename to launcher/dialogs/NewComponentDialog.ui diff --git a/application/dialogs/NewInstanceDialog.cpp b/launcher/dialogs/NewInstanceDialog.cpp similarity index 95% rename from application/dialogs/NewInstanceDialog.cpp rename to launcher/dialogs/NewInstanceDialog.cpp index d70cbffed7..86963149fd 100644 --- a/application/dialogs/NewInstanceDialog.cpp +++ b/launcher/dialogs/NewInstanceDialog.cpp @@ -37,7 +37,7 @@ #include #include #include -#include +#include #include #include @@ -124,17 +124,17 @@ void NewInstanceDialog::accept() QList NewInstanceDialog::getPages() { importPage = new ImportPage(this); - twitchPage = new TwitchPage(this); + flamePage = new FlamePage(this); auto technicPage = new TechnicPage(this); return { new VanillaPage(this), importPage, new AtlPage(this), + flamePage, new FtbPage(this), new LegacyFTB::Page(this), - technicPage, - twitchPage + technicPage }; } @@ -173,6 +173,14 @@ void NewInstanceDialog::setSuggestedIconFromFile(const QString &path, const QStr ui->iconButton->setIcon(QIcon(path)); } +void NewInstanceDialog::setSuggestedIcon(const QString &key) +{ + auto icon = MMC->icons()->getIcon(key); + importIcon = false; + + ui->iconButton->setIcon(icon); +} + InstanceTask * NewInstanceDialog::extractTask() { InstanceTask * extracted = creationTask.get(); diff --git a/application/dialogs/NewInstanceDialog.h b/launcher/dialogs/NewInstanceDialog.h similarity index 95% rename from application/dialogs/NewInstanceDialog.h rename to launcher/dialogs/NewInstanceDialog.h index 88ed00e58d..53abf8cfbd 100644 --- a/application/dialogs/NewInstanceDialog.h +++ b/launcher/dialogs/NewInstanceDialog.h @@ -29,7 +29,7 @@ class NewInstanceDialog; class PageContainer; class QDialogButtonBox; class ImportPage; -class TwitchPage; +class FlamePage; class NewInstanceDialog : public QDialog, public BasePageProvider { @@ -43,6 +43,7 @@ class NewInstanceDialog : public QDialog, public BasePageProvider void setSuggestedPack(const QString & name = QString(), InstanceTask * task = nullptr); void setSuggestedIconFromFile(const QString &path, const QString &name); + void setSuggestedIcon(const QString &key); InstanceTask * extractTask(); @@ -68,7 +69,7 @@ private slots: QString InstIconKey; ImportPage *importPage = nullptr; - TwitchPage *twitchPage = nullptr; + FlamePage *flamePage = nullptr; std::unique_ptr creationTask; bool importIcon = false; diff --git a/application/dialogs/NewInstanceDialog.ui b/launcher/dialogs/NewInstanceDialog.ui similarity index 100% rename from application/dialogs/NewInstanceDialog.ui rename to launcher/dialogs/NewInstanceDialog.ui diff --git a/application/dialogs/NotificationDialog.cpp b/launcher/dialogs/NotificationDialog.cpp similarity index 100% rename from application/dialogs/NotificationDialog.cpp rename to launcher/dialogs/NotificationDialog.cpp diff --git a/application/dialogs/NotificationDialog.h b/launcher/dialogs/NotificationDialog.h similarity index 100% rename from application/dialogs/NotificationDialog.h rename to launcher/dialogs/NotificationDialog.h diff --git a/application/dialogs/NotificationDialog.ui b/launcher/dialogs/NotificationDialog.ui similarity index 100% rename from application/dialogs/NotificationDialog.ui rename to launcher/dialogs/NotificationDialog.ui diff --git a/application/dialogs/ProfileSelectDialog.cpp b/launcher/dialogs/ProfileSelectDialog.cpp similarity index 76% rename from application/dialogs/ProfileSelectDialog.cpp rename to launcher/dialogs/ProfileSelectDialog.cpp index ae34709f2a..e2ad73e4ff 100644 --- a/application/dialogs/ProfileSelectDialog.cpp +++ b/launcher/dialogs/ProfileSelectDialog.cpp @@ -33,9 +33,10 @@ ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWid m_accounts = MMC->accounts(); auto view = ui->listView; //view->setModel(m_accounts.get()); - //view->hideColumn(MojangAccountList::ActiveColumn); + //view->hideColumn(AccountList::ActiveColumn); view->setColumnCount(1); view->setRootIsDecorated(false); + // FIXME: use a real model, not this if(QTreeWidgetItem* header = view->headerItem()) { header->setText(0, tr("Name")); @@ -47,20 +48,19 @@ ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWid QList items; for (int i = 0; i < m_accounts->count(); i++) { - MojangAccountPtr account = m_accounts->at(i); - for (auto profile : account->profiles()) - { - auto profileLabel = profile.name; - if(account->isInUse()) - { - profileLabel += tr(" (in use)"); - } - auto item = new QTreeWidgetItem(view); - item->setText(0, profileLabel); - item->setIcon(0, SkinUtils::getFaceFromCache(profile.id)); - item->setData(0, MojangAccountList::PointerRole, QVariant::fromValue(account)); - items.append(item); + MinecraftAccountPtr account = m_accounts->at(i); + QString profileLabel; + if(account->isInUse()) { + profileLabel = tr("%1 (in use)").arg(account->profileName()); } + else { + profileLabel = account->profileName(); + } + auto item = new QTreeWidgetItem(view); + item->setText(0, profileLabel); + item->setIcon(0, account->getFace()); + item->setData(0, AccountList::PointerRole, QVariant::fromValue(account)); + items.append(item); } view->addTopLevelItems(items); @@ -84,7 +84,7 @@ ProfileSelectDialog::~ProfileSelectDialog() delete ui; } -MojangAccountPtr ProfileSelectDialog::selectedAccount() const +MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const { return m_selected; } @@ -105,7 +105,7 @@ void ProfileSelectDialog::on_buttonBox_accepted() if (selection.size() > 0) { QModelIndex selected = selection.first(); - m_selected = selected.data(MojangAccountList::PointerRole).value(); + m_selected = selected.data(AccountList::PointerRole).value(); } close(); } diff --git a/application/dialogs/ProfileSelectDialog.h b/launcher/dialogs/ProfileSelectDialog.h similarity index 93% rename from application/dialogs/ProfileSelectDialog.h rename to launcher/dialogs/ProfileSelectDialog.h index 9f95830c15..a4acd9a1b4 100644 --- a/application/dialogs/ProfileSelectDialog.h +++ b/launcher/dialogs/ProfileSelectDialog.h @@ -19,7 +19,7 @@ #include -#include "minecraft/auth/MojangAccountList.h" +#include "minecraft/auth/AccountList.h" namespace Ui { @@ -59,7 +59,7 @@ class ProfileSelectDialog : public QDialog * Gets a pointer to the account that the user selected. * This is null if the user clicked cancel or hasn't clicked OK yet. */ - MojangAccountPtr selectedAccount() const; + MinecraftAccountPtr selectedAccount() const; /*! * Returns true if the user checked the "use as global default" checkbox. @@ -80,10 +80,10 @@ public void on_buttonBox_rejected(); protected: - std::shared_ptr m_accounts; + std::shared_ptr m_accounts; //! The account that was selected when the user clicked OK. - MojangAccountPtr m_selected; + MinecraftAccountPtr m_selected; private: Ui::ProfileSelectDialog *ui; diff --git a/application/dialogs/ProfileSelectDialog.ui b/launcher/dialogs/ProfileSelectDialog.ui similarity index 100% rename from application/dialogs/ProfileSelectDialog.ui rename to launcher/dialogs/ProfileSelectDialog.ui diff --git a/application/dialogs/ProgressDialog.cpp b/launcher/dialogs/ProgressDialog.cpp similarity index 100% rename from application/dialogs/ProgressDialog.cpp rename to launcher/dialogs/ProgressDialog.cpp diff --git a/application/dialogs/ProgressDialog.h b/launcher/dialogs/ProgressDialog.h similarity index 100% rename from application/dialogs/ProgressDialog.h rename to launcher/dialogs/ProgressDialog.h diff --git a/application/dialogs/ProgressDialog.ui b/launcher/dialogs/ProgressDialog.ui similarity index 100% rename from application/dialogs/ProgressDialog.ui rename to launcher/dialogs/ProgressDialog.ui diff --git a/application/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp similarity index 64% rename from application/dialogs/SkinUploadDialog.cpp rename to launcher/dialogs/SkinUploadDialog.cpp index 56133529bd..97478f4b18 100644 --- a/application/dialogs/SkinUploadDialog.cpp +++ b/launcher/dialogs/SkinUploadDialog.cpp @@ -1,11 +1,16 @@ #include #include +#include + #include #include +#include + #include "SkinUploadDialog.h" #include "ui_SkinUploadDialog.h" #include "ProgressDialog.h" #include "CustomMessageBox.h" +#include void SkinUploadDialog::on_buttonBox_rejected() { @@ -15,7 +20,7 @@ void SkinUploadDialog::on_buttonBox_rejected() void SkinUploadDialog::on_buttonBox_accepted() { AuthSessionPtr session = std::make_shared(); - auto login = m_acct->login(session); + auto login = m_acct->refresh(session); ProgressDialog prog(this); if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) { @@ -85,8 +90,13 @@ void SkinUploadDialog::on_buttonBox_accepted() { model = SkinUpload::ALEX; } - SkinUploadPtr upload = std::make_shared(this, session, FS::read(fileName), model); - if (prog.execWithTask((Task*)upload.get()) != QDialog::Accepted) + SequentialTask skinUpload; + skinUpload.addTask(std::make_shared(this, session, FS::read(fileName), model)); + auto selectedCape = ui->capeCombo->currentData().toString(); + if(selectedCape != session->m_accountPtr->accountData()->minecraftProfile.currentCape) { + skinUpload.addTask(std::make_shared(this, session, selectedCape)); + } + if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); close(); @@ -107,8 +117,38 @@ void SkinUploadDialog::on_skinBrowseBtn_clicked() ui->skinPathTextBox->setText(cooked_path); } -SkinUploadDialog::SkinUploadDialog(MojangAccountPtr acct, QWidget *parent) +SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent) :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) { ui->setupUi(this); + + // FIXME: add a model for this, download/refresh the capes on demand + auto &data = *acct->accountData(); + int index = 0; + ui->capeCombo->addItem(tr("No Cape"), QVariant()); + auto currentCape = data.minecraftProfile.currentCape; + if(currentCape.isEmpty()) { + ui->capeCombo->setCurrentIndex(index); + } + + for(auto & cape: data.minecraftProfile.capes) { + index++; + if(cape.data.size()) { + QPixmap capeImage; + if(capeImage.loadFromData(cape.data, "PNG")) { + QPixmap preview = QPixmap(10, 16); + QPainter painter(&preview); + painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); + ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + continue; + } + } + ui->capeCombo->addItem(cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + } } diff --git a/application/dialogs/SkinUploadDialog.h b/launcher/dialogs/SkinUploadDialog.h similarity index 69% rename from application/dialogs/SkinUploadDialog.h rename to launcher/dialogs/SkinUploadDialog.h index deb44eac0d..84d17dc6fb 100644 --- a/application/dialogs/SkinUploadDialog.h +++ b/launcher/dialogs/SkinUploadDialog.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include namespace Ui { @@ -11,7 +11,7 @@ namespace Ui class SkinUploadDialog : public QDialog { Q_OBJECT public: - explicit SkinUploadDialog(MojangAccountPtr acct, QWidget *parent = 0); + explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent = 0); virtual ~SkinUploadDialog() {}; public slots: @@ -22,7 +22,7 @@ public slots: void on_skinBrowseBtn_clicked(); protected: - MojangAccountPtr m_acct; + MinecraftAccountPtr m_acct; private: Ui::SkinUploadDialog *ui; diff --git a/launcher/dialogs/SkinUploadDialog.ui b/launcher/dialogs/SkinUploadDialog.ui new file mode 100644 index 0000000000..f4b0ed0aa7 --- /dev/null +++ b/launcher/dialogs/SkinUploadDialog.ui @@ -0,0 +1,97 @@ + + + SkinUploadDialog + + + + 0 + 0 + 394 + 360 + + + + Skin Upload + + + + + + Skin File + + + + + + + + + + 0 + 0 + + + + + 28 + 16777215 + + + + ... + + + + + + + + + + Player Model + + + + + + Steve Model + + + true + + + + + + + Alex Model + + + + + + + + + + Cape + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/application/dialogs/UpdateDialog.cpp b/launcher/dialogs/UpdateDialog.cpp similarity index 100% rename from application/dialogs/UpdateDialog.cpp rename to launcher/dialogs/UpdateDialog.cpp diff --git a/application/dialogs/UpdateDialog.h b/launcher/dialogs/UpdateDialog.h similarity index 100% rename from application/dialogs/UpdateDialog.h rename to launcher/dialogs/UpdateDialog.h diff --git a/application/dialogs/UpdateDialog.ui b/launcher/dialogs/UpdateDialog.ui similarity index 100% rename from application/dialogs/UpdateDialog.ui rename to launcher/dialogs/UpdateDialog.ui diff --git a/application/dialogs/VersionSelectDialog.cpp b/launcher/dialogs/VersionSelectDialog.cpp similarity index 100% rename from application/dialogs/VersionSelectDialog.cpp rename to launcher/dialogs/VersionSelectDialog.cpp diff --git a/application/dialogs/VersionSelectDialog.h b/launcher/dialogs/VersionSelectDialog.h similarity index 100% rename from application/dialogs/VersionSelectDialog.h rename to launcher/dialogs/VersionSelectDialog.h diff --git a/application/groupview/AccessibleGroupView.cpp b/launcher/groupview/AccessibleGroupView.cpp similarity index 100% rename from application/groupview/AccessibleGroupView.cpp rename to launcher/groupview/AccessibleGroupView.cpp diff --git a/application/groupview/AccessibleGroupView.h b/launcher/groupview/AccessibleGroupView.h similarity index 100% rename from application/groupview/AccessibleGroupView.h rename to launcher/groupview/AccessibleGroupView.h diff --git a/application/groupview/AccessibleGroupView_p.h b/launcher/groupview/AccessibleGroupView_p.h similarity index 100% rename from application/groupview/AccessibleGroupView_p.h rename to launcher/groupview/AccessibleGroupView_p.h diff --git a/application/groupview/GroupView.cpp b/launcher/groupview/GroupView.cpp similarity index 100% rename from application/groupview/GroupView.cpp rename to launcher/groupview/GroupView.cpp diff --git a/application/groupview/GroupView.h b/launcher/groupview/GroupView.h similarity index 100% rename from application/groupview/GroupView.h rename to launcher/groupview/GroupView.h diff --git a/application/groupview/GroupedProxyModel.cpp b/launcher/groupview/GroupedProxyModel.cpp similarity index 100% rename from application/groupview/GroupedProxyModel.cpp rename to launcher/groupview/GroupedProxyModel.cpp diff --git a/application/groupview/GroupedProxyModel.h b/launcher/groupview/GroupedProxyModel.h similarity index 100% rename from application/groupview/GroupedProxyModel.h rename to launcher/groupview/GroupedProxyModel.h diff --git a/application/groupview/InstanceDelegate.cpp b/launcher/groupview/InstanceDelegate.cpp similarity index 100% rename from application/groupview/InstanceDelegate.cpp rename to launcher/groupview/InstanceDelegate.cpp diff --git a/application/groupview/InstanceDelegate.h b/launcher/groupview/InstanceDelegate.h similarity index 100% rename from application/groupview/InstanceDelegate.h rename to launcher/groupview/InstanceDelegate.h diff --git a/application/groupview/VisualGroup.cpp b/launcher/groupview/VisualGroup.cpp similarity index 100% rename from application/groupview/VisualGroup.cpp rename to launcher/groupview/VisualGroup.cpp diff --git a/application/groupview/VisualGroup.h b/launcher/groupview/VisualGroup.h similarity index 100% rename from application/groupview/VisualGroup.h rename to launcher/groupview/VisualGroup.h diff --git a/api/logic/icons/IIconList.cpp b/launcher/icons/IIconList.cpp similarity index 100% rename from api/logic/icons/IIconList.cpp rename to launcher/icons/IIconList.cpp diff --git a/api/logic/icons/IIconList.h b/launcher/icons/IIconList.h similarity index 90% rename from api/logic/icons/IIconList.h rename to launcher/icons/IIconList.h index 9a3fe022d9..15d7dd1526 100644 --- a/api/logic/icons/IIconList.h +++ b/launcher/icons/IIconList.h @@ -2,7 +2,6 @@ #include #include -#include "multimc_logic_export.h" enum IconType : unsigned { @@ -13,7 +12,7 @@ enum IconType : unsigned ToBeDeleted }; -class MULTIMC_LOGIC_EXPORT IIconList +class IIconList { public: virtual ~IIconList(); diff --git a/api/gui/icons/IconList.cpp b/launcher/icons/IconList.cpp similarity index 100% rename from api/gui/icons/IconList.cpp rename to launcher/icons/IconList.cpp diff --git a/api/gui/icons/IconList.h b/launcher/icons/IconList.h similarity index 96% rename from api/gui/icons/IconList.h rename to launcher/icons/IconList.h index f07415fa6f..70266ebb1d 100644 --- a/api/gui/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -26,11 +26,9 @@ #include "Env.h" // there is a global icon list inside Env. #include -#include "multimc_gui_export.h" - class QFileSystemWatcher; -class MULTIMC_GUI_EXPORT IconList : public QAbstractListModel, public IIconList +class IconList : public QAbstractListModel, public IIconList { Q_OBJECT public: diff --git a/api/logic/icons/IconUtils.cpp b/launcher/icons/IconUtils.cpp similarity index 100% rename from api/logic/icons/IconUtils.cpp rename to launcher/icons/IconUtils.cpp diff --git a/api/logic/icons/IconUtils.h b/launcher/icons/IconUtils.h similarity index 56% rename from api/logic/icons/IconUtils.h rename to launcher/icons/IconUtils.h index ce236946c7..be93d91431 100644 --- a/api/logic/icons/IconUtils.h +++ b/launcher/icons/IconUtils.h @@ -1,14 +1,13 @@ #pragma once #include -#include "multimc_logic_export.h" namespace IconUtils { // Given a folder and an icon key, find 'best' of the icons with the given key in there and return its path -MULTIMC_LOGIC_EXPORT QString findBestIconIn(const QString &folder, const QString & iconKey); +QString findBestIconIn(const QString &folder, const QString & iconKey); // Get icon file type filter for file browser dialogs -MULTIMC_LOGIC_EXPORT QString getIconFilter(); +QString getIconFilter(); } diff --git a/api/gui/icons/MMCIcon.cpp b/launcher/icons/MMCIcon.cpp similarity index 100% rename from api/gui/icons/MMCIcon.cpp rename to launcher/icons/MMCIcon.cpp diff --git a/api/gui/icons/MMCIcon.h b/launcher/icons/MMCIcon.h similarity index 92% rename from api/gui/icons/MMCIcon.h rename to launcher/icons/MMCIcon.h index fd14b5b765..1f05f28e5b 100644 --- a/api/gui/icons/MMCIcon.h +++ b/launcher/icons/MMCIcon.h @@ -19,9 +19,7 @@ #include #include -#include "multimc_gui_export.h" - -struct MULTIMC_GUI_EXPORT MMCImage +struct MMCImage { QIcon icon; QString key; @@ -32,7 +30,7 @@ struct MULTIMC_GUI_EXPORT MMCImage } }; -struct MULTIMC_GUI_EXPORT MMCIcon +struct MMCIcon { QString m_key; QString m_name; diff --git a/application/install_prereqs.cmake.in b/launcher/install_prereqs.cmake.in similarity index 100% rename from application/install_prereqs.cmake.in rename to launcher/install_prereqs.cmake.in diff --git a/api/logic/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp similarity index 100% rename from api/logic/java/JavaChecker.cpp rename to launcher/java/JavaChecker.cpp diff --git a/api/logic/java/JavaChecker.h b/launcher/java/JavaChecker.h similarity index 89% rename from api/logic/java/JavaChecker.h rename to launcher/java/JavaChecker.h index 0a96249a7a..122861cf72 100644 --- a/api/logic/java/JavaChecker.h +++ b/launcher/java/JavaChecker.h @@ -5,13 +5,11 @@ #include "QObjectPtr.h" -#include "multimc_logic_export.h" - #include "JavaVersion.h" class JavaChecker; -struct MULTIMC_LOGIC_EXPORT JavaCheckResult +struct JavaCheckResult { QString path; QString mojangPlatform; @@ -32,7 +30,7 @@ struct MULTIMC_LOGIC_EXPORT JavaCheckResult typedef shared_qobject_ptr QProcessPtr; typedef shared_qobject_ptr JavaCheckerPtr; -class MULTIMC_LOGIC_EXPORT JavaChecker : public QObject +class JavaChecker : public QObject { Q_OBJECT public: diff --git a/api/logic/java/JavaCheckerJob.cpp b/launcher/java/JavaCheckerJob.cpp similarity index 100% rename from api/logic/java/JavaCheckerJob.cpp rename to launcher/java/JavaCheckerJob.cpp diff --git a/api/logic/java/JavaCheckerJob.h b/launcher/java/JavaCheckerJob.h similarity index 100% rename from api/logic/java/JavaCheckerJob.h rename to launcher/java/JavaCheckerJob.h diff --git a/api/logic/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp similarity index 100% rename from api/logic/java/JavaInstall.cpp rename to launcher/java/JavaInstall.cpp diff --git a/api/logic/java/JavaInstall.h b/launcher/java/JavaInstall.h similarity index 100% rename from api/logic/java/JavaInstall.h rename to launcher/java/JavaInstall.h diff --git a/api/logic/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp similarity index 100% rename from api/logic/java/JavaInstallList.cpp rename to launcher/java/JavaInstallList.cpp diff --git a/api/logic/java/JavaInstallList.h b/launcher/java/JavaInstallList.h similarity index 95% rename from api/logic/java/JavaInstallList.h rename to launcher/java/JavaInstallList.h index 1785a7b6ee..e5c72b174a 100644 --- a/api/logic/java/JavaInstallList.h +++ b/launcher/java/JavaInstallList.h @@ -26,11 +26,9 @@ #include "QObjectPtr.h" -#include "multimc_logic_export.h" - class JavaListLoadTask; -class MULTIMC_LOGIC_EXPORT JavaInstallList : public BaseVersionList +class JavaInstallList : public BaseVersionList { Q_OBJECT enum class Status diff --git a/api/logic/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp similarity index 75% rename from api/logic/java/JavaUtils.cpp rename to launcher/java/JavaUtils.cpp index 647711e518..c00ee7108d 100644 --- a/api/logic/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -150,7 +150,7 @@ JavaInstallPtr JavaUtils::GetDefaultJava() } #if defined(Q_OS_WIN32) -QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName) +QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix) { QList javas; @@ -175,8 +175,6 @@ QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz); } - QString recommended = value; - TCHAR subKeyName[255]; DWORD subKeyNameSize, numSubKeys, retCode; @@ -195,7 +193,7 @@ QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString if (retCode == ERROR_SUCCESS) { // Now open the registry key for the version that we just got. - QString newKeyName = keyName + "\\" + subKeyName; + QString newKeyName = keyName + "\\" + subKeyName + subkeySuffix; HKEY newKey; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, newKeyName.toStdString().c_str(), 0, @@ -204,11 +202,11 @@ QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString // Read the JavaHome value to find where Java is installed. value = new char[0]; valueSz = 0; - if (RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value, + if (RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, &valueSz) == ERROR_MORE_DATA) { value = new char[valueSz]; - RegQueryValueEx(newKey, "JavaHome", NULL, NULL, (BYTE *)value, + RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, &valueSz); // Now, we construct the version object and add it to the list. @@ -237,25 +235,86 @@ QList JavaUtils::FindJavaPaths() { QList java_candidates; + // Oracle QList JRE64s = this->FindJavaFromRegistryKey( - KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList JDK64s = this->FindJavaFromRegistryKey( - KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); QList JRE32s = this->FindJavaFromRegistryKey( - KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList JDK32s = this->FindJavaFromRegistryKey( - KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); - + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); + + // Oracle for Java 9 and newer + QList NEWJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); + QList NEWJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); + QList NEWJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); + QList NEWJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); + + // AdoptOpenJDK + QList ADOPTOPENJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); + QList ADOPTOPENJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); + QList ADOPTOPENJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); + QList ADOPTOPENJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); + + // Adoptium (Eclipse) + QList ECLIPSEJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); + QList ECLIPSEJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); + + // Microsoft + QList MICROSOFTJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); + + // Azul Zulu + QList ZULU64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); + QList ZULU32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); + + // BellSoft Liberica + QList LIBERICA64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); + QList LIBERICA32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); + + // List x64 before x86 java_candidates.append(JRE64s); + java_candidates.append(NEWJRE64s); + java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK64s); + java_candidates.append(NEWJDK64s); + java_candidates.append(ADOPTOPENJDK64s); + java_candidates.append(ECLIPSEJDK64s); + java_candidates.append(MICROSOFTJDK64s); + java_candidates.append(ZULU64s); + java_candidates.append(LIBERICA64s); + java_candidates.append(JRE32s); + java_candidates.append(NEWJRE32s); + java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK32s); + java_candidates.append(NEWJDK32s); + java_candidates.append(ADOPTOPENJDK32s); + java_candidates.append(ECLIPSEJDK32s); + java_candidates.append(ZULU32s); + java_candidates.append(LIBERICA32s); + java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); QList candidates; @@ -330,6 +389,9 @@ QList JavaUtils::FindJavaPaths() scanJavaDir("/usr/lib32/jvm"); // javas stored in MultiMC's folder scanJavaDir("java"); + // manually installed JDKs in /opt + scanJavaDir("/opt/jdk"); + scanJavaDir("/opt/jdks"); return javas; } #else diff --git a/api/logic/java/JavaUtils.h b/launcher/java/JavaUtils.h similarity index 90% rename from api/logic/java/JavaUtils.h rename to launcher/java/JavaUtils.h index 7474c49456..3152d143cd 100644 --- a/api/logic/java/JavaUtils.h +++ b/launcher/java/JavaUtils.h @@ -24,11 +24,9 @@ #include #endif -#include "multimc_logic_export.h" - QProcessEnvironment CleanEnviroment(); -class MULTIMC_LOGIC_EXPORT JavaUtils : public QObject +class JavaUtils : public QObject { Q_OBJECT public: @@ -39,6 +37,6 @@ class MULTIMC_LOGIC_EXPORT JavaUtils : public QObject JavaInstallPtr GetDefaultJava(); #ifdef Q_OS_WIN - QList FindJavaFromRegistryKey(DWORD keyType, QString keyName); + QList FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix = ""); #endif }; diff --git a/api/logic/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp similarity index 100% rename from api/logic/java/JavaVersion.cpp rename to launcher/java/JavaVersion.cpp diff --git a/api/logic/java/JavaVersion.h b/launcher/java/JavaVersion.h similarity index 92% rename from api/logic/java/JavaVersion.h rename to launcher/java/JavaVersion.h index 8589c21a49..9bbf064257 100644 --- a/api/logic/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -1,6 +1,5 @@ #pragma once -#include "multimc_logic_export.h" #include // NOTE: apparently the GNU C library pollutes the global namespace with these... undef them. @@ -11,7 +10,7 @@ #undef minor #endif -class MULTIMC_LOGIC_EXPORT JavaVersion +class JavaVersion { friend class JavaVersionTest; public: diff --git a/api/logic/java/JavaVersion_test.cpp b/launcher/java/JavaVersion_test.cpp similarity index 100% rename from api/logic/java/JavaVersion_test.cpp rename to launcher/java/JavaVersion_test.cpp diff --git a/api/logic/java/launch/CheckJava.cpp b/launcher/java/launch/CheckJava.cpp similarity index 100% rename from api/logic/java/launch/CheckJava.cpp rename to launcher/java/launch/CheckJava.cpp diff --git a/api/logic/java/launch/CheckJava.h b/launcher/java/launch/CheckJava.h similarity index 100% rename from api/logic/java/launch/CheckJava.h rename to launcher/java/launch/CheckJava.h diff --git a/api/logic/launch/LaunchStep.cpp b/launcher/launch/LaunchStep.cpp similarity index 100% rename from api/logic/launch/LaunchStep.cpp rename to launcher/launch/LaunchStep.cpp diff --git a/api/logic/launch/LaunchStep.h b/launcher/launch/LaunchStep.h similarity index 100% rename from api/logic/launch/LaunchStep.h rename to launcher/launch/LaunchStep.h diff --git a/api/logic/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp similarity index 100% rename from api/logic/launch/LaunchTask.cpp rename to launcher/launch/LaunchTask.cpp diff --git a/api/logic/launch/LaunchTask.h b/launcher/launch/LaunchTask.h similarity index 97% rename from api/logic/launch/LaunchTask.h rename to launcher/launch/LaunchTask.h index 2be59c836e..ae81462ffa 100644 --- a/api/logic/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -24,9 +24,7 @@ #include "LoggedProcess.h" #include "LaunchStep.h" -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT LaunchTask: public Task +class LaunchTask: public Task { Q_OBJECT protected: diff --git a/api/logic/launch/LogModel.cpp b/launcher/launch/LogModel.cpp similarity index 100% rename from api/logic/launch/LogModel.cpp rename to launcher/launch/LogModel.cpp diff --git a/api/logic/launch/LogModel.h b/launcher/launch/LogModel.h similarity index 92% rename from api/logic/launch/LogModel.h rename to launcher/launch/LogModel.h index bccceaeffb..6aabc82362 100644 --- a/api/logic/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -4,9 +4,7 @@ #include #include "MessageLevel.h" -#include - -class MULTIMC_LOGIC_EXPORT LogModel : public QAbstractListModel +class LogModel : public QAbstractListModel { Q_OBJECT public: diff --git a/launcher/launch/steps/LookupServerAddress.cpp b/launcher/launch/steps/LookupServerAddress.cpp new file mode 100644 index 0000000000..de56c28a81 --- /dev/null +++ b/launcher/launch/steps/LookupServerAddress.cpp @@ -0,0 +1,95 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include "LookupServerAddress.h" + +#include + +LookupServerAddress::LookupServerAddress(LaunchTask *parent) : + LaunchStep(parent), m_dnsLookup(new QDnsLookup(this)) +{ + connect(m_dnsLookup, &QDnsLookup::finished, this, &LookupServerAddress::on_dnsLookupFinished); + + m_dnsLookup->setType(QDnsLookup::SRV); +} + +void LookupServerAddress::setLookupAddress(const QString &lookupAddress) +{ + m_lookupAddress = lookupAddress; + m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress)); +} + +void LookupServerAddress::setOutputAddressPtr(MinecraftServerTargetPtr output) +{ + m_output = std::move(output); +} + +bool LookupServerAddress::abort() +{ + m_dnsLookup->abort(); + emitFailed("Aborted"); + return true; +} + +void LookupServerAddress::executeTask() +{ + m_dnsLookup->lookup(); +} + +void LookupServerAddress::on_dnsLookupFinished() +{ + if (isFinished()) + { + // Aborted + return; + } + + if (m_dnsLookup->error() != QDnsLookup::NoError) + { + emit logLine(QString("Failed to resolve server address (this is NOT an error!) %1: %2\n") + .arg(m_dnsLookup->name(), m_dnsLookup->errorString()), MessageLevel::MultiMC); + resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch + // and leave it up to minecraft to fail (or maybe not) when connecting + return; + } + + const auto records = m_dnsLookup->serviceRecords(); + if (records.empty()) + { + emit logLine( + QString("Failed to resolve server address %1: the DNS lookup succeeded, but no records were returned.\n") + .arg(m_dnsLookup->name()), MessageLevel::Warning); + resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch + // and leave it up to minecraft to fail (or maybe not) when connecting + return; + } + + const auto &firstRecord = records.at(0); + quint16 port = firstRecord.port(); + + emit logLine(QString("Resolved server address %1 to %2 with port %3\n").arg( + m_dnsLookup->name(), firstRecord.target(), QString::number(port)),MessageLevel::MultiMC); + resolve(firstRecord.target(), port); +} + +void LookupServerAddress::resolve(const QString &address, quint16 port) +{ + m_output->address = address; + m_output->port = port; + + emitSucceeded(); + m_dnsLookup->deleteLater(); +} diff --git a/launcher/launch/steps/LookupServerAddress.h b/launcher/launch/steps/LookupServerAddress.h new file mode 100644 index 0000000000..5a5c3de17a --- /dev/null +++ b/launcher/launch/steps/LookupServerAddress.h @@ -0,0 +1,49 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "minecraft/launch/MinecraftServerTarget.h" + +class LookupServerAddress: public LaunchStep { +Q_OBJECT +public: + explicit LookupServerAddress(LaunchTask *parent); + virtual ~LookupServerAddress() {}; + + virtual void executeTask(); + virtual bool abort(); + virtual bool canAbort() const + { + return true; + } + + void setLookupAddress(const QString &lookupAddress); + void setOutputAddressPtr(MinecraftServerTargetPtr output); + +private slots: + void on_dnsLookupFinished(); + +private: + void resolve(const QString &address, quint16 port); + + QDnsLookup *m_dnsLookup; + QString m_lookupAddress; + MinecraftServerTargetPtr m_output; +}; diff --git a/api/logic/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp similarity index 100% rename from api/logic/launch/steps/PostLaunchCommand.cpp rename to launcher/launch/steps/PostLaunchCommand.cpp diff --git a/api/logic/launch/steps/PostLaunchCommand.h b/launcher/launch/steps/PostLaunchCommand.h similarity index 100% rename from api/logic/launch/steps/PostLaunchCommand.h rename to launcher/launch/steps/PostLaunchCommand.h diff --git a/api/logic/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp similarity index 100% rename from api/logic/launch/steps/PreLaunchCommand.cpp rename to launcher/launch/steps/PreLaunchCommand.cpp diff --git a/api/logic/launch/steps/PreLaunchCommand.h b/launcher/launch/steps/PreLaunchCommand.h similarity index 100% rename from api/logic/launch/steps/PreLaunchCommand.h rename to launcher/launch/steps/PreLaunchCommand.h diff --git a/api/logic/launch/steps/TextPrint.cpp b/launcher/launch/steps/TextPrint.cpp similarity index 100% rename from api/logic/launch/steps/TextPrint.cpp rename to launcher/launch/steps/TextPrint.cpp diff --git a/api/logic/launch/steps/TextPrint.h b/launcher/launch/steps/TextPrint.h similarity index 92% rename from api/logic/launch/steps/TextPrint.h rename to launcher/launch/steps/TextPrint.h index 2937c64ac1..36fa7f9a97 100644 --- a/api/logic/launch/steps/TextPrint.h +++ b/launcher/launch/steps/TextPrint.h @@ -19,13 +19,11 @@ #include #include -#include "multimc_logic_export.h" - /* * FIXME: maybe do not export */ -class MULTIMC_LOGIC_EXPORT TextPrint: public LaunchStep +class TextPrint: public LaunchStep { Q_OBJECT public: diff --git a/api/logic/launch/steps/Update.cpp b/launcher/launch/steps/Update.cpp similarity index 100% rename from api/logic/launch/steps/Update.cpp rename to launcher/launch/steps/Update.cpp diff --git a/api/logic/launch/steps/Update.h b/launcher/launch/steps/Update.h similarity index 100% rename from api/logic/launch/steps/Update.h rename to launcher/launch/steps/Update.h diff --git a/application/main.cpp b/launcher/main.cpp similarity index 100% rename from application/main.cpp rename to launcher/main.cpp diff --git a/api/logic/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp similarity index 100% rename from api/logic/meta/BaseEntity.cpp rename to launcher/meta/BaseEntity.cpp diff --git a/api/logic/meta/BaseEntity.h b/launcher/meta/BaseEntity.h similarity index 95% rename from api/logic/meta/BaseEntity.h rename to launcher/meta/BaseEntity.h index 04a374208f..eff4387994 100644 --- a/api/logic/meta/BaseEntity.h +++ b/launcher/meta/BaseEntity.h @@ -19,13 +19,12 @@ #include #include "QObjectPtr.h" -#include "multimc_logic_export.h" #include "net/Mode.h" class Task; namespace Meta { -class MULTIMC_LOGIC_EXPORT BaseEntity +class BaseEntity { public: /* types */ using Ptr = std::shared_ptr; diff --git a/api/logic/meta/Index.cpp b/launcher/meta/Index.cpp similarity index 100% rename from api/logic/meta/Index.cpp rename to launcher/meta/Index.cpp diff --git a/api/logic/meta/Index.h b/launcher/meta/Index.h similarity index 94% rename from api/logic/meta/Index.h rename to launcher/meta/Index.h index e9412e7078..d33ab0c898 100644 --- a/api/logic/meta/Index.h +++ b/launcher/meta/Index.h @@ -20,8 +20,6 @@ #include "BaseEntity.h" -#include "multimc_logic_export.h" - class Task; namespace Meta @@ -29,7 +27,7 @@ namespace Meta using VersionListPtr = std::shared_ptr; using VersionPtr = std::shared_ptr; -class MULTIMC_LOGIC_EXPORT Index : public QAbstractListModel, public BaseEntity +class Index : public QAbstractListModel, public BaseEntity { Q_OBJECT public: diff --git a/api/logic/meta/Index_test.cpp b/launcher/meta/Index_test.cpp similarity index 100% rename from api/logic/meta/Index_test.cpp rename to launcher/meta/Index_test.cpp diff --git a/api/logic/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp similarity index 100% rename from api/logic/meta/JsonFormat.cpp rename to launcher/meta/JsonFormat.cpp diff --git a/api/logic/meta/JsonFormat.h b/launcher/meta/JsonFormat.h similarity index 100% rename from api/logic/meta/JsonFormat.h rename to launcher/meta/JsonFormat.h diff --git a/api/logic/meta/Version.cpp b/launcher/meta/Version.cpp similarity index 100% rename from api/logic/meta/Version.cpp rename to launcher/meta/Version.cpp diff --git a/api/logic/meta/Version.h b/launcher/meta/Version.h similarity index 95% rename from api/logic/meta/Version.h rename to launcher/meta/Version.h index a38d7bcd7e..dea8dc8a3e 100644 --- a/api/logic/meta/Version.h +++ b/launcher/meta/Version.h @@ -26,15 +26,13 @@ #include "BaseEntity.h" -#include "multimc_logic_export.h" - #include "JsonFormat.h" namespace Meta { using VersionPtr = std::shared_ptr; -class MULTIMC_LOGIC_EXPORT Version : public QObject, public BaseVersion, public BaseEntity +class Version : public QObject, public BaseVersion, public BaseEntity { Q_OBJECT diff --git a/api/logic/meta/VersionList.cpp b/launcher/meta/VersionList.cpp similarity index 100% rename from api/logic/meta/VersionList.cpp rename to launcher/meta/VersionList.cpp diff --git a/api/logic/meta/VersionList.h b/launcher/meta/VersionList.h similarity index 96% rename from api/logic/meta/VersionList.h rename to launcher/meta/VersionList.h index bba32ca37d..58cdafe788 100644 --- a/api/logic/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -25,7 +25,7 @@ namespace Meta using VersionPtr = std::shared_ptr; using VersionListPtr = std::shared_ptr; -class MULTIMC_LOGIC_EXPORT VersionList : public BaseVersionList, public BaseEntity +class VersionList : public BaseVersionList, public BaseEntity { Q_OBJECT Q_PROPERTY(QString uid READ uid CONSTANT) diff --git a/api/logic/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp similarity index 100% rename from api/logic/minecraft/AssetsUtils.cpp rename to launcher/minecraft/AssetsUtils.cpp diff --git a/api/logic/minecraft/AssetsUtils.h b/launcher/minecraft/AssetsUtils.h similarity index 100% rename from api/logic/minecraft/AssetsUtils.h rename to launcher/minecraft/AssetsUtils.h diff --git a/api/logic/minecraft/Component.cpp b/launcher/minecraft/Component.cpp similarity index 100% rename from api/logic/minecraft/Component.cpp rename to launcher/minecraft/Component.cpp diff --git a/api/logic/minecraft/Component.h b/launcher/minecraft/Component.h similarity index 96% rename from api/logic/minecraft/Component.h rename to launcher/minecraft/Component.h index cb202f7f1a..ef7c994706 100644 --- a/api/logic/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -7,7 +7,6 @@ #include "meta/JsonFormat.h" #include "ProblemProvider.h" #include "QObjectPtr.h" -#include "multimc_logic_export.h" class PackProfile; class LaunchProfile; @@ -18,7 +17,7 @@ namespace Meta } class VersionFile; -class MULTIMC_LOGIC_EXPORT Component : public QObject, public ProblemProvider +class Component : public QObject, public ProblemProvider { Q_OBJECT public: diff --git a/api/logic/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp similarity index 100% rename from api/logic/minecraft/ComponentUpdateTask.cpp rename to launcher/minecraft/ComponentUpdateTask.cpp diff --git a/api/logic/minecraft/ComponentUpdateTask.h b/launcher/minecraft/ComponentUpdateTask.h similarity index 100% rename from api/logic/minecraft/ComponentUpdateTask.h rename to launcher/minecraft/ComponentUpdateTask.h diff --git a/api/logic/minecraft/ComponentUpdateTask_p.h b/launcher/minecraft/ComponentUpdateTask_p.h similarity index 100% rename from api/logic/minecraft/ComponentUpdateTask_p.h rename to launcher/minecraft/ComponentUpdateTask_p.h diff --git a/api/logic/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h similarity index 81% rename from api/logic/minecraft/GradleSpecifier.h rename to launcher/minecraft/GradleSpecifier.h index 959325c6d4..60e0a726d8 100644 --- a/api/logic/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -18,32 +18,35 @@ struct GradleSpecifier { /* 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" + 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" + 1 "org.gradle.test.classifiers" + 2 "service" + 3 "1.0" + 4 "jdk15" + 5 "jar" */ - QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(:([^:@]+))?" "(@([^:@]+))?"); + QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?"); m_valid = matcher.exactMatch(value); + if(!m_valid) { + m_invalidValue = value; + return *this; + } auto elements = matcher.capturedTexts(); m_groupId = elements[1]; m_artifactId = elements[2]; m_version = elements[3]; - m_classifier = elements[5]; - if(!elements[7].isEmpty()) + m_classifier = elements[4]; + if(!elements[5].isEmpty()) { - m_extension = elements[7]; + m_extension = elements[5]; } return *this; } - operator QString() const + QString serialize() const { - if(!m_valid) - return "INVALID"; + if(!m_valid) { + return m_invalidValue; + } QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; if(!m_classifier.isEmpty()) { @@ -57,6 +60,9 @@ struct GradleSpecifier } QString getFileName() const { + if(!m_valid) { + return QString(); + } QString filename = m_artifactId + '-' + m_version; if(!m_classifier.isEmpty()) { @@ -67,8 +73,9 @@ struct GradleSpecifier } QString toPath(const QString & filenameOverride = QString()) const { - if(!m_valid) - return "INVALID"; + if(!m_valid) { + return QString(); + } QString filename; if(filenameOverride.isEmpty()) { @@ -134,6 +141,7 @@ struct GradleSpecifier return true; } private: + QString m_invalidValue; QString m_groupId; QString m_artifactId; QString m_version; diff --git a/api/logic/minecraft/GradleSpecifier_test.cpp b/launcher/minecraft/GradleSpecifier_test.cpp similarity index 92% rename from api/logic/minecraft/GradleSpecifier_test.cpp rename to launcher/minecraft/GradleSpecifier_test.cpp index f49ec71879..0900c9d8e8 100644 --- a/api/logic/minecraft/GradleSpecifier_test.cpp +++ b/launcher/minecraft/GradleSpecifier_test.cpp @@ -31,7 +31,7 @@ private { QFETCH(QString, through); - QString converted = GradleSpecifier(through); + QString converted = GradleSpecifier(through).serialize(); QCOMPARE(converted, through); } @@ -68,7 +68,8 @@ private GradleSpecifier spec(input); QVERIFY(!spec.valid()); - QCOMPARE(spec.operator QString(), QString("INVALID")); + QCOMPARE(spec.serialize(), input); + QCOMPARE(spec.toPath(), QString()); } }; diff --git a/api/logic/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp similarity index 100% rename from api/logic/minecraft/LaunchProfile.cpp rename to launcher/minecraft/LaunchProfile.cpp diff --git a/api/logic/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h similarity index 100% rename from api/logic/minecraft/LaunchProfile.h rename to launcher/minecraft/LaunchProfile.h diff --git a/api/logic/minecraft/Library.cpp b/launcher/minecraft/Library.cpp similarity index 95% rename from api/logic/minecraft/Library.cpp rename to launcher/minecraft/Library.cpp index b3c7657c32..f22936794e 100644 --- a/api/logic/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -94,13 +94,13 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads( auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); auto dl = Net::Download::makeCached(url, entry, options); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); - qDebug() << "Checksummed Download for:" << rawName() << "storage:" << storage << "url:" << url; + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; out.append(dl); } else { out.append(Net::Download::makeCached(url, entry, options)); - qDebug() << "Download for:" << rawName() << "storage:" << storage << "url:" << url; + qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; } return true; }; @@ -145,7 +145,7 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads( } else { - qDebug() << "Ignoring native library" << m_name << "because it has no classifier for current OS"; + qDebug() << "Ignoring native library" << m_name.serialize() << "because it has no classifier for current OS"; } } else @@ -157,7 +157,7 @@ QList< std::shared_ptr< NetAction > > Library::getDownloads( } else { - qDebug() << "Ignoring java library" << m_name << "because it has no artifact"; + qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact"; } } } diff --git a/api/logic/minecraft/Library.h b/launcher/minecraft/Library.h similarity index 98% rename from api/logic/minecraft/Library.h rename to launcher/minecraft/Library.h index acdd6c9c69..119b4a86ad 100644 --- a/api/logic/minecraft/Library.h +++ b/launcher/minecraft/Library.h @@ -14,14 +14,12 @@ #include "GradleSpecifier.h" #include "MojangDownloadInfo.h" -#include "multimc_logic_export.h" - class Library; class MinecraftInstance; typedef std::shared_ptr LibraryPtr; -class MULTIMC_LOGIC_EXPORT Library +class Library { friend class OneSixVersionFormat; friend class MojangVersionFormat; diff --git a/api/logic/minecraft/Library_test.cpp b/launcher/minecraft/Library_test.cpp similarity index 98% rename from api/logic/minecraft/Library_test.cpp rename to launcher/minecraft/Library_test.cpp index c3d6150d67..75bb4db15b 100644 --- a/api/logic/minecraft/Library_test.cpp +++ b/launcher/minecraft/Library_test.cpp @@ -18,7 +18,8 @@ class LibraryTest : public QObject jsonFile.open(QIODevice::ReadOnly); auto data = jsonFile.readAll(); jsonFile.close(); - return MojangVersionFormat::libraryFromJson(QJsonDocument::fromJson(data).object(), file); + ProblemContainer problems; + return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), file); } // get absolute path to expected storage, assuming default cache prefix QStringList getStorage(QString relative) diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp similarity index 88% rename from api/logic/minecraft/MinecraftInstance.cpp rename to launcher/minecraft/MinecraftInstance.cpp index b034331636..b6ff4e2494 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -12,6 +12,7 @@ #include #include "launch/LaunchTask.h" +#include "launch/steps/LookupServerAddress.h" #include "launch/steps/PostLaunchCommand.h" #include "launch/steps/Update.h" #include "launch/steps/PreLaunchCommand.h" @@ -22,12 +23,15 @@ #include "minecraft/launch/ClaimAccount.h" #include "minecraft/launch/ReconstructAssets.h" #include "minecraft/launch/ScanModFolders.h" +#include "minecraft/launch/VerifyJavaInstall.h" #include "java/launch/CheckJava.h" #include "java/JavaUtils.h" #include "meta/Index.h" #include "meta/VersionList.h" #include "mod/ModFolderModel.h" +#include "mod/ResourcePackFolderModel.h" +#include "mod/TexturePackFolderModel.h" #include "WorldList.h" #include "icons/IIconList.h" @@ -106,6 +110,15 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); + // Game time + auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); + m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); + m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride); + + // Join server on launch, this does not have a global override + m_settings->registerSetting("JoinServerOnLaunch", false); + m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // DEPRECATED: Read what versions the user configuration thinks should be used m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); m_settings->registerSetting("LWJGLVersion", ""); @@ -390,7 +403,8 @@ static QString replaceTokensIn(QString text, QMap with) return result; } -QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) const +QStringList MinecraftInstance::processMinecraftArgs( + AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const { auto profile = m_components->getProfile(); QString args_pattern = profile->getMinecraftArguments(); @@ -399,11 +413,17 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) cons args_pattern += " --tweakClass " + tweaker; } + if (serverToJoin && !serverToJoin->address.isEmpty()) + { + args_pattern += " --server " + serverToJoin->address; + args_pattern += " --port " + QString::number(serverToJoin->port); + } + QMap token_mapping; // yggdrasil! if(session) { - token_mapping["auth_username"] = session->username; + // token_mapping["auth_username"] = session->username; token_mapping["auth_session"] = session->session; token_mapping["auth_access_token"] = session->access_token; token_mapping["auth_player_name"] = session->player_name; @@ -435,7 +455,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session) cons return parts; } -QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) +QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QString launchScript; @@ -456,8 +476,17 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) launchScript += "appletClass " + appletClass + "\n"; } + if (serverToJoin && !serverToJoin->address.isEmpty()) + { + launchScript += "serverAddress " + serverToJoin->address + "\n"; + launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n"; + } + // generic minecraft params - for (auto param : processMinecraftArgs(session)) + for (auto param : processMinecraftArgs( + session, + nullptr /* When using a launch script, the server parameters are handled by it*/ + )) { launchScript += "param " + param + "\n"; } @@ -507,7 +536,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session) return launchScript; } -QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) +QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QStringList out; out << "Main Class:" << " " + getMainClass() << ""; @@ -526,11 +555,23 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } + auto settings = this->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + if (nativeOpenAL || nativeGLFW) + { + if (nativeOpenAL) + out << "Using system OpenAL."; + if (nativeGLFW) + out << "Using system GLFW."; + out << ""; + } + // libraries and class path. { out << "Libraries:"; QStringList jars, nativeJars; - auto javaArchitecture = settings()->get("JavaArchitecture").toString(); + auto javaArchitecture = settings->get("JavaArchitecture").toString(); profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); auto printLibFile = [&](const QString & path) { @@ -610,20 +651,20 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session) out << ""; } - auto params = processMinecraftArgs(nullptr); + auto params = processMinecraftArgs(nullptr, serverToJoin); out << "Params:"; out << " " + params.join(' '); out << ""; QString windowParams; - if (settings()->get("LaunchMaximized").toBool()) + if (settings->get("LaunchMaximized").toBool()) { out << "Window size: max (if available)"; } else { - auto width = settings()->get("MinecraftWinWidth").toInt(); - auto height = settings()->get("MinecraftWinHeight").toInt(); + auto width = settings->get("MinecraftWinWidth").toInt(); + auto height = settings->get("MinecraftWinHeight").toInt(); out << "Window size: " + QString::number(width) + " x " + QString::number(height); } out << ""; @@ -650,19 +691,11 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess addToFilter(sessionRef.session, tr("")); } addToFilter(sessionRef.access_token, tr("")); - addToFilter(sessionRef.client_token, tr("")); + if(sessionRef.client_token.size()) { + addToFilter(sessionRef.client_token, tr("")); + } addToFilter(sessionRef.uuid, tr("")); - auto i = sessionRef.u.properties.begin(); - while (i != sessionRef.u.properties.end()) - { - if(i.value().length() <= 3) { - ++i; - continue; - } - addToFilter(i.value(), "<" + i.key().toUpper() + ">"); - ++i; - } return filter; } @@ -757,9 +790,15 @@ QString MinecraftInstance::getStatusbarDescription() QString description; description.append(tr("Minecraft %1 (%2)").arg(m_components->getComponentVersion("net.minecraft")).arg(typeName())); - if(totalTimePlayed() > 0) + if(m_settings->get("ShowGameTime").toBool()) { - description.append(tr(", played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); + if (lastTimePlayed() > 0) { + description.append(tr(", last played for %1").arg(prettifyTimeDuration(lastTimePlayed()))); + } + + if (totalTimePlayed() > 0) { + description.append(tr(", total played for %1").arg(prettifyTimeDuration(totalTimePlayed()))); + } } if(hasCrashed()) { @@ -784,7 +823,7 @@ shared_qobject_ptr MinecraftInstance::createUpdateTask(Net::Mode mode) return nullptr; } -shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session) +shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { // FIXME: get rid of shared_from_this ... auto process = LaunchTask::create(std::dynamic_pointer_cast(shared_from_this())); @@ -816,6 +855,21 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(new CreateGameFolders(pptr)); } + if (!serverToJoin && m_settings->get("JoinServerOnLaunch").toBool()) + { + QString fullAddress = m_settings->get("JoinServerOnLaunchAddress").toString(); + serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress))); + } + + if(serverToJoin && serverToJoin->port == 25565) + { + // Resolve server address to join on launch + auto *step = new LookupServerAddress(pptr); + step->setLookupAddress(serverToJoin->address); + step->setOutputAddressPtr(serverToJoin); + process->appendStep(step); + } + // run pre-launch command if that's needed if(getPreLaunchCommand().size()) { @@ -832,7 +886,9 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt } else { - process->appendStep(new Update(pptr, Net::Mode::Offline)); + // Offline mode based by this code: + // https://github.com/MultiMC/MultiMC5/commit/6ede3c13b2bcda315e65dd78f2bfd729bc8b699b + process->appendStep(new Update(pptr, Net::Mode::Online)); } // if there are any jar mods @@ -847,7 +903,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // print some instance info here... { - process->appendStep(new PrintInstanceInfo(pptr, session)); + process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin)); } // extract native jars if needed @@ -860,6 +916,11 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(new ReconstructAssets(pptr)); } + // verify that minimum Java requirements are met + { + process->appendStep(new VerifyJavaInstall(pptr)); + } + { // actually launch the game auto method = launchMethod(); @@ -868,6 +929,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt auto step = new LauncherPartLaunch(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); + step->setServerToJoin(serverToJoin); process->appendStep(step); } else if (method == "DirectJava") @@ -875,6 +937,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt auto step = new DirectJavaLaunch(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); + step->setServerToJoin(serverToJoin); process->appendStep(step); } } @@ -931,7 +994,7 @@ std::shared_ptr MinecraftInstance::resourcePackList() const { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new ModFolderModel(resourcePacksDir())); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); m_resource_pack_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction); } @@ -942,7 +1005,7 @@ std::shared_ptr MinecraftInstance::texturePackList() const { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new ModFolderModel(texturePacksDir())); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir())); m_texture_pack_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction); } diff --git a/api/logic/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h similarity index 90% rename from api/logic/minecraft/MinecraftInstance.h rename to launcher/minecraft/MinecraftInstance.h index 985a8a7657..b55a277628 100644 --- a/api/logic/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -4,7 +4,7 @@ #include "minecraft/mod/Mod.h" #include #include -#include "multimc_logic_export.h" +#include "minecraft/launch/MinecraftServerTarget.h" class ModFolderModel; class WorldList; @@ -12,7 +12,7 @@ class GameOptions; class LaunchStep; class PackProfile; -class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance +class MinecraftInstance: public BaseInstance { Q_OBJECT public: @@ -76,11 +76,11 @@ class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance ////// Launch stuff ////// shared_qobject_ptr createUpdateTask(Net::Mode mode) override; - shared_qobject_ptr createLaunchTask(AuthSessionPtr account) override; + shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override; QStringList extraArguments() const override; - QStringList verboseDescription(AuthSessionPtr session) override; + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; QList getJarMods() const; - QString createLaunchScript(AuthSessionPtr session); + QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); /// get arguments passed to java QStringList javaArguments() const; @@ -107,7 +107,7 @@ class MULTIMC_LOGIC_EXPORT MinecraftInstance: public BaseInstance virtual QString getMainClass() const; // FIXME: remove - virtual QStringList processMinecraftArgs(AuthSessionPtr account) const; + virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const; virtual JavaVersion getJavaVersion() const; diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp similarity index 100% rename from api/logic/minecraft/MinecraftLoadAndCheck.cpp rename to launcher/minecraft/MinecraftLoadAndCheck.cpp diff --git a/api/logic/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h similarity index 100% rename from api/logic/minecraft/MinecraftLoadAndCheck.h rename to launcher/minecraft/MinecraftLoadAndCheck.h diff --git a/api/logic/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp similarity index 100% rename from api/logic/minecraft/MinecraftUpdate.cpp rename to launcher/minecraft/MinecraftUpdate.cpp diff --git a/api/logic/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h similarity index 100% rename from api/logic/minecraft/MinecraftUpdate.h rename to launcher/minecraft/MinecraftUpdate.h diff --git a/api/logic/minecraft/MojangDownloadInfo.h b/launcher/minecraft/MojangDownloadInfo.h similarity index 100% rename from api/logic/minecraft/MojangDownloadInfo.h rename to launcher/minecraft/MojangDownloadInfo.h diff --git a/api/logic/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp similarity index 95% rename from api/logic/minecraft/MojangVersionFormat.cpp rename to launcher/minecraft/MojangVersionFormat.cpp index 33d3c54cb2..f9cb2228a5 100644 --- a/api/logic/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -220,7 +220,7 @@ VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument &doc { auto libObj = requireObject(libVal); - auto lib = MojangVersionFormat::libraryFromJson(libObj, filename); + auto lib = MojangVersionFormat::libraryFromJson(*out, libObj, filename); out->libraries.append(lib); } } @@ -283,14 +283,18 @@ QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch } } -LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); } - out->m_name = libObj.value("name").toString(); + auto rawName = libObj.value("name").toString(); + out->m_name = rawName; + if(!out->m_name.valid()) { + problems.addProblem(ProblemSeverity::Error, QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName)); + } Bits::readString(libObj, "url", out->m_repositoryURL); if (libObj.contains("extract")) @@ -333,7 +337,7 @@ LibraryPtr MojangVersionFormat::libraryFromJson(const QJsonObject &libObj, const QJsonObject MojangVersionFormat::libraryToJson(Library *library) { QJsonObject libRoot; - libRoot.insert("name", (QString)library->m_name); + libRoot.insert("name", library->m_name.serialize()); if (!library->m_repositoryURL.isEmpty()) { libRoot.insert("url", library->m_repositoryURL); diff --git a/api/logic/minecraft/MojangVersionFormat.h b/launcher/minecraft/MojangVersionFormat.h similarity index 79% rename from api/logic/minecraft/MojangVersionFormat.h rename to launcher/minecraft/MojangVersionFormat.h index 76c529e9b1..d38f0a2ff1 100644 --- a/api/logic/minecraft/MojangVersionFormat.h +++ b/launcher/minecraft/MojangVersionFormat.h @@ -3,10 +3,9 @@ #include #include #include +#include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT MojangVersionFormat +class MojangVersionFormat { friend class OneSixVersionFormat; protected: @@ -20,6 +19,6 @@ friend class OneSixVersionFormat; static QJsonDocument versionFileToJson(const VersionFilePtr &patch); // libraries - static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject libraryToJson(Library *library); }; diff --git a/api/logic/minecraft/MojangVersionFormat_test.cpp b/launcher/minecraft/MojangVersionFormat_test.cpp similarity index 100% rename from api/logic/minecraft/MojangVersionFormat_test.cpp rename to launcher/minecraft/MojangVersionFormat_test.cpp diff --git a/api/logic/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp similarity index 90% rename from api/logic/minecraft/OneSixVersionFormat.cpp rename to launcher/minecraft/OneSixVersionFormat.cpp index 7ac9e2db63..0329d70ef2 100644 --- a/api/logic/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -13,9 +13,9 @@ static void readString(const QJsonObject &root, const QString &key, QString &var } } -LibraryPtr OneSixVersionFormat::libraryFromJson(const QJsonObject &libObj, const QString &filename) +LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename) { - LibraryPtr out = MojangVersionFormat::libraryFromJson(libObj, filename); + LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename); readString(libObj, "MMC-hint", out->m_hint); readString(libObj, "MMC-absulute_url", out->m_absoluteURL); readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); @@ -115,7 +115,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::jarModFromJson(libObj, filename); + auto lib = OneSixVersionFormat::jarModFromJson(*out, libObj, filename); // and add to jar mods out->jarMods.append(lib); } @@ -126,7 +126,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::plusJarModFromJson(libObj, filename, out->name); + auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name); // and add to jar mods out->jarMods.append(lib); } @@ -138,20 +138,20 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject libObj = requireObject(libVal); // parse the jarmod - auto lib = OneSixVersionFormat::modFromJson(libObj, filename); + auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename); // and add to jar mods out->mods.append(lib); } } - auto readLibs = [&](const char * which, QList & out) + auto readLibs = [&](const char * which, QList & outList) { for (auto libVal : requireArray(root.value(which))) { QJsonObject libObj = requireObject(libVal); // parse the library - auto lib = libraryFromJson(libObj, filename); - out.append(lib); + auto lib = libraryFromJson(*out, libObj, filename); + outList.append(lib); } }; bool hasPlusLibs = root.contains("+libraries"); @@ -180,7 +180,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc if(root.contains("mainJar")) { QJsonObject libObj = requireObject(root, "mainJar"); - out->mainJar = libraryFromJson(libObj, filename); + out->mainJar = libraryFromJson(*out, libObj, filename); } // else reconstruct it from downloads and id ... if that's available else if(!out->minecraftVersion.isEmpty()) @@ -194,8 +194,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc LibDLInfo->artifact = out->mojangDownloads["client"]; lib->setMojangDownloadInfo(LibDLInfo); } - // we got nothing... guess based on ancient hardcoded Mojang behaviour - // FIXME: this will eventually break... + // we got nothing... else { out->addProblem( @@ -330,8 +329,12 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch } } -LibraryPtr OneSixVersionFormat::plusJarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName) -{ +LibraryPtr OneSixVersionFormat::plusJarModFromJson( + ProblemContainer & problems, + const QJsonObject &libObj, + const QString &filename, + const QString &originalName +) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { @@ -366,9 +369,9 @@ LibraryPtr OneSixVersionFormat::plusJarModFromJson(const QJsonObject &libObj, co return out; } -LibraryPtr OneSixVersionFormat::jarModFromJson(const QJsonObject& libObj, const QString& filename) +LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename) { - return libraryFromJson(libObj, filename); + return libraryFromJson(problems, libObj, filename); } @@ -377,9 +380,9 @@ QJsonObject OneSixVersionFormat::jarModtoJson(Library *jarmod) return libraryToJson(jarmod); } -LibraryPtr OneSixVersionFormat::modFromJson(const QJsonObject& libObj, const QString& filename) +LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename) { - return libraryFromJson(libObj, filename); + return libraryFromJson(problems, libObj, filename); } QJsonObject OneSixVersionFormat::modtoJson(Library *jarmod) diff --git a/api/logic/minecraft/OneSixVersionFormat.h b/launcher/minecraft/OneSixVersionFormat.h similarity index 58% rename from api/logic/minecraft/OneSixVersionFormat.h rename to launcher/minecraft/OneSixVersionFormat.h index 14ae385c24..1a091d8805 100644 --- a/api/logic/minecraft/OneSixVersionFormat.h +++ b/launcher/minecraft/OneSixVersionFormat.h @@ -4,6 +4,7 @@ #include #include #include +#include class OneSixVersionFormat { @@ -13,17 +14,17 @@ class OneSixVersionFormat static QJsonDocument versionFileToJson(const VersionFilePtr &patch); // libraries - static LibraryPtr libraryFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject libraryToJson(Library *library); // DEPRECATED: old 'plus' jar mods generated by the application - static LibraryPtr plusJarModFromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName); + static LibraryPtr plusJarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename, const QString &originalName); // new jar mods derived from libraries - static LibraryPtr jarModFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr jarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject jarModtoJson(Library * jarmod); // mods, also derived from libraries - static LibraryPtr modFromJson(const QJsonObject &libObj, const QString &filename); + static LibraryPtr modFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); static QJsonObject modtoJson(Library * jarmod); }; diff --git a/api/logic/minecraft/OpSys.cpp b/launcher/minecraft/OpSys.cpp similarity index 100% rename from api/logic/minecraft/OpSys.cpp rename to launcher/minecraft/OpSys.cpp diff --git a/api/logic/minecraft/OpSys.h b/launcher/minecraft/OpSys.h similarity index 100% rename from api/logic/minecraft/OpSys.h rename to launcher/minecraft/OpSys.h diff --git a/api/logic/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp similarity index 100% rename from api/logic/minecraft/PackProfile.cpp rename to launcher/minecraft/PackProfile.cpp diff --git a/api/logic/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h similarity index 97% rename from api/logic/minecraft/PackProfile.h rename to launcher/minecraft/PackProfile.h index e55e6a5802..3d6cc6c33b 100644 --- a/api/logic/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -27,14 +27,13 @@ #include "ProfileUtils.h" #include "BaseVersion.h" #include "MojangDownloadInfo.h" -#include "multimc_logic_export.h" #include "net/Mode.h" class MinecraftInstance; struct PackProfileData; class ComponentUpdateTask; -class MULTIMC_LOGIC_EXPORT PackProfile : public QAbstractListModel +class PackProfile : public QAbstractListModel { Q_OBJECT friend ComponentUpdateTask; diff --git a/api/logic/minecraft/PackProfile_p.h b/launcher/minecraft/PackProfile_p.h similarity index 100% rename from api/logic/minecraft/PackProfile_p.h rename to launcher/minecraft/PackProfile_p.h diff --git a/api/logic/minecraft/ParseUtils.cpp b/launcher/minecraft/ParseUtils.cpp similarity index 100% rename from api/logic/minecraft/ParseUtils.cpp rename to launcher/minecraft/ParseUtils.cpp diff --git a/api/logic/minecraft/ParseUtils.h b/launcher/minecraft/ParseUtils.h similarity index 53% rename from api/logic/minecraft/ParseUtils.h rename to launcher/minecraft/ParseUtils.h index 2b367a10a4..aad82748d9 100644 --- a/api/logic/minecraft/ParseUtils.h +++ b/launcher/minecraft/ParseUtils.h @@ -2,10 +2,8 @@ #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); +QDateTime timeFromS3Time(QString str); /// take a timestamp and convert it into an S3 timestamp -MULTIMC_LOGIC_EXPORT QString timeToS3Time(QDateTime); +QString timeToS3Time(QDateTime); diff --git a/api/logic/minecraft/ParseUtils_test.cpp b/launcher/minecraft/ParseUtils_test.cpp similarity index 100% rename from api/logic/minecraft/ParseUtils_test.cpp rename to launcher/minecraft/ParseUtils_test.cpp diff --git a/api/logic/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp similarity index 100% rename from api/logic/minecraft/ProfileUtils.cpp rename to launcher/minecraft/ProfileUtils.cpp diff --git a/api/logic/minecraft/ProfileUtils.h b/launcher/minecraft/ProfileUtils.h similarity index 100% rename from api/logic/minecraft/ProfileUtils.h rename to launcher/minecraft/ProfileUtils.h diff --git a/api/logic/minecraft/Rule.cpp b/launcher/minecraft/Rule.cpp similarity index 100% rename from api/logic/minecraft/Rule.cpp rename to launcher/minecraft/Rule.cpp diff --git a/api/logic/minecraft/Rule.h b/launcher/minecraft/Rule.h similarity index 100% rename from api/logic/minecraft/Rule.h rename to launcher/minecraft/Rule.h diff --git a/api/logic/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp similarity index 100% rename from api/logic/minecraft/VersionFile.cpp rename to launcher/minecraft/VersionFile.cpp diff --git a/api/logic/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h similarity index 100% rename from api/logic/minecraft/VersionFile.h rename to launcher/minecraft/VersionFile.h diff --git a/api/logic/minecraft/VersionFilterData.cpp b/launcher/minecraft/VersionFilterData.cpp similarity index 84% rename from api/logic/minecraft/VersionFilterData.cpp rename to launcher/minecraft/VersionFilterData.cpp index 11f7eea923..38e7b60c8d 100644 --- a/api/logic/minecraft/VersionFilterData.cpp +++ b/launcher/minecraft/VersionFilterData.cpp @@ -7,18 +7,18 @@ 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}}; + QList{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}}; fmlLibsMapping["1.3.2"] = libs13; // 1.4.* auto libs14 = QList{ - {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", false}, - {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", false}, - {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", false}, - {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", false}}; + {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}, + {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb"}}; fmlLibsMapping["1.4"] = libs14; fmlLibsMapping["1.4.1"] = libs14; @@ -31,30 +31,30 @@ VersionFilterData::VersionFilterData() // 1.5 fmlLibsMapping["1.5"] = QList{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // 1.5.1 fmlLibsMapping["1.5.1"] = QList{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // 1.5.2 fmlLibsMapping["1.5.2"] = QList{ - {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", false}, - {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", false}, - {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", false}, - {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", true}, - {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", false}, - {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", true}}; + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; // don't use installers for those. forgeInstallerBlacklist = QSet({"1.5.2"}); @@ -65,4 +65,7 @@ VersionFilterData::VersionFilterData() 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"}; + + java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); + java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); } diff --git a/api/logic/minecraft/VersionFilterData.h b/launcher/minecraft/VersionFilterData.h similarity index 72% rename from api/logic/minecraft/VersionFilterData.h rename to launcher/minecraft/VersionFilterData.h index 88e91f11a0..79756c3ff6 100644 --- a/api/logic/minecraft/VersionFilterData.h +++ b/launcher/minecraft/VersionFilterData.h @@ -4,13 +4,10 @@ #include #include -#include "multimc_logic_export.h" - struct FMLlib { QString filename; QString checksum; - bool ours; }; struct VersionFilterData @@ -24,5 +21,9 @@ struct VersionFilterData QDateTime legacyCutoffDate; // Libraries that belong to LWJGL QSet lwjglWhitelist; + // release date of first version to require Java 8 (17w13a) + QDateTime java8BeginsDate; + // release data of first version to require Java 16 (21w19a) + QDateTime java16BeginsDate; }; -extern VersionFilterData MULTIMC_LOGIC_EXPORT g_VersionFilterData; +extern VersionFilterData g_VersionFilterData; diff --git a/api/logic/minecraft/World.cpp b/launcher/minecraft/World.cpp similarity index 100% rename from api/logic/minecraft/World.cpp rename to launcher/minecraft/World.cpp diff --git a/api/logic/minecraft/World.h b/launcher/minecraft/World.h similarity index 96% rename from api/logic/minecraft/World.h rename to launcher/minecraft/World.h index 1d94d54dea..35e327880e 100644 --- a/api/logic/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -18,9 +18,7 @@ #include #include -#include "multimc_logic_export.h" - -struct MULTIMC_LOGIC_EXPORT GameType { +struct GameType { GameType() = default; GameType (nonstd::optional original); @@ -38,7 +36,7 @@ struct MULTIMC_LOGIC_EXPORT GameType { nonstd::optional original; }; -class MULTIMC_LOGIC_EXPORT World +class World { public: World(const QFileInfo &file); diff --git a/api/logic/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp similarity index 99% rename from api/logic/minecraft/WorldList.cpp rename to launcher/minecraft/WorldList.cpp index f6309dbdef..dcdbc32147 100644 --- a/api/logic/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -26,8 +26,7 @@ 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.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); is_watching = false; diff --git a/api/logic/minecraft/WorldList.h b/launcher/minecraft/WorldList.h similarity index 97% rename from api/logic/minecraft/WorldList.h rename to launcher/minecraft/WorldList.h index 740b1461db..8e238ee366 100644 --- a/api/logic/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -22,11 +22,9 @@ #include #include "minecraft/World.h" -#include "multimc_logic_export.h" - class QFileSystemWatcher; -class MULTIMC_LOGIC_EXPORT WorldList : public QAbstractListModel +class WorldList : public QAbstractListModel { Q_OBJECT public: diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp new file mode 100644 index 0000000000..5c6de9dfd3 --- /dev/null +++ b/launcher/minecraft/auth/AccountData.cpp @@ -0,0 +1,399 @@ +#include "AccountData.h" +#include +#include +#include +#include +#include + +namespace { +void tokenToJSONV3(QJsonObject &parent, Katabasis::Token t, const char * tokenName) { + if(!t.persistent) { + return; + } + QJsonObject out; + if(t.issueInstant.isValid()) { + out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000); + } + + if(t.notAfter.isValid()) { + out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000); + } + + bool save = false; + if(!t.token.isEmpty()) { + out["token"] = QJsonValue(t.token); + save = true; + } + if(!t.refresh_token.isEmpty()) { + out["refresh_token"] = QJsonValue(t.refresh_token); + save = true; + } + if(t.extra.size()) { + out["extra"] = QJsonObject::fromVariantMap(t.extra); + save = true; + } + if(save) { + parent[tokenName] = out; + } +} + +Katabasis::Token tokenFromJSONV3(const QJsonObject &parent, const char * tokenName) { + Katabasis::Token out; + auto tokenObject = parent.value(tokenName).toObject(); + if(tokenObject.isEmpty()) { + return out; + } + auto issueInstant = tokenObject.value("iat"); + if(issueInstant.isDouble()) { + out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t) issueInstant.toDouble()) * 1000); + } + + auto notAfter = tokenObject.value("exp"); + if(notAfter.isDouble()) { + out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t) notAfter.toDouble()) * 1000); + } + + auto token = tokenObject.value("token"); + if(token.isString()) { + out.token = token.toString(); + out.validity = Katabasis::Validity::Assumed; + } + + auto refresh_token = tokenObject.value("refresh_token"); + if(refresh_token.isString()) { + out.refresh_token = refresh_token.toString(); + } + + auto extra = tokenObject.value("extra"); + if(extra.isObject()) { + out.extra = extra.toObject().toVariantMap(); + } + return out; +} + +void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * tokenName) { + if(p.id.isEmpty()) { + return; + } + QJsonObject out; + out["id"] = QJsonValue(p.id); + out["name"] = QJsonValue(p.name); + if(!p.currentCape.isEmpty()) { + out["cape"] = p.currentCape; + } + + { + QJsonObject skinObj; + skinObj["id"] = p.skin.id; + skinObj["url"] = p.skin.url; + skinObj["variant"] = p.skin.variant; + if(p.skin.data.size()) { + skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); + } + out["skin"] = skinObj; + } + + QJsonArray capesArray; + for(auto & cape: p.capes) { + QJsonObject capeObj; + capeObj["id"] = cape.id; + capeObj["url"] = cape.url; + capeObj["alias"] = cape.alias; + if(cape.data.size()) { + capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); + } + capesArray.push_back(capeObj); + } + out["capes"] = capesArray; + parent[tokenName] = out; +} + +MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * tokenName) { + MinecraftProfile out; + auto tokenObject = parent.value(tokenName).toObject(); + if(tokenObject.isEmpty()) { + return out; + } + { + auto idV = tokenObject.value("id"); + auto nameV = tokenObject.value("name"); + if(!idV.isString() || !nameV.isString()) { + qWarning() << "mandatory profile attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.name = nameV.toString(); + out.id = idV.toString(); + } + + { + auto skinV = tokenObject.value("skin"); + if(!skinV.isObject()) { + qWarning() << "skin is missing"; + return MinecraftProfile(); + } + auto skinObj = skinV.toObject(); + auto idV = skinObj.value("id"); + auto urlV = skinObj.value("url"); + auto variantV = skinObj.value("variant"); + if(!idV.isString() || !urlV.isString() || !variantV.isString()) { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.skin.id = idV.toString(); + out.skin.url = urlV.toString(); + out.skin.variant = variantV.toString(); + + // data for skin is optional + auto dataV = skinObj.value("data"); + if(dataV.isString()) { + // TODO: validate base64 + out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + } + else if (!dataV.isUndefined()) { + qWarning() << "skin data is something unexpected"; + return MinecraftProfile(); + } + } + + { + auto capesV = tokenObject.value("capes"); + if(!capesV.isArray()) { + qWarning() << "capes is not an array!"; + return MinecraftProfile(); + } + auto capesArray = capesV.toArray(); + for(auto capeV: capesArray) { + if(!capeV.isObject()) { + qWarning() << "cape is not an object!"; + return MinecraftProfile(); + } + auto capeObj = capeV.toObject(); + auto idV = capeObj.value("id"); + auto urlV = capeObj.value("url"); + auto aliasV = capeObj.value("alias"); + if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + Cape cape; + cape.id = idV.toString(); + cape.url = urlV.toString(); + cape.alias = aliasV.toString(); + + // data for cape is optional. + auto dataV = capeObj.value("data"); + if(dataV.isString()) { + // TODO: validate base64 + cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + } + else if (!dataV.isUndefined()) { + qWarning() << "cape data is something unexpected"; + return MinecraftProfile(); + } + out.capes[cape.id] = cape; + } + } + // current cape + { + auto capeV = tokenObject.value("cape"); + if(capeV.isString()) { + auto currentCape = capeV.toString(); + if(out.capes.contains(currentCape)) { + out.currentCape = currentCape; + } + } + } + out.validity = Katabasis::Validity::Assumed; + return out; +} + +} + +bool AccountData::resumeStateFromV2(QJsonObject data) { + // The JSON object must at least have a username for it to be valid. + if (!data.value("username").isString()) + { + qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type."; + return false; + } + + QString userName = data.value("username").toString(""); + QString clientToken = data.value("clientToken").toString(""); + QString accessToken = data.value("accessToken").toString(""); + + QJsonArray profileArray = data.value("profiles").toArray(); + if (profileArray.size() < 1) + { + qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found."; + return false; + } + + struct AccountProfile + { + QString id; + QString name; + bool legacy; + }; + + QList profiles; + int currentProfileIndex = 0; + int index = -1; + QString currentProfile = data.value("activeProfile").toString(""); + for (QJsonValue profileVal : profileArray) + { + index++; + 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" << name << "because it was missing an ID or a name."; + continue; + } + if(id == currentProfile) { + currentProfileIndex = index; + } + profiles.append({id, name, legacy}); + } + auto & profile = profiles[currentProfileIndex]; + + type = AccountType::Mojang; + legacy = profile.legacy; + + minecraftProfile.id = profile.id; + minecraftProfile.name = profile.name; + minecraftProfile.validity = Katabasis::Validity::Assumed; + + yggdrasilToken.token = accessToken; + yggdrasilToken.extra["clientToken"] = clientToken; + yggdrasilToken.extra["userName"] = userName; + yggdrasilToken.validity = Katabasis::Validity::Assumed; + + validity_ = minecraftProfile.validity; + return true; +} + +bool AccountData::resumeStateFromV3(QJsonObject data) { + auto typeV = data.value("type"); + if(!typeV.isString()) { + qWarning() << "Failed to parse account data: type is missing."; + return false; + } + auto typeS = typeV.toString(); + if(typeS == "MSA") { + type = AccountType::MSA; + } else if (typeS == "Mojang") { + type = AccountType::Mojang; + } else { + qWarning() << "Failed to parse account data: type is not recognized."; + return false; + } + + if(type == AccountType::Mojang) { + legacy = data.value("legacy").toBool(false); + canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); + } + + if(type == AccountType::MSA) { + msaToken = tokenFromJSONV3(data, "msa"); + userToken = tokenFromJSONV3(data, "utoken"); + xboxApiToken = tokenFromJSONV3(data, "xrp-main"); + mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); + } + + yggdrasilToken = tokenFromJSONV3(data, "ygg"); + minecraftProfile = profileFromJSONV3(data, "profile"); + + validity_ = minecraftProfile.validity; + + return true; +} + +QJsonObject AccountData::saveState() const { + QJsonObject output; + if(type == AccountType::Mojang) { + output["type"] = "Mojang"; + if(legacy) { + output["legacy"] = true; + } + if(canMigrateToMSA) { + output["canMigrateToMSA"] = true; + } + } + else if (type == AccountType::MSA) { + output["type"] = "MSA"; + tokenToJSONV3(output, msaToken, "msa"); + tokenToJSONV3(output, userToken, "utoken"); + tokenToJSONV3(output, xboxApiToken, "xrp-main"); + tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); + } + + tokenToJSONV3(output, yggdrasilToken, "ygg"); + profileToJSONV3(output, minecraftProfile, "profile"); + return output; +} + +QString AccountData::userName() const { + if(type != AccountType::Mojang) { + return QString(); + } + return yggdrasilToken.extra["userName"].toString(); +} + +QString AccountData::accessToken() const { + return yggdrasilToken.token; +} + +QString AccountData::clientToken() const { + if(type != AccountType::Mojang) { + return QString(); + } + return yggdrasilToken.extra["clientToken"].toString(); +} + +void AccountData::setClientToken(QString clientToken) { + if(type != AccountType::Mojang) { + return; + } + yggdrasilToken.extra["clientToken"] = clientToken; +} + +void AccountData::generateClientTokenIfMissing() { + if(yggdrasilToken.extra.contains("clientToken")) { + return; + } + invalidateClientToken(); +} + +void AccountData::invalidateClientToken() { + if(type != AccountType::Mojang) { + return; + } + yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{-}]")); +} + +QString AccountData::profileId() const { + return minecraftProfile.id; +} + +QString AccountData::profileName() const { + return minecraftProfile.name; +} + +QString AccountData::accountDisplayString() const { + switch(type) { + case AccountType::Mojang: { + return userName(); + } + case AccountType::MSA: { + if(xboxApiToken.extra.contains("gtg")) { + return xboxApiToken.extra["gtg"].toString(); + } + return "Xbox profile missing"; + } + default: { + return "Invalid Account"; + } + } +} diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h new file mode 100644 index 0000000000..cf58fb76fb --- /dev/null +++ b/launcher/minecraft/auth/AccountData.h @@ -0,0 +1,73 @@ +#pragma once +#include +#include +#include +#include +#include + +struct Skin { + QString id; + QString url; + QString variant; + + QByteArray data; +}; + +struct Cape { + QString id; + QString url; + QString alias; + + QByteArray data; +}; + +struct MinecraftProfile { + QString id; + QString name; + Skin skin; + QString currentCape; + QMap capes; + Katabasis::Validity validity = Katabasis::Validity::None; +}; + +enum class AccountType { + MSA, + Mojang +}; + +struct AccountData { + QJsonObject saveState() const; + bool resumeStateFromV2(QJsonObject data); + bool resumeStateFromV3(QJsonObject data); + + //! userName for Mojang accounts, gamertag for MSA + QString accountDisplayString() const; + + //! Only valid for Mojang accounts. MSA does not preserve this information + QString userName() const; + + //! Only valid for Mojang accounts. + QString clientToken() const; + void setClientToken(QString clientToken); + void invalidateClientToken(); + void generateClientTokenIfMissing(); + + //! Yggdrasil access token, as passed to the game. + QString accessToken() const; + + QString profileId() const; + QString profileName() const; + + AccountType type = AccountType::MSA; + bool legacy = false; + bool canMigrateToMSA = false; + + Katabasis::Token msaToken; + Katabasis::Token userToken; + Katabasis::Token xboxApiToken; + Katabasis::Token mojangservicesToken; + + Katabasis::Token yggdrasilToken; + MinecraftProfile minecraftProfile; + Katabasis::Validity validity_ = Katabasis::Validity::None; +}; diff --git a/api/logic/minecraft/auth/MojangAccountList.cpp b/launcher/minecraft/auth/AccountList.cpp similarity index 50% rename from api/logic/minecraft/auth/MojangAccountList.cpp rename to launcher/minecraft/auth/AccountList.cpp index e584cb3b59..76af0ac0fa 100644 --- a/api/logic/minecraft/auth/MojangAccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -13,8 +13,8 @@ * limitations under the License. */ -#include "MojangAccountList.h" -#include "MojangAccount.h" +#include "AccountList.h" +#include "AccountData.h" #include #include @@ -28,31 +28,49 @@ #include #include +#include -#define ACCOUNT_LIST_FORMAT_VERSION 2 +enum AccountListVersion { + MojangOnly = 2, + MojangMSA = 3 +}; -MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent) -{ -} +AccountList::AccountList(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; +int AccountList::findAccountByProfileId(const QString& profileId) const { + for (int i = 0; i < count(); i++) { + MinecraftAccountPtr account = at(i); + if (account->profileId() == profileId) { + return i; + } } - return nullptr; + return -1; } -const MojangAccountPtr MojangAccountList::at(int i) const +const MinecraftAccountPtr AccountList::at(int i) const { - return MojangAccountPtr(m_accounts.at(i)); + return MinecraftAccountPtr(m_accounts.at(i)); } -void MojangAccountList::addAccount(const MojangAccountPtr account) +void AccountList::addAccount(const MinecraftAccountPtr account) { + // We only ever want accounts with valid profiles. + // Keeping profile-less accounts is pointless and serves no purpose. + auto profileId = account->profileId(); + if(!profileId.size()) { + return; + } + + // override/replace existing account with the same profileId + auto existingAccount = findAccountByProfileId(profileId); + if(existingAccount != -1) { + m_accounts[existingAccount] = account; + emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); + onListChanged(); + return; + } + + // if we don't have this porfileId yet, add the account to the end int row = m_accounts.count(); beginInsertRows(QModelIndex(), row, row); connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); @@ -61,24 +79,7 @@ void MojangAccountList::addAccount(const MojangAccountPtr account) onListChanged(); } -void MojangAccountList::removeAccount(const QString &username) -{ - int idx = 0; - for (auto account : m_accounts) - { - if (account->username() == username) - { - beginRemoveRows(QModelIndex(), idx, idx); - m_accounts.removeOne(account); - endRemoveRows(); - return; - } - idx++; - } - onListChanged(); -} - -void MojangAccountList::removeAccount(QModelIndex index) +void AccountList::removeAccount(QModelIndex index) { int row = index.row(); if(index.isValid() && row >= 0 && row < m_accounts.size()) @@ -96,19 +97,19 @@ void MojangAccountList::removeAccount(QModelIndex index) } } -MojangAccountPtr MojangAccountList::activeAccount() const +MinecraftAccountPtr AccountList::activeAccount() const { return m_activeAccount; } -void MojangAccountList::setActiveAccount(const QString &username) +void AccountList::setActiveAccount(const QString &profileId) { - if (username.isEmpty() && m_activeAccount) + if (profileId.isEmpty() && m_activeAccount) { int idx = 0; auto prevActiveAcc = m_activeAccount; m_activeAccount = nullptr; - for (MojangAccountPtr account : m_accounts) + for (MinecraftAccountPtr account : m_accounts) { if (account == prevActiveAcc) { @@ -125,9 +126,9 @@ void MojangAccountList::setActiveAccount(const QString &username) auto newActiveAccount = m_activeAccount; int newActiveAccountIdx = -1; int idx = 0; - for (MojangAccountPtr account : m_accounts) + for (MinecraftAccountPtr account : m_accounts) { - if (account->username() == username) + if (account->profileId() == profileId) { newActiveAccount = account; newActiveAccountIdx = idx; @@ -148,13 +149,13 @@ void MojangAccountList::setActiveAccount(const QString &username) } } -void MojangAccountList::accountChanged() +void AccountList::accountChanged() { // the list changed. there is no doubt. onListChanged(); } -void MojangAccountList::onListChanged() +void AccountList::onListChanged() { if (m_autosave) // TODO: Alert the user if this fails. @@ -163,7 +164,7 @@ void MojangAccountList::onListChanged() emit listChanged(); } -void MojangAccountList::onActiveChanged() +void AccountList::onActiveChanged() { if (m_autosave) saveList(); @@ -171,12 +172,12 @@ void MojangAccountList::onActiveChanged() emit activeAccountChanged(); } -int MojangAccountList::count() const +int AccountList::count() const { return m_accounts.count(); } -QVariant MojangAccountList::data(const QModelIndex &index, int role) const +QVariant AccountList::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); @@ -184,51 +185,75 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const if (index.row() > count()) return QVariant(); - MojangAccountPtr account = at(index.row()); + MinecraftAccountPtr account = at(index.row()); switch (role) { - case Qt::DisplayRole: - switch (index.column()) - { - case NameColumn: - return account->username(); + case Qt::DisplayRole: + switch (index.column()) + { + case NameColumn: + return account->accountDisplayString(); - default: - return QVariant(); - } + case TypeColumn: { + auto typeStr = account->typeString(); + typeStr[0] = typeStr[0].toUpper(); + return typeStr; + } - case Qt::ToolTipRole: - return account->username(); + case ProfileNameColumn: { + return account->profileName(); + } - case PointerRole: - return qVariantFromValue(account); + case MigrationColumn: { + if(account->isMSA()) { + return tr("N/A", "Can Migrate?"); + } + if (account->canMigrate()) { + return tr("Yes", "Can Migrate?"); + } + else { + return tr("No", "Can Migrate?"); + } + } - case Qt::CheckStateRole: - switch (index.column()) - { - case ActiveColumn: - return account == m_activeAccount ? Qt::Checked : Qt::Unchecked; - } + default: + return QVariant(); + } - default: - return QVariant(); + case Qt::ToolTipRole: + return account->accountDisplayString(); + + case PointerRole: + return qVariantFromValue(account); + + case Qt::CheckStateRole: + switch (index.column()) + { + case NameColumn: + return account == m_activeAccount ? Qt::Checked : Qt::Unchecked; + } + + default: + return QVariant(); } } -QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const +QVariant AccountList::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"); - + return tr("Account"); + case TypeColumn: + return tr("Type"); + case MigrationColumn: + return tr("Can Migrate?"); + case ProfileNameColumn: + return tr("Profile"); default: return QVariant(); } @@ -237,8 +262,13 @@ QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, switch (section) { case NameColumn: - return tr("The name of the version."); - + return tr("User name of the account."); + case TypeColumn: + return tr("Type of the account - Mojang or MSA."); + case MigrationColumn: + return tr("Can this account migrate to Microsoft account?"); + case ProfileNameColumn: + return tr("Name of the Minecraft profile associated with the account."); default: return QVariant(); } @@ -248,18 +278,18 @@ QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, } } -int MojangAccountList::rowCount(const QModelIndex &) const +int AccountList::rowCount(const QModelIndex &) const { // Return count return count(); } -int MojangAccountList::columnCount(const QModelIndex &) const +int AccountList::columnCount(const QModelIndex &) const { - return 2; + return NUM_COLUMNS; } -Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const +Qt::ItemFlags AccountList::flags(const QModelIndex &index) const { if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) { @@ -269,7 +299,7 @@ Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; } -bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role) +bool AccountList::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) { @@ -280,8 +310,8 @@ bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, { if(value == Qt::Checked) { - MojangAccountPtr account = this->at(index.row()); - this->setActiveAccount(account->username()); + MinecraftAccountPtr account = at(index.row()); + setActiveAccount(account->profileId()); } } @@ -289,31 +319,21 @@ bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, return true; } -void MojangAccountList::updateListData(QList versions) -{ - beginResetModel(); - m_accounts = versions; - endResetModel(); -} - -bool MojangAccountList::loadList(const QString &filePath) +bool AccountList::loadList() { - QString path = filePath; - if (path.isEmpty()) - path = m_listFilePath; - if (path.isEmpty()) + if (m_listFilePath.isEmpty()) { qCritical() << "Can't load Mojang account list. No file path given and no default set."; return false; } - QFile file(path); + QFile file(m_listFilePath); // 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(); + qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); return false; } @@ -343,121 +363,168 @@ bool MojangAccountList::loadList(const QString &filePath) 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; + auto listVersion = root.value("formatVersion").toVariant().toInt(); + switch(listVersion) { + case AccountListVersion::MojangOnly: { + return loadV2(root); + } + break; + case AccountListVersion::MojangMSA: { + return loadV3(root); + } + break; + default: { + QString newName = "accounts-old.json"; + qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; + // Attempt to rename the old version. + file.rename(newName); + return false; + } + } +} - // Attempt to rename the old version. - file.rename(newName); - return false; +bool AccountList::loadV2(QJsonObject& root) { + beginResetModel(); + auto activeUserName = root.value("activeAccount").toString(""); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj); + if (account.get() != nullptr) + { + auto profileId = account->profileId(); + if(!profileId.size()) { + continue; + } + if(findAccountByProfileId(profileId) != -1) { + continue; + } + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + m_accounts.append(account); + if (activeUserName.size() && account->mojangUserName() == activeUserName) { + m_activeAccount = account; + } + } + else + { + qWarning() << "Failed to load an account."; + } } + endResetModel(); + return true; +} - // Now, load the accounts array. +bool AccountList::loadV3(QJsonObject& root) { beginResetModel(); QJsonArray accounts = root.value("accounts").toArray(); for (QJsonValue accountVal : accounts) { QJsonObject accountObj = accountVal.toObject(); - MojangAccountPtr account = MojangAccount::loadFromJson(accountObj); + MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj); if (account.get() != nullptr) { - connect(account.get(), SIGNAL(changed()), SLOT(accountChanged())); + auto profileId = account->profileId(); + if(!profileId.size()) { + continue; + } + if(findAccountByProfileId(profileId) != -1) { + continue; + } + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); m_accounts.append(account); + if(accountObj.value("active").toBool(false)) { + m_activeAccount = 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) + +bool AccountList::saveList() { - QString path(filePath); - if (path.isEmpty()) - path = m_listFilePath; - if (path.isEmpty()) + if (m_listFilePath.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)) + if(!FS::ensureFilePathExists(m_listFilePath)) return false; // make sure the file wasn't overwritten with a folder before (fixes a bug) - QFileInfo finfo(path); + QFileInfo finfo(m_listFilePath); if(finfo.isDir()) { - QDir badDir(path); + QDir badDir(m_listFilePath); badDir.removeRecursively(); } - qDebug() << "Writing account list to" << path; + qDebug() << "Writing account list to" << m_listFilePath; qDebug() << "Building JSON data structure."; // Build the JSON document to write to the list file. QJsonObject root; - root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION); + root.insert("formatVersion", AccountListVersion::MojangMSA); // Build a list of accounts. qDebug() << "Building account array."; QJsonArray accounts; - for (MojangAccountPtr account : m_accounts) + for (MinecraftAccountPtr account : m_accounts) { QJsonObject accountObj = account->saveToJson(); + if(m_activeAccount == account) { + accountObj["active"] = true; + } 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); + QSaveFile file(m_listFilePath); // 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(); + qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).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; + if(file.commit()) { + qDebug() << "Saved account list to" << m_listFilePath; + return true; + } + else { + qDebug() << "Failed to save accounts to" << m_listFilePath; + return false; + } } -void MojangAccountList::setListFilePath(QString path, bool autosave) +void AccountList::setListFilePath(QString path, bool autosave) { m_listFilePath = path; m_autosave = autosave; } -bool MojangAccountList::anyAccountIsValid() +bool AccountList::anyAccountIsValid() { for(auto account:m_accounts) { diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h new file mode 100644 index 0000000000..ed08bb1d8e --- /dev/null +++ b/launcher/minecraft/auth/AccountList.h @@ -0,0 +1,119 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "MinecraftAccount.h" + +#include +#include +#include +#include + +/*! + * List of available Mojang accounts. + * This should be loaded in the background by MultiMC on startup. + */ +class AccountList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + PointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + // TODO: Add icon column. + NameColumn = 0, + ProfileNameColumn, + MigrationColumn, + TypeColumn, + + NUM_COLUMNS + }; + + explicit AccountList(QObject *parent = 0); + + const MinecraftAccountPtr at(int i) const; + int count() const; + + //////// List Model Functions //////// + QVariant data(const QModelIndex &index, int role) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex &parent) const override; + virtual int columnCount(const QModelIndex &parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + void addAccount(const MinecraftAccountPtr account); + void removeAccount(QModelIndex index); + int findAccountByProfileId(const QString &profileId) const; + + /*! + * Sets the path to load/save the list file from/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. + */ + void setListFilePath(QString path, bool autosave = false); + + bool loadList(); + bool loadV2(QJsonObject &root); + bool loadV3(QJsonObject &root); + bool saveList(); + + MinecraftAccountPtr activeAccount() const; + void setActiveAccount(const QString &profileId); + bool anyAccountIsValid(); + +signals: + void listChanged(); + 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; + + MinecraftAccountPtr 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; +}; diff --git a/launcher/minecraft/auth/AccountTask.cpp b/launcher/minecraft/auth/AccountTask.cpp new file mode 100644 index 0000000000..d400ce8ddb --- /dev/null +++ b/launcher/minecraft/auth/AccountTask.cpp @@ -0,0 +1,71 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AccountTask.h" +#include "MinecraftAccount.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +AccountTask::AccountTask(AccountData *data, QObject *parent) + : Task(parent), m_data(data) +{ + changeState(STATE_CREATED); +} + +QString AccountTask::getStateMessage() const +{ + switch (m_accountState) + { + case STATE_CREATED: + return "Waiting..."; + case STATE_WORKING: + return tr("Sending request to auth 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."); + case STATE_FAILED_GONE: + return tr("Failed to authenticate. The account no longer exists."); + default: + return tr("..."); + } +} + +void AccountTask::changeState(AccountTask::State newState, QString reason) +{ + m_accountState = newState; + setStatus(getStateMessage()); + if (newState == STATE_SUCCEEDED) + { + emitSucceeded(); + } + else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT || newState == STATE_FAILED_GONE) + { + emitFailed(reason); + } +} diff --git a/api/logic/minecraft/auth/YggdrasilTask.h b/launcher/minecraft/auth/AccountTask.h similarity index 52% rename from api/logic/minecraft/auth/YggdrasilTask.h rename to launcher/minecraft/auth/AccountTask.h index 8af2e13243..4f3bd52aee 100644 --- a/api/logic/minecraft/auth/YggdrasilTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -22,19 +22,17 @@ #include #include -#include "MojangAccount.h" +#include "MinecraftAccount.h" class QNetworkReply; -/** - * A Yggdrasil task is a task that performs an operation on a given mojang account. - */ -class YggdrasilTask : public Task +class AccountTask : public Task { + friend class AuthContext; Q_OBJECT public: - explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0); - virtual ~YggdrasilTask() {}; + explicit AccountTask(AccountData * data, QObject *parent = 0); + virtual ~AccountTask() {}; /** * assign a session to this task. the session will be filled with required infomration @@ -52,7 +50,7 @@ class YggdrasilTask : public Task } /** - * Class describing a Yggdrasil error response. + * Class describing a Account error response. */ struct Error { @@ -75,45 +73,22 @@ class YggdrasilTask : public Task enum State { STATE_CREATED, - STATE_SENDING_REQUEST, - STATE_PROCESSING_RESPONSE, + STATE_WORKING, 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_FAILED_GONE, //!< hard failure. auth is invalid, and the account no longer exists STATE_SUCCEEDED - } m_state = STATE_CREATED; - -protected: + } m_accountState = STATE_CREATED; - 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; + State accountState() { + return m_accountState; + } - /** - * Gets the endpoint to POST to. - * No leading slash. - */ - virtual QString getEndpoint() const = 0; +signals: + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); - /** - * 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); +protected: /** * Returns the state message for the given state. @@ -122,30 +97,12 @@ class YggdrasilTask : public Task */ virtual QString getStateMessage() const; -protected -slots: - void processReply(); - void refreshTimers(qint64, qint64); - void heartbeat(); - void sslErrors(QList); - +protected slots: 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; + AccountData *m_data = 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/AuthSession.cpp b/launcher/minecraft/auth/AuthSession.cpp similarity index 98% rename from api/logic/minecraft/auth/AuthSession.cpp rename to launcher/minecraft/auth/AuthSession.cpp index 4e858796ab..d44f9098e7 100644 --- a/api/logic/minecraft/auth/AuthSession.cpp +++ b/launcher/minecraft/auth/AuthSession.cpp @@ -7,11 +7,13 @@ 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); diff --git a/api/logic/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h similarity index 76% rename from api/logic/minecraft/auth/AuthSession.h rename to launcher/minecraft/auth/AuthSession.h index b397d9a11f..f609d5d39e 100644 --- a/api/logic/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -4,17 +4,9 @@ #include #include -#include "multimc_logic_export.h" +class MinecraftAccount; -class MojangAccount; - -struct User -{ - QString id; - QMultiMap properties; -}; - -struct MULTIMC_LOGIC_EXPORT AuthSession +struct AuthSession { bool MakeOffline(QString offline_playername); @@ -23,13 +15,13 @@ struct MULTIMC_LOGIC_EXPORT AuthSession enum Status { Undetermined, + RequiresOAuth, RequiresPassword, PlayableOffline, - PlayableOnline + PlayableOnline, + GoneOrMigrated } status = Undetermined; - User u; - // client token QString client_token; // account user name @@ -48,7 +40,7 @@ struct MULTIMC_LOGIC_EXPORT AuthSession bool auth_server_online = false; // Did the user request online mode? bool wants_online = true; - std::shared_ptr m_accountPtr; + std::shared_ptr m_accountPtr; }; typedef std::shared_ptr AuthSessionPtr; diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp new file mode 100644 index 0000000000..9349093740 --- /dev/null +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -0,0 +1,352 @@ +/* Copyright 2013-2021 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 "MinecraftAccount.h" +#include "flows/AuthContext.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include + +MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) { + MinecraftAccountPtr account(new MinecraftAccount()); + if(account->data.resumeStateFromV2(json)) { + return account; + } + return nullptr; +} + +MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { + MinecraftAccountPtr account(new MinecraftAccount()); + if(account->data.resumeStateFromV3(json)) { + return account; + } + return nullptr; +} + +MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username) +{ + MinecraftAccountPtr account(new MinecraftAccount()); + account->data.type = AccountType::Mojang; + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + return account; +} + +MinecraftAccountPtr MinecraftAccount::createFromUsernameOffline(const QString &username) +{ + // Offline mode based by this code: + // https://github.com/MultiMC/MultiMC5/commit/6ede3c13b2bcda315e65dd78f2bfd729bc8b699b + MinecraftAccountPtr account(new MinecraftAccount()); + account->data.type = AccountType::Mojang; + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = "ff64ff64ff64ff64ff64ff64ff64ff64"; + account->data.yggdrasilToken.token = "ff64ff64ff64ff64ff64ff64ff64ff64"; + account->data.minecraftProfile.name = username; + account->data.minecraftProfile.id = QCryptographicHash::hash(username.toLocal8Bit(), QCryptographicHash::Md5).toHex(); + return account; +} + +MinecraftAccountPtr MinecraftAccount::createBlankMSA() +{ + MinecraftAccountPtr account(new MinecraftAccount()); + account->data.type = AccountType::MSA; + return account; +} + + +QJsonObject MinecraftAccount::saveToJson() const +{ + return data.saveState(); +} + +AccountStatus MinecraftAccount::accountStatus() const { + if(data.type == AccountType::Mojang) { + if (data.accessToken().isEmpty()) { + return NotVerified; + } + else { + return Verified; + } + } + // MSA + // FIXME: this is extremely crude and probably wrong + if(data.msaToken.token.isEmpty()) { + return NotVerified; + } + else { + return Verified; + } +} + +QPixmap MinecraftAccount::getFace() const { + QPixmap skinTexture; + if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { + return QPixmap(); + } + QPixmap skin = QPixmap(8, 8); + QPainter painter(&skin); + painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); + painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); + return skin.scaled(64, 64, Qt::KeepAspectRatio); +} + + +std::shared_ptr MinecraftAccount::login(AuthSessionPtr session, QString password) +{ + Q_ASSERT(m_currentTask.get() == nullptr); + + // take care of the true offline status + if (accountStatus() == NotVerified && password.isEmpty()) + { + if (session) + { + session->status = AuthSession::RequiresPassword; + fillSession(session); + } + return nullptr; + } + + if(accountStatus() == Verified && !session->wants_online) + { + session->status = AuthSession::PlayableOffline; + session->auth_server_online = false; + fillSession(session); + return nullptr; + } + else + { + if (password.isEmpty()) + { + m_currentTask.reset(new MojangRefresh(&data)); + } + else + { + m_currentTask.reset(new MojangLogin(&data, 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; +} + +std::shared_ptr MinecraftAccount::loginMSA(AuthSessionPtr session) { + Q_ASSERT(m_currentTask.get() == nullptr); + + if(accountStatus() == Verified && !session->wants_online) + { + session->status = AuthSession::PlayableOffline; + session->auth_server_online = false; + fillSession(session); + return nullptr; + } + else + { + m_currentTask.reset(new MSAInteractive(&data)); + 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; +} + +std::shared_ptr MinecraftAccount::refresh(AuthSessionPtr session) { + Q_ASSERT(m_currentTask.get() == nullptr); + + // take care of the true offline status + if (accountStatus() == NotVerified) + { + if (session) + { + if(data.type == AccountType::MSA) { + session->status = AuthSession::RequiresOAuth; + } + else { + session->status = AuthSession::RequiresPassword; + } + fillSession(session); + } + return nullptr; + } + + if(accountStatus() == Verified && !session->wants_online) + { + session->status = AuthSession::PlayableOffline; + session->auth_server_online = false; + fillSession(session); + return nullptr; + } + else + { + if(data.type == AccountType::MSA) { + m_currentTask.reset(new MSASilent(&data)); + } + else { + m_currentTask.reset(new MojangRefresh(&data)); + } + 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 MinecraftAccount::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 MinecraftAccount::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 + switch (m_currentTask->accountState()) { + case AccountTask::STATE_FAILED_SOFT: { + if (session) + { + if(accountStatus() == Verified) { + session->status = AuthSession::PlayableOffline; + } + else { + if(data.type == AccountType::MSA) { + session->status = AuthSession::RequiresOAuth; + } + else { + session->status = AuthSession::RequiresPassword; + } + } + session->auth_server_online = false; + fillSession(session); + } + } + break; + case AccountTask::STATE_FAILED_HARD: { + // FIXME: MSA data clearing + data.yggdrasilToken.token = QString(); + data.yggdrasilToken.validity = Katabasis::Validity::None; + data.validity_ = Katabasis::Validity::None; + emit changed(); + if (session) + { + if(data.type == AccountType::MSA) { + session->status = AuthSession::RequiresOAuth; + } + else { + session->status = AuthSession::RequiresPassword; + } + session->auth_server_online = true; + fillSession(session); + } + } + break; + case AccountTask::STATE_FAILED_GONE: { + data.validity_ = Katabasis::Validity::None; + emit changed(); + if (session) + { + session->status = AuthSession::GoneOrMigrated; + session->auth_server_online = true; + fillSession(session); + } + } + break; + case AccountTask::STATE_CREATED: + case AccountTask::STATE_WORKING: + case AccountTask::STATE_SUCCEEDED: { + // Not reachable here, as they are not failures. + } + } + m_currentTask.reset(); +} + +void MinecraftAccount::fillSession(AuthSessionPtr session) +{ + // the user name. you have to have an user name + // FIXME: not with MSA + session->username = data.userName(); + // volatile auth token + session->access_token = data.accessToken(); + // the semi-permanent client token + session->client_token = data.clientToken(); + // profile name + session->player_name = data.profileName(); + // profile ID + session->uuid = data.profileId(); + // 'legacy' or 'mojang', depending on account type + session->user_type = typeString(); + if (!session->access_token.isEmpty()) + { + session->session = "token:" + data.accessToken() + ":" + data.profileId(); + } + else + { + session->session = "-"; + } + session->m_accountPtr = shared_from_this(); +} + +void MinecraftAccount::decrementUses() +{ + Usable::decrementUses(); + if(!isInUse()) + { + emit changed(); + // FIXME: we now need a better way to identify accounts... + qWarning() << "Profile" << data.profileId() << "is no longer in use."; + } +} + +void MinecraftAccount::incrementUses() +{ + bool wasInUse = isInUse(); + Usable::incrementUses(); + if(!wasInUse) + { + emit changed(); + // FIXME: we now need a better way to identify accounts... + qWarning() << "Profile" << data.profileId() << "is now in use."; + } +} diff --git a/api/logic/minecraft/auth/MojangAccount.h b/launcher/minecraft/auth/MinecraftAccount.h similarity index 51% rename from api/logic/minecraft/auth/MojangAccount.h rename to launcher/minecraft/auth/MinecraftAccount.h index 30a5f2ffb6..6dc3d1f814 100644 --- a/api/logic/minecraft/auth/MojangAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -21,19 +21,19 @@ #include #include #include +#include #include #include "AuthSession.h" #include "Usable.h" - -#include "multimc_logic_export.h" +#include "AccountData.h" class Task; -class YggdrasilTask; -class MojangAccount; +class AccountTask; +class MinecraftAccount; -typedef std::shared_ptr MojangAccountPtr; -Q_DECLARE_METATYPE(MojangAccountPtr) +typedef std::shared_ptr MinecraftAccountPtr; +Q_DECLARE_METATYPE(MinecraftAccountPtr) /** * A profile within someone's Mojang account. @@ -61,75 +61,101 @@ enum AccountStatus * 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 : +class MinecraftAccount : public QObject, public Usable, - public std::enable_shared_from_this + public std::enable_shared_from_this { Q_OBJECT public: /* construction */ //! Do not copy accounts. ever. - explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete; + explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete; //! Default constructor - explicit MojangAccount(QObject *parent = 0) : QObject(parent) {}; + explicit MinecraftAccount(QObject *parent = 0) : QObject(parent) {}; + + static MinecraftAccountPtr createFromUsername(const QString &username); + + //! Creates an offline account + static MinecraftAccountPtr createFromUsernameOffline(const QString &username); - //! Creates an empty account for the specified user name. - static MojangAccountPtr createFromUsername(const QString &username); + static MinecraftAccountPtr createBlankMSA(); - //! Loads a MojangAccount from the given JSON object. - static MojangAccountPtr loadFromJson(const QJsonObject &json); + static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); + static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); - //! Saves a MojangAccount to a JSON object and returns it. + //! Saves a MinecraftAccount 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()); - void invalidateClientToken(); + std::shared_ptr login(AuthSessionPtr session, QString password = QString()); + + std::shared_ptr loginMSA(AuthSessionPtr session); + + std::shared_ptr refresh(AuthSessionPtr session); public: /* queries */ - const QString &username() const - { - return m_username; + QString accountDisplayString() const { + return data.accountDisplayString(); + } + + QString mojangUserName() const { + return data.userName(); + } + + QString accessToken() const { + return data.accessToken(); } - const QString &clientToken() const - { - return m_clientToken; + QString profileId() const { + return data.profileId(); } - const QString &accessToken() const - { - return m_accessToken; + QString profileName() const { + return data.profileName(); } - const QList &profiles() const - { - return m_profiles; + bool canMigrate() const { + return data.canMigrateToMSA; } - const User &user() - { - return m_user; + bool isMSA() const { + return data.type == AccountType::MSA; } - //! Returns the currently selected profile (if none, returns nullptr) - const AccountProfile *currentProfile() const; + QString typeString() const { + switch(data.type) { + case AccountType::Mojang: { + if(data.legacy) { + return "legacy"; + } + return "mojang"; + } + break; + case AccountType::MSA: { + return "msa"; + } + break; + default: { + return "unknown"; + } + } + } + + QPixmap getFace() const; //! Returns whether the account is NotVerified, Verified or Online AccountStatus accountStatus() const; + AccountData * accountData() { + return &data; + } + signals: /** * This signal is emitted when the account changes @@ -139,27 +165,10 @@ class MULTIMC_LOGIC_EXPORT MojangAccount : // 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; + AccountData data; // current task we are executing here - std::shared_ptr m_currentTask; + std::shared_ptr m_currentTask; protected: /* methods */ @@ -173,10 +182,4 @@ private private: void fillSession(AuthSessionPtr session); - -public: - friend class YggdrasilTask; - friend class AuthenticateTask; - friend class ValidateTask; - friend class RefreshTask; }; diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp new file mode 100644 index 0000000000..b4db6c2d31 --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -0,0 +1,912 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include + +#include "AuthContext.h" +#include "katabasis/Globals.h" +#include "AuthRequest.h" + +#include "Secrets.h" + +#include "Env.h" + +using OAuth2 = Katabasis::OAuth2; +using Requestor = AuthRequest; +using Activity = Katabasis::Activity; + +AuthContext::AuthContext(AccountData * data, QObject *parent) : + AccountTask(data, parent) +{ +} + +void AuthContext::beginActivity(Activity activity) { + if(isBusy()) { + throw 0; + } + m_activity = activity; + changeState(STATE_WORKING, "Initializing"); + emit activityChanged(m_activity); +} + +void AuthContext::finishActivity() { + if(!isBusy()) { + throw 0; + } + m_activity = Katabasis::Activity::Idle; + setStage(AuthStage::Complete); + m_data->validity_ = m_data->minecraftProfile.validity; + emit activityChanged(m_activity); +} + +void AuthContext::initMSA() { + if(m_oauth2) { + return; + } + + auto clientId = Secrets::getMSAClientID('-'); + if(clientId.isEmpty()) { + return; + } + + Katabasis::OAuth2::Options opts; + opts.scope = "XboxLive.signin offline_access"; + opts.clientIdentifier = clientId; + opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; + + m_oauth2 = new OAuth2(opts, m_data->msaToken, this, &ENV.qnam()); + m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice); + + connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed); + connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); + connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); +} + +void AuthContext::initMojang() { + if(m_yggdrasil) { + return; + } + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed); + connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded); +} + +void AuthContext::onMojangSucceeded() { + doMinecraftProfile(); +} + + +void AuthContext::onMojangFailed() { + finishActivity(); + m_error = m_yggdrasil->m_error; + m_aborted = m_yggdrasil->m_aborted; + changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed.")); +} + +/* +bool AuthContext::signOut() { + if(isBusy()) { + return false; + } + + start(); + + beginActivity(Activity::LoggingOut); + m_oauth2->unlink(); + m_account = AccountData(); + finishActivity(); + return true; +} +*/ + +void AuthContext::onOAuthLinkingFailed() { + emit hideVerificationUriAndCode(); + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); +} + +void AuthContext::onOAuthLinkingSucceeded() { + emit hideVerificationUriAndCode(); + auto *o2t = qobject_cast(sender()); + if (!o2t->linked()) { + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).")); + return; + } + QVariantMap extraTokens = o2t->extraTokens(); +#ifndef NDEBUG + if (!extraTokens.isEmpty()) { + qDebug() << "Extra tokens in response:"; + foreach (QString key, extraTokens.keys()) { + qDebug() << "\t" << key << ":" << extraTokens.value(key); + } + } +#endif + doUserAuth(); +} + +void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { + // respond to activity change here +} + +void AuthContext::doUserAuth() { + setStage(AuthStage::UserAuth); + changeState(STATE_WORKING, tr("Starting user authentication")); + + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "AuthMethod": "RPS", + "SiteName": "user.auth.xboxlive.com", + "RpsTicket": "d=%1" + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT" +} +)XXX"; + auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token); + + QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + auto *requestor = new Requestor(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onUserAuthDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "First layer of XBox auth ... commencing."; +} + +namespace { +bool getDateTime(QJsonValue value, QDateTime & out) { + if(!value.isString()) { + return false; + } + out = QDateTime::fromString(value.toString(), Qt::ISODate); + return out.isValid(); +} + +bool getString(QJsonValue value, QString & out) { + if(!value.isString()) { + return false; + } + out = value.toString(); + return true; +} + +bool getNumber(QJsonValue value, double & out) { + if(!value.isDouble()) { + return false; + } + out = value.toDouble(); + return true; +} + +bool getNumber(QJsonValue value, int64_t & out) { + if(!value.isDouble()) { + return false; + } + out = (int64_t) value.toDouble(); + return true; +} + +bool getBool(QJsonValue value, bool & out) { + if(!value.isBool()) { + return false; + } + out = value.toBool(); + return true; +} + +/* +{ + "IssueInstant":"2020-12-07T19:52:08.4463796Z", + "NotAfter":"2020-12-21T19:52:08.4463796Z", + "Token":"token", + "DisplayClaims":{ + "xui":[ + { + "uhs":"userhash" + } + ] + } + } +*/ +// TODO: handle error responses ... +/* +{ + "Identity":"0", + "XErr":2148916238, + "Message":"", + "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" +} +// 2148916233 = missing XBox account +// 2148916238 = child account not linked to a family +*/ + +bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { + qDebug() << "Parsing" << name <<":"; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { + qWarning() << "User IssueInstant is not a timestamp"; + return false; + } + if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { + qWarning() << "User NotAfter is not a timestamp"; + return false; + } + if(!getString(obj.value("Token"), output.token)) { + qWarning() << "User Token is not a timestamp"; + return false; + } + auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); + if(!arrayVal.isArray()) { + qWarning() << "Missing xui claims array"; + return false; + } + bool foundUHS = false; + for(auto item: arrayVal.toArray()) { + if(!item.isObject()) { + continue; + } + auto obj = item.toObject(); + if(obj.contains("uhs")) { + foundUHS = true; + } else { + continue; + } + // consume all 'display claims' ... whatever that means + for(auto iter = obj.begin(); iter != obj.end(); iter++) { + QString claim; + if(!getString(obj.value(iter.key()), claim)) { + qWarning() << "display claim " << iter.key() << " is not a string..."; + return false; + } + output.extra[iter.key()] = claim; + } + + break; + } + if(!foundUHS) { + qWarning() << "Missing uhs"; + return false; + } + output.validity = Katabasis::Validity::Certain; + qDebug() << name << "is valid."; + return true; +} + +} + +void AuthContext::onUserAuthDone( + QNetworkReply::NetworkError error, + QByteArray replyData, + QList headers +) { + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + finishActivity(); + changeState(STATE_FAILED_HARD, tr("XBox user authentication failed.")); + return; + } + + Katabasis::Token temp; + if(!parseXTokenResponse(replyData, temp, "UToken")) { + qWarning() << "Could not parse user authentication response..."; + finishActivity(); + changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood.")); + return; + } + m_data->userToken = temp; + + setStage(AuthStage::XboxAuth); + changeState(STATE_WORKING, tr("Starting XBox authentication")); + + doSTSAuthMinecraft(); + doSTSAuthGeneric(); +} +/* + url = "https://xsts.auth.xboxlive.com/xsts/authorize" + headers = {"x-xbl-contract-version": "1"} + data = { + "RelyingParty": relying_party, + "TokenType": "JWT", + "Properties": { + "UserTokens": [self.user_token.token], + "SandboxId": "RETAIL", + }, + } +*/ +void AuthContext::doSTSAuthMinecraft() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": [ + "%1" + ] + }, + "RelyingParty": "rp://api.minecraftservices.com/", + "TokenType": "JWT" +} +)XXX"; + auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token); + + QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + Requestor *requestor = new Requestor(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "Getting Minecraft services STS token..."; +} + +void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList headers) { + if(error == QNetworkReply::AuthenticationRequiredError) { + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); + return; + } + + int64_t errorCode = -1; + auto obj = doc.object(); + if(!getNumber(obj.value("XErr"), errorCode)) { + qWarning() << "XErr is not a number"; + return; + } + stsErrors.insert(errorCode); + stsFailed = true; + } +} + + +void AuthContext::onSTSAuthMinecraftDone( + QNetworkReply::NetworkError error, + QByteArray replyData, + QList headers +) { +#ifndef NDEBUG + qDebug() << replyData; +#endif + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + processSTSError(error, replyData, headers); + failResult(m_mcAuthSucceeded); + return; + } + + Katabasis::Token temp; + if(!parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { + qWarning() << "Could not parse authorization response for access to mojang services..."; + failResult(m_mcAuthSucceeded); + return; + } + + if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { + qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; + failResult(m_mcAuthSucceeded); + return; + } + m_data->mojangservicesToken = temp; + + doMinecraftAuth(); +} + +void AuthContext::doMinecraftAuth() { + QString mc_auth_template = R"XXX( +{ + "identityToken": "XBL3.0 x=%1;%2" +} +)XXX"; + auto data = mc_auth_template.arg(m_data->mojangservicesToken.extra["uhs"].toString(), m_data->mojangservicesToken.token); + + QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + Requestor *requestor = new Requestor(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftAuthDone); + requestor->post(request, data.toUtf8()); + qDebug() << "Getting Minecraft access token..."; +} + +namespace { +bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { + QJsonParseError jsonError; + qDebug() << "Parsing Mojang response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from api.minecraftservices.com/authentication/login_with_xbox as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + double expires_in = 0; + if(!getNumber(obj.value("expires_in"), expires_in)) { + qWarning() << "expires_in is not a valid number"; + return false; + } + auto currentTime = QDateTime::currentDateTimeUtc(); + output.issueInstant = currentTime; + output.notAfter = currentTime.addSecs(expires_in); + + QString username; + if(!getString(obj.value("username"), username)) { + qWarning() << "username is not valid"; + return false; + } + + // TODO: it's a JWT... validate it? + if(!getString(obj.value("access_token"), output.token)) { + qWarning() << "access_token is not valid"; + return false; + } + output.validity = Katabasis::Validity::Certain; + qDebug() << "Mojang response is valid."; + return true; +} +} + +void AuthContext::onMinecraftAuthDone( + QNetworkReply::NetworkError error, + QByteArray replyData, + QList headers +) { + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; +#ifndef NDEBUG + qDebug() << replyData; +#endif + failResult(m_mcAuthSucceeded); + return; + } + + if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) { + qWarning() << "Could not parse login_with_xbox response..."; +#ifndef NDEBUG + qDebug() << replyData; +#endif + failResult(m_mcAuthSucceeded); + return; + } + + succeedResult(m_mcAuthSucceeded); +} + +void AuthContext::doSTSAuthGeneric() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": [ + "%1" + ] + }, + "RelyingParty": "http://xboxlive.com", + "TokenType": "JWT" +} +)XXX"; + auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token); + + QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + Requestor *requestor = new Requestor(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "Getting generic STS token..."; +} + +void AuthContext::onSTSAuthGenericDone( + QNetworkReply::NetworkError error, + QByteArray replyData, + QList headers +) { +#ifndef NDEBUG + qDebug() << replyData; +#endif + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + processSTSError(error, replyData, headers); + failResult(m_xboxProfileSucceeded); + return; + } + + Katabasis::Token temp; + if(!parseXTokenResponse(replyData, temp, "STSAuthGeneric")) { + qWarning() << "Could not parse authorization response for access to xbox API..."; + failResult(m_xboxProfileSucceeded); + return; + } + + if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { + qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING"; + failResult(m_xboxProfileSucceeded); + return; + } + m_data->xboxApiToken = temp; + + doXBoxProfile(); +} + +void AuthContext::doXBoxProfile() { + auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); + QUrlQuery q; + q.addQueryItem( + "settings", + "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," + "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," + "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," + "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined" + ); + url.setQuery(q); + + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("x-xbl-contract-version", "3"); + request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8()); + Requestor *requestor = new Requestor(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onXBoxProfileDone); + requestor->get(request); + qDebug() << "Getting Xbox profile..."; +} + +void AuthContext::onXBoxProfileDone( + QNetworkReply::NetworkError error, + QByteArray replyData, + QList headers +) { + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; +#ifndef NDEBUG + qDebug() << replyData; +#endif + failResult(m_xboxProfileSucceeded); + return; + } + +#ifndef NDEBUG + qDebug() << "XBox profile: " << replyData; +#endif + + succeedResult(m_xboxProfileSucceeded); +} + +void AuthContext::succeedResult(bool& flag) { + m_requestsDone ++; + flag = true; + checkResult(); +} + +void AuthContext::failResult(bool& flag) { + m_requestsDone ++; + flag = false; + checkResult(); +} + +void AuthContext::checkResult() { + qDebug() << "AuthContext::checkResult called"; + if(m_requestsDone != 2) { + qDebug() << "Number of ready results:" << m_requestsDone; + return; + } + if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { + doMinecraftProfile(); + } + else { + finishActivity(); + if(stsFailed) { + if(stsErrors.contains(2148916233)) { + changeState( + STATE_FAILED_HARD, + tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.") + .arg("minecraft.net") + ); + } + else if (stsErrors.contains(2148916235)){ + // NOTE: this is the Grulovia error + changeState( + STATE_FAILED_HARD, + tr("XBox Live is not available in your country. You've been blocked.") + ); + } + else if (stsErrors.contains(2148916238)){ + changeState( + STATE_FAILED_HARD, + tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") + .arg("help.minecraft.net") + ); + } + else { + QStringList errorList; + for(auto & error: stsErrors) { + errorList.append(QString::number(error)); + } + changeState( + STATE_FAILED_HARD, + tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorList.join("\n")) + ); + } + } + else { + changeState(STATE_FAILED_HARD, tr("XBox and/or Mojang authentication steps did not succeed")); + } + } +} + +namespace { +bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if(!getString(obj.value("id"), output.id)) { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if(!getString(obj.value("name"), output.name)) { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto skinsArray = obj.value("skins").toArray(); + for(auto skin: skinsArray) { + auto skinObj = skin.toObject(); + Skin skinOut; + if(!getString(skinObj.value("id"), skinOut.id)) { + continue; + } + QString state; + if(!getString(skinObj.value("state"), state)) { + continue; + } + if(state != "ACTIVE") { + continue; + } + if(!getString(skinObj.value("url"), skinOut.url)) { + continue; + } + if(!getString(skinObj.value("variant"), skinOut.variant)) { + continue; + } + // we deal with only the active skin + output.skin = skinOut; + break; + } + auto capesArray = obj.value("capes").toArray(); + + QString currentCape; + for(auto cape: capesArray) { + auto capeObj = cape.toObject(); + Cape capeOut; + if(!getString(capeObj.value("id"), capeOut.id)) { + continue; + } + QString state; + if(!getString(capeObj.value("state"), state)) { + continue; + } + if(state == "ACTIVE") { + currentCape = capeOut.id; + } + if(!getString(capeObj.value("url"), capeOut.url)) { + continue; + } + if(!getString(capeObj.value("alias"), capeOut.alias)) { + continue; + } + + output.capes[capeOut.id] = capeOut; + } + output.currentCape = currentCape; + output.validity = Katabasis::Validity::Certain; + return true; +} +} + +void AuthContext::doMinecraftProfile() { + setStage(AuthStage::MinecraftProfile); + changeState(STATE_WORKING, tr("Starting minecraft profile acquisition")); + + auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + // request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + Requestor *requestor = new Requestor(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onMinecraftProfileDone); + requestor->get(request); +} + +void AuthContext::onMinecraftProfileDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + m_data->minecraftProfile = MinecraftProfile(); + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Account is missing a Minecraft Java profile.\n\nWhile the Microsoft account is valid, it does not own the game.\n\nYou might own Bedrock on this account, but that does not give you access to Java currently.")); + return; + } + if (error != QNetworkReply::NoError) { + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); + return; + } + if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed")); + return; + } + + if(m_data->type == AccountType::Mojang) { + doMigrationEligibilityCheck(); + } + else { + doGetSkin(); + } +} + +void AuthContext::doMigrationEligibilityCheck() { + setStage(AuthStage::MigrationEligibility); + changeState(STATE_WORKING, tr("Starting check for migration eligibility")); + + auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + Requestor *requestor = new Requestor(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onMigrationEligibilityCheckDone); + requestor->get(request); +} + +bool parseRolloutResponse(QByteArray & data, bool& result) { + qDebug() << "Parsing Rollout response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if(!getString(obj.value("feature"), feature)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + if(feature != "msamigration") { + qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; + return false; + } + if(!getBool(obj.value("rollout"), result)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; +} + +void AuthContext::onMigrationEligibilityCheckDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + if (error == QNetworkReply::NoError) { + parseRolloutResponse(data, m_data->canMigrateToMSA); + } + doGetSkin(); +} + +void AuthContext::doGetSkin() { + setStage(AuthStage::Skin); + changeState(STATE_WORKING, tr("Fetching player skin")); + + auto url = QUrl(m_data->minecraftProfile.skin.url); + QNetworkRequest request = QNetworkRequest(url); + Requestor *requestor = new Requestor(this); + connect(requestor, &Requestor::finished, this, &AuthContext::onSkinDone); + requestor->get(request); +} + +void AuthContext::onSkinDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList +) { + if (error == QNetworkReply::NoError) { + m_data->minecraftProfile.skin.data = data; + } + m_data->validity_ = Katabasis::Validity::Certain; + finishActivity(); + changeState(STATE_SUCCEEDED, tr("Finished all authentication steps")); +} + +void AuthContext::setStage(AuthContext::AuthStage stage) { + m_stage = stage; + emit progress((int)m_stage, (int)AuthStage::Complete); +} + + +QString AuthContext::getStateMessage() const { + switch (m_accountState) + { + case STATE_WORKING: + switch(m_stage) { + case AuthStage::Initial: { + QString loginMessage = tr("Logging in as %1 user"); + if(m_data->type == AccountType::MSA) { + return loginMessage.arg("Microsoft"); + } + else { + return loginMessage.arg("Mojang"); + } + } + case AuthStage::UserAuth: + return tr("Logging in as XBox user"); + case AuthStage::XboxAuth: + return tr("Logging in with XBox and Mojang services"); + case AuthStage::MinecraftProfile: + return tr("Getting Minecraft profile"); + case AuthStage::MigrationEligibility: + return tr("Checking for migration eligibility"); + case AuthStage::Skin: + return tr("Getting Minecraft skin"); + case AuthStage::Complete: + return tr("Finished"); + default: + break; + } + default: + return AccountTask::getStateMessage(); + } +} diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h new file mode 100644 index 0000000000..dc7552acad --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include "Yggdrasil.h" +#include "../AccountData.h" +#include "../AccountTask.h" + +class AuthContext : public AccountTask +{ + Q_OBJECT + +public: + explicit AuthContext(AccountData * data, QObject *parent = 0); + + bool isBusy() { + return m_activity != Katabasis::Activity::Idle; + }; + Katabasis::Validity validity() { + return m_data->validity_; + }; + + //bool signOut(); + + QString getStateMessage() const override; + +signals: + void activityChanged(Katabasis::Activity activity); + +private slots: +// OAuth-specific callbacks + void onOAuthLinkingSucceeded(); + void onOAuthLinkingFailed(); + + void onOAuthActivityChanged(Katabasis::Activity activity); + +// Yggdrasil specific callbacks + void onMojangSucceeded(); + void onMojangFailed(); + +protected: + void initMSA(); + void initMojang(); + + void doUserAuth(); + Q_SLOT void onUserAuthDone(QNetworkReply::NetworkError, QByteArray, QList); + + void processSTSError(QNetworkReply::NetworkError, QByteArray, QList); + + void doSTSAuthMinecraft(); + Q_SLOT void onSTSAuthMinecraftDone(QNetworkReply::NetworkError, QByteArray, QList); + void doMinecraftAuth(); + Q_SLOT void onMinecraftAuthDone(QNetworkReply::NetworkError, QByteArray, QList); + + void doSTSAuthGeneric(); + Q_SLOT void onSTSAuthGenericDone(QNetworkReply::NetworkError, QByteArray, QList); + void doXBoxProfile(); + Q_SLOT void onXBoxProfileDone(QNetworkReply::NetworkError, QByteArray, QList); + + void doMinecraftProfile(); + Q_SLOT void onMinecraftProfileDone(QNetworkReply::NetworkError, QByteArray, QList); + + void doMigrationEligibilityCheck(); + Q_SLOT void onMigrationEligibilityCheckDone(QNetworkReply::NetworkError, QByteArray, QList); + + void doGetSkin(); + Q_SLOT void onSkinDone(QNetworkReply::NetworkError, QByteArray, QList); + + void failResult(bool & flag); + void succeedResult(bool & flag); + void checkResult(); + +protected: + void beginActivity(Katabasis::Activity activity); + void finishActivity(); + void clearTokens(); + +protected: + Katabasis::OAuth2 *m_oauth2 = nullptr; + Yggdrasil *m_yggdrasil = nullptr; + + int m_requestsDone = 0; + bool m_xboxProfileSucceeded = false; + bool m_mcAuthSucceeded = false; + + QSet stsErrors; + bool stsFailed = false; + + Katabasis::Activity m_activity = Katabasis::Activity::Idle; + enum class AuthStage { + Initial, + UserAuth, + XboxAuth, + MinecraftProfile, + MigrationEligibility, + Skin, + Complete + } m_stage = AuthStage::Initial; + + void setStage(AuthStage stage); +}; diff --git a/launcher/minecraft/auth/flows/AuthRequest.cpp b/launcher/minecraft/auth/flows/AuthRequest.cpp new file mode 100644 index 0000000000..77558fd3b3 --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthRequest.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include +#include +#include + +#include "AuthRequest.h" +#include "katabasis/Globals.h" +#include "Env.h" + +AuthRequest::AuthRequest(QObject *parent): QObject(parent) { +} + +AuthRequest::~AuthRequest() { +} + +void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { + setup(req, QNetworkAccessManager::GetOperation); + reply_ = ENV.qnam().get(request_); + status_ = Requesting; + timedReplies_.add(new Katabasis::Reply(reply_, timeout)); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); +} + +void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) { + setup(req, QNetworkAccessManager::PostOperation); + data_ = data; + status_ = Requesting; + reply_ = ENV.qnam().post(request_, data_); + timedReplies_.add(new Katabasis::Reply(reply_, timeout)); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); + connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); +} + +void AuthRequest::onRequestFinished() { + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + finish(); +} + +void AuthRequest::onRequestError(QNetworkReply::NetworkError error) { + qWarning() << "AuthRequest::onRequestError: Error" << (int)error; + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + qWarning() << "AuthRequest::onRequestError: Error string: " << reply_->errorString(); + int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + error_ = error; + + // QTimer::singleShot(10, this, SLOT(finish())); +} + +void AuthRequest::onSslErrors(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 AuthRequest::onUploadProgress(qint64 uploaded, qint64 total) { + if (status_ == Idle) { + qWarning() << "AuthRequest::onUploadProgress: No pending request"; + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + // Restart timeout because request in progress + Katabasis::Reply *o2Reply = timedReplies_.find(reply_); + if(o2Reply) { + o2Reply->start(); + } + emit uploadProgress(uploaded, total); +} + +void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation, const QByteArray &verb) { + request_ = req; + operation_ = operation; + url_ = req.url(); + + QUrl url = url_; + request_.setUrl(url); + + if (!verb.isEmpty()) { + request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb); + } + + status_ = Requesting; + error_ = QNetworkReply::NoError; +} + +void AuthRequest::finish() { + QByteArray data; + if (status_ == Idle) { + qWarning() << "AuthRequest::finish: No pending request"; + return; + } + data = reply_->readAll(); + status_ = Idle; + timedReplies_.remove(reply_); + reply_->disconnect(this); + reply_->deleteLater(); + QList headers = reply_->rawHeaderPairs(); + emit finished(error_, data, headers); +} diff --git a/launcher/minecraft/auth/flows/AuthRequest.h b/launcher/minecraft/auth/flows/AuthRequest.h new file mode 100644 index 0000000000..6a45a0bded --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthRequest.h @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include "katabasis/Reply.h" + +/// Makes authentication requests. +class AuthRequest: public QObject { + Q_OBJECT + +public: + explicit AuthRequest(QObject *parent = 0); + ~AuthRequest(); + +public slots: + void get(const QNetworkRequest &req, int timeout = 60*1000); + void post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); + + +signals: + + /// Emitted when a request has been completed or failed. + void finished(QNetworkReply::NetworkError error, QByteArray data, QList headers); + + /// Emitted when an upload has progressed. + void uploadProgress(qint64 bytesSent, qint64 bytesTotal); + +protected slots: + + /// Handle request finished. + void onRequestFinished(); + + /// Handle request error. + void onRequestError(QNetworkReply::NetworkError error); + + /// Handle ssl errors. + void onSslErrors(QList errors); + + /// Finish the request, emit finished() signal. + void finish(); + + /// Handle upload progress. + void onUploadProgress(qint64 uploaded, qint64 total); + +protected: + void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); + + enum Status { + Idle, Requesting, ReRequesting + }; + + QNetworkRequest request_; + QByteArray data_; + QNetworkReply *reply_; + Status status_; + QNetworkAccessManager::Operation operation_; + QUrl url_; + Katabasis::ReplyList timedReplies_; + QNetworkReply::NetworkError error_; +}; diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp new file mode 100644 index 0000000000..03beb2797b --- /dev/null +++ b/launcher/minecraft/auth/flows/MSAInteractive.cpp @@ -0,0 +1,20 @@ +#include "MSAInteractive.h" + +MSAInteractive::MSAInteractive(AccountData* data, QObject* parent) : AuthContext(data, parent) {} + +void MSAInteractive::executeTask() { + m_requestsDone = 0; + m_xboxProfileSucceeded = false; + m_mcAuthSucceeded = false; + + initMSA(); + + QVariantMap extraOpts; + extraOpts["prompt"] = "select_account"; + m_oauth2->setExtraRequestParams(extraOpts); + + beginActivity(Katabasis::Activity::LoggingIn); + m_oauth2->unlink(); + *m_data = AccountData(); + m_oauth2->link(); +} diff --git a/launcher/minecraft/auth/flows/MSAInteractive.h b/launcher/minecraft/auth/flows/MSAInteractive.h new file mode 100644 index 0000000000..9556f25484 --- /dev/null +++ b/launcher/minecraft/auth/flows/MSAInteractive.h @@ -0,0 +1,10 @@ +#pragma once +#include "AuthContext.h" + +class MSAInteractive : public AuthContext +{ + Q_OBJECT +public: + explicit MSAInteractive(AccountData * data, QObject *parent = 0); + void executeTask() override; +}; diff --git a/launcher/minecraft/auth/flows/MSASilent.cpp b/launcher/minecraft/auth/flows/MSASilent.cpp new file mode 100644 index 0000000000..8ce43c1f79 --- /dev/null +++ b/launcher/minecraft/auth/flows/MSASilent.cpp @@ -0,0 +1,16 @@ +#include "MSASilent.h" + +MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthContext(data, parent) {} + +void MSASilent::executeTask() { + m_requestsDone = 0; + m_xboxProfileSucceeded = false; + m_mcAuthSucceeded = false; + + initMSA(); + + beginActivity(Katabasis::Activity::Refreshing); + if(!m_oauth2->refresh()) { + finishActivity(); + } +} diff --git a/launcher/minecraft/auth/flows/MSASilent.h b/launcher/minecraft/auth/flows/MSASilent.h new file mode 100644 index 0000000000..e1b3d43d8a --- /dev/null +++ b/launcher/minecraft/auth/flows/MSASilent.h @@ -0,0 +1,10 @@ +#pragma once +#include "AuthContext.h" + +class MSASilent : public AuthContext +{ + Q_OBJECT +public: + explicit MSASilent(AccountData * data, QObject *parent = 0); + void executeTask() override; +}; diff --git a/launcher/minecraft/auth/flows/MojangLogin.cpp b/launcher/minecraft/auth/flows/MojangLogin.cpp new file mode 100644 index 0000000000..cca911b5ef --- /dev/null +++ b/launcher/minecraft/auth/flows/MojangLogin.cpp @@ -0,0 +1,14 @@ +#include "MojangLogin.h" + +MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthContext(data, parent), m_password(password) {} + +void MojangLogin::executeTask() { + m_requestsDone = 0; + m_xboxProfileSucceeded = false; + m_mcAuthSucceeded = false; + + initMojang(); + + beginActivity(Katabasis::Activity::LoggingIn); + m_yggdrasil->login(m_password); +} diff --git a/launcher/minecraft/auth/flows/MojangLogin.h b/launcher/minecraft/auth/flows/MojangLogin.h new file mode 100644 index 0000000000..2e765ae89d --- /dev/null +++ b/launcher/minecraft/auth/flows/MojangLogin.h @@ -0,0 +1,13 @@ +#pragma once +#include "AuthContext.h" + +class MojangLogin : public AuthContext +{ + Q_OBJECT +public: + explicit MojangLogin(AccountData * data, QString password, QObject *parent = 0); + void executeTask() override; + +private: + QString m_password; +}; diff --git a/launcher/minecraft/auth/flows/MojangRefresh.cpp b/launcher/minecraft/auth/flows/MojangRefresh.cpp new file mode 100644 index 0000000000..af99175c7c --- /dev/null +++ b/launcher/minecraft/auth/flows/MojangRefresh.cpp @@ -0,0 +1,14 @@ +#include "MojangRefresh.h" + +MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthContext(data, parent) {} + +void MojangRefresh::executeTask() { + m_requestsDone = 0; + m_xboxProfileSucceeded = false; + m_mcAuthSucceeded = false; + + initMojang(); + + beginActivity(Katabasis::Activity::Refreshing); + m_yggdrasil->refresh(); +} diff --git a/launcher/minecraft/auth/flows/MojangRefresh.h b/launcher/minecraft/auth/flows/MojangRefresh.h new file mode 100644 index 0000000000..fb4facd522 --- /dev/null +++ b/launcher/minecraft/auth/flows/MojangRefresh.h @@ -0,0 +1,10 @@ +#pragma once +#include "AuthContext.h" + +class MojangRefresh : public AuthContext +{ + Q_OBJECT +public: + explicit MojangRefresh(AccountData * data, QObject *parent = 0); + void executeTask() override; +}; diff --git a/launcher/minecraft/auth/flows/Yggdrasil.cpp b/launcher/minecraft/auth/flows/Yggdrasil.cpp new file mode 100644 index 0000000000..c2935d052b --- /dev/null +++ b/launcher/minecraft/auth/flows/Yggdrasil.cpp @@ -0,0 +1,348 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Yggdrasil.h" +#include "../AccountData.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) + : AccountTask(data, parent) +{ + changeState(STATE_CREATED); +} + +void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { + changeState(STATE_WORKING); + + QNetworkRequest netRequest(endpoint); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + m_netReply = ENV.qnam().post(netRequest, content); + connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply); + connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers); + connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers); + connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::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, &Yggdrasil::abortByTimeout); + connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat); +} + +void Yggdrasil::executeTask() { +} + +void Yggdrasil::refresh() { + start(); + /* + * { + * "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_data->clientToken()); + req.insert("accessToken", m_data->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", false); + QJsonDocument doc(req); + + QUrl reqUrl(BuildConfig.AUTH_BASE + "refresh"); + QByteArray requestData = doc.toJson(); + + sendRequest(reqUrl, requestData); +} + +void Yggdrasil::login(QString password) { + start(); + /* + * { + * "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_data->userName()); + req.insert("password", password); + req.insert("requestUser", false); + + // If we already have a client token, give it to the server. + // Otherwise, let the server give us one. + + m_data->generateClientTokenIfMissing(); + req.insert("clientToken", m_data->clientToken()); + + QJsonDocument doc(req); + + QUrl reqUrl(BuildConfig.AUTH_BASE + "authenticate"); + QNetworkRequest netRequest(reqUrl); + QByteArray requestData = doc.toJson(); + + sendRequest(reqUrl, requestData); +} + + + +void Yggdrasil::refreshTimers(qint64, qint64) +{ + timeout_keeper.stop(); + timeout_keeper.start(timeout_max); + progress(count = 0, timeout_max); +} +void Yggdrasil::heartbeat() +{ + count += time_step; + progress(count, timeout_max); +} + +bool Yggdrasil::abort() +{ + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = Yggdrasil::BY_USER; + m_netReply->abort(); + return true; +} + +void Yggdrasil::abortByTimeout() +{ + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = Yggdrasil::BY_TIMEOUT; + m_netReply->abort(); +} + +void Yggdrasil::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 Yggdrasil::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_data->clientToken().isEmpty()) { + m_data->setClientToken(clientToken); + } + else if(clientToken != m_data->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 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_data->yggdrasilToken.token = accessToken; + m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; + + // 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); +} + +void Yggdrasil::processReply() +{ + changeState(STATE_WORKING); + + 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::ContentAccessDenied: + case QNetworkReply::ContentOperationNotPermittedError: + break; + case QNetworkReply::ContentGoneError: { + changeState( + STATE_FAILED_GONE, + tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") + ); + } + 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 Yggdrasil::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.")); + } +} diff --git a/launcher/minecraft/auth/flows/Yggdrasil.h b/launcher/minecraft/auth/flows/Yggdrasil.h new file mode 100644 index 0000000000..e709cb9f27 --- /dev/null +++ b/launcher/minecraft/auth/flows/Yggdrasil.h @@ -0,0 +1,82 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../AccountTask.h" + +#include +#include +#include +#include + +#include "../MinecraftAccount.h" + +class QNetworkReply; + +/** + * A Yggdrasil task is a task that performs an operation on a given mojang account. + */ +class Yggdrasil : public AccountTask +{ + Q_OBJECT +public: + explicit Yggdrasil(AccountData * data, QObject *parent = 0); + virtual ~Yggdrasil() {}; + + void refresh(); + void login(QString password); +protected: + void executeTask() override; + + /** + * Processes the response received from the server. + * If an error occurred, this should emit a failed signal. + * 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. + */ + void processResponse(QJsonObject responseData); + + /** + * 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); + +protected slots: + void processReply(); + void refreshTimers(qint64, qint64); + void heartbeat(); + void sslErrors(QList); + void abortByTimeout(); + +public slots: + virtual bool abort() override; + +private: + void sendRequest(QUrl endpoint, QByteArray content); + +protected: + QNetworkReply *m_netReply = nullptr; + QTimer timeout_keeper; + QTimer counter; + int count = 0; // num msec since time reset + + const int timeout_max = 30000; + const int time_step = 50; +}; diff --git a/api/logic/minecraft/gameoptions/GameOptions.cpp b/launcher/minecraft/gameoptions/GameOptions.cpp similarity index 100% rename from api/logic/minecraft/gameoptions/GameOptions.cpp rename to launcher/minecraft/gameoptions/GameOptions.cpp diff --git a/api/logic/minecraft/gameoptions/GameOptions.h b/launcher/minecraft/gameoptions/GameOptions.h similarity index 100% rename from api/logic/minecraft/gameoptions/GameOptions.h rename to launcher/minecraft/gameoptions/GameOptions.h diff --git a/api/logic/minecraft/launch/ClaimAccount.cpp b/launcher/minecraft/launch/ClaimAccount.cpp similarity index 100% rename from api/logic/minecraft/launch/ClaimAccount.cpp rename to launcher/minecraft/launch/ClaimAccount.cpp diff --git a/api/logic/minecraft/launch/ClaimAccount.h b/launcher/minecraft/launch/ClaimAccount.h similarity index 92% rename from api/logic/minecraft/launch/ClaimAccount.h rename to launcher/minecraft/launch/ClaimAccount.h index c5bd75f38e..cb4de23f55 100644 --- a/api/logic/minecraft/launch/ClaimAccount.h +++ b/launcher/minecraft/launch/ClaimAccount.h @@ -16,7 +16,7 @@ #pragma once #include -#include +#include class ClaimAccount: public LaunchStep { @@ -33,5 +33,5 @@ class ClaimAccount: public LaunchStep } private: std::unique_ptr lock; - MojangAccountPtr m_account; + MinecraftAccountPtr m_account; }; diff --git a/api/logic/minecraft/launch/CreateGameFolders.cpp b/launcher/minecraft/launch/CreateGameFolders.cpp similarity index 93% rename from api/logic/minecraft/launch/CreateGameFolders.cpp rename to launcher/minecraft/launch/CreateGameFolders.cpp index 415b7e232a..4081e72eda 100644 --- a/api/logic/minecraft/launch/CreateGameFolders.cpp +++ b/launcher/minecraft/launch/CreateGameFolders.cpp @@ -15,7 +15,7 @@ void CreateGameFolders::executeTask() if(!FS::ensureFolderPathExists(minecraftInstance->gameRoot())) { emit logLine("Couldn't create the main game folder", MessageLevel::Error); - emitFailed("Couldn't create the main game folder"); + emitFailed(tr("Couldn't create the main game folder")); return; } diff --git a/api/logic/minecraft/launch/CreateGameFolders.h b/launcher/minecraft/launch/CreateGameFolders.h similarity index 100% rename from api/logic/minecraft/launch/CreateGameFolders.h rename to launcher/minecraft/launch/CreateGameFolders.h diff --git a/api/logic/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp similarity index 87% rename from api/logic/minecraft/launch/DirectJavaLaunch.cpp rename to launcher/minecraft/launch/DirectJavaLaunch.cpp index d9841a43e3..2110384fba 100644 --- a/api/logic/minecraft/launch/DirectJavaLaunch.cpp +++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp @@ -55,7 +55,7 @@ void DirectJavaLaunch::executeTask() // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); - auto mcArgs = minecraftInstance->processMinecraftArgs(m_session); + auto mcArgs = minecraftInstance->processMinecraftArgs(m_session, m_serverToJoin); args.append(mcArgs); QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); @@ -66,9 +66,9 @@ void DirectJavaLaunch::executeTask() auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); if (realWrapperCommand.isEmpty()) { - QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); return; } emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::MultiMC); @@ -87,18 +87,17 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) { case LoggedProcess::FailedToStart: { - //: Error message displayed if instace can't start - QString reason = tr("Could not launch minecraft!"); + //: Error message displayed if instance can't start + const char *reason = QT_TR_NOOP("Could not launch minecraft!"); emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + emitFailed(tr(reason)); return; } case LoggedProcess::Aborted: case LoggedProcess::Crashed: - { m_parent->setPid(-1); - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } case LoggedProcess::Finished: @@ -108,7 +107,7 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) auto exitCode = m_process.exitCode(); if(exitCode != 0) { - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } //FIXME: make this work again @@ -118,7 +117,7 @@ void DirectJavaLaunch::on_state(LoggedProcess::State state) break; } case LoggedProcess::Running: - emit logLine(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); m_parent->setPid(m_process.processId()); m_parent->instance()->setLastLaunch(); break; diff --git a/api/logic/minecraft/launch/DirectJavaLaunch.h b/launcher/minecraft/launch/DirectJavaLaunch.h similarity index 86% rename from api/logic/minecraft/launch/DirectJavaLaunch.h rename to launcher/minecraft/launch/DirectJavaLaunch.h index d9d9bdc228..58b119b8cc 100644 --- a/api/logic/minecraft/launch/DirectJavaLaunch.h +++ b/launcher/minecraft/launch/DirectJavaLaunch.h @@ -19,6 +19,8 @@ #include #include +#include "MinecraftServerTarget.h" + class DirectJavaLaunch: public LaunchStep { Q_OBJECT @@ -38,6 +40,12 @@ class DirectJavaLaunch: public LaunchStep { m_session = session; } + + void setServerToJoin(MinecraftServerTargetPtr serverToJoin) + { + m_serverToJoin = std::move(serverToJoin); + } + private slots: void on_state(LoggedProcess::State state); @@ -45,5 +53,6 @@ private slots: LoggedProcess m_process; QString m_command; AuthSessionPtr m_session; + MinecraftServerTargetPtr m_serverToJoin; }; diff --git a/api/logic/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp similarity index 92% rename from api/logic/minecraft/launch/ExtractNatives.cpp rename to launcher/minecraft/launch/ExtractNatives.cpp index d41cb8fdc9..d57499aa23 100644 --- a/api/logic/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -94,9 +94,9 @@ void ExtractNatives::executeTask() { if(!unzipNatives(source, outputPath, jniHackEnabled, nativeOpenAL, nativeGLFW)) { - auto reason = tr("Couldn't extract native jar '%1' to destination '%2'").arg(source, outputPath); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); + emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); + emitFailed(tr(reason).arg(source, outputPath)); } } emitSucceeded(); diff --git a/api/logic/minecraft/launch/ExtractNatives.h b/launcher/minecraft/launch/ExtractNatives.h similarity index 100% rename from api/logic/minecraft/launch/ExtractNatives.h rename to launcher/minecraft/launch/ExtractNatives.h diff --git a/api/logic/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp similarity index 91% rename from api/logic/minecraft/launch/LauncherPartLaunch.cpp rename to launcher/minecraft/launch/LauncherPartLaunch.cpp index 1408e6add6..ee469770a3 100644 --- a/api/logic/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -59,7 +59,7 @@ void LauncherPartLaunch::executeTask() auto instance = m_parent->instance(); std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - m_launchScript = minecraftInstance->createLaunchScript(m_session); + m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin); QStringList args = minecraftInstance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::MultiMC); @@ -118,9 +118,9 @@ void LauncherPartLaunch::executeTask() auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); if (realWrapperCommand.isEmpty()) { - QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand); - emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); return; } emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::MultiMC); @@ -140,17 +140,16 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) case LoggedProcess::FailedToStart: { //: Error message displayed if instace can't start - QString reason = tr("Could not launch minecraft!"); + const char *reason = QT_TR_NOOP("Could not launch minecraft!"); emit logLine(reason, MessageLevel::Fatal); - emitFailed(reason); + emitFailed(tr(reason)); return; } case LoggedProcess::Aborted: case LoggedProcess::Crashed: - { m_parent->setPid(-1); - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } case LoggedProcess::Finished: @@ -160,7 +159,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) auto exitCode = m_process.exitCode(); if(exitCode != 0) { - emitFailed("Game crashed."); + emitFailed(tr("Game crashed.")); return; } //FIXME: make this work again @@ -170,7 +169,7 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) break; } case LoggedProcess::Running: - emit logLine(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); m_parent->setPid(m_process.processId()); m_parent->instance()->setLastLaunch(); // send the launch script to the launcher part diff --git a/api/logic/minecraft/launch/LauncherPartLaunch.h b/launcher/minecraft/launch/LauncherPartLaunch.h similarity index 86% rename from api/logic/minecraft/launch/LauncherPartLaunch.h rename to launcher/minecraft/launch/LauncherPartLaunch.h index 9e951849e8..6a7ee0e5fb 100644 --- a/api/logic/minecraft/launch/LauncherPartLaunch.h +++ b/launcher/minecraft/launch/LauncherPartLaunch.h @@ -19,6 +19,8 @@ #include #include +#include "MinecraftServerTarget.h" + class LauncherPartLaunch: public LaunchStep { Q_OBJECT @@ -39,6 +41,11 @@ class LauncherPartLaunch: public LaunchStep m_session = session; } + void setServerToJoin(MinecraftServerTargetPtr serverToJoin) + { + m_serverToJoin = std::move(serverToJoin); + } + private slots: void on_state(LoggedProcess::State state); @@ -47,5 +54,7 @@ private slots: QString m_command; AuthSessionPtr m_session; QString m_launchScript; + MinecraftServerTargetPtr m_serverToJoin; + bool mayProceed = false; }; diff --git a/launcher/minecraft/launch/MinecraftServerTarget.cpp b/launcher/minecraft/launch/MinecraftServerTarget.cpp new file mode 100644 index 0000000000..569273b62a --- /dev/null +++ b/launcher/minecraft/launch/MinecraftServerTarget.cpp @@ -0,0 +1,66 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MinecraftServerTarget.h" + +#include + +MinecraftServerTarget MinecraftServerTarget::parse(const QString &fullAddress) { + QStringList split = fullAddress.split(":"); + + // The logic below replicates the exact logic minecraft uses for parsing server addresses. + // While the conversion is not lossless and eats errors, it ensures the same behavior + // within Minecraft and MultiMC when entering server addresses. + if (fullAddress.startsWith("[")) + { + int bracket = fullAddress.indexOf("]"); + if (bracket > 0) + { + QString ipv6 = fullAddress.mid(1, bracket - 1); + QString port = fullAddress.mid(bracket + 1).trimmed(); + + if (port.startsWith(":") && !ipv6.isEmpty()) + { + port = port.mid(1); + split = QStringList({ ipv6, port }); + } + else + { + split = QStringList({ipv6}); + } + } + } + + if (split.size() > 2) + { + split = QStringList({fullAddress}); + } + + QString realAddress = split[0]; + + quint16 realPort = 25565; + if (split.size() > 1) + { + bool ok; + realPort = split[1].toUInt(&ok); + + if (!ok) + { + realPort = 25565; + } + } + + return MinecraftServerTarget { realAddress, realPort }; +} diff --git a/api/logic/minecraft/auth/flows/ValidateTask.h b/launcher/minecraft/launch/MinecraftServerTarget.h similarity index 51% rename from api/logic/minecraft/auth/flows/ValidateTask.h rename to launcher/minecraft/launch/MinecraftServerTarget.h index 986c2e9f91..a402421af5 100644 --- a/api/logic/minecraft/auth/flows/ValidateTask.h +++ b/launcher/minecraft/launch/MinecraftServerTarget.h @@ -13,35 +13,17 @@ * limitations under the License. */ -/* - * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME: - */ - #pragma once -#include "../YggdrasilTask.h" +#include -#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; +struct MinecraftServerTarget { + QString address; + quint16 port; - virtual QString getEndpoint() const override; - - virtual void processResponse(QJsonObject responseData) override; - - virtual QString getStateMessage() const override; - -private: + static MinecraftServerTarget parse(const QString &fullAddress); }; + +typedef std::shared_ptr MinecraftServerTargetPtr; diff --git a/api/logic/minecraft/launch/ModMinecraftJar.cpp b/launcher/minecraft/launch/ModMinecraftJar.cpp similarity index 100% rename from api/logic/minecraft/launch/ModMinecraftJar.cpp rename to launcher/minecraft/launch/ModMinecraftJar.cpp diff --git a/api/logic/minecraft/launch/ModMinecraftJar.h b/launcher/minecraft/launch/ModMinecraftJar.h similarity index 100% rename from api/logic/minecraft/launch/ModMinecraftJar.h rename to launcher/minecraft/launch/ModMinecraftJar.h diff --git a/api/logic/minecraft/launch/PrintInstanceInfo.cpp b/launcher/minecraft/launch/PrintInstanceInfo.cpp similarity index 96% rename from api/logic/minecraft/launch/PrintInstanceInfo.cpp rename to launcher/minecraft/launch/PrintInstanceInfo.cpp index af0b5bbf5a..0b9611adba 100644 --- a/api/logic/minecraft/launch/PrintInstanceInfo.cpp +++ b/launcher/minecraft/launch/PrintInstanceInfo.cpp @@ -101,6 +101,6 @@ void PrintInstanceInfo::executeTask() #endif logLines(log, MessageLevel::MultiMC); - logLines(instance->verboseDescription(m_session), MessageLevel::MultiMC); + logLines(instance->verboseDescription(m_session, m_serverToJoin), MessageLevel::MultiMC); emitSucceeded(); } diff --git a/api/logic/minecraft/launch/PrintInstanceInfo.h b/launcher/minecraft/launch/PrintInstanceInfo.h similarity index 82% rename from api/logic/minecraft/launch/PrintInstanceInfo.h rename to launcher/minecraft/launch/PrintInstanceInfo.h index 168eff9dcb..fdc30f3168 100644 --- a/api/logic/minecraft/launch/PrintInstanceInfo.h +++ b/launcher/minecraft/launch/PrintInstanceInfo.h @@ -18,13 +18,15 @@ #include #include #include "minecraft/auth/AuthSession.h" +#include "minecraft/launch/MinecraftServerTarget.h" // FIXME: temporary wrapper for existing task. class PrintInstanceInfo: public LaunchStep { Q_OBJECT public: - explicit PrintInstanceInfo(LaunchTask *parent, AuthSessionPtr session) : LaunchStep(parent), m_session(session) {}; + explicit PrintInstanceInfo(LaunchTask *parent, AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) : + LaunchStep(parent), m_session(session), m_serverToJoin(serverToJoin) {}; virtual ~PrintInstanceInfo(){}; virtual void executeTask(); @@ -34,5 +36,6 @@ class PrintInstanceInfo: public LaunchStep } private: AuthSessionPtr m_session; + MinecraftServerTargetPtr m_serverToJoin; }; diff --git a/api/logic/minecraft/launch/ReconstructAssets.cpp b/launcher/minecraft/launch/ReconstructAssets.cpp similarity index 100% rename from api/logic/minecraft/launch/ReconstructAssets.cpp rename to launcher/minecraft/launch/ReconstructAssets.cpp diff --git a/api/logic/minecraft/launch/ReconstructAssets.h b/launcher/minecraft/launch/ReconstructAssets.h similarity index 100% rename from api/logic/minecraft/launch/ReconstructAssets.h rename to launcher/minecraft/launch/ReconstructAssets.h diff --git a/api/logic/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp similarity index 100% rename from api/logic/minecraft/launch/ScanModFolders.cpp rename to launcher/minecraft/launch/ScanModFolders.cpp diff --git a/api/logic/minecraft/launch/ScanModFolders.h b/launcher/minecraft/launch/ScanModFolders.h similarity index 100% rename from api/logic/minecraft/launch/ScanModFolders.h rename to launcher/minecraft/launch/ScanModFolders.h diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp new file mode 100644 index 0000000000..657669af04 --- /dev/null +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -0,0 +1,34 @@ +#include "VerifyJavaInstall.h" + +#include +#include +#include +#include + +void VerifyJavaInstall::executeTask() { + auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + + auto javaVersion = m_inst->getJavaVersion(); + auto minecraftComponent = m_inst->getPackProfile()->getComponent("net.minecraft"); + + // Java 16 requirement + if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate) { + if (javaVersion.major() < 16) { + emit logLine("Minecraft 21w19a and above require the use of Java 16", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 21w19a and above require the use of Java 16")); + return; + } + } + // Java 8 requirement + else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java8BeginsDate) { + if (javaVersion.major() < 8) { + emit logLine("Minecraft 17w13a and above require the use of Java 8", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 17w13a and above require the use of Java 8")); + return; + } + } + + emitSucceeded(); +} diff --git a/launcher/minecraft/launch/VerifyJavaInstall.h b/launcher/minecraft/launch/VerifyJavaInstall.h new file mode 100644 index 0000000000..a553106d1f --- /dev/null +++ b/launcher/minecraft/launch/VerifyJavaInstall.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +class VerifyJavaInstall : public LaunchStep { + Q_OBJECT + +public: + explicit VerifyJavaInstall(LaunchTask *parent) : LaunchStep(parent) { + }; + ~VerifyJavaInstall() override = default; + + void executeTask() override; + bool canAbort() const override { + return false; + } +}; diff --git a/api/logic/minecraft/legacy/LegacyInstance.cpp b/launcher/minecraft/legacy/LegacyInstance.cpp similarity index 99% rename from api/logic/minecraft/legacy/LegacyInstance.cpp rename to launcher/minecraft/legacy/LegacyInstance.cpp index f86e5d0517..9f9bda5a26 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.cpp +++ b/launcher/minecraft/legacy/LegacyInstance.cpp @@ -225,7 +225,7 @@ QString LegacyInstance::getStatusbarDescription() return tr("Instance from previous versions."); } -QStringList LegacyInstance::verboseDescription(AuthSessionPtr session) +QStringList LegacyInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) { QStringList out; diff --git a/api/logic/minecraft/legacy/LegacyInstance.h b/launcher/minecraft/legacy/LegacyInstance.h similarity index 92% rename from api/logic/minecraft/legacy/LegacyInstance.h rename to launcher/minecraft/legacy/LegacyInstance.h index 9caddcba73..ac2a8543fd 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.h +++ b/launcher/minecraft/legacy/LegacyInstance.h @@ -18,8 +18,6 @@ #include "BaseInstance.h" #include "launch/LaunchTask.h" -#include "multimc_logic_export.h" - class ModFolderModel; class LegacyModList; class WorldList; @@ -27,7 +25,7 @@ class Task; /* * WHY: Legacy instances - from MultiMC 3 and 4 - are here only to provide a way to upgrade them to the current format. */ -class MULTIMC_LOGIC_EXPORT LegacyInstance : public BaseInstance +class LegacyInstance : public BaseInstance { Q_OBJECT public: @@ -111,7 +109,8 @@ class MULTIMC_LOGIC_EXPORT LegacyInstance : public BaseInstance { return false; } - shared_qobject_ptr createLaunchTask(AuthSessionPtr account) override + shared_qobject_ptr createLaunchTask( + AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override { return nullptr; } @@ -125,7 +124,7 @@ class MULTIMC_LOGIC_EXPORT LegacyInstance : public BaseInstance } QString getStatusbarDescription() override; - QStringList verboseDescription(AuthSessionPtr session) override; + QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; QProcessEnvironment createEnvironment() override { diff --git a/api/logic/minecraft/legacy/LegacyModList.cpp b/launcher/minecraft/legacy/LegacyModList.cpp similarity index 99% rename from api/logic/minecraft/legacy/LegacyModList.cpp rename to launcher/minecraft/legacy/LegacyModList.cpp index 7301eb8c22..e9948ab14b 100644 --- a/api/logic/minecraft/legacy/LegacyModList.cpp +++ b/launcher/minecraft/legacy/LegacyModList.cpp @@ -22,7 +22,7 @@ LegacyModList::LegacyModList(const QString &dir, const QString &list_file) : m_dir(dir), m_list_file(list_file) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); } diff --git a/api/logic/minecraft/legacy/LegacyModList.h b/launcher/minecraft/legacy/LegacyModList.h similarity index 93% rename from api/logic/minecraft/legacy/LegacyModList.h rename to launcher/minecraft/legacy/LegacyModList.h index 8881d471fd..fade736ed7 100644 --- a/api/logic/minecraft/legacy/LegacyModList.h +++ b/launcher/minecraft/legacy/LegacyModList.h @@ -19,9 +19,7 @@ #include #include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT LegacyModList +class LegacyModList { public: diff --git a/api/logic/minecraft/legacy/LegacyUpgradeTask.cpp b/launcher/minecraft/legacy/LegacyUpgradeTask.cpp similarity index 100% rename from api/logic/minecraft/legacy/LegacyUpgradeTask.cpp rename to launcher/minecraft/legacy/LegacyUpgradeTask.cpp diff --git a/api/logic/minecraft/legacy/LegacyUpgradeTask.h b/launcher/minecraft/legacy/LegacyUpgradeTask.h similarity index 84% rename from api/logic/minecraft/legacy/LegacyUpgradeTask.h rename to launcher/minecraft/legacy/LegacyUpgradeTask.h index e35e43b7dc..542e17b8d0 100644 --- a/api/logic/minecraft/legacy/LegacyUpgradeTask.h +++ b/launcher/minecraft/legacy/LegacyUpgradeTask.h @@ -1,7 +1,6 @@ #pragma once #include "InstanceTask.h" -#include "multimc_logic_export.h" #include "net/NetJob.h" #include #include @@ -11,7 +10,7 @@ #include "BaseInstance.h" -class MULTIMC_LOGIC_EXPORT LegacyUpgradeTask : public InstanceTask +class LegacyUpgradeTask : public InstanceTask { Q_OBJECT public: diff --git a/api/logic/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp similarity index 61% rename from api/logic/minecraft/mod/LocalModParseTask.cpp rename to launcher/minecraft/mod/LocalModParseTask.cpp index 22ebd7d4ce..0d6972fbbc 100644 --- a/api/logic/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "settings/INIFile.h" #include "FileSystem.h" @@ -90,6 +91,124 @@ std::shared_ptr ReadMCModInfo(QByteArray contents) return nullptr; } +// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md +std::shared_ptr ReadMCModTOML(QByteArray contents) +{ + std::shared_ptr details = std::make_shared(); + + char errbuf[200]; + // top-level table + toml_table_t* tomlData = toml_parse(contents.data(), errbuf, sizeof(errbuf)); + + if(!tomlData) + { + return nullptr; + } + + // array defined by [[mods]] + toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods"); + // we only really care about the first element, since multiple mods in one file is not supported by us at the moment + toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0); + + // mandatory properties - always in [[mods]] + toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId"); + if(modIdDatum.ok) + { + details->mod_id = modIdDatum.u.s; + // library says this is required for strings + free(modIdDatum.u.s); + } + toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version"); + if(versionDatum.ok) + { + details->version = versionDatum.u.s; + free(versionDatum.u.s); + } + toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName"); + if(displayNameDatum.ok) + { + details->name = displayNameDatum.u.s; + free(displayNameDatum.u.s); + } + toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description"); + if(descriptionDatum.ok) + { + details->description = descriptionDatum.u.s; + free(descriptionDatum.u.s); + } + + // optional properties - can be in the root table or [[mods]] + toml_datum_t authorsDatum = toml_string_in(tomlData, "authors"); + QString authors = ""; + if(authorsDatum.ok) + { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + else + { + authorsDatum = toml_string_in(tomlModsTable0, "authors"); + if(authorsDatum.ok) + { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + } + if(!authors.isEmpty()) + { + // author information is stored as a string now, not a list + details->authors.append(authors); + } + // is credits even used anywhere? including this for completion/parity with old data version + toml_datum_t creditsDatum = toml_string_in(tomlData, "credits"); + QString credits = ""; + if(creditsDatum.ok) + { + authors = creditsDatum.u.s; + free(creditsDatum.u.s); + } + else + { + creditsDatum = toml_string_in(tomlModsTable0, "credits"); + if(creditsDatum.ok) + { + credits = creditsDatum.u.s; + free(creditsDatum.u.s); + } + } + details->credits = credits; + toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); + QString homeurl = ""; + if(homeurlDatum.ok) + { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + else + { + homeurlDatum = toml_string_in(tomlModsTable0, "displayURL"); + if(homeurlDatum.ok) + { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + } + if(!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + + // this seems to be recursive, so it should free everything + toml_free(tomlData); + + return details; +} + // https://fabricmc.net/wiki/documentation:fabric_mod_json std::shared_ptr ReadFabricModInfo(QByteArray contents) { @@ -198,7 +317,57 @@ void LocalModParseTask::processAsZip() QuaZipFile file(&zip); - if (zip.setCurrentFile("mcmod.info")) + if (zip.setCurrentFile("META-INF/mods.toml")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadMCModTOML(file.readAll()); + file.close(); + + // to replace ${file.jarVersion} with the actual version, as needed + if (m_result->details && m_result->details->version == "${file.jarVersion}") + { + if (zip.setCurrentFile("META-INF/MANIFEST.MF")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + // quick and dirty line-by-line parser + auto manifestLines = file.readAll().split('\n'); + QString manifestVersion = ""; + for (auto &line : manifestLines) + { + if (QString(line).startsWith("Implementation-Version: ")) + { + manifestVersion = QString(line).remove("Implementation-Version: "); + break; + } + } + + // some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF + // also keep with forge's behavior of setting the version to "NONE" if none is found + if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") + { + manifestVersion = "NONE"; + } + + m_result->details->version = manifestVersion; + + file.close(); + } + } + + zip.close(); + return; + } + else if (zip.setCurrentFile("mcmod.info")) { if (!file.open(QIODevice::ReadOnly)) { diff --git a/api/logic/minecraft/mod/LocalModParseTask.h b/launcher/minecraft/mod/LocalModParseTask.h similarity index 100% rename from api/logic/minecraft/mod/LocalModParseTask.h rename to launcher/minecraft/mod/LocalModParseTask.h diff --git a/api/logic/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp similarity index 100% rename from api/logic/minecraft/mod/Mod.cpp rename to launcher/minecraft/mod/Mod.cpp diff --git a/api/logic/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h similarity index 97% rename from api/logic/minecraft/mod/Mod.h rename to launcher/minecraft/mod/Mod.h index f77ffd4108..921faeb159 100644 --- a/api/logic/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -19,13 +19,11 @@ #include #include -#include "multimc_logic_export.h" - #include "ModDetails.h" -class MULTIMC_LOGIC_EXPORT Mod +class Mod { public: enum ModType diff --git a/api/logic/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h similarity index 100% rename from api/logic/minecraft/mod/ModDetails.h rename to launcher/minecraft/mod/ModDetails.h diff --git a/api/logic/minecraft/mod/ModFolderLoadTask.cpp b/launcher/minecraft/mod/ModFolderLoadTask.cpp similarity index 100% rename from api/logic/minecraft/mod/ModFolderLoadTask.cpp rename to launcher/minecraft/mod/ModFolderLoadTask.cpp diff --git a/api/logic/minecraft/mod/ModFolderLoadTask.h b/launcher/minecraft/mod/ModFolderLoadTask.h similarity index 100% rename from api/logic/minecraft/mod/ModFolderLoadTask.h rename to launcher/minecraft/mod/ModFolderLoadTask.h diff --git a/api/logic/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp similarity index 99% rename from api/logic/minecraft/mod/ModFolderModel.cpp rename to launcher/minecraft/mod/ModFolderModel.cpp index 031eebe5e8..f0c53c3929 100644 --- a/api/logic/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -29,7 +29,7 @@ ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); diff --git a/api/logic/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h similarity index 97% rename from api/logic/minecraft/mod/ModFolderModel.h rename to launcher/minecraft/mod/ModFolderModel.h index b0a761217e..62c504dfcb 100644 --- a/api/logic/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -24,7 +24,6 @@ #include "Mod.h" -#include "multimc_logic_export.h" #include "ModFolderLoadTask.h" #include "LocalModParseTask.h" @@ -36,7 +35,7 @@ class QFileSystemWatcher; * A legacy mod list. * Backed by a folder. */ -class MULTIMC_LOGIC_EXPORT ModFolderModel : public QAbstractListModel +class ModFolderModel : public QAbstractListModel { Q_OBJECT public: diff --git a/api/logic/minecraft/mod/ModFolderModel_test.cpp b/launcher/minecraft/mod/ModFolderModel_test.cpp similarity index 100% rename from api/logic/minecraft/mod/ModFolderModel_test.cpp rename to launcher/minecraft/mod/ModFolderModel_test.cpp diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp new file mode 100644 index 0000000000..f3d7f56685 --- /dev/null +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -0,0 +1,23 @@ +#include "ResourcePackFolderModel.h" + +ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { +} + +QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::ToolTipRole) { + switch (section) { + case ActiveColumn: + return tr("Is the resource pack enabled?"); + case NameColumn: + return tr("The name of the resource pack."); + case VersionColumn: + return tr("The version of the resource pack."); + case DateColumn: + return tr("The date and time this resource pack was last changed (or added)."); + default: + return QVariant(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h new file mode 100644 index 0000000000..0cd6214bf0 --- /dev/null +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ModFolderModel.h" + +class ResourcePackFolderModel : public ModFolderModel +{ + Q_OBJECT + +public: + explicit ResourcePackFolderModel(const QString &dir); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp new file mode 100644 index 0000000000..d5956da10d --- /dev/null +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -0,0 +1,23 @@ +#include "TexturePackFolderModel.h" + +TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { +} + +QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::ToolTipRole) { + switch (section) { + case ActiveColumn: + return tr("Is the texture pack enabled?"); + case NameColumn: + return tr("The name of the texture pack."); + case VersionColumn: + return tr("The version of the texture pack."); + case DateColumn: + return tr("The date and time this texture pack was last changed (or added)."); + default: + return QVariant(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h new file mode 100644 index 0000000000..a59d511928 --- /dev/null +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ModFolderModel.h" + +class TexturePackFolderModel : public ModFolderModel +{ + Q_OBJECT + +public: + explicit TexturePackFolderModel(const QString &dir); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp new file mode 100644 index 0000000000..c1d88d14de --- /dev/null +++ b/launcher/minecraft/services/CapeChange.cpp @@ -0,0 +1,67 @@ +#include "CapeChange.h" +#include +#include +#include + +CapeChange::CapeChange(QObject *parent, AuthSessionPtr session, QString cape) + : Task(parent), m_capeId(cape), m_session(session) +{ +} + +void CapeChange::setCape(QString& cape) { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().put(request, requestString.toUtf8()); + + setStatus(tr("Equipping cape")); + + m_reply = std::shared_ptr(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void CapeChange::clearCape() { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().deleteResource(request); + + setStatus(tr("Removing cape")); + + m_reply = std::shared_ptr(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + + +void CapeChange::executeTask() +{ + if(m_capeId.isEmpty()) { + clearCape(); + } + else { + setCape(m_capeId); + } +} + +void CapeChange::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void CapeChange::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h new file mode 100644 index 0000000000..1b6f2f7212 --- /dev/null +++ b/launcher/minecraft/services/CapeChange.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include "tasks/Task.h" + +class CapeChange : public Task +{ + Q_OBJECT +public: + CapeChange(QObject *parent, AuthSessionPtr session, QString capeId); + virtual ~CapeChange() {} + +private: + void setCape(QString & cape); + void clearCape(); + +private: + QString m_capeId; + AuthSessionPtr m_session; + std::shared_ptr m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; + diff --git a/api/logic/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp similarity index 100% rename from api/logic/minecraft/services/SkinDelete.cpp rename to launcher/minecraft/services/SkinDelete.cpp diff --git a/api/logic/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h similarity index 86% rename from api/logic/minecraft/services/SkinDelete.h rename to launcher/minecraft/services/SkinDelete.h index 705ce8ef7d..839bf9bc74 100644 --- a/api/logic/minecraft/services/SkinDelete.h +++ b/launcher/minecraft/services/SkinDelete.h @@ -5,11 +5,10 @@ #include #include #include "tasks/Task.h" -#include "multimc_logic_export.h" typedef std::shared_ptr SkinDeletePtr; -class MULTIMC_LOGIC_EXPORT SkinDelete : public Task +class SkinDelete : public Task { Q_OBJECT public: diff --git a/api/logic/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp similarity index 100% rename from api/logic/minecraft/services/SkinUpload.cpp rename to launcher/minecraft/services/SkinUpload.cpp diff --git a/api/logic/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h similarity index 89% rename from api/logic/minecraft/services/SkinUpload.h rename to launcher/minecraft/services/SkinUpload.h index c77abb034f..ec859699e0 100644 --- a/api/logic/minecraft/services/SkinUpload.h +++ b/launcher/minecraft/services/SkinUpload.h @@ -5,11 +5,10 @@ #include #include #include "tasks/Task.h" -#include "multimc_logic_export.h" typedef std::shared_ptr SkinUploadPtr; -class MULTIMC_LOGIC_EXPORT SkinUpload : public Task +class SkinUpload : public Task { Q_OBJECT public: diff --git a/api/logic/minecraft/testdata/1.9-simple.json b/launcher/minecraft/testdata/1.9-simple.json similarity index 100% rename from api/logic/minecraft/testdata/1.9-simple.json rename to launcher/minecraft/testdata/1.9-simple.json diff --git a/api/logic/minecraft/testdata/1.9.json b/launcher/minecraft/testdata/1.9.json similarity index 100% rename from api/logic/minecraft/testdata/1.9.json rename to launcher/minecraft/testdata/1.9.json diff --git a/api/logic/minecraft/testdata/codecwav-20101023.jar b/launcher/minecraft/testdata/codecwav-20101023.jar similarity index 100% rename from api/logic/minecraft/testdata/codecwav-20101023.jar rename to launcher/minecraft/testdata/codecwav-20101023.jar diff --git a/api/logic/minecraft/testdata/lib-native-arch.json b/launcher/minecraft/testdata/lib-native-arch.json similarity index 100% rename from api/logic/minecraft/testdata/lib-native-arch.json rename to launcher/minecraft/testdata/lib-native-arch.json diff --git a/api/logic/minecraft/testdata/lib-native.json b/launcher/minecraft/testdata/lib-native.json similarity index 100% rename from api/logic/minecraft/testdata/lib-native.json rename to launcher/minecraft/testdata/lib-native.json diff --git a/api/logic/minecraft/testdata/lib-simple.json b/launcher/minecraft/testdata/lib-simple.json similarity index 100% rename from api/logic/minecraft/testdata/lib-simple.json rename to launcher/minecraft/testdata/lib-simple.json diff --git a/api/logic/minecraft/testdata/testname-testversion-linux-32.jar b/launcher/minecraft/testdata/testname-testversion-linux-32.jar similarity index 100% rename from api/logic/minecraft/testdata/testname-testversion-linux-32.jar rename to launcher/minecraft/testdata/testname-testversion-linux-32.jar diff --git a/api/logic/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp similarity index 100% rename from api/logic/minecraft/update/AssetUpdateTask.cpp rename to launcher/minecraft/update/AssetUpdateTask.cpp diff --git a/api/logic/minecraft/update/AssetUpdateTask.h b/launcher/minecraft/update/AssetUpdateTask.h similarity index 100% rename from api/logic/minecraft/update/AssetUpdateTask.h rename to launcher/minecraft/update/AssetUpdateTask.h diff --git a/api/logic/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp similarity index 96% rename from api/logic/minecraft/update/FMLLibrariesTask.cpp rename to launcher/minecraft/update/FMLLibrariesTask.cpp index 85993e8148..a05a7c2a77 100644 --- a/api/logic/minecraft/update/FMLLibrariesTask.cpp +++ b/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -64,7 +64,7 @@ void FMLLibrariesTask::executeTask() for (auto &lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); - QString urlString = (lib.ours ? BuildConfig.FMLLIBS_OUR_BASE_URL : BuildConfig.FMLLIBS_FORGE_BASE_URL) + lib.filename; + QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry)); } diff --git a/api/logic/minecraft/update/FMLLibrariesTask.h b/launcher/minecraft/update/FMLLibrariesTask.h similarity index 100% rename from api/logic/minecraft/update/FMLLibrariesTask.h rename to launcher/minecraft/update/FMLLibrariesTask.h diff --git a/api/logic/minecraft/update/FoldersTask.cpp b/launcher/minecraft/update/FoldersTask.cpp similarity index 100% rename from api/logic/minecraft/update/FoldersTask.cpp rename to launcher/minecraft/update/FoldersTask.cpp diff --git a/api/logic/minecraft/update/FoldersTask.h b/launcher/minecraft/update/FoldersTask.h similarity index 100% rename from api/logic/minecraft/update/FoldersTask.h rename to launcher/minecraft/update/FoldersTask.h diff --git a/api/logic/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp similarity index 100% rename from api/logic/minecraft/update/LibrariesTask.cpp rename to launcher/minecraft/update/LibrariesTask.cpp diff --git a/api/logic/minecraft/update/LibrariesTask.h b/launcher/minecraft/update/LibrariesTask.h similarity index 100% rename from api/logic/minecraft/update/LibrariesTask.h rename to launcher/minecraft/update/LibrariesTask.h diff --git a/api/logic/modplatform/atlauncher/ATLPackIndex.cpp b/launcher/modplatform/atlauncher/ATLPackIndex.cpp similarity index 100% rename from api/logic/modplatform/atlauncher/ATLPackIndex.cpp rename to launcher/modplatform/atlauncher/ATLPackIndex.cpp diff --git a/api/logic/modplatform/atlauncher/ATLPackIndex.h b/launcher/modplatform/atlauncher/ATLPackIndex.h similarity index 79% rename from api/logic/modplatform/atlauncher/ATLPackIndex.h rename to launcher/modplatform/atlauncher/ATLPackIndex.h index 5e2e6487cd..405a344838 100644 --- a/api/logic/modplatform/atlauncher/ATLPackIndex.h +++ b/launcher/modplatform/atlauncher/ATLPackIndex.h @@ -6,8 +6,6 @@ #include #include -#include "multimc_logic_export.h" - namespace ATLauncher { @@ -30,7 +28,7 @@ struct IndexedPack QString safeName; }; -MULTIMC_LOGIC_EXPORT void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +void loadIndexedPack(IndexedPack & m, QJsonObject & obj); } Q_DECLARE_METATYPE(ATLauncher::IndexedPack) diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp similarity index 82% rename from api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp rename to launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 25c6d58d83..55087a27b9 100644 --- a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "ATLPackInstallTask.h" #include "BuildConfig.h" @@ -18,15 +19,20 @@ namespace ATLauncher { -PackInstallTask::PackInstallTask(QString pack, QString version) +PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) { + m_support = support; m_pack = pack; m_version_name = version; } bool PackInstallTask::abort() { - return true; + if(abortable) + { + return jobPtr->abort(); + } + return false; } void PackInstallTask::executeTask() @@ -73,13 +79,13 @@ void PackInstallTask::onDownloadSucceeded() auto vlist = ENV.metadataIndex()->get("net.minecraft"); if(!vlist) { - emitFailed(tr("Failed to get local metadata index for ") + "net.minecraft"); + emitFailed(tr("Failed to get local metadata index for %1").arg("net.minecraft")); return; } auto ver = vlist->getVersion(m_version.minecraft); if (!ver) { - emitFailed(tr("Failed to get local metadata index for ") + "net.minecraft" + " " + m_version.minecraft); + emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft").arg(m_version.minecraft)); return; } ver->load(Net::Mode::Online); @@ -141,7 +147,7 @@ QString PackInstallTask::getDirForModType(ModType type, QString raw) qWarning() << "Unsupported mod type: " + raw; return Q_NULLPTR; case ModType::Unknown: - emitFailed(tr("Unknown mod type: ") + raw); + emitFailed(tr("Unknown mod type: %1").arg(raw)); return Q_NULLPTR; } @@ -154,23 +160,56 @@ QString PackInstallTask::getVersionForLoader(QString uid) auto vlist = ENV.metadataIndex()->get(uid); if(!vlist) { - emitFailed(tr("Failed to get local metadata index for ") + uid); + emitFailed(tr("Failed to get local metadata index for %1").arg(uid)); return Q_NULLPTR; } - // todo: filter by Minecraft version - - if(m_version.loader.recommended) { - return vlist.get()->getRecommended().get()->descriptor(); + if(!vlist->isLoaded()) { + vlist->load(Net::Mode::Online); } - else if(m_version.loader.latest) { - return vlist.get()->at(0)->descriptor(); + + if(m_version.loader.recommended || m_version.loader.latest) { + for (int i = 0; i < vlist->versions().size(); i++) { + auto version = vlist->versions().at(i); + auto reqs = version->requires(); + + // filter by minecraft version, if the loader depends on a certain version. + // not all mod loaders depend on a given Minecraft version, so we won't do this + // filtering for those loaders. + if (m_version.loader.type != "fabric") { + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) { + return req.uid == "net.minecraft"; + }); + if (iter == reqs.end()) continue; + if (iter->equalsVersion != m_version.minecraft) continue; + } + + if (m_version.loader.recommended) { + // first recommended build we find, we use. + if (!version->isRecommended()) continue; + } + + return version->descriptor(); + } + + emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); + return Q_NULLPTR; } else if(m_version.loader.choose) { - // todo: implement + // Fabric Loader doesn't depend on a given Minecraft version. + if (m_version.loader.type == "fabric") { + return m_support->chooseVersion(vlist, Q_NULLPTR); + } + + return m_support->chooseVersion(vlist, m_version.minecraft); } } + if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) { + emitFailed(tr("No loader version set for modpack!")); + return Q_NULLPTR; + } + return m_version.loader.version; } @@ -271,7 +310,7 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared break; case DownloadType::Browser: case DownloadType::Unknown: - emitFailed(tr("Unknown or unsupported download type: ") + lib.download_raw); + emitFailed(tr("Unknown or unsupported download type: %1").arg(lib.download_raw)); return false; } @@ -373,21 +412,29 @@ void PackInstallTask::installConfigs() auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", path); entry->setStale(true); - jobPtr->addNetAction(Net::Download::makeCached(url, entry)); + auto dl = Net::Download::makeCached(url, entry); + if (!m_version.configs.sha1.isEmpty()) { + auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + } + jobPtr->addNetAction(dl); archivePath = entry->getFullPath(); connect(jobPtr.get(), &NetJob::succeeded, this, [&]() { + abortable = false; jobPtr.reset(); extractConfigs(); }); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { + abortable = false; jobPtr.reset(); emitFailed(reason); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + abortable = true; setProgress(current, total); }); @@ -423,13 +470,31 @@ void PackInstallTask::extractConfigs() void PackInstallTask::downloadMods() { qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); + + QVector optionalMods; + for (const auto& mod : m_version.mods) { + if (mod.optional) { + optionalMods.push_back(mod); + } + } + + // Select optional mods, if pack contains any + QVector selectedMods; + if (!optionalMods.isEmpty()) { + setStatus(tr("Selecting optional mods...")); + selectedMods = m_support->chooseOptionalMods(optionalMods); + } + setStatus(tr("Downloading mods...")); jarmods.clear(); jobPtr.reset(new NetJob(tr("Mod download"))); for(const auto& mod : m_version.mods) { - // skip optional mods for now - if(mod.optional) continue; + // skip non-client mods + if(!mod.client) continue; + + // skip optional mods that were not selected + if(mod.optional && !selectedMods.contains(mod.name)) continue; QString url; switch(mod.download) { @@ -437,13 +502,13 @@ void PackInstallTask::downloadMods() url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; break; case DownloadType::Browser: - emitFailed(tr("Unsupported download type: ") + mod.download_raw); + emitFailed(tr("Unsupported download type: %1").arg(mod.download_raw)); return; case DownloadType::Direct: url = mod.url; break; case DownloadType::Unknown: - emitFailed(tr("Unknown download type: ") + mod.download_raw); + emitFailed(tr("Unknown download type: %1").arg(mod.download_raw)); return; } @@ -451,12 +516,15 @@ void PackInstallTask::downloadMods() auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { - auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); modsToExtract.insert(entry->getFullPath(), mod); auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } jobPtr->addNetAction(dl); } else if(mod.type == ModType::Decomp) { @@ -465,6 +533,10 @@ void PackInstallTask::downloadMods() modsToDecomp.insert(entry->getFullPath(), mod); auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } jobPtr->addNetAction(dl); } else { @@ -475,6 +547,10 @@ void PackInstallTask::downloadMods() entry->setStale(true); auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } jobPtr->addNetAction(dl); auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); @@ -507,11 +583,13 @@ void PackInstallTask::downloadMods() connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { + abortable = false; jobPtr.reset(); emitFailed(reason); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + abortable = true; setProgress(current, total); }); @@ -519,10 +597,12 @@ void PackInstallTask::downloadMods() } void PackInstallTask::onModsDownloaded() { + abortable = false; + qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId(); jobPtr.reset(); - if(modsToExtract.size() || modsToDecomp.size() || modsToCopy.size()) { + if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, [&]() @@ -629,6 +709,7 @@ void PackInstallTask::install() // Use a component to add libraries BEFORE Minecraft if(!createLibrariesComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create libraries component")); return; } @@ -666,6 +747,7 @@ void PackInstallTask::install() // Use a component to fill in the rest of the data // todo: use more detection if(!createPackComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create pack component")); return; } diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h similarity index 71% rename from api/logic/modplatform/atlauncher/ATLPackInstallTask.h rename to launcher/modplatform/atlauncher/ATLPackInstallTask.h index 78544bab64..39e2b01352 100644 --- a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -4,7 +4,6 @@ #include "ATLPackManifest.h" #include "InstanceTask.h" -#include "multimc_logic_export.h" #include "net/NetJob.h" #include "settings/INISettingsObject.h" #include "minecraft/MinecraftInstance.h" @@ -15,14 +14,31 @@ namespace ATLauncher { -class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask +class UserInteractionSupport { + +public: + /** + * Requests a user interaction to select which optional mods should be installed. + */ + virtual QVector chooseOptionalMods(QVector mods) = 0; + + /** + * Requests a user interaction to select a component version from a given version list + * and constrained to a given Minecraft version. + */ + virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0; + +}; + +class PackInstallTask : public InstanceTask { Q_OBJECT public: - explicit PackInstallTask(QString pack, QString version); + explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version); virtual ~PackInstallTask(){} + bool canAbort() const override { return true; } bool abort() override; protected: @@ -54,6 +70,10 @@ private slots: void install(); private: + UserInteractionSupport *m_support; + + bool abortable = false; + NetJobPtr jobPtr; QByteArray response; @@ -76,9 +96,6 @@ private slots: QFuture m_modExtractFuture; QFutureWatcher m_modExtractFutureWatcher; - QFuture m_decompFuture; - QFutureWatcher m_decompFutureWatcher; - }; } diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp similarity index 77% rename from api/logic/modplatform/atlauncher/ATLPackManifest.cpp rename to launcher/modplatform/atlauncher/ATLPackManifest.cpp index 8438933040..e25d8346c0 100644 --- a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -81,12 +81,21 @@ static ATLauncher::ModType parseModType(QString rawType) { static void loadVersionLoader(ATLauncher::VersionLoader & p, QJsonObject & obj) { p.type = Json::requireString(obj, "type"); - p.latest = Json::ensureBoolean(obj, QString("latest"), false); p.choose = Json::ensureBoolean(obj, QString("choose"), false); - p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); auto metadata = Json::requireObject(obj, "metadata"); - p.version = Json::requireString(metadata, "version"); + p.latest = Json::ensureBoolean(metadata, QString("latest"), false); + p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false); + + // Minecraft Forge + if (p.type == "forge") { + p.version = Json::ensureString(metadata, "version", ""); + } + + // Fabric Loader + if (p.type == "fabric") { + p.version = Json::ensureString(metadata, "loader", ""); + } } static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj) { @@ -100,6 +109,11 @@ static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj p.server = Json::ensureString(obj, "server", ""); } +static void loadVersionConfigs(ATLauncher::VersionConfigs & p, QJsonObject & obj) { + p.filesize = Json::requireInteger(obj, "filesize"); + p.sha1 = Json::requireString(obj, "sha1"); +} + static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.name = Json::requireString(obj, "name"); p.version = Json::requireString(obj, "version"); @@ -134,7 +148,24 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.decompFile = Json::requireString(obj, "decompFile"); } + p.description = Json::ensureString(obj, QString("description"), ""); p.optional = Json::ensureBoolean(obj, QString("optional"), false); + p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); + p.selected = Json::ensureBoolean(obj, QString("selected"), false); + p.hidden = Json::ensureBoolean(obj, QString("hidden"), false); + p.library = Json::ensureBoolean(obj, QString("library"), false); + p.group = Json::ensureString(obj, QString("group"), ""); + if(obj.contains("depends")) { + auto dependsArr = Json::requireArray(obj, "depends"); + for (const auto depends : dependsArr) { + p.depends.append(Json::requireString(depends)); + } + } + + p.client = Json::ensureBoolean(obj, QString("client"), false); + + // computed + p.effectively_hidden = p.hidden || p.library; } void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) @@ -169,12 +200,19 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) } } - auto mods = Json::requireArray(obj, "mods"); - for (const auto modRaw : mods) - { - auto modObj = Json::requireObject(modRaw); - ATLauncher::VersionMod mod; - loadVersionMod(mod, modObj); - v.mods.append(mod); + if(obj.contains("mods")) { + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) + { + auto modObj = Json::requireObject(modRaw); + ATLauncher::VersionMod mod; + loadVersionMod(mod, modObj); + v.mods.append(mod); + } + } + + if(obj.contains("configs")) { + auto configsObj = Json::requireObject(obj, "configs"); + loadVersionConfigs(v.configs, configsObj); } } diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h similarity index 79% rename from api/logic/modplatform/atlauncher/ATLPackManifest.h rename to launcher/modplatform/atlauncher/ATLPackManifest.h index 1adf889bca..ead216a5c9 100644 --- a/api/logic/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -3,7 +3,6 @@ #include #include #include -#include namespace ATLauncher { @@ -86,7 +85,25 @@ struct VersionMod QString decompType_raw; QString decompFile; + QString description; bool optional; + bool recommended; + bool selected; + bool hidden; + bool library; + QString group; + QVector depends; + + bool client; + + // computed + bool effectively_hidden; +}; + +struct VersionConfigs +{ + int filesize; + QString sha1; }; struct PackVersion @@ -100,8 +117,9 @@ struct PackVersion VersionLoader loader; QVector libraries; QVector mods; + VersionConfigs configs; }; -MULTIMC_LOGIC_EXPORT void loadVersion(PackVersion & v, QJsonObject & obj); +void loadVersion(PackVersion & v, QJsonObject & obj); } diff --git a/api/logic/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp similarity index 100% rename from api/logic/modplatform/flame/FileResolvingTask.cpp rename to launcher/modplatform/flame/FileResolvingTask.cpp diff --git a/api/logic/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h similarity index 84% rename from api/logic/modplatform/flame/FileResolvingTask.h rename to launcher/modplatform/flame/FileResolvingTask.h index 5679b9072a..78a38fcb4b 100644 --- a/api/logic/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -4,11 +4,9 @@ #include "net/NetJob.h" #include "PackManifest.h" -#include "multimc_logic_export.h" - namespace Flame { -class MULTIMC_LOGIC_EXPORT FileResolvingTask : public Task +class FileResolvingTask : public Task { Q_OBJECT public: diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp new file mode 100644 index 0000000000..3d8ea22ae4 --- /dev/null +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -0,0 +1,92 @@ +#include "FlamePackIndex.h" + +#include "Json.h" + +void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj) +{ + pack.addonId = Json::requireInteger(obj, "id"); + pack.name = Json::requireString(obj, "name"); + pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.description = Json::ensureString(obj, "summary", ""); + + bool thumbnailFound = false; + auto attachments = Json::requireArray(obj, "attachments"); + for(auto attachmentRaw: attachments) { + auto attachmentObj = Json::requireObject(attachmentRaw); + bool isDefault = attachmentObj.value("isDefault").toBool(false); + if(isDefault) { + thumbnailFound = true; + pack.logoName = Json::requireString(attachmentObj, "title"); + pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl"); + break; + } + } + + if(!thumbnailFound) { + throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); + } + + auto authors = Json::requireArray(obj, "authors"); + for(auto authorIter: authors) { + auto author = Json::requireObject(authorIter); + Flame::ModpackAuthor packAuthor; + packAuthor.name = Json::requireString(author, "name"); + packAuthor.url = Json::requireString(author, "url"); + pack.authors.append(packAuthor); + } + int defaultFileId = Json::requireInteger(obj, "defaultFileId"); + + bool found = false; + // check if there are some files before adding the pack + auto files = Json::requireArray(obj, "latestFiles"); + for(auto fileIter: files) { + auto file = Json::requireObject(fileIter); + int id = Json::requireInteger(file, "id"); + + // NOTE: for now, ignore everything that's not the default... + if(id != defaultFileId) { + continue; + } + + auto versionArray = Json::requireArray(file, "gameVersion"); + if(versionArray.size() < 1) { + continue; + } + + found = true; + break; + } + if(!found) { + throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); + } +} + +void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr) +{ + QVector unsortedVersions; + for(auto versionIter: arr) { + auto version = Json::requireObject(versionIter); + Flame::IndexedVersion file; + + file.addonId = pack.addonId; + file.fileId = Json::requireInteger(version, "id"); + auto versionArray = Json::requireArray(version, "gameVersion"); + if(versionArray.size() < 1) { + continue; + } + + // pick the latest version supported + file.mcVersion = versionArray[0].toString(); + file.version = Json::requireString(version, "displayName"); + file.downloadUrl = Json::requireString(version, "downloadUrl"); + unsortedVersions.append(file); + } + + auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool + { + return a.fileId > b.fileId; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + pack.versions = unsortedVersions; + pack.versionsLoaded = true; +} diff --git a/application/pages/modplatform/twitch/TwitchData.h b/launcher/modplatform/flame/FlamePackIndex.h similarity index 51% rename from application/pages/modplatform/twitch/TwitchData.h rename to launcher/modplatform/flame/FlamePackIndex.h index dd000b8467..7ffa29c3de 100644 --- a/application/pages/modplatform/twitch/TwitchData.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -1,16 +1,18 @@ #pragma once -#include #include +#include +#include +#include -namespace Twitch { +namespace Flame { struct ModpackAuthor { QString name; QString url; }; -struct ModpackFile { +struct IndexedVersion { int addonId; int fileId; QString version; @@ -18,21 +20,22 @@ struct ModpackFile { QString downloadUrl; }; -struct Modpack +struct IndexedPack { - bool broken = true; - int addonId = 0; - + int addonId; QString name; QString description; QList authors; - QString mcVersion; QString logoName; QString logoUrl; QString websiteUrl; - ModpackFile latestFile; + bool versionsLoaded = false; + QVector versions; }; + +void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr); } -Q_DECLARE_METATYPE(Twitch::Modpack) +Q_DECLARE_METATYPE(Flame::IndexedPack) diff --git a/api/logic/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp similarity index 98% rename from api/logic/modplatform/flame/PackManifest.cpp rename to launcher/modplatform/flame/PackManifest.cpp index 1db0a1616f..b928fd1684 100644 --- a/api/logic/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -21,7 +21,7 @@ static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft) // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing m.libraries = Json::ensureString(minecraft, QString("libraries"), QString()); auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray()); - for (const auto & item : arr) + for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::Modloader loader; @@ -38,7 +38,7 @@ static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest) m.version = Json::ensureString(manifest, QString("version"), QString()); m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward"); auto arr = Json::ensureArray(manifest, "files", QJsonArray()); - for (const auto & item : arr) + for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::File file; diff --git a/api/logic/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h similarity index 100% rename from api/logic/modplatform/flame/PackManifest.h rename to launcher/modplatform/flame/PackManifest.h diff --git a/api/logic/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp similarity index 100% rename from api/logic/modplatform/legacy_ftb/PackFetchTask.cpp rename to launcher/modplatform/legacy_ftb/PackFetchTask.cpp diff --git a/api/logic/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h similarity index 93% rename from api/logic/modplatform/legacy_ftb/PackFetchTask.h rename to launcher/modplatform/legacy_ftb/PackFetchTask.h index 4a8469b111..3ab32fab2b 100644 --- a/api/logic/modplatform/legacy_ftb/PackFetchTask.h +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -8,7 +8,7 @@ namespace LegacyFTB { -class MULTIMC_LOGIC_EXPORT PackFetchTask : public QObject { +class PackFetchTask : public QObject { Q_OBJECT diff --git a/api/logic/modplatform/legacy_ftb/PackHelpers.h b/launcher/modplatform/legacy_ftb/PackHelpers.h similarity index 100% rename from api/logic/modplatform/legacy_ftb/PackHelpers.h rename to launcher/modplatform/legacy_ftb/PackHelpers.h diff --git a/api/logic/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp similarity index 100% rename from api/logic/modplatform/legacy_ftb/PackInstallTask.cpp rename to launcher/modplatform/legacy_ftb/PackInstallTask.cpp diff --git a/api/logic/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h similarity index 91% rename from api/logic/modplatform/legacy_ftb/PackInstallTask.h rename to launcher/modplatform/legacy_ftb/PackInstallTask.h index 7868d1c468..600f72e7c4 100644 --- a/api/logic/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -12,7 +12,7 @@ namespace LegacyFTB { -class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask +class PackInstallTask : public InstanceTask { Q_OBJECT @@ -20,6 +20,7 @@ class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask explicit PackInstallTask(Modpack pack, QString version); virtual ~PackInstallTask(){} + bool canAbort() const override { return true; } bool abort() override; protected: diff --git a/api/logic/modplatform/legacy_ftb/PrivatePackManager.cpp b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp similarity index 100% rename from api/logic/modplatform/legacy_ftb/PrivatePackManager.cpp rename to launcher/modplatform/legacy_ftb/PrivatePackManager.cpp diff --git a/api/logic/modplatform/legacy_ftb/PrivatePackManager.h b/launcher/modplatform/legacy_ftb/PrivatePackManager.h similarity index 89% rename from api/logic/modplatform/legacy_ftb/PrivatePackManager.h rename to launcher/modplatform/legacy_ftb/PrivatePackManager.h index 0232bac725..0e8146462c 100644 --- a/api/logic/modplatform/legacy_ftb/PrivatePackManager.h +++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.h @@ -3,11 +3,10 @@ #include #include #include -#include "multimc_logic_export.h" namespace LegacyFTB { -class MULTIMC_LOGIC_EXPORT PrivatePackManager +class PrivatePackManager { public: ~PrivatePackManager() diff --git a/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp similarity index 77% rename from api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp rename to launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 59546b00e3..f22373bcc7 100644 --- a/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,10 +1,12 @@ #include "FTBPackInstallTask.h" #include "BuildConfig.h" +#include "Env.h" #include "FileSystem.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" namespace ModpacksCH { @@ -17,7 +19,11 @@ PackInstallTask::PackInstallTask(Modpack pack, QString version) bool PackInstallTask::abort() { - return true; + if(abortable) + { + return jobPtr->abort(); + } + return false; } void PackInstallTask::executeTask() @@ -35,7 +41,7 @@ void PackInstallTask::executeTask() } if(!found) { - emitFailed("failed to find pack version " + m_version_name); + emitFailed(tr("Failed to find pack version %1").arg(m_version_name)); return; } @@ -93,31 +99,41 @@ void PackInstallTask::downloadPack() for(auto file : m_version.files) { if(file.serverOnly) continue; + QFileInfo fileName(file.name); + auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix(); + + auto entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", cacheName); + entry->setStale(true); + auto relpath = FS::PathCombine("minecraft", file.path, file.name); auto path = FS::PathCombine(m_stagingPath, relpath); qDebug() << "Will download" << file.url << "to" << path; - auto dl = Net::Download::makeFile(file.url, path); + filesToCopy[entry->getFullPath()] = path; + + auto dl = Net::Download::makeCached(file.url, entry); + if (!file.sha1.isEmpty()) { + auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + } jobPtr->addNetAction(dl); } connect(jobPtr.get(), &NetJob::succeeded, this, [&]() { + abortable = false; jobPtr.reset(); install(); }); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { + abortable = false; jobPtr.reset(); - - // FIXME: Temporarily ignore file download failures (matching FTB's installer), - // while FTB's data is fucked. - qWarning() << "Failed to download files for modpack: " + reason; - - install(); + emitFailed(reason); }); connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + abortable = true; setProgress(current, total); }); @@ -126,6 +142,19 @@ void PackInstallTask::downloadPack() void PackInstallTask::install() { + setStatus(tr("Copying modpack files")); + + for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) { + auto &from = iter.key(); + auto &to = iter.value(); + FS::copy fileCopyOperation(from, to); + if(!fileCopyOperation()) { + qWarning() << "Failed to copy" << from << "to" << to; + emitFailed(tr("Failed to copy files")); + return; + } + } + setStatus(tr("Installing modpack")); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); diff --git a/api/logic/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h similarity index 78% rename from api/logic/modplatform/modpacksch/FTBPackInstallTask.h rename to launcher/modplatform/modpacksch/FTBPackInstallTask.h index 4f7786fd72..fdd84c4ed9 100644 --- a/api/logic/modplatform/modpacksch/FTBPackInstallTask.h +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h @@ -3,12 +3,11 @@ #include "FTBPackManifest.h" #include "InstanceTask.h" -#include "multimc_logic_export.h" #include "net/NetJob.h" namespace ModpacksCH { -class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask +class PackInstallTask : public InstanceTask { Q_OBJECT @@ -16,6 +15,7 @@ class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask explicit PackInstallTask(Modpack pack, QString version); virtual ~PackInstallTask(){} + bool canAbort() const override { return true; } bool abort() override; protected: @@ -30,6 +30,8 @@ private slots: void install(); private: + bool abortable = false; + NetJobPtr jobPtr; QByteArray response; @@ -37,6 +39,8 @@ private slots: QString m_version_name; Version m_version; + QMap filesToCopy; + }; } diff --git a/api/logic/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp similarity index 95% rename from api/logic/modplatform/modpacksch/FTBPackManifest.cpp rename to launcher/modplatform/modpacksch/FTBPackManifest.cpp index 35626cb848..fd99d3324c 100644 --- a/api/logic/modplatform/modpacksch/FTBPackManifest.cpp +++ b/launcher/modplatform/modpacksch/FTBPackManifest.cpp @@ -60,7 +60,7 @@ void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj) m.updated = Json::requireInteger(obj, "updated"); m.refreshed = Json::requireInteger(obj, "refreshed"); auto artArr = Json::requireArray(obj, "art"); - for (const auto & artRaw : artArr) + for (QJsonValueRef artRaw : artArr) { auto artObj = Json::requireObject(artRaw); ModpacksCH::Art art; @@ -68,7 +68,7 @@ void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj) m.art.append(art); } auto authorArr = Json::requireArray(obj, "authors"); - for (const auto & authorRaw : authorArr) + for (QJsonValueRef authorRaw : authorArr) { auto authorObj = Json::requireObject(authorRaw); ModpacksCH::Author author; @@ -76,7 +76,7 @@ void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj) m.authors.append(author); } auto versionArr = Json::requireArray(obj, "versions"); - for (const auto & versionRaw : versionArr) + for (QJsonValueRef versionRaw : versionArr) { auto versionObj = Json::requireObject(versionRaw); ModpacksCH::VersionInfo version; @@ -84,7 +84,7 @@ void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj) m.versions.append(version); } auto tagArr = Json::requireArray(obj, "tags"); - for (const auto & tagRaw : tagArr) + for (QJsonValueRef tagRaw : tagArr) { auto tagObj = Json::requireObject(tagRaw); ModpacksCH::Tag tag; @@ -132,7 +132,7 @@ void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj) auto specs = Json::requireObject(obj, "specs"); loadSpecs(m.specs, specs); auto targetArr = Json::requireArray(obj, "targets"); - for (const auto & targetRaw : targetArr) + for (QJsonValueRef targetRaw : targetArr) { auto versionObj = Json::requireObject(targetRaw); ModpacksCH::VersionTarget target; @@ -140,7 +140,7 @@ void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj) m.targets.append(target); } auto fileArr = Json::requireArray(obj, "files"); - for (const auto & fileRaw : fileArr) + for (QJsonValueRef fileRaw : fileArr) { auto fileObj = Json::requireObject(fileRaw); ModpacksCH::VersionFile file; diff --git a/api/logic/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h similarity index 90% rename from api/logic/modplatform/modpacksch/FTBPackManifest.h rename to launcher/modplatform/modpacksch/FTBPackManifest.h index 518fffbf83..7818b36d41 100644 --- a/api/logic/modplatform/modpacksch/FTBPackManifest.h +++ b/launcher/modplatform/modpacksch/FTBPackManifest.h @@ -6,8 +6,6 @@ #include #include -#include "multimc_logic_export.h" - namespace ModpacksCH { @@ -119,9 +117,9 @@ struct VersionChangelog int64_t updated; }; -MULTIMC_LOGIC_EXPORT void loadModpack(Modpack & m, QJsonObject & obj); +void loadModpack(Modpack & m, QJsonObject & obj); -MULTIMC_LOGIC_EXPORT void loadVersion(Version & m, QJsonObject & obj); +void loadVersion(Version & m, QJsonObject & obj); } Q_DECLARE_METATYPE(ModpacksCH::Modpack) diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp similarity index 95% rename from api/logic/modplatform/technic/SingleZipPackInstallTask.cpp rename to launcher/modplatform/technic/SingleZipPackInstallTask.cpp index 96e1804d75..dbce8e535f 100644 --- a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -28,6 +28,14 @@ Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUr m_minecraftVersion = minecraftVersion; } +bool Technic::SingleZipPackInstallTask::abort() { + if(m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + void Technic::SingleZipPackInstallTask::executeTask() { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); @@ -47,6 +55,8 @@ void Technic::SingleZipPackInstallTask::executeTask() void Technic::SingleZipPackInstallTask::downloadSucceeded() { + m_abortable = false; + setStatus(tr("Extracting modpack")); QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft")); qDebug() << "Attempting to create instance from" << m_archivePath; @@ -67,12 +77,14 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded() void Technic::SingleZipPackInstallTask::downloadFailed(QString reason) { + m_abortable = false; emitFailed(reason); m_filesNetJob.reset(); } void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) { + m_abortable = true; setProgress(current / 2, total); } diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h similarity index 90% rename from api/logic/modplatform/technic/SingleZipPackInstallTask.h rename to launcher/modplatform/technic/SingleZipPackInstallTask.h index c56b9e46d0..80f10a985a 100644 --- a/api/logic/modplatform/technic/SingleZipPackInstallTask.h +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -17,7 +17,6 @@ #include "InstanceTask.h" #include "net/NetJob.h" -#include "multimc_logic_export.h" #include "quazip.h" @@ -29,13 +28,16 @@ namespace Technic { -class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask +class SingleZipPackInstallTask : public InstanceTask { Q_OBJECT public: SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + bool canAbort() const override { return true; } + bool abort() override; + protected: void executeTask() override; @@ -48,6 +50,8 @@ private slots: void extractAborted(); private: + bool m_abortable = false; + QUrl m_sourceUrl; QString m_minecraftVersion; QString m_archivePath; diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp similarity index 97% rename from api/logic/modplatform/technic/SolderPackInstallTask.cpp rename to launcher/modplatform/technic/SolderPackInstallTask.cpp index 1d17073c14..1b4186d4e4 100644 --- a/api/logic/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -27,6 +27,14 @@ Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, con m_minecraftVersion = minecraftVersion; } +bool Technic::SolderPackInstallTask::abort() { + if(m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + void Technic::SolderPackInstallTask::executeTask() { setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString())); @@ -106,6 +114,8 @@ void Technic::SolderPackInstallTask::fileListSucceeded() void Technic::SolderPackInstallTask::downloadSucceeded() { + m_abortable = false; + setStatus(tr("Extracting modpack")); m_filesNetJob.reset(); m_extractFuture = QtConcurrent::run([this]() @@ -132,12 +142,14 @@ void Technic::SolderPackInstallTask::downloadSucceeded() void Technic::SolderPackInstallTask::downloadFailed(QString reason) { + m_abortable = false; emitFailed(reason); m_filesNetJob.reset(); } void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) { + m_abortable = true; setProgress(current / 2, total); } diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h similarity index 89% rename from api/logic/modplatform/technic/SolderPackInstallTask.h rename to launcher/modplatform/technic/SolderPackInstallTask.h index 0fe6cb83db..6e1057ebb7 100644 --- a/api/logic/modplatform/technic/SolderPackInstallTask.h +++ b/launcher/modplatform/technic/SolderPackInstallTask.h @@ -23,12 +23,15 @@ namespace Technic { - class MULTIMC_LOGIC_EXPORT SolderPackInstallTask : public InstanceTask + class SolderPackInstallTask : public InstanceTask { Q_OBJECT public: explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + bool canAbort() const override { return true; } + bool abort() override; + protected: //! Entry point for tasks. virtual void executeTask() override; @@ -43,6 +46,8 @@ namespace Technic void extractAborted(); private: + bool m_abortable = false; + NetJobPtr m_filesNetJob; QUrl m_sourceUrl; QString m_minecraftVersion; diff --git a/api/logic/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp similarity index 100% rename from api/logic/modplatform/technic/TechnicPackProcessor.cpp rename to launcher/modplatform/technic/TechnicPackProcessor.cpp diff --git a/api/logic/modplatform/technic/TechnicPackProcessor.h b/launcher/modplatform/technic/TechnicPackProcessor.h similarity index 100% rename from api/logic/modplatform/technic/TechnicPackProcessor.h rename to launcher/modplatform/technic/TechnicPackProcessor.h diff --git a/api/logic/mojang/PackageManifest.cpp b/launcher/mojang/PackageManifest.cpp similarity index 100% rename from api/logic/mojang/PackageManifest.cpp rename to launcher/mojang/PackageManifest.cpp diff --git a/api/logic/mojang/PackageManifest.h b/launcher/mojang/PackageManifest.h similarity index 92% rename from api/logic/mojang/PackageManifest.h rename to launcher/mojang/PackageManifest.h index d01a05543e..fd7ab0ad41 100644 --- a/api/logic/mojang/PackageManifest.h +++ b/launcher/mojang/PackageManifest.h @@ -6,15 +6,13 @@ #include #include "tasks/Task.h" -#include "multimc_logic_export.h" - namespace mojang_files { using Hash = QString; extern const Hash empty_hash; // simple-ish path implementation. assumes always relative and does not allow '..' entries -class MULTIMC_LOGIC_EXPORT Path +class Path { public: using parts_type = QStringList; @@ -106,7 +104,7 @@ enum class Compression { }; -struct MULTIMC_LOGIC_EXPORT FileSource +struct FileSource { Compression compression = Compression::Unknown; Hash hash; @@ -122,14 +120,14 @@ struct MULTIMC_LOGIC_EXPORT FileSource } }; -struct MULTIMC_LOGIC_EXPORT File +struct File { Hash hash; bool executable; std::uint64_t size = 0; }; -struct MULTIMC_LOGIC_EXPORT Package { +struct Package { static Package fromInspectedFolder(const QString &folderPath); static Package fromManifestFile(const QString &path); static Package fromManifestContents(const QByteArray& contents); @@ -150,7 +148,7 @@ struct MULTIMC_LOGIC_EXPORT Package { std::map symlinks; }; -struct MULTIMC_LOGIC_EXPORT FileDownload : FileSource +struct FileDownload : FileSource { FileDownload(const FileSource& source, bool executable) { static_cast (*this) = source; @@ -159,7 +157,7 @@ struct MULTIMC_LOGIC_EXPORT FileDownload : FileSource bool executable = false; }; -struct MULTIMC_LOGIC_EXPORT UpdateOperations { +struct UpdateOperations { static UpdateOperations resolve(const Package & from, const Package & to); bool valid = false; std::vector deletes; diff --git a/api/logic/mojang/PackageManifest_test.cpp b/launcher/mojang/PackageManifest_test.cpp similarity index 100% rename from api/logic/mojang/PackageManifest_test.cpp rename to launcher/mojang/PackageManifest_test.cpp diff --git a/api/logic/mojang/testdata/1.8.0_202-x64.json b/launcher/mojang/testdata/1.8.0_202-x64.json similarity index 100% rename from api/logic/mojang/testdata/1.8.0_202-x64.json rename to launcher/mojang/testdata/1.8.0_202-x64.json diff --git a/api/logic/mojang/testdata/inspect/a/b.txt b/launcher/mojang/testdata/inspect/a/b.txt similarity index 100% rename from api/logic/mojang/testdata/inspect/a/b.txt rename to launcher/mojang/testdata/inspect/a/b.txt diff --git a/api/logic/mojang/testdata/inspect/a/b/b.txt b/launcher/mojang/testdata/inspect/a/b/b.txt similarity index 100% rename from api/logic/mojang/testdata/inspect/a/b/b.txt rename to launcher/mojang/testdata/inspect/a/b/b.txt diff --git a/api/logic/mojang/testdata/inspect_win/a/b.txt b/launcher/mojang/testdata/inspect_win/a/b.txt similarity index 100% rename from api/logic/mojang/testdata/inspect_win/a/b.txt rename to launcher/mojang/testdata/inspect_win/a/b.txt diff --git a/api/logic/mojang/testdata/inspect_win/a/b/b.txt b/launcher/mojang/testdata/inspect_win/a/b/b.txt similarity index 100% rename from api/logic/mojang/testdata/inspect_win/a/b/b.txt rename to launcher/mojang/testdata/inspect_win/a/b/b.txt diff --git a/api/logic/net/ByteArraySink.h b/launcher/net/ByteArraySink.h similarity index 100% rename from api/logic/net/ByteArraySink.h rename to launcher/net/ByteArraySink.h diff --git a/api/logic/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h similarity index 100% rename from api/logic/net/ChecksumValidator.h rename to launcher/net/ChecksumValidator.h diff --git a/api/logic/net/Download.cpp b/launcher/net/Download.cpp similarity index 98% rename from api/logic/net/Download.cpp rename to launcher/net/Download.cpp index 340f865790..3f183b7d28 100644 --- a/api/logic/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -15,6 +15,7 @@ #include "Download.h" +#include "BuildConfig.h" #include #include #include @@ -94,7 +95,7 @@ void Download::start() return; } - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); QNetworkReply *rep = ENV.qnam().get(request); diff --git a/api/logic/net/Download.h b/launcher/net/Download.h similarity index 95% rename from api/logic/net/Download.h rename to launcher/net/Download.h index 2c43603297..a224bb86fe 100644 --- a/api/logic/net/Download.h +++ b/launcher/net/Download.h @@ -20,9 +20,8 @@ #include "Validator.h" #include "Sink.h" -#include "multimc_logic_export.h" namespace Net { -class MULTIMC_LOGIC_EXPORT Download : public NetAction +class Download : public NetAction { Q_OBJECT diff --git a/api/logic/net/FileSink.cpp b/launcher/net/FileSink.cpp similarity index 100% rename from api/logic/net/FileSink.cpp rename to launcher/net/FileSink.cpp diff --git a/api/logic/net/FileSink.h b/launcher/net/FileSink.h similarity index 100% rename from api/logic/net/FileSink.h rename to launcher/net/FileSink.h diff --git a/api/logic/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp similarity index 100% rename from api/logic/net/HttpMetaCache.cpp rename to launcher/net/HttpMetaCache.cpp diff --git a/api/logic/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h similarity index 96% rename from api/logic/net/HttpMetaCache.h rename to launcher/net/HttpMetaCache.h index c324879301..1c10e8c791 100644 --- a/api/logic/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -19,11 +19,9 @@ #include #include -#include "multimc_logic_export.h" - class HttpMetaCache; -class MULTIMC_LOGIC_EXPORT MetaEntry +class MetaEntry { friend class HttpMetaCache; protected: @@ -79,7 +77,7 @@ friend class HttpMetaCache; typedef std::shared_ptr MetaEntryPtr; -class MULTIMC_LOGIC_EXPORT HttpMetaCache : public QObject +class HttpMetaCache : public QObject { Q_OBJECT public: diff --git a/api/logic/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp similarity index 100% rename from api/logic/net/MetaCacheSink.cpp rename to launcher/net/MetaCacheSink.cpp diff --git a/api/logic/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h similarity index 100% rename from api/logic/net/MetaCacheSink.h rename to launcher/net/MetaCacheSink.h diff --git a/api/logic/net/Mode.h b/launcher/net/Mode.h similarity index 100% rename from api/logic/net/Mode.h rename to launcher/net/Mode.h diff --git a/api/logic/net/NetAction.h b/launcher/net/NetAction.h similarity index 96% rename from api/logic/net/NetAction.h rename to launcher/net/NetAction.h index 02b249a3e1..c13c187fce 100644 --- a/api/logic/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -21,8 +21,6 @@ #include #include -#include "multimc_logic_export.h" - enum JobStatus { Job_NotStarted, @@ -38,7 +36,7 @@ enum JobStatus }; typedef std::shared_ptr NetActionPtr; -class MULTIMC_LOGIC_EXPORT NetAction : public QObject +class NetAction : public QObject { Q_OBJECT protected: diff --git a/api/logic/net/NetJob.cpp b/launcher/net/NetJob.cpp similarity index 100% rename from api/logic/net/NetJob.cpp rename to launcher/net/NetJob.cpp diff --git a/api/logic/net/NetJob.h b/launcher/net/NetJob.h similarity index 96% rename from api/logic/net/NetJob.h rename to launcher/net/NetJob.h index 480d803758..338f8e7122 100644 --- a/api/logic/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -21,12 +21,10 @@ #include "tasks/Task.h" #include "QObjectPtr.h" -#include "multimc_logic_export.h" - class NetJob; typedef shared_qobject_ptr NetJobPtr; -class MULTIMC_LOGIC_EXPORT NetJob : public Task +class NetJob : public Task { Q_OBJECT public: diff --git a/api/logic/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp similarity index 96% rename from api/logic/net/PasteUpload.cpp rename to launcher/net/PasteUpload.cpp index 3526e20754..cb470c49af 100644 --- a/api/logic/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -5,6 +5,7 @@ #include #include #include +#include PasteUpload::PasteUpload(QWidget *window, QString text, QString key) : m_window(window) { @@ -34,7 +35,7 @@ bool PasteUpload::validateText() void PasteUpload::executeTask() { QNetworkRequest request(QUrl("https://api.paste.ee/v1/pastes")); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setRawHeader("Content-Type", "application/json"); request.setRawHeader("Content-Length", QByteArray::number(m_jsonContent.size())); diff --git a/api/logic/net/PasteUpload.h b/launcher/net/PasteUpload.h similarity index 91% rename from api/logic/net/PasteUpload.h rename to launcher/net/PasteUpload.h index 11e05c2e41..5514e058c6 100644 --- a/api/logic/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -4,9 +4,7 @@ #include #include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT PasteUpload : public Task +class PasteUpload : public Task { Q_OBJECT public: diff --git a/api/logic/net/Sink.h b/launcher/net/Sink.h similarity index 95% rename from api/logic/net/Sink.h rename to launcher/net/Sink.h index d526895c22..d367fb15c3 100644 --- a/api/logic/net/Sink.h +++ b/launcher/net/Sink.h @@ -2,11 +2,10 @@ #include "net/NetAction.h" -#include "multimc_logic_export.h" #include "Validator.h" namespace Net { -class MULTIMC_LOGIC_EXPORT Sink +class Sink { public: /* con/des */ Sink() {}; diff --git a/api/logic/net/Validator.h b/launcher/net/Validator.h similarity index 82% rename from api/logic/net/Validator.h rename to launcher/net/Validator.h index 955412cee7..59b72a0b04 100644 --- a/api/logic/net/Validator.h +++ b/launcher/net/Validator.h @@ -2,10 +2,8 @@ #include "net/NetAction.h" -#include "multimc_logic_export.h" - namespace Net { -class MULTIMC_LOGIC_EXPORT Validator +class Validator { public: /* con/des */ Validator() {}; @@ -17,4 +15,4 @@ class MULTIMC_LOGIC_EXPORT Validator virtual bool abort() = 0; virtual bool validate(QNetworkReply & reply) = 0; }; -} \ No newline at end of file +} diff --git a/api/logic/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp similarity index 100% rename from api/logic/news/NewsChecker.cpp rename to launcher/news/NewsChecker.cpp diff --git a/api/logic/news/NewsChecker.h b/launcher/news/NewsChecker.h similarity index 96% rename from api/logic/news/NewsChecker.h rename to launcher/news/NewsChecker.h index c473ecab0c..84b1f5526c 100644 --- a/api/logic/news/NewsChecker.h +++ b/launcher/news/NewsChecker.h @@ -23,9 +23,7 @@ #include "NewsEntry.h" -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT NewsChecker : public QObject +class NewsChecker : public QObject { Q_OBJECT public: diff --git a/api/logic/news/NewsEntry.cpp b/launcher/news/NewsEntry.cpp similarity index 100% rename from api/logic/news/NewsEntry.cpp rename to launcher/news/NewsEntry.cpp diff --git a/api/logic/news/NewsEntry.h b/launcher/news/NewsEntry.h similarity index 100% rename from api/logic/news/NewsEntry.h rename to launcher/news/NewsEntry.h diff --git a/api/logic/notifications/NotificationChecker.cpp b/launcher/notifications/NotificationChecker.cpp similarity index 100% rename from api/logic/notifications/NotificationChecker.cpp rename to launcher/notifications/NotificationChecker.cpp diff --git a/api/logic/notifications/NotificationChecker.h b/launcher/notifications/NotificationChecker.h similarity index 92% rename from api/logic/notifications/NotificationChecker.h rename to launcher/notifications/NotificationChecker.h index 4b1b893d1e..eb2b32a2a5 100644 --- a/api/logic/notifications/NotificationChecker.h +++ b/launcher/notifications/NotificationChecker.h @@ -5,9 +5,7 @@ #include "net/NetJob.h" #include "net/Download.h" -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT NotificationChecker : public QObject +class NotificationChecker : public QObject { Q_OBJECT diff --git a/application/package/linux/MultiMC b/launcher/package/linux/MultiMC similarity index 97% rename from application/package/linux/MultiMC rename to launcher/package/linux/MultiMC index da6373bcbf..be35e83c8e 100755 --- a/application/package/linux/MultiMC +++ b/launcher/package/linux/MultiMC @@ -35,7 +35,7 @@ if [ "x$DEPS_LIST" = "x" ]; then chmod +x "${MMC_DIR}/bin/MultiMC" # Run MultiMC - "${MMC_DIR}/bin/MultiMC" -d "${MMC_DIR}" "$@" + exec -a "${MMC_DIR}/MultiMC" "${MMC_DIR}/bin/MultiMC" -d "${MMC_DIR}" "$@" # Run MultiMC in valgrind # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${MMC_DIR}/bin/MultiMC" -d "${MMC_DIR}" "$@" @@ -45,7 +45,7 @@ if [ "x$DEPS_LIST" = "x" ]; then # use callgrind_control -i on/off to profile actions # Exit with MultiMC's exit code. - exit $? + # exit $? else # apt if which apt-file &>/dev/null; then diff --git a/application/package/linux/multimc.desktop b/launcher/package/linux/multimc.desktop similarity index 100% rename from application/package/linux/multimc.desktop rename to launcher/package/linux/multimc.desktop diff --git a/application/package/rpm/MultiMC5.spec b/launcher/package/rpm/MultiMC5.spec similarity index 88% rename from application/package/rpm/MultiMC5.spec rename to launcher/package/rpm/MultiMC5.spec index 5b72c78114..78b9000e7a 100644 --- a/application/package/rpm/MultiMC5.spec +++ b/launcher/package/rpm/MultiMC5.spec @@ -1,13 +1,13 @@ Name: MultiMC5 Version: 1.4 -Release: 1%{?dist} +Release: 2%{?dist} Summary: A local install wrapper for MultiMC License: ASL 2.0 URL: https://multimc.org BuildArch: x86_64 -Requires: zenity qt5-qtbase wget +Requires: zenity qt5-qtbase wget xrandr Provides: multimc MultiMC multimc5 %description @@ -37,6 +37,8 @@ install -m 0644 ../ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml %{buil %changelog +* Tue Jun 01 2021 kb1000 - 1.4-2 +- Add xrandr to the dependencies * Tue Dec 08 00:34:35 CET 2020 joshua-stone - Add metainfo.xml for improving package metadata diff --git a/application/package/rpm/README.md b/launcher/package/rpm/README.md similarity index 100% rename from application/package/rpm/README.md rename to launcher/package/rpm/README.md diff --git a/application/package/ubuntu/README.md b/launcher/package/ubuntu/README.md similarity index 82% rename from application/package/ubuntu/README.md rename to launcher/package/ubuntu/README.md index 5c0f4eeb24..892abd1237 100644 --- a/application/package/ubuntu/README.md +++ b/launcher/package/ubuntu/README.md @@ -6,9 +6,9 @@ It contains a `.desktop` file, an icon, and a simple script that does the heavy This is also the source for the files in the [RPM package](../rpm). If you rename, create or delete files here, you'll likely also have to update the RPM spec file there. # How to build this? -You need dpkg utils. Rename the `multimc` folder to `multimc_1.3-1` and then run: +You need dpkg utils. Rename the `multimc` folder to `multimc_1.5-1` and then run: ``` -fakeroot dpkg-deb --build multimc_1.3-1 +fakeroot dpkg-deb --build multimc_1.5-1 ``` Replace the version with whatever is appropriate. diff --git a/application/package/ubuntu/multimc/DEBIAN/control b/launcher/package/ubuntu/multimc/DEBIAN/control similarity index 96% rename from application/package/ubuntu/multimc/DEBIAN/control rename to launcher/package/ubuntu/multimc/DEBIAN/control index 509dfe3c6b..3e0f570cca 100644 --- a/application/package/ubuntu/multimc/DEBIAN/control +++ b/launcher/package/ubuntu/multimc/DEBIAN/control @@ -1,5 +1,5 @@ Package: multimc -Version: 1.4-1 +Version: 1.5-1 Architecture: all Maintainer: Petr Mrázek Section: games diff --git a/application/package/ubuntu/multimc/DEBIAN/postrm b/launcher/package/ubuntu/multimc/DEBIAN/postrm similarity index 100% rename from application/package/ubuntu/multimc/DEBIAN/postrm rename to launcher/package/ubuntu/multimc/DEBIAN/postrm diff --git a/application/package/ubuntu/multimc/opt/multimc/icon.svg b/launcher/package/ubuntu/multimc/opt/multimc/icon.svg similarity index 100% rename from application/package/ubuntu/multimc/opt/multimc/icon.svg rename to launcher/package/ubuntu/multimc/opt/multimc/icon.svg diff --git a/application/package/ubuntu/multimc/opt/multimc/run.sh b/launcher/package/ubuntu/multimc/opt/multimc/run.sh similarity index 100% rename from application/package/ubuntu/multimc/opt/multimc/run.sh rename to launcher/package/ubuntu/multimc/opt/multimc/run.sh diff --git a/application/package/ubuntu/multimc/usr/share/applications/multimc.desktop b/launcher/package/ubuntu/multimc/usr/share/applications/multimc.desktop similarity index 100% rename from application/package/ubuntu/multimc/usr/share/applications/multimc.desktop rename to launcher/package/ubuntu/multimc/usr/share/applications/multimc.desktop diff --git a/application/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml b/launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml similarity index 100% rename from application/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml rename to launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml diff --git a/application/pagedialog/PageDialog.cpp b/launcher/pagedialog/PageDialog.cpp similarity index 100% rename from application/pagedialog/PageDialog.cpp rename to launcher/pagedialog/PageDialog.cpp diff --git a/application/pagedialog/PageDialog.h b/launcher/pagedialog/PageDialog.h similarity index 100% rename from application/pagedialog/PageDialog.h rename to launcher/pagedialog/PageDialog.h diff --git a/application/pages/BasePage.h b/launcher/pages/BasePage.h similarity index 100% rename from application/pages/BasePage.h rename to launcher/pages/BasePage.h diff --git a/application/pages/BasePageContainer.h b/launcher/pages/BasePageContainer.h similarity index 100% rename from application/pages/BasePageContainer.h rename to launcher/pages/BasePageContainer.h diff --git a/application/pages/BasePageProvider.h b/launcher/pages/BasePageProvider.h similarity index 100% rename from application/pages/BasePageProvider.h rename to launcher/pages/BasePageProvider.h diff --git a/application/pages/global/AccountListPage.cpp b/launcher/pages/global/AccountListPage.cpp similarity index 64% rename from application/pages/global/AccountListPage.cpp rename to launcher/pages/global/AccountListPage.cpp index ff3736ed21..745377126f 100644 --- a/application/pages/global/AccountListPage.cpp +++ b/launcher/pages/global/AccountListPage.cpp @@ -29,12 +29,15 @@ #include "dialogs/CustomMessageBox.h" #include "dialogs/SkinUploadDialog.h" #include "tasks/Task.h" -#include "minecraft/auth/YggdrasilTask.h" +#include "minecraft/auth/AccountTask.h" #include "minecraft/services/SkinDelete.h" #include "MultiMC.h" #include "BuildConfig.h" +#include + +#include "Secrets.h" AccountListPage::AccountListPage(QWidget *parent) : QMainWindow(parent), ui(new Ui::AccountListPage) @@ -50,11 +53,12 @@ AccountListPage::AccountListPage(QWidget *parent) m_accounts = MMC->accounts(); ui->listView->setModel(m_accounts.get()); - ui->listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch); + ui->listView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); // Expand the account column - ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch); QItemSelectionModel *selectionModel = ui->listView->selectionModel(); @@ -63,10 +67,13 @@ AccountListPage::AccountListPage(QWidget *parent) }); connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu); - connect(m_accounts.get(), SIGNAL(listChanged()), SLOT(listChanged())); - connect(m_accounts.get(), SIGNAL(activeAccountChanged()), SLOT(listChanged())); + connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); + connect(m_accounts.get(), &AccountList::activeAccountChanged, this, &AccountListPage::listChanged); updateButtonStates(); + + // Xbox authentication won't work without a client identifier, so disable the button if it is missing + ui->actionAddMicrosoft->setVisible(Secrets::hasMSAClientID()); } AccountListPage::~AccountListPage() @@ -103,9 +110,48 @@ void AccountListPage::listChanged() updateButtonStates(); } -void AccountListPage::on_actionAdd_triggered() +void AccountListPage::on_actionAddMojang_triggered() { - addAccount(tr("Please enter your Minecraft account email and password to add your account.")); + MinecraftAccountPtr account = LoginDialog::newAccount( + this, + tr("Please enter your Mojang account email and password to add your account.") + ); + + if (account != nullptr) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setActiveAccount(account->profileId()); + } + } +} + +void AccountListPage::on_actionAddMicrosoft_triggered() +{ + if(BuildConfig.BUILD_PLATFORM == "osx64") { + CustomMessageBox::selectable( + this, + tr("Microsoft Accounts not available"), + tr( + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n" + "Please update both your operating system and MultiMC." + ), + QMessageBox::Warning + )->exec(); + return; + } + MinecraftAccountPtr account = MSALoginDialog::newAccount( + this, + tr("Please enter your Mojang account email and password to add your account.") + ); + + if (account != nullptr) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setActiveAccount(account->profileId()); + } + } } void AccountListPage::on_actionRemove_triggered() @@ -118,15 +164,30 @@ void AccountListPage::on_actionRemove_triggered() } } +void AccountListPage::on_actionRefresh_triggered() { + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + AuthSessionPtr session = std::make_shared(); + auto task = account->refresh(session); + if (task) { + ProgressDialog progDialog(this); + progDialog.execWithTask(task.get()); + // TODO: respond to results of the task + } + } +} + + void AccountListPage::on_actionSetDefault_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); - MojangAccountPtr account = - selected.data(MojangAccountList::PointerRole).value(); - m_accounts->setActiveAccount(account->username()); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + m_accounts->setActiveAccount(account->profileId()); } } @@ -144,6 +205,7 @@ void AccountListPage::updateButtonStates() ui->actionSetDefault->setEnabled(selection.size() > 0); ui->actionUploadSkin->setEnabled(selection.size() > 0); ui->actionDeleteSkin->setEnabled(selection.size() > 0); + ui->actionRefresh->setEnabled(selection.size() > 0); if(m_accounts->activeAccount().get() == nullptr) { ui->actionNoDefault->setEnabled(false); @@ -156,39 +218,13 @@ void AccountListPage::updateButtonStates() } -void AccountListPage::addAccount(const QString &errMsg) -{ - // TODO: The login dialog isn't quite done yet - MojangAccountPtr account = LoginDialog::newAccount(this, errMsg); - - if (account != nullptr) - { - m_accounts->addAccount(account); - if (m_accounts->count() == 1) - m_accounts->setActiveAccount(account->username()); - - // Grab associated player skins - auto job = new NetJob("Player skins: " + account->username()); - - for (AccountProfile profile : account->profiles()) - { - auto meta = Env::getInstance().metacache()->resolveEntry("skins", profile.id + ".png"); - auto action = Net::Download::makeCached(QUrl(BuildConfig.SKINS_BASE + profile.id + ".png"), meta); - job->addNetAction(action); - meta->setStale(true); - } - - job->start(); - } -} - void AccountListPage::on_actionUploadSkin_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); - MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); SkinUploadDialog dialog(account, this); dialog.exec(); } @@ -202,8 +238,8 @@ void AccountListPage::on_actionDeleteSkin_triggered() QModelIndex selected = selection.first(); AuthSessionPtr session = std::make_shared(); - MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value(); - auto login = account->login(session); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + auto login = account->refresh(session); ProgressDialog prog(this); if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec(); diff --git a/application/pages/global/AccountListPage.h b/launcher/pages/global/AccountListPage.h similarity index 90% rename from application/pages/global/AccountListPage.h rename to launcher/pages/global/AccountListPage.h index fba1833f89..4474802e13 100644 --- a/application/pages/global/AccountListPage.h +++ b/launcher/pages/global/AccountListPage.h @@ -20,7 +20,7 @@ #include "pages/BasePage.h" -#include "minecraft/auth/MojangAccountList.h" +#include "minecraft/auth/AccountList.h" #include "MultiMC.h" namespace Ui @@ -60,8 +60,10 @@ class AccountListPage : public QMainWindow, public BasePage } public slots: - void on_actionAdd_triggered(); + void on_actionAddMojang_triggered(); + void on_actionAddMicrosoft_triggered(); void on_actionRemove_triggered(); + void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); void on_actionNoDefault_triggered(); void on_actionUploadSkin_triggered(); @@ -74,11 +76,10 @@ public slots: protected slots: void ShowContextMenu(const QPoint &pos); - void addAccount(const QString& errMsg=""); private: void changeEvent(QEvent * event) override; QMenu * createPopupMenu() override; - std::shared_ptr m_accounts; + std::shared_ptr m_accounts; Ui::AccountListPage *ui; }; diff --git a/application/pages/global/AccountListPage.ui b/launcher/pages/global/AccountListPage.ui similarity index 69% rename from application/pages/global/AccountListPage.ui rename to launcher/pages/global/AccountListPage.ui index 71647db3dd..8af23a2f06 100644 --- a/application/pages/global/AccountListPage.ui +++ b/launcher/pages/global/AccountListPage.ui @@ -25,7 +25,23 @@ 0
- + + + true + + + false + + + false + + + true + + + false + +
@@ -36,7 +52,9 @@ false - + + + @@ -44,9 +62,9 @@ - + - Add + Add Mojang @@ -80,6 +98,19 @@ Delete the currently active skin and go back to the default one + + + Add Microsoft + + + + + Refresh + + + Refresh the account tokens + + diff --git a/application/pages/global/CustomCommandsPage.cpp b/launcher/pages/global/CustomCommandsPage.cpp similarity index 100% rename from application/pages/global/CustomCommandsPage.cpp rename to launcher/pages/global/CustomCommandsPage.cpp diff --git a/application/pages/global/CustomCommandsPage.h b/launcher/pages/global/CustomCommandsPage.h similarity index 100% rename from application/pages/global/CustomCommandsPage.h rename to launcher/pages/global/CustomCommandsPage.h diff --git a/application/pages/global/ExternalToolsPage.cpp b/launcher/pages/global/ExternalToolsPage.cpp similarity index 100% rename from application/pages/global/ExternalToolsPage.cpp rename to launcher/pages/global/ExternalToolsPage.cpp diff --git a/application/pages/global/ExternalToolsPage.h b/launcher/pages/global/ExternalToolsPage.h similarity index 100% rename from application/pages/global/ExternalToolsPage.h rename to launcher/pages/global/ExternalToolsPage.h diff --git a/application/pages/global/ExternalToolsPage.ui b/launcher/pages/global/ExternalToolsPage.ui similarity index 100% rename from application/pages/global/ExternalToolsPage.ui rename to launcher/pages/global/ExternalToolsPage.ui diff --git a/application/pages/global/JavaPage.cpp b/launcher/pages/global/JavaPage.cpp similarity index 97% rename from application/pages/global/JavaPage.cpp rename to launcher/pages/global/JavaPage.cpp index 95271c9179..cde0e035b6 100644 --- a/application/pages/global/JavaPage.cpp +++ b/launcher/pages/global/JavaPage.cpp @@ -37,8 +37,8 @@ JavaPage::JavaPage(QWidget *parent) : QWidget(parent), ui(new Ui::JavaPage) ui->setupUi(this); ui->tabWidget->tabBar()->hide(); - auto sysMB = Sys::getSystemRam() / Sys::megabyte; - ui->maxMemSpinBox->setMaximum(sysMB); + auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; + ui->maxMemSpinBox->setMaximum(sysMiB); loadSettings(); } diff --git a/application/pages/global/JavaPage.h b/launcher/pages/global/JavaPage.h similarity index 100% rename from application/pages/global/JavaPage.h rename to launcher/pages/global/JavaPage.h diff --git a/application/pages/global/JavaPage.ui b/launcher/pages/global/JavaPage.ui similarity index 98% rename from application/pages/global/JavaPage.ui rename to launcher/pages/global/JavaPage.ui index 201b310c3d..b67e9994ed 100644 --- a/application/pages/global/JavaPage.ui +++ b/launcher/pages/global/JavaPage.ui @@ -51,7 +51,7 @@ The maximum amount of memory Minecraft is allowed to use. - MB + MiB 128 @@ -87,7 +87,7 @@ The amount of memory Minecraft is started with. - MB + MiB 128 @@ -116,7 +116,7 @@ The amount of memory available to store loaded Java classes. - MB + MiB 64 diff --git a/application/pages/global/LanguagePage.cpp b/launcher/pages/global/LanguagePage.cpp similarity index 100% rename from application/pages/global/LanguagePage.cpp rename to launcher/pages/global/LanguagePage.cpp diff --git a/application/pages/global/LanguagePage.h b/launcher/pages/global/LanguagePage.h similarity index 100% rename from application/pages/global/LanguagePage.h rename to launcher/pages/global/LanguagePage.h diff --git a/application/pages/global/MinecraftPage.cpp b/launcher/pages/global/MinecraftPage.cpp similarity index 89% rename from application/pages/global/MinecraftPage.cpp rename to launcher/pages/global/MinecraftPage.cpp index 6a780fb442..6c9bd3074f 100644 --- a/application/pages/global/MinecraftPage.cpp +++ b/launcher/pages/global/MinecraftPage.cpp @@ -67,6 +67,10 @@ void MinecraftPage::applySettings() // Native library workarounds s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + + // Game time + s->set("ShowGameTime", ui->showGameTime->isChecked()); + s->set("RecordGameTime", ui->recordGameTime->isChecked()); } void MinecraftPage::loadSettings() @@ -80,4 +84,7 @@ void MinecraftPage::loadSettings() ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool()); + + ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); + ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); } diff --git a/application/pages/global/MinecraftPage.h b/launcher/pages/global/MinecraftPage.h similarity index 100% rename from application/pages/global/MinecraftPage.h rename to launcher/pages/global/MinecraftPage.h diff --git a/application/pages/global/MinecraftPage.ui b/launcher/pages/global/MinecraftPage.ui similarity index 87% rename from application/pages/global/MinecraftPage.ui rename to launcher/pages/global/MinecraftPage.ui index c096c96965..2abd4bd4c4 100644 --- a/application/pages/global/MinecraftPage.ui +++ b/launcher/pages/global/MinecraftPage.ui @@ -134,6 +134,29 @@
+ + + + Game time + + + + + + Show time spent playing instances + + + + + + + Record time spent playing instances + + + + + + diff --git a/application/pages/global/MultiMCPage.cpp b/launcher/pages/global/MultiMCPage.cpp similarity index 96% rename from application/pages/global/MultiMCPage.cpp rename to launcher/pages/global/MultiMCPage.cpp index 80d5c5442b..5d43b18707 100644 --- a/application/pages/global/MultiMCPage.cpp +++ b/launcher/pages/global/MultiMCPage.cpp @@ -29,6 +29,9 @@ #include "BuildConfig.h" #include "themes/ITheme.h" +#include +#include + // FIXME: possibly move elsewhere enum InstSortMode { @@ -55,8 +58,7 @@ MultiMCPage::MultiMCPage(QWidget *parent) : QWidget(parent), ui(new Ui::MultiMCP if(BuildConfig.UPDATER_ENABLED) { - QObject::connect(MMC->updateChecker().get(), &UpdateChecker::channelListLoaded, this, - &MultiMCPage::refreshUpdateChannelList); + QObject::connect(MMC->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &MultiMCPage::refreshUpdateChannelList); if (MMC->updateChecker()->hasChannels()) { @@ -78,6 +80,13 @@ MultiMCPage::MultiMCPage(QWidget *parent) : QWidget(parent), ui(new Ui::MultiMCP } connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); + + //move mac data button + QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); + if (!file.exists()) + { + ui->migrateDataFolderMacBtn->setVisible(false); + } } MultiMCPage::~MultiMCPage() @@ -146,6 +155,13 @@ void MultiMCPage::on_modsDirBrowseBtn_clicked() ui->modsDirTextBox->setText(cooked_dir); } } +void MultiMCPage::on_migrateDataFolderMacBtn_clicked() +{ + QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); + file.remove(); + QProcess::startDetached(qApp->arguments()[0]); + qApp->quit(); +} void MultiMCPage::refreshUpdateChannelList() { diff --git a/application/pages/global/MultiMCPage.h b/launcher/pages/global/MultiMCPage.h similarity index 97% rename from application/pages/global/MultiMCPage.h rename to launcher/pages/global/MultiMCPage.h index e81832eb36..fae75bf22a 100644 --- a/application/pages/global/MultiMCPage.h +++ b/launcher/pages/global/MultiMCPage.h @@ -67,6 +67,7 @@ private void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); + void on_migrateDataFolderMacBtn_clicked(); /*! * Updates the list of update channels in the combo box. diff --git a/application/pages/global/MultiMCPage.ui b/launcher/pages/global/MultiMCPage.ui similarity index 98% rename from application/pages/global/MultiMCPage.ui rename to launcher/pages/global/MultiMCPage.ui index ea03491936..4ad2024257 100644 --- a/application/pages/global/MultiMCPage.ui +++ b/launcher/pages/global/MultiMCPage.ui @@ -157,6 +157,13 @@ + + + + Move MultiMC data to new location (will restart MultiMC) + + + diff --git a/application/pages/global/PasteEEPage.cpp b/launcher/pages/global/PasteEEPage.cpp similarity index 100% rename from application/pages/global/PasteEEPage.cpp rename to launcher/pages/global/PasteEEPage.cpp diff --git a/application/pages/global/PasteEEPage.h b/launcher/pages/global/PasteEEPage.h similarity index 100% rename from application/pages/global/PasteEEPage.h rename to launcher/pages/global/PasteEEPage.h diff --git a/application/pages/global/PasteEEPage.ui b/launcher/pages/global/PasteEEPage.ui similarity index 100% rename from application/pages/global/PasteEEPage.ui rename to launcher/pages/global/PasteEEPage.ui diff --git a/application/pages/global/ProxyPage.cpp b/launcher/pages/global/ProxyPage.cpp similarity index 100% rename from application/pages/global/ProxyPage.cpp rename to launcher/pages/global/ProxyPage.cpp diff --git a/application/pages/global/ProxyPage.h b/launcher/pages/global/ProxyPage.h similarity index 100% rename from application/pages/global/ProxyPage.h rename to launcher/pages/global/ProxyPage.h diff --git a/application/pages/global/ProxyPage.ui b/launcher/pages/global/ProxyPage.ui similarity index 100% rename from application/pages/global/ProxyPage.ui rename to launcher/pages/global/ProxyPage.ui diff --git a/application/pages/instance/GameOptionsPage.cpp b/launcher/pages/instance/GameOptionsPage.cpp similarity index 96% rename from application/pages/instance/GameOptionsPage.cpp rename to launcher/pages/instance/GameOptionsPage.cpp index 5555fc79ca..782f2ab335 100644 --- a/application/pages/instance/GameOptionsPage.cpp +++ b/launcher/pages/instance/GameOptionsPage.cpp @@ -35,6 +35,3 @@ void GameOptionsPage::closedImpl() { // m_model->unobserve(); } - -#include "GameOptionsPage.moc" - diff --git a/application/pages/instance/GameOptionsPage.h b/launcher/pages/instance/GameOptionsPage.h similarity index 100% rename from application/pages/instance/GameOptionsPage.h rename to launcher/pages/instance/GameOptionsPage.h diff --git a/application/pages/instance/GameOptionsPage.ui b/launcher/pages/instance/GameOptionsPage.ui similarity index 100% rename from application/pages/instance/GameOptionsPage.ui rename to launcher/pages/instance/GameOptionsPage.ui diff --git a/application/pages/instance/InstanceSettingsPage.cpp b/launcher/pages/instance/InstanceSettingsPage.cpp similarity index 88% rename from application/pages/instance/InstanceSettingsPage.cpp rename to launcher/pages/instance/InstanceSettingsPage.cpp index 9a39b03430..7bd424c055 100644 --- a/application/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/pages/instance/InstanceSettingsPage.cpp @@ -19,7 +19,7 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) { m_settings = inst->settings(); ui->setupUi(this); - auto sysMB = Sys::getSystemRam() / Sys::megabyte; + auto sysMB = Sys::getSystemRam() / Sys::mebibyte; ui->maxMemSpinBox->setMaximum(sysMB); connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); connect(MMC, &MultiMC::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); @@ -177,6 +177,32 @@ void InstanceSettingsPage::applySettings() m_settings->reset("UseNativeOpenAL"); m_settings->reset("UseNativeGLFW"); } + + // Game time + bool gameTime = ui->gameTimeGroupBox->isChecked(); + m_settings->set("OverrideGameTime", gameTime); + if (gameTime) + { + m_settings->set("ShowGameTime", ui->showGameTime->isChecked()); + m_settings->set("RecordGameTime", ui->recordGameTime->isChecked()); + } + else + { + m_settings->reset("ShowGameTime"); + m_settings->reset("RecordGameTime"); + } + + // Join server on launch + bool joinServerOnLaunch = ui->serverJoinGroupBox->isChecked(); + m_settings->set("JoinServerOnLaunch", joinServerOnLaunch); + if (joinServerOnLaunch) + { + m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); + } + else + { + m_settings->reset("JoinServerOnLaunchAddress"); + } } void InstanceSettingsPage::loadSettings() @@ -238,6 +264,14 @@ void InstanceSettingsPage::loadSettings() ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool()); ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool()); ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool()); + + // Miscellanous + ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool()); + ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool()); + ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool()); + + ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); + ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); } void InstanceSettingsPage::on_javaDetectBtn_clicked() diff --git a/application/pages/instance/InstanceSettingsPage.h b/launcher/pages/instance/InstanceSettingsPage.h similarity index 100% rename from application/pages/instance/InstanceSettingsPage.h rename to launcher/pages/instance/InstanceSettingsPage.h diff --git a/application/pages/instance/InstanceSettingsPage.ui b/launcher/pages/instance/InstanceSettingsPage.ui similarity index 82% rename from application/pages/instance/InstanceSettingsPage.ui rename to launcher/pages/instance/InstanceSettingsPage.ui index c91570c65f..e569ce5687 100644 --- a/application/pages/instance/InstanceSettingsPage.ui +++ b/launcher/pages/instance/InstanceSettingsPage.ui @@ -116,7 +116,7 @@ The maximum amount of memory Minecraft is allowed to use. - MB + MiB 128 @@ -138,7 +138,7 @@ The amount of memory Minecraft is started with. - MB + MiB 128 @@ -160,7 +160,7 @@ The amount of memory available to store loaded Java classes. - MB + MiB 64 @@ -416,6 +416,93 @@ + + + Miscellanous + + + + + + true + + + Override global game time settings + + + true + + + false + + + + + + Show time spent playing this instance + + + + + + + Record time spent playing this instance + + + + + + + + + + Set a server to join on launch + + + true + + + false + + + + + + + + + 0 + 0 + + + + Server address: + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + @@ -453,6 +540,8 @@ nativeWorkaroundsGroupBox useNativeGLFWCheck useNativeOpenALCheck + showGameTime + recordGameTime diff --git a/application/pages/instance/LegacyUpgradePage.cpp b/launcher/pages/instance/LegacyUpgradePage.cpp similarity index 100% rename from application/pages/instance/LegacyUpgradePage.cpp rename to launcher/pages/instance/LegacyUpgradePage.cpp diff --git a/application/pages/instance/LegacyUpgradePage.h b/launcher/pages/instance/LegacyUpgradePage.h similarity index 100% rename from application/pages/instance/LegacyUpgradePage.h rename to launcher/pages/instance/LegacyUpgradePage.h diff --git a/application/pages/instance/LegacyUpgradePage.ui b/launcher/pages/instance/LegacyUpgradePage.ui similarity index 100% rename from application/pages/instance/LegacyUpgradePage.ui rename to launcher/pages/instance/LegacyUpgradePage.ui diff --git a/application/pages/instance/LogPage.cpp b/launcher/pages/instance/LogPage.cpp similarity index 96% rename from application/pages/instance/LogPage.cpp rename to launcher/pages/instance/LogPage.cpp index 94ada42480..3d2085c6ff 100644 --- a/application/pages/instance/LogPage.cpp +++ b/launcher/pages/instance/LogPage.cpp @@ -236,15 +236,15 @@ void LogPage::on_btnPaste_clicked() return; //FIXME: turn this into a proper task and move the upload logic out of GuiUtil! - m_model->append(MessageLevel::MultiMC, tr("MultiMC: Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + m_model->append(MessageLevel::MultiMC, QString("MultiMC: Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this); if(!url.isEmpty()) { - m_model->append(MessageLevel::MultiMC, tr("MultiMC: Log uploaded to: %1").arg(url)); + m_model->append(MessageLevel::MultiMC, QString("MultiMC: Log uploaded to: %1").arg(url)); } else { - m_model->append(MessageLevel::Error, tr("MultiMC: Log upload failed!")); + m_model->append(MessageLevel::Error, "MultiMC: Log upload failed!"); } } diff --git a/application/pages/instance/LogPage.h b/launcher/pages/instance/LogPage.h similarity index 100% rename from application/pages/instance/LogPage.h rename to launcher/pages/instance/LogPage.h diff --git a/application/pages/instance/LogPage.ui b/launcher/pages/instance/LogPage.ui similarity index 100% rename from application/pages/instance/LogPage.ui rename to launcher/pages/instance/LogPage.ui diff --git a/application/pages/instance/ModFolderPage.cpp b/launcher/pages/instance/ModFolderPage.cpp similarity index 99% rename from application/pages/instance/ModFolderPage.cpp rename to launcher/pages/instance/ModFolderPage.cpp index c3d6483a95..98f20e77ca 100644 --- a/application/pages/instance/ModFolderPage.cpp +++ b/launcher/pages/instance/ModFolderPage.cpp @@ -163,7 +163,7 @@ ModFolderPage::ModFolderPage( auto smodel = ui->modTreeView->selectionModel(); connect(smodel, &QItemSelectionModel::currentChanged, this, &ModFolderPage::modCurrent); - connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged ); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged); connect(m_inst, &BaseInstance::runningStatusChanged, this, &ModFolderPage::on_RunningState_changed); } diff --git a/application/pages/instance/ModFolderPage.h b/launcher/pages/instance/ModFolderPage.h similarity index 100% rename from application/pages/instance/ModFolderPage.h rename to launcher/pages/instance/ModFolderPage.h diff --git a/application/pages/instance/ModFolderPage.ui b/launcher/pages/instance/ModFolderPage.ui similarity index 100% rename from application/pages/instance/ModFolderPage.ui rename to launcher/pages/instance/ModFolderPage.ui diff --git a/application/pages/instance/NotesPage.cpp b/launcher/pages/instance/NotesPage.cpp similarity index 100% rename from application/pages/instance/NotesPage.cpp rename to launcher/pages/instance/NotesPage.cpp diff --git a/application/pages/instance/NotesPage.h b/launcher/pages/instance/NotesPage.h similarity index 100% rename from application/pages/instance/NotesPage.h rename to launcher/pages/instance/NotesPage.h diff --git a/application/pages/instance/NotesPage.ui b/launcher/pages/instance/NotesPage.ui similarity index 100% rename from application/pages/instance/NotesPage.ui rename to launcher/pages/instance/NotesPage.ui diff --git a/application/pages/instance/OtherLogsPage.cpp b/launcher/pages/instance/OtherLogsPage.cpp similarity index 100% rename from application/pages/instance/OtherLogsPage.cpp rename to launcher/pages/instance/OtherLogsPage.cpp diff --git a/application/pages/instance/OtherLogsPage.h b/launcher/pages/instance/OtherLogsPage.h similarity index 100% rename from application/pages/instance/OtherLogsPage.h rename to launcher/pages/instance/OtherLogsPage.h diff --git a/application/pages/instance/OtherLogsPage.ui b/launcher/pages/instance/OtherLogsPage.ui similarity index 100% rename from application/pages/instance/OtherLogsPage.ui rename to launcher/pages/instance/OtherLogsPage.ui diff --git a/application/pages/instance/ResourcePackPage.h b/launcher/pages/instance/ResourcePackPage.h similarity index 99% rename from application/pages/instance/ResourcePackPage.h rename to launcher/pages/instance/ResourcePackPage.h index e11c78a3da..1486bf5263 100644 --- a/application/pages/instance/ResourcePackPage.h +++ b/launcher/pages/instance/ResourcePackPage.h @@ -1,4 +1,5 @@ #pragma once + #include "ModFolderPage.h" #include "ui_ModFolderPage.h" @@ -12,8 +13,8 @@ class ResourcePackPage : public ModFolderPage { ui->actionView_configs->setVisible(false); } - virtual ~ResourcePackPage() {} + virtual bool shouldDisplay() const override { return !m_inst->traits().contains("no-texturepacks") && diff --git a/application/pages/instance/ScreenshotsPage.cpp b/launcher/pages/instance/ScreenshotsPage.cpp similarity index 100% rename from application/pages/instance/ScreenshotsPage.cpp rename to launcher/pages/instance/ScreenshotsPage.cpp diff --git a/application/pages/instance/ScreenshotsPage.h b/launcher/pages/instance/ScreenshotsPage.h similarity index 100% rename from application/pages/instance/ScreenshotsPage.h rename to launcher/pages/instance/ScreenshotsPage.h diff --git a/application/pages/instance/ScreenshotsPage.ui b/launcher/pages/instance/ScreenshotsPage.ui similarity index 100% rename from application/pages/instance/ScreenshotsPage.ui rename to launcher/pages/instance/ScreenshotsPage.ui diff --git a/application/pages/instance/ServersPage.cpp b/launcher/pages/instance/ServersPage.cpp similarity index 97% rename from application/pages/instance/ServersPage.cpp rename to launcher/pages/instance/ServersPage.cpp index 8b0c655c24..d63c6e7006 100644 --- a/application/pages/instance/ServersPage.cpp +++ b/launcher/pages/instance/ServersPage.cpp @@ -556,7 +556,7 @@ private slots: QTimer m_saveTimer; }; -ServersPage::ServersPage(MinecraftInstance * inst, QWidget* parent) +ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage) { ui->setupUi(this); @@ -579,7 +579,7 @@ ServersPage::ServersPage(MinecraftInstance * inst, QWidget* parent) auto selectionModel = ui->serversView->selectionModel(); connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged); - connect(m_inst, &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed); + connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed); connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); @@ -695,6 +695,7 @@ void ServersPage::updateState() ui->actionMove_Down->setEnabled(serverEditEnabled); ui->actionMove_Up->setEnabled(serverEditEnabled); ui->actionRemove->setEnabled(serverEditEnabled); + ui->actionJoin->setEnabled(serverEditEnabled); if(server) { @@ -758,4 +759,10 @@ void ServersPage::on_actionMove_Down_triggered() } } +void ServersPage::on_actionJoin_triggered() +{ + const auto &address = m_model->at(currentServer)->m_address; + MMC->launch(m_inst, true, nullptr, std::make_shared(MinecraftServerTarget::parse(address))); +} + #include "ServersPage.moc" diff --git a/application/pages/instance/ServersPage.h b/launcher/pages/instance/ServersPage.h similarity index 94% rename from application/pages/instance/ServersPage.h rename to launcher/pages/instance/ServersPage.h index f164da1e57..8c5b7eb884 100644 --- a/application/pages/instance/ServersPage.h +++ b/launcher/pages/instance/ServersPage.h @@ -35,7 +35,7 @@ class ServersPage : public QMainWindow, public BasePage Q_OBJECT public: - explicit ServersPage(MinecraftInstance *inst, QWidget *parent = 0); + explicit ServersPage(InstancePtr inst, QWidget *parent = 0); virtual ~ServersPage(); void openedImpl() override; @@ -74,6 +74,7 @@ private slots: void on_actionRemove_triggered(); void on_actionMove_Up_triggered(); void on_actionMove_Down_triggered(); + void on_actionJoin_triggered(); void on_RunningState_changed(bool running); @@ -88,6 +89,6 @@ private slots: bool m_locked = true; Ui::ServersPage *ui = nullptr; ServersModel * m_model = nullptr; - MinecraftInstance * m_inst = nullptr; + InstancePtr m_inst = nullptr; }; diff --git a/application/pages/instance/ServersPage.ui b/launcher/pages/instance/ServersPage.ui similarity index 97% rename from application/pages/instance/ServersPage.ui rename to launcher/pages/instance/ServersPage.ui index e9518e3598..d89b7cba63 100644 --- a/application/pages/instance/ServersPage.ui +++ b/launcher/pages/instance/ServersPage.ui @@ -148,6 +148,7 @@ + @@ -169,6 +170,11 @@ Move Down + + + Join + + diff --git a/application/pages/instance/TexturePackPage.h b/launcher/pages/instance/TexturePackPage.h similarity index 99% rename from application/pages/instance/TexturePackPage.h rename to launcher/pages/instance/TexturePackPage.h index a792ba0769..3f04997d2c 100644 --- a/application/pages/instance/TexturePackPage.h +++ b/launcher/pages/instance/TexturePackPage.h @@ -1,4 +1,5 @@ #pragma once + #include "ModFolderPage.h" #include "ui_ModFolderPage.h" @@ -13,6 +14,7 @@ class TexturePackPage : public ModFolderPage ui->actionView_configs->setVisible(false); } virtual ~TexturePackPage() {} + virtual bool shouldDisplay() const override { return m_inst->traits().contains("texturepacks"); diff --git a/application/pages/instance/VersionPage.cpp b/launcher/pages/instance/VersionPage.cpp similarity index 94% rename from application/pages/instance/VersionPage.cpp rename to launcher/pages/instance/VersionPage.cpp index 54d75d0663..20cb2c9f45 100644 --- a/application/pages/instance/VersionPage.cpp +++ b/launcher/pages/instance/VersionPage.cpp @@ -38,7 +38,7 @@ #include #include "minecraft/PackProfile.h" -#include "minecraft/auth/MojangAccountList.h" +#include "minecraft/auth/AccountList.h" #include "minecraft/mod/Mod.h" #include "icons/IconList.h" #include "Exception.h" @@ -120,7 +120,15 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) auto proxy = new IconProxy(ui->packageView); proxy->setSourceModel(m_profile.get()); - ui->packageView->setModel(proxy); + + m_filterModel = new QSortFilterProxyModel(); + m_filterModel->setDynamicSortFilter(true); + m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSourceModel(proxy); + m_filterModel->setFilterKeyColumn(-1); + + ui->packageView->setModel(m_filterModel); ui->packageView->installEventFilter(this); ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection); ui->packageView->setContextMenuPolicy(Qt::CustomContextMenu); @@ -134,7 +142,8 @@ VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) updateVersionControls(); preselect(0); connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus); - connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::ShowContextMenu); + connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged); } VersionPage::~VersionPage() @@ -142,7 +151,7 @@ VersionPage::~VersionPage() delete ui; } -void VersionPage::ShowContextMenu(const QPoint& pos) +void VersionPage::showContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); menu->exec(ui->packageView->mapToGlobal(pos)); @@ -203,12 +212,16 @@ void VersionPage::updateVersionControls() { // FIXME: this is a dirty hack auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft")); - bool newCraft = controlsEnabled && (minecraftVersion >= Version("1.14")); - bool oldCraft = controlsEnabled && (minecraftVersion <= Version("1.12.2")); - ui->actionInstall_Fabric->setEnabled(newCraft); - ui->actionInstall_Forge->setEnabled(true); - ui->actionInstall_LiteLoader->setEnabled(oldCraft); - ui->actionReload->setEnabled(true); + + bool supportsFabric = minecraftVersion >= Version("1.14"); + ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); + + bool supportsForge = minecraftVersion <= Version("1.16.5"); + ui->actionInstall_Forge->setEnabled(controlsEnabled && supportsForge); + + bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); + ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader); + updateButtons(); } @@ -620,5 +633,10 @@ void VersionPage::on_actionRevert_triggered() m_container->refreshContainer(); } +void VersionPage::onFilterTextChanged(const QString &newContents) +{ + m_filterModel->setFilterFixedString(newContents); +} + #include "VersionPage.moc" diff --git a/application/pages/instance/VersionPage.h b/launcher/pages/instance/VersionPage.h similarity index 95% rename from application/pages/instance/VersionPage.h rename to launcher/pages/instance/VersionPage.h index dbd9c1ee31..b5b4a6f5d5 100644 --- a/application/pages/instance/VersionPage.h +++ b/launcher/pages/instance/VersionPage.h @@ -86,6 +86,7 @@ private slots: private: Ui::VersionPage *ui; + QSortFilterProxyModel *m_filterModel; std::shared_ptr m_profile; MinecraftInstance *m_inst; int currentIdx = 0; @@ -98,5 +99,6 @@ private slots: void updateRunningStatus(bool running); void onGameUpdateError(QString error); void packageCurrent(const QModelIndex ¤t, const QModelIndex &previous); - void ShowContextMenu(const QPoint &pos); + void showContextMenu(const QPoint &pos); + void onFilterTextChanged(const QString & newContents); }; diff --git a/application/pages/instance/VersionPage.ui b/launcher/pages/instance/VersionPage.ui similarity index 93% rename from application/pages/instance/VersionPage.ui rename to launcher/pages/instance/VersionPage.ui index 718ad067d3..84d06e2e82 100644 --- a/application/pages/instance/VersionPage.ui +++ b/launcher/pages/instance/VersionPage.ui @@ -45,6 +45,24 @@ + + + + + + true + + + + + + + Filter: + + + + + diff --git a/application/pages/instance/WorldListPage.cpp b/launcher/pages/instance/WorldListPage.cpp similarity index 99% rename from application/pages/instance/WorldListPage.cpp rename to launcher/pages/instance/WorldListPage.cpp index d18c535560..119cff3eb1 100644 --- a/application/pages/instance/WorldListPage.cpp +++ b/launcher/pages/instance/WorldListPage.cpp @@ -314,6 +314,7 @@ void WorldListPage::worldChanged(const QModelIndex ¤t, const QModelIndex & ui->actionRemove->setEnabled(enable); ui->actionCopy->setEnabled(enable); ui->actionRename->setEnabled(enable); + ui->actionDatapacks->setEnabled(enable); bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); ui->actionReset_Icon->setEnabled(enable && hasIcon); } diff --git a/application/pages/instance/WorldListPage.h b/launcher/pages/instance/WorldListPage.h similarity index 100% rename from application/pages/instance/WorldListPage.h rename to launcher/pages/instance/WorldListPage.h diff --git a/application/pages/instance/WorldListPage.ui b/launcher/pages/instance/WorldListPage.ui similarity index 100% rename from application/pages/instance/WorldListPage.ui rename to launcher/pages/instance/WorldListPage.ui diff --git a/application/pages/modplatform/ImportPage.cpp b/launcher/pages/modplatform/ImportPage.cpp similarity index 96% rename from application/pages/modplatform/ImportPage.cpp rename to launcher/pages/modplatform/ImportPage.cpp index 3910dfda0b..c2369bdcc8 100644 --- a/application/pages/modplatform/ImportPage.cpp +++ b/launcher/pages/modplatform/ImportPage.cpp @@ -71,6 +71,7 @@ void ImportPage::updateState() { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedIcon("default"); } } else @@ -83,6 +84,7 @@ void ImportPage::updateState() // hook, line and sinker. QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedIcon("default"); } } else diff --git a/application/pages/modplatform/ImportPage.h b/launcher/pages/modplatform/ImportPage.h similarity index 100% rename from application/pages/modplatform/ImportPage.h rename to launcher/pages/modplatform/ImportPage.h diff --git a/application/pages/modplatform/ImportPage.ui b/launcher/pages/modplatform/ImportPage.ui similarity index 100% rename from application/pages/modplatform/ImportPage.ui rename to launcher/pages/modplatform/ImportPage.ui diff --git a/application/pages/modplatform/VanillaPage.cpp b/launcher/pages/modplatform/VanillaPage.cpp similarity index 89% rename from application/pages/modplatform/VanillaPage.cpp rename to launcher/pages/modplatform/VanillaPage.cpp index 17535f1e27..02638315db 100644 --- a/application/pages/modplatform/VanillaPage.cpp +++ b/launcher/pages/modplatform/VanillaPage.cpp @@ -82,10 +82,19 @@ BaseVersionPtr VanillaPage::selectedVersion() const void VanillaPage::suggestCurrent() { - if(m_selectedVersion && isOpened) + if (!isOpened) { - dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); + return; } + + if(!m_selectedVersion) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); + dialog->setSuggestedIcon("default"); } void VanillaPage::setSelectedVersion(BaseVersionPtr version) diff --git a/application/pages/modplatform/VanillaPage.h b/launcher/pages/modplatform/VanillaPage.h similarity index 100% rename from application/pages/modplatform/VanillaPage.h rename to launcher/pages/modplatform/VanillaPage.h diff --git a/application/pages/modplatform/VanillaPage.ui b/launcher/pages/modplatform/VanillaPage.ui similarity index 100% rename from application/pages/modplatform/VanillaPage.ui rename to launcher/pages/modplatform/VanillaPage.ui diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp similarity index 100% rename from application/pages/modplatform/atlauncher/AtlFilterModel.cpp rename to launcher/pages/modplatform/atlauncher/AtlFilterModel.cpp diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.h b/launcher/pages/modplatform/atlauncher/AtlFilterModel.h similarity index 100% rename from application/pages/modplatform/atlauncher/AtlFilterModel.h rename to launcher/pages/modplatform/atlauncher/AtlFilterModel.h diff --git a/application/pages/modplatform/atlauncher/AtlModel.cpp b/launcher/pages/modplatform/atlauncher/AtlListModel.cpp similarity index 93% rename from application/pages/modplatform/atlauncher/AtlModel.cpp rename to launcher/pages/modplatform/atlauncher/AtlListModel.cpp index 4b1b1c8e05..f3be619892 100644 --- a/application/pages/modplatform/atlauncher/AtlModel.cpp +++ b/launcher/pages/modplatform/atlauncher/AtlListModel.cpp @@ -1,8 +1,9 @@ -#include "AtlModel.h" +#include "AtlListModel.h" #include #include #include +#include namespace Atl { @@ -99,7 +100,15 @@ void ListModel::requestFinished() auto packObj = packRaw.toObject(); ATLauncher::IndexedPack pack; - ATLauncher::loadIndexedPack(pack, packObj); + + try { + ATLauncher::loadIndexedPack(pack, packObj); + } + catch (const JSONValidationError &e) { + qDebug() << QString::fromUtf8(response); + qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause(); + return; + } // ignore packs without a published version if(pack.versions.length() == 0) continue; diff --git a/application/pages/modplatform/atlauncher/AtlModel.h b/launcher/pages/modplatform/atlauncher/AtlListModel.h similarity index 100% rename from application/pages/modplatform/atlauncher/AtlModel.h rename to launcher/pages/modplatform/atlauncher/AtlListModel.h diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp new file mode 100644 index 0000000000..14bbd18b66 --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -0,0 +1,209 @@ +#include "AtlOptionalModDialog.h" +#include "ui_AtlOptionalModDialog.h" + +AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector mods) + : QAbstractListModel(parent), m_mods(mods) { + + // fill mod index + for (int i = 0; i < m_mods.size(); i++) { + auto mod = m_mods.at(i); + m_index[mod.name] = i; + } + // set initial state + for (int i = 0; i < m_mods.size(); i++) { + auto mod = m_mods.at(i); + m_selection[mod.name] = false; + setMod(mod, i, mod.selected, false); + } +} + +QVector AtlOptionalModListModel::getResult() { + QVector result; + + for (const auto& mod : m_mods) { + if (m_selection[mod.name]) { + result.push_back(mod.name); + } + } + + return result; +} + +int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const { + return m_mods.size(); +} + +int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const { + // Enabled, Name, Description + return 3; +} + +QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const { + auto row = index.row(); + auto mod = m_mods.at(row); + + if (role == Qt::DisplayRole) { + if (index.column() == NameColumn) { + return mod.name; + } + if (index.column() == DescriptionColumn) { + return mod.description; + } + } + else if (role == Qt::ToolTipRole) { + if (index.column() == DescriptionColumn) { + return mod.description; + } + } + else if (role == Qt::CheckStateRole) { + if (index.column() == EnabledColumn) { + return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked; + } + } + + return QVariant(); +} + +bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) { + if (role == Qt::CheckStateRole) { + auto row = index.row(); + auto mod = m_mods.at(row); + + toggleMod(mod, row); + return true; + } + + return false; +} + +QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + switch (section) { + case EnabledColumn: + return QString(); + case NameColumn: + return QString("Name"); + case DescriptionColumn: + return QString("Description"); + } + } + + return QVariant(); +} + +Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { + auto flags = QAbstractListModel::flags(index); + if (index.isValid() && index.column() == EnabledColumn) { + flags |= Qt::ItemIsUserCheckable; + } + return flags; +} + +void AtlOptionalModListModel::selectRecommended() { + for (const auto& mod : m_mods) { + m_selection[mod.name] = mod.recommended; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::clearAll() { + for (const auto& mod : m_mods) { + m_selection[mod.name] = false; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) { + setMod(mod, index, !m_selection[mod.name]); +} + +void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) { + if (m_selection[mod.name] == enable) return; + + m_selection[mod.name] = enable; + + // disable other mods in the group, if applicable + if (enable && !mod.group.isEmpty()) { + for (int i = 0; i < m_mods.size(); i++) { + if (index == i) continue; + auto other = m_mods.at(i); + + if (mod.group == other.group) { + setMod(other, i, false, shouldEmit); + } + } + } + + for (const auto& dependencyName : mod.depends) { + auto dependencyIndex = m_index[dependencyName]; + auto dependencyMod = m_mods.at(dependencyIndex); + + // enable/disable dependencies + if (enable) { + setMod(dependencyMod, dependencyIndex, true, shouldEmit); + } + + // if the dependency is 'effectively hidden', then track which mods + // depend on it - so we can efficiently disable it when no more dependents + // depend on it. + auto dependants = m_dependants[dependencyName]; + + if (enable) { + dependants.append(mod.name); + } + else { + dependants.removeAll(mod.name); + + // if there are no longer any dependents, let's disable the mod + if (dependencyMod.effectively_hidden && dependants.isEmpty()) { + setMod(dependencyMod, dependencyIndex, false, shouldEmit); + } + } + } + + // disable mods that depend on this one, if disabling + if (!enable) { + auto dependants = m_dependants[mod.name]; + for (const auto& dependencyName : dependants) { + auto dependencyIndex = m_index[dependencyName]; + auto dependencyMod = m_mods.at(dependencyIndex); + + setMod(dependencyMod, dependencyIndex, false, shouldEmit); + } + } + + if (shouldEmit) { + emit dataChanged(AtlOptionalModListModel::index(index, EnabledColumn), + AtlOptionalModListModel::index(index, EnabledColumn)); + } +} + + +AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector mods) + : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { + ui->setupUi(this); + + listModel = new AtlOptionalModListModel(this, mods); + ui->treeView->setModel(listModel); + + ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ui->treeView->header()->setSectionResizeMode( + AtlOptionalModListModel::NameColumn, QHeaderView::ResizeToContents); + ui->treeView->header()->setSectionResizeMode( + AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); + + connect(ui->selectRecommendedButton, &QPushButton::pressed, + listModel, &AtlOptionalModListModel::selectRecommended); + connect(ui->clearAllButton, &QPushButton::pressed, + listModel, &AtlOptionalModListModel::clearAll); + connect(ui->installButton, &QPushButton::pressed, + this, &QDialog::close); +} + +AtlOptionalModDialog::~AtlOptionalModDialog() { + delete ui; +} diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h new file mode 100644 index 0000000000..a1df43f60f --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +#include "modplatform/atlauncher/ATLPackIndex.h" + +namespace Ui { +class AtlOptionalModDialog; +} + +class AtlOptionalModListModel : public QAbstractListModel { + Q_OBJECT + +public: + enum Columns + { + EnabledColumn = 0, + NameColumn, + DescriptionColumn, + }; + + AtlOptionalModListModel(QWidget *parent, QVector mods); + + QVector getResult(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + +public slots: + void selectRecommended(); + void clearAll(); + +private: + void toggleMod(ATLauncher::VersionMod mod, int index); + void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); + +private: + QVector m_mods; + QMap m_selection; + QMap m_index; + QMap> m_dependants; +}; + +class AtlOptionalModDialog : public QDialog { + Q_OBJECT + +public: + AtlOptionalModDialog(QWidget *parent, QVector mods); + ~AtlOptionalModDialog() override; + + QVector getResult() { + return listModel->getResult(); + } + +private: + Ui::AtlOptionalModDialog *ui; + + AtlOptionalModListModel *listModel; +}; diff --git a/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui new file mode 100644 index 0000000000..5d3193a42b --- /dev/null +++ b/launcher/pages/modplatform/atlauncher/AtlOptionalModDialog.ui @@ -0,0 +1,65 @@ + + + AtlOptionalModDialog + + + + 0 + 0 + 550 + 310 + + + + Select Mods To Install + + + + + + Install + + + true + + + + + + + Select Recommended + + + + + + + false + + + Use Share Code + + + + + + + Clear All + + + + + + + + + + + ModListView + QTreeView +
widgets/ModListView.h
+
+
+ + +
diff --git a/application/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/pages/modplatform/atlauncher/AtlPage.cpp similarity index 56% rename from application/pages/modplatform/atlauncher/AtlPage.cpp rename to launcher/pages/modplatform/atlauncher/AtlPage.cpp index f90d734cd8..9fdf111f48 100644 --- a/application/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/pages/modplatform/atlauncher/AtlPage.cpp @@ -2,8 +2,10 @@ #include "ui_AtlPage.h" #include "dialogs/NewInstanceDialog.h" +#include "AtlOptionalModDialog.h" #include #include +#include AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) @@ -19,6 +21,9 @@ AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) ui->packView->header()->hide(); ui->packView->setIndentation(0); + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) { ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); @@ -44,15 +49,29 @@ bool AtlPage::shouldDisplay() const void AtlPage::openedImpl() { - listModel->request(); + if(!initialized) + { + listModel->request(); + initialized = true; + } + + suggestCurrent(); } void AtlPage::suggestCurrent() { - if(isOpened) { - dialog->setSuggestedPack(selected.name, new ATLauncher::PackInstallTask(selected.safeName, selectedVersion)); + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; } + dialog->setSuggestedPack(selected.name, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion)); auto editedLogoName = selected.safeName; auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) @@ -112,3 +131,45 @@ void AtlPage::onVersionSelectionChanged(QString data) selectedVersion = data; suggestCurrent(); } + +QVector AtlPage::chooseOptionalMods(QVector mods) { + AtlOptionalModDialog optionalModDialog(this, mods); + optionalModDialog.exec(); + return optionalModDialog.getResult(); +} + +QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) { + VersionSelectDialog vselect(vlist.get(), "Choose Version", MMC->activeWindow(), false); + if (minecraftVersion != Q_NULLPTR) { + vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); + vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); + } + else { + vselect.setEmptyString(tr("No versions are currently available")); + } + vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!")); + + // select recommended build + for (int i = 0; i < vlist->versions().size(); i++) { + auto version = vlist->versions().at(i); + auto reqs = version->requires(); + + // filter by minecraft version, if the loader depends on a certain version. + if (minecraftVersion != Q_NULLPTR) { + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) { + return req.uid == "net.minecraft"; + }); + if (iter == reqs.end()) continue; + if (iter->equalsVersion != minecraftVersion) continue; + } + + // first recommended build we find, we use. + if (version->isRecommended()) { + vselect.setCurrentVersion(version->descriptor()); + break; + } + } + + vselect.exec(); + return vselect.selectedVersion()->descriptor(); +} diff --git a/application/pages/modplatform/atlauncher/AtlPage.h b/launcher/pages/modplatform/atlauncher/AtlPage.h similarity index 83% rename from application/pages/modplatform/atlauncher/AtlPage.h rename to launcher/pages/modplatform/atlauncher/AtlPage.h index 368de666f0..932ec6a652 100644 --- a/application/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/pages/modplatform/atlauncher/AtlPage.h @@ -16,9 +16,10 @@ #pragma once #include "AtlFilterModel.h" -#include "AtlModel.h" +#include "AtlListModel.h" #include +#include #include "MultiMC.h" #include "pages/BasePage.h" @@ -31,7 +32,7 @@ namespace Ui class NewInstanceDialog; -class AtlPage : public QWidget, public BasePage +class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport { Q_OBJECT @@ -61,6 +62,9 @@ Q_OBJECT private: void suggestCurrent(); + QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; + QVector chooseOptionalMods(QVector mods) override; + private slots: void triggerSearch(); void resetSearch(); @@ -78,4 +82,6 @@ private slots: ATLauncher::IndexedPack selected; QString selectedVersion; + + bool initialized = false; }; diff --git a/application/pages/modplatform/atlauncher/AtlPage.ui b/launcher/pages/modplatform/atlauncher/AtlPage.ui similarity index 96% rename from application/pages/modplatform/atlauncher/AtlPage.ui rename to launcher/pages/modplatform/atlauncher/AtlPage.ui index bb0d531093..f16c24b820 100644 --- a/application/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/pages/modplatform/atlauncher/AtlPage.ui @@ -21,6 +21,9 @@ 48
+ + true +
diff --git a/application/pages/modplatform/twitch/TwitchModel.cpp b/launcher/pages/modplatform/flame/FlameModel.cpp similarity index 57% rename from application/pages/modplatform/twitch/TwitchModel.cpp rename to launcher/pages/modplatform/flame/FlameModel.cpp index 5c6c7858c8..228a88c50e 100644 --- a/application/pages/modplatform/twitch/TwitchModel.cpp +++ b/launcher/pages/modplatform/flame/FlameModel.cpp @@ -1,5 +1,6 @@ -#include "TwitchModel.h" +#include "FlameModel.h" #include "MultiMC.h" +#include #include #include @@ -10,7 +11,7 @@ #include #include -namespace Twitch { +namespace Flame { ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) { @@ -38,7 +39,7 @@ QVariant ListModel::data(const QModelIndex &index, int role) const return QString("INVALID INDEX %1").arg(pos); } - Modpack pack = modpacks.at(pos); + IndexedPack pack = modpacks.at(pos); if(role == Qt::DisplayRole) { return pack.name; @@ -99,8 +100,8 @@ void ListModel::requestLogo(QString logo, QString url) return; } - MetaEntryPtr entry = ENV.metacache()->resolveEntry("TwitchPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); - NetJob *job = new NetJob(QString("Twitch Icon Download %1").arg(logo)); + MetaEntryPtr entry = ENV.metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + NetJob *job = new NetJob(QString("Flame Icon Download %1").arg(logo)); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); @@ -127,7 +128,7 @@ void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallbac { if(m_logoMap.contains(logo)) { - callback(ENV.metacache()->resolveEntry("TwitchPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + callback(ENV.metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); } else { @@ -158,18 +159,17 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { - NetJob *netJob = new NetJob("Twitch::Search"); + NetJob *netJob = new NetJob("Flame::Search"); auto searchUrl = QString( "https://addons-ecs.forgesvc.net/api/v2/addon/search?" "categoryId=0&" "gameId=432&" - //"gameVersion=1.12.2&" "index=%1&" "pageSize=25&" "searchFilter=%2&" "sectionId=4471&" - "sort=0" - ).arg(nextSearchOffset).arg(currentSearchTerm); + "sort=%3" + ).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); @@ -177,12 +177,13 @@ void ListModel::performPaginatedSearch() QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); } -void ListModel::searchWithTerm(const QString& term) +void ListModel::searchWithTerm(const QString& term, int sort) { - if(currentSearchTerm == term) { + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { return; } currentSearchTerm = term; + currentSort = sort; if(jobPtr) { jobPtr->abort(); searchState = ResetRequested; @@ -198,90 +199,36 @@ void ListModel::searchWithTerm(const QString& term) performPaginatedSearch(); } -void Twitch::ListModel::searchRequestFinished() +void Flame::ListModel::searchRequestFinished() { jobPtr.reset(); QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Twitch at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } - QList newList; - auto objs = doc.array(); - for(auto projectIter: objs) { - Modpack pack; - auto project = projectIter.toObject(); - pack.addonId = project.value("id").toInt(0); - if (pack.addonId == 0) { - qWarning() << "Pack without an ID, skipping: " << pack.name; - continue; - } - pack.name = project.value("name").toString(); - pack.websiteUrl = project.value("websiteUrl").toString(); - pack.description = project.value("summary").toString(); - bool thumbnailFound = false; - auto attachments = project.value("attachments").toArray(); - for(auto attachmentIter: attachments) { - auto attachment = attachmentIter.toObject(); - bool isDefault = attachment.value("isDefault").toBool(false); - if(isDefault) { - thumbnailFound = true; - pack.logoName = attachment.value("title").toString(); - pack.logoUrl = attachment.value("thumbnailUrl").toString(); - break; - } - } - if(!thumbnailFound) { - qWarning() << "Pack without an icon, skipping: " << pack.name; - continue; - } - auto authors = project.value("authors").toArray(); - for(auto authorIter: authors) { - auto author = authorIter.toObject(); - ModpackAuthor packAuthor; - packAuthor.name = author.value("name").toString(); - packAuthor.url = author.value("url").toString(); - pack.authors.append(packAuthor); - } - int defaultFileId = project.value("defaultFileId").toInt(0); - if(defaultFileId == 0) { - qWarning() << "Pack without default file, skipping: " << pack.name; - continue; - } - bool found = false; - auto files = project.value("latestFiles").toArray(); - for(auto fileIter: files) { - auto file = fileIter.toObject(); - int id = file.value("id").toInt(0); - // NOTE: for now, ignore everything that's not the default... - if(id != defaultFileId) { - continue; - } - pack.latestFile.addonId = pack.addonId; - pack.latestFile.fileId = id; - // FIXME: what to do when there's more than one, or there's no version? - auto versionArray = file.value("gameVersion").toArray(); - if(versionArray.size() != 1) { - continue; - } - pack.latestFile.mcVersion = versionArray[0].toString(); - pack.latestFile.version = file.value("displayName").toString(); - pack.latestFile.downloadUrl = file.value("downloadUrl").toString(); - found = true; - break; + QList newList; + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + Flame::IndexedPack pack; + try + { + Flame::loadIndexedPack(pack, packObj); + newList.append(pack); } - if(!found) { - qWarning() << "Pack with no good file, skipping: " << pack.name; + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading pack from CurseForge: " << e.cause(); continue; } - pack.broken = false; - newList.append(pack); } - if(objs.size() < 25) { + if(packs.size() < 25) { searchState = Finished; } else { nextSearchOffset += 25; @@ -292,7 +239,7 @@ void Twitch::ListModel::searchRequestFinished() endInsertRows(); } -void Twitch::ListModel::searchRequestFailed(QString reason) +void Flame::ListModel::searchRequestFailed(QString reason) { jobPtr.reset(); diff --git a/application/pages/modplatform/twitch/TwitchModel.h b/launcher/pages/modplatform/flame/FlameModel.h similarity index 89% rename from application/pages/modplatform/twitch/TwitchModel.h rename to launcher/pages/modplatform/flame/FlameModel.h index ad355c64a8..24383db010 100644 --- a/application/pages/modplatform/twitch/TwitchModel.h +++ b/launcher/pages/modplatform/flame/FlameModel.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -16,9 +15,9 @@ #include #include -#include "TwitchData.h" +#include -namespace Twitch { +namespace Flame { typedef QMap LogoMap; @@ -40,7 +39,7 @@ class ListModel : public QAbstractListModel void fetchMore(const QModelIndex & parent) override; void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); - void searchWithTerm(const QString & term); + void searchWithTerm(const QString & term, const int sort); private slots: void performPaginatedSearch(); @@ -55,13 +54,14 @@ private slots: void requestLogo(QString file, QString url); private: - QList modpacks; + QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; LogoMap m_logoMap; QMap waitingCallbacks; QString currentSearchTerm; + int currentSort = 0; int nextSearchOffset = 0; enum SearchState { None, diff --git a/launcher/pages/modplatform/flame/FlamePage.cpp b/launcher/pages/modplatform/flame/FlamePage.cpp new file mode 100644 index 0000000000..ade5843131 --- /dev/null +++ b/launcher/pages/modplatform/flame/FlamePage.cpp @@ -0,0 +1,185 @@ +#include "FlamePage.h" +#include "ui_FlamePage.h" + +#include "MultiMC.h" +#include +#include "dialogs/NewInstanceDialog.h" +#include +#include "FlameModel.h" +#include + +FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); + ui->searchEdit->installEventFilter(this); + listModel = new Flame::ListModel(this); + ui->packView->setModel(listModel); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + // index is used to set the sorting with the curseforge api + ui->sortByBox->addItem(tr("Sort by featured")); + ui->sortByBox->addItem(tr("Sort by popularity")); + ui->sortByBox->addItem(tr("Sort by last updated")); + ui->sortByBox->addItem(tr("Sort by name")); + ui->sortByBox->addItem(tr("Sort by author")); + ui->sortByBox->addItem(tr("Sort by total downloads")); + + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged); +} + +FlamePage::~FlamePage() +{ + delete ui; +} + +bool FlamePage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool FlamePage::shouldDisplay() const +{ + return true; +} + +void FlamePage::openedImpl() +{ + suggestCurrent(); + triggerSearch(); +} + +void FlamePage::triggerSearch() +{ + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + current = listModel->data(first, Qt::UserRole).value(); + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name; + else + text = "" + name + ""; + if (!current.authors.empty()) { + auto authorToStr = [](Flame::ModpackAuthor & author) { + if(author.url.isEmpty()) { + return author.name; + } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for(auto & author: current.authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "
" + tr(" by ") + authorStrs.join(", "); + } + text += "

"; + + ui->packDescription->setHtml(text + current.description); + + if (current.versionsLoaded == false) + { + qDebug() << "Loading flame modpack versions"; + NetJob *netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name)); + std::shared_ptr response = std::make_shared(); + int addonId = current.addonId; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response.get())); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response] + { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + QJsonArray arr = doc.array(); + try + { + Flame::loadIndexedPackVersions(current, arr); + } + catch(const JSONValidationError &e) + { + qDebug() << *response; + qWarning() << "Error while reading flame modpack version: " << e.cause(); + } + + for(auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + } + + suggestCurrent(); + }); + netJob->start(); + } + else + { + for(auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + } + + suggestCurrent(); + } +} + +void FlamePage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion)); + QString editedLogoName; + editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); + listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); +} + +void FlamePage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toString(); + suggestCurrent(); +} diff --git a/application/pages/modplatform/twitch/TwitchPage.h b/launcher/pages/modplatform/flame/FlamePage.h similarity index 71% rename from application/pages/modplatform/twitch/TwitchPage.h rename to launcher/pages/modplatform/flame/FlamePage.h index 093900ff82..467bb44b27 100644 --- a/application/pages/modplatform/twitch/TwitchPage.h +++ b/launcher/pages/modplatform/flame/FlamePage.h @@ -20,41 +20,41 @@ #include "pages/BasePage.h" #include #include "tasks/Task.h" -#include "TwitchData.h" +#include namespace Ui { -class TwitchPage; +class FlamePage; } class NewInstanceDialog; -namespace Twitch { +namespace Flame { class ListModel; } -class TwitchPage : public QWidget, public BasePage +class FlamePage : public QWidget, public BasePage { Q_OBJECT public: - explicit TwitchPage(NewInstanceDialog* dialog, QWidget *parent = 0); - virtual ~TwitchPage(); + explicit FlamePage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~FlamePage(); virtual QString displayName() const override { - return tr("Twitch"); + return tr("CurseForge"); } virtual QIcon icon() const override { - return MMC->getThemedIcon("twitch"); + return MMC->getThemedIcon("flame"); } virtual QString id() const override { - return "twitch"; + return "flame"; } virtual QString helpPage() const override { - return "Twitch-platform"; + return "Flame-platform"; } virtual bool shouldDisplay() const override; @@ -68,10 +68,13 @@ class TwitchPage : public QWidget, public BasePage private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); private: - Ui::TwitchPage *ui = nullptr; + Ui::FlamePage *ui = nullptr; NewInstanceDialog* dialog = nullptr; - Twitch::ListModel* model = nullptr; - Twitch::Modpack current; + Flame::ListModel* listModel = nullptr; + Flame::IndexedPack current; + + QString selectedVersion; }; diff --git a/launcher/pages/modplatform/flame/FlamePage.ui b/launcher/pages/modplatform/flame/FlamePage.ui new file mode 100644 index 0000000000..9723815a6f --- /dev/null +++ b/launcher/pages/modplatform/flame/FlamePage.ui @@ -0,0 +1,90 @@ + + + FlamePage + + + + 0 + 0 + 837 + 685 + + + + + + + + + + 48 + 48 + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + + + true + + + true + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + Search + + + + + + + Search and filter ... + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + + diff --git a/application/pages/modplatform/ftb/FtbFilterModel.cpp b/launcher/pages/modplatform/ftb/FtbFilterModel.cpp similarity index 100% rename from application/pages/modplatform/ftb/FtbFilterModel.cpp rename to launcher/pages/modplatform/ftb/FtbFilterModel.cpp diff --git a/application/pages/modplatform/ftb/FtbFilterModel.h b/launcher/pages/modplatform/ftb/FtbFilterModel.h similarity index 100% rename from application/pages/modplatform/ftb/FtbFilterModel.h rename to launcher/pages/modplatform/ftb/FtbFilterModel.h diff --git a/application/pages/modplatform/ftb/FtbListModel.cpp b/launcher/pages/modplatform/ftb/FtbListModel.cpp similarity index 94% rename from application/pages/modplatform/ftb/FtbListModel.cpp rename to launcher/pages/modplatform/ftb/FtbListModel.cpp index 6323682786..98973f2ef0 100644 --- a/application/pages/modplatform/ftb/FtbListModel.cpp +++ b/launcher/pages/modplatform/ftb/FtbListModel.cpp @@ -106,21 +106,22 @@ void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallbac void ListModel::searchWithTerm(const QString &term) { - if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + if(searchState != Failed && currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + // unless the search has failed, then there is no need to perform an identical search. return; } currentSearchTerm = term; + if(jobPtr) { jobPtr->abort(); - searchState = ResetRequested; - return; - } - else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; + jobPtr.reset(); } + + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + performSearch(); } @@ -154,15 +155,7 @@ void ListModel::searchRequestFailed(QString reason) jobPtr.reset(); remainingPacks.clear(); - if(searchState == ResetRequested) { - beginResetModel(); - modpacks.clear(); - endResetModel(); - - performSearch(); - } else { - searchState = Finished; - } + searchState = Failed; } void ListModel::requestPack() diff --git a/application/pages/modplatform/ftb/FtbListModel.h b/launcher/pages/modplatform/ftb/FtbListModel.h similarity index 97% rename from application/pages/modplatform/ftb/FtbListModel.h rename to launcher/pages/modplatform/ftb/FtbListModel.h index 9c057d7354..de94e6baf1 100644 --- a/application/pages/modplatform/ftb/FtbListModel.h +++ b/launcher/pages/modplatform/ftb/FtbListModel.h @@ -57,7 +57,8 @@ private slots: None, CanPossiblyFetchMore, ResetRequested, - Finished + Finished, + Failed, } searchState = None; NetJobPtr jobPtr; int currentPack; diff --git a/application/pages/modplatform/ftb/FtbPage.cpp b/launcher/pages/modplatform/ftb/FtbPage.cpp similarity index 79% rename from application/pages/modplatform/ftb/FtbPage.cpp rename to launcher/pages/modplatform/ftb/FtbPage.cpp index dd2ff66629..b7f35c5df0 100644 --- a/application/pages/modplatform/ftb/FtbPage.cpp +++ b/launcher/pages/modplatform/ftb/FtbPage.cpp @@ -23,6 +23,9 @@ FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent) ui->searchEdit->installEventFilter(this); + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) { ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); @@ -60,26 +63,33 @@ bool FtbPage::shouldDisplay() const void FtbPage::openedImpl() { - dialog->setSuggestedPack(); triggerSearch(); + suggestCurrent(); } void FtbPage::suggestCurrent() { - if(isOpened) + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) { - dialog->setSuggestedPack(selected.name, new ModpacksCH::PackInstallTask(selected, selectedVersion)); - - for(auto art : selected.art) { - if(art.type == "square") { - QString editedLogoName; - editedLogoName = selected.name; - - listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo) - { - dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName); - }); - } + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name, new ModpacksCH::PackInstallTask(selected, selectedVersion)); + for(auto art : selected.art) { + if(art.type == "square") { + QString editedLogoName; + editedLogoName = selected.name; + + listModel->getLogo(selected.name, art.url, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName); + }); } } } diff --git a/application/pages/modplatform/ftb/FtbPage.h b/launcher/pages/modplatform/ftb/FtbPage.h similarity index 100% rename from application/pages/modplatform/ftb/FtbPage.h rename to launcher/pages/modplatform/ftb/FtbPage.h diff --git a/application/pages/modplatform/ftb/FtbPage.ui b/launcher/pages/modplatform/ftb/FtbPage.ui similarity index 89% rename from application/pages/modplatform/ftb/FtbPage.ui rename to launcher/pages/modplatform/ftb/FtbPage.ui index 475d78bb46..135afc6dac 100644 --- a/application/pages/modplatform/ftb/FtbPage.ui +++ b/launcher/pages/modplatform/ftb/FtbPage.ui @@ -32,7 +32,11 @@
- + + + Search and filter ... + + @@ -51,6 +55,9 @@ 48 + + true + diff --git a/application/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/pages/modplatform/legacy_ftb/ListModel.cpp similarity index 100% rename from application/pages/modplatform/legacy_ftb/ListModel.cpp rename to launcher/pages/modplatform/legacy_ftb/ListModel.cpp diff --git a/application/pages/modplatform/legacy_ftb/ListModel.h b/launcher/pages/modplatform/legacy_ftb/ListModel.h similarity index 100% rename from application/pages/modplatform/legacy_ftb/ListModel.h rename to launcher/pages/modplatform/legacy_ftb/ListModel.h diff --git a/application/pages/modplatform/legacy_ftb/Page.cpp b/launcher/pages/modplatform/legacy_ftb/Page.cpp similarity index 86% rename from application/pages/modplatform/legacy_ftb/Page.cpp rename to launcher/pages/modplatform/legacy_ftb/Page.cpp index 8e40ba9ee0..a438f76c0c 100644 --- a/application/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/pages/modplatform/legacy_ftb/Page.cpp @@ -122,49 +122,50 @@ void Page::openedImpl() void Page::suggestCurrent() { - if(isOpened) + if(!isOpened) { - if(!selected.broken) - { - dialog->setSuggestedPack(selected.name, new PackInstallTask(selected, selectedVersion)); - QString editedLogoName; - if(selected.logo.toLower().startsWith("ftb")) - { - editedLogoName = selected.logo; - } - else - { - editedLogoName = "ftb_" + selected.logo; - } + return; + } - editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png")); + if(selected.broken || selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } - if(selected.type == PackType::Public) - { - publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) - { - dialog->setSuggestedIconFromFile(logo, editedLogoName); - }); - } - else if (selected.type == PackType::ThirdParty) - { - thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo) - { - dialog->setSuggestedIconFromFile(logo, editedLogoName); - }); - } - else if (selected.type == PackType::Private) - { - privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) - { - dialog->setSuggestedIconFromFile(logo, editedLogoName); - }); - } - } - else + dialog->setSuggestedPack(selected.name, new PackInstallTask(selected, selectedVersion)); + QString editedLogoName; + if(selected.logo.toLower().startsWith("ftb")) + { + editedLogoName = selected.logo; + } + else + { + editedLogoName = "ftb_" + selected.logo; + } + + editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png")); + + if(selected.type == PackType::Public) + { + publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) { - dialog->setSuggestedPack(); - } + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } + else if (selected.type == PackType::ThirdParty) + { + thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } + else if (selected.type == PackType::Private) + { + privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); } } diff --git a/application/pages/modplatform/legacy_ftb/Page.h b/launcher/pages/modplatform/legacy_ftb/Page.h similarity index 100% rename from application/pages/modplatform/legacy_ftb/Page.h rename to launcher/pages/modplatform/legacy_ftb/Page.h diff --git a/application/pages/modplatform/legacy_ftb/Page.ui b/launcher/pages/modplatform/legacy_ftb/Page.ui similarity index 92% rename from application/pages/modplatform/legacy_ftb/Page.ui rename to launcher/pages/modplatform/legacy_ftb/Page.ui index 36fb235950..15e5d4325c 100644 --- a/application/pages/modplatform/legacy_ftb/Page.ui +++ b/launcher/pages/modplatform/legacy_ftb/Page.ui @@ -29,6 +29,9 @@ 16777215 + + true + @@ -52,6 +55,9 @@ 16777215 + + true + @@ -69,6 +75,9 @@ 16777215 + + true + diff --git a/application/pages/modplatform/technic/TechnicData.h b/launcher/pages/modplatform/technic/TechnicData.h similarity index 100% rename from application/pages/modplatform/technic/TechnicData.h rename to launcher/pages/modplatform/technic/TechnicData.h diff --git a/application/pages/modplatform/technic/TechnicModel.cpp b/launcher/pages/modplatform/technic/TechnicModel.cpp similarity index 94% rename from application/pages/modplatform/technic/TechnicModel.cpp rename to launcher/pages/modplatform/technic/TechnicModel.cpp index bf256ab6fe..def3078301 100644 --- a/application/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/pages/modplatform/technic/TechnicModel.cpp @@ -72,7 +72,7 @@ int Technic::ListModel::rowCount(const QModelIndex&) const void Technic::ListModel::searchWithTerm(const QString& term) { - if(currentSearchTerm == term) { + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { return; } currentSearchTerm = term; @@ -93,9 +93,16 @@ void Technic::ListModel::searchWithTerm(const QString& term) void Technic::ListModel::performSearch() { NetJob *netJob = new NetJob("Technic::Search"); - auto searchUrl = QString( - "https://api.technicpack.net/search?build=multimc&q=%1" - ).arg(currentSearchTerm); + QString searchUrl = ""; + if (currentSearchTerm.isEmpty()) { + searchUrl = "https://api.technicpack.net/trending?build=multimc"; + } + else + { + searchUrl = QString( + "https://api.technicpack.net/search?build=multimc&q=%1" + ).arg(currentSearchTerm); + } netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); jobPtr = netJob; jobPtr->start(); diff --git a/application/pages/modplatform/technic/TechnicModel.h b/launcher/pages/modplatform/technic/TechnicModel.h similarity index 100% rename from application/pages/modplatform/technic/TechnicModel.h rename to launcher/pages/modplatform/technic/TechnicModel.h diff --git a/application/pages/modplatform/technic/TechnicPage.cpp b/launcher/pages/modplatform/technic/TechnicPage.cpp similarity index 61% rename from application/pages/modplatform/technic/TechnicPage.cpp rename to launcher/pages/modplatform/technic/TechnicPage.cpp index d246edf225..e836f7675d 100644 --- a/application/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/pages/modplatform/technic/TechnicPage.cpp @@ -60,7 +60,8 @@ bool TechnicPage::shouldDisplay() const void TechnicPage::openedImpl() { - dialog->setSuggestedPack(); + suggestCurrent(); + triggerSearch(); } void TechnicPage::triggerSearch() { @@ -95,8 +96,7 @@ void TechnicPage::suggestCurrent() return; } - QString editedLogoName; - editedLogoName = "technic_" + current.logoName.section(".", 0, 0); + QString editedLogoName = "technic_" + current.logoName.section(".", 0, 0); model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); @@ -105,67 +105,66 @@ void TechnicPage::suggestCurrent() if (current.metadataLoaded) { metadataLoaded(); + return; } - else + + NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name)); + std::shared_ptr response = std::make_shared(); + QString slug = current.slug; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug] { - NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name)); - std::shared_ptr response = std::make_shared(); - QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get())); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug] + if (current.slug != slug) { - if (current.slug != slug) - { - return; - } - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - QJsonObject obj = doc.object(); - if(parse_error.error != QJsonParseError::NoError) - { - qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - if (!obj.contains("url")) + return; + } + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonObject obj = doc.object(); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + if (!obj.contains("url")) + { + qWarning() << "Json doesn't contain an url key"; + return; + } + QJsonValueRef url = obj["url"]; + if (url.isString()) + { + current.url = url.toString(); + } + else + { + if (!obj.contains("solder")) { - qWarning() << "Json doesn't contain an url key"; + qWarning() << "Json doesn't contain a valid url or solder key"; return; } - QJsonValueRef url = obj["url"]; - if (url.isString()) + QJsonValueRef solderUrl = obj["solder"]; + if (solderUrl.isString()) { - current.url = url.toString(); + current.url = solderUrl.toString(); + current.isSolder = true; } else { - if (!obj.contains("solder")) - { - qWarning() << "Json doesn't contain a valid url or solder key"; - return; - } - QJsonValueRef solderUrl = obj["solder"]; - if (solderUrl.isString()) - { - current.url = solderUrl.toString(); - current.isSolder = true; - } - else - { - qWarning() << "Json doesn't contain a valid url or solder key"; - return; - } + qWarning() << "Json doesn't contain a valid url or solder key"; + return; } + } - current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); - current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); - current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); - current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); - current.metadataLoaded = true; - metadataLoaded(); - }); - netJob->start(); - } + current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); + current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); + current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); + current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); + current.metadataLoaded = true; + metadataLoaded(); + }); + netJob->start(); } // expects current.metadataLoaded to be true diff --git a/application/pages/modplatform/technic/TechnicPage.h b/launcher/pages/modplatform/technic/TechnicPage.h similarity index 100% rename from application/pages/modplatform/technic/TechnicPage.h rename to launcher/pages/modplatform/technic/TechnicPage.h diff --git a/application/pages/modplatform/technic/TechnicPage.ui b/launcher/pages/modplatform/technic/TechnicPage.ui similarity index 92% rename from application/pages/modplatform/technic/TechnicPage.ui rename to launcher/pages/modplatform/technic/TechnicPage.ui index 36ce2ecfc8..2ca45dd2bb 100644 --- a/application/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/pages/modplatform/technic/TechnicPage.ui @@ -27,7 +27,11 @@ 0 - + + + Search and filter ... + + diff --git a/api/logic/pathmatcher/FSTreeMatcher.h b/launcher/pathmatcher/FSTreeMatcher.h similarity index 100% rename from api/logic/pathmatcher/FSTreeMatcher.h rename to launcher/pathmatcher/FSTreeMatcher.h diff --git a/api/logic/pathmatcher/IPathMatcher.h b/launcher/pathmatcher/IPathMatcher.h similarity index 100% rename from api/logic/pathmatcher/IPathMatcher.h rename to launcher/pathmatcher/IPathMatcher.h diff --git a/api/logic/pathmatcher/MultiMatcher.h b/launcher/pathmatcher/MultiMatcher.h similarity index 100% rename from api/logic/pathmatcher/MultiMatcher.h rename to launcher/pathmatcher/MultiMatcher.h diff --git a/api/logic/pathmatcher/RegexpMatcher.h b/launcher/pathmatcher/RegexpMatcher.h similarity index 100% rename from api/logic/pathmatcher/RegexpMatcher.h rename to launcher/pathmatcher/RegexpMatcher.h diff --git a/application/resources/MultiMC.icns b/launcher/resources/MultiMC.icns similarity index 100% rename from application/resources/MultiMC.icns rename to launcher/resources/MultiMC.icns diff --git a/launcher/resources/MultiMC.ico b/launcher/resources/MultiMC.ico new file mode 100644 index 0000000000..a86a1f0d9a Binary files /dev/null and b/launcher/resources/MultiMC.ico differ diff --git a/application/resources/MultiMC.manifest b/launcher/resources/MultiMC.manifest similarity index 100% rename from application/resources/MultiMC.manifest rename to launcher/resources/MultiMC.manifest diff --git a/application/resources/OSX/OSX.qrc b/launcher/resources/OSX/OSX.qrc similarity index 96% rename from application/resources/OSX/OSX.qrc rename to launcher/resources/OSX/OSX.qrc index 19fd4b6a81..1a6ec0dc4b 100644 --- a/application/resources/OSX/OSX.qrc +++ b/launcher/resources/OSX/OSX.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/application/resources/OSX/index.theme b/launcher/resources/OSX/index.theme similarity index 100% rename from application/resources/OSX/index.theme rename to launcher/resources/OSX/index.theme diff --git a/application/resources/OSX/scalable/about.svg b/launcher/resources/OSX/scalable/about.svg similarity index 100% rename from application/resources/OSX/scalable/about.svg rename to launcher/resources/OSX/scalable/about.svg diff --git a/application/resources/OSX/scalable/accounts.svg b/launcher/resources/OSX/scalable/accounts.svg similarity index 100% rename from application/resources/OSX/scalable/accounts.svg rename to launcher/resources/OSX/scalable/accounts.svg diff --git a/application/resources/OSX/scalable/bug.svg b/launcher/resources/OSX/scalable/bug.svg similarity index 100% rename from application/resources/OSX/scalable/bug.svg rename to launcher/resources/OSX/scalable/bug.svg diff --git a/application/resources/OSX/scalable/centralmods.svg b/launcher/resources/OSX/scalable/centralmods.svg similarity index 100% rename from application/resources/OSX/scalable/centralmods.svg rename to launcher/resources/OSX/scalable/centralmods.svg diff --git a/application/resources/OSX/scalable/checkupdate.svg b/launcher/resources/OSX/scalable/checkupdate.svg similarity index 100% rename from application/resources/OSX/scalable/checkupdate.svg rename to launcher/resources/OSX/scalable/checkupdate.svg diff --git a/application/resources/OSX/scalable/copy.svg b/launcher/resources/OSX/scalable/copy.svg similarity index 100% rename from application/resources/OSX/scalable/copy.svg rename to launcher/resources/OSX/scalable/copy.svg diff --git a/application/resources/OSX/scalable/coremods.svg b/launcher/resources/OSX/scalable/coremods.svg similarity index 100% rename from application/resources/OSX/scalable/coremods.svg rename to launcher/resources/OSX/scalable/coremods.svg diff --git a/launcher/resources/OSX/scalable/custom-commands.svg b/launcher/resources/OSX/scalable/custom-commands.svg new file mode 100644 index 0000000000..e663452b6f --- /dev/null +++ b/launcher/resources/OSX/scalable/custom-commands.svg @@ -0,0 +1,71 @@ + +image/svg+xml diff --git a/application/resources/OSX/scalable/externaltools.svg b/launcher/resources/OSX/scalable/externaltools.svg similarity index 100% rename from application/resources/OSX/scalable/externaltools.svg rename to launcher/resources/OSX/scalable/externaltools.svg diff --git a/application/resources/OSX/scalable/help.svg b/launcher/resources/OSX/scalable/help.svg similarity index 100% rename from application/resources/OSX/scalable/help.svg rename to launcher/resources/OSX/scalable/help.svg diff --git a/application/resources/OSX/scalable/instance-settings.svg b/launcher/resources/OSX/scalable/instance-settings.svg similarity index 100% rename from application/resources/OSX/scalable/instance-settings.svg rename to launcher/resources/OSX/scalable/instance-settings.svg diff --git a/application/resources/OSX/scalable/jarmods.svg b/launcher/resources/OSX/scalable/jarmods.svg similarity index 100% rename from application/resources/OSX/scalable/jarmods.svg rename to launcher/resources/OSX/scalable/jarmods.svg diff --git a/application/resources/OSX/scalable/java.svg b/launcher/resources/OSX/scalable/java.svg similarity index 100% rename from application/resources/OSX/scalable/java.svg rename to launcher/resources/OSX/scalable/java.svg diff --git a/application/resources/OSX/scalable/language.svg b/launcher/resources/OSX/scalable/language.svg similarity index 100% rename from application/resources/OSX/scalable/language.svg rename to launcher/resources/OSX/scalable/language.svg diff --git a/application/resources/OSX/scalable/loadermods.svg b/launcher/resources/OSX/scalable/loadermods.svg similarity index 100% rename from application/resources/OSX/scalable/loadermods.svg rename to launcher/resources/OSX/scalable/loadermods.svg diff --git a/application/resources/OSX/scalable/log.svg b/launcher/resources/OSX/scalable/log.svg similarity index 100% rename from application/resources/OSX/scalable/log.svg rename to launcher/resources/OSX/scalable/log.svg diff --git a/application/resources/OSX/scalable/minecraft.svg b/launcher/resources/OSX/scalable/minecraft.svg similarity index 100% rename from application/resources/OSX/scalable/minecraft.svg rename to launcher/resources/OSX/scalable/minecraft.svg diff --git a/application/resources/OSX/scalable/multimc.svg b/launcher/resources/OSX/scalable/multimc.svg similarity index 100% rename from application/resources/OSX/scalable/multimc.svg rename to launcher/resources/OSX/scalable/multimc.svg diff --git a/application/resources/OSX/scalable/new.svg b/launcher/resources/OSX/scalable/new.svg similarity index 100% rename from application/resources/OSX/scalable/new.svg rename to launcher/resources/OSX/scalable/new.svg diff --git a/application/resources/OSX/scalable/news.svg b/launcher/resources/OSX/scalable/news.svg similarity index 100% rename from application/resources/OSX/scalable/news.svg rename to launcher/resources/OSX/scalable/news.svg diff --git a/application/resources/OSX/scalable/notes.svg b/launcher/resources/OSX/scalable/notes.svg similarity index 100% rename from application/resources/OSX/scalable/notes.svg rename to launcher/resources/OSX/scalable/notes.svg diff --git a/application/resources/OSX/scalable/patreon.svg b/launcher/resources/OSX/scalable/patreon.svg similarity index 100% rename from application/resources/OSX/scalable/patreon.svg rename to launcher/resources/OSX/scalable/patreon.svg diff --git a/application/resources/OSX/scalable/proxy.svg b/launcher/resources/OSX/scalable/proxy.svg similarity index 100% rename from application/resources/OSX/scalable/proxy.svg rename to launcher/resources/OSX/scalable/proxy.svg diff --git a/application/resources/OSX/scalable/quickmods.svg b/launcher/resources/OSX/scalable/quickmods.svg similarity index 100% rename from application/resources/OSX/scalable/quickmods.svg rename to launcher/resources/OSX/scalable/quickmods.svg diff --git a/application/resources/OSX/scalable/refresh.svg b/launcher/resources/OSX/scalable/refresh.svg similarity index 100% rename from application/resources/OSX/scalable/refresh.svg rename to launcher/resources/OSX/scalable/refresh.svg diff --git a/application/resources/OSX/scalable/resourcepacks.svg b/launcher/resources/OSX/scalable/resourcepacks.svg similarity index 100% rename from application/resources/OSX/scalable/resourcepacks.svg rename to launcher/resources/OSX/scalable/resourcepacks.svg diff --git a/application/resources/OSX/scalable/screenshots.svg b/launcher/resources/OSX/scalable/screenshots.svg similarity index 100% rename from application/resources/OSX/scalable/screenshots.svg rename to launcher/resources/OSX/scalable/screenshots.svg diff --git a/application/resources/OSX/scalable/settings.svg b/launcher/resources/OSX/scalable/settings.svg similarity index 100% rename from application/resources/OSX/scalable/settings.svg rename to launcher/resources/OSX/scalable/settings.svg diff --git a/application/resources/OSX/scalable/status-bad.svg b/launcher/resources/OSX/scalable/status-bad.svg similarity index 100% rename from application/resources/OSX/scalable/status-bad.svg rename to launcher/resources/OSX/scalable/status-bad.svg diff --git a/application/resources/OSX/scalable/status-good.svg b/launcher/resources/OSX/scalable/status-good.svg similarity index 100% rename from application/resources/OSX/scalable/status-good.svg rename to launcher/resources/OSX/scalable/status-good.svg diff --git a/application/resources/OSX/scalable/status-yellow.svg b/launcher/resources/OSX/scalable/status-yellow.svg similarity index 100% rename from application/resources/OSX/scalable/status-yellow.svg rename to launcher/resources/OSX/scalable/status-yellow.svg diff --git a/application/resources/OSX/scalable/viewfolder.svg b/launcher/resources/OSX/scalable/viewfolder.svg similarity index 100% rename from application/resources/OSX/scalable/viewfolder.svg rename to launcher/resources/OSX/scalable/viewfolder.svg diff --git a/application/resources/OSX/scalable/worlds.svg b/launcher/resources/OSX/scalable/worlds.svg similarity index 100% rename from application/resources/OSX/scalable/worlds.svg rename to launcher/resources/OSX/scalable/worlds.svg diff --git a/application/resources/assets/underconstruction.png b/launcher/resources/assets/underconstruction.png similarity index 100% rename from application/resources/assets/underconstruction.png rename to launcher/resources/assets/underconstruction.png diff --git a/application/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc similarity index 100% rename from application/resources/backgrounds/backgrounds.qrc rename to launcher/resources/backgrounds/backgrounds.qrc diff --git a/application/resources/backgrounds/catbgrnd2.png b/launcher/resources/backgrounds/catbgrnd2.png similarity index 100% rename from application/resources/backgrounds/catbgrnd2.png rename to launcher/resources/backgrounds/catbgrnd2.png diff --git a/application/resources/backgrounds/catmas.png b/launcher/resources/backgrounds/catmas.png similarity index 100% rename from application/resources/backgrounds/catmas.png rename to launcher/resources/backgrounds/catmas.png diff --git a/application/resources/documents/documents.qrc b/launcher/resources/documents/documents.qrc similarity index 100% rename from application/resources/documents/documents.qrc rename to launcher/resources/documents/documents.qrc diff --git a/application/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc similarity index 97% rename from application/resources/flat/flat.qrc rename to launcher/resources/flat/flat.qrc index b6e2ee3820..614d7f989b 100644 --- a/application/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -10,6 +10,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/application/resources/flat/index.theme b/launcher/resources/flat/index.theme similarity index 100% rename from application/resources/flat/index.theme rename to launcher/resources/flat/index.theme diff --git a/application/resources/flat/scalable/about.svg b/launcher/resources/flat/scalable/about.svg similarity index 100% rename from application/resources/flat/scalable/about.svg rename to launcher/resources/flat/scalable/about.svg diff --git a/application/resources/flat/scalable/accounts.svg b/launcher/resources/flat/scalable/accounts.svg similarity index 100% rename from application/resources/flat/scalable/accounts.svg rename to launcher/resources/flat/scalable/accounts.svg diff --git a/application/resources/flat/scalable/bug.svg b/launcher/resources/flat/scalable/bug.svg similarity index 100% rename from application/resources/flat/scalable/bug.svg rename to launcher/resources/flat/scalable/bug.svg diff --git a/application/resources/flat/scalable/cat.svg b/launcher/resources/flat/scalable/cat.svg similarity index 100% rename from application/resources/flat/scalable/cat.svg rename to launcher/resources/flat/scalable/cat.svg diff --git a/application/resources/flat/scalable/centralmods.svg b/launcher/resources/flat/scalable/centralmods.svg similarity index 100% rename from application/resources/flat/scalable/centralmods.svg rename to launcher/resources/flat/scalable/centralmods.svg diff --git a/application/resources/flat/scalable/checkupdate.svg b/launcher/resources/flat/scalable/checkupdate.svg similarity index 100% rename from application/resources/flat/scalable/checkupdate.svg rename to launcher/resources/flat/scalable/checkupdate.svg diff --git a/application/resources/flat/scalable/copy.svg b/launcher/resources/flat/scalable/copy.svg similarity index 100% rename from application/resources/flat/scalable/copy.svg rename to launcher/resources/flat/scalable/copy.svg diff --git a/application/resources/flat/scalable/coremods.svg b/launcher/resources/flat/scalable/coremods.svg similarity index 100% rename from application/resources/flat/scalable/coremods.svg rename to launcher/resources/flat/scalable/coremods.svg diff --git a/launcher/resources/flat/scalable/custom-commands.svg b/launcher/resources/flat/scalable/custom-commands.svg new file mode 100644 index 0000000000..a35634b167 --- /dev/null +++ b/launcher/resources/flat/scalable/custom-commands.svg @@ -0,0 +1,86 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/application/resources/flat/scalable/discord.svg b/launcher/resources/flat/scalable/discord.svg similarity index 100% rename from application/resources/flat/scalable/discord.svg rename to launcher/resources/flat/scalable/discord.svg diff --git a/application/resources/flat/scalable/externaltools.svg b/launcher/resources/flat/scalable/externaltools.svg similarity index 100% rename from application/resources/flat/scalable/externaltools.svg rename to launcher/resources/flat/scalable/externaltools.svg diff --git a/application/resources/flat/scalable/help.svg b/launcher/resources/flat/scalable/help.svg similarity index 100% rename from application/resources/flat/scalable/help.svg rename to launcher/resources/flat/scalable/help.svg diff --git a/application/resources/flat/scalable/instance-settings.svg b/launcher/resources/flat/scalable/instance-settings.svg similarity index 100% rename from application/resources/flat/scalable/instance-settings.svg rename to launcher/resources/flat/scalable/instance-settings.svg diff --git a/application/resources/flat/scalable/jarmods.svg b/launcher/resources/flat/scalable/jarmods.svg similarity index 100% rename from application/resources/flat/scalable/jarmods.svg rename to launcher/resources/flat/scalable/jarmods.svg diff --git a/application/resources/flat/scalable/java.svg b/launcher/resources/flat/scalable/java.svg similarity index 100% rename from application/resources/flat/scalable/java.svg rename to launcher/resources/flat/scalable/java.svg diff --git a/application/resources/flat/scalable/language.svg b/launcher/resources/flat/scalable/language.svg similarity index 100% rename from application/resources/flat/scalable/language.svg rename to launcher/resources/flat/scalable/language.svg diff --git a/application/resources/flat/scalable/loadermods.svg b/launcher/resources/flat/scalable/loadermods.svg similarity index 100% rename from application/resources/flat/scalable/loadermods.svg rename to launcher/resources/flat/scalable/loadermods.svg diff --git a/application/resources/flat/scalable/log.svg b/launcher/resources/flat/scalable/log.svg similarity index 100% rename from application/resources/flat/scalable/log.svg rename to launcher/resources/flat/scalable/log.svg diff --git a/application/resources/flat/scalable/minecraft.svg b/launcher/resources/flat/scalable/minecraft.svg similarity index 100% rename from application/resources/flat/scalable/minecraft.svg rename to launcher/resources/flat/scalable/minecraft.svg diff --git a/application/resources/flat/scalable/multimc.svg b/launcher/resources/flat/scalable/multimc.svg similarity index 100% rename from application/resources/flat/scalable/multimc.svg rename to launcher/resources/flat/scalable/multimc.svg diff --git a/application/resources/flat/scalable/new.svg b/launcher/resources/flat/scalable/new.svg similarity index 100% rename from application/resources/flat/scalable/new.svg rename to launcher/resources/flat/scalable/new.svg diff --git a/application/resources/flat/scalable/news.svg b/launcher/resources/flat/scalable/news.svg similarity index 100% rename from application/resources/flat/scalable/news.svg rename to launcher/resources/flat/scalable/news.svg diff --git a/application/resources/flat/scalable/notes.svg b/launcher/resources/flat/scalable/notes.svg similarity index 100% rename from application/resources/flat/scalable/notes.svg rename to launcher/resources/flat/scalable/notes.svg diff --git a/application/resources/flat/scalable/packages.svg b/launcher/resources/flat/scalable/packages.svg similarity index 100% rename from application/resources/flat/scalable/packages.svg rename to launcher/resources/flat/scalable/packages.svg diff --git a/application/resources/flat/scalable/patreon.svg b/launcher/resources/flat/scalable/patreon.svg similarity index 100% rename from application/resources/flat/scalable/patreon.svg rename to launcher/resources/flat/scalable/patreon.svg diff --git a/application/resources/flat/scalable/proxy.svg b/launcher/resources/flat/scalable/proxy.svg similarity index 100% rename from application/resources/flat/scalable/proxy.svg rename to launcher/resources/flat/scalable/proxy.svg diff --git a/application/resources/flat/scalable/quickmods.svg b/launcher/resources/flat/scalable/quickmods.svg similarity index 100% rename from application/resources/flat/scalable/quickmods.svg rename to launcher/resources/flat/scalable/quickmods.svg diff --git a/application/resources/flat/scalable/reddit-alien.svg b/launcher/resources/flat/scalable/reddit-alien.svg similarity index 100% rename from application/resources/flat/scalable/reddit-alien.svg rename to launcher/resources/flat/scalable/reddit-alien.svg diff --git a/application/resources/flat/scalable/refresh.svg b/launcher/resources/flat/scalable/refresh.svg similarity index 100% rename from application/resources/flat/scalable/refresh.svg rename to launcher/resources/flat/scalable/refresh.svg diff --git a/application/resources/flat/scalable/resourcepacks.svg b/launcher/resources/flat/scalable/resourcepacks.svg similarity index 100% rename from application/resources/flat/scalable/resourcepacks.svg rename to launcher/resources/flat/scalable/resourcepacks.svg diff --git a/application/resources/flat/scalable/screenshot-placeholder.svg b/launcher/resources/flat/scalable/screenshot-placeholder.svg similarity index 100% rename from application/resources/flat/scalable/screenshot-placeholder.svg rename to launcher/resources/flat/scalable/screenshot-placeholder.svg diff --git a/application/resources/flat/scalable/screenshots.svg b/launcher/resources/flat/scalable/screenshots.svg similarity index 100% rename from application/resources/flat/scalable/screenshots.svg rename to launcher/resources/flat/scalable/screenshots.svg diff --git a/application/resources/flat/scalable/settings.svg b/launcher/resources/flat/scalable/settings.svg similarity index 100% rename from application/resources/flat/scalable/settings.svg rename to launcher/resources/flat/scalable/settings.svg diff --git a/application/resources/flat/scalable/star.svg b/launcher/resources/flat/scalable/star.svg similarity index 100% rename from application/resources/flat/scalable/star.svg rename to launcher/resources/flat/scalable/star.svg diff --git a/application/resources/flat/scalable/status-bad.svg b/launcher/resources/flat/scalable/status-bad.svg similarity index 100% rename from application/resources/flat/scalable/status-bad.svg rename to launcher/resources/flat/scalable/status-bad.svg diff --git a/application/resources/flat/scalable/status-good.svg b/launcher/resources/flat/scalable/status-good.svg similarity index 100% rename from application/resources/flat/scalable/status-good.svg rename to launcher/resources/flat/scalable/status-good.svg diff --git a/application/resources/flat/scalable/status-running.svg b/launcher/resources/flat/scalable/status-running.svg similarity index 100% rename from application/resources/flat/scalable/status-running.svg rename to launcher/resources/flat/scalable/status-running.svg diff --git a/application/resources/flat/scalable/status-yellow.svg b/launcher/resources/flat/scalable/status-yellow.svg similarity index 100% rename from application/resources/flat/scalable/status-yellow.svg rename to launcher/resources/flat/scalable/status-yellow.svg diff --git a/application/resources/flat/scalable/viewfolder.svg b/launcher/resources/flat/scalable/viewfolder.svg similarity index 100% rename from application/resources/flat/scalable/viewfolder.svg rename to launcher/resources/flat/scalable/viewfolder.svg diff --git a/application/resources/flat/scalable/worlds.svg b/launcher/resources/flat/scalable/worlds.svg similarity index 100% rename from application/resources/flat/scalable/worlds.svg rename to launcher/resources/flat/scalable/worlds.svg diff --git a/application/resources/iOS/iOS.qrc b/launcher/resources/iOS/iOS.qrc similarity index 96% rename from application/resources/iOS/iOS.qrc rename to launcher/resources/iOS/iOS.qrc index 511e390bab..75c88bb036 100644 --- a/application/resources/iOS/iOS.qrc +++ b/launcher/resources/iOS/iOS.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/application/resources/iOS/index.theme b/launcher/resources/iOS/index.theme similarity index 100% rename from application/resources/iOS/index.theme rename to launcher/resources/iOS/index.theme diff --git a/application/resources/iOS/scalable/about.svg b/launcher/resources/iOS/scalable/about.svg similarity index 100% rename from application/resources/iOS/scalable/about.svg rename to launcher/resources/iOS/scalable/about.svg diff --git a/application/resources/iOS/scalable/accounts.svg b/launcher/resources/iOS/scalable/accounts.svg similarity index 100% rename from application/resources/iOS/scalable/accounts.svg rename to launcher/resources/iOS/scalable/accounts.svg diff --git a/application/resources/iOS/scalable/bug.svg b/launcher/resources/iOS/scalable/bug.svg similarity index 100% rename from application/resources/iOS/scalable/bug.svg rename to launcher/resources/iOS/scalable/bug.svg diff --git a/application/resources/iOS/scalable/centralmods.svg b/launcher/resources/iOS/scalable/centralmods.svg similarity index 100% rename from application/resources/iOS/scalable/centralmods.svg rename to launcher/resources/iOS/scalable/centralmods.svg diff --git a/application/resources/iOS/scalable/checkupdate.svg b/launcher/resources/iOS/scalable/checkupdate.svg similarity index 100% rename from application/resources/iOS/scalable/checkupdate.svg rename to launcher/resources/iOS/scalable/checkupdate.svg diff --git a/application/resources/iOS/scalable/copy.svg b/launcher/resources/iOS/scalable/copy.svg similarity index 100% rename from application/resources/iOS/scalable/copy.svg rename to launcher/resources/iOS/scalable/copy.svg diff --git a/application/resources/iOS/scalable/coremods.svg b/launcher/resources/iOS/scalable/coremods.svg similarity index 100% rename from application/resources/iOS/scalable/coremods.svg rename to launcher/resources/iOS/scalable/coremods.svg diff --git a/launcher/resources/iOS/scalable/custom-commands.svg b/launcher/resources/iOS/scalable/custom-commands.svg new file mode 100644 index 0000000000..f44e2bfe28 --- /dev/null +++ b/launcher/resources/iOS/scalable/custom-commands.svg @@ -0,0 +1,63 @@ + +image/svg+xml + + + + + diff --git a/application/resources/iOS/scalable/externaltools.svg b/launcher/resources/iOS/scalable/externaltools.svg similarity index 100% rename from application/resources/iOS/scalable/externaltools.svg rename to launcher/resources/iOS/scalable/externaltools.svg diff --git a/application/resources/iOS/scalable/help.svg b/launcher/resources/iOS/scalable/help.svg similarity index 100% rename from application/resources/iOS/scalable/help.svg rename to launcher/resources/iOS/scalable/help.svg diff --git a/application/resources/iOS/scalable/instance-settings.svg b/launcher/resources/iOS/scalable/instance-settings.svg similarity index 100% rename from application/resources/iOS/scalable/instance-settings.svg rename to launcher/resources/iOS/scalable/instance-settings.svg diff --git a/application/resources/iOS/scalable/jarmods.svg b/launcher/resources/iOS/scalable/jarmods.svg similarity index 100% rename from application/resources/iOS/scalable/jarmods.svg rename to launcher/resources/iOS/scalable/jarmods.svg diff --git a/application/resources/iOS/scalable/java.svg b/launcher/resources/iOS/scalable/java.svg similarity index 100% rename from application/resources/iOS/scalable/java.svg rename to launcher/resources/iOS/scalable/java.svg diff --git a/application/resources/iOS/scalable/language.svg b/launcher/resources/iOS/scalable/language.svg similarity index 100% rename from application/resources/iOS/scalable/language.svg rename to launcher/resources/iOS/scalable/language.svg diff --git a/application/resources/iOS/scalable/loadermods.svg b/launcher/resources/iOS/scalable/loadermods.svg similarity index 100% rename from application/resources/iOS/scalable/loadermods.svg rename to launcher/resources/iOS/scalable/loadermods.svg diff --git a/application/resources/iOS/scalable/log.svg b/launcher/resources/iOS/scalable/log.svg similarity index 100% rename from application/resources/iOS/scalable/log.svg rename to launcher/resources/iOS/scalable/log.svg diff --git a/application/resources/iOS/scalable/minecraft.svg b/launcher/resources/iOS/scalable/minecraft.svg similarity index 100% rename from application/resources/iOS/scalable/minecraft.svg rename to launcher/resources/iOS/scalable/minecraft.svg diff --git a/application/resources/iOS/scalable/multimc.svg b/launcher/resources/iOS/scalable/multimc.svg similarity index 100% rename from application/resources/iOS/scalable/multimc.svg rename to launcher/resources/iOS/scalable/multimc.svg diff --git a/application/resources/iOS/scalable/new.svg b/launcher/resources/iOS/scalable/new.svg similarity index 100% rename from application/resources/iOS/scalable/new.svg rename to launcher/resources/iOS/scalable/new.svg diff --git a/application/resources/iOS/scalable/news.svg b/launcher/resources/iOS/scalable/news.svg similarity index 100% rename from application/resources/iOS/scalable/news.svg rename to launcher/resources/iOS/scalable/news.svg diff --git a/application/resources/iOS/scalable/notes.svg b/launcher/resources/iOS/scalable/notes.svg similarity index 100% rename from application/resources/iOS/scalable/notes.svg rename to launcher/resources/iOS/scalable/notes.svg diff --git a/application/resources/iOS/scalable/patreon.svg b/launcher/resources/iOS/scalable/patreon.svg similarity index 100% rename from application/resources/iOS/scalable/patreon.svg rename to launcher/resources/iOS/scalable/patreon.svg diff --git a/application/resources/iOS/scalable/proxy.svg b/launcher/resources/iOS/scalable/proxy.svg similarity index 100% rename from application/resources/iOS/scalable/proxy.svg rename to launcher/resources/iOS/scalable/proxy.svg diff --git a/application/resources/iOS/scalable/quickmods.svg b/launcher/resources/iOS/scalable/quickmods.svg similarity index 100% rename from application/resources/iOS/scalable/quickmods.svg rename to launcher/resources/iOS/scalable/quickmods.svg diff --git a/application/resources/iOS/scalable/refresh.svg b/launcher/resources/iOS/scalable/refresh.svg similarity index 100% rename from application/resources/iOS/scalable/refresh.svg rename to launcher/resources/iOS/scalable/refresh.svg diff --git a/application/resources/iOS/scalable/resourcepacks.svg b/launcher/resources/iOS/scalable/resourcepacks.svg similarity index 100% rename from application/resources/iOS/scalable/resourcepacks.svg rename to launcher/resources/iOS/scalable/resourcepacks.svg diff --git a/application/resources/iOS/scalable/screenshots.svg b/launcher/resources/iOS/scalable/screenshots.svg similarity index 100% rename from application/resources/iOS/scalable/screenshots.svg rename to launcher/resources/iOS/scalable/screenshots.svg diff --git a/application/resources/iOS/scalable/settings.svg b/launcher/resources/iOS/scalable/settings.svg similarity index 100% rename from application/resources/iOS/scalable/settings.svg rename to launcher/resources/iOS/scalable/settings.svg diff --git a/application/resources/iOS/scalable/status-bad.svg b/launcher/resources/iOS/scalable/status-bad.svg similarity index 100% rename from application/resources/iOS/scalable/status-bad.svg rename to launcher/resources/iOS/scalable/status-bad.svg diff --git a/application/resources/iOS/scalable/status-good.svg b/launcher/resources/iOS/scalable/status-good.svg similarity index 100% rename from application/resources/iOS/scalable/status-good.svg rename to launcher/resources/iOS/scalable/status-good.svg diff --git a/application/resources/iOS/scalable/status-yellow.svg b/launcher/resources/iOS/scalable/status-yellow.svg similarity index 100% rename from application/resources/iOS/scalable/status-yellow.svg rename to launcher/resources/iOS/scalable/status-yellow.svg diff --git a/application/resources/iOS/scalable/viewfolder.svg b/launcher/resources/iOS/scalable/viewfolder.svg similarity index 100% rename from application/resources/iOS/scalable/viewfolder.svg rename to launcher/resources/iOS/scalable/viewfolder.svg diff --git a/application/resources/iOS/scalable/worlds.svg b/launcher/resources/iOS/scalable/worlds.svg similarity index 100% rename from application/resources/iOS/scalable/worlds.svg rename to launcher/resources/iOS/scalable/worlds.svg diff --git a/application/resources/multimc.rc b/launcher/resources/multimc.rc similarity index 100% rename from application/resources/multimc.rc rename to launcher/resources/multimc.rc diff --git a/application/resources/multimc/128x128/instances/chicken.png b/launcher/resources/multimc/128x128/instances/chicken.png similarity index 100% rename from application/resources/multimc/128x128/instances/chicken.png rename to launcher/resources/multimc/128x128/instances/chicken.png diff --git a/application/resources/multimc/128x128/instances/creeper.png b/launcher/resources/multimc/128x128/instances/creeper.png similarity index 100% rename from application/resources/multimc/128x128/instances/creeper.png rename to launcher/resources/multimc/128x128/instances/creeper.png diff --git a/application/resources/multimc/128x128/instances/enderpearl.png b/launcher/resources/multimc/128x128/instances/enderpearl.png similarity index 100% rename from application/resources/multimc/128x128/instances/enderpearl.png rename to launcher/resources/multimc/128x128/instances/enderpearl.png diff --git a/application/resources/multimc/128x128/instances/flame.png b/launcher/resources/multimc/128x128/instances/flame.png similarity index 100% rename from application/resources/multimc/128x128/instances/flame.png rename to launcher/resources/multimc/128x128/instances/flame.png diff --git a/application/resources/multimc/128x128/instances/ftb_glow.png b/launcher/resources/multimc/128x128/instances/ftb_glow.png similarity index 100% rename from application/resources/multimc/128x128/instances/ftb_glow.png rename to launcher/resources/multimc/128x128/instances/ftb_glow.png diff --git a/application/resources/multimc/128x128/instances/ftb_logo.png b/launcher/resources/multimc/128x128/instances/ftb_logo.png similarity index 100% rename from application/resources/multimc/128x128/instances/ftb_logo.png rename to launcher/resources/multimc/128x128/instances/ftb_logo.png diff --git a/application/resources/multimc/128x128/instances/gear.png b/launcher/resources/multimc/128x128/instances/gear.png similarity index 100% rename from application/resources/multimc/128x128/instances/gear.png rename to launcher/resources/multimc/128x128/instances/gear.png diff --git a/application/resources/multimc/128x128/instances/herobrine.png b/launcher/resources/multimc/128x128/instances/herobrine.png similarity index 100% rename from application/resources/multimc/128x128/instances/herobrine.png rename to launcher/resources/multimc/128x128/instances/herobrine.png diff --git a/application/resources/multimc/128x128/instances/infinity.png b/launcher/resources/multimc/128x128/instances/infinity.png similarity index 100% rename from application/resources/multimc/128x128/instances/infinity.png rename to launcher/resources/multimc/128x128/instances/infinity.png diff --git a/application/resources/multimc/128x128/instances/magitech.png b/launcher/resources/multimc/128x128/instances/magitech.png similarity index 100% rename from application/resources/multimc/128x128/instances/magitech.png rename to launcher/resources/multimc/128x128/instances/magitech.png diff --git a/application/resources/multimc/128x128/instances/meat.png b/launcher/resources/multimc/128x128/instances/meat.png similarity index 100% rename from application/resources/multimc/128x128/instances/meat.png rename to launcher/resources/multimc/128x128/instances/meat.png diff --git a/application/resources/multimc/128x128/instances/netherstar.png b/launcher/resources/multimc/128x128/instances/netherstar.png similarity index 100% rename from application/resources/multimc/128x128/instances/netherstar.png rename to launcher/resources/multimc/128x128/instances/netherstar.png diff --git a/application/resources/multimc/128x128/instances/skeleton.png b/launcher/resources/multimc/128x128/instances/skeleton.png similarity index 100% rename from application/resources/multimc/128x128/instances/skeleton.png rename to launcher/resources/multimc/128x128/instances/skeleton.png diff --git a/application/resources/multimc/128x128/instances/squarecreeper.png b/launcher/resources/multimc/128x128/instances/squarecreeper.png similarity index 100% rename from application/resources/multimc/128x128/instances/squarecreeper.png rename to launcher/resources/multimc/128x128/instances/squarecreeper.png diff --git a/application/resources/multimc/128x128/instances/steve.png b/launcher/resources/multimc/128x128/instances/steve.png similarity index 100% rename from application/resources/multimc/128x128/instances/steve.png rename to launcher/resources/multimc/128x128/instances/steve.png diff --git a/application/resources/multimc/128x128/unknown_server.png b/launcher/resources/multimc/128x128/unknown_server.png similarity index 100% rename from application/resources/multimc/128x128/unknown_server.png rename to launcher/resources/multimc/128x128/unknown_server.png diff --git a/application/resources/multimc/16x16/about.png b/launcher/resources/multimc/16x16/about.png similarity index 100% rename from application/resources/multimc/16x16/about.png rename to launcher/resources/multimc/16x16/about.png diff --git a/application/resources/multimc/16x16/bug.png b/launcher/resources/multimc/16x16/bug.png similarity index 100% rename from application/resources/multimc/16x16/bug.png rename to launcher/resources/multimc/16x16/bug.png diff --git a/application/resources/multimc/16x16/cat.png b/launcher/resources/multimc/16x16/cat.png similarity index 100% rename from application/resources/multimc/16x16/cat.png rename to launcher/resources/multimc/16x16/cat.png diff --git a/application/resources/multimc/16x16/centralmods.png b/launcher/resources/multimc/16x16/centralmods.png similarity index 100% rename from application/resources/multimc/16x16/centralmods.png rename to launcher/resources/multimc/16x16/centralmods.png diff --git a/application/resources/multimc/16x16/checkupdate.png b/launcher/resources/multimc/16x16/checkupdate.png similarity index 100% rename from application/resources/multimc/16x16/checkupdate.png rename to launcher/resources/multimc/16x16/checkupdate.png diff --git a/application/resources/multimc/16x16/copy.png b/launcher/resources/multimc/16x16/copy.png similarity index 100% rename from application/resources/multimc/16x16/copy.png rename to launcher/resources/multimc/16x16/copy.png diff --git a/application/resources/multimc/16x16/coremods.png b/launcher/resources/multimc/16x16/coremods.png similarity index 100% rename from application/resources/multimc/16x16/coremods.png rename to launcher/resources/multimc/16x16/coremods.png diff --git a/application/resources/multimc/16x16/help.png b/launcher/resources/multimc/16x16/help.png similarity index 100% rename from application/resources/multimc/16x16/help.png rename to launcher/resources/multimc/16x16/help.png diff --git a/application/resources/multimc/16x16/instance-settings.png b/launcher/resources/multimc/16x16/instance-settings.png similarity index 100% rename from application/resources/multimc/16x16/instance-settings.png rename to launcher/resources/multimc/16x16/instance-settings.png diff --git a/application/resources/multimc/16x16/jarmods.png b/launcher/resources/multimc/16x16/jarmods.png similarity index 100% rename from application/resources/multimc/16x16/jarmods.png rename to launcher/resources/multimc/16x16/jarmods.png diff --git a/application/resources/multimc/16x16/loadermods.png b/launcher/resources/multimc/16x16/loadermods.png similarity index 100% rename from application/resources/multimc/16x16/loadermods.png rename to launcher/resources/multimc/16x16/loadermods.png diff --git a/application/resources/multimc/16x16/log.png b/launcher/resources/multimc/16x16/log.png similarity index 100% rename from application/resources/multimc/16x16/log.png rename to launcher/resources/multimc/16x16/log.png diff --git a/application/resources/multimc/16x16/minecraft.png b/launcher/resources/multimc/16x16/minecraft.png similarity index 100% rename from application/resources/multimc/16x16/minecraft.png rename to launcher/resources/multimc/16x16/minecraft.png diff --git a/application/resources/multimc/16x16/new.png b/launcher/resources/multimc/16x16/new.png similarity index 100% rename from application/resources/multimc/16x16/new.png rename to launcher/resources/multimc/16x16/new.png diff --git a/application/resources/multimc/16x16/news.png b/launcher/resources/multimc/16x16/news.png similarity index 100% rename from application/resources/multimc/16x16/news.png rename to launcher/resources/multimc/16x16/news.png diff --git a/application/resources/multimc/16x16/noaccount.png b/launcher/resources/multimc/16x16/noaccount.png similarity index 100% rename from application/resources/multimc/16x16/noaccount.png rename to launcher/resources/multimc/16x16/noaccount.png diff --git a/launcher/resources/multimc/16x16/patreon.png b/launcher/resources/multimc/16x16/patreon.png new file mode 100644 index 0000000000..9150c478fa Binary files /dev/null and b/launcher/resources/multimc/16x16/patreon.png differ diff --git a/application/resources/multimc/16x16/refresh.png b/launcher/resources/multimc/16x16/refresh.png similarity index 100% rename from application/resources/multimc/16x16/refresh.png rename to launcher/resources/multimc/16x16/refresh.png diff --git a/application/resources/multimc/16x16/resourcepacks.png b/launcher/resources/multimc/16x16/resourcepacks.png similarity index 100% rename from application/resources/multimc/16x16/resourcepacks.png rename to launcher/resources/multimc/16x16/resourcepacks.png diff --git a/application/resources/multimc/16x16/screenshots.png b/launcher/resources/multimc/16x16/screenshots.png similarity index 100% rename from application/resources/multimc/16x16/screenshots.png rename to launcher/resources/multimc/16x16/screenshots.png diff --git a/application/resources/multimc/16x16/settings.png b/launcher/resources/multimc/16x16/settings.png similarity index 100% rename from application/resources/multimc/16x16/settings.png rename to launcher/resources/multimc/16x16/settings.png diff --git a/application/resources/multimc/16x16/star.png b/launcher/resources/multimc/16x16/star.png similarity index 100% rename from application/resources/multimc/16x16/star.png rename to launcher/resources/multimc/16x16/star.png diff --git a/application/resources/multimc/16x16/status-bad.png b/launcher/resources/multimc/16x16/status-bad.png similarity index 100% rename from application/resources/multimc/16x16/status-bad.png rename to launcher/resources/multimc/16x16/status-bad.png diff --git a/application/resources/multimc/16x16/status-good.png b/launcher/resources/multimc/16x16/status-good.png similarity index 100% rename from application/resources/multimc/16x16/status-good.png rename to launcher/resources/multimc/16x16/status-good.png diff --git a/application/resources/multimc/16x16/status-running.png b/launcher/resources/multimc/16x16/status-running.png similarity index 100% rename from application/resources/multimc/16x16/status-running.png rename to launcher/resources/multimc/16x16/status-running.png diff --git a/application/resources/multimc/16x16/status-yellow.png b/launcher/resources/multimc/16x16/status-yellow.png similarity index 100% rename from application/resources/multimc/16x16/status-yellow.png rename to launcher/resources/multimc/16x16/status-yellow.png diff --git a/application/resources/multimc/16x16/viewfolder.png b/launcher/resources/multimc/16x16/viewfolder.png similarity index 100% rename from application/resources/multimc/16x16/viewfolder.png rename to launcher/resources/multimc/16x16/viewfolder.png diff --git a/application/resources/multimc/16x16/worlds.png b/launcher/resources/multimc/16x16/worlds.png similarity index 100% rename from application/resources/multimc/16x16/worlds.png rename to launcher/resources/multimc/16x16/worlds.png diff --git a/application/resources/multimc/22x22/about.png b/launcher/resources/multimc/22x22/about.png similarity index 100% rename from application/resources/multimc/22x22/about.png rename to launcher/resources/multimc/22x22/about.png diff --git a/application/resources/multimc/22x22/bug.png b/launcher/resources/multimc/22x22/bug.png similarity index 100% rename from application/resources/multimc/22x22/bug.png rename to launcher/resources/multimc/22x22/bug.png diff --git a/application/resources/multimc/22x22/cat.png b/launcher/resources/multimc/22x22/cat.png similarity index 100% rename from application/resources/multimc/22x22/cat.png rename to launcher/resources/multimc/22x22/cat.png diff --git a/application/resources/multimc/22x22/centralmods.png b/launcher/resources/multimc/22x22/centralmods.png similarity index 100% rename from application/resources/multimc/22x22/centralmods.png rename to launcher/resources/multimc/22x22/centralmods.png diff --git a/application/resources/multimc/22x22/checkupdate.png b/launcher/resources/multimc/22x22/checkupdate.png similarity index 100% rename from application/resources/multimc/22x22/checkupdate.png rename to launcher/resources/multimc/22x22/checkupdate.png diff --git a/application/resources/multimc/22x22/copy.png b/launcher/resources/multimc/22x22/copy.png similarity index 100% rename from application/resources/multimc/22x22/copy.png rename to launcher/resources/multimc/22x22/copy.png diff --git a/application/resources/multimc/22x22/help.png b/launcher/resources/multimc/22x22/help.png similarity index 100% rename from application/resources/multimc/22x22/help.png rename to launcher/resources/multimc/22x22/help.png diff --git a/application/resources/multimc/22x22/instance-settings.png b/launcher/resources/multimc/22x22/instance-settings.png similarity index 100% rename from application/resources/multimc/22x22/instance-settings.png rename to launcher/resources/multimc/22x22/instance-settings.png diff --git a/application/resources/multimc/22x22/new.png b/launcher/resources/multimc/22x22/new.png similarity index 100% rename from application/resources/multimc/22x22/new.png rename to launcher/resources/multimc/22x22/new.png diff --git a/application/resources/multimc/22x22/news.png b/launcher/resources/multimc/22x22/news.png similarity index 100% rename from application/resources/multimc/22x22/news.png rename to launcher/resources/multimc/22x22/news.png diff --git a/launcher/resources/multimc/22x22/patreon.png b/launcher/resources/multimc/22x22/patreon.png new file mode 100644 index 0000000000..f2c2076c0b Binary files /dev/null and b/launcher/resources/multimc/22x22/patreon.png differ diff --git a/application/resources/multimc/22x22/refresh.png b/launcher/resources/multimc/22x22/refresh.png similarity index 100% rename from application/resources/multimc/22x22/refresh.png rename to launcher/resources/multimc/22x22/refresh.png diff --git a/application/resources/multimc/22x22/screenshots.png b/launcher/resources/multimc/22x22/screenshots.png similarity index 100% rename from application/resources/multimc/22x22/screenshots.png rename to launcher/resources/multimc/22x22/screenshots.png diff --git a/application/resources/multimc/22x22/settings.png b/launcher/resources/multimc/22x22/settings.png similarity index 100% rename from application/resources/multimc/22x22/settings.png rename to launcher/resources/multimc/22x22/settings.png diff --git a/application/resources/multimc/22x22/status-bad.png b/launcher/resources/multimc/22x22/status-bad.png similarity index 100% rename from application/resources/multimc/22x22/status-bad.png rename to launcher/resources/multimc/22x22/status-bad.png diff --git a/application/resources/multimc/22x22/status-good.png b/launcher/resources/multimc/22x22/status-good.png similarity index 100% rename from application/resources/multimc/22x22/status-good.png rename to launcher/resources/multimc/22x22/status-good.png diff --git a/application/resources/multimc/22x22/status-running.png b/launcher/resources/multimc/22x22/status-running.png similarity index 100% rename from application/resources/multimc/22x22/status-running.png rename to launcher/resources/multimc/22x22/status-running.png diff --git a/application/resources/multimc/22x22/status-yellow.png b/launcher/resources/multimc/22x22/status-yellow.png similarity index 100% rename from application/resources/multimc/22x22/status-yellow.png rename to launcher/resources/multimc/22x22/status-yellow.png diff --git a/application/resources/multimc/22x22/viewfolder.png b/launcher/resources/multimc/22x22/viewfolder.png similarity index 100% rename from application/resources/multimc/22x22/viewfolder.png rename to launcher/resources/multimc/22x22/viewfolder.png diff --git a/application/resources/multimc/22x22/worlds.png b/launcher/resources/multimc/22x22/worlds.png similarity index 100% rename from application/resources/multimc/22x22/worlds.png rename to launcher/resources/multimc/22x22/worlds.png diff --git a/application/resources/multimc/24x24/cat.png b/launcher/resources/multimc/24x24/cat.png similarity index 100% rename from application/resources/multimc/24x24/cat.png rename to launcher/resources/multimc/24x24/cat.png diff --git a/application/resources/multimc/24x24/coremods.png b/launcher/resources/multimc/24x24/coremods.png similarity index 100% rename from application/resources/multimc/24x24/coremods.png rename to launcher/resources/multimc/24x24/coremods.png diff --git a/application/resources/multimc/24x24/jarmods.png b/launcher/resources/multimc/24x24/jarmods.png similarity index 100% rename from application/resources/multimc/24x24/jarmods.png rename to launcher/resources/multimc/24x24/jarmods.png diff --git a/application/resources/multimc/24x24/loadermods.png b/launcher/resources/multimc/24x24/loadermods.png similarity index 100% rename from application/resources/multimc/24x24/loadermods.png rename to launcher/resources/multimc/24x24/loadermods.png diff --git a/application/resources/multimc/24x24/log.png b/launcher/resources/multimc/24x24/log.png similarity index 100% rename from application/resources/multimc/24x24/log.png rename to launcher/resources/multimc/24x24/log.png diff --git a/application/resources/multimc/24x24/minecraft.png b/launcher/resources/multimc/24x24/minecraft.png similarity index 100% rename from application/resources/multimc/24x24/minecraft.png rename to launcher/resources/multimc/24x24/minecraft.png diff --git a/application/resources/multimc/24x24/noaccount.png b/launcher/resources/multimc/24x24/noaccount.png similarity index 100% rename from application/resources/multimc/24x24/noaccount.png rename to launcher/resources/multimc/24x24/noaccount.png diff --git a/launcher/resources/multimc/24x24/patreon.png b/launcher/resources/multimc/24x24/patreon.png new file mode 100644 index 0000000000..add8066861 Binary files /dev/null and b/launcher/resources/multimc/24x24/patreon.png differ diff --git a/application/resources/multimc/24x24/resourcepacks.png b/launcher/resources/multimc/24x24/resourcepacks.png similarity index 100% rename from application/resources/multimc/24x24/resourcepacks.png rename to launcher/resources/multimc/24x24/resourcepacks.png diff --git a/application/resources/multimc/24x24/star.png b/launcher/resources/multimc/24x24/star.png similarity index 100% rename from application/resources/multimc/24x24/star.png rename to launcher/resources/multimc/24x24/star.png diff --git a/application/resources/multimc/24x24/status-bad.png b/launcher/resources/multimc/24x24/status-bad.png similarity index 100% rename from application/resources/multimc/24x24/status-bad.png rename to launcher/resources/multimc/24x24/status-bad.png diff --git a/application/resources/multimc/24x24/status-good.png b/launcher/resources/multimc/24x24/status-good.png similarity index 100% rename from application/resources/multimc/24x24/status-good.png rename to launcher/resources/multimc/24x24/status-good.png diff --git a/application/resources/multimc/24x24/status-running.png b/launcher/resources/multimc/24x24/status-running.png similarity index 100% rename from application/resources/multimc/24x24/status-running.png rename to launcher/resources/multimc/24x24/status-running.png diff --git a/application/resources/multimc/24x24/status-yellow.png b/launcher/resources/multimc/24x24/status-yellow.png similarity index 100% rename from application/resources/multimc/24x24/status-yellow.png rename to launcher/resources/multimc/24x24/status-yellow.png diff --git a/application/resources/multimc/256x256/minecraft.png b/launcher/resources/multimc/256x256/minecraft.png similarity index 100% rename from application/resources/multimc/256x256/minecraft.png rename to launcher/resources/multimc/256x256/minecraft.png diff --git a/application/resources/multimc/32x32/about.png b/launcher/resources/multimc/32x32/about.png similarity index 100% rename from application/resources/multimc/32x32/about.png rename to launcher/resources/multimc/32x32/about.png diff --git a/application/resources/multimc/32x32/bug.png b/launcher/resources/multimc/32x32/bug.png similarity index 100% rename from application/resources/multimc/32x32/bug.png rename to launcher/resources/multimc/32x32/bug.png diff --git a/application/resources/multimc/32x32/cat.png b/launcher/resources/multimc/32x32/cat.png similarity index 100% rename from application/resources/multimc/32x32/cat.png rename to launcher/resources/multimc/32x32/cat.png diff --git a/application/resources/multimc/32x32/centralmods.png b/launcher/resources/multimc/32x32/centralmods.png similarity index 100% rename from application/resources/multimc/32x32/centralmods.png rename to launcher/resources/multimc/32x32/centralmods.png diff --git a/application/resources/multimc/32x32/checkupdate.png b/launcher/resources/multimc/32x32/checkupdate.png similarity index 100% rename from application/resources/multimc/32x32/checkupdate.png rename to launcher/resources/multimc/32x32/checkupdate.png diff --git a/application/resources/multimc/32x32/copy.png b/launcher/resources/multimc/32x32/copy.png similarity index 100% rename from application/resources/multimc/32x32/copy.png rename to launcher/resources/multimc/32x32/copy.png diff --git a/application/resources/multimc/32x32/coremods.png b/launcher/resources/multimc/32x32/coremods.png similarity index 100% rename from application/resources/multimc/32x32/coremods.png rename to launcher/resources/multimc/32x32/coremods.png diff --git a/application/resources/multimc/32x32/help.png b/launcher/resources/multimc/32x32/help.png similarity index 100% rename from application/resources/multimc/32x32/help.png rename to launcher/resources/multimc/32x32/help.png diff --git a/application/resources/multimc/32x32/instance-settings.png b/launcher/resources/multimc/32x32/instance-settings.png similarity index 100% rename from application/resources/multimc/32x32/instance-settings.png rename to launcher/resources/multimc/32x32/instance-settings.png diff --git a/launcher/resources/multimc/32x32/instances/brick.png b/launcher/resources/multimc/32x32/instances/brick.png new file mode 100644 index 0000000000..c324fda063 Binary files /dev/null and b/launcher/resources/multimc/32x32/instances/brick.png differ diff --git a/application/resources/multimc/32x32/instances/chicken.png b/launcher/resources/multimc/32x32/instances/chicken.png similarity index 100% rename from application/resources/multimc/32x32/instances/chicken.png rename to launcher/resources/multimc/32x32/instances/chicken.png diff --git a/application/resources/multimc/32x32/instances/creeper.png b/launcher/resources/multimc/32x32/instances/creeper.png similarity index 100% rename from application/resources/multimc/32x32/instances/creeper.png rename to launcher/resources/multimc/32x32/instances/creeper.png diff --git a/launcher/resources/multimc/32x32/instances/diamond.png b/launcher/resources/multimc/32x32/instances/diamond.png new file mode 100644 index 0000000000..1eb2646978 Binary files /dev/null and b/launcher/resources/multimc/32x32/instances/diamond.png differ diff --git a/application/resources/multimc/32x32/instances/dirt.png b/launcher/resources/multimc/32x32/instances/dirt.png similarity index 100% rename from application/resources/multimc/32x32/instances/dirt.png rename to launcher/resources/multimc/32x32/instances/dirt.png diff --git a/application/resources/multimc/32x32/instances/enderpearl.png b/launcher/resources/multimc/32x32/instances/enderpearl.png similarity index 100% rename from application/resources/multimc/32x32/instances/enderpearl.png rename to launcher/resources/multimc/32x32/instances/enderpearl.png diff --git a/application/resources/multimc/32x32/instances/flame.png b/launcher/resources/multimc/32x32/instances/flame.png similarity index 100% rename from application/resources/multimc/32x32/instances/flame.png rename to launcher/resources/multimc/32x32/instances/flame.png diff --git a/application/resources/multimc/32x32/instances/ftb_glow.png b/launcher/resources/multimc/32x32/instances/ftb_glow.png similarity index 100% rename from application/resources/multimc/32x32/instances/ftb_glow.png rename to launcher/resources/multimc/32x32/instances/ftb_glow.png diff --git a/application/resources/multimc/32x32/instances/ftb_logo.png b/launcher/resources/multimc/32x32/instances/ftb_logo.png similarity index 100% rename from application/resources/multimc/32x32/instances/ftb_logo.png rename to launcher/resources/multimc/32x32/instances/ftb_logo.png diff --git a/application/resources/multimc/32x32/instances/gear.png b/launcher/resources/multimc/32x32/instances/gear.png similarity index 100% rename from application/resources/multimc/32x32/instances/gear.png rename to launcher/resources/multimc/32x32/instances/gear.png diff --git a/launcher/resources/multimc/32x32/instances/gold.png b/launcher/resources/multimc/32x32/instances/gold.png new file mode 100644 index 0000000000..593410fac9 Binary files /dev/null and b/launcher/resources/multimc/32x32/instances/gold.png differ diff --git a/application/resources/multimc/32x32/instances/grass.png b/launcher/resources/multimc/32x32/instances/grass.png similarity index 100% rename from application/resources/multimc/32x32/instances/grass.png rename to launcher/resources/multimc/32x32/instances/grass.png diff --git a/application/resources/multimc/32x32/instances/herobrine.png b/launcher/resources/multimc/32x32/instances/herobrine.png similarity index 100% rename from application/resources/multimc/32x32/instances/herobrine.png rename to launcher/resources/multimc/32x32/instances/herobrine.png diff --git a/application/resources/multimc/32x32/instances/infinity.png b/launcher/resources/multimc/32x32/instances/infinity.png similarity index 100% rename from application/resources/multimc/32x32/instances/infinity.png rename to launcher/resources/multimc/32x32/instances/infinity.png diff --git a/launcher/resources/multimc/32x32/instances/iron.png b/launcher/resources/multimc/32x32/instances/iron.png new file mode 100644 index 0000000000..3e811bd63f Binary files /dev/null and b/launcher/resources/multimc/32x32/instances/iron.png differ diff --git a/application/resources/multimc/32x32/instances/magitech.png b/launcher/resources/multimc/32x32/instances/magitech.png similarity index 100% rename from application/resources/multimc/32x32/instances/magitech.png rename to launcher/resources/multimc/32x32/instances/magitech.png diff --git a/application/resources/multimc/32x32/instances/meat.png b/launcher/resources/multimc/32x32/instances/meat.png similarity index 100% rename from application/resources/multimc/32x32/instances/meat.png rename to launcher/resources/multimc/32x32/instances/meat.png diff --git a/application/resources/multimc/32x32/instances/netherstar.png b/launcher/resources/multimc/32x32/instances/netherstar.png similarity index 100% rename from application/resources/multimc/32x32/instances/netherstar.png rename to launcher/resources/multimc/32x32/instances/netherstar.png diff --git a/launcher/resources/multimc/32x32/instances/planks.png b/launcher/resources/multimc/32x32/instances/planks.png new file mode 100644 index 0000000000..a94b75029b Binary files /dev/null and b/launcher/resources/multimc/32x32/instances/planks.png differ diff --git a/application/resources/multimc/32x32/instances/skeleton.png b/launcher/resources/multimc/32x32/instances/skeleton.png similarity index 100% rename from application/resources/multimc/32x32/instances/skeleton.png rename to launcher/resources/multimc/32x32/instances/skeleton.png diff --git a/application/resources/multimc/32x32/instances/squarecreeper.png b/launcher/resources/multimc/32x32/instances/squarecreeper.png similarity index 100% rename from application/resources/multimc/32x32/instances/squarecreeper.png rename to launcher/resources/multimc/32x32/instances/squarecreeper.png diff --git a/application/resources/multimc/32x32/instances/steve.png b/launcher/resources/multimc/32x32/instances/steve.png similarity index 100% rename from application/resources/multimc/32x32/instances/steve.png rename to launcher/resources/multimc/32x32/instances/steve.png diff --git a/launcher/resources/multimc/32x32/instances/stone.png b/launcher/resources/multimc/32x32/instances/stone.png new file mode 100644 index 0000000000..1b6ef7a434 Binary files /dev/null and b/launcher/resources/multimc/32x32/instances/stone.png differ diff --git a/application/resources/multimc/32x32/instances/tnt.png b/launcher/resources/multimc/32x32/instances/tnt.png similarity index 100% rename from application/resources/multimc/32x32/instances/tnt.png rename to launcher/resources/multimc/32x32/instances/tnt.png diff --git a/application/resources/multimc/32x32/jarmods.png b/launcher/resources/multimc/32x32/jarmods.png similarity index 100% rename from application/resources/multimc/32x32/jarmods.png rename to launcher/resources/multimc/32x32/jarmods.png diff --git a/application/resources/multimc/32x32/loadermods.png b/launcher/resources/multimc/32x32/loadermods.png similarity index 100% rename from application/resources/multimc/32x32/loadermods.png rename to launcher/resources/multimc/32x32/loadermods.png diff --git a/application/resources/multimc/32x32/log.png b/launcher/resources/multimc/32x32/log.png similarity index 100% rename from application/resources/multimc/32x32/log.png rename to launcher/resources/multimc/32x32/log.png diff --git a/application/resources/multimc/32x32/minecraft.png b/launcher/resources/multimc/32x32/minecraft.png similarity index 100% rename from application/resources/multimc/32x32/minecraft.png rename to launcher/resources/multimc/32x32/minecraft.png diff --git a/application/resources/multimc/32x32/new.png b/launcher/resources/multimc/32x32/new.png similarity index 100% rename from application/resources/multimc/32x32/new.png rename to launcher/resources/multimc/32x32/new.png diff --git a/application/resources/multimc/32x32/news.png b/launcher/resources/multimc/32x32/news.png similarity index 100% rename from application/resources/multimc/32x32/news.png rename to launcher/resources/multimc/32x32/news.png diff --git a/application/resources/multimc/32x32/noaccount.png b/launcher/resources/multimc/32x32/noaccount.png similarity index 100% rename from application/resources/multimc/32x32/noaccount.png rename to launcher/resources/multimc/32x32/noaccount.png diff --git a/launcher/resources/multimc/32x32/patreon.png b/launcher/resources/multimc/32x32/patreon.png new file mode 100644 index 0000000000..70085aa1c2 Binary files /dev/null and b/launcher/resources/multimc/32x32/patreon.png differ diff --git a/application/resources/multimc/32x32/refresh.png b/launcher/resources/multimc/32x32/refresh.png similarity index 100% rename from application/resources/multimc/32x32/refresh.png rename to launcher/resources/multimc/32x32/refresh.png diff --git a/application/resources/multimc/32x32/resourcepacks.png b/launcher/resources/multimc/32x32/resourcepacks.png similarity index 100% rename from application/resources/multimc/32x32/resourcepacks.png rename to launcher/resources/multimc/32x32/resourcepacks.png diff --git a/application/resources/multimc/32x32/screenshots.png b/launcher/resources/multimc/32x32/screenshots.png similarity index 100% rename from application/resources/multimc/32x32/screenshots.png rename to launcher/resources/multimc/32x32/screenshots.png diff --git a/application/resources/multimc/32x32/settings.png b/launcher/resources/multimc/32x32/settings.png similarity index 100% rename from application/resources/multimc/32x32/settings.png rename to launcher/resources/multimc/32x32/settings.png diff --git a/application/resources/multimc/32x32/star.png b/launcher/resources/multimc/32x32/star.png similarity index 100% rename from application/resources/multimc/32x32/star.png rename to launcher/resources/multimc/32x32/star.png diff --git a/application/resources/multimc/32x32/status-bad.png b/launcher/resources/multimc/32x32/status-bad.png similarity index 100% rename from application/resources/multimc/32x32/status-bad.png rename to launcher/resources/multimc/32x32/status-bad.png diff --git a/application/resources/multimc/32x32/status-good.png b/launcher/resources/multimc/32x32/status-good.png similarity index 100% rename from application/resources/multimc/32x32/status-good.png rename to launcher/resources/multimc/32x32/status-good.png diff --git a/application/resources/multimc/32x32/status-running.png b/launcher/resources/multimc/32x32/status-running.png similarity index 100% rename from application/resources/multimc/32x32/status-running.png rename to launcher/resources/multimc/32x32/status-running.png diff --git a/application/resources/multimc/32x32/status-yellow.png b/launcher/resources/multimc/32x32/status-yellow.png similarity index 100% rename from application/resources/multimc/32x32/status-yellow.png rename to launcher/resources/multimc/32x32/status-yellow.png diff --git a/application/resources/multimc/32x32/viewfolder.png b/launcher/resources/multimc/32x32/viewfolder.png similarity index 100% rename from application/resources/multimc/32x32/viewfolder.png rename to launcher/resources/multimc/32x32/viewfolder.png diff --git a/application/resources/multimc/32x32/worlds.png b/launcher/resources/multimc/32x32/worlds.png similarity index 100% rename from application/resources/multimc/32x32/worlds.png rename to launcher/resources/multimc/32x32/worlds.png diff --git a/application/resources/multimc/48x48/about.png b/launcher/resources/multimc/48x48/about.png similarity index 100% rename from application/resources/multimc/48x48/about.png rename to launcher/resources/multimc/48x48/about.png diff --git a/application/resources/multimc/48x48/bug.png b/launcher/resources/multimc/48x48/bug.png similarity index 100% rename from application/resources/multimc/48x48/bug.png rename to launcher/resources/multimc/48x48/bug.png diff --git a/application/resources/multimc/48x48/cat.png b/launcher/resources/multimc/48x48/cat.png similarity index 100% rename from application/resources/multimc/48x48/cat.png rename to launcher/resources/multimc/48x48/cat.png diff --git a/application/resources/multimc/48x48/centralmods.png b/launcher/resources/multimc/48x48/centralmods.png similarity index 100% rename from application/resources/multimc/48x48/centralmods.png rename to launcher/resources/multimc/48x48/centralmods.png diff --git a/application/resources/multimc/48x48/checkupdate.png b/launcher/resources/multimc/48x48/checkupdate.png similarity index 100% rename from application/resources/multimc/48x48/checkupdate.png rename to launcher/resources/multimc/48x48/checkupdate.png diff --git a/application/resources/multimc/48x48/copy.png b/launcher/resources/multimc/48x48/copy.png similarity index 100% rename from application/resources/multimc/48x48/copy.png rename to launcher/resources/multimc/48x48/copy.png diff --git a/application/resources/multimc/48x48/help.png b/launcher/resources/multimc/48x48/help.png similarity index 100% rename from application/resources/multimc/48x48/help.png rename to launcher/resources/multimc/48x48/help.png diff --git a/application/resources/multimc/48x48/instance-settings.png b/launcher/resources/multimc/48x48/instance-settings.png similarity index 100% rename from application/resources/multimc/48x48/instance-settings.png rename to launcher/resources/multimc/48x48/instance-settings.png diff --git a/application/resources/multimc/48x48/log.png b/launcher/resources/multimc/48x48/log.png similarity index 100% rename from application/resources/multimc/48x48/log.png rename to launcher/resources/multimc/48x48/log.png diff --git a/application/resources/multimc/48x48/minecraft.png b/launcher/resources/multimc/48x48/minecraft.png similarity index 100% rename from application/resources/multimc/48x48/minecraft.png rename to launcher/resources/multimc/48x48/minecraft.png diff --git a/application/resources/multimc/48x48/new.png b/launcher/resources/multimc/48x48/new.png similarity index 100% rename from application/resources/multimc/48x48/new.png rename to launcher/resources/multimc/48x48/new.png diff --git a/application/resources/multimc/48x48/news.png b/launcher/resources/multimc/48x48/news.png similarity index 100% rename from application/resources/multimc/48x48/news.png rename to launcher/resources/multimc/48x48/news.png diff --git a/application/resources/multimc/48x48/noaccount.png b/launcher/resources/multimc/48x48/noaccount.png similarity index 100% rename from application/resources/multimc/48x48/noaccount.png rename to launcher/resources/multimc/48x48/noaccount.png diff --git a/launcher/resources/multimc/48x48/patreon.png b/launcher/resources/multimc/48x48/patreon.png new file mode 100644 index 0000000000..7aec4d7d36 Binary files /dev/null and b/launcher/resources/multimc/48x48/patreon.png differ diff --git a/application/resources/multimc/48x48/refresh.png b/launcher/resources/multimc/48x48/refresh.png similarity index 100% rename from application/resources/multimc/48x48/refresh.png rename to launcher/resources/multimc/48x48/refresh.png diff --git a/application/resources/multimc/48x48/screenshots.png b/launcher/resources/multimc/48x48/screenshots.png similarity index 100% rename from application/resources/multimc/48x48/screenshots.png rename to launcher/resources/multimc/48x48/screenshots.png diff --git a/application/resources/multimc/48x48/settings.png b/launcher/resources/multimc/48x48/settings.png similarity index 100% rename from application/resources/multimc/48x48/settings.png rename to launcher/resources/multimc/48x48/settings.png diff --git a/application/resources/multimc/48x48/star.png b/launcher/resources/multimc/48x48/star.png similarity index 100% rename from application/resources/multimc/48x48/star.png rename to launcher/resources/multimc/48x48/star.png diff --git a/application/resources/multimc/48x48/status-bad.png b/launcher/resources/multimc/48x48/status-bad.png similarity index 100% rename from application/resources/multimc/48x48/status-bad.png rename to launcher/resources/multimc/48x48/status-bad.png diff --git a/application/resources/multimc/48x48/status-good.png b/launcher/resources/multimc/48x48/status-good.png similarity index 100% rename from application/resources/multimc/48x48/status-good.png rename to launcher/resources/multimc/48x48/status-good.png diff --git a/application/resources/multimc/48x48/status-running.png b/launcher/resources/multimc/48x48/status-running.png similarity index 100% rename from application/resources/multimc/48x48/status-running.png rename to launcher/resources/multimc/48x48/status-running.png diff --git a/application/resources/multimc/48x48/status-yellow.png b/launcher/resources/multimc/48x48/status-yellow.png similarity index 100% rename from application/resources/multimc/48x48/status-yellow.png rename to launcher/resources/multimc/48x48/status-yellow.png diff --git a/application/resources/multimc/48x48/viewfolder.png b/launcher/resources/multimc/48x48/viewfolder.png similarity index 100% rename from application/resources/multimc/48x48/viewfolder.png rename to launcher/resources/multimc/48x48/viewfolder.png diff --git a/application/resources/multimc/48x48/worlds.png b/launcher/resources/multimc/48x48/worlds.png similarity index 100% rename from application/resources/multimc/48x48/worlds.png rename to launcher/resources/multimc/48x48/worlds.png diff --git a/application/resources/multimc/50x50/instances/enderman.png b/launcher/resources/multimc/50x50/instances/enderman.png similarity index 100% rename from application/resources/multimc/50x50/instances/enderman.png rename to launcher/resources/multimc/50x50/instances/enderman.png diff --git a/application/resources/multimc/64x64/about.png b/launcher/resources/multimc/64x64/about.png similarity index 100% rename from application/resources/multimc/64x64/about.png rename to launcher/resources/multimc/64x64/about.png diff --git a/application/resources/multimc/64x64/bug.png b/launcher/resources/multimc/64x64/bug.png similarity index 100% rename from application/resources/multimc/64x64/bug.png rename to launcher/resources/multimc/64x64/bug.png diff --git a/application/resources/multimc/64x64/cat.png b/launcher/resources/multimc/64x64/cat.png similarity index 100% rename from application/resources/multimc/64x64/cat.png rename to launcher/resources/multimc/64x64/cat.png diff --git a/application/resources/multimc/64x64/centralmods.png b/launcher/resources/multimc/64x64/centralmods.png similarity index 100% rename from application/resources/multimc/64x64/centralmods.png rename to launcher/resources/multimc/64x64/centralmods.png diff --git a/application/resources/multimc/64x64/checkupdate.png b/launcher/resources/multimc/64x64/checkupdate.png similarity index 100% rename from application/resources/multimc/64x64/checkupdate.png rename to launcher/resources/multimc/64x64/checkupdate.png diff --git a/application/resources/multimc/64x64/copy.png b/launcher/resources/multimc/64x64/copy.png similarity index 100% rename from application/resources/multimc/64x64/copy.png rename to launcher/resources/multimc/64x64/copy.png diff --git a/application/resources/multimc/64x64/coremods.png b/launcher/resources/multimc/64x64/coremods.png similarity index 100% rename from application/resources/multimc/64x64/coremods.png rename to launcher/resources/multimc/64x64/coremods.png diff --git a/application/resources/multimc/64x64/help.png b/launcher/resources/multimc/64x64/help.png similarity index 100% rename from application/resources/multimc/64x64/help.png rename to launcher/resources/multimc/64x64/help.png diff --git a/application/resources/multimc/64x64/instance-settings.png b/launcher/resources/multimc/64x64/instance-settings.png similarity index 100% rename from application/resources/multimc/64x64/instance-settings.png rename to launcher/resources/multimc/64x64/instance-settings.png diff --git a/application/resources/multimc/64x64/jarmods.png b/launcher/resources/multimc/64x64/jarmods.png similarity index 100% rename from application/resources/multimc/64x64/jarmods.png rename to launcher/resources/multimc/64x64/jarmods.png diff --git a/application/resources/multimc/64x64/loadermods.png b/launcher/resources/multimc/64x64/loadermods.png similarity index 100% rename from application/resources/multimc/64x64/loadermods.png rename to launcher/resources/multimc/64x64/loadermods.png diff --git a/application/resources/multimc/64x64/log.png b/launcher/resources/multimc/64x64/log.png similarity index 100% rename from application/resources/multimc/64x64/log.png rename to launcher/resources/multimc/64x64/log.png diff --git a/application/resources/multimc/64x64/new.png b/launcher/resources/multimc/64x64/new.png similarity index 100% rename from application/resources/multimc/64x64/new.png rename to launcher/resources/multimc/64x64/new.png diff --git a/application/resources/multimc/64x64/news.png b/launcher/resources/multimc/64x64/news.png similarity index 100% rename from application/resources/multimc/64x64/news.png rename to launcher/resources/multimc/64x64/news.png diff --git a/launcher/resources/multimc/64x64/patreon.png b/launcher/resources/multimc/64x64/patreon.png new file mode 100644 index 0000000000..ef5d690ebf Binary files /dev/null and b/launcher/resources/multimc/64x64/patreon.png differ diff --git a/application/resources/multimc/64x64/refresh.png b/launcher/resources/multimc/64x64/refresh.png similarity index 100% rename from application/resources/multimc/64x64/refresh.png rename to launcher/resources/multimc/64x64/refresh.png diff --git a/application/resources/multimc/64x64/resourcepacks.png b/launcher/resources/multimc/64x64/resourcepacks.png similarity index 100% rename from application/resources/multimc/64x64/resourcepacks.png rename to launcher/resources/multimc/64x64/resourcepacks.png diff --git a/application/resources/multimc/64x64/screenshots.png b/launcher/resources/multimc/64x64/screenshots.png similarity index 100% rename from application/resources/multimc/64x64/screenshots.png rename to launcher/resources/multimc/64x64/screenshots.png diff --git a/application/resources/multimc/64x64/settings.png b/launcher/resources/multimc/64x64/settings.png similarity index 100% rename from application/resources/multimc/64x64/settings.png rename to launcher/resources/multimc/64x64/settings.png diff --git a/application/resources/multimc/64x64/star.png b/launcher/resources/multimc/64x64/star.png similarity index 100% rename from application/resources/multimc/64x64/star.png rename to launcher/resources/multimc/64x64/star.png diff --git a/application/resources/multimc/64x64/status-bad.png b/launcher/resources/multimc/64x64/status-bad.png similarity index 100% rename from application/resources/multimc/64x64/status-bad.png rename to launcher/resources/multimc/64x64/status-bad.png diff --git a/application/resources/multimc/64x64/status-good.png b/launcher/resources/multimc/64x64/status-good.png similarity index 100% rename from application/resources/multimc/64x64/status-good.png rename to launcher/resources/multimc/64x64/status-good.png diff --git a/application/resources/multimc/64x64/status-running.png b/launcher/resources/multimc/64x64/status-running.png similarity index 100% rename from application/resources/multimc/64x64/status-running.png rename to launcher/resources/multimc/64x64/status-running.png diff --git a/application/resources/multimc/64x64/status-yellow.png b/launcher/resources/multimc/64x64/status-yellow.png similarity index 100% rename from application/resources/multimc/64x64/status-yellow.png rename to launcher/resources/multimc/64x64/status-yellow.png diff --git a/application/resources/multimc/64x64/viewfolder.png b/launcher/resources/multimc/64x64/viewfolder.png similarity index 100% rename from application/resources/multimc/64x64/viewfolder.png rename to launcher/resources/multimc/64x64/viewfolder.png diff --git a/application/resources/multimc/64x64/worlds.png b/launcher/resources/multimc/64x64/worlds.png similarity index 100% rename from application/resources/multimc/64x64/worlds.png rename to launcher/resources/multimc/64x64/worlds.png diff --git a/application/resources/multimc/8x8/noaccount.png b/launcher/resources/multimc/8x8/noaccount.png similarity index 100% rename from application/resources/multimc/8x8/noaccount.png rename to launcher/resources/multimc/8x8/noaccount.png diff --git a/application/resources/multimc/index.theme b/launcher/resources/multimc/index.theme similarity index 100% rename from application/resources/multimc/index.theme rename to launcher/resources/multimc/index.theme diff --git a/application/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc similarity index 98% rename from application/resources/multimc/multimc.qrc rename to launcher/resources/multimc/multimc.qrc index 4e95869ea6..249e8e2827 100644 --- a/application/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -11,8 +11,9 @@ scalable/reddit-alien.svg - - scalable/twitch.svg + + 32x32/instances/flame.png + 128x128/instances/flame.png scalable/technic.svg diff --git a/application/resources/multimc/scalable/atlauncher-placeholder.png b/launcher/resources/multimc/scalable/atlauncher-placeholder.png similarity index 100% rename from application/resources/multimc/scalable/atlauncher-placeholder.png rename to launcher/resources/multimc/scalable/atlauncher-placeholder.png diff --git a/application/resources/multimc/scalable/atlauncher.svg b/launcher/resources/multimc/scalable/atlauncher.svg similarity index 100% rename from application/resources/multimc/scalable/atlauncher.svg rename to launcher/resources/multimc/scalable/atlauncher.svg diff --git a/application/resources/multimc/scalable/bug.svg b/launcher/resources/multimc/scalable/bug.svg similarity index 100% rename from application/resources/multimc/scalable/bug.svg rename to launcher/resources/multimc/scalable/bug.svg diff --git a/application/resources/multimc/scalable/centralmods.svg b/launcher/resources/multimc/scalable/centralmods.svg similarity index 100% rename from application/resources/multimc/scalable/centralmods.svg rename to launcher/resources/multimc/scalable/centralmods.svg diff --git a/application/resources/multimc/scalable/checkupdate.svg b/launcher/resources/multimc/scalable/checkupdate.svg similarity index 100% rename from application/resources/multimc/scalable/checkupdate.svg rename to launcher/resources/multimc/scalable/checkupdate.svg diff --git a/application/resources/multimc/scalable/custom-commands.svg b/launcher/resources/multimc/scalable/custom-commands.svg similarity index 100% rename from application/resources/multimc/scalable/custom-commands.svg rename to launcher/resources/multimc/scalable/custom-commands.svg diff --git a/application/resources/multimc/scalable/discord.svg b/launcher/resources/multimc/scalable/discord.svg similarity index 100% rename from application/resources/multimc/scalable/discord.svg rename to launcher/resources/multimc/scalable/discord.svg diff --git a/application/resources/multimc/scalable/instances/bee.svg b/launcher/resources/multimc/scalable/instances/bee.svg similarity index 100% rename from application/resources/multimc/scalable/instances/bee.svg rename to launcher/resources/multimc/scalable/instances/bee.svg diff --git a/application/resources/multimc/scalable/instances/fox.svg b/launcher/resources/multimc/scalable/instances/fox.svg similarity index 100% rename from application/resources/multimc/scalable/instances/fox.svg rename to launcher/resources/multimc/scalable/instances/fox.svg diff --git a/application/resources/multimc/scalable/java.svg b/launcher/resources/multimc/scalable/java.svg similarity index 100% rename from application/resources/multimc/scalable/java.svg rename to launcher/resources/multimc/scalable/java.svg diff --git a/application/resources/multimc/scalable/language.svg b/launcher/resources/multimc/scalable/language.svg similarity index 100% rename from application/resources/multimc/scalable/language.svg rename to launcher/resources/multimc/scalable/language.svg diff --git a/application/resources/multimc/scalable/logo.svg b/launcher/resources/multimc/scalable/logo.svg similarity index 100% rename from application/resources/multimc/scalable/logo.svg rename to launcher/resources/multimc/scalable/logo.svg diff --git a/application/resources/multimc/scalable/multimc.svg b/launcher/resources/multimc/scalable/multimc.svg similarity index 100% rename from application/resources/multimc/scalable/multimc.svg rename to launcher/resources/multimc/scalable/multimc.svg diff --git a/application/resources/multimc/scalable/new.svg b/launcher/resources/multimc/scalable/new.svg similarity index 100% rename from application/resources/multimc/scalable/new.svg rename to launcher/resources/multimc/scalable/new.svg diff --git a/application/resources/multimc/scalable/news.svg b/launcher/resources/multimc/scalable/news.svg similarity index 100% rename from application/resources/multimc/scalable/news.svg rename to launcher/resources/multimc/scalable/news.svg diff --git a/application/resources/multimc/scalable/proxy.svg b/launcher/resources/multimc/scalable/proxy.svg similarity index 100% rename from application/resources/multimc/scalable/proxy.svg rename to launcher/resources/multimc/scalable/proxy.svg diff --git a/application/resources/multimc/scalable/reddit-alien.svg b/launcher/resources/multimc/scalable/reddit-alien.svg similarity index 100% rename from application/resources/multimc/scalable/reddit-alien.svg rename to launcher/resources/multimc/scalable/reddit-alien.svg diff --git a/application/resources/multimc/scalable/screenshot-placeholder.svg b/launcher/resources/multimc/scalable/screenshot-placeholder.svg similarity index 100% rename from application/resources/multimc/scalable/screenshot-placeholder.svg rename to launcher/resources/multimc/scalable/screenshot-placeholder.svg diff --git a/application/resources/multimc/scalable/screenshots.svg b/launcher/resources/multimc/scalable/screenshots.svg similarity index 100% rename from application/resources/multimc/scalable/screenshots.svg rename to launcher/resources/multimc/scalable/screenshots.svg diff --git a/application/resources/multimc/scalable/status-bad.svg b/launcher/resources/multimc/scalable/status-bad.svg similarity index 100% rename from application/resources/multimc/scalable/status-bad.svg rename to launcher/resources/multimc/scalable/status-bad.svg diff --git a/application/resources/multimc/scalable/status-good.svg b/launcher/resources/multimc/scalable/status-good.svg similarity index 100% rename from application/resources/multimc/scalable/status-good.svg rename to launcher/resources/multimc/scalable/status-good.svg diff --git a/application/resources/multimc/scalable/status-running.svg b/launcher/resources/multimc/scalable/status-running.svg similarity index 100% rename from application/resources/multimc/scalable/status-running.svg rename to launcher/resources/multimc/scalable/status-running.svg diff --git a/application/resources/multimc/scalable/status-yellow.svg b/launcher/resources/multimc/scalable/status-yellow.svg similarity index 100% rename from application/resources/multimc/scalable/status-yellow.svg rename to launcher/resources/multimc/scalable/status-yellow.svg diff --git a/application/resources/multimc/scalable/technic.svg b/launcher/resources/multimc/scalable/technic.svg similarity index 100% rename from application/resources/multimc/scalable/technic.svg rename to launcher/resources/multimc/scalable/technic.svg diff --git a/application/resources/multimc/scalable/viewfolder.svg b/launcher/resources/multimc/scalable/viewfolder.svg similarity index 100% rename from application/resources/multimc/scalable/viewfolder.svg rename to launcher/resources/multimc/scalable/viewfolder.svg diff --git a/application/resources/pe_blue/index.theme b/launcher/resources/pe_blue/index.theme similarity index 100% rename from application/resources/pe_blue/index.theme rename to launcher/resources/pe_blue/index.theme diff --git a/application/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc similarity index 96% rename from application/resources/pe_blue/pe_blue.qrc rename to launcher/resources/pe_blue/pe_blue.qrc index 98445d885a..2a68597929 100644 --- a/application/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/application/resources/pe_blue/scalable/about.svg b/launcher/resources/pe_blue/scalable/about.svg similarity index 100% rename from application/resources/pe_blue/scalable/about.svg rename to launcher/resources/pe_blue/scalable/about.svg diff --git a/application/resources/pe_blue/scalable/accounts.svg b/launcher/resources/pe_blue/scalable/accounts.svg similarity index 100% rename from application/resources/pe_blue/scalable/accounts.svg rename to launcher/resources/pe_blue/scalable/accounts.svg diff --git a/application/resources/pe_blue/scalable/bug.svg b/launcher/resources/pe_blue/scalable/bug.svg similarity index 100% rename from application/resources/pe_blue/scalable/bug.svg rename to launcher/resources/pe_blue/scalable/bug.svg diff --git a/application/resources/pe_blue/scalable/centralmods.svg b/launcher/resources/pe_blue/scalable/centralmods.svg similarity index 100% rename from application/resources/pe_blue/scalable/centralmods.svg rename to launcher/resources/pe_blue/scalable/centralmods.svg diff --git a/application/resources/pe_blue/scalable/checkupdate.svg b/launcher/resources/pe_blue/scalable/checkupdate.svg similarity index 100% rename from application/resources/pe_blue/scalable/checkupdate.svg rename to launcher/resources/pe_blue/scalable/checkupdate.svg diff --git a/application/resources/pe_blue/scalable/copy.svg b/launcher/resources/pe_blue/scalable/copy.svg similarity index 100% rename from application/resources/pe_blue/scalable/copy.svg rename to launcher/resources/pe_blue/scalable/copy.svg diff --git a/application/resources/pe_blue/scalable/coremods.svg b/launcher/resources/pe_blue/scalable/coremods.svg similarity index 100% rename from application/resources/pe_blue/scalable/coremods.svg rename to launcher/resources/pe_blue/scalable/coremods.svg diff --git a/launcher/resources/pe_blue/scalable/custom-commands.svg b/launcher/resources/pe_blue/scalable/custom-commands.svg new file mode 100644 index 0000000000..be76ece94d --- /dev/null +++ b/launcher/resources/pe_blue/scalable/custom-commands.svg @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/application/resources/pe_blue/scalable/externaltools.svg b/launcher/resources/pe_blue/scalable/externaltools.svg similarity index 100% rename from application/resources/pe_blue/scalable/externaltools.svg rename to launcher/resources/pe_blue/scalable/externaltools.svg diff --git a/application/resources/pe_blue/scalable/help.svg b/launcher/resources/pe_blue/scalable/help.svg similarity index 100% rename from application/resources/pe_blue/scalable/help.svg rename to launcher/resources/pe_blue/scalable/help.svg diff --git a/application/resources/pe_blue/scalable/instance-settings.svg b/launcher/resources/pe_blue/scalable/instance-settings.svg similarity index 100% rename from application/resources/pe_blue/scalable/instance-settings.svg rename to launcher/resources/pe_blue/scalable/instance-settings.svg diff --git a/application/resources/pe_blue/scalable/jarmods.svg b/launcher/resources/pe_blue/scalable/jarmods.svg similarity index 100% rename from application/resources/pe_blue/scalable/jarmods.svg rename to launcher/resources/pe_blue/scalable/jarmods.svg diff --git a/application/resources/pe_blue/scalable/java.svg b/launcher/resources/pe_blue/scalable/java.svg similarity index 100% rename from application/resources/pe_blue/scalable/java.svg rename to launcher/resources/pe_blue/scalable/java.svg diff --git a/application/resources/pe_blue/scalable/language.svg b/launcher/resources/pe_blue/scalable/language.svg similarity index 100% rename from application/resources/pe_blue/scalable/language.svg rename to launcher/resources/pe_blue/scalable/language.svg diff --git a/application/resources/pe_blue/scalable/loadermods.svg b/launcher/resources/pe_blue/scalable/loadermods.svg similarity index 100% rename from application/resources/pe_blue/scalable/loadermods.svg rename to launcher/resources/pe_blue/scalable/loadermods.svg diff --git a/application/resources/pe_blue/scalable/log.svg b/launcher/resources/pe_blue/scalable/log.svg similarity index 100% rename from application/resources/pe_blue/scalable/log.svg rename to launcher/resources/pe_blue/scalable/log.svg diff --git a/application/resources/pe_blue/scalable/minecraft.svg b/launcher/resources/pe_blue/scalable/minecraft.svg similarity index 100% rename from application/resources/pe_blue/scalable/minecraft.svg rename to launcher/resources/pe_blue/scalable/minecraft.svg diff --git a/application/resources/pe_blue/scalable/multimc.svg b/launcher/resources/pe_blue/scalable/multimc.svg similarity index 100% rename from application/resources/pe_blue/scalable/multimc.svg rename to launcher/resources/pe_blue/scalable/multimc.svg diff --git a/application/resources/pe_blue/scalable/new.svg b/launcher/resources/pe_blue/scalable/new.svg similarity index 100% rename from application/resources/pe_blue/scalable/new.svg rename to launcher/resources/pe_blue/scalable/new.svg diff --git a/application/resources/pe_blue/scalable/news.svg b/launcher/resources/pe_blue/scalable/news.svg similarity index 100% rename from application/resources/pe_blue/scalable/news.svg rename to launcher/resources/pe_blue/scalable/news.svg diff --git a/application/resources/pe_blue/scalable/notes.svg b/launcher/resources/pe_blue/scalable/notes.svg similarity index 100% rename from application/resources/pe_blue/scalable/notes.svg rename to launcher/resources/pe_blue/scalable/notes.svg diff --git a/application/resources/pe_blue/scalable/patreon.svg b/launcher/resources/pe_blue/scalable/patreon.svg similarity index 100% rename from application/resources/pe_blue/scalable/patreon.svg rename to launcher/resources/pe_blue/scalable/patreon.svg diff --git a/application/resources/pe_blue/scalable/proxy.svg b/launcher/resources/pe_blue/scalable/proxy.svg similarity index 100% rename from application/resources/pe_blue/scalable/proxy.svg rename to launcher/resources/pe_blue/scalable/proxy.svg diff --git a/application/resources/pe_blue/scalable/quickmods.svg b/launcher/resources/pe_blue/scalable/quickmods.svg similarity index 100% rename from application/resources/pe_blue/scalable/quickmods.svg rename to launcher/resources/pe_blue/scalable/quickmods.svg diff --git a/application/resources/pe_blue/scalable/refresh.svg b/launcher/resources/pe_blue/scalable/refresh.svg similarity index 100% rename from application/resources/pe_blue/scalable/refresh.svg rename to launcher/resources/pe_blue/scalable/refresh.svg diff --git a/application/resources/pe_blue/scalable/resourcepacks.svg b/launcher/resources/pe_blue/scalable/resourcepacks.svg similarity index 100% rename from application/resources/pe_blue/scalable/resourcepacks.svg rename to launcher/resources/pe_blue/scalable/resourcepacks.svg diff --git a/application/resources/pe_blue/scalable/screenshots.svg b/launcher/resources/pe_blue/scalable/screenshots.svg similarity index 100% rename from application/resources/pe_blue/scalable/screenshots.svg rename to launcher/resources/pe_blue/scalable/screenshots.svg diff --git a/application/resources/pe_blue/scalable/settings.svg b/launcher/resources/pe_blue/scalable/settings.svg similarity index 100% rename from application/resources/pe_blue/scalable/settings.svg rename to launcher/resources/pe_blue/scalable/settings.svg diff --git a/application/resources/pe_blue/scalable/status-bad.svg b/launcher/resources/pe_blue/scalable/status-bad.svg similarity index 100% rename from application/resources/pe_blue/scalable/status-bad.svg rename to launcher/resources/pe_blue/scalable/status-bad.svg diff --git a/application/resources/pe_blue/scalable/status-good.svg b/launcher/resources/pe_blue/scalable/status-good.svg similarity index 100% rename from application/resources/pe_blue/scalable/status-good.svg rename to launcher/resources/pe_blue/scalable/status-good.svg diff --git a/application/resources/pe_blue/scalable/status-yellow.svg b/launcher/resources/pe_blue/scalable/status-yellow.svg similarity index 100% rename from application/resources/pe_blue/scalable/status-yellow.svg rename to launcher/resources/pe_blue/scalable/status-yellow.svg diff --git a/application/resources/pe_blue/scalable/viewfolder.svg b/launcher/resources/pe_blue/scalable/viewfolder.svg similarity index 100% rename from application/resources/pe_blue/scalable/viewfolder.svg rename to launcher/resources/pe_blue/scalable/viewfolder.svg diff --git a/application/resources/pe_blue/scalable/worlds.svg b/launcher/resources/pe_blue/scalable/worlds.svg similarity index 100% rename from application/resources/pe_blue/scalable/worlds.svg rename to launcher/resources/pe_blue/scalable/worlds.svg diff --git a/application/resources/pe_colored/index.theme b/launcher/resources/pe_colored/index.theme similarity index 100% rename from application/resources/pe_colored/index.theme rename to launcher/resources/pe_colored/index.theme diff --git a/application/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc similarity index 96% rename from application/resources/pe_colored/pe_colored.qrc rename to launcher/resources/pe_colored/pe_colored.qrc index fbaaf9e4c9..4472f5e01b 100644 --- a/application/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/application/resources/pe_colored/scalable/about.svg b/launcher/resources/pe_colored/scalable/about.svg similarity index 100% rename from application/resources/pe_colored/scalable/about.svg rename to launcher/resources/pe_colored/scalable/about.svg diff --git a/application/resources/pe_colored/scalable/accounts.svg b/launcher/resources/pe_colored/scalable/accounts.svg similarity index 100% rename from application/resources/pe_colored/scalable/accounts.svg rename to launcher/resources/pe_colored/scalable/accounts.svg diff --git a/application/resources/pe_colored/scalable/bug.svg b/launcher/resources/pe_colored/scalable/bug.svg similarity index 100% rename from application/resources/pe_colored/scalable/bug.svg rename to launcher/resources/pe_colored/scalable/bug.svg diff --git a/application/resources/pe_colored/scalable/centralmods.svg b/launcher/resources/pe_colored/scalable/centralmods.svg similarity index 100% rename from application/resources/pe_colored/scalable/centralmods.svg rename to launcher/resources/pe_colored/scalable/centralmods.svg diff --git a/application/resources/pe_colored/scalable/checkupdate.svg b/launcher/resources/pe_colored/scalable/checkupdate.svg similarity index 100% rename from application/resources/pe_colored/scalable/checkupdate.svg rename to launcher/resources/pe_colored/scalable/checkupdate.svg diff --git a/application/resources/pe_colored/scalable/copy.svg b/launcher/resources/pe_colored/scalable/copy.svg similarity index 100% rename from application/resources/pe_colored/scalable/copy.svg rename to launcher/resources/pe_colored/scalable/copy.svg diff --git a/application/resources/pe_colored/scalable/coremods.svg b/launcher/resources/pe_colored/scalable/coremods.svg similarity index 100% rename from application/resources/pe_colored/scalable/coremods.svg rename to launcher/resources/pe_colored/scalable/coremods.svg diff --git a/launcher/resources/pe_colored/scalable/custom-commands.svg b/launcher/resources/pe_colored/scalable/custom-commands.svg new file mode 100644 index 0000000000..44dd199232 --- /dev/null +++ b/launcher/resources/pe_colored/scalable/custom-commands.svg @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/application/resources/pe_colored/scalable/externaltools.svg b/launcher/resources/pe_colored/scalable/externaltools.svg similarity index 100% rename from application/resources/pe_colored/scalable/externaltools.svg rename to launcher/resources/pe_colored/scalable/externaltools.svg diff --git a/application/resources/pe_colored/scalable/help.svg b/launcher/resources/pe_colored/scalable/help.svg similarity index 100% rename from application/resources/pe_colored/scalable/help.svg rename to launcher/resources/pe_colored/scalable/help.svg diff --git a/application/resources/pe_colored/scalable/instance-settings.svg b/launcher/resources/pe_colored/scalable/instance-settings.svg similarity index 100% rename from application/resources/pe_colored/scalable/instance-settings.svg rename to launcher/resources/pe_colored/scalable/instance-settings.svg diff --git a/application/resources/pe_colored/scalable/jarmods.svg b/launcher/resources/pe_colored/scalable/jarmods.svg similarity index 100% rename from application/resources/pe_colored/scalable/jarmods.svg rename to launcher/resources/pe_colored/scalable/jarmods.svg diff --git a/application/resources/pe_colored/scalable/java.svg b/launcher/resources/pe_colored/scalable/java.svg similarity index 100% rename from application/resources/pe_colored/scalable/java.svg rename to launcher/resources/pe_colored/scalable/java.svg diff --git a/application/resources/pe_colored/scalable/language.svg b/launcher/resources/pe_colored/scalable/language.svg similarity index 100% rename from application/resources/pe_colored/scalable/language.svg rename to launcher/resources/pe_colored/scalable/language.svg diff --git a/application/resources/pe_colored/scalable/loadermods.svg b/launcher/resources/pe_colored/scalable/loadermods.svg similarity index 100% rename from application/resources/pe_colored/scalable/loadermods.svg rename to launcher/resources/pe_colored/scalable/loadermods.svg diff --git a/application/resources/pe_colored/scalable/log.svg b/launcher/resources/pe_colored/scalable/log.svg similarity index 100% rename from application/resources/pe_colored/scalable/log.svg rename to launcher/resources/pe_colored/scalable/log.svg diff --git a/application/resources/pe_colored/scalable/minecraft.svg b/launcher/resources/pe_colored/scalable/minecraft.svg similarity index 100% rename from application/resources/pe_colored/scalable/minecraft.svg rename to launcher/resources/pe_colored/scalable/minecraft.svg diff --git a/application/resources/pe_colored/scalable/multimc.svg b/launcher/resources/pe_colored/scalable/multimc.svg similarity index 100% rename from application/resources/pe_colored/scalable/multimc.svg rename to launcher/resources/pe_colored/scalable/multimc.svg diff --git a/application/resources/pe_colored/scalable/new.svg b/launcher/resources/pe_colored/scalable/new.svg similarity index 100% rename from application/resources/pe_colored/scalable/new.svg rename to launcher/resources/pe_colored/scalable/new.svg diff --git a/application/resources/pe_colored/scalable/news.svg b/launcher/resources/pe_colored/scalable/news.svg similarity index 100% rename from application/resources/pe_colored/scalable/news.svg rename to launcher/resources/pe_colored/scalable/news.svg diff --git a/application/resources/pe_colored/scalable/notes.svg b/launcher/resources/pe_colored/scalable/notes.svg similarity index 100% rename from application/resources/pe_colored/scalable/notes.svg rename to launcher/resources/pe_colored/scalable/notes.svg diff --git a/application/resources/pe_colored/scalable/patreon.svg b/launcher/resources/pe_colored/scalable/patreon.svg similarity index 100% rename from application/resources/pe_colored/scalable/patreon.svg rename to launcher/resources/pe_colored/scalable/patreon.svg diff --git a/application/resources/pe_colored/scalable/proxy.svg b/launcher/resources/pe_colored/scalable/proxy.svg similarity index 100% rename from application/resources/pe_colored/scalable/proxy.svg rename to launcher/resources/pe_colored/scalable/proxy.svg diff --git a/application/resources/pe_colored/scalable/quickmods.svg b/launcher/resources/pe_colored/scalable/quickmods.svg similarity index 100% rename from application/resources/pe_colored/scalable/quickmods.svg rename to launcher/resources/pe_colored/scalable/quickmods.svg diff --git a/application/resources/pe_colored/scalable/refresh.svg b/launcher/resources/pe_colored/scalable/refresh.svg similarity index 100% rename from application/resources/pe_colored/scalable/refresh.svg rename to launcher/resources/pe_colored/scalable/refresh.svg diff --git a/application/resources/pe_colored/scalable/resourcepacks.svg b/launcher/resources/pe_colored/scalable/resourcepacks.svg similarity index 100% rename from application/resources/pe_colored/scalable/resourcepacks.svg rename to launcher/resources/pe_colored/scalable/resourcepacks.svg diff --git a/application/resources/pe_colored/scalable/screenshots.svg b/launcher/resources/pe_colored/scalable/screenshots.svg similarity index 100% rename from application/resources/pe_colored/scalable/screenshots.svg rename to launcher/resources/pe_colored/scalable/screenshots.svg diff --git a/application/resources/pe_colored/scalable/settings.svg b/launcher/resources/pe_colored/scalable/settings.svg similarity index 100% rename from application/resources/pe_colored/scalable/settings.svg rename to launcher/resources/pe_colored/scalable/settings.svg diff --git a/application/resources/pe_colored/scalable/status-bad.svg b/launcher/resources/pe_colored/scalable/status-bad.svg similarity index 100% rename from application/resources/pe_colored/scalable/status-bad.svg rename to launcher/resources/pe_colored/scalable/status-bad.svg diff --git a/application/resources/pe_colored/scalable/status-good.svg b/launcher/resources/pe_colored/scalable/status-good.svg similarity index 100% rename from application/resources/pe_colored/scalable/status-good.svg rename to launcher/resources/pe_colored/scalable/status-good.svg diff --git a/application/resources/pe_colored/scalable/status-yellow.svg b/launcher/resources/pe_colored/scalable/status-yellow.svg similarity index 100% rename from application/resources/pe_colored/scalable/status-yellow.svg rename to launcher/resources/pe_colored/scalable/status-yellow.svg diff --git a/application/resources/pe_colored/scalable/viewfolder.svg b/launcher/resources/pe_colored/scalable/viewfolder.svg similarity index 100% rename from application/resources/pe_colored/scalable/viewfolder.svg rename to launcher/resources/pe_colored/scalable/viewfolder.svg diff --git a/application/resources/pe_colored/scalable/worlds.svg b/launcher/resources/pe_colored/scalable/worlds.svg similarity index 100% rename from application/resources/pe_colored/scalable/worlds.svg rename to launcher/resources/pe_colored/scalable/worlds.svg diff --git a/application/resources/pe_dark/index.theme b/launcher/resources/pe_dark/index.theme similarity index 100% rename from application/resources/pe_dark/index.theme rename to launcher/resources/pe_dark/index.theme diff --git a/application/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc similarity index 96% rename from application/resources/pe_dark/pe_dark.qrc rename to launcher/resources/pe_dark/pe_dark.qrc index a57b6a1457..a138abc4d6 100644 --- a/application/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/application/resources/pe_dark/scalable/about.svg b/launcher/resources/pe_dark/scalable/about.svg similarity index 100% rename from application/resources/pe_dark/scalable/about.svg rename to launcher/resources/pe_dark/scalable/about.svg diff --git a/application/resources/pe_dark/scalable/accounts.svg b/launcher/resources/pe_dark/scalable/accounts.svg similarity index 100% rename from application/resources/pe_dark/scalable/accounts.svg rename to launcher/resources/pe_dark/scalable/accounts.svg diff --git a/application/resources/pe_dark/scalable/bug.svg b/launcher/resources/pe_dark/scalable/bug.svg similarity index 100% rename from application/resources/pe_dark/scalable/bug.svg rename to launcher/resources/pe_dark/scalable/bug.svg diff --git a/application/resources/pe_dark/scalable/centralmods.svg b/launcher/resources/pe_dark/scalable/centralmods.svg similarity index 100% rename from application/resources/pe_dark/scalable/centralmods.svg rename to launcher/resources/pe_dark/scalable/centralmods.svg diff --git a/application/resources/pe_dark/scalable/checkupdate.svg b/launcher/resources/pe_dark/scalable/checkupdate.svg similarity index 100% rename from application/resources/pe_dark/scalable/checkupdate.svg rename to launcher/resources/pe_dark/scalable/checkupdate.svg diff --git a/application/resources/pe_dark/scalable/copy.svg b/launcher/resources/pe_dark/scalable/copy.svg similarity index 100% rename from application/resources/pe_dark/scalable/copy.svg rename to launcher/resources/pe_dark/scalable/copy.svg diff --git a/application/resources/pe_dark/scalable/coremods.svg b/launcher/resources/pe_dark/scalable/coremods.svg similarity index 100% rename from application/resources/pe_dark/scalable/coremods.svg rename to launcher/resources/pe_dark/scalable/coremods.svg diff --git a/launcher/resources/pe_dark/scalable/custom-commands.svg b/launcher/resources/pe_dark/scalable/custom-commands.svg new file mode 100644 index 0000000000..42185e378c --- /dev/null +++ b/launcher/resources/pe_dark/scalable/custom-commands.svg @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/application/resources/pe_dark/scalable/externaltools.svg b/launcher/resources/pe_dark/scalable/externaltools.svg similarity index 100% rename from application/resources/pe_dark/scalable/externaltools.svg rename to launcher/resources/pe_dark/scalable/externaltools.svg diff --git a/application/resources/pe_dark/scalable/help.svg b/launcher/resources/pe_dark/scalable/help.svg similarity index 100% rename from application/resources/pe_dark/scalable/help.svg rename to launcher/resources/pe_dark/scalable/help.svg diff --git a/application/resources/pe_dark/scalable/instance-settings.svg b/launcher/resources/pe_dark/scalable/instance-settings.svg similarity index 100% rename from application/resources/pe_dark/scalable/instance-settings.svg rename to launcher/resources/pe_dark/scalable/instance-settings.svg diff --git a/application/resources/pe_dark/scalable/jarmods.svg b/launcher/resources/pe_dark/scalable/jarmods.svg similarity index 100% rename from application/resources/pe_dark/scalable/jarmods.svg rename to launcher/resources/pe_dark/scalable/jarmods.svg diff --git a/application/resources/pe_dark/scalable/java.svg b/launcher/resources/pe_dark/scalable/java.svg similarity index 100% rename from application/resources/pe_dark/scalable/java.svg rename to launcher/resources/pe_dark/scalable/java.svg diff --git a/application/resources/pe_dark/scalable/language.svg b/launcher/resources/pe_dark/scalable/language.svg similarity index 100% rename from application/resources/pe_dark/scalable/language.svg rename to launcher/resources/pe_dark/scalable/language.svg diff --git a/application/resources/pe_dark/scalable/loadermods.svg b/launcher/resources/pe_dark/scalable/loadermods.svg similarity index 100% rename from application/resources/pe_dark/scalable/loadermods.svg rename to launcher/resources/pe_dark/scalable/loadermods.svg diff --git a/application/resources/pe_dark/scalable/log.svg b/launcher/resources/pe_dark/scalable/log.svg similarity index 100% rename from application/resources/pe_dark/scalable/log.svg rename to launcher/resources/pe_dark/scalable/log.svg diff --git a/application/resources/pe_dark/scalable/minecraft.svg b/launcher/resources/pe_dark/scalable/minecraft.svg similarity index 100% rename from application/resources/pe_dark/scalable/minecraft.svg rename to launcher/resources/pe_dark/scalable/minecraft.svg diff --git a/application/resources/pe_dark/scalable/multimc.svg b/launcher/resources/pe_dark/scalable/multimc.svg similarity index 100% rename from application/resources/pe_dark/scalable/multimc.svg rename to launcher/resources/pe_dark/scalable/multimc.svg diff --git a/application/resources/pe_dark/scalable/new.svg b/launcher/resources/pe_dark/scalable/new.svg similarity index 100% rename from application/resources/pe_dark/scalable/new.svg rename to launcher/resources/pe_dark/scalable/new.svg diff --git a/application/resources/pe_dark/scalable/news.svg b/launcher/resources/pe_dark/scalable/news.svg similarity index 100% rename from application/resources/pe_dark/scalable/news.svg rename to launcher/resources/pe_dark/scalable/news.svg diff --git a/application/resources/pe_dark/scalable/notes.svg b/launcher/resources/pe_dark/scalable/notes.svg similarity index 100% rename from application/resources/pe_dark/scalable/notes.svg rename to launcher/resources/pe_dark/scalable/notes.svg diff --git a/application/resources/pe_dark/scalable/patreon.svg b/launcher/resources/pe_dark/scalable/patreon.svg similarity index 100% rename from application/resources/pe_dark/scalable/patreon.svg rename to launcher/resources/pe_dark/scalable/patreon.svg diff --git a/application/resources/pe_dark/scalable/proxy.svg b/launcher/resources/pe_dark/scalable/proxy.svg similarity index 100% rename from application/resources/pe_dark/scalable/proxy.svg rename to launcher/resources/pe_dark/scalable/proxy.svg diff --git a/application/resources/pe_dark/scalable/quickmods.svg b/launcher/resources/pe_dark/scalable/quickmods.svg similarity index 100% rename from application/resources/pe_dark/scalable/quickmods.svg rename to launcher/resources/pe_dark/scalable/quickmods.svg diff --git a/application/resources/pe_dark/scalable/refresh.svg b/launcher/resources/pe_dark/scalable/refresh.svg similarity index 100% rename from application/resources/pe_dark/scalable/refresh.svg rename to launcher/resources/pe_dark/scalable/refresh.svg diff --git a/application/resources/pe_dark/scalable/resourcepacks.svg b/launcher/resources/pe_dark/scalable/resourcepacks.svg similarity index 100% rename from application/resources/pe_dark/scalable/resourcepacks.svg rename to launcher/resources/pe_dark/scalable/resourcepacks.svg diff --git a/application/resources/pe_dark/scalable/screenshots.svg b/launcher/resources/pe_dark/scalable/screenshots.svg similarity index 100% rename from application/resources/pe_dark/scalable/screenshots.svg rename to launcher/resources/pe_dark/scalable/screenshots.svg diff --git a/application/resources/pe_dark/scalable/settings.svg b/launcher/resources/pe_dark/scalable/settings.svg similarity index 100% rename from application/resources/pe_dark/scalable/settings.svg rename to launcher/resources/pe_dark/scalable/settings.svg diff --git a/application/resources/pe_dark/scalable/status-bad.svg b/launcher/resources/pe_dark/scalable/status-bad.svg similarity index 100% rename from application/resources/pe_dark/scalable/status-bad.svg rename to launcher/resources/pe_dark/scalable/status-bad.svg diff --git a/application/resources/pe_dark/scalable/status-good.svg b/launcher/resources/pe_dark/scalable/status-good.svg similarity index 100% rename from application/resources/pe_dark/scalable/status-good.svg rename to launcher/resources/pe_dark/scalable/status-good.svg diff --git a/application/resources/pe_dark/scalable/status-yellow.svg b/launcher/resources/pe_dark/scalable/status-yellow.svg similarity index 100% rename from application/resources/pe_dark/scalable/status-yellow.svg rename to launcher/resources/pe_dark/scalable/status-yellow.svg diff --git a/application/resources/pe_dark/scalable/viewfolder.svg b/launcher/resources/pe_dark/scalable/viewfolder.svg similarity index 100% rename from application/resources/pe_dark/scalable/viewfolder.svg rename to launcher/resources/pe_dark/scalable/viewfolder.svg diff --git a/application/resources/pe_dark/scalable/worlds.svg b/launcher/resources/pe_dark/scalable/worlds.svg similarity index 100% rename from application/resources/pe_dark/scalable/worlds.svg rename to launcher/resources/pe_dark/scalable/worlds.svg diff --git a/application/resources/pe_light/index.theme b/launcher/resources/pe_light/index.theme similarity index 100% rename from application/resources/pe_light/index.theme rename to launcher/resources/pe_light/index.theme diff --git a/application/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc similarity index 96% rename from application/resources/pe_light/pe_light.qrc rename to launcher/resources/pe_light/pe_light.qrc index 6d77c835d0..f518b65003 100644 --- a/application/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -9,6 +9,7 @@ scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg + scalable/custom-commands.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/application/resources/pe_light/scalable/about.svg b/launcher/resources/pe_light/scalable/about.svg similarity index 100% rename from application/resources/pe_light/scalable/about.svg rename to launcher/resources/pe_light/scalable/about.svg diff --git a/application/resources/pe_light/scalable/accounts.svg b/launcher/resources/pe_light/scalable/accounts.svg similarity index 100% rename from application/resources/pe_light/scalable/accounts.svg rename to launcher/resources/pe_light/scalable/accounts.svg diff --git a/application/resources/pe_light/scalable/bug.svg b/launcher/resources/pe_light/scalable/bug.svg similarity index 100% rename from application/resources/pe_light/scalable/bug.svg rename to launcher/resources/pe_light/scalable/bug.svg diff --git a/application/resources/pe_light/scalable/centralmods.svg b/launcher/resources/pe_light/scalable/centralmods.svg similarity index 100% rename from application/resources/pe_light/scalable/centralmods.svg rename to launcher/resources/pe_light/scalable/centralmods.svg diff --git a/application/resources/pe_light/scalable/checkupdate.svg b/launcher/resources/pe_light/scalable/checkupdate.svg similarity index 100% rename from application/resources/pe_light/scalable/checkupdate.svg rename to launcher/resources/pe_light/scalable/checkupdate.svg diff --git a/application/resources/pe_light/scalable/copy.svg b/launcher/resources/pe_light/scalable/copy.svg similarity index 100% rename from application/resources/pe_light/scalable/copy.svg rename to launcher/resources/pe_light/scalable/copy.svg diff --git a/application/resources/pe_light/scalable/coremods.svg b/launcher/resources/pe_light/scalable/coremods.svg similarity index 100% rename from application/resources/pe_light/scalable/coremods.svg rename to launcher/resources/pe_light/scalable/coremods.svg diff --git a/launcher/resources/pe_light/scalable/custom-commands.svg b/launcher/resources/pe_light/scalable/custom-commands.svg new file mode 100644 index 0000000000..b3dfe12a4c --- /dev/null +++ b/launcher/resources/pe_light/scalable/custom-commands.svg @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/application/resources/pe_light/scalable/externaltools.svg b/launcher/resources/pe_light/scalable/externaltools.svg similarity index 100% rename from application/resources/pe_light/scalable/externaltools.svg rename to launcher/resources/pe_light/scalable/externaltools.svg diff --git a/application/resources/pe_light/scalable/help.svg b/launcher/resources/pe_light/scalable/help.svg similarity index 100% rename from application/resources/pe_light/scalable/help.svg rename to launcher/resources/pe_light/scalable/help.svg diff --git a/application/resources/pe_light/scalable/instance-settings.svg b/launcher/resources/pe_light/scalable/instance-settings.svg similarity index 100% rename from application/resources/pe_light/scalable/instance-settings.svg rename to launcher/resources/pe_light/scalable/instance-settings.svg diff --git a/application/resources/pe_light/scalable/jarmods.svg b/launcher/resources/pe_light/scalable/jarmods.svg similarity index 100% rename from application/resources/pe_light/scalable/jarmods.svg rename to launcher/resources/pe_light/scalable/jarmods.svg diff --git a/application/resources/pe_light/scalable/java.svg b/launcher/resources/pe_light/scalable/java.svg similarity index 100% rename from application/resources/pe_light/scalable/java.svg rename to launcher/resources/pe_light/scalable/java.svg diff --git a/application/resources/pe_light/scalable/language.svg b/launcher/resources/pe_light/scalable/language.svg similarity index 100% rename from application/resources/pe_light/scalable/language.svg rename to launcher/resources/pe_light/scalable/language.svg diff --git a/application/resources/pe_light/scalable/loadermods.svg b/launcher/resources/pe_light/scalable/loadermods.svg similarity index 100% rename from application/resources/pe_light/scalable/loadermods.svg rename to launcher/resources/pe_light/scalable/loadermods.svg diff --git a/application/resources/pe_light/scalable/log.svg b/launcher/resources/pe_light/scalable/log.svg similarity index 100% rename from application/resources/pe_light/scalable/log.svg rename to launcher/resources/pe_light/scalable/log.svg diff --git a/application/resources/pe_light/scalable/minecraft.svg b/launcher/resources/pe_light/scalable/minecraft.svg similarity index 100% rename from application/resources/pe_light/scalable/minecraft.svg rename to launcher/resources/pe_light/scalable/minecraft.svg diff --git a/application/resources/pe_light/scalable/multimc.svg b/launcher/resources/pe_light/scalable/multimc.svg similarity index 100% rename from application/resources/pe_light/scalable/multimc.svg rename to launcher/resources/pe_light/scalable/multimc.svg diff --git a/application/resources/pe_light/scalable/new.svg b/launcher/resources/pe_light/scalable/new.svg similarity index 100% rename from application/resources/pe_light/scalable/new.svg rename to launcher/resources/pe_light/scalable/new.svg diff --git a/application/resources/pe_light/scalable/news.svg b/launcher/resources/pe_light/scalable/news.svg similarity index 100% rename from application/resources/pe_light/scalable/news.svg rename to launcher/resources/pe_light/scalable/news.svg diff --git a/application/resources/pe_light/scalable/notes.svg b/launcher/resources/pe_light/scalable/notes.svg similarity index 100% rename from application/resources/pe_light/scalable/notes.svg rename to launcher/resources/pe_light/scalable/notes.svg diff --git a/application/resources/pe_light/scalable/patreon.svg b/launcher/resources/pe_light/scalable/patreon.svg similarity index 100% rename from application/resources/pe_light/scalable/patreon.svg rename to launcher/resources/pe_light/scalable/patreon.svg diff --git a/application/resources/pe_light/scalable/proxy.svg b/launcher/resources/pe_light/scalable/proxy.svg similarity index 100% rename from application/resources/pe_light/scalable/proxy.svg rename to launcher/resources/pe_light/scalable/proxy.svg diff --git a/application/resources/pe_light/scalable/quickmods.svg b/launcher/resources/pe_light/scalable/quickmods.svg similarity index 100% rename from application/resources/pe_light/scalable/quickmods.svg rename to launcher/resources/pe_light/scalable/quickmods.svg diff --git a/application/resources/pe_light/scalable/refresh.svg b/launcher/resources/pe_light/scalable/refresh.svg similarity index 100% rename from application/resources/pe_light/scalable/refresh.svg rename to launcher/resources/pe_light/scalable/refresh.svg diff --git a/application/resources/pe_light/scalable/resourcepacks.svg b/launcher/resources/pe_light/scalable/resourcepacks.svg similarity index 100% rename from application/resources/pe_light/scalable/resourcepacks.svg rename to launcher/resources/pe_light/scalable/resourcepacks.svg diff --git a/application/resources/pe_light/scalable/screenshots.svg b/launcher/resources/pe_light/scalable/screenshots.svg similarity index 100% rename from application/resources/pe_light/scalable/screenshots.svg rename to launcher/resources/pe_light/scalable/screenshots.svg diff --git a/application/resources/pe_light/scalable/settings.svg b/launcher/resources/pe_light/scalable/settings.svg similarity index 100% rename from application/resources/pe_light/scalable/settings.svg rename to launcher/resources/pe_light/scalable/settings.svg diff --git a/application/resources/pe_light/scalable/status-bad.svg b/launcher/resources/pe_light/scalable/status-bad.svg similarity index 100% rename from application/resources/pe_light/scalable/status-bad.svg rename to launcher/resources/pe_light/scalable/status-bad.svg diff --git a/application/resources/pe_light/scalable/status-good.svg b/launcher/resources/pe_light/scalable/status-good.svg similarity index 100% rename from application/resources/pe_light/scalable/status-good.svg rename to launcher/resources/pe_light/scalable/status-good.svg diff --git a/application/resources/pe_light/scalable/status-yellow.svg b/launcher/resources/pe_light/scalable/status-yellow.svg similarity index 100% rename from application/resources/pe_light/scalable/status-yellow.svg rename to launcher/resources/pe_light/scalable/status-yellow.svg diff --git a/application/resources/pe_light/scalable/viewfolder.svg b/launcher/resources/pe_light/scalable/viewfolder.svg similarity index 100% rename from application/resources/pe_light/scalable/viewfolder.svg rename to launcher/resources/pe_light/scalable/viewfolder.svg diff --git a/application/resources/pe_light/scalable/worlds.svg b/launcher/resources/pe_light/scalable/worlds.svg similarity index 100% rename from application/resources/pe_light/scalable/worlds.svg rename to launcher/resources/pe_light/scalable/worlds.svg diff --git a/application/resources/sources/clucker.svg b/launcher/resources/sources/clucker.svg similarity index 100% rename from application/resources/sources/clucker.svg rename to launcher/resources/sources/clucker.svg diff --git a/application/resources/sources/creeper.svg b/launcher/resources/sources/creeper.svg similarity index 100% rename from application/resources/sources/creeper.svg rename to launcher/resources/sources/creeper.svg diff --git a/application/resources/sources/enderpearl.svg b/launcher/resources/sources/enderpearl.svg similarity index 100% rename from application/resources/sources/enderpearl.svg rename to launcher/resources/sources/enderpearl.svg diff --git a/application/resources/sources/flame.svg b/launcher/resources/sources/flame.svg similarity index 100% rename from application/resources/sources/flame.svg rename to launcher/resources/sources/flame.svg diff --git a/application/resources/sources/ftb-glow.svg b/launcher/resources/sources/ftb-glow.svg similarity index 100% rename from application/resources/sources/ftb-glow.svg rename to launcher/resources/sources/ftb-glow.svg diff --git a/application/resources/sources/ftb-logo.svg b/launcher/resources/sources/ftb-logo.svg similarity index 100% rename from application/resources/sources/ftb-logo.svg rename to launcher/resources/sources/ftb-logo.svg diff --git a/application/resources/sources/gear.svg b/launcher/resources/sources/gear.svg similarity index 100% rename from application/resources/sources/gear.svg rename to launcher/resources/sources/gear.svg diff --git a/application/resources/sources/herobrine.svg b/launcher/resources/sources/herobrine.svg similarity index 100% rename from application/resources/sources/herobrine.svg rename to launcher/resources/sources/herobrine.svg diff --git a/application/resources/sources/magitech.svg b/launcher/resources/sources/magitech.svg similarity index 100% rename from application/resources/sources/magitech.svg rename to launcher/resources/sources/magitech.svg diff --git a/application/resources/sources/meat.svg b/launcher/resources/sources/meat.svg similarity index 100% rename from application/resources/sources/meat.svg rename to launcher/resources/sources/meat.svg diff --git a/application/resources/sources/multimc-discord.svg b/launcher/resources/sources/multimc-discord.svg similarity index 100% rename from application/resources/sources/multimc-discord.svg rename to launcher/resources/sources/multimc-discord.svg diff --git a/application/resources/sources/netherstar.svg b/launcher/resources/sources/netherstar.svg similarity index 100% rename from application/resources/sources/netherstar.svg rename to launcher/resources/sources/netherstar.svg diff --git a/application/resources/sources/pskeleton.svg b/launcher/resources/sources/pskeleton.svg similarity index 100% rename from application/resources/sources/pskeleton.svg rename to launcher/resources/sources/pskeleton.svg diff --git a/application/resources/sources/skeleton.svg b/launcher/resources/sources/skeleton.svg similarity index 100% rename from application/resources/sources/skeleton.svg rename to launcher/resources/sources/skeleton.svg diff --git a/application/resources/sources/squarecreeper.svg b/launcher/resources/sources/squarecreeper.svg similarity index 100% rename from application/resources/sources/squarecreeper.svg rename to launcher/resources/sources/squarecreeper.svg diff --git a/application/resources/sources/steve.svg b/launcher/resources/sources/steve.svg similarity index 100% rename from application/resources/sources/steve.svg rename to launcher/resources/sources/steve.svg diff --git a/api/logic/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp similarity index 92% rename from api/logic/screenshots/ImgurAlbumCreation.cpp rename to launcher/screenshots/ImgurAlbumCreation.cpp index ff9ec6fde9..1f195f004b 100644 --- a/api/logic/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -20,9 +20,9 @@ void ImgurAlbumCreation::start() { m_status = Job_InProgress; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - request.setRawHeader("Authorization", "Client-ID 5b97b0713fba4a3"); + request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); QStringList hashes; diff --git a/api/logic/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h similarity index 89% rename from api/logic/screenshots/ImgurAlbumCreation.h rename to launcher/screenshots/ImgurAlbumCreation.h index 5547802187..954637e68f 100644 --- a/api/logic/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -2,10 +2,8 @@ #include "net/NetAction.h" #include "Screenshot.h" -#include "multimc_logic_export.h" - typedef std::shared_ptr ImgurAlbumCreationPtr; -class MULTIMC_LOGIC_EXPORT ImgurAlbumCreation : public NetAction +class ImgurAlbumCreation : public NetAction { public: explicit ImgurAlbumCreation(QList screenshots); diff --git a/api/logic/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp similarity index 94% rename from api/logic/screenshots/ImgurUpload.cpp rename to launcher/screenshots/ImgurUpload.cpp index 1585b06152..7e95d5ca21 100644 --- a/api/logic/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -23,8 +23,8 @@ void ImgurUpload::start() finished = false; m_status = Job_InProgress; QNetworkRequest request(m_url); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); - request.setRawHeader("Authorization", "Client-ID 5b97b0713fba4a3"); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); request.setRawHeader("Accept", "application/json"); QFile f(m_shot->m_file.absoluteFilePath()); diff --git a/api/logic/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h similarity index 87% rename from api/logic/screenshots/ImgurUpload.h rename to launcher/screenshots/ImgurUpload.h index d79807f223..0507d4992a 100644 --- a/api/logic/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -2,10 +2,8 @@ #include "net/NetAction.h" #include "Screenshot.h" -#include "multimc_logic_export.h" - typedef std::shared_ptr ImgurUploadPtr; -class MULTIMC_LOGIC_EXPORT ImgurUpload : public NetAction +class ImgurUpload : public NetAction { public: explicit ImgurUpload(ScreenshotPtr shot); diff --git a/api/logic/screenshots/Screenshot.h b/launcher/screenshots/Screenshot.h similarity index 100% rename from api/logic/screenshots/Screenshot.h rename to launcher/screenshots/Screenshot.h diff --git a/api/logic/settings/INIFile.cpp b/launcher/settings/INIFile.cpp similarity index 100% rename from api/logic/settings/INIFile.cpp rename to launcher/settings/INIFile.cpp diff --git a/api/logic/settings/INIFile.h b/launcher/settings/INIFile.h similarity index 91% rename from api/logic/settings/INIFile.h rename to launcher/settings/INIFile.h index 9e8c68eaf4..4313e8293f 100644 --- a/api/logic/settings/INIFile.h +++ b/launcher/settings/INIFile.h @@ -19,10 +19,8 @@ #include #include -#include "multimc_logic_export.h" - // Sectionless INI parser (for instance config files) -class MULTIMC_LOGIC_EXPORT INIFile : public QMap +class INIFile : public QMap { public: explicit INIFile(); diff --git a/api/logic/settings/INIFile_test.cpp b/launcher/settings/INIFile_test.cpp similarity index 100% rename from api/logic/settings/INIFile_test.cpp rename to launcher/settings/INIFile_test.cpp diff --git a/api/logic/settings/INISettingsObject.cpp b/launcher/settings/INISettingsObject.cpp similarity index 100% rename from api/logic/settings/INISettingsObject.cpp rename to launcher/settings/INISettingsObject.cpp diff --git a/api/logic/settings/INISettingsObject.h b/launcher/settings/INISettingsObject.h similarity index 94% rename from api/logic/settings/INISettingsObject.h rename to launcher/settings/INISettingsObject.h index 313f151222..26cc32e518 100644 --- a/api/logic/settings/INISettingsObject.h +++ b/launcher/settings/INISettingsObject.h @@ -21,12 +21,10 @@ #include "settings/SettingsObject.h" -#include "multimc_logic_export.h" - /*! * \brief A settings object that stores its settings in an INIFile. */ -class MULTIMC_LOGIC_EXPORT INISettingsObject : public SettingsObject +class INISettingsObject : public SettingsObject { Q_OBJECT public: diff --git a/api/logic/settings/OverrideSetting.cpp b/launcher/settings/OverrideSetting.cpp similarity index 100% rename from api/logic/settings/OverrideSetting.cpp rename to launcher/settings/OverrideSetting.cpp diff --git a/api/logic/settings/OverrideSetting.h b/launcher/settings/OverrideSetting.h similarity index 100% rename from api/logic/settings/OverrideSetting.h rename to launcher/settings/OverrideSetting.h diff --git a/api/logic/settings/PassthroughSetting.cpp b/launcher/settings/PassthroughSetting.cpp similarity index 100% rename from api/logic/settings/PassthroughSetting.cpp rename to launcher/settings/PassthroughSetting.cpp diff --git a/api/logic/settings/PassthroughSetting.h b/launcher/settings/PassthroughSetting.h similarity index 100% rename from api/logic/settings/PassthroughSetting.h rename to launcher/settings/PassthroughSetting.h diff --git a/api/logic/settings/Setting.cpp b/launcher/settings/Setting.cpp similarity index 100% rename from api/logic/settings/Setting.cpp rename to launcher/settings/Setting.cpp diff --git a/api/logic/settings/Setting.h b/launcher/settings/Setting.h similarity index 97% rename from api/logic/settings/Setting.h rename to launcher/settings/Setting.h index a31193ac0d..9beeb35e01 100644 --- a/api/logic/settings/Setting.h +++ b/launcher/settings/Setting.h @@ -20,14 +20,12 @@ #include #include -#include "multimc_logic_export.h" - class SettingsObject; /*! * */ -class MULTIMC_LOGIC_EXPORT Setting : public QObject +class Setting : public QObject { Q_OBJECT public: diff --git a/api/logic/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp similarity index 100% rename from api/logic/settings/SettingsObject.cpp rename to launcher/settings/SettingsObject.cpp diff --git a/api/logic/settings/SettingsObject.h b/launcher/settings/SettingsObject.h similarity index 98% rename from api/logic/settings/SettingsObject.h rename to launcher/settings/SettingsObject.h index 3ebdebdfd1..3d61e707c5 100644 --- a/api/logic/settings/SettingsObject.h +++ b/launcher/settings/SettingsObject.h @@ -21,8 +21,6 @@ #include #include -#include "multimc_logic_export.h" - class Setting; class SettingsObject; @@ -40,7 +38,7 @@ typedef std::shared_ptr SettingsObjectPtr; * * \sa Setting */ -class MULTIMC_LOGIC_EXPORT SettingsObject : public QObject +class SettingsObject : public QObject { Q_OBJECT public: diff --git a/application/setupwizard/AnalyticsWizardPage.cpp b/launcher/setupwizard/AnalyticsWizardPage.cpp similarity index 100% rename from application/setupwizard/AnalyticsWizardPage.cpp rename to launcher/setupwizard/AnalyticsWizardPage.cpp diff --git a/application/setupwizard/AnalyticsWizardPage.h b/launcher/setupwizard/AnalyticsWizardPage.h similarity index 100% rename from application/setupwizard/AnalyticsWizardPage.h rename to launcher/setupwizard/AnalyticsWizardPage.h diff --git a/application/setupwizard/BaseWizardPage.h b/launcher/setupwizard/BaseWizardPage.h similarity index 100% rename from application/setupwizard/BaseWizardPage.h rename to launcher/setupwizard/BaseWizardPage.h diff --git a/application/setupwizard/JavaWizardPage.cpp b/launcher/setupwizard/JavaWizardPage.cpp similarity index 100% rename from application/setupwizard/JavaWizardPage.cpp rename to launcher/setupwizard/JavaWizardPage.cpp diff --git a/application/setupwizard/JavaWizardPage.h b/launcher/setupwizard/JavaWizardPage.h similarity index 100% rename from application/setupwizard/JavaWizardPage.h rename to launcher/setupwizard/JavaWizardPage.h diff --git a/application/setupwizard/LanguageWizardPage.cpp b/launcher/setupwizard/LanguageWizardPage.cpp similarity index 100% rename from application/setupwizard/LanguageWizardPage.cpp rename to launcher/setupwizard/LanguageWizardPage.cpp diff --git a/application/setupwizard/LanguageWizardPage.h b/launcher/setupwizard/LanguageWizardPage.h similarity index 100% rename from application/setupwizard/LanguageWizardPage.h rename to launcher/setupwizard/LanguageWizardPage.h diff --git a/application/setupwizard/SetupWizard.cpp b/launcher/setupwizard/SetupWizard.cpp similarity index 100% rename from application/setupwizard/SetupWizard.cpp rename to launcher/setupwizard/SetupWizard.cpp diff --git a/application/setupwizard/SetupWizard.h b/launcher/setupwizard/SetupWizard.h similarity index 100% rename from application/setupwizard/SetupWizard.h rename to launcher/setupwizard/SetupWizard.h diff --git a/api/logic/status/StatusChecker.cpp b/launcher/status/StatusChecker.cpp similarity index 100% rename from api/logic/status/StatusChecker.cpp rename to launcher/status/StatusChecker.cpp diff --git a/api/logic/status/StatusChecker.h b/launcher/status/StatusChecker.h similarity index 94% rename from api/logic/status/StatusChecker.h rename to launcher/status/StatusChecker.h index e9961affa9..06b3f576fe 100644 --- a/api/logic/status/StatusChecker.h +++ b/launcher/status/StatusChecker.h @@ -21,9 +21,7 @@ #include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT StatusChecker : public QObject +class StatusChecker : public QObject { Q_OBJECT public: /* con/des */ diff --git a/api/logic/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp similarity index 100% rename from api/logic/tasks/SequentialTask.cpp rename to launcher/tasks/SequentialTask.cpp diff --git a/api/logic/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h similarity index 85% rename from api/logic/tasks/SequentialTask.h rename to launcher/tasks/SequentialTask.h index 2ca77c0088..6898c8a675 100644 --- a/api/logic/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -5,9 +5,7 @@ #include #include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT SequentialTask : public Task +class SequentialTask : public Task { Q_OBJECT public: diff --git a/api/logic/tasks/Task.cpp b/launcher/tasks/Task.cpp similarity index 100% rename from api/logic/tasks/Task.cpp rename to launcher/tasks/Task.cpp diff --git a/api/logic/tasks/Task.h b/launcher/tasks/Task.h similarity index 96% rename from api/logic/tasks/Task.h rename to launcher/tasks/Task.h index 7ed7086c5b..2367f1ec23 100644 --- a/api/logic/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -19,9 +19,7 @@ #include #include -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT Task : public QObject +class Task : public QObject { Q_OBJECT public: diff --git a/api/logic/testdata/FileSystem-test_createShortcut-unix b/launcher/testdata/FileSystem-test_createShortcut-unix similarity index 100% rename from api/logic/testdata/FileSystem-test_createShortcut-unix rename to launcher/testdata/FileSystem-test_createShortcut-unix diff --git a/api/logic/testdata/test_folder/assets/minecraft/textures/blah.txt b/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt similarity index 100% rename from api/logic/testdata/test_folder/assets/minecraft/textures/blah.txt rename to launcher/testdata/test_folder/assets/minecraft/textures/blah.txt diff --git a/api/logic/testdata/test_folder/pack.mcmeta b/launcher/testdata/test_folder/pack.mcmeta similarity index 100% rename from api/logic/testdata/test_folder/pack.mcmeta rename to launcher/testdata/test_folder/pack.mcmeta diff --git a/api/logic/testdata/test_folder/pack.nfo b/launcher/testdata/test_folder/pack.nfo similarity index 100% rename from api/logic/testdata/test_folder/pack.nfo rename to launcher/testdata/test_folder/pack.nfo diff --git a/application/themes/BrightTheme.cpp b/launcher/themes/BrightTheme.cpp similarity index 100% rename from application/themes/BrightTheme.cpp rename to launcher/themes/BrightTheme.cpp diff --git a/application/themes/BrightTheme.h b/launcher/themes/BrightTheme.h similarity index 100% rename from application/themes/BrightTheme.h rename to launcher/themes/BrightTheme.h diff --git a/application/themes/CustomTheme.cpp b/launcher/themes/CustomTheme.cpp similarity index 100% rename from application/themes/CustomTheme.cpp rename to launcher/themes/CustomTheme.cpp diff --git a/application/themes/CustomTheme.h b/launcher/themes/CustomTheme.h similarity index 100% rename from application/themes/CustomTheme.h rename to launcher/themes/CustomTheme.h diff --git a/application/themes/DarkTheme.cpp b/launcher/themes/DarkTheme.cpp similarity index 100% rename from application/themes/DarkTheme.cpp rename to launcher/themes/DarkTheme.cpp diff --git a/application/themes/DarkTheme.h b/launcher/themes/DarkTheme.h similarity index 100% rename from application/themes/DarkTheme.h rename to launcher/themes/DarkTheme.h diff --git a/application/themes/FusionTheme.cpp b/launcher/themes/FusionTheme.cpp similarity index 100% rename from application/themes/FusionTheme.cpp rename to launcher/themes/FusionTheme.cpp diff --git a/application/themes/FusionTheme.h b/launcher/themes/FusionTheme.h similarity index 100% rename from application/themes/FusionTheme.h rename to launcher/themes/FusionTheme.h diff --git a/application/themes/ITheme.cpp b/launcher/themes/ITheme.cpp similarity index 100% rename from application/themes/ITheme.cpp rename to launcher/themes/ITheme.cpp diff --git a/application/themes/ITheme.h b/launcher/themes/ITheme.h similarity index 100% rename from application/themes/ITheme.h rename to launcher/themes/ITheme.h diff --git a/application/themes/SystemTheme.cpp b/launcher/themes/SystemTheme.cpp similarity index 84% rename from application/themes/SystemTheme.cpp rename to launcher/themes/SystemTheme.cpp index 00b2300d79..49b1afaa1d 100644 --- a/application/themes/SystemTheme.cpp +++ b/launcher/themes/SystemTheme.cpp @@ -6,16 +6,19 @@ SystemTheme::SystemTheme() { + qDebug() << "Determining System Theme..."; const auto & style = QApplication::style(); systemPalette = style->standardPalette(); QString lowerThemeName = style->objectName(); - qDebug() << systemTheme; + qDebug() << "System theme seems to be:" << lowerThemeName; QStringList styles = QStyleFactory::keys(); for(auto &st: styles) { + qDebug() << "Considering theme from theme factory:" << st.toLower(); if(st.toLower() == lowerThemeName) { systemTheme = st; + qDebug() << "System theme has been determined to be:" << systemTheme; return; } } diff --git a/application/themes/SystemTheme.h b/launcher/themes/SystemTheme.h similarity index 100% rename from application/themes/SystemTheme.h rename to launcher/themes/SystemTheme.h diff --git a/api/logic/tools/BaseExternalTool.cpp b/launcher/tools/BaseExternalTool.cpp similarity index 100% rename from api/logic/tools/BaseExternalTool.cpp rename to launcher/tools/BaseExternalTool.cpp diff --git a/api/logic/tools/BaseExternalTool.h b/launcher/tools/BaseExternalTool.h similarity index 78% rename from api/logic/tools/BaseExternalTool.h rename to launcher/tools/BaseExternalTool.h index b393b9ef5e..1ebed6ae2e 100644 --- a/api/logic/tools/BaseExternalTool.h +++ b/launcher/tools/BaseExternalTool.h @@ -3,13 +3,11 @@ #include #include -#include "multimc_logic_export.h" - class BaseInstance; class SettingsObject; class QProcess; -class MULTIMC_LOGIC_EXPORT BaseExternalTool : public QObject +class BaseExternalTool : public QObject { Q_OBJECT public: @@ -21,7 +19,7 @@ class MULTIMC_LOGIC_EXPORT BaseExternalTool : public QObject SettingsObjectPtr globalSettings; }; -class MULTIMC_LOGIC_EXPORT BaseDetachedTool : public BaseExternalTool +class BaseDetachedTool : public BaseExternalTool { Q_OBJECT public: @@ -35,7 +33,7 @@ public virtual void runImpl() = 0; }; -class MULTIMC_LOGIC_EXPORT BaseExternalToolFactory +class BaseExternalToolFactory { public: virtual ~BaseExternalToolFactory(); @@ -53,7 +51,7 @@ class MULTIMC_LOGIC_EXPORT BaseExternalToolFactory SettingsObjectPtr globalSettings; }; -class MULTIMC_LOGIC_EXPORT BaseDetachedToolFactory : public BaseExternalToolFactory +class BaseDetachedToolFactory : public BaseExternalToolFactory { public: virtual BaseDetachedTool *createDetachedTool(InstancePtr instance, QObject *parent = 0); diff --git a/api/logic/tools/BaseProfiler.cpp b/launcher/tools/BaseProfiler.cpp similarity index 100% rename from api/logic/tools/BaseProfiler.cpp rename to launcher/tools/BaseProfiler.cpp diff --git a/api/logic/tools/BaseProfiler.h b/launcher/tools/BaseProfiler.h similarity index 80% rename from api/logic/tools/BaseProfiler.h rename to launcher/tools/BaseProfiler.h index da817f5237..1c934aa358 100644 --- a/api/logic/tools/BaseProfiler.h +++ b/launcher/tools/BaseProfiler.h @@ -3,14 +3,12 @@ #include "BaseExternalTool.h" #include "QObjectPtr.h" -#include "multimc_logic_export.h" - class BaseInstance; class SettingsObject; class LaunchTask; class QProcess; -class MULTIMC_LOGIC_EXPORT BaseProfiler : public BaseExternalTool +class BaseProfiler : public BaseExternalTool { Q_OBJECT public: @@ -32,7 +30,7 @@ public void abortLaunch(const QString &message); }; -class MULTIMC_LOGIC_EXPORT BaseProfilerFactory : public BaseExternalToolFactory +class BaseProfilerFactory : public BaseExternalToolFactory { public: virtual BaseProfiler *createProfiler(InstancePtr instance, QObject *parent = 0); diff --git a/api/logic/tools/JProfiler.cpp b/launcher/tools/JProfiler.cpp similarity index 100% rename from api/logic/tools/JProfiler.cpp rename to launcher/tools/JProfiler.cpp diff --git a/api/logic/tools/JProfiler.h b/launcher/tools/JProfiler.h similarity index 77% rename from api/logic/tools/JProfiler.h rename to launcher/tools/JProfiler.h index f211ddbf18..0e9a3a85ab 100644 --- a/api/logic/tools/JProfiler.h +++ b/launcher/tools/JProfiler.h @@ -2,9 +2,7 @@ #include "BaseProfiler.h" -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT JProfilerFactory : public BaseProfilerFactory +class JProfilerFactory : public BaseProfilerFactory { public: QString name() const override { return "JProfiler"; } diff --git a/api/logic/tools/JVisualVM.cpp b/launcher/tools/JVisualVM.cpp similarity index 100% rename from api/logic/tools/JVisualVM.cpp rename to launcher/tools/JVisualVM.cpp diff --git a/api/logic/tools/JVisualVM.h b/launcher/tools/JVisualVM.h similarity index 77% rename from api/logic/tools/JVisualVM.h rename to launcher/tools/JVisualVM.h index 91d48d94dc..ebdea9f333 100644 --- a/api/logic/tools/JVisualVM.h +++ b/launcher/tools/JVisualVM.h @@ -2,9 +2,7 @@ #include "BaseProfiler.h" -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT JVisualVMFactory : public BaseProfilerFactory +class JVisualVMFactory : public BaseProfilerFactory { public: QString name() const override { return "JVisualVM"; } diff --git a/api/logic/tools/MCEditTool.cpp b/launcher/tools/MCEditTool.cpp similarity index 100% rename from api/logic/tools/MCEditTool.cpp rename to launcher/tools/MCEditTool.cpp diff --git a/api/logic/tools/MCEditTool.h b/launcher/tools/MCEditTool.h similarity index 81% rename from api/logic/tools/MCEditTool.h rename to launcher/tools/MCEditTool.h index 1465494e00..733dff8661 100644 --- a/api/logic/tools/MCEditTool.h +++ b/launcher/tools/MCEditTool.h @@ -2,9 +2,8 @@ #include #include "settings/SettingsObject.h" -#include "multimc_logic_export.h" -class MULTIMC_LOGIC_EXPORT MCEditTool +class MCEditTool { public: MCEditTool(SettingsObjectPtr settings); diff --git a/api/logic/translations/POTranslator.cpp b/launcher/translations/POTranslator.cpp similarity index 100% rename from api/logic/translations/POTranslator.cpp rename to launcher/translations/POTranslator.cpp diff --git a/api/logic/translations/POTranslator.h b/launcher/translations/POTranslator.h similarity index 100% rename from api/logic/translations/POTranslator.h rename to launcher/translations/POTranslator.h diff --git a/api/logic/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp similarity index 97% rename from api/logic/translations/TranslationsModel.cpp rename to launcher/translations/TranslationsModel.cpp index adb3fa98d4..29a952b039 100644 --- a/api/logic/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -15,7 +15,7 @@ #include "POTranslator.h" -const static QLatin1Literal defaultLangCode("en"); +const static QLatin1Literal defaultLangCode("en_US"); enum class FileType { @@ -33,12 +33,7 @@ struct Language Language(const QString & _key) { key = _key; - if(key == "pt") { - locale = QLocale("pt_PT"); - } - else { - locale = QLocale(key); - } + locale = QLocale(key); updated = (key == defaultLangCode); } @@ -303,11 +298,14 @@ void TranslationsModel::reloadLocalFiles() { return; } - beginInsertRows(QModelIndex(), d->m_languages.size(), d->m_languages.size() + languages.size() - 1); + beginInsertRows(QModelIndex(), 0, d->m_languages.size() + languages.size() - 1); for(auto & language: languages) { d->m_languages.append(language); } + std::sort(d->m_languages.begin(), d->m_languages.end(), [](const Language& a, const Language& b) { + return a.key.compare(b.key) < 0; + }); endInsertRows(); } @@ -340,7 +338,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const { case Column::Language: { - return d->m_languages[row].locale.nativeLanguageName(); + return lang.locale.nativeLanguageName(); } case Column::Completeness: { @@ -355,7 +353,7 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total").arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total)); } case Qt::UserRole: - return d->m_languages[row].key; + return lang.key; default: return QVariant(); } @@ -452,7 +450,7 @@ bool TranslationsModel::selectLanguage(QString key) * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. * This function is not reentrant. */ - QLocale locale(langCode); + QLocale locale = QLocale(langCode); QLocale::setDefault(locale); // if it's the default UI language, finish diff --git a/api/logic/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h similarity index 94% rename from api/logic/translations/TranslationsModel.h rename to launcher/translations/TranslationsModel.h index 17e5b12412..3abf84e6e8 100644 --- a/api/logic/translations/TranslationsModel.h +++ b/launcher/translations/TranslationsModel.h @@ -17,11 +17,10 @@ #include #include -#include "multimc_logic_export.h" struct Language; -class MULTIMC_LOGIC_EXPORT TranslationsModel : public QAbstractListModel +class TranslationsModel : public QAbstractListModel { Q_OBJECT public: diff --git a/api/logic/updater/DownloadTask.cpp b/launcher/updater/DownloadTask.cpp similarity index 99% rename from api/logic/updater/DownloadTask.cpp rename to launcher/updater/DownloadTask.cpp index 20b26ebb31..2c62ad2424 100644 --- a/api/logic/updater/DownloadTask.cpp +++ b/launcher/updater/DownloadTask.cpp @@ -170,4 +170,4 @@ OperationList DownloadTask::operations() return m_operations; } -} \ No newline at end of file +} diff --git a/api/logic/updater/DownloadTask.h b/launcher/updater/DownloadTask.h similarity index 97% rename from api/logic/updater/DownloadTask.h rename to launcher/updater/DownloadTask.h index 88e608659c..fc5030b423 100644 --- a/api/logic/updater/DownloadTask.h +++ b/launcher/updater/DownloadTask.h @@ -19,15 +19,13 @@ #include "net/NetJob.h" #include "GoUpdate.h" -#include "multimc_logic_export.h" - namespace GoUpdate { /*! * The DownloadTask is a task that takes a given version ID and repository URL, * downloads that version's files from the repository, and prepares to install them. */ -class MULTIMC_LOGIC_EXPORT DownloadTask : public Task +class DownloadTask : public Task { Q_OBJECT @@ -95,4 +93,4 @@ protected slots: void fileDownloadProgressChanged(qint64 current, qint64 total); }; -} \ No newline at end of file +} diff --git a/api/logic/updater/DownloadTask_test.cpp b/launcher/updater/DownloadTask_test.cpp similarity index 100% rename from api/logic/updater/DownloadTask_test.cpp rename to launcher/updater/DownloadTask_test.cpp diff --git a/api/logic/updater/GoUpdate.cpp b/launcher/updater/GoUpdate.cpp similarity index 100% rename from api/logic/updater/GoUpdate.cpp rename to launcher/updater/GoUpdate.cpp diff --git a/api/logic/updater/GoUpdate.h b/launcher/updater/GoUpdate.h similarity index 89% rename from api/logic/updater/GoUpdate.h rename to launcher/updater/GoUpdate.h index 8f92bb9969..8058e5437d 100644 --- a/api/logic/updater/GoUpdate.h +++ b/launcher/updater/GoUpdate.h @@ -2,15 +2,13 @@ #include #include -#include "multimc_logic_export.h" - namespace GoUpdate { /** * A temporary object exchanged between updated checker and the actual update task */ -struct MULTIMC_LOGIC_EXPORT Status +struct Status { bool updateAvailable = false; @@ -27,7 +25,7 @@ struct MULTIMC_LOGIC_EXPORT Status /** * Struct that describes an entry in a VersionFileEntry's `Sources` list. */ -struct MULTIMC_LOGIC_EXPORT FileSource +struct FileSource { FileSource(QString type, QString url, QString compression="") { @@ -50,7 +48,7 @@ typedef QList FileSourceList; /** * Structure that describes an entry in a GoUpdate version's `Files` list. */ -struct MULTIMC_LOGIC_EXPORT VersionFileEntry +struct VersionFileEntry { QString path; int mode; @@ -66,7 +64,7 @@ typedef QList VersionFileList; /** * Structure that describes an operation to perform when installing updates. */ -struct MULTIMC_LOGIC_EXPORT Operation +struct Operation { static Operation CopyOp(QString from, QString to, int fmode=0644) { @@ -107,13 +105,13 @@ typedef QList OperationList; /** * Loads the file list from the given version info JSON object into the given list. */ -bool MULTIMC_LOGIC_EXPORT parseVersionInfo(const QByteArray &data, VersionFileList& list, QString &error); +bool parseVersionInfo(const QByteArray &data, VersionFileList& list, QString &error); /*! * Takes a list of file entries for the current version's files and the new version's files * and populates the downloadList and operationList with information about how to download and install the update. */ -bool MULTIMC_LOGIC_EXPORT processFileLists +bool processFileLists ( const VersionFileList ¤tVersion, const VersionFileList &newVersion, diff --git a/api/logic/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp similarity index 89% rename from api/logic/updater/UpdateChecker.cpp rename to launcher/updater/UpdateChecker.cpp index be33c73c64..eea73dcf5e 100644 --- a/api/logic/updater/UpdateChecker.cpp +++ b/launcher/updater/UpdateChecker.cpp @@ -23,9 +23,12 @@ #define API_VERSION 0 #define CHANLIST_FORMAT 0 -UpdateChecker::UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild) +#include "BuildConfig.h" +#include "sys.h" + +UpdateChecker::UpdateChecker(QString channelUrl, QString currentChannel, int currentBuild) { - m_channelListUrl = channelListUrl; + m_channelUrl = channelUrl; m_currentChannel = currentChannel; m_currentBuild = currentBuild; } @@ -48,8 +51,7 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) // later. if (!m_chanListLoaded) { - qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring " - "update check."; + qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; m_checkUpdateWaiting = true; m_deferredUpdateChannel = updateChannel; updateChanList(notifyNoUpdate); @@ -62,29 +64,43 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) return; } - m_updateChecking = true; - // Find the desired channel within the channel list and get its repo URL. If if cannot be // found, error. + QString stableUrl; m_newRepoUrl = ""; for (ChannelListEntry entry : m_channels) { - if (entry.id == updateChannel) + qDebug() << "channelEntry = " << entry.id; + if(entry.id == "stable") { + stableUrl = entry.url; + } + if (entry.id == updateChannel) { m_newRepoUrl = entry.url; - if (entry.id == m_currentChannel) + qDebug() << "is intended update channel: " << entry.id; + } + if (entry.id == m_currentChannel) { m_currentRepoUrl = entry.url; + qDebug() << "is current update channel: " << entry.id; + } } qDebug() << "m_repoUrl = " << m_newRepoUrl; - // If we didn't find our channel, error. + if (m_newRepoUrl.isEmpty()) { + qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; + m_newRepoUrl = stableUrl; + } + + // If nothing applies, error if (m_newRepoUrl.isEmpty()) { - qCritical() << "m_repoUrl is empty!"; + qCritical() << "failed to select any update repository for: " << updateChannel; emit updateCheckFailed(); return; } + m_updateChecking = true; + QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); auto job = new NetJob("GoUpdate Repository Index"); @@ -174,7 +190,7 @@ void UpdateChecker::updateChanList(bool notifyNoUpdate) return; } - if (m_channelListUrl.isEmpty()) + if (m_channelUrl.isEmpty()) { qCritical() << "Failed to update channel list. No channel list URL set." << "If you'd like to use MultiMC's update system, please pass the channel " @@ -184,7 +200,7 @@ void UpdateChecker::updateChanList(bool notifyNoUpdate) m_chanListLoading = true; NetJob *job = new NetJob("Update System Channel List"); - job->addNetAction(Net::Download::makeByteArray(QUrl(m_channelListUrl), &chanlistData)); + job->addNetAction(Net::Download::makeByteArray(QUrl(m_channelUrl), &chanlistData)); connect(job, &NetJob::succeeded, [this, notifyNoUpdate]() { chanListDownloadFinished(notifyNoUpdate); }); QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed); chanListJob.reset(job); diff --git a/api/logic/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h similarity index 93% rename from api/logic/updater/UpdateChecker.h rename to launcher/updater/UpdateChecker.h index 20906207a0..219c3c628c 100644 --- a/api/logic/updater/UpdateChecker.h +++ b/launcher/updater/UpdateChecker.h @@ -18,14 +18,12 @@ #include "net/NetJob.h" #include "GoUpdate.h" -#include "multimc_logic_export.h" - -class MULTIMC_LOGIC_EXPORT UpdateChecker : public QObject +class UpdateChecker : public QObject { Q_OBJECT public: - UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild); + UpdateChecker(QString channelUrl, QString currentChannel, int currentBuild); void checkForUpdate(QString updateChannel, bool notifyNoUpdate); /*! @@ -80,7 +78,7 @@ private slots: NetJobPtr chanListJob; QByteArray chanlistData; - QString m_channelListUrl; + QString m_channelUrl; QList m_channels; diff --git a/api/logic/updater/UpdateChecker_test.cpp b/launcher/updater/UpdateChecker_test.cpp similarity index 100% rename from api/logic/updater/UpdateChecker_test.cpp rename to launcher/updater/UpdateChecker_test.cpp diff --git a/api/logic/updater/testdata/1.json b/launcher/updater/testdata/1.json similarity index 100% rename from api/logic/updater/testdata/1.json rename to launcher/updater/testdata/1.json diff --git a/api/logic/updater/testdata/2.json b/launcher/updater/testdata/2.json similarity index 100% rename from api/logic/updater/testdata/2.json rename to launcher/updater/testdata/2.json diff --git a/api/logic/updater/testdata/channels.json b/launcher/updater/testdata/channels.json similarity index 100% rename from api/logic/updater/testdata/channels.json rename to launcher/updater/testdata/channels.json diff --git a/api/logic/updater/testdata/errorChannels.json b/launcher/updater/testdata/errorChannels.json similarity index 100% rename from api/logic/updater/testdata/errorChannels.json rename to launcher/updater/testdata/errorChannels.json diff --git a/api/logic/updater/testdata/fileOneA b/launcher/updater/testdata/fileOneA similarity index 100% rename from api/logic/updater/testdata/fileOneA rename to launcher/updater/testdata/fileOneA diff --git a/api/logic/updater/testdata/fileOneB b/launcher/updater/testdata/fileOneB similarity index 100% rename from api/logic/updater/testdata/fileOneB rename to launcher/updater/testdata/fileOneB diff --git a/api/logic/updater/testdata/fileThree b/launcher/updater/testdata/fileThree similarity index 100% rename from api/logic/updater/testdata/fileThree rename to launcher/updater/testdata/fileThree diff --git a/api/logic/updater/testdata/fileTwo b/launcher/updater/testdata/fileTwo similarity index 100% rename from api/logic/updater/testdata/fileTwo rename to launcher/updater/testdata/fileTwo diff --git a/api/logic/updater/testdata/garbageChannels.json b/launcher/updater/testdata/garbageChannels.json similarity index 100% rename from api/logic/updater/testdata/garbageChannels.json rename to launcher/updater/testdata/garbageChannels.json diff --git a/api/logic/updater/testdata/index.json b/launcher/updater/testdata/index.json similarity index 100% rename from api/logic/updater/testdata/index.json rename to launcher/updater/testdata/index.json diff --git a/api/logic/updater/testdata/noChannels.json b/launcher/updater/testdata/noChannels.json similarity index 100% rename from api/logic/updater/testdata/noChannels.json rename to launcher/updater/testdata/noChannels.json diff --git a/api/logic/updater/testdata/oneChannel.json b/launcher/updater/testdata/oneChannel.json similarity index 100% rename from api/logic/updater/testdata/oneChannel.json rename to launcher/updater/testdata/oneChannel.json diff --git a/api/logic/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml b/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml similarity index 100% rename from api/logic/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml rename to launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml diff --git a/application/widgets/Common.cpp b/launcher/widgets/Common.cpp similarity index 100% rename from application/widgets/Common.cpp rename to launcher/widgets/Common.cpp diff --git a/application/widgets/Common.h b/launcher/widgets/Common.h similarity index 100% rename from application/widgets/Common.h rename to launcher/widgets/Common.h diff --git a/application/widgets/CustomCommands.cpp b/launcher/widgets/CustomCommands.cpp similarity index 100% rename from application/widgets/CustomCommands.cpp rename to launcher/widgets/CustomCommands.cpp diff --git a/application/widgets/CustomCommands.h b/launcher/widgets/CustomCommands.h similarity index 100% rename from application/widgets/CustomCommands.h rename to launcher/widgets/CustomCommands.h diff --git a/application/widgets/CustomCommands.ui b/launcher/widgets/CustomCommands.ui similarity index 100% rename from application/widgets/CustomCommands.ui rename to launcher/widgets/CustomCommands.ui diff --git a/application/widgets/DropLabel.cpp b/launcher/widgets/DropLabel.cpp similarity index 100% rename from application/widgets/DropLabel.cpp rename to launcher/widgets/DropLabel.cpp diff --git a/application/widgets/DropLabel.h b/launcher/widgets/DropLabel.h similarity index 100% rename from application/widgets/DropLabel.h rename to launcher/widgets/DropLabel.h diff --git a/application/widgets/FocusLineEdit.cpp b/launcher/widgets/FocusLineEdit.cpp similarity index 100% rename from application/widgets/FocusLineEdit.cpp rename to launcher/widgets/FocusLineEdit.cpp diff --git a/application/widgets/FocusLineEdit.h b/launcher/widgets/FocusLineEdit.h similarity index 100% rename from application/widgets/FocusLineEdit.h rename to launcher/widgets/FocusLineEdit.h diff --git a/application/widgets/IconLabel.cpp b/launcher/widgets/IconLabel.cpp similarity index 100% rename from application/widgets/IconLabel.cpp rename to launcher/widgets/IconLabel.cpp diff --git a/application/widgets/IconLabel.h b/launcher/widgets/IconLabel.h similarity index 100% rename from application/widgets/IconLabel.h rename to launcher/widgets/IconLabel.h diff --git a/application/widgets/InstanceCardWidget.ui b/launcher/widgets/InstanceCardWidget.ui similarity index 100% rename from application/widgets/InstanceCardWidget.ui rename to launcher/widgets/InstanceCardWidget.ui diff --git a/application/widgets/JavaSettingsWidget.cpp b/launcher/widgets/JavaSettingsWidget.cpp similarity index 99% rename from application/widgets/JavaSettingsWidget.cpp rename to launcher/widgets/JavaSettingsWidget.cpp index a11dd1aad4..7f53dc2301 100644 --- a/application/widgets/JavaSettingsWidget.cpp +++ b/launcher/widgets/JavaSettingsWidget.cpp @@ -19,7 +19,7 @@ JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent) { - m_availableMemory = Sys::getSystemRam() / Sys::megabyte; + m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; goodIcon = MMC->getThemedIcon("status-good"); yellowIcon = MMC->getThemedIcon("status-yellow"); diff --git a/application/widgets/JavaSettingsWidget.h b/launcher/widgets/JavaSettingsWidget.h similarity index 100% rename from application/widgets/JavaSettingsWidget.h rename to launcher/widgets/JavaSettingsWidget.h diff --git a/application/widgets/LabeledToolButton.cpp b/launcher/widgets/LabeledToolButton.cpp similarity index 100% rename from application/widgets/LabeledToolButton.cpp rename to launcher/widgets/LabeledToolButton.cpp diff --git a/application/widgets/LabeledToolButton.h b/launcher/widgets/LabeledToolButton.h similarity index 100% rename from application/widgets/LabeledToolButton.h rename to launcher/widgets/LabeledToolButton.h diff --git a/application/widgets/LanguageSelectionWidget.cpp b/launcher/widgets/LanguageSelectionWidget.cpp similarity index 100% rename from application/widgets/LanguageSelectionWidget.cpp rename to launcher/widgets/LanguageSelectionWidget.cpp diff --git a/application/widgets/LanguageSelectionWidget.h b/launcher/widgets/LanguageSelectionWidget.h similarity index 100% rename from application/widgets/LanguageSelectionWidget.h rename to launcher/widgets/LanguageSelectionWidget.h diff --git a/application/widgets/LineSeparator.cpp b/launcher/widgets/LineSeparator.cpp similarity index 100% rename from application/widgets/LineSeparator.cpp rename to launcher/widgets/LineSeparator.cpp diff --git a/application/widgets/LineSeparator.h b/launcher/widgets/LineSeparator.h similarity index 100% rename from application/widgets/LineSeparator.h rename to launcher/widgets/LineSeparator.h diff --git a/application/widgets/LogView.cpp b/launcher/widgets/LogView.cpp similarity index 100% rename from application/widgets/LogView.cpp rename to launcher/widgets/LogView.cpp diff --git a/application/widgets/LogView.h b/launcher/widgets/LogView.h similarity index 100% rename from application/widgets/LogView.h rename to launcher/widgets/LogView.h diff --git a/application/widgets/MCModInfoFrame.cpp b/launcher/widgets/MCModInfoFrame.cpp similarity index 100% rename from application/widgets/MCModInfoFrame.cpp rename to launcher/widgets/MCModInfoFrame.cpp diff --git a/application/widgets/MCModInfoFrame.h b/launcher/widgets/MCModInfoFrame.h similarity index 100% rename from application/widgets/MCModInfoFrame.h rename to launcher/widgets/MCModInfoFrame.h diff --git a/application/widgets/MCModInfoFrame.ui b/launcher/widgets/MCModInfoFrame.ui similarity index 100% rename from application/widgets/MCModInfoFrame.ui rename to launcher/widgets/MCModInfoFrame.ui diff --git a/application/widgets/ModListView.cpp b/launcher/widgets/ModListView.cpp similarity index 100% rename from application/widgets/ModListView.cpp rename to launcher/widgets/ModListView.cpp diff --git a/application/widgets/ModListView.h b/launcher/widgets/ModListView.h similarity index 100% rename from application/widgets/ModListView.h rename to launcher/widgets/ModListView.h diff --git a/application/widgets/PageContainer.cpp b/launcher/widgets/PageContainer.cpp similarity index 100% rename from application/widgets/PageContainer.cpp rename to launcher/widgets/PageContainer.cpp diff --git a/application/widgets/PageContainer.h b/launcher/widgets/PageContainer.h similarity index 100% rename from application/widgets/PageContainer.h rename to launcher/widgets/PageContainer.h diff --git a/application/widgets/PageContainer_p.h b/launcher/widgets/PageContainer_p.h similarity index 100% rename from application/widgets/PageContainer_p.h rename to launcher/widgets/PageContainer_p.h diff --git a/application/widgets/ProgressWidget.cpp b/launcher/widgets/ProgressWidget.cpp similarity index 100% rename from application/widgets/ProgressWidget.cpp rename to launcher/widgets/ProgressWidget.cpp diff --git a/application/widgets/ProgressWidget.h b/launcher/widgets/ProgressWidget.h similarity index 100% rename from application/widgets/ProgressWidget.h rename to launcher/widgets/ProgressWidget.h diff --git a/application/widgets/ServerStatus.cpp b/launcher/widgets/ServerStatus.cpp similarity index 100% rename from application/widgets/ServerStatus.cpp rename to launcher/widgets/ServerStatus.cpp diff --git a/application/widgets/ServerStatus.h b/launcher/widgets/ServerStatus.h similarity index 100% rename from application/widgets/ServerStatus.h rename to launcher/widgets/ServerStatus.h diff --git a/application/widgets/VersionListView.cpp b/launcher/widgets/VersionListView.cpp similarity index 100% rename from application/widgets/VersionListView.cpp rename to launcher/widgets/VersionListView.cpp diff --git a/application/widgets/VersionListView.h b/launcher/widgets/VersionListView.h similarity index 100% rename from application/widgets/VersionListView.h rename to launcher/widgets/VersionListView.h diff --git a/application/widgets/VersionSelectWidget.cpp b/launcher/widgets/VersionSelectWidget.cpp similarity index 100% rename from application/widgets/VersionSelectWidget.cpp rename to launcher/widgets/VersionSelectWidget.cpp diff --git a/application/widgets/VersionSelectWidget.h b/launcher/widgets/VersionSelectWidget.h similarity index 100% rename from application/widgets/VersionSelectWidget.h rename to launcher/widgets/VersionSelectWidget.h diff --git a/application/widgets/WideBar.cpp b/launcher/widgets/WideBar.cpp similarity index 100% rename from application/widgets/WideBar.cpp rename to launcher/widgets/WideBar.cpp diff --git a/application/widgets/WideBar.h b/launcher/widgets/WideBar.h similarity index 100% rename from application/widgets/WideBar.h rename to launcher/widgets/WideBar.h diff --git a/libraries/README.md b/libraries/README.md index cdc7200426..ac861148ad 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -158,3 +158,10 @@ A Google Analytics library for Qt. BSD licensed, derived from [qt-google-analytics](https://github.com/HSAnet/qt-google-analytics). Modifications include better handling of IP anonymization (can be enabled) and general improvements of the API (application handles persistence and ID generation instead of the library). + +## tomlc99 +A TOML language parser. Used by Forge 1.14+ to store mod metadata. + +See [github repo](https://github.com/cktan/tomlc99). + +Licenced under the MIT licence. diff --git a/libraries/ganalytics/src/ganalytics_worker.cpp b/libraries/ganalytics/src/ganalytics_worker.cpp index 5980d3bd94..b0ae75a408 100644 --- a/libraries/ganalytics/src/ganalytics_worker.cpp +++ b/libraries/ganalytics/src/ganalytics_worker.cpp @@ -237,7 +237,7 @@ void GAnalyticsWorker::postMessageFinished() int httpStausCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (httpStausCode < 200 || httpStausCode > 299) { - logMessage(GAnalytics::Error, QString("Error posting message: %s").arg(reply->errorString())); + logMessage(GAnalytics::Error, QString("Error posting message: %1").arg(reply->errorString())); // An error ocurred. Try sending later. m_timer.start(); diff --git a/libraries/katabasis/.gitignore b/libraries/katabasis/.gitignore new file mode 100644 index 0000000000..35e189c5ef --- /dev/null +++ b/libraries/katabasis/.gitignore @@ -0,0 +1,2 @@ +build/ +*.kdev4 diff --git a/libraries/katabasis/CMakeLists.txt b/libraries/katabasis/CMakeLists.txt new file mode 100644 index 0000000000..170bc2c9af --- /dev/null +++ b/libraries/katabasis/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.6) + +string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) +if(IS_IN_SOURCE_BUILD) + message(FATAL_ERROR "You are building Katabasis in-source. Please separate the build tree from the source tree.") +endif() + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_HOST_SYSTEM_VERSION MATCHES ".*[Mm]icrosoft.*" OR + CMAKE_HOST_SYSTEM_VERSION MATCHES ".*WSL.*" + ) + message(FATAL_ERROR "Building Katabasis is not supported in Linux-on-Windows distributions. Use a real Linux machine, not a fraudulent one.") + endif() +endif() + +project(Katabasis) +enable_testing() + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_CXX_STANDARD_REQUIRED true) +set(CMAKE_C_STANDARD_REQUIRED true) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_C_STANDARD 11) + +find_package(Qt5 COMPONENTS Core Network REQUIRED) + +set( katabasis_PRIVATE + src/OAuth2.cpp + + src/JsonResponse.cpp + src/JsonResponse.h + src/PollServer.cpp + src/Reply.cpp + src/ReplyServer.cpp + src/Requestor.cpp +) + +set( katabasis_PUBLIC + include/katabasis/OAuth2.h + + include/katabasis/Globals.h + include/katabasis/PollServer.h + include/katabasis/Reply.h + include/katabasis/ReplyServer.h + + include/katabasis/Requestor.h + include/katabasis/RequestParameter.h +) + +add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} ) +target_link_libraries(Katabasis Qt5::Core Qt5::Network) + +# needed for statically linked Katabasis in shared libs on x86_64 +set_target_properties(Katabasis + PROPERTIES POSITION_INDEPENDENT_CODE TRUE +) + +target_include_directories(Katabasis PUBLIC include PRIVATE src include/katabasis) diff --git a/libraries/katabasis/LICENSE b/libraries/katabasis/LICENSE new file mode 100644 index 0000000000..9ac8d42fb0 --- /dev/null +++ b/libraries/katabasis/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2012, Akos Polster +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libraries/katabasis/README.md b/libraries/katabasis/README.md new file mode 100644 index 0000000000..a4dc099445 --- /dev/null +++ b/libraries/katabasis/README.md @@ -0,0 +1,36 @@ +# Katabasis - MS-flavoerd OAuth for Qt, derived from the O2 library + +This library's sole purpose is to make interacting with MSA and various MSA and XBox authenticated services less painful. + +It may be possible to backport some of the changes to O2 in the future, but for the sake of going fast, all compatibility concerns have been ignored. + +[You can find the original library's git repository here.](https://github.com/pipacs/o2) + +Notes to contributors: + + * Please follow the coding style of the existing source, where reasonable + * Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code + * If you are interested in working on this, come to the MultiMC Discord server and talk first + +## Installation + +Clone the Github repository, integrate the it into your CMake build system. + +The library is static only, dynamic linking and system-wide installation are out of scope and undesirable. + +## Usage + +At this stage, don't, unless you want to help with the library itself. + +This is an experimental fork of the O2 library and is undergoing a big design/architecture shift in order to support different features: + +* Multiple accounts +* Multi-stage authentication/authorization schemes +* Tighter control over token chains and their storage +* Talking to complex APIs and individually authorized microservices +* Token lifetime management, 'offline mode' and resilience in face of network failures +* Token and claims/entitlements validation +* Caching of some API results +* XBox magic +* Mojang magic +* Generally, magic that you would spend weeks on researching while getting confused by contradictory/incomplete documentation (if any is available) diff --git a/libraries/katabasis/acknowledgements.md b/libraries/katabasis/acknowledgements.md new file mode 100644 index 0000000000..c1c8a3d49e --- /dev/null +++ b/libraries/katabasis/acknowledgements.md @@ -0,0 +1,110 @@ +# O2 library by Akos Polster and contributors + +[The origin of this fork.](https://github.com/pipacs/o2) + +> Copyright (c) 2012, Akos Polster +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> * Redistributions of source code must retain the above copyright notice, this +> list of conditions and the following disclaimer. +> +> * Redistributions in binary form must reproduce the above copyright notice, +> this list of conditions and the following disclaimer in the documentation +> and/or other materials provided with the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# SimpleCrypt by Andre Somers + +Cryptographic methods for Qt. + +> Copyright (c) 2011, Andre Somers +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> * Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> * Redistributions in binary form must reproduce the above copyright +> notice, this list of conditions and the following disclaimer in the +> documentation and/or other materials provided with the distribution. +> * Neither the name of the Rathenau Instituut, Andre Somers nor the +> names of its contributors may be used to endorse or promote products +> derived from this software without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +> DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY +> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Mandeep Sandhu + +Configurable settings storage, Twitter XAuth specialization, new demos, cleanups. + +> "Hi Akos, +> +> I'm writing this mail to confirm that my contributions to the O2 library, available here https://github.com/pipacs/o2, can be freely distributed according to the project's license (as shown in the LICENSE file). +> +> Regards, +> -mandeep" + +# Sergey Gavrushkin + +FreshBooks specialization + +# Theofilos Intzoglou + +Hubic specialization + +# Dimitar + +SurveyMonkey specialization + +# David Brooks + +CMake related fixes and improvements. + +# Lukas Vogel + +Spotify support + +# Alan Garny + +Windows DLL build support + +# MartinMikita + +Bug fixes + +# Larry Shaffer + +Versioning, shared lib, install target and header support + +# Gilmanov Ildar + +Bug fixes, support for ```qml``` module + +# Fabian Vogt + +Bug fixes, support for building without Qt keywords enabled + diff --git a/libraries/katabasis/include/katabasis/Bits.h b/libraries/katabasis/include/katabasis/Bits.h new file mode 100644 index 0000000000..3fd2f5304f --- /dev/null +++ b/libraries/katabasis/include/katabasis/Bits.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace Katabasis { +enum class Activity { + Idle, + LoggingIn, + LoggingOut, + Refreshing +}; + +enum class Validity { + None, + Assumed, + Certain +}; + +struct Token { + QDateTime issueInstant; + QDateTime notAfter; + QString token; + QString refresh_token; + QVariantMap extra; + + Validity validity = Validity::None; + bool persistent = true; +}; + +} diff --git a/libraries/katabasis/include/katabasis/Globals.h b/libraries/katabasis/include/katabasis/Globals.h new file mode 100644 index 0000000000..512745d3c7 --- /dev/null +++ b/libraries/katabasis/include/katabasis/Globals.h @@ -0,0 +1,59 @@ +#pragma once + +namespace Katabasis { + +// Common constants +const char ENCRYPTION_KEY[] = "12345678"; +const char MIME_TYPE_XFORM[] = "application/x-www-form-urlencoded"; +const char MIME_TYPE_JSON[] = "application/json"; + +// OAuth 1/1.1 Request Parameters +const char OAUTH_CALLBACK[] = "oauth_callback"; +const char OAUTH_CONSUMER_KEY[] = "oauth_consumer_key"; +const char OAUTH_NONCE[] = "oauth_nonce"; +const char OAUTH_SIGNATURE[] = "oauth_signature"; +const char OAUTH_SIGNATURE_METHOD[] = "oauth_signature_method"; +const char OAUTH_TIMESTAMP[] = "oauth_timestamp"; +const char OAUTH_VERSION[] = "oauth_version"; +// OAuth 1/1.1 Response Parameters +const char OAUTH_TOKEN[] = "oauth_token"; +const char OAUTH_TOKEN_SECRET[] = "oauth_token_secret"; +const char OAUTH_CALLBACK_CONFIRMED[] = "oauth_callback_confirmed"; +const char OAUTH_VERFIER[] = "oauth_verifier"; + +// OAuth 2 Request Parameters +const char OAUTH2_RESPONSE_TYPE[] = "response_type"; +const char OAUTH2_CLIENT_ID[] = "client_id"; +const char OAUTH2_CLIENT_SECRET[] = "client_secret"; +const char OAUTH2_USERNAME[] = "username"; +const char OAUTH2_PASSWORD[] = "password"; +const char OAUTH2_REDIRECT_URI[] = "redirect_uri"; +const char OAUTH2_SCOPE[] = "scope"; +const char OAUTH2_GRANT_TYPE_CODE[] = "code"; +const char OAUTH2_GRANT_TYPE_TOKEN[] = "token"; +const char OAUTH2_GRANT_TYPE_PASSWORD[] = "password"; +const char OAUTH2_GRANT_TYPE_DEVICE[] = "urn:ietf:params:oauth:grant-type:device_code"; +const char OAUTH2_GRANT_TYPE[] = "grant_type"; +const char OAUTH2_API_KEY[] = "api_key"; +const char OAUTH2_STATE[] = "state"; +const char OAUTH2_CODE[] = "code"; + +// OAuth 2 Response Parameters +const char OAUTH2_ACCESS_TOKEN[] = "access_token"; +const char OAUTH2_REFRESH_TOKEN[] = "refresh_token"; +const char OAUTH2_EXPIRES_IN[] = "expires_in"; +const char OAUTH2_DEVICE_CODE[] = "device_code"; +const char OAUTH2_USER_CODE[] = "user_code"; +const char OAUTH2_VERIFICATION_URI[] = "verification_uri"; +const char OAUTH2_VERIFICATION_URL[] = "verification_url"; // Google sign-in +const char OAUTH2_VERIFICATION_URI_COMPLETE[] = "verification_uri_complete"; +const char OAUTH2_INTERVAL[] = "interval"; + +// Parameter values +const char AUTHORIZATION_CODE[] = "authorization_code"; + +// Standard HTTP headers +const char HTTP_HTTP_HEADER[] = "HTTP"; +const char HTTP_AUTHORIZATION_HEADER[] = "Authorization"; + +} diff --git a/libraries/katabasis/include/katabasis/OAuth2.h b/libraries/katabasis/include/katabasis/OAuth2.h new file mode 100644 index 0000000000..9dbe5c7147 --- /dev/null +++ b/libraries/katabasis/include/katabasis/OAuth2.h @@ -0,0 +1,233 @@ +#pragma once + +#include +#include +#include +#include + +#include "Reply.h" +#include "RequestParameter.h" +#include "Bits.h" + +namespace Katabasis { + +class ReplyServer; +class PollServer; + + +/* + * FIXME: this is not as simple as it should be. it squishes 4 different grant flows into one big ball of mud + * This serves no practical purpose and simply makes the code less readable / maintainable. + * + * Therefore: Split this into the 4 different OAuth2 flows that people can use as authentication steps. Write tests/examples for all of them. + */ + +/// Simple OAuth2 authenticator. +class OAuth2: public QObject +{ + Q_OBJECT +public: + Q_ENUMS(GrantFlow) + +public: + + struct Options { + QString userAgent = QStringLiteral("Katabasis/1.0"); + QString redirectionUrl = QStringLiteral("http://localhost:%1"); + QString responseType = QStringLiteral("code"); + QString scope; + QString clientIdentifier; + QString clientSecret; + QUrl authorizationUrl; + QUrl accessTokenUrl; + QVector listenerPorts = { 0 }; + }; + + /// Authorization flow types. + enum GrantFlow { + GrantFlowAuthorizationCode, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1 + GrantFlowImplicit, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.2 + GrantFlowResourceOwnerPasswordCredentials, + GrantFlowDevice ///< @see https://tools.ietf.org/html/rfc8628#section-1 + }; + + /// Authorization flow. + GrantFlow grantFlow(); + void setGrantFlow(GrantFlow value); + +public: + /// Are we authenticated? + bool linked(); + + /// Authentication token. + QString token(); + + /// Provider-specific extra tokens, available after a successful authentication + QVariantMap extraTokens(); + + /// Page content on local host after successful oauth. + /// Provide it in case you do not want to close the browser, but display something + QByteArray replyContent() const; + void setReplyContent(const QByteArray &value); + +public: + + // TODO: remove + /// Resource owner username. + /// instances with the same (username, password) share the same "linked" and "token" properties. + QString username(); + void setUsername(const QString &value); + + // TODO: remove + /// Resource owner password. + /// instances with the same (username, password) share the same "linked" and "token" properties. + QString password(); + void setPassword(const QString &value); + + // TODO: remove + /// API key. + QString apiKey(); + void setApiKey(const QString &value); + + // TODO: remove + /// Allow ignoring SSL errors? + /// E.g. SurveyMonkey fails on Mac due to SSL error. Ignoring the error circumvents the problem + bool ignoreSslErrors(); + void setIgnoreSslErrors(bool ignoreSslErrors); + + // TODO: put in `Options` + /// User-defined extra parameters to append to request URL + QVariantMap extraRequestParams(); + void setExtraRequestParams(const QVariantMap &value); + + // TODO: split up the class into multiple, each implementing one OAuth2 flow + /// Grant type (if non-standard) + QString grantType(); + void setGrantType(const QString &value); + +public: + /// Constructor. + /// @param parent Parent object. + explicit OAuth2(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0); + + /// Get refresh token. + QString refreshToken(); + + /// Get token expiration time + QDateTime expires(); + +public slots: + /// Authenticate. + virtual void link(); + + /// De-authenticate. + virtual void unlink(); + + /// Refresh token. + bool refresh(); + + /// Handle situation where reply server has opted to close its connection + void serverHasClosed(bool paramsfound = false); + +signals: + /// Emitted when a token refresh has been completed or failed. + void refreshFinished(QNetworkReply::NetworkError error); + + /// Emitted when client needs to open a web browser window, with the given URL. + void openBrowser(const QUrl &url); + + /// Emitted when client can close the browser window. + void closeBrowser(); + + /// Emitted when client needs to show a verification uri and user code + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + + /// Emitted when authentication/deauthentication succeeded. + void linkingSucceeded(); + + /// Emitted when authentication/deauthentication failed. + void linkingFailed(); + + void activityChanged(Activity activity); + +public slots: + /// Handle verification response. + virtual void onVerificationReceived(QMap); + +protected slots: + /// Handle completion of a token request. + virtual void onTokenReplyFinished(); + + /// Handle failure of a token request. + virtual void onTokenReplyError(QNetworkReply::NetworkError error); + + /// Handle completion of a refresh request. + virtual void onRefreshFinished(); + + /// Handle failure of a refresh request. + virtual void onRefreshError(QNetworkReply::NetworkError error); + + /// Handle completion of a Device Authorization Request + virtual void onDeviceAuthReplyFinished(); + +protected: + /// Build HTTP request body. + QByteArray buildRequestBody(const QMap ¶meters); + + /// Set refresh token. + void setRefreshToken(const QString &v); + + /// Set token expiration time. + void setExpires(QDateTime v); + + /// Start polling authorization server + void startPollServer(const QVariantMap ¶ms, int expiresIn); + + /// Set authentication token. + void setToken(const QString &v); + + /// Set the linked state + void setLinked(bool v); + + /// Set extra tokens found in OAuth response + void setExtraTokens(QVariantMap extraTokens); + + /// Set local reply server + void setReplyServer(ReplyServer *server); + + ReplyServer * replyServer() const; + + /// Set local poll server + void setPollServer(PollServer *server); + + PollServer * pollServer() const; + + void updateActivity(Activity activity); + +protected: + QString username_; + QString password_; + + Options options_; + + QVariantMap extraReqParams_; + QString apiKey_; + QNetworkAccessManager *manager_ = nullptr; + ReplyList timedReplies_; + GrantFlow grantFlow_; + QString grantType_; + +protected: + QString redirectUri_; + Token &token_; + + // this should be part of the reply server impl + QByteArray replyContent_; + +private: + ReplyServer *replyServer_ = nullptr; + PollServer *pollServer_ = nullptr; + Activity activity_ = Activity::Idle; +}; + +} diff --git a/libraries/katabasis/include/katabasis/PollServer.h b/libraries/katabasis/include/katabasis/PollServer.h new file mode 100644 index 0000000000..77103867c0 --- /dev/null +++ b/libraries/katabasis/include/katabasis/PollServer.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class QNetworkAccessManager; + +namespace Katabasis { + +/// Poll an authorization server for token +class PollServer : public QObject +{ + Q_OBJECT + +public: + explicit PollServer(QNetworkAccessManager * manager, const QNetworkRequest &request, const QByteArray & payload, int expiresIn, QObject *parent = 0); + + /// Seconds to wait between polling requests + Q_PROPERTY(int interval READ interval WRITE setInterval) + int interval() const; + void setInterval(int interval); + +signals: + void verificationReceived(QMap); + void serverClosed(bool); // whether it has found parameters + +public slots: + void startPolling(); + +protected slots: + void onPollTimeout(); + void onExpiration(); + void onReplyFinished(); + +protected: + QNetworkAccessManager *manager_; + const QNetworkRequest request_; + const QByteArray payload_; + const int expiresIn_; + QTimer expirationTimer; + QTimer pollTimer; +}; + +} diff --git a/libraries/katabasis/include/katabasis/Reply.h b/libraries/katabasis/include/katabasis/Reply.h new file mode 100644 index 0000000000..3af1d49f0d --- /dev/null +++ b/libraries/katabasis/include/katabasis/Reply.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Katabasis { + +/// A network request/reply pair that can time out. +class Reply: public QTimer { + Q_OBJECT + +public: + Reply(QNetworkReply *reply, int timeOut = 60 * 1000, QObject *parent = 0); + +signals: + void error(QNetworkReply::NetworkError); + +public slots: + /// When time out occurs, the QNetworkReply's error() signal is triggered. + void onTimeOut(); + +public: + QNetworkReply *reply; +}; + +/// List of O2Replies. +class ReplyList { +public: + ReplyList() { ignoreSslErrors_ = false; } + + /// Destructor. + /// Deletes all O2Reply instances in the list. + virtual ~ReplyList(); + + /// Create a new O2Reply from a QNetworkReply, and add it to this list. + void add(QNetworkReply *reply); + + /// Add an O2Reply to the list, while taking ownership of it. + void add(Reply *reply); + + /// Remove item from the list that corresponds to a QNetworkReply. + void remove(QNetworkReply *reply); + + /// Find an O2Reply in the list, corresponding to a QNetworkReply. + /// @return Matching O2Reply or NULL. + Reply *find(QNetworkReply *reply); + + bool ignoreSslErrors(); + void setIgnoreSslErrors(bool ignoreSslErrors); + +protected: + QList replies_; + bool ignoreSslErrors_; +}; + +} diff --git a/libraries/katabasis/include/katabasis/ReplyServer.h b/libraries/katabasis/include/katabasis/ReplyServer.h new file mode 100644 index 0000000000..bf47df6930 --- /dev/null +++ b/libraries/katabasis/include/katabasis/ReplyServer.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +namespace Katabasis { + +/// HTTP server to process authentication response. +class ReplyServer: public QTcpServer { + Q_OBJECT + +public: + explicit ReplyServer(QObject *parent = 0); + + /// Page content on local host after successful oauth - in case you do not want to close the browser, but display something + Q_PROPERTY(QByteArray replyContent READ replyContent WRITE setReplyContent) + QByteArray replyContent(); + void setReplyContent(const QByteArray &value); + + /// Seconds to keep listening *after* first response for a callback with token content + Q_PROPERTY(int timeout READ timeout WRITE setTimeout) + int timeout(); + void setTimeout(int timeout); + + /// Maximum number of callback tries to accept, in case some don't have token content (favicons, etc.) + Q_PROPERTY(int callbackTries READ callbackTries WRITE setCallbackTries) + int callbackTries(); + void setCallbackTries(int maxtries); + + QString uniqueState(); + void setUniqueState(const QString &state); + +signals: + void verificationReceived(QMap); + void serverClosed(bool); // whether it has found parameters + +public slots: + void onIncomingConnection(); + void onBytesReady(); + QMap parseQueryParams(QByteArray *data); + void closeServer(QTcpSocket *socket = 0, bool hasparameters = false); + +protected: + QByteArray replyContent_; + int timeout_; + int maxtries_; + int tries_; + QString uniqueState_; +}; + +} diff --git a/libraries/katabasis/include/katabasis/RequestParameter.h b/libraries/katabasis/include/katabasis/RequestParameter.h new file mode 100644 index 0000000000..ca36934a3a --- /dev/null +++ b/libraries/katabasis/include/katabasis/RequestParameter.h @@ -0,0 +1,15 @@ +#pragma once + +namespace Katabasis { + +/// Request parameter (name-value pair) participating in authentication. +struct RequestParameter { + RequestParameter(const QByteArray &n, const QByteArray &v): name(n), value(v) {} + bool operator <(const RequestParameter &other) const { + return (name == other.name)? (value < other.value): (name < other.name); + } + QByteArray name; + QByteArray value; +}; + +} diff --git a/libraries/katabasis/include/katabasis/Requestor.h b/libraries/katabasis/include/katabasis/Requestor.h new file mode 100644 index 0000000000..de8016cbfb --- /dev/null +++ b/libraries/katabasis/include/katabasis/Requestor.h @@ -0,0 +1,79 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include "Reply.h" + +namespace Katabasis { + +class OAuth2; + +/// Makes authenticated requests. +class Requestor: public QObject { + Q_OBJECT + +public: + explicit Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent = 0); + ~Requestor(); + +public slots: + int get(const QNetworkRequest &req, int timeout = 60*1000); + int post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); + + +signals: + + /// Emitted when a request has been completed or failed. + void finished(int id, QNetworkReply::NetworkError error, QByteArray data, QList headers); + + /// Emitted when an upload has progressed. + void uploadProgress(int id, qint64 bytesSent, qint64 bytesTotal); + +protected slots: + /// Handle refresh completion. + void onRefreshFinished(QNetworkReply::NetworkError error); + + /// Handle request finished. + void onRequestFinished(); + + /// Handle request error. + void onRequestError(QNetworkReply::NetworkError error); + + /// Handle ssl errors. + void onSslErrors(QList errors); + + /// Re-try request (after successful token refresh). + void retry(); + + /// Finish the request, emit finished() signal. + void finish(); + + /// Handle upload progress. + void onUploadProgress(qint64 uploaded, qint64 total); + +protected: + int setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); + + enum Status { + Idle, Requesting, ReRequesting + }; + + QNetworkAccessManager *manager_; + OAuth2 *authenticator_; + QNetworkRequest request_; + QByteArray data_; + QNetworkReply *reply_; + Status status_; + int id_; + QNetworkAccessManager::Operation operation_; + QUrl url_; + ReplyList timedReplies_; + QNetworkReply::NetworkError error_; +}; + +} diff --git a/libraries/katabasis/src/JsonResponse.cpp b/libraries/katabasis/src/JsonResponse.cpp new file mode 100644 index 0000000000..63384d1214 --- /dev/null +++ b/libraries/katabasis/src/JsonResponse.cpp @@ -0,0 +1,26 @@ +#include "JsonResponse.h" + +#include +#include +#include +#include + +namespace Katabasis { + +QVariantMap parseJsonResponse(const QByteArray &data) { + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) { + qWarning() << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString(); + return QVariantMap(); + } + + if (!doc.isObject()) { + qWarning() << "parseTokenResponse: Token response is not an object"; + return QVariantMap(); + } + + return doc.object().toVariantMap(); +} + +} diff --git a/libraries/katabasis/src/JsonResponse.h b/libraries/katabasis/src/JsonResponse.h new file mode 100644 index 0000000000..e7fe7e30d3 --- /dev/null +++ b/libraries/katabasis/src/JsonResponse.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class QByteArray; + +namespace Katabasis { + + /// Parse JSON data into a QVariantMap +QVariantMap parseJsonResponse(const QByteArray &data); + +} diff --git a/libraries/katabasis/src/OAuth2.cpp b/libraries/katabasis/src/OAuth2.cpp new file mode 100644 index 0000000000..260aa9c19a --- /dev/null +++ b/libraries/katabasis/src/OAuth2.cpp @@ -0,0 +1,672 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "katabasis/OAuth2.h" +#include "katabasis/PollServer.h" +#include "katabasis/ReplyServer.h" +#include "katabasis/Globals.h" + +#include "JsonResponse.h" + +namespace { +// ref: https://tools.ietf.org/html/rfc8628#section-3.2 +// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both. +bool hasMandatoryDeviceAuthParams(const QVariantMap& params) +{ + if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE)) + return false; + + if (!params.contains(Katabasis::OAUTH2_USER_CODE)) + return false; + + if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL))) + return false; + + if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN)) + return false; + + return true; +} + +QByteArray createQueryParameters(const QList ¶meters) { + QByteArray ret; + bool first = true; + for( auto & h: parameters) { + if (first) { + first = false; + } else { + ret.append("&"); + } + ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value)); + } + return ret; +} +} + +namespace Katabasis { + +OAuth2::OAuth2(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) { + manager_ = manager ? manager : new QNetworkAccessManager(this); + grantFlow_ = GrantFlowAuthorizationCode; + qRegisterMetaType("QNetworkReply::NetworkError"); + options_ = opts; +} + +bool OAuth2::linked() { + return token_.validity != Validity::None; +} +void OAuth2::setLinked(bool v) { + qDebug() << "OAuth2::setLinked:" << (v? "true": "false"); + token_.validity = v ? Validity::Certain : Validity::None; +} + +QString OAuth2::token() { + return token_.token; +} +void OAuth2::setToken(const QString &v) { + token_.token = v; +} + +QByteArray OAuth2::replyContent() const { + return replyContent_; +} + +void OAuth2::setReplyContent(const QByteArray &value) { + replyContent_ = value; + if (replyServer_) { + replyServer_->setReplyContent(replyContent_); + } +} + +QVariantMap OAuth2::extraTokens() { + return token_.extra; +} + +void OAuth2::setExtraTokens(QVariantMap extraTokens) { + token_.extra = extraTokens; +} + +void OAuth2::setReplyServer(ReplyServer * server) +{ + delete replyServer_; + + replyServer_ = server; + replyServer_->setReplyContent(replyContent_); +} + +ReplyServer * OAuth2::replyServer() const +{ + return replyServer_; +} + +void OAuth2::setPollServer(PollServer *server) +{ + if (pollServer_) + pollServer_->deleteLater(); + + pollServer_ = server; +} + +PollServer *OAuth2::pollServer() const +{ + return pollServer_; +} + +OAuth2::GrantFlow OAuth2::grantFlow() { + return grantFlow_; +} + +void OAuth2::setGrantFlow(OAuth2::GrantFlow value) { + grantFlow_ = value; +} + +QString OAuth2::username() { + return username_; +} + +void OAuth2::setUsername(const QString &value) { + username_ = value; +} + +QString OAuth2::password() { + return password_; +} + +void OAuth2::setPassword(const QString &value) { + password_ = value; +} + +QVariantMap OAuth2::extraRequestParams() +{ + return extraReqParams_; +} + +void OAuth2::setExtraRequestParams(const QVariantMap &value) +{ + extraReqParams_ = value; +} + +QString OAuth2::grantType() +{ + if (!grantType_.isEmpty()) + return grantType_; + + switch (grantFlow_) { + case GrantFlowAuthorizationCode: + return OAUTH2_GRANT_TYPE_CODE; + case GrantFlowImplicit: + return OAUTH2_GRANT_TYPE_TOKEN; + case GrantFlowResourceOwnerPasswordCredentials: + return OAUTH2_GRANT_TYPE_PASSWORD; + case GrantFlowDevice: + return OAUTH2_GRANT_TYPE_DEVICE; + } + + return QString(); +} + +void OAuth2::setGrantType(const QString &value) +{ + grantType_ = value; +} + +void OAuth2::updateActivity(Activity activity) +{ + if(activity_ != activity) { + activity_ = activity; + emit activityChanged(activity_); + } +} + +void OAuth2::link() { + qDebug() << "OAuth2::link"; + + // Create the reply server if it doesn't exist + if(replyServer() == NULL) { + ReplyServer * replyServer = new ReplyServer(this); + connect(replyServer, &ReplyServer::verificationReceived, this, &OAuth2::onVerificationReceived); + connect(replyServer, &ReplyServer::serverClosed, this, &OAuth2::serverHasClosed); + setReplyServer(replyServer); + } + + if (linked()) { + qDebug() << "OAuth2::link: Linked already"; + emit linkingSucceeded(); + return; + } + + setLinked(false); + setToken(""); + setExtraTokens(QVariantMap()); + setRefreshToken(QString()); + setExpires(QDateTime()); + + if (grantFlow_ == GrantFlowAuthorizationCode || grantFlow_ == GrantFlowImplicit) { + + QString uniqueState = QUuid::createUuid().toString().remove(QRegExp("([^a-zA-Z0-9]|[-])")); + + // FIXME: this should be part of a 'redirection handler' that would get injected into O2 + { + quint16 foundPort = 0; + // Start listening to authentication replies + if (!replyServer()->isListening()) { + auto ports = options_.listenerPorts; + for(auto & port: ports) { + if (replyServer()->listen(QHostAddress::Any, port)) { + foundPort = replyServer()->serverPort(); + qDebug() << "OAuth2::link: Reply server listening on port " << foundPort; + break; + } + } + if(foundPort == 0) { + qWarning() << "OAuth2::link: Reply server failed to start listening on any port out of " << ports; + emit linkingFailed(); + return; + } + } + + // Save redirect URI, as we have to reuse it when requesting the access token + redirectUri_ = options_.redirectionUrl.arg(foundPort); + replyServer()->setUniqueState(uniqueState); + } + + // Assemble intial authentication URL + QUrl url(options_.authorizationUrl); + QUrlQuery query(url); + QList > parameters; + query.addQueryItem(OAUTH2_RESPONSE_TYPE, (grantFlow_ == GrantFlowAuthorizationCode)? OAUTH2_GRANT_TYPE_CODE: OAUTH2_GRANT_TYPE_TOKEN); + query.addQueryItem(OAUTH2_CLIENT_ID, options_.clientIdentifier); + query.addQueryItem(OAUTH2_REDIRECT_URI, redirectUri_); + query.addQueryItem(OAUTH2_SCOPE, options_.scope.replace( " ", "+" )); + query.addQueryItem(OAUTH2_STATE, uniqueState); + if (!apiKey_.isEmpty()) { + query.addQueryItem(OAUTH2_API_KEY, apiKey_); + } + for(auto iter = extraReqParams_.begin(); iter != extraReqParams_.end(); iter++) { + query.addQueryItem(iter.key(), iter.value().toString()); + } + url.setQuery(query); + + // Show authentication URL with a web browser + qDebug() << "OAuth2::link: Emit openBrowser" << url.toString(); + emit openBrowser(url); + updateActivity(Activity::LoggingIn); + } else if (grantFlow_ == GrantFlowResourceOwnerPasswordCredentials) { + QList parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); + if ( !options_.clientSecret.isEmpty() ) { + parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8())); + } + parameters.append(RequestParameter(OAUTH2_USERNAME, username_.toUtf8())); + parameters.append(RequestParameter(OAUTH2_PASSWORD, password_.toUtf8())); + parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, OAUTH2_GRANT_TYPE_PASSWORD)); + parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8())); + if ( !apiKey_.isEmpty() ) + parameters.append(RequestParameter(OAUTH2_API_KEY, apiKey_.toUtf8())); + foreach (QString key, extraRequestParams().keys()) { + parameters.append(RequestParameter(key.toUtf8(), extraRequestParams().value(key).toByteArray())); + } + QByteArray payload = createQueryParameters(parameters); + + qDebug() << "OAuth2::link: Sending token request for resource owner flow"; + QUrl url(options_.accessTokenUrl); + QNetworkRequest tokenRequest(url); + tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + QNetworkReply *tokenReply = manager_->post(tokenRequest, payload); + + connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection); + connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + updateActivity(Activity::LoggingIn); + } + else if (grantFlow_ == GrantFlowDevice) { + QList parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); + parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8())); + QByteArray payload = createQueryParameters(parameters); + + QUrl url(options_.authorizationUrl); + QNetworkRequest deviceRequest(url); + deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + QNetworkReply *tokenReply = manager_->post(deviceRequest, payload); + + connect(tokenReply, SIGNAL(finished()), this, SLOT(onDeviceAuthReplyFinished()), Qt::QueuedConnection); + connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + updateActivity(Activity::LoggingIn); + } +} + +void OAuth2::unlink() { + qDebug() << "OAuth2::unlink"; + updateActivity(Activity::LoggingOut); + // FIXME: implement logout flows... if they exist + token_ = Token(); + updateActivity(Activity::Idle); +} + +void OAuth2::onVerificationReceived(const QMap response) { + qDebug() << "OAuth2::onVerificationReceived: Emitting closeBrowser()"; + emit closeBrowser(); + + if (response.contains("error")) { + qWarning() << "OAuth2::onVerificationReceived: Verification failed:" << response; + emit linkingFailed(); + updateActivity(Activity::Idle); + return; + } + + if (grantFlow_ == GrantFlowAuthorizationCode) { + // NOTE: access code is temporary and should never be saved anywhere! + auto access_code = response.value(QString(OAUTH2_GRANT_TYPE_CODE)); + + // Exchange access code for access/refresh tokens + QString query; + if(!apiKey_.isEmpty()) + query = QString("?" + QString(OAUTH2_API_KEY) + "=" + apiKey_); + QNetworkRequest tokenRequest(QUrl(options_.accessTokenUrl.toString() + query)); + tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM); + tokenRequest.setRawHeader("Accept", MIME_TYPE_JSON); + QMap parameters; + parameters.insert(OAUTH2_GRANT_TYPE_CODE, access_code); + parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier); + if ( !options_.clientSecret.isEmpty() ) { + parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret); + } + parameters.insert(OAUTH2_REDIRECT_URI, redirectUri_); + parameters.insert(OAUTH2_GRANT_TYPE, AUTHORIZATION_CODE); + QByteArray data = buildRequestBody(parameters); + + qDebug() << QString("OAuth2::onVerificationReceived: Exchange access code data:\n%1").arg(QString(data)); + + QNetworkReply *tokenReply = manager_->post(tokenRequest, data); + timedReplies_.add(tokenReply); + connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection); + connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + } else if (grantFlow_ == GrantFlowImplicit || grantFlow_ == GrantFlowDevice) { + // Check for mandatory tokens + if (response.contains(OAUTH2_ACCESS_TOKEN)) { + qDebug() << "OAuth2::onVerificationReceived: Access token returned for implicit or device flow"; + setToken(response.value(OAUTH2_ACCESS_TOKEN)); + if (response.contains(OAUTH2_EXPIRES_IN)) { + bool ok = false; + int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok); + if (ok) { + qDebug() << "OAuth2::onVerificationReceived: Token expires in" << expiresIn << "seconds"; + setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn)); + } + } + if (response.contains(OAUTH2_REFRESH_TOKEN)) { + setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN)); + } + setLinked(true); + emit linkingSucceeded(); + } else { + qWarning() << "OAuth2::onVerificationReceived: Access token missing from response for implicit or device flow"; + emit linkingFailed(); + } + updateActivity(Activity::Idle); + } else { + setToken(response.value(OAUTH2_ACCESS_TOKEN)); + setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN)); + updateActivity(Activity::Idle); + } +} + +void OAuth2::onTokenReplyFinished() { + qDebug() << "OAuth2::onTokenReplyFinished"; + QNetworkReply *tokenReply = qobject_cast(sender()); + if (!tokenReply) + { + qDebug() << "OAuth2::onTokenReplyFinished: reply is null"; + return; + } + if (tokenReply->error() == QNetworkReply::NoError) { + QByteArray replyData = tokenReply->readAll(); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + //qDebug() << "OAuth2::onTokenReplyFinished: replyData\n"; + //qDebug() << QString( replyData ); + + QVariantMap tokens = parseJsonResponse(replyData); + + // Dump tokens + qDebug() << "OAuth2::onTokenReplyFinished: Tokens returned:\n"; + foreach (QString key, tokens.keys()) { + // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first + qDebug() << key << ": "<< tokens.value( key ).toString(); + } + + // Check for mandatory tokens + if (tokens.contains(OAUTH2_ACCESS_TOKEN)) { + qDebug() << "OAuth2::onTokenReplyFinished: Access token returned"; + setToken(tokens.take(OAUTH2_ACCESS_TOKEN).toString()); + bool ok = false; + int expiresIn = tokens.take(OAUTH2_EXPIRES_IN).toInt(&ok); + if (ok) { + qDebug() << "OAuth2::onTokenReplyFinished: Token expires in" << expiresIn << "seconds"; + setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn)); + } + setRefreshToken(tokens.take(OAUTH2_REFRESH_TOKEN).toString()); + setExtraTokens(tokens); + timedReplies_.remove(tokenReply); + setLinked(true); + emit linkingSucceeded(); + } else { + qWarning() << "OAuth2::onTokenReplyFinished: Access token missing from response"; + emit linkingFailed(); + } + } + tokenReply->deleteLater(); + updateActivity(Activity::Idle); +} + +void OAuth2::onTokenReplyError(QNetworkReply::NetworkError error) { + QNetworkReply *tokenReply = qobject_cast(sender()); + if (!tokenReply) + { + qDebug() << "OAuth2::onTokenReplyError: reply is null"; + } else { + qWarning() << "OAuth2::onTokenReplyError: " << error << ": " << tokenReply->errorString(); + qDebug() << "OAuth2::onTokenReplyError: " << tokenReply->readAll(); + timedReplies_.remove(tokenReply); + } + + setToken(QString()); + setRefreshToken(QString()); + emit linkingFailed(); +} + +QByteArray OAuth2::buildRequestBody(const QMap ¶meters) { + QByteArray body; + bool first = true; + foreach (QString key, parameters.keys()) { + if (first) { + first = false; + } else { + body.append("&"); + } + QString value = parameters.value(key); + body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value)); + } + return body; +} + +QDateTime OAuth2::expires() { + return token_.notAfter; +} +void OAuth2::setExpires(QDateTime v) { + token_.notAfter = v; +} + +void OAuth2::startPollServer(const QVariantMap ¶ms, int expiresIn) +{ + qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds"; + + QUrl url(options_.accessTokenUrl); + QNetworkRequest authRequest(url); + authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString(); + const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_; + + QList parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); + if ( !options_.clientSecret.isEmpty() ) { + parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8())); + } + parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8())); + parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8())); + QByteArray payload = createQueryParameters(parameters); + + PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this); + if (params.contains(OAUTH2_INTERVAL)) { + bool ok = false; + int interval = params[OAUTH2_INTERVAL].toInt(&ok); + if (ok) + pollServer->setInterval(interval); + } + connect(pollServer, SIGNAL(verificationReceived(QMap)), this, SLOT(onVerificationReceived(QMap))); + connect(pollServer, SIGNAL(serverClosed(bool)), this, SLOT(serverHasClosed(bool))); + setPollServer(pollServer); + pollServer->startPolling(); +} + +QString OAuth2::refreshToken() { + return token_.refresh_token; +} +void OAuth2::setRefreshToken(const QString &v) { +#ifndef NDEBUG + qDebug() << "OAuth2::setRefreshToken" << v << "..."; +#endif + token_.refresh_token = v; +} + +bool OAuth2::refresh() { + qDebug() << "OAuth2::refresh: Token: ..." << refreshToken().right(7); + + if (refreshToken().isEmpty()) { + qWarning() << "OAuth2::refresh: No refresh token"; + onRefreshError(QNetworkReply::AuthenticationRequiredError); + return false; + } + if (options_.accessTokenUrl.isEmpty()) { + qWarning() << "OAuth2::refresh: Refresh token URL not set"; + onRefreshError(QNetworkReply::AuthenticationRequiredError); + return false; + } + + updateActivity(Activity::Refreshing); + + QNetworkRequest refreshRequest(options_.accessTokenUrl); + refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM); + QMap parameters; + parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier); + if ( !options_.clientSecret.isEmpty() ) { + parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret); + } + parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken()); + parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN); + + QByteArray data = buildRequestBody(parameters); + QNetworkReply *refreshReply = manager_->post(refreshRequest, data); + timedReplies_.add(refreshReply); + connect(refreshReply, SIGNAL(finished()), this, SLOT(onRefreshFinished()), Qt::QueuedConnection); + connect(refreshReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRefreshError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + return true; +} + +void OAuth2::onRefreshFinished() { + QNetworkReply *refreshReply = qobject_cast(sender()); + + if (refreshReply->error() == QNetworkReply::NoError) { + QByteArray reply = refreshReply->readAll(); + QVariantMap tokens = parseJsonResponse(reply); + setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString()); + setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt())); + QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString(); + if(!refreshToken.isEmpty()) { + setRefreshToken(refreshToken); + } + else { + qDebug() << "No new refresh token. Keep the old one."; + } + timedReplies_.remove(refreshReply); + setLinked(true); + emit linkingSucceeded(); + emit refreshFinished(QNetworkReply::NoError); + qDebug() << "New token expires in" << expires() << "seconds"; + } else { + emit linkingFailed(); + qDebug() << "OAuth2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString(); + } + refreshReply->deleteLater(); + updateActivity(Activity::Idle); +} + +void OAuth2::onRefreshError(QNetworkReply::NetworkError error) { + QNetworkReply *refreshReply = qobject_cast(sender()); + qWarning() << "OAuth2::onRefreshError: " << error; + unlink(); + timedReplies_.remove(refreshReply); + emit refreshFinished(error); +} + +void OAuth2::onDeviceAuthReplyFinished() +{ + qDebug() << "OAuth2::onDeviceAuthReplyFinished"; + QNetworkReply *tokenReply = qobject_cast(sender()); + if (!tokenReply) + { + qDebug() << "OAuth2::onDeviceAuthReplyFinished: reply is null"; + return; + } + if (tokenReply->error() == QNetworkReply::NoError) { + QByteArray replyData = tokenReply->readAll(); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + //qDebug() << "OAuth2::onDeviceAuthReplyFinished: replyData\n"; + //qDebug() << QString( replyData ); + + QVariantMap params = parseJsonResponse(replyData); + + // Dump tokens + qDebug() << "OAuth2::onDeviceAuthReplyFinished: Tokens returned:\n"; + foreach (QString key, params.keys()) { + // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first + qDebug() << key << ": "<< params.value( key ).toString(); + } + + // Check for mandatory parameters + if (hasMandatoryDeviceAuthParams(params)) { + qDebug() << "OAuth2::onDeviceAuthReplyFinished: Device auth request response"; + + const QString userCode = params.take(OAUTH2_USER_CODE).toString(); + QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl(); + if (uri.isEmpty()) + uri = params.take(OAUTH2_VERIFICATION_URL).toUrl(); + + if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) + emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); + + bool ok = false; + int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); + if (!ok) { + qWarning() << "OAuth2::startPollServer: No expired_in parameter"; + emit linkingFailed(); + return; + } + + emit showVerificationUriAndCode(uri, userCode, expiresIn); + + startPollServer(params, expiresIn); + } else { + qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; + emit linkingFailed(); + updateActivity(Activity::Idle); + } + } + tokenReply->deleteLater(); +} + +void OAuth2::serverHasClosed(bool paramsfound) +{ + if ( !paramsfound ) { + // server has probably timed out after receiving first response + emit linkingFailed(); + } + // poll server is not re-used for later auth requests + setPollServer(NULL); +} + +QString OAuth2::apiKey() { + return apiKey_; +} + +void OAuth2::setApiKey(const QString &value) { + apiKey_ = value; +} + +bool OAuth2::ignoreSslErrors() { + return timedReplies_.ignoreSslErrors(); +} + +void OAuth2::setIgnoreSslErrors(bool ignoreSslErrors) { + timedReplies_.setIgnoreSslErrors(ignoreSslErrors); +} + +} diff --git a/libraries/katabasis/src/PollServer.cpp b/libraries/katabasis/src/PollServer.cpp new file mode 100644 index 0000000000..1083c59947 --- /dev/null +++ b/libraries/katabasis/src/PollServer.cpp @@ -0,0 +1,123 @@ +#include +#include + +#include "katabasis/PollServer.h" +#include "JsonResponse.h" + +namespace { +QMap toVerificationParams(const QVariantMap &map) +{ + QMap params; + for (QVariantMap::const_iterator i = map.constBegin(); + i != map.constEnd(); ++i) + { + params[i.key()] = i.value().toString(); + } + return params; +} +} + +namespace Katabasis { + +PollServer::PollServer(QNetworkAccessManager *manager, const QNetworkRequest &request, const QByteArray &payload, int expiresIn, QObject *parent) + : QObject(parent) + , manager_(manager) + , request_(request) + , payload_(payload) + , expiresIn_(expiresIn) +{ + expirationTimer.setTimerType(Qt::VeryCoarseTimer); + expirationTimer.setInterval(expiresIn * 1000); + expirationTimer.setSingleShot(true); + connect(&expirationTimer, SIGNAL(timeout()), this, SLOT(onExpiration())); + expirationTimer.start(); + + pollTimer.setTimerType(Qt::VeryCoarseTimer); + pollTimer.setInterval(5 * 1000); + pollTimer.setSingleShot(true); + connect(&pollTimer, SIGNAL(timeout()), this, SLOT(onPollTimeout())); +} + +int PollServer::interval() const +{ + return pollTimer.interval() / 1000; +} + +void PollServer::setInterval(int interval) +{ + pollTimer.setInterval(interval * 1000); +} + +void PollServer::startPolling() +{ + if (expirationTimer.isActive()) { + pollTimer.start(); + } +} + +void PollServer::onPollTimeout() +{ + qDebug() << "PollServer::onPollTimeout: retrying"; + QNetworkReply * reply = manager_->post(request_, payload_); + connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished())); +} + +void PollServer::onExpiration() +{ + pollTimer.stop(); + emit serverClosed(false); +} + +void PollServer::onReplyFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + + if (!reply) { + qDebug() << "PollServer::onReplyFinished: reply is null"; + return; + } + + QByteArray replyData = reply->readAll(); + QMap params = toVerificationParams(parseJsonResponse(replyData)); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + // qDebug() << "PollServer::onReplyFinished: replyData\n"; + // qDebug() << QString( replyData ); + + if (reply->error() == QNetworkReply::TimeoutError) { + // rfc8628#section-3.2 + // "On encountering a connection timeout, clients MUST unilaterally + // reduce their polling frequency before retrying. The use of an + // exponential backoff algorithm to achieve this, such as doubling the + // polling interval on each such connection timeout, is RECOMMENDED." + setInterval(interval() * 2); + pollTimer.start(); + } + else { + QString error = params.value("error"); + if (error == "slow_down") { + // rfc8628#section-3.2 + // "A variant of 'authorization_pending', the authorization request is + // still pending and polling should continue, but the interval MUST + // be increased by 5 seconds for this and all subsequent requests." + setInterval(interval() + 5); + pollTimer.start(); + } + else if (error == "authorization_pending") { + // keep trying - rfc8628#section-3.2 + // "The authorization request is still pending as the end user hasn't + // yet completed the user-interaction steps (Section 3.3)." + pollTimer.start(); + } + else { + expirationTimer.stop(); + emit serverClosed(true); + // let O2 handle the other cases + emit verificationReceived(params); + } + } + reply->deleteLater(); +} + +} diff --git a/libraries/katabasis/src/Reply.cpp b/libraries/katabasis/src/Reply.cpp new file mode 100644 index 0000000000..775b92021b --- /dev/null +++ b/libraries/katabasis/src/Reply.cpp @@ -0,0 +1,62 @@ +#include +#include + +#include "katabasis/Reply.h" + +namespace Katabasis { + +Reply::Reply(QNetworkReply *r, int timeOut, QObject *parent): QTimer(parent), reply(r) { + setSingleShot(true); + connect(this, SIGNAL(error(QNetworkReply::NetworkError)), reply, SIGNAL(error(QNetworkReply::NetworkError)), Qt::QueuedConnection); + connect(this, SIGNAL(timeout()), this, SLOT(onTimeOut()), Qt::QueuedConnection); + start(timeOut); +} + +void Reply::onTimeOut() { + emit error(QNetworkReply::TimeoutError); +} + +ReplyList::~ReplyList() { + foreach (Reply *timedReply, replies_) { + delete timedReply; + } +} + +void ReplyList::add(QNetworkReply *reply) { + if (reply && ignoreSslErrors()) + reply->ignoreSslErrors(); + add(new Reply(reply)); +} + +void ReplyList::add(Reply *reply) { + replies_.append(reply); +} + +void ReplyList::remove(QNetworkReply *reply) { + Reply *o2Reply = find(reply); + if (o2Reply) { + o2Reply->stop(); + (void)replies_.removeOne(o2Reply); + } +} + +Reply *ReplyList::find(QNetworkReply *reply) { + foreach (Reply *timedReply, replies_) { + if (timedReply->reply == reply) { + return timedReply; + } + } + return 0; +} + +bool ReplyList::ignoreSslErrors() +{ + return ignoreSslErrors_; +} + +void ReplyList::setIgnoreSslErrors(bool ignoreSslErrors) +{ + ignoreSslErrors_ = ignoreSslErrors; +} + +} diff --git a/libraries/katabasis/src/ReplyServer.cpp b/libraries/katabasis/src/ReplyServer.cpp new file mode 100755 index 0000000000..4598b18af3 --- /dev/null +++ b/libraries/katabasis/src/ReplyServer.cpp @@ -0,0 +1,182 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "katabasis/Globals.h" +#include "katabasis/ReplyServer.h" + +namespace Katabasis { + +ReplyServer::ReplyServer(QObject *parent): QTcpServer(parent), + timeout_(15), maxtries_(3), tries_(0) { + qDebug() << "O2ReplyServer: Starting"; + connect(this, SIGNAL(newConnection()), this, SLOT(onIncomingConnection())); + replyContent_ = ""; +} + +void ReplyServer::onIncomingConnection() { + qDebug() << "O2ReplyServer::onIncomingConnection: Receiving..."; + QTcpSocket *socket = nextPendingConnection(); + connect(socket, SIGNAL(readyRead()), this, SLOT(onBytesReady()), Qt::UniqueConnection); + connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); + + // Wait for a bit *after* first response, then close server if no useable data has arrived + // Helps with implicit flow, where a URL fragment may need processed by local user-agent and + // sent as secondary query string callback, or additional requests make it through first, + // like for favicons, etc., before such secondary callbacks are fired + QTimer *timer = new QTimer(socket); + timer->setObjectName("timeoutTimer"); + connect(timer, SIGNAL(timeout()), this, SLOT(closeServer())); + timer->setSingleShot(true); + timer->setInterval(timeout() * 1000); + connect(socket, SIGNAL(readyRead()), timer, SLOT(start())); +} + +void ReplyServer::onBytesReady() { + if (!isListening()) { + // server has been closed, stop processing queued connections + return; + } + qDebug() << "O2ReplyServer::onBytesReady: Processing request"; + // NOTE: on first call, the timeout timer is started + QTcpSocket *socket = qobject_cast(sender()); + if (!socket) { + qWarning() << "O2ReplyServer::onBytesReady: No socket available"; + return; + } + QByteArray reply; + reply.append("HTTP/1.0 200 OK \r\n"); + reply.append("Content-Type: text/html; charset=\"utf-8\"\r\n"); + reply.append(QString("Content-Length: %1\r\n\r\n").arg(replyContent_.size()).toLatin1()); + reply.append(replyContent_); + socket->write(reply); + qDebug() << "O2ReplyServer::onBytesReady: Sent reply"; + + QByteArray data = socket->readAll(); + QMap queryParams = parseQueryParams(&data); + if (queryParams.isEmpty()) { + if (tries_ < maxtries_ ) { + qDebug() << "O2ReplyServer::onBytesReady: No query params found, waiting for more callbacks"; + ++tries_; + return; + } else { + tries_ = 0; + qWarning() << "O2ReplyServer::onBytesReady: No query params found, maximum callbacks received"; + closeServer(socket, false); + return; + } + } + if (!uniqueState_.isEmpty() && !queryParams.contains(QString(OAUTH2_STATE))) { + qDebug() << "O2ReplyServer::onBytesReady: Malicious or service request"; + closeServer(socket, true); + return; // Malicious or service (e.g. favicon.ico) request + } + qDebug() << "O2ReplyServer::onBytesReady: Query params found, closing server"; + closeServer(socket, true); + emit verificationReceived(queryParams); +} + +QMap ReplyServer::parseQueryParams(QByteArray *data) { + qDebug() << "O2ReplyServer::parseQueryParams"; + + //qDebug() << QString("O2ReplyServer::parseQueryParams data:\n%1").arg(QString(*data)); + + QString splitGetLine = QString(*data).split("\r\n").first(); + splitGetLine.remove("GET "); + splitGetLine.remove("HTTP/1.1"); + splitGetLine.remove("\r\n"); + splitGetLine.prepend("http://localhost"); + QUrl getTokenUrl(splitGetLine); + + QList< QPair > tokens; + QUrlQuery query(getTokenUrl); + tokens = query.queryItems(); + QMap queryParams; + QPair tokenPair; + foreach (tokenPair, tokens) { + // FIXME: We are decoding key and value again. This helps with Google OAuth, but is it mandated by the standard? + QString key = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.first.trimmed().toLatin1())); + QString value = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.second.trimmed().toLatin1())); + queryParams.insert(key, value); + } + return queryParams; +} + +void ReplyServer::closeServer(QTcpSocket *socket, bool hasparameters) +{ + if (!isListening()) { + return; + } + + qDebug() << "O2ReplyServer::closeServer: Initiating"; + int port = serverPort(); + + if (!socket && sender()) { + QTimer *timer = qobject_cast(sender()); + if (timer) { + qWarning() << "O2ReplyServer::closeServer: Closing due to timeout"; + timer->stop(); + socket = qobject_cast(timer->parent()); + timer->deleteLater(); + } + } + if (socket) { + QTimer *timer = socket->findChild("timeoutTimer"); + if (timer) { + qDebug() << "O2ReplyServer::closeServer: Stopping socket's timeout timer"; + timer->stop(); + } + socket->disconnectFromHost(); + } + close(); + qDebug() << "O2ReplyServer::closeServer: Closed, no longer listening on port" << port; + emit serverClosed(hasparameters); +} + +QByteArray ReplyServer::replyContent() { + return replyContent_; +} + +void ReplyServer::setReplyContent(const QByteArray &value) { + replyContent_ = value; +} + +int ReplyServer::timeout() +{ + return timeout_; +} + +void ReplyServer::setTimeout(int timeout) +{ + timeout_ = timeout; +} + +int ReplyServer::callbackTries() +{ + return maxtries_; +} + +void ReplyServer::setCallbackTries(int maxtries) +{ + maxtries_ = maxtries; +} + +QString ReplyServer::uniqueState() +{ + return uniqueState_; +} + +void ReplyServer::setUniqueState(const QString &state) +{ + uniqueState_ = state; +} + +} diff --git a/libraries/katabasis/src/Requestor.cpp b/libraries/katabasis/src/Requestor.cpp new file mode 100644 index 0000000000..53d779255e --- /dev/null +++ b/libraries/katabasis/src/Requestor.cpp @@ -0,0 +1,195 @@ +#include + +#include +#include +#include +#include + +#include "katabasis/Requestor.h" +#include "katabasis/OAuth2.h" +#include "katabasis/Globals.h" + +namespace Katabasis { + +Requestor::Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent): QObject(parent), reply_(NULL), status_(Idle) { + manager_ = manager; + authenticator_ = authenticator; + if (authenticator) { + timedReplies_.setIgnoreSslErrors(authenticator->ignoreSslErrors()); + } + qRegisterMetaType("QNetworkReply::NetworkError"); + connect(authenticator, &OAuth2::refreshFinished, this, &Requestor::onRefreshFinished); +} + +Requestor::~Requestor() { +} + +int Requestor::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { + if (-1 == setup(req, QNetworkAccessManager::GetOperation)) { + return -1; + } + reply_ = manager_->get(request_); + timedReplies_.add(new Reply(reply_, timeout)); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); + return id_; +} + +int Requestor::post(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) { + if (-1 == setup(req, QNetworkAccessManager::PostOperation)) { + return -1; + } + data_ = data; + reply_ = manager_->post(request_, data_); + timedReplies_.add(new Reply(reply_, timeout)); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); + connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); + return id_; +} + +void Requestor::onRefreshFinished(QNetworkReply::NetworkError error) { + if (status_ != Requesting) { + qWarning() << "O2Requestor::onRefreshFinished: No pending request"; + return; + } + if (QNetworkReply::NoError == error) { + QTimer::singleShot(100, this, &Requestor::retry); + } else { + error_ = error; + QTimer::singleShot(10, this, &Requestor::finish); + } +} + +void Requestor::onRequestFinished() { + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + if (reply_->error() == QNetworkReply::NoError) { + QTimer::singleShot(10, this, SLOT(finish())); + } +} + +void Requestor::onRequestError(QNetworkReply::NetworkError error) { + qWarning() << "O2Requestor::onRequestError: Error" << (int)error; + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + qWarning() << "O2Requestor::onRequestError: Error string: " << reply_->errorString(); + int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qWarning() << "O2Requestor::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + if ((status_ == Requesting) && (httpStatus == 401)) { + // Call OAuth2::refresh. Note the O2 instance might live in a different thread + if (QMetaObject::invokeMethod(authenticator_, "refresh")) { + return; + } + qCritical() << "O2Requestor::onRequestError: Invoking remote refresh failed"; + } + error_ = error; + QTimer::singleShot(10, this, SLOT(finish())); +} + +void Requestor::onSslErrors(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 Requestor::onUploadProgress(qint64 uploaded, qint64 total) { + if (status_ == Idle) { + qWarning() << "O2Requestor::onUploadProgress: No pending request"; + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + // Restart timeout because request in progress + Reply *o2Reply = timedReplies_.find(reply_); + if(o2Reply) + o2Reply->start(); + emit uploadProgress(id_, uploaded, total); +} + +int Requestor::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation, const QByteArray &verb) { + static int currentId; + + if (status_ != Idle) { + qWarning() << "O2Requestor::setup: Another request pending"; + return -1; + } + + request_ = req; + operation_ = operation; + id_ = currentId++; + url_ = req.url(); + + QUrl url = url_; + request_.setUrl(url); + + if (!verb.isEmpty()) { + request_.setRawHeader(HTTP_HTTP_HEADER, verb); + } + + status_ = Requesting; + error_ = QNetworkReply::NoError; + return id_; +} + +void Requestor::finish() { + QByteArray data; + if (status_ == Idle) { + qWarning() << "O2Requestor::finish: No pending request"; + return; + } + data = reply_->readAll(); + status_ = Idle; + timedReplies_.remove(reply_); + reply_->disconnect(this); + reply_->deleteLater(); + QList headers = reply_->rawHeaderPairs(); + emit finished(id_, error_, data, headers); +} + +void Requestor::retry() { + if (status_ != Requesting) { + qWarning() << "O2Requestor::retry: No pending request"; + return; + } + timedReplies_.remove(reply_); + reply_->disconnect(this); + reply_->deleteLater(); + QUrl url = url_; + request_.setUrl(url); + + status_ = ReRequesting; + switch (operation_) { + case QNetworkAccessManager::GetOperation: + reply_ = manager_->get(request_); + break; + case QNetworkAccessManager::PostOperation: + reply_ = manager_->post(request_, data_); + break; + default: + assert(!"Unspecified operation for request"); + reply_ = manager_->get(request_); + break; + } + timedReplies_.add(reply_); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); +} + +} diff --git a/libraries/launcher/org/multimc/LegacyFrame.java b/libraries/launcher/org/multimc/LegacyFrame.java index c72c053e18..985a10e6ab 100644 --- a/libraries/launcher/org/multimc/LegacyFrame.java +++ b/libraries/launcher/org/multimc/LegacyFrame.java @@ -46,7 +46,16 @@ public LegacyFrame(String title) this.addWindowListener ( this ); } - public void start ( Applet mcApplet, String user, String session, int winSizeW, int winSizeH, boolean maximize ) + public void start ( + Applet mcApplet, + String user, + String session, + int winSizeW, + int winSizeH, + boolean maximize, + String serverAddress, + String serverPort + ) { try { appletWrap = new Launcher( mcApplet, new URL ( "http://www.minecraft.net/game" ) ); @@ -95,6 +104,13 @@ public void start ( Applet mcApplet, String user, String session, int winSizeW, e.printStackTrace(System.err); System.exit(-1); } + + if (serverAddress != null) + { + appletWrap.setParameter("server", serverAddress); + appletWrap.setParameter("port", serverPort); + } + appletWrap.setParameter ( "username", user ); appletWrap.setParameter ( "sessionid", session ); appletWrap.setParameter ( "stand-alone", "true" ); // Show the quit button. diff --git a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java index b6b384ab4a..ea445995eb 100644 --- a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java @@ -47,6 +47,9 @@ public class OneSixLauncher implements Launcher private boolean maximize; private String cwd; + private String serverAddress; + private String serverPort; + // the much abused system classloader, for convenience (for further abuse) private ClassLoader cl; @@ -64,6 +67,9 @@ private void processParams(ParamBucket params) throws NotFoundException windowTitle = params.firstSafe("windowTitle", "Minecraft"); windowParams = params.firstSafe("windowParams", "854x480"); + serverAddress = params.firstSafe("serverAddress", null); + serverPort = params.firstSafe("serverPort", null); + cwd = System.getProperty("user.dir"); winSizeW = 854; @@ -122,7 +128,7 @@ int legacyLaunch() Class MCAppletClass = cl.loadClass(appletClass); Applet mcappl = (Applet) MCAppletClass.newInstance(); LegacyFrame mcWindow = new LegacyFrame(windowTitle); - mcWindow.start(mcappl, userName, sessionId, winSizeW, winSizeH, maximize); + mcWindow.start(mcappl, userName, sessionId, winSizeW, winSizeH, maximize, serverAddress, serverPort); return 0; } catch (Exception e) { @@ -164,6 +170,14 @@ int launchWithMainClass() mcparams.add(Integer.toString(winSizeH)); } + if (serverAddress != null) + { + mcparams.add("--server"); + mcparams.add(serverAddress); + mcparams.add("--port"); + mcparams.add(serverPort); + } + // Get the Minecraft Class. Class mc; try diff --git a/libraries/systeminfo/include/sys.h b/libraries/systeminfo/include/sys.h index 7980dfdf11..bd6e2486a2 100644 --- a/libraries/systeminfo/include/sys.h +++ b/libraries/systeminfo/include/sys.h @@ -3,11 +3,25 @@ namespace Sys { -const uint64_t megabyte = 1024ull * 1024ull; +const uint64_t mebibyte = 1024ull * 1024ull; + +enum class KernelType { + Undetermined, + Windows, + Darwin, + Linux +}; + struct KernelInfo { QString kernelName; QString kernelVersion; + + KernelType kernelType = KernelType::Undetermined; + int kernelMajor = 0; + int kernelMinor = 0; + int kernelPatch = 0; + bool isCursed = false; }; KernelInfo getKernelInfo(); diff --git a/libraries/systeminfo/src/sys_apple.cpp b/libraries/systeminfo/src/sys_apple.cpp index 4bcffae4af..6353b747b7 100644 --- a/libraries/systeminfo/src/sys_apple.cpp +++ b/libraries/systeminfo/src/sys_apple.cpp @@ -2,13 +2,40 @@ #include +#include +#include +#include + Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; struct utsname buf; uname(&buf); + out.kernelType = KernelType::Darwin; out.kernelName = buf.sysname; - out.kernelVersion = buf.release; + QString release = out.kernelVersion = buf.release; + + // TODO: figure out how to detect cursed-ness (macOS emulated on linux via mad hacks and so on) + out.isCursed = false; + + out.kernelMajor = 0; + out.kernelMinor = 0; + out.kernelPatch = 0; + auto sections = release.split('-'); + if(sections.size() >= 1) { + auto versionParts = sections[0].split('.'); + if(versionParts.size() >= 3) { + out.kernelMajor = versionParts[0].toInt(); + out.kernelMinor = versionParts[1].toInt(); + out.kernelPatch = versionParts[2].toInt(); + } + else { + qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); + } + } + else { + qWarning() << "Not enough '-' sections in " << release << " found " << sections.size(); + } return out; } diff --git a/libraries/systeminfo/src/sys_unix.cpp b/libraries/systeminfo/src/sys_unix.cpp index ab3f302eb8..fb96c72c42 100644 --- a/libraries/systeminfo/src/sys_unix.cpp +++ b/libraries/systeminfo/src/sys_unix.cpp @@ -4,14 +4,43 @@ #include #include +#include + +#include +#include +#include Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; struct utsname buf; uname(&buf); + // NOTE: we assume linux here. this needs further elaboration + out.kernelType = KernelType::Linux; out.kernelName = buf.sysname; - out.kernelVersion = buf.release; + QString release = out.kernelVersion = buf.release; + + // linux binary running on WSL is cursed. + out.isCursed = release.contains("WSL", Qt::CaseInsensitive) || release.contains("Microsoft", Qt::CaseInsensitive); + + out.kernelMajor = 0; + out.kernelMinor = 0; + out.kernelPatch = 0; + auto sections = release.split('-'); + if(sections.size() >= 1) { + auto versionParts = sections[0].split('.'); + if(versionParts.size() >= 3) { + out.kernelMajor = versionParts[0].toInt(); + out.kernelMinor = versionParts[1].toInt(); + out.kernelPatch = versionParts[2].toInt(); + } + else { + qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); + } + } + else { + qWarning() << "Not enough '-' sections in " << release << " found " << sections.size(); + } return out; } diff --git a/libraries/systeminfo/src/sys_win32.cpp b/libraries/systeminfo/src/sys_win32.cpp index a750b3a709..430b87e474 100644 --- a/libraries/systeminfo/src/sys_win32.cpp +++ b/libraries/systeminfo/src/sys_win32.cpp @@ -5,12 +5,16 @@ Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; + out.kernelType = KernelType::Windows; out.kernelName = "Windows"; OSVERSIONINFOW osvi; ZeroMemory(&osvi, sizeof(OSVERSIONINFOW)); osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOW); GetVersionExW(&osvi); out.kernelVersion = QString("%1.%2").arg(osvi.dwMajorVersion).arg(osvi.dwMinorVersion); + out.kernelMajor = osvi.dwMajorVersion; + out.kernelMinor = osvi.dwMinorVersion; + out.kernelPatch = osvi.dwBuildNumber; return out; } diff --git a/libraries/tomlc99/CMakeLists.txt b/libraries/tomlc99/CMakeLists.txt new file mode 100644 index 0000000000..60786923c1 --- /dev/null +++ b/libraries/tomlc99/CMakeLists.txt @@ -0,0 +1,10 @@ +project(tomlc99) + +set(tomlc99_SOURCES +include/toml.h +src/toml.c +) + +add_library(tomlc99 STATIC ${tomlc99_SOURCES}) + +target_include_directories(tomlc99 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/libraries/tomlc99/LICENSE b/libraries/tomlc99/LICENSE new file mode 100644 index 0000000000..a3292b1600 --- /dev/null +++ b/libraries/tomlc99/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2017 CK Tan +https://github.com/cktan/tomlc99 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libraries/tomlc99/README.md b/libraries/tomlc99/README.md new file mode 100644 index 0000000000..6715b5be26 --- /dev/null +++ b/libraries/tomlc99/README.md @@ -0,0 +1,194 @@ +# tomlc99 + +TOML in c99; v1.0 compliant. + +If you are looking for a C++ library, you might try this wrapper: [https://github.com/cktan/tomlcpp](https://github.com/cktan/tomlcpp). + +* Compatible with [TOML v1.0.0](https://toml.io/en/v1.0.0). +* Tested with multiple test suites, including +[BurntSushi/toml-test](https://github.com/BurntSushi/toml-test) and +[iarna/toml-spec-tests](https://github.com/iarna/toml-spec-tests). +* Provides very simple and intuitive interface. + + +## Usage + +Please see the `toml.h` file for details. What follows is a simple example that +parses this config file: + +```toml +[server] + host = "www.example.com" + port = [ 8080, 8181, 8282 ] +``` + +The steps for getting values from our file is usually : + +1. Parse the TOML file. +2. Traverse and locate a table in TOML. +3. Extract values from the table. +4. Free up allocated memory. + +Below is an example of parsing the values from the example table. + +```c +#include +#include +#include +#include +#include "toml.h" + +static void error(const char* msg, const char* msg1) +{ + fprintf(stderr, "ERROR: %s%s\n", msg, msg1?msg1:""); + exit(1); +} + + +int main() +{ + FILE* fp; + char errbuf[200]; + + // 1. Read and parse toml file + fp = fopen("sample.toml", "r"); + if (!fp) { + error("cannot open sample.toml - ", strerror(errno)); + } + + toml_table_t* conf = toml_parse_file(fp, errbuf, sizeof(errbuf)); + fclose(fp); + + if (!conf) { + error("cannot parse - ", errbuf); + } + + // 2. Traverse to a table. + toml_table_t* server = toml_table_in(conf, "server"); + if (!server) { + error("missing [server]", ""); + } + + // 3. Extract values + toml_datum_t host = toml_string_in(server, "host"); + if (!host.ok) { + error("cannot read server.host", ""); + } + + toml_array_t* portarray = toml_array_in(server, "port"); + if (!portarray) { + error("cannot read server.port", ""); + } + + printf("host: %s\n", host.u.s); + printf("port: "); + for (int i = 0; ; i++) { + toml_datum_t port = toml_int_at(portarray, i); + if (!port.ok) break; + printf("%d ", (int)port.u.i); + } + printf("\n"); + + // 4. Free memory + free(host.u.s); + toml_free(conf); + return 0; +} +``` + +#### Accessing Table Content + +TOML tables are dictionaries where lookups are done using string keys. In +general, all access functions on tables are named `toml_*_in(...)`. + +In the normal case, you know the key and its content type, and retrievals can be done +using one of these functions: +```c +toml_string_in(tab, key); +toml_bool_in(tab, key); +toml_int_in(tab, key); +toml_double_in(tab, key); +toml_timestamp_in(tab, key); +toml_table_in(tab, key); +toml_array_in(tab, key); +``` + +You can also interrogate the keys in a table using an integer index: +```c +toml_table_t* tab = toml_parse_file(...); +for (int i = 0; ; i++) { + const char* key = toml_key_in(tab, i); + if (!key) break; + printf("key %d: %s\n", i, key); +} +``` + +#### Accessing Array Content + +TOML arrays can be deref-ed using integer indices. In general, all access methods on arrays are named `toml_*_at()`. + +To obtain the size of an array: +```c +int size = toml_array_nelem(arr); +``` + +To obtain the content of an array, use a valid index and call one of these functions: +```c +toml_string_at(arr, idx); +toml_bool_at(arr, idx); +toml_int_at(arr, idx); +toml_double_at(arr, idx); +toml_timestamp_at(arr, idx); +toml_table_at(arr, idx); +toml_array_at(arr, idx); +``` + +#### toml_datum_t + +Some `toml_*_at` and `toml_*_in` functions return a toml_datum_t +structure. The `ok` flag in the structure indicates if the function +call was successful. If so, you may proceed to read the value +corresponding to the type of the content. + +For example: +``` +toml_datum_t host = toml_string_in(tab, "host"); +if (host.ok) { + printf("host: %s\n", host.u.s); + free(host.u.s); /* FREE applies to string and timestamp types only */ +} +``` + +** IMPORTANT: if the accessed value is a string or a timestamp, you must call `free(datum.u.s)` or `free(datum.u.ts)` respectively after usage. ** + +## Building and installing + +A normal *make* suffices. You can also simply include the +`toml.c` and `toml.h` files in your project. + +Invoking `make install` will install the header and library files into +/usr/local/{include,lib}. + +Alternatively, specify `make install prefix=/a/file/path` to install into +/a/file/path/{include,lib}. + +## Testing + +To test against the standard test set provided by BurntSushi/toml-test: + +```sh +% make +% cd test1 +% bash build.sh # do this once +% bash run.sh # this will run the test suite +``` + + +To test against the standard test set provided by iarna/toml: + +```sh +% make +% cd test2 +% bash build.sh # do this once +% bash run.sh # this will run the test suite +``` diff --git a/libraries/tomlc99/include/toml.h b/libraries/tomlc99/include/toml.h new file mode 100644 index 0000000000..b91ef89030 --- /dev/null +++ b/libraries/tomlc99/include/toml.h @@ -0,0 +1,175 @@ +/* + MIT License + + Copyright (c) 2017 - 2019 CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#ifndef TOML_H +#define TOML_H + + +#include +#include + + +#ifdef __cplusplus +#define TOML_EXTERN extern "C" +#else +#define TOML_EXTERN extern +#endif + +typedef struct toml_timestamp_t toml_timestamp_t; +typedef struct toml_table_t toml_table_t; +typedef struct toml_array_t toml_array_t; +typedef struct toml_datum_t toml_datum_t; + +/* Parse a file. Return a table on success, or 0 otherwise. + * Caller must toml_free(the-return-value) after use. + */ +TOML_EXTERN toml_table_t* toml_parse_file(FILE* fp, + char* errbuf, + int errbufsz); + +/* Parse a string containing the full config. + * Return a table on success, or 0 otherwise. + * Caller must toml_free(the-return-value) after use. + */ +TOML_EXTERN toml_table_t* toml_parse(char* conf, /* NUL terminated, please. */ + char* errbuf, + int errbufsz); + +/* Free the table returned by toml_parse() or toml_parse_file(). Once + * this function is called, any handles accessed through this tab + * directly or indirectly are no longer valid. + */ +TOML_EXTERN void toml_free(toml_table_t* tab); + + +/* Timestamp types. The year, month, day, hour, minute, second, z + * fields may be NULL if they are not relevant. e.g. In a DATE + * type, the hour, minute, second and z fields will be NULLs. + */ +struct toml_timestamp_t { + struct { /* internal. do not use. */ + int year, month, day; + int hour, minute, second, millisec; + char z[10]; + } __buffer; + int *year, *month, *day; + int *hour, *minute, *second, *millisec; + char* z; +}; + + +/*----------------------------------------------------------------- + * Enhanced access methods + */ +struct toml_datum_t { + int ok; + union { + toml_timestamp_t* ts; /* ts must be freed after use */ + char* s; /* string value. s must be freed after use */ + int b; /* bool value */ + int64_t i; /* int value */ + double d; /* double value */ + } u; +}; + +/* on arrays: */ +/* ... retrieve size of array. */ +TOML_EXTERN int toml_array_nelem(const toml_array_t* arr); +/* ... retrieve values using index. */ +TOML_EXTERN toml_datum_t toml_string_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_bool_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_int_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_double_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx); +/* ... retrieve array or table using index. */ +TOML_EXTERN toml_array_t* toml_array_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_table_t* toml_table_at(const toml_array_t* arr, int idx); + +/* on tables: */ +/* ... retrieve the key in table at keyidx. Return 0 if out of range. */ +TOML_EXTERN const char* toml_key_in(const toml_table_t* tab, int keyidx); +/* ... retrieve values using key. */ +TOML_EXTERN toml_datum_t toml_string_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_int_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_double_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key); +/* .. retrieve array or table using key. */ +TOML_EXTERN toml_array_t* toml_array_in(const toml_table_t* tab, + const char* key); +TOML_EXTERN toml_table_t* toml_table_in(const toml_table_t* tab, + const char* key); + +/*----------------------------------------------------------------- + * lesser used + */ +/* Return the array kind: 't'able, 'a'rray, 'v'alue, 'm'ixed */ +TOML_EXTERN char toml_array_kind(const toml_array_t* arr); + +/* For array kind 'v'alue, return the type of values + i:int, d:double, b:bool, s:string, t:time, D:date, T:timestamp, 'm'ixed + 0 if unknown +*/ +TOML_EXTERN char toml_array_type(const toml_array_t* arr); + +/* Return the key of an array */ +TOML_EXTERN const char* toml_array_key(const toml_array_t* arr); + +/* Return the number of key-values in a table */ +TOML_EXTERN int toml_table_nkval(const toml_table_t* tab); + +/* Return the number of arrays in a table */ +TOML_EXTERN int toml_table_narr(const toml_table_t* tab); + +/* Return the number of sub-tables in a table */ +TOML_EXTERN int toml_table_ntab(const toml_table_t* tab); + +/* Return the key of a table*/ +TOML_EXTERN const char* toml_table_key(const toml_table_t* tab); + +/*-------------------------------------------------------------- + * misc + */ +TOML_EXTERN int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret); +TOML_EXTERN int toml_ucs_to_utf8(int64_t code, char buf[6]); +TOML_EXTERN void toml_set_memutil(void* (*xxmalloc)(size_t), + void (*xxfree)(void*)); + + +/*-------------------------------------------------------------- + * deprecated + */ +/* A raw value, must be processed by toml_rto* before using. */ +typedef const char* toml_raw_t; +TOML_EXTERN toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key); +TOML_EXTERN toml_raw_t toml_raw_at(const toml_array_t* arr, int idx); +TOML_EXTERN int toml_rtos(toml_raw_t s, char** ret); +TOML_EXTERN int toml_rtob(toml_raw_t s, int* ret); +TOML_EXTERN int toml_rtoi(toml_raw_t s, int64_t* ret); +TOML_EXTERN int toml_rtod(toml_raw_t s, double* ret); +TOML_EXTERN int toml_rtod_ex(toml_raw_t s, double* ret, char* buf, int buflen); +TOML_EXTERN int toml_rtots(toml_raw_t s, toml_timestamp_t* ret); + + +#endif /* TOML_H */ diff --git a/libraries/tomlc99/src/toml.c b/libraries/tomlc99/src/toml.c new file mode 100644 index 0000000000..e46e62e629 --- /dev/null +++ b/libraries/tomlc99/src/toml.c @@ -0,0 +1,2300 @@ +/* + + MIT License + + Copyright (c) 2017 - 2021 CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +*/ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include "toml.h" + + +static void* (*ppmalloc)(size_t) = malloc; +static void (*ppfree)(void*) = free; + +void toml_set_memutil(void* (*xxmalloc)(size_t), + void (*xxfree)(void*)) +{ + if (xxmalloc) ppmalloc = xxmalloc; + if (xxfree) ppfree = xxfree; +} + + +#define MALLOC(a) ppmalloc(a) +#define FREE(a) ppfree(a) + +static void* CALLOC(size_t nmemb, size_t sz) +{ + int nb = sz * nmemb; + void* p = MALLOC(nb); + if (p) { + memset(p, 0, nb); + } + return p; +} + + +static char* STRDUP(const char* s) +{ + int len = strlen(s); + char* p = MALLOC(len+1); + if (p) { + memcpy(p, s, len); + p[len] = 0; + } + return p; +} + +static char* STRNDUP(const char* s, size_t n) +{ + size_t len = strnlen(s, n); + char* p = MALLOC(len+1); + if (p) { + memcpy(p, s, len); + p[len] = 0; + } + return p; +} + + + +/** + * Convert a char in utf8 into UCS, and store it in *ret. + * Return #bytes consumed or -1 on failure. + */ +int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret) +{ + const unsigned char* buf = (const unsigned char*) orig; + unsigned i = *buf++; + int64_t v; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (0 == (i >> 7)) { + if (len < 1) return -1; + v = i; + return *ret = v, 1; + } + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (0x6 == (i >> 5)) { + if (len < 2) return -1; + v = i & 0x1f; + for (int j = 0; j < 1; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (0xE == (i >> 4)) { + if (len < 3) return -1; + v = i & 0x0F; + for (int j = 0; j < 2; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x1E == (i >> 3)) { + if (len < 4) return -1; + v = i & 0x07; + for (int j = 0; j < 3; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x3E == (i >> 2)) { + if (len < 5) return -1; + v = i & 0x03; + for (int j = 0; j < 4; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x7e == (i >> 1)) { + if (len < 6) return -1; + v = i & 0x01; + for (int j = 0; j < 5; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + return -1; +} + + +/** + * Convert a UCS char to utf8 code, and return it in buf. + * Return #bytes used in buf to encode the char, or + * -1 on error. + */ +int toml_ucs_to_utf8(int64_t code, char buf[6]) +{ + /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16 */ + /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well + * as 0xfffe and 0xffff (UCS noncharacters) should not appear in + * conforming UTF-8 streams. + */ + if (0xd800 <= code && code <= 0xdfff) return -1; + if (0xfffe <= code && code <= 0xffff) return -1; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (code < 0) return -1; + if (code <= 0x7F) { + buf[0] = (unsigned char) code; + return 1; + } + + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (code <= 0x000007FF) { + buf[0] = 0xc0 | (code >> 6); + buf[1] = 0x80 | (code & 0x3f); + return 2; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x0000FFFF) { + buf[0] = 0xe0 | (code >> 12); + buf[1] = 0x80 | ((code >> 6) & 0x3f); + buf[2] = 0x80 | (code & 0x3f); + return 3; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x001FFFFF) { + buf[0] = 0xf0 | (code >> 18); + buf[1] = 0x80 | ((code >> 12) & 0x3f); + buf[2] = 0x80 | ((code >> 6) & 0x3f); + buf[3] = 0x80 | (code & 0x3f); + return 4; + } + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x03FFFFFF) { + buf[0] = 0xf8 | (code >> 24); + buf[1] = 0x80 | ((code >> 18) & 0x3f); + buf[2] = 0x80 | ((code >> 12) & 0x3f); + buf[3] = 0x80 | ((code >> 6) & 0x3f); + buf[4] = 0x80 | (code & 0x3f); + return 5; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x7FFFFFFF) { + buf[0] = 0xfc | (code >> 30); + buf[1] = 0x80 | ((code >> 24) & 0x3f); + buf[2] = 0x80 | ((code >> 18) & 0x3f); + buf[3] = 0x80 | ((code >> 12) & 0x3f); + buf[4] = 0x80 | ((code >> 6) & 0x3f); + buf[5] = 0x80 | (code & 0x3f); + return 6; + } + + return -1; +} + +/* + * TOML has 3 data structures: value, array, table. + * Each of them can have identification key. + */ +typedef struct toml_keyval_t toml_keyval_t; +struct toml_keyval_t { + const char* key; /* key to this value */ + const char* val; /* the raw value */ +}; + +typedef struct toml_arritem_t toml_arritem_t; +struct toml_arritem_t { + int valtype; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, 'D'ate, 'T'imestamp */ + char* val; + toml_array_t* arr; + toml_table_t* tab; +}; + + +struct toml_array_t { + const char* key; /* key to this array */ + int kind; /* element kind: 'v'alue, 'a'rray, or 't'able, 'm'ixed */ + int type; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, 'D'ate, 'T'imestamp, 'm'ixed */ + + int nitem; /* number of elements */ + toml_arritem_t* item; +}; + + +struct toml_table_t { + const char* key; /* key to this table */ + bool implicit; /* table was created implicitly */ + bool readonly; /* no more modification allowed */ + + /* key-values in the table */ + int nkval; + toml_keyval_t** kval; + + /* arrays in the table */ + int narr; + toml_array_t** arr; + + /* tables in the table */ + int ntab; + toml_table_t** tab; +}; + + +static inline void xfree(const void* x) { if (x) FREE((void*)(intptr_t)x); } + + +enum tokentype_t { + INVALID, + DOT, + COMMA, + EQUAL, + LBRACE, + RBRACE, + NEWLINE, + LBRACKET, + RBRACKET, + STRING, +}; +typedef enum tokentype_t tokentype_t; + +typedef struct token_t token_t; +struct token_t { + tokentype_t tok; + int lineno; + char* ptr; /* points into context->start */ + int len; + int eof; +}; + + +typedef struct context_t context_t; +struct context_t { + char* start; + char* stop; + char* errbuf; + int errbufsz; + + token_t tok; + toml_table_t* root; + toml_table_t* curtab; + + struct { + int top; + char* key[10]; + token_t tok[10]; + } tpath; + +}; + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) +#define FLINE __FILE__ ":" TOSTRING(__LINE__) + +static int next_token(context_t* ctx, int dotisspecial); + +/* + Error reporting. Call when an error is detected. Always return -1. +*/ +static int e_outofmemory(context_t* ctx, const char* fline) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "ERROR: out of memory (%s)", fline); + return -1; +} + + +static int e_internal(context_t* ctx, const char* fline) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "internal error (%s)", fline); + return -1; +} + +static int e_syntax(context_t* ctx, int lineno, const char* msg) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg); + return -1; +} + +static int e_badkey(context_t* ctx, int lineno) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: bad key", lineno); + return -1; +} + +static int e_keyexists(context_t* ctx, int lineno) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: key exists", lineno); + return -1; +} + +static int e_forbid(context_t* ctx, int lineno, const char* msg) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg); + return -1; +} + +static void* expand(void* p, int sz, int newsz) +{ + void* s = MALLOC(newsz); + if (!s) return 0; + + memcpy(s, p, sz); + FREE(p); + return s; +} + +static void** expand_ptrarr(void** p, int n) +{ + void** s = MALLOC((n+1) * sizeof(void*)); + if (!s) return 0; + + s[n] = 0; + memcpy(s, p, n * sizeof(void*)); + FREE(p); + return s; +} + +static toml_arritem_t* expand_arritem(toml_arritem_t* p, int n) +{ + toml_arritem_t* pp = expand(p, n*sizeof(*p), (n+1)*sizeof(*p)); + if (!pp) return 0; + + memset(&pp[n], 0, sizeof(pp[n])); + return pp; +} + + +static char* norm_lit_str(const char* src, int srclen, + int multiline, + char* errbuf, int errbufsz) +{ + char* dst = 0; /* will write to dst[] and return it */ + int max = 0; /* max size of dst[] */ + int off = 0; /* cur offset in dst[] */ + const char* sp = src; + const char* sq = src + srclen; + int ch; + + /* scan forward on src */ + for (;;) { + if (off >= max - 10) { /* have some slack for misc stuff */ + int newmax = max + 50; + char* x = expand(dst, max, newmax); + if (!x) { + xfree(dst); + snprintf(errbuf, errbufsz, "out of memory"); + return 0; + } + dst = x; + max = newmax; + } + + /* finished? */ + if (sp >= sq) break; + + ch = *sp++; + /* control characters other than tab is not allowed */ + if ((0 <= ch && ch <= 0x08) + || (0x0a <= ch && ch <= 0x1f) + || (ch == 0x7f)) { + if (! (multiline && (ch == '\r' || ch == '\n'))) { + xfree(dst); + snprintf(errbuf, errbufsz, "invalid char U+%04x", ch); + return 0; + } + } + + // a plain copy suffice + dst[off++] = ch; + } + + dst[off++] = 0; + return dst; +} + + + + +/* + * Convert src to raw unescaped utf-8 string. + * Returns NULL if error with errmsg in errbuf. + */ +static char* norm_basic_str(const char* src, int srclen, + int multiline, + char* errbuf, int errbufsz) +{ + char* dst = 0; /* will write to dst[] and return it */ + int max = 0; /* max size of dst[] */ + int off = 0; /* cur offset in dst[] */ + const char* sp = src; + const char* sq = src + srclen; + int ch; + + /* scan forward on src */ + for (;;) { + if (off >= max - 10) { /* have some slack for misc stuff */ + int newmax = max + 50; + char* x = expand(dst, max, newmax); + if (!x) { + xfree(dst); + snprintf(errbuf, errbufsz, "out of memory"); + return 0; + } + dst = x; + max = newmax; + } + + /* finished? */ + if (sp >= sq) break; + + ch = *sp++; + if (ch != '\\') { + /* these chars must be escaped: U+0000 to U+0008, U+000A to U+001F, U+007F */ + if ((0 <= ch && ch <= 0x08) + || (0x0a <= ch && ch <= 0x1f) + || (ch == 0x7f)) { + if (! (multiline && (ch == '\r' || ch == '\n'))) { + xfree(dst); + snprintf(errbuf, errbufsz, "invalid char U+%04x", ch); + return 0; + } + } + + // a plain copy suffice + dst[off++] = ch; + continue; + } + + /* ch was backslash. we expect the escape char. */ + if (sp >= sq) { + snprintf(errbuf, errbufsz, "last backslash is invalid"); + xfree(dst); + return 0; + } + + /* for multi-line, we want to kill line-ending-backslash ... */ + if (multiline) { + + // if there is only whitespace after the backslash ... + if (sp[strspn(sp, " \t\r")] == '\n') { + /* skip all the following whitespaces */ + sp += strspn(sp, " \t\r\n"); + continue; + } + } + + /* get the escaped char */ + ch = *sp++; + switch (ch) { + case 'u': case 'U': + { + int64_t ucs = 0; + int nhex = (ch == 'u' ? 4 : 8); + for (int i = 0; i < nhex; i++) { + if (sp >= sq) { + snprintf(errbuf, errbufsz, "\\%c expects %d hex chars", ch, nhex); + xfree(dst); + return 0; + } + ch = *sp++; + int v = ('0' <= ch && ch <= '9') + ? ch - '0' + : (('A' <= ch && ch <= 'F') ? ch - 'A' + 10 : -1); + if (-1 == v) { + snprintf(errbuf, errbufsz, "invalid hex chars for \\u or \\U"); + xfree(dst); + return 0; + } + ucs = ucs * 16 + v; + } + int n = toml_ucs_to_utf8(ucs, &dst[off]); + if (-1 == n) { + snprintf(errbuf, errbufsz, "illegal ucs code in \\u or \\U"); + xfree(dst); + return 0; + } + off += n; + } + continue; + + case 'b': ch = '\b'; break; + case 't': ch = '\t'; break; + case 'n': ch = '\n'; break; + case 'f': ch = '\f'; break; + case 'r': ch = '\r'; break; + case '"': ch = '"'; break; + case '\\': ch = '\\'; break; + default: + snprintf(errbuf, errbufsz, "illegal escape char \\%c", ch); + xfree(dst); + return 0; + } + + dst[off++] = ch; + } + + // Cap with NUL and return it. + dst[off++] = 0; + return dst; +} + + +/* Normalize a key. Convert all special chars to raw unescaped utf-8 chars. */ +static char* normalize_key(context_t* ctx, token_t strtok) +{ + const char* sp = strtok.ptr; + const char* sq = strtok.ptr + strtok.len; + int lineno = strtok.lineno; + char* ret; + int ch = *sp; + char ebuf[80]; + + /* handle quoted string */ + if (ch == '\'' || ch == '\"') { + /* if ''' or """, take 3 chars off front and back. Else, take 1 char off. */ + int multiline = 0; + if (sp[1] == ch && sp[2] == ch) { + sp += 3, sq -= 3; + multiline = 1; + } + else + sp++, sq--; + + if (ch == '\'') { + /* for single quote, take it verbatim. */ + if (! (ret = STRNDUP(sp, sq - sp))) { + e_outofmemory(ctx, FLINE); + return 0; + } + } else { + /* for double quote, we need to normalize */ + ret = norm_basic_str(sp, sq - sp, multiline, ebuf, sizeof(ebuf)); + if (!ret) { + e_syntax(ctx, lineno, ebuf); + return 0; + } + } + + /* newlines are not allowed in keys */ + if (strchr(ret, '\n')) { + xfree(ret); + e_badkey(ctx, lineno); + return 0; + } + return ret; + } + + /* for bare-key allow only this regex: [A-Za-z0-9_-]+ */ + const char* xp; + for (xp = sp; xp != sq; xp++) { + int k = *xp; + if (isalnum(k)) continue; + if (k == '_' || k == '-') continue; + e_badkey(ctx, lineno); + return 0; + } + + /* dup and return it */ + if (! (ret = STRNDUP(sp, sq - sp))) { + e_outofmemory(ctx, FLINE); + return 0; + } + return ret; +} + + +/* + * Look up key in tab. Return 0 if not found, or + * 'v'alue, 'a'rray or 't'able depending on the element. + */ +static int check_key(toml_table_t* tab, const char* key, + toml_keyval_t** ret_val, + toml_array_t** ret_arr, + toml_table_t** ret_tab) +{ + int i; + void* dummy; + + if (!ret_tab) ret_tab = (toml_table_t**) &dummy; + if (!ret_arr) ret_arr = (toml_array_t**) &dummy; + if (!ret_val) ret_val = (toml_keyval_t**) &dummy; + + *ret_tab = 0; *ret_arr = 0; *ret_val = 0; + + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) { + *ret_val = tab->kval[i]; + return 'v'; + } + } + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) { + *ret_arr = tab->arr[i]; + return 'a'; + } + } + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) { + *ret_tab = tab->tab[i]; + return 't'; + } + } + return 0; +} + + +static int key_kind(toml_table_t* tab, const char* key) +{ + return check_key(tab, key, 0, 0, 0); +} + +/* Create a keyval in the table. + */ +static toml_keyval_t* create_keyval_in_table(context_t* ctx, toml_table_t* tab, token_t keytok) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out. */ + toml_keyval_t* dest = 0; + if (key_kind(tab, newkey)) { + xfree(newkey); + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* make a new entry */ + int n = tab->nkval; + toml_keyval_t** base; + if (0 == (base = (toml_keyval_t**) expand_ptrarr((void**)tab->kval, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->kval = base; + + if (0 == (base[n] = (toml_keyval_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + dest = tab->kval[tab->nkval++]; + + /* save the key in the new value struct */ + dest->key = newkey; + return dest; +} + + +/* Create a table in the table. + */ +static toml_table_t* create_keytable_in_table(context_t* ctx, toml_table_t* tab, token_t keytok) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out */ + toml_table_t* dest = 0; + if (check_key(tab, newkey, 0, 0, &dest)) { + xfree(newkey); /* don't need this anymore */ + + /* special case: if table exists, but was created implicitly ... */ + if (dest && dest->implicit) { + /* we make it explicit now, and simply return it. */ + dest->implicit = false; + return dest; + } + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* create a new table entry */ + int n = tab->ntab; + toml_table_t** base; + if (0 == (base = (toml_table_t**) expand_ptrarr((void**)tab->tab, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->tab = base; + + if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + dest = tab->tab[tab->ntab++]; + + /* save the key in the new table struct */ + dest->key = newkey; + return dest; +} + + +/* Create an array in the table. + */ +static toml_array_t* create_keyarray_in_table(context_t* ctx, + toml_table_t* tab, + token_t keytok, + char kind) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out */ + if (key_kind(tab, newkey)) { + xfree(newkey); /* don't need this anymore */ + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* make a new array entry */ + int n = tab->narr; + toml_array_t** base; + if (0 == (base = (toml_array_t**) expand_ptrarr((void**)tab->arr, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->arr = base; + + if (0 == (base[n] = (toml_array_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + toml_array_t* dest = tab->arr[tab->narr++]; + + /* save the key in the new array struct */ + dest->key = newkey; + dest->kind = kind; + return dest; +} + + +static toml_arritem_t* create_value_in_array(context_t* ctx, + toml_array_t* parent) +{ + const int n = parent->nitem; + toml_arritem_t* base = expand_arritem(parent->item, n); + if (!base) { + e_outofmemory(ctx, FLINE); + return 0; + } + parent->item = base; + parent->nitem++; + return &parent->item[n]; +} + +/* Create an array in an array + */ +static toml_array_t* create_array_in_array(context_t* ctx, + toml_array_t* parent) +{ + const int n = parent->nitem; + toml_arritem_t* base = expand_arritem(parent->item, n); + if (!base) { + e_outofmemory(ctx, FLINE); + return 0; + } + toml_array_t* ret = (toml_array_t*) CALLOC(1, sizeof(toml_array_t)); + if (!ret) { + e_outofmemory(ctx, FLINE); + return 0; + } + base[n].arr = ret; + parent->item = base; + parent->nitem++; + return ret; +} + +/* Create a table in an array + */ +static toml_table_t* create_table_in_array(context_t* ctx, + toml_array_t* parent) +{ + int n = parent->nitem; + toml_arritem_t* base = expand_arritem(parent->item, n); + if (!base) { + e_outofmemory(ctx, FLINE); + return 0; + } + toml_table_t* ret = (toml_table_t*) CALLOC(1, sizeof(toml_table_t)); + if (!ret) { + e_outofmemory(ctx, FLINE); + return 0; + } + base[n].tab = ret; + parent->item = base; + parent->nitem++; + return ret; +} + + +static int skip_newlines(context_t* ctx, int isdotspecial) +{ + while (ctx->tok.tok == NEWLINE) { + if (next_token(ctx, isdotspecial)) return -1; + if (ctx->tok.eof) break; + } + return 0; +} + + +static int parse_keyval(context_t* ctx, toml_table_t* tab); + +static inline int eat_token(context_t* ctx, tokentype_t typ, int isdotspecial, const char* fline) +{ + if (ctx->tok.tok != typ) + return e_internal(ctx, fline); + + if (next_token(ctx, isdotspecial)) + return -1; + + return 0; +} + + + +/* We are at '{ ... }'. + * Parse the table. + */ +static int parse_inline_table(context_t* ctx, toml_table_t* tab) +{ + if (eat_token(ctx, LBRACE, 1, FLINE)) + return -1; + + for (;;) { + if (ctx->tok.tok == NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "newline not allowed in inline table"); + + /* until } */ + if (ctx->tok.tok == RBRACE) + break; + + if (ctx->tok.tok != STRING) + return e_syntax(ctx, ctx->tok.lineno, "expect a string"); + + if (parse_keyval(ctx, tab)) + return -1; + + if (ctx->tok.tok == NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "newline not allowed in inline table"); + + /* on comma, continue to scan for next keyval */ + if (ctx->tok.tok == COMMA) { + if (eat_token(ctx, COMMA, 1, FLINE)) + return -1; + continue; + } + break; + } + + if (eat_token(ctx, RBRACE, 1, FLINE)) + return -1; + + tab->readonly = 1; + + return 0; +} + +static int valtype(const char* val) +{ + toml_timestamp_t ts; + if (*val == '\'' || *val == '"') return 's'; + if (0 == toml_rtob(val, 0)) return 'b'; + if (0 == toml_rtoi(val, 0)) return 'i'; + if (0 == toml_rtod(val, 0)) return 'd'; + if (0 == toml_rtots(val, &ts)) { + if (ts.year && ts.hour) return 'T'; /* timestamp */ + if (ts.year) return 'D'; /* date */ + return 't'; /* time */ + } + return 'u'; /* unknown */ +} + + +/* We are at '[...]' */ +static int parse_array(context_t* ctx, toml_array_t* arr) +{ + if (eat_token(ctx, LBRACKET, 0, FLINE)) return -1; + + for (;;) { + if (skip_newlines(ctx, 0)) return -1; + + /* until ] */ + if (ctx->tok.tok == RBRACKET) break; + + switch (ctx->tok.tok) { + case STRING: + { + /* set array kind if this will be the first entry */ + if (arr->kind == 0) + arr->kind = 'v'; + else if (arr->kind != 'v') + arr->kind = 'm'; + + char* val = ctx->tok.ptr; + int vlen = ctx->tok.len; + + /* make a new value in array */ + toml_arritem_t* newval = create_value_in_array(ctx, arr); + if (!newval) + return e_outofmemory(ctx, FLINE); + + if (! (newval->val = STRNDUP(val, vlen))) + return e_outofmemory(ctx, FLINE); + + newval->valtype = valtype(newval->val); + + /* set array type if this is the first entry */ + if (arr->nitem == 1) + arr->type = newval->valtype; + else if (arr->type != newval->valtype) + arr->type = 'm'; /* mixed */ + + if (eat_token(ctx, STRING, 0, FLINE)) return -1; + break; + } + + case LBRACKET: + { /* [ [array], [array] ... ] */ + /* set the array kind if this will be the first entry */ + if (arr->kind == 0) + arr->kind = 'a'; + else if (arr->kind != 'a') + arr->kind = 'm'; + + toml_array_t* subarr = create_array_in_array(ctx, arr); + if (!subarr) return -1; + if (parse_array(ctx, subarr)) return -1; + break; + } + + case LBRACE: + { /* [ {table}, {table} ... ] */ + /* set the array kind if this will be the first entry */ + if (arr->kind == 0) + arr->kind = 't'; + else if (arr->kind != 't') + arr->kind = 'm'; + + toml_table_t* subtab = create_table_in_array(ctx, arr); + if (!subtab) return -1; + if (parse_inline_table(ctx, subtab)) return -1; + break; + } + + default: + return e_syntax(ctx, ctx->tok.lineno, "syntax error"); + } + + if (skip_newlines(ctx, 0)) return -1; + + /* on comma, continue to scan for next element */ + if (ctx->tok.tok == COMMA) { + if (eat_token(ctx, COMMA, 0, FLINE)) return -1; + continue; + } + break; + } + + if (eat_token(ctx, RBRACKET, 1, FLINE)) return -1; + return 0; +} + + +/* handle lines like these: + key = "value" + key = [ array ] + key = { table } +*/ +static int parse_keyval(context_t* ctx, toml_table_t* tab) +{ + if (tab->readonly) { + return e_forbid(ctx, ctx->tok.lineno, "cannot insert new entry into existing table"); + } + + token_t key = ctx->tok; + if (eat_token(ctx, STRING, 1, FLINE)) return -1; + + if (ctx->tok.tok == DOT) { + /* handle inline dotted key. + e.g. + physical.color = "orange" + physical.shape = "round" + */ + toml_table_t* subtab = 0; + { + char* subtabstr = normalize_key(ctx, key); + if (!subtabstr) return -1; + + subtab = toml_table_in(tab, subtabstr); + xfree(subtabstr); + } + if (!subtab) { + subtab = create_keytable_in_table(ctx, tab, key); + if (!subtab) return -1; + } + if (next_token(ctx, 1)) return -1; + if (parse_keyval(ctx, subtab)) return -1; + return 0; + } + + if (ctx->tok.tok != EQUAL) { + return e_syntax(ctx, ctx->tok.lineno, "missing ="); + } + + if (next_token(ctx, 0)) return -1; + + switch (ctx->tok.tok) { + case STRING: + { /* key = "value" */ + toml_keyval_t* keyval = create_keyval_in_table(ctx, tab, key); + if (!keyval) return -1; + token_t val = ctx->tok; + + assert(keyval->val == 0); + if (! (keyval->val = STRNDUP(val.ptr, val.len))) + return e_outofmemory(ctx, FLINE); + + if (next_token(ctx, 1)) return -1; + + return 0; + } + + case LBRACKET: + { /* key = [ array ] */ + toml_array_t* arr = create_keyarray_in_table(ctx, tab, key, 0); + if (!arr) return -1; + if (parse_array(ctx, arr)) return -1; + return 0; + } + + case LBRACE: + { /* key = { table } */ + toml_table_t* nxttab = create_keytable_in_table(ctx, tab, key); + if (!nxttab) return -1; + if (parse_inline_table(ctx, nxttab)) return -1; + return 0; + } + + default: + return e_syntax(ctx, ctx->tok.lineno, "syntax error"); + } + return 0; +} + + +typedef struct tabpath_t tabpath_t; +struct tabpath_t { + int cnt; + token_t key[10]; +}; + +/* at [x.y.z] or [[x.y.z]] + * Scan forward and fill tabpath until it enters ] or ]] + * There will be at least one entry on return. + */ +static int fill_tabpath(context_t* ctx) +{ + int lineno = ctx->tok.lineno; + int i; + + /* clear tpath */ + for (i = 0; i < ctx->tpath.top; i++) { + char** p = &ctx->tpath.key[i]; + xfree(*p); + *p = 0; + } + ctx->tpath.top = 0; + + for (;;) { + if (ctx->tpath.top >= 10) + return e_syntax(ctx, lineno, "table path is too deep; max allowed is 10."); + + if (ctx->tok.tok != STRING) + return e_syntax(ctx, lineno, "invalid or missing key"); + + char* key = normalize_key(ctx, ctx->tok); + if (!key) return -1; + ctx->tpath.tok[ctx->tpath.top] = ctx->tok; + ctx->tpath.key[ctx->tpath.top] = key; + ctx->tpath.top++; + + if (next_token(ctx, 1)) return -1; + + if (ctx->tok.tok == RBRACKET) break; + + if (ctx->tok.tok != DOT) + return e_syntax(ctx, lineno, "invalid key"); + + if (next_token(ctx, 1)) return -1; + } + + if (ctx->tpath.top <= 0) + return e_syntax(ctx, lineno, "empty table selector"); + + return 0; +} + + +/* Walk tabpath from the root, and create new tables on the way. + * Sets ctx->curtab to the final table. + */ +static int walk_tabpath(context_t* ctx) +{ + /* start from root */ + toml_table_t* curtab = ctx->root; + + for (int i = 0; i < ctx->tpath.top; i++) { + const char* key = ctx->tpath.key[i]; + + toml_keyval_t* nextval = 0; + toml_array_t* nextarr = 0; + toml_table_t* nexttab = 0; + switch (check_key(curtab, key, &nextval, &nextarr, &nexttab)) { + case 't': + /* found a table. nexttab is where we will go next. */ + break; + + case 'a': + /* found an array. nexttab is the last table in the array. */ + if (nextarr->kind != 't') + return e_internal(ctx, FLINE); + + if (nextarr->nitem == 0) + return e_internal(ctx, FLINE); + + nexttab = nextarr->item[nextarr->nitem-1].tab; + break; + + case 'v': + return e_keyexists(ctx, ctx->tpath.tok[i].lineno); + + default: + { /* Not found. Let's create an implicit table. */ + int n = curtab->ntab; + toml_table_t** base = (toml_table_t**) expand_ptrarr((void**)curtab->tab, n); + if (0 == base) + return e_outofmemory(ctx, FLINE); + + curtab->tab = base; + + if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) + return e_outofmemory(ctx, FLINE); + + if (0 == (base[n]->key = STRDUP(key))) + return e_outofmemory(ctx, FLINE); + + nexttab = curtab->tab[curtab->ntab++]; + + /* tabs created by walk_tabpath are considered implicit */ + nexttab->implicit = true; + } + break; + } + + /* switch to next tab */ + curtab = nexttab; + } + + /* save it */ + ctx->curtab = curtab; + + return 0; +} + + +/* handle lines like [x.y.z] or [[x.y.z]] */ +static int parse_select(context_t* ctx) +{ + assert(ctx->tok.tok == LBRACKET); + + /* true if [[ */ + int llb = (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == '['); + /* need to detect '[[' on our own because next_token() will skip whitespace, + and '[ [' would be taken as '[[', which is wrong. */ + + /* eat [ or [[ */ + if (eat_token(ctx, LBRACKET, 1, FLINE)) return -1; + if (llb) { + assert(ctx->tok.tok == LBRACKET); + if (eat_token(ctx, LBRACKET, 1, FLINE)) return -1; + } + + if (fill_tabpath(ctx)) return -1; + + /* For [x.y.z] or [[x.y.z]], remove z from tpath. + */ + token_t z = ctx->tpath.tok[ctx->tpath.top-1]; + xfree(ctx->tpath.key[ctx->tpath.top-1]); + ctx->tpath.top--; + + /* set up ctx->curtab */ + if (walk_tabpath(ctx)) return -1; + + if (! llb) { + /* [x.y.z] -> create z = {} in x.y */ + toml_table_t* curtab = create_keytable_in_table(ctx, ctx->curtab, z); + if (!curtab) return -1; + ctx->curtab = curtab; + } else { + /* [[x.y.z]] -> create z = [] in x.y */ + toml_array_t* arr = 0; + { + char* zstr = normalize_key(ctx, z); + if (!zstr) return -1; + arr = toml_array_in(ctx->curtab, zstr); + xfree(zstr); + } + if (!arr) { + arr = create_keyarray_in_table(ctx, ctx->curtab, z, 't'); + if (!arr) return -1; + } + if (arr->kind != 't') + return e_syntax(ctx, z.lineno, "array mismatch"); + + /* add to z[] */ + toml_table_t* dest; + { + toml_table_t* t = create_table_in_array(ctx, arr); + if (!t) return -1; + + if (0 == (t->key = STRDUP("__anon__"))) + return e_outofmemory(ctx, FLINE); + + dest = t; + } + + ctx->curtab = dest; + } + + if (ctx->tok.tok != RBRACKET) { + return e_syntax(ctx, ctx->tok.lineno, "expects ]"); + } + if (llb) { + if (! (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == ']')) { + return e_syntax(ctx, ctx->tok.lineno, "expects ]]"); + } + if (eat_token(ctx, RBRACKET, 1, FLINE)) return -1; + } + + if (eat_token(ctx, RBRACKET, 1, FLINE)) + return -1; + + if (ctx->tok.tok != NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "extra chars after ] or ]]"); + + return 0; +} + + + + +toml_table_t* toml_parse(char* conf, + char* errbuf, + int errbufsz) +{ + context_t ctx; + + // clear errbuf + if (errbufsz <= 0) errbufsz = 0; + if (errbufsz > 0) errbuf[0] = 0; + + // init context + memset(&ctx, 0, sizeof(ctx)); + ctx.start = conf; + ctx.stop = ctx.start + strlen(conf); + ctx.errbuf = errbuf; + ctx.errbufsz = errbufsz; + + // start with an artificial newline of length 0 + ctx.tok.tok = NEWLINE; + ctx.tok.lineno = 1; + ctx.tok.ptr = conf; + ctx.tok.len = 0; + + // make a root table + if (0 == (ctx.root = CALLOC(1, sizeof(*ctx.root)))) { + e_outofmemory(&ctx, FLINE); + // Do not goto fail, root table not set up yet + return 0; + } + + // set root as default table + ctx.curtab = ctx.root; + + /* Scan forward until EOF */ + for (token_t tok = ctx.tok; ! tok.eof ; tok = ctx.tok) { + switch (tok.tok) { + + case NEWLINE: + if (next_token(&ctx, 1)) goto fail; + break; + + case STRING: + if (parse_keyval(&ctx, ctx.curtab)) goto fail; + + if (ctx.tok.tok != NEWLINE) { + e_syntax(&ctx, ctx.tok.lineno, "extra chars after value"); + goto fail; + } + + if (eat_token(&ctx, NEWLINE, 1, FLINE)) goto fail; + break; + + case LBRACKET: /* [ x.y.z ] or [[ x.y.z ]] */ + if (parse_select(&ctx)) goto fail; + break; + + default: + e_syntax(&ctx, tok.lineno, "syntax error"); + goto fail; + } + } + + /* success */ + for (int i = 0; i < ctx.tpath.top; i++) xfree(ctx.tpath.key[i]); + return ctx.root; + +fail: + // Something bad has happened. Free resources and return error. + for (int i = 0; i < ctx.tpath.top; i++) xfree(ctx.tpath.key[i]); + toml_free(ctx.root); + return 0; +} + + +toml_table_t* toml_parse_file(FILE* fp, + char* errbuf, + int errbufsz) +{ + int bufsz = 0; + char* buf = 0; + int off = 0; + + /* read from fp into buf */ + while (! feof(fp)) { + + if (off == bufsz) { + int xsz = bufsz + 1000; + char* x = expand(buf, bufsz, xsz); + if (!x) { + snprintf(errbuf, errbufsz, "out of memory"); + xfree(buf); + return 0; + } + buf = x; + bufsz = xsz; + } + + errno = 0; + int n = fread(buf + off, 1, bufsz - off, fp); + if (ferror(fp)) { + snprintf(errbuf, errbufsz, "%s", + errno ? strerror(errno) : "Error reading file"); + xfree(buf); + return 0; + } + off += n; + } + + /* tag on a NUL to cap the string */ + if (off == bufsz) { + int xsz = bufsz + 1; + char* x = expand(buf, bufsz, xsz); + if (!x) { + snprintf(errbuf, errbufsz, "out of memory"); + xfree(buf); + return 0; + } + buf = x; + bufsz = xsz; + } + buf[off] = 0; + + /* parse it, cleanup and finish */ + toml_table_t* ret = toml_parse(buf, errbuf, errbufsz); + xfree(buf); + return ret; +} + + +static void xfree_kval(toml_keyval_t* p) +{ + if (!p) return; + xfree(p->key); + xfree(p->val); + xfree(p); +} + +static void xfree_tab(toml_table_t* p); + +static void xfree_arr(toml_array_t* p) +{ + if (!p) return; + + xfree(p->key); + const int n = p->nitem; + for (int i = 0; i < n; i++) { + toml_arritem_t* a = &p->item[i]; + if (a->val) + xfree(a->val); + else if (a->arr) + xfree_arr(a->arr); + else if (a->tab) + xfree_tab(a->tab); + } + xfree(p->item); + xfree(p); +} + + +static void xfree_tab(toml_table_t* p) +{ + int i; + + if (!p) return; + + xfree(p->key); + + for (i = 0; i < p->nkval; i++) xfree_kval(p->kval[i]); + xfree(p->kval); + + for (i = 0; i < p->narr; i++) xfree_arr(p->arr[i]); + xfree(p->arr); + + for (i = 0; i < p->ntab; i++) xfree_tab(p->tab[i]); + xfree(p->tab); + + xfree(p); +} + + +void toml_free(toml_table_t* tab) +{ + xfree_tab(tab); +} + + +static void set_token(context_t* ctx, tokentype_t tok, int lineno, char* ptr, int len) +{ + token_t t; + t.tok = tok; + t.lineno = lineno; + t.ptr = ptr; + t.len = len; + t.eof = 0; + ctx->tok = t; +} + +static void set_eof(context_t* ctx, int lineno) +{ + set_token(ctx, NEWLINE, lineno, ctx->stop, 0); + ctx->tok.eof = 1; +} + + +/* Scan p for n digits compositing entirely of [0-9] */ +static int scan_digits(const char* p, int n) +{ + int ret = 0; + for ( ; n > 0 && isdigit(*p); n--, p++) { + ret = 10 * ret + (*p - '0'); + } + return n ? -1 : ret; +} + +static int scan_date(const char* p, int* YY, int* MM, int* DD) +{ + int year, month, day; + year = scan_digits(p, 4); + month = (year >= 0 && p[4] == '-') ? scan_digits(p+5, 2) : -1; + day = (month >= 0 && p[7] == '-') ? scan_digits(p+8, 2) : -1; + if (YY) *YY = year; + if (MM) *MM = month; + if (DD) *DD = day; + return (year >= 0 && month >= 0 && day >= 0) ? 0 : -1; +} + +static int scan_time(const char* p, int* hh, int* mm, int* ss) +{ + int hour, minute, second; + hour = scan_digits(p, 2); + minute = (hour >= 0 && p[2] == ':') ? scan_digits(p+3, 2) : -1; + second = (minute >= 0 && p[5] == ':') ? scan_digits(p+6, 2) : -1; + if (hh) *hh = hour; + if (mm) *mm = minute; + if (ss) *ss = second; + return (hour >= 0 && minute >= 0 && second >= 0) ? 0 : -1; +} + + +static int scan_string(context_t* ctx, char* p, int lineno, int dotisspecial) +{ + char* orig = p; + if (0 == strncmp(p, "'''", 3)) { + char* q = p + 3; + + while (1) { + q = strstr(q, "'''"); + if (0 == q) { + return e_syntax(ctx, lineno, "unterminated triple-s-quote"); + } + while (q[3] == '\'') q++; + break; + } + + set_token(ctx, STRING, lineno, orig, q + 3 - orig); + return 0; + } + + if (0 == strncmp(p, "\"\"\"", 3)) { + char* q = p + 3; + + while (1) { + q = strstr(q, "\"\"\""); + if (0 == q) { + return e_syntax(ctx, lineno, "unterminated triple-d-quote"); + } + if (q[-1] == '\\') { + q++; + continue; + } + while (q[3] == '\"') q++; + break; + } + + // the string is [p+3, q-1] + + int hexreq = 0; /* #hex required */ + int escape = 0; + for (p += 3; p < q; p++) { + if (escape) { + escape = 0; + if (strchr("btnfr\"\\", *p)) continue; + if (*p == 'u') { hexreq = 4; continue; } + if (*p == 'U') { hexreq = 8; continue; } + if (p[strspn(p, " \t\r")] == '\n') continue; /* allow for line ending backslash */ + return e_syntax(ctx, lineno, "bad escape char"); + } + if (hexreq) { + hexreq--; + if (strchr("0123456789ABCDEF", *p)) continue; + return e_syntax(ctx, lineno, "expect hex char"); + } + if (*p == '\\') { escape = 1; continue; } + } + if (escape) + return e_syntax(ctx, lineno, "expect an escape char"); + if (hexreq) + return e_syntax(ctx, lineno, "expected more hex char"); + + set_token(ctx, STRING, lineno, orig, q + 3 - orig); + return 0; + } + + if ('\'' == *p) { + for (p++; *p && *p != '\n' && *p != '\''; p++); + if (*p != '\'') { + return e_syntax(ctx, lineno, "unterminated s-quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 1 - orig); + return 0; + } + + if ('\"' == *p) { + int hexreq = 0; /* #hex required */ + int escape = 0; + for (p++; *p; p++) { + if (escape) { + escape = 0; + if (strchr("btnfr\"\\", *p)) continue; + if (*p == 'u') { hexreq = 4; continue; } + if (*p == 'U') { hexreq = 8; continue; } + return e_syntax(ctx, lineno, "bad escape char"); + } + if (hexreq) { + hexreq--; + if (strchr("0123456789ABCDEF", *p)) continue; + return e_syntax(ctx, lineno, "expect hex char"); + } + if (*p == '\\') { escape = 1; continue; } + if (*p == '\'') { + if (p[1] == '\'' && p[2] == '\'') { + return e_syntax(ctx, lineno, "triple-s-quote inside string lit"); + } + continue; + } + if (*p == '\n') break; + if (*p == '"') break; + } + if (*p != '"') { + return e_syntax(ctx, lineno, "unterminated quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 1 - orig); + return 0; + } + + /* check for timestamp without quotes */ + if (0 == scan_date(p, 0, 0, 0) || 0 == scan_time(p, 0, 0, 0)) { + // forward thru the timestamp + for ( ; strchr("0123456789.:+-T Z", toupper(*p)); p++); + // squeeze out any spaces at end of string + for ( ; p[-1] == ' '; p--); + // tokenize + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; + } + + /* literals */ + for ( ; *p && *p != '\n'; p++) { + int ch = *p; + if (ch == '.' && dotisspecial) break; + if ('A' <= ch && ch <= 'Z') continue; + if ('a' <= ch && ch <= 'z') continue; + if (strchr("0123456789+-_.", ch)) continue; + break; + } + + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; +} + + +static int next_token(context_t* ctx, int dotisspecial) +{ + int lineno = ctx->tok.lineno; + char* p = ctx->tok.ptr; + int i; + + /* eat this tok */ + for (i = 0; i < ctx->tok.len; i++) { + if (*p++ == '\n') + lineno++; + } + + /* make next tok */ + while (p < ctx->stop) { + /* skip comment. stop just before the \n. */ + if (*p == '#') { + for (p++; p < ctx->stop && *p != '\n'; p++); + continue; + } + + if (dotisspecial && *p == '.') { + set_token(ctx, DOT, lineno, p, 1); + return 0; + } + + switch (*p) { + case ',': set_token(ctx, COMMA, lineno, p, 1); return 0; + case '=': set_token(ctx, EQUAL, lineno, p, 1); return 0; + case '{': set_token(ctx, LBRACE, lineno, p, 1); return 0; + case '}': set_token(ctx, RBRACE, lineno, p, 1); return 0; + case '[': set_token(ctx, LBRACKET, lineno, p, 1); return 0; + case ']': set_token(ctx, RBRACKET, lineno, p, 1); return 0; + case '\n': set_token(ctx, NEWLINE, lineno, p, 1); return 0; + case '\r': case ' ': case '\t': + /* ignore white spaces */ + p++; + continue; + } + + return scan_string(ctx, p, lineno, dotisspecial); + } + + set_eof(ctx, lineno); + return 0; +} + + +const char* toml_key_in(const toml_table_t* tab, int keyidx) +{ + if (keyidx < tab->nkval) return tab->kval[keyidx]->key; + + keyidx -= tab->nkval; + if (keyidx < tab->narr) return tab->arr[keyidx]->key; + + keyidx -= tab->narr; + if (keyidx < tab->ntab) return tab->tab[keyidx]->key; + + return 0; +} + +toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) + return tab->kval[i]->val; + } + return 0; +} + +toml_array_t* toml_array_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) + return tab->arr[i]; + } + return 0; +} + + +toml_table_t* toml_table_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) + return tab->tab[i]; + } + return 0; +} + +toml_raw_t toml_raw_at(const toml_array_t* arr, int idx) +{ + return (0 <= idx && idx < arr->nitem) ? arr->item[idx].val : 0; +} + +char toml_array_kind(const toml_array_t* arr) +{ + return arr->kind; +} + +char toml_array_type(const toml_array_t* arr) +{ + if (arr->kind != 'v') + return 0; + + if (arr->nitem == 0) + return 0; + + return arr->type; +} + + +int toml_array_nelem(const toml_array_t* arr) +{ + return arr->nitem; +} + +const char* toml_array_key(const toml_array_t* arr) +{ + return arr ? arr->key : (const char*) NULL; +} + +int toml_table_nkval(const toml_table_t* tab) +{ + return tab->nkval; +} + +int toml_table_narr(const toml_table_t* tab) +{ + return tab->narr; +} + +int toml_table_ntab(const toml_table_t* tab) +{ + return tab->ntab; +} + +const char* toml_table_key(const toml_table_t* tab) +{ + return tab ? tab->key : (const char*) NULL; +} + +toml_array_t* toml_array_at(const toml_array_t* arr, int idx) +{ + return (0 <= idx && idx < arr->nitem) ? arr->item[idx].arr : 0; +} + +toml_table_t* toml_table_at(const toml_array_t* arr, int idx) +{ + return (0 <= idx && idx < arr->nitem) ? arr->item[idx].tab : 0; +} + + +int toml_rtots(toml_raw_t src_, toml_timestamp_t* ret) +{ + if (! src_) return -1; + + const char* p = src_; + int must_parse_time = 0; + + memset(ret, 0, sizeof(*ret)); + + int* year = &ret->__buffer.year; + int* month = &ret->__buffer.month; + int* day = &ret->__buffer.day; + int* hour = &ret->__buffer.hour; + int* minute = &ret->__buffer.minute; + int* second = &ret->__buffer.second; + int* millisec = &ret->__buffer.millisec; + + /* parse date YYYY-MM-DD */ + if (0 == scan_date(p, year, month, day)) { + ret->year = year; + ret->month = month; + ret->day = day; + + p += 10; + if (*p) { + // parse the T or space separator + if (*p != 'T' && *p != ' ') return -1; + must_parse_time = 1; + p++; + } + } + + /* parse time HH:MM:SS */ + if (0 == scan_time(p, hour, minute, second)) { + ret->hour = hour; + ret->minute = minute; + ret->second = second; + + /* optionally, parse millisec */ + p += 8; + if (*p == '.') { + char* qq; + p++; + errno = 0; + *millisec = strtol(p, &qq, 0); + if (errno) { + return -1; + } + while (*millisec > 999) { + *millisec /= 10; + } + + ret->millisec = millisec; + p = qq; + } + + if (*p) { + /* parse and copy Z */ + char* z = ret->__buffer.z; + ret->z = z; + if (*p == 'Z' || *p == 'z') { + *z++ = 'Z'; p++; + *z = 0; + + } else if (*p == '+' || *p == '-') { + *z++ = *p++; + + if (! (isdigit(p[0]) && isdigit(p[1]))) return -1; + *z++ = *p++; + *z++ = *p++; + + if (*p == ':') { + *z++ = *p++; + + if (! (isdigit(p[0]) && isdigit(p[1]))) return -1; + *z++ = *p++; + *z++ = *p++; + } + + *z = 0; + } + } + } + if (*p != 0) + return -1; + + if (must_parse_time && !ret->hour) + return -1; + + return 0; +} + + +/* Raw to boolean */ +int toml_rtob(toml_raw_t src, int* ret_) +{ + if (!src) return -1; + int dummy; + int* ret = ret_ ? ret_ : &dummy; + + if (0 == strcmp(src, "true")) { + *ret = 1; + return 0; + } + if (0 == strcmp(src, "false")) { + *ret = 0; + return 0; + } + return -1; +} + + +/* Raw to integer */ +int toml_rtoi(toml_raw_t src, int64_t* ret_) +{ + if (!src) return -1; + + char buf[100]; + char* p = buf; + char* q = p + sizeof(buf); + const char* s = src; + int base = 0; + int64_t dummy; + int64_t* ret = ret_ ? ret_ : &dummy; + + + /* allow +/- */ + if (s[0] == '+' || s[0] == '-') + *p++ = *s++; + + /* disallow +_100 */ + if (s[0] == '_') + return -1; + + /* if 0 ... */ + if ('0' == s[0]) { + switch (s[1]) { + case 'x': base = 16; s += 2; break; + case 'o': base = 8; s += 2; break; + case 'b': base = 2; s += 2; break; + case '\0': return *ret = 0, 0; + default: + /* ensure no other digits after it */ + if (s[1]) return -1; + } + } + + /* just strip underscores and pass to strtoll */ + while (*s && p < q) { + int ch = *s++; + switch (ch) { + case '_': + // disallow '__' + if (s[0] == '_') return -1; + continue; /* skip _ */ + default: + break; + } + *p++ = ch; + } + if (*s || p == q) return -1; + + /* last char cannot be '_' */ + if (s[-1] == '_') return -1; + + /* cap with NUL */ + *p = 0; + + /* Run strtoll on buf to get the integer */ + char* endp; + errno = 0; + *ret = strtoll(buf, &endp, base); + return (errno || *endp) ? -1 : 0; +} + + +int toml_rtod_ex(toml_raw_t src, double* ret_, char* buf, int buflen) +{ + if (!src) return -1; + + char* p = buf; + char* q = p + buflen; + const char* s = src; + double dummy; + double* ret = ret_ ? ret_ : &dummy; + + + /* allow +/- */ + if (s[0] == '+' || s[0] == '-') + *p++ = *s++; + + /* disallow +_1.00 */ + if (s[0] == '_') + return -1; + + /* decimal point, if used, must be surrounded by at least one digit on each side */ + { + char* dot = strchr(s, '.'); + if (dot) { + if (dot == s || !isdigit(dot[-1]) || !isdigit(dot[1])) + return -1; + } + } + + /* zero must be followed by . or 'e', or NUL */ + if (s[0] == '0' && s[1] && !strchr("eE.", s[1])) + return -1; + + /* just strip underscores and pass to strtod */ + while (*s && p < q) { + int ch = *s++; + if (ch == '_') { + // disallow '__' + if (s[0] == '_') return -1; + // disallow last char '_' + if (s[0] == 0) return -1; + continue; /* skip _ */ + } + *p++ = ch; + } + if (*s || p == q) return -1; /* reached end of string or buffer is full? */ + + /* cap with NUL */ + *p = 0; + + /* Run strtod on buf to get the value */ + char* endp; + errno = 0; + *ret = strtod(buf, &endp); + return (errno || *endp) ? -1 : 0; +} + +int toml_rtod(toml_raw_t src, double* ret_) +{ + char buf[100]; + return toml_rtod_ex(src, ret_, buf, sizeof(buf)); +} + + + + +int toml_rtos(toml_raw_t src, char** ret) +{ + int multiline = 0; + const char* sp; + const char* sq; + + *ret = 0; + if (!src) return -1; + + int qchar = src[0]; + int srclen = strlen(src); + if (! (qchar == '\'' || qchar == '"')) { + return -1; + } + + // triple quotes? + if (qchar == src[1] && qchar == src[2]) { + multiline = 1; + sp = src + 3; + sq = src + srclen - 3; + /* last 3 chars in src must be qchar */ + if (! (sp <= sq && sq[0] == qchar && sq[1] == qchar && sq[2] == qchar)) + return -1; + + /* skip new line immediate after qchar */ + if (sp[0] == '\n') + sp++; + else if (sp[0] == '\r' && sp[1] == '\n') + sp += 2; + + } else { + sp = src + 1; + sq = src + srclen - 1; + /* last char in src must be qchar */ + if (! (sp <= sq && *sq == qchar)) + return -1; + } + + if (qchar == '\'') { + *ret = norm_lit_str(sp, sq - sp, + multiline, + 0, 0); + } else { + *ret = norm_basic_str(sp, sq - sp, + multiline, + 0, 0); + } + + return *ret ? 0 : -1; +} + + +toml_datum_t toml_string_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtos(toml_raw_at(arr, idx), &ret.u.s)); + return ret; +} + +toml_datum_t toml_bool_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtob(toml_raw_at(arr, idx), &ret.u.b)); + return ret; +} + +toml_datum_t toml_int_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtoi(toml_raw_at(arr, idx), &ret.u.i)); + return ret; +} + +toml_datum_t toml_double_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtod(toml_raw_at(arr, idx), &ret.u.d)); + return ret; +} + +toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx) +{ + toml_timestamp_t ts; + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtots(toml_raw_at(arr, idx), &ts)); + if (ret.ok) { + ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts))); + if (ret.ok) { + *ret.u.ts = ts; + if (ret.u.ts->year) ret.u.ts->year = &ret.u.ts->__buffer.year; + if (ret.u.ts->month) ret.u.ts->month = &ret.u.ts->__buffer.month; + if (ret.u.ts->day) ret.u.ts->day = &ret.u.ts->__buffer.day; + if (ret.u.ts->hour) ret.u.ts->hour = &ret.u.ts->__buffer.hour; + if (ret.u.ts->minute) ret.u.ts->minute = &ret.u.ts->__buffer.minute; + if (ret.u.ts->second) ret.u.ts->second = &ret.u.ts->__buffer.second; + if (ret.u.ts->millisec) ret.u.ts->millisec = &ret.u.ts->__buffer.millisec; + if (ret.u.ts->z) ret.u.ts->z = ret.u.ts->__buffer.z; + } + } + return ret; +} + +toml_datum_t toml_string_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + toml_raw_t raw = toml_raw_in(arr, key); + if (raw) { + ret.ok = (0 == toml_rtos(raw, &ret.u.s)); + } + return ret; +} + +toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtob(toml_raw_in(arr, key), &ret.u.b)); + return ret; +} + +toml_datum_t toml_int_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtoi(toml_raw_in(arr, key), &ret.u.i)); + return ret; +} + +toml_datum_t toml_double_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtod(toml_raw_in(arr, key), &ret.u.d)); + return ret; +} + +toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key) +{ + toml_timestamp_t ts; + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtots(toml_raw_in(arr, key), &ts)); + if (ret.ok) { + ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts))); + if (ret.ok) { + *ret.u.ts = ts; + if (ret.u.ts->year) ret.u.ts->year = &ret.u.ts->__buffer.year; + if (ret.u.ts->month) ret.u.ts->month = &ret.u.ts->__buffer.month; + if (ret.u.ts->day) ret.u.ts->day = &ret.u.ts->__buffer.day; + if (ret.u.ts->hour) ret.u.ts->hour = &ret.u.ts->__buffer.hour; + if (ret.u.ts->minute) ret.u.ts->minute = &ret.u.ts->__buffer.minute; + if (ret.u.ts->second) ret.u.ts->second = &ret.u.ts->__buffer.second; + if (ret.u.ts->millisec) ret.u.ts->millisec = &ret.u.ts->__buffer.millisec; + if (ret.u.ts->z) ret.u.ts->z = ret.u.ts->__buffer.z; + } + } + return ret; +} diff --git a/notsecrets/CMakeLists.txt b/notsecrets/CMakeLists.txt new file mode 100644 index 0000000000..f27aeb706b --- /dev/null +++ b/notsecrets/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(secrets STATIC Secrets.cpp Secrets.h) +target_link_libraries(secrets Qt5::Core) +target_compile_definitions(secrets PUBLIC -DEMBED_SECRETS) +target_include_directories(secrets PUBLIC .) diff --git a/notsecrets/Secrets.cpp b/notsecrets/Secrets.cpp new file mode 100644 index 0000000000..8899563579 --- /dev/null +++ b/notsecrets/Secrets.cpp @@ -0,0 +1,42 @@ +#include "Secrets.h" + +#include +#include + +namespace { + +/* + * This is the MSA client ID. It is confidential and should not be reused. + * You can obtain one for yourself by using azure app registration: + * https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app + * + * The app registration should: + * - Be only for personal accounts. + * - Not have any redirect URI. + * - Not have any platform. + * - Have no credentials. + * - No certificates. + * - No client secrets. + * - Enable 'Live SDK support' for access to XBox APIs. + * - Enable 'public client flows' for OAuth2 device flow. + * + * By putting one in here, you accept the terms and conditions for using the MS Identity Plaform and assume all responsibilities associated with it. + * See: https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use + * + * Above all else, do not impersonate other applications! This includes the Mojang Launcher and MultiMC - your builds are *NOT* MultiMC. + * + * If you intend to base your own launcher on this code, take care and customize this to obfuscate the client ID, so it cannot be trivially found by casual attackers. + */ + +QString MSAClientID = ""; +} + +namespace Secrets { +bool hasMSAClientID() { + return !MSAClientID.isEmpty(); +} + +QString getMSAClientID(uint8_t separator) { + return MSAClientID; +} +} diff --git a/notsecrets/Secrets.h b/notsecrets/Secrets.h new file mode 100644 index 0000000000..6872b68ef5 --- /dev/null +++ b/notsecrets/Secrets.h @@ -0,0 +1,8 @@ +#pragma once +#include +#include + +namespace Secrets { +bool hasMSAClientID(); +QString getMSAClientID(uint8_t separator); +}