From c666c3e2513c47552e91142925b2edabf1fd04ee Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 20 Jul 2022 14:21:44 +0200 Subject: [PATCH 01/14] refactor!: bump to C++17 and C17 Signed-off-by: Sefa Eyeoglu --- CMakeLists.txt | 7 +- COPYING.md | 28 - launcher/CMakeLists.txt | 1 - launcher/InstanceImportTask.h | 6 +- launcher/MMCZip.cpp | 16 +- launcher/MMCZip.h | 8 +- launcher/minecraft/World.cpp | 8 +- launcher/minecraft/World.h | 6 +- .../atlauncher/ATLPackInstallTask.h | 6 +- .../modplatform/legacy_ftb/PackInstallTask.h | 6 +- .../technic/SingleZipPackInstallTask.h | 6 +- libraries/README.md | 8 - libraries/optional-bare/CMakeLists.txt | 5 - libraries/optional-bare/LICENSE.txt | 23 - libraries/optional-bare/README.md | 5 - .../optional-bare/include/nonstd/optional | 508 ------------------ 16 files changed, 34 insertions(+), 613 deletions(-) delete mode 100644 libraries/optional-bare/CMakeLists.txt delete mode 100644 libraries/optional-bare/LICENSE.txt delete mode 100644 libraries/optional-bare/README.md delete mode 100644 libraries/optional-bare/include/nonstd/optional diff --git a/CMakeLists.txt b/CMakeLists.txt index 33c53b820..bc906ad4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,10 +29,10 @@ set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) ######## Set compiler flags ######## set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true) -set(CMAKE_CXX_STANDARD 11) -set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_C_STANDARD 17) include(GenerateExportHeader) -set(CMAKE_CXX_FLAGS "-Wall -pedantic -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() @@ -319,7 +319,6 @@ endif() add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions add_subdirectory(libraries/classparser) # class parser 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 add_subdirectory(libraries/gamemode) diff --git a/COPYING.md b/COPYING.md index 3f9608ffc..1dd18e173 100644 --- a/COPYING.md +++ b/COPYING.md @@ -295,34 +295,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# optional-bare - - Code from https://github.com/martinmoene/optional-bare/ - - Boost Software License - Version 1.0 - August 17th, 2003 - - Permission is hereby granted, free of charge, to any person or organization - obtaining a copy of the software and accompanying documentation covered by - this license (the "Software") to use, reproduce, display, distribute, - execute, and transmit the Software, and to prepare derivative works of the - Software, and to permit third-parties to whom the Software is furnished to - do so, all subject to the following: - - The copyright notices in the Software and this entire statement, including - the above license grant, this restriction and the following disclaimer, - must be included in all copies of the Software, in whole or in part, and - all derivative works of the Software, unless such copies or derivative - works are solely in the form of machine-executable object code generated by - a source language processor. - - 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT - SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE - 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 diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3c9aee6ae..b540dcab5 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -987,7 +987,6 @@ target_link_libraries(Launcher_logic Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} - optional-bare tomlc99 BuildConfig Katabasis diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index b67d48f3c..48ba2161d 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -44,7 +44,7 @@ #include "QObjectPtr.h" #include "modplatform/flame/PackManifest.h" -#include +#include class QuaZip; namespace Flame @@ -90,8 +90,8 @@ private slots: QString m_archivePath; bool m_downloadRequired = false; std::unique_ptr m_packZip; - QFuture> m_extractFuture; - QFutureWatcher> m_extractFutureWatcher; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; QVector m_blockedMods; enum class ModpackType{ Unknown, diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 1627ee07b..71cca03bc 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -267,7 +267,7 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re // ours -nonstd::optional MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) { QDir directory(target); QStringList extracted; @@ -276,7 +276,7 @@ nonstd::optional MMCZip::extractSubDir(QuaZip *zip, const QString & auto numEntries = zip->getEntriesCount(); if(numEntries < 0) { qWarning() << "Failed to enumerate files in archive"; - return nonstd::nullopt; + return std::nullopt; } else if(numEntries == 0) { qDebug() << "Extracting empty archives seems odd..."; @@ -285,7 +285,7 @@ nonstd::optional MMCZip::extractSubDir(QuaZip *zip, const QString & else if (!zip->goToFirstFile()) { qWarning() << "Failed to seek to first file in zip"; - return nonstd::nullopt; + return std::nullopt; } do @@ -322,7 +322,7 @@ nonstd::optional MMCZip::extractSubDir(QuaZip *zip, const QString & { qWarning() << "Failed to extract file" << original_name << "to" << absFilePath; JlCompress::removeFile(extracted); - return nonstd::nullopt; + return std::nullopt; } extracted.append(absFilePath); @@ -340,7 +340,7 @@ bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &tar } // ours -nonstd::optional MMCZip::extractDir(QString fileCompressed, QString dir) +std::optional MMCZip::extractDir(QString fileCompressed, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) @@ -351,13 +351,13 @@ nonstd::optional MMCZip::extractDir(QString fileCompressed, QString return QStringList(); } qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; - return nonstd::nullopt; + return std::nullopt; } return MMCZip::extractSubDir(&zip, "", dir); } // ours -nonstd::optional MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +std::optional MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) @@ -368,7 +368,7 @@ nonstd::optional MMCZip::extractDir(QString fileCompressed, QString return QStringList(); } qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; - return nonstd::nullopt; + return std::nullopt; } return MMCZip::extractSubDir(&zip, subdir, dir); } diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 7f43d1589..ce9775bdb 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -42,7 +42,7 @@ #include #include -#include +#include namespace MMCZip { @@ -95,7 +95,7 @@ namespace MMCZip /** * Extract a subdirectory from an archive */ - nonstd::optional extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + std::optional extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); bool extractRelFile(QuaZip *zip, const QString & file, const QString &target); @@ -106,7 +106,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 extractDir(QString fileCompressed, QString dir); + std::optional extractDir(QString fileCompressed, QString dir); /** * Extract a subdirectory from an archive @@ -116,7 +116,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 extractDir(QString fileCompressed, QString subdir, QString dir); + std::optional extractDir(QString fileCompressed, QString subdir, QString dir); /** * Extract a single file from an archive into a directory diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index dfcb43d88..90fcf3376 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -53,12 +53,12 @@ #include -#include +#include -using nonstd::optional; -using nonstd::nullopt; +using std::optional; +using std::nullopt; -GameType::GameType(nonstd::optional original): +GameType::GameType(std::optional original): original(original) { if(!original) { diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 0f587620b..8327253a3 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -16,11 +16,11 @@ #pragma once #include #include -#include +#include struct GameType { GameType() = default; - GameType (nonstd::optional original); + GameType (std::optional original); QString toTranslatedString() const; QString toLogString() const; @@ -33,7 +33,7 @@ struct GameType { Adventure, Spectator } type = Unknown; - nonstd::optional original; + std::optional original; }; class World diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index f55873e96..992ba9c53 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -46,7 +46,7 @@ #include "minecraft/PackProfile.h" #include "meta/Version.h" -#include +#include namespace ATLauncher { @@ -131,8 +131,8 @@ private slots: Meta::VersionPtr minecraftVersion; QMap componentsToInstall; - QFuture> m_extractFuture; - QFutureWatcher> m_extractFutureWatcher; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; QFuture m_modExtractFuture; QFutureWatcher m_modExtractFutureWatcher; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h index a73952209..da4c0da52 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.h +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -10,7 +10,7 @@ #include "net/NetJob.h" -#include +#include namespace LegacyFTB { @@ -46,8 +46,8 @@ private slots: shared_qobject_ptr m_network; bool abortable = false; std::unique_ptr m_packZip; - QFuture> m_extractFuture; - QFutureWatcher> m_extractFutureWatcher; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; NetJob::Ptr netJobContainer; QString archivePath; diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h index 4d1fcbff0..981ccf8a7 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.h +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -24,7 +24,7 @@ #include #include -#include +#include namespace Technic { @@ -57,8 +57,8 @@ private slots: QString m_archivePath; NetJob::Ptr m_filesNetJob; std::unique_ptr m_packZip; - QFuture> m_extractFuture; - QFutureWatcher> m_extractFutureWatcher; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; }; } // namespace Technic diff --git a/libraries/README.md b/libraries/README.md index 946e34d86..37f53385e 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -155,14 +155,6 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith Public domain (the author disclaimed the copyright). -## optional-bare - -A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later. - -Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81 - -Boost Software License - Version 1.0 - ## quazip A zip manipulation library, forked for MultiMC's use. diff --git a/libraries/optional-bare/CMakeLists.txt b/libraries/optional-bare/CMakeLists.txt deleted file mode 100644 index 952df6e28..000000000 --- a/libraries/optional-bare/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -cmake_minimum_required(VERSION 3.9.4) -project(optional-bare) - -add_library(optional-bare INTERFACE) -target_include_directories(optional-bare INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") diff --git a/libraries/optional-bare/LICENSE.txt b/libraries/optional-bare/LICENSE.txt deleted file mode 100644 index 36b7cd93c..000000000 --- a/libraries/optional-bare/LICENSE.txt +++ /dev/null @@ -1,23 +0,0 @@ -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -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, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -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. diff --git a/libraries/optional-bare/README.md b/libraries/optional-bare/README.md deleted file mode 100644 index e29ff7c14..000000000 --- a/libraries/optional-bare/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# optional bare - -A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later. - -Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81 diff --git a/libraries/optional-bare/include/nonstd/optional b/libraries/optional-bare/include/nonstd/optional deleted file mode 100644 index ecbfa030d..000000000 --- a/libraries/optional-bare/include/nonstd/optional +++ /dev/null @@ -1,508 +0,0 @@ -// -// Copyright 2017-2019 by Martin Moene -// -// https://github.com/martinmoene/optional-bare -// -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) - -#ifndef NONSTD_OPTIONAL_BARE_HPP -#define NONSTD_OPTIONAL_BARE_HPP - -#define optional_bare_MAJOR 1 -#define optional_bare_MINOR 1 -#define optional_bare_PATCH 0 - -#define optional_bare_VERSION optional_STRINGIFY(optional_bare_MAJOR) "." optional_STRINGIFY(optional_bare_MINOR) "." optional_STRINGIFY(optional_bare_PATCH) - -#define optional_STRINGIFY( x ) optional_STRINGIFY_( x ) -#define optional_STRINGIFY_( x ) #x - -// optional-bare configuration: - -#define optional_OPTIONAL_DEFAULT 0 -#define optional_OPTIONAL_NONSTD 1 -#define optional_OPTIONAL_STD 2 - -#if !defined( optional_CONFIG_SELECT_OPTIONAL ) -# define optional_CONFIG_SELECT_OPTIONAL ( optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD ) -#endif - -// Control presence of exception handling (try and auto discover): - -#ifndef optional_CONFIG_NO_EXCEPTIONS -# if _MSC_VER -# include // for _HAS_EXCEPTIONS -# endif -# if _MSC_VER -# include // for _HAS_EXCEPTIONS -# endif -# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS) -# define optional_CONFIG_NO_EXCEPTIONS 0 -# else -# define optional_CONFIG_NO_EXCEPTIONS 1 -# endif -#endif - -// C++ language version detection (C++20 is speculative): -// Note: VC14.0/1900 (VS2015) lacks too much from C++14. - -#ifndef optional_CPLUSPLUS -# if defined(_MSVC_LANG ) && !defined(__clang__) -# define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG ) -# else -# define optional_CPLUSPLUS __cplusplus -# endif -#endif - -#define optional_CPP98_OR_GREATER ( optional_CPLUSPLUS >= 199711L ) -#define optional_CPP11_OR_GREATER ( optional_CPLUSPLUS >= 201103L ) -#define optional_CPP14_OR_GREATER ( optional_CPLUSPLUS >= 201402L ) -#define optional_CPP17_OR_GREATER ( optional_CPLUSPLUS >= 201703L ) -#define optional_CPP20_OR_GREATER ( optional_CPLUSPLUS >= 202000L ) - -// C++ language version (represent 98 as 3): - -#define optional_CPLUSPLUS_V ( optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994) ) - -// Use C++17 std::optional if available and requested: - -#if optional_CPP17_OR_GREATER && defined(__has_include ) -# if __has_include( ) -# define optional_HAVE_STD_OPTIONAL 1 -# else -# define optional_HAVE_STD_OPTIONAL 0 -# endif -#else -# define optional_HAVE_STD_OPTIONAL 0 -#endif - -#define optional_USES_STD_OPTIONAL ( (optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) || ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL) ) - -// -// Using std::optional: -// - -#if optional_USES_STD_OPTIONAL - -#include -#include - -namespace nonstd { - - using std::in_place; - using std::in_place_type; - using std::in_place_index; - using std::in_place_t; - using std::in_place_type_t; - using std::in_place_index_t; - - using std::optional; - using std::bad_optional_access; - using std::hash; - - using std::nullopt; - using std::nullopt_t; - - using std::operator==; - using std::operator!=; - using std::operator<; - using std::operator<=; - using std::operator>; - using std::operator>=; - using std::make_optional; - using std::swap; -} - -#else // optional_USES_STD_OPTIONAL - -#include - -#if ! optional_CONFIG_NO_EXCEPTIONS -# include -#endif - -namespace nonstd { namespace optional_bare { - -// type for nullopt - -struct nullopt_t -{ - struct init{}; - nullopt_t( init ) {} -}; - -// extra parenthesis to prevent the most vexing parse: - -const nullopt_t nullopt(( nullopt_t::init() )); - -// optional access error. - -#if ! optional_CONFIG_NO_EXCEPTIONS - -class bad_optional_access : public std::logic_error -{ -public: - explicit bad_optional_access() - : logic_error( "bad optional access" ) {} -}; - -#endif // optional_CONFIG_NO_EXCEPTIONS - -// Simplistic optional: requires T to be default constructible, copyable. - -template< typename T > -class optional -{ -private: - typedef void (optional::*safe_bool)() const; - -public: - typedef T value_type; - - optional() - : has_value_( false ) - {} - - optional( nullopt_t ) - : has_value_( false ) - {} - - optional( T const & arg ) - : has_value_( true ) - , value_ ( arg ) - {} - - template< class U > - optional( optional const & other ) - : has_value_( other.has_value() ) - , value_ ( other.value() ) - {} - - optional & operator=( nullopt_t ) - { - reset(); - return *this; - } - - template< class U > - optional & operator=( optional const & other ) - { - has_value_ = other.has_value(); - value_ = other.value(); - return *this; - } - - void swap( optional & rhs ) - { - using std::swap; - if ( has_value() == true && rhs.has_value() == true ) { swap( **this, *rhs ); } - else if ( has_value() == false && rhs.has_value() == true ) { initialize( *rhs ); rhs.reset(); } - else if ( has_value() == true && rhs.has_value() == false ) { rhs.initialize( **this ); reset(); } - } - - // observers - - value_type const * operator->() const - { - return assert( has_value() ), - &value_; - } - - value_type * operator->() - { - return assert( has_value() ), - &value_; - } - - value_type const & operator*() const - { - return assert( has_value() ), - value_; - } - - value_type & operator*() - { - return assert( has_value() ), - value_; - } - -#if optional_CPP11_OR_GREATER - explicit operator bool() const - { - return has_value(); - } -#else - operator safe_bool() const - { - return has_value() ? &optional::this_type_does_not_support_comparisons : 0; - } -#endif - - bool has_value() const - { - return has_value_; - } - - value_type const & value() const - { -#if optional_CONFIG_NO_EXCEPTIONS - assert( has_value() ); -#else - if ( ! has_value() ) - throw bad_optional_access(); -#endif - return value_; - } - - value_type & value() - { -#if optional_CONFIG_NO_EXCEPTIONS - assert( has_value() ); -#else - if ( ! has_value() ) - throw bad_optional_access(); -#endif - return value_; - } - - template< class U > - value_type value_or( U const & v ) const - { - return has_value() ? value() : static_cast( v ); - } - - // modifiers - - void reset() - { - has_value_ = false; - } - -private: - void this_type_does_not_support_comparisons() const {} - - template< typename V > - void initialize( V const & value ) - { - assert( ! has_value() ); - value_ = value; - has_value_ = true; - } - -private: - bool has_value_; - value_type value_; -}; - -// Relational operators - -template< typename T, typename U > -inline bool operator==( optional const & x, optional const & y ) -{ - return bool(x) != bool(y) ? false : bool(x) == false ? true : *x == *y; -} - -template< typename T, typename U > -inline bool operator!=( optional const & x, optional const & y ) -{ - return !(x == y); -} - -template< typename T, typename U > -inline bool operator<( optional const & x, optional const & y ) -{ - return (!y) ? false : (!x) ? true : *x < *y; -} - -template< typename T, typename U > -inline bool operator>( optional const & x, optional const & y ) -{ - return (y < x); -} - -template< typename T, typename U > -inline bool operator<=( optional const & x, optional const & y ) -{ - return !(y < x); -} - -template< typename T, typename U > -inline bool operator>=( optional const & x, optional const & y ) -{ - return !(x < y); -} - -// Comparison with nullopt - -template< typename T > -inline bool operator==( optional const & x, nullopt_t ) -{ - return (!x); -} - -template< typename T > -inline bool operator==( nullopt_t, optional const & x ) -{ - return (!x); -} - -template< typename T > -inline bool operator!=( optional const & x, nullopt_t ) -{ - return bool(x); -} - -template< typename T > -inline bool operator!=( nullopt_t, optional const & x ) -{ - return bool(x); -} - -template< typename T > -inline bool operator<( optional const &, nullopt_t ) -{ - return false; -} - -template< typename T > -inline bool operator<( nullopt_t, optional const & x ) -{ - return bool(x); -} - -template< typename T > -inline bool operator<=( optional const & x, nullopt_t ) -{ - return (!x); -} - -template< typename T > -inline bool operator<=( nullopt_t, optional const & ) -{ - return true; -} - -template< typename T > -inline bool operator>( optional const & x, nullopt_t ) -{ - return bool(x); -} - -template< typename T > -inline bool operator>( nullopt_t, optional const & ) -{ - return false; -} - -template< typename T > -inline bool operator>=( optional const &, nullopt_t ) -{ - return true; -} - -template< typename T > -inline bool operator>=( nullopt_t, optional const & x ) -{ - return (!x); -} - -// Comparison with T - -template< typename T, typename U > -inline bool operator==( optional const & x, U const & v ) -{ - return bool(x) ? *x == v : false; -} - -template< typename T, typename U > -inline bool operator==( U const & v, optional const & x ) -{ - return bool(x) ? v == *x : false; -} - -template< typename T, typename U > -inline bool operator!=( optional const & x, U const & v ) -{ - return bool(x) ? *x != v : true; -} - -template< typename T, typename U > -inline bool operator!=( U const & v, optional const & x ) -{ - return bool(x) ? v != *x : true; -} - -template< typename T, typename U > -inline bool operator<( optional const & x, U const & v ) -{ - return bool(x) ? *x < v : true; -} - -template< typename T, typename U > -inline bool operator<( U const & v, optional const & x ) -{ - return bool(x) ? v < *x : false; -} - -template< typename T, typename U > -inline bool operator<=( optional const & x, U const & v ) -{ - return bool(x) ? *x <= v : true; -} - -template< typename T, typename U > -inline bool operator<=( U const & v, optional const & x ) -{ - return bool(x) ? v <= *x : false; -} - -template< typename T, typename U > -inline bool operator>( optional const & x, U const & v ) -{ - return bool(x) ? *x > v : false; -} - -template< typename T, typename U > -inline bool operator>( U const & v, optional const & x ) -{ - return bool(x) ? v > *x : true; -} - -template< typename T, typename U > -inline bool operator>=( optional const & x, U const & v ) -{ - return bool(x) ? *x >= v : false; -} - -template< typename T, typename U > -inline bool operator>=( U const & v, optional const & x ) -{ - return bool(x) ? v >= *x : true; -} - -// Specialized algorithms - -template< typename T > -void swap( optional & x, optional & y ) -{ - x.swap( y ); -} - -// Convenience function to create an optional. - -template< typename T > -inline optional make_optional( T const & v ) -{ - return optional( v ); -} - -} // namespace optional-bare - -using namespace optional_bare; - -} // namespace nonstd - -#endif // optional_USES_STD_OPTIONAL - -#endif // NONSTD_OPTIONAL_BARE_HPP From da0a7dbbd4803777896cba401f1c4b1c41841f20 Mon Sep 17 00:00:00 2001 From: fn2006 <92369097+fn2006@users.noreply.github.com> Date: Thu, 28 Jul 2022 23:30:19 +0100 Subject: [PATCH 02/14] Also include FTB in CurseForge workaround --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c1c9814f5..257a5ae89 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ If you have any problems open an issue here, do not bug the PolyMC maintainers. Binaries can be found in releases. -Workaround for downloading mods from CurseForge not working [here](https://github.com/fn2006/PollyMC/wiki/CurseForge-Workaround). +Workaround for downloading from CurseForge and FTB not working [here](https://github.com/fn2006/PollyMC/wiki/CurseForge-Workaround). -Workaround for downloading mods from Modrinth not working [here](https://github.com/fn2006/PollyMC/wiki/Modrinth-Workaround). +Workaround for downloading from Modrinth not working [here](https://github.com/fn2006/PollyMC/wiki/Modrinth-Workaround). To build this yourself, follow the instructions on the PolyMC website but clone this repo instead. From 1b1e90f4cd9c1e235474fbeff3fa9ed52ccd33fe Mon Sep 17 00:00:00 2001 From: Fintan Martin Date: Fri, 29 Jul 2022 11:33:14 +0100 Subject: [PATCH 03/14] Attempt to fix nix stuff --- flake.nix | 8 ++++---- nix/NIX.md | 16 ++++++++-------- nix/default.nix | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/flake.nix b/flake.nix index 51bc1fda0..ae4a0609f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; + description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of PolyMC)"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; @@ -22,14 +22,14 @@ pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); packagesFn = pkgs: rec { - polymc = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; - polymc-qt6 = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; }; + pollymc = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; + pollymc-qt6 = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; }; }; in { packages = forAllSystems (system: let packages = packagesFn pkgs.${system}; in - packages // { default = packages.polymc; } + packages // { default = packages.pollymc; } ); overlay = final: packagesFn; diff --git a/nix/NIX.md b/nix/NIX.md index 1ceba9a3c..565b1fb61 100644 --- a/nix/NIX.md +++ b/nix/NIX.md @@ -3,29 +3,29 @@ To import with flakes use ```nix inputs = { - polymc.url = "github:PolyMC/PolyMC"; + pollymc.url = "github:fn2006/PollyMC"; }; ... -nixpkgs.overlays = [ inputs.polymc.overlay ]; ## Within configuration.nix -environment.systemPackages = with pkgs; [ polymc ]; ## +nixpkgs.overlays = [ inputs.pollymc.overlay ]; ## Within configuration.nix +environment.systemPackages = with pkgs; [ pollymc ]; ## ``` To import without flakes use channels: ``` -nix-channel --add https://github.com/PolyMC/PolyMC/archive/master.tar.gz polymc -nix-channel --update polymc -nix-env -iA polymc +nix-channel --add https://github.com/fn2006/PollyMC/archive/master.tar.gz pollymc +nix-channel --update pollymc +nix-env -iA pollymc ``` or alternatively you can use ``` nixpkgs.overlays = [ - (import (builtins.fetchTarball "https://github.com/PolyMC/PolyMC/archive/develop.tar.gz")).overlay + (import (builtins.fetchTarball "https://github.com/fn2006/PollyMC/archive/develop.tar.gz")).overlay ]; -environment.systemPackages = with pkgs; [ polymc ]; +environment.systemPackages = with pkgs; [ pollymc ]; ``` diff --git a/nix/default.nix b/nix/default.nix index 42ddda188..63bcf925c 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -36,14 +36,14 @@ let libGL ]; - # This variable will be passed to Minecraft by PolyMC + # This variable will be passed to Minecraft by PollyMC gameLibraryPath = libpath + ":/run/opengl-driver/lib"; javaPaths = lib.makeSearchPath "bin/java" ([ jdk jdk8 ] ++ extraJDKs); in stdenv.mkDerivation rec { - pname = "polymc"; + pname = "pollymc"; inherit version; src = lib.cleanSource self; @@ -70,7 +70,7 @@ stdenv.mkDerivation rec { # we have to check if the system is NixOS before adding stdenv.cc.cc.lib (#923) postInstall = '' # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 - wrapQtApp $out/bin/polymc \ + wrapQtApp $out/bin/pollymc \ --run '[ -f /etc/NIXOS ] && export LD_LIBRARY_PATH="${stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"' \ --prefix LD_LIBRARY_PATH : ${gameLibraryPath} \ --prefix POLYMC_JAVA_PATHS : ${javaPaths} \ @@ -78,9 +78,9 @@ stdenv.mkDerivation rec { ''; meta = with lib; { - homepage = "https://polymc.org/"; - downloadPage = "https://polymc.org/download/"; - changelog = "https://github.com/PolyMC/PolyMC/releases"; + homepage = "https://github.com/fn2006/PollyMC"; + downloadPage = "https://github.com/fn2006/PollyMC/releases"; + changelog = "https://github.com/fn2006/PollyMC/releases"; description = "A free, open source launcher for Minecraft"; longDescription = '' Allows you to have multiple, separate instances of Minecraft (each with From 22f5011451088857d470733aed4b47952eb2887a Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 29 Jul 2022 16:25:12 +0200 Subject: [PATCH 04/14] fix(winget): strictly match non-Legacy setup Signed-off-by: Sefa Eyeoglu --- .github/workflows/winget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index b8ecce133..98981e809 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -10,5 +10,5 @@ jobs: - uses: vedantmgoyal2009/winget-releaser@latest with: identifier: PolyMC.PolyMC - installers-regex: '\.exe$' + installers-regex: 'PolyMC-Windows-Setup-.+\.exe$' token: ${{ secrets.WINGET_TOKEN }} From 842b7e6c3974c16adddf3cccda4e0008d8c64597 Mon Sep 17 00:00:00 2001 From: LennyMcLennington Date: Sat, 30 Jul 2022 15:35:48 +0100 Subject: [PATCH 05/14] use c11 instead c17 dont work properly with lgtm's build system and c11 is already almost identical to c17 at least in gcc --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bc906ad4e..62724323c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 17) -set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) From b15544c163ccbca08ea498b3b3a51c82d7fb3e12 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Sun, 31 Jul 2022 01:42:33 +0800 Subject: [PATCH 06/14] Trash instances instead of deleting (when possible) (#549) Squashed because of :pofat: commit history --- launcher/FileSystem.cpp | 227 ++++++++------------ launcher/FileSystem.h | 51 +++-- launcher/InstanceList.cpp | 428 +++++++++++++++++-------------------- launcher/InstanceList.h | 13 ++ launcher/ui/MainWindow.cpp | 32 ++- launcher/ui/MainWindow.h | 1 + 6 files changed, 353 insertions(+), 399 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index ebb4460dc..21edbb484 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -35,76 +35,64 @@ #include "FileSystem.h" +#include #include #include -#include #include -#include -#include +#include #include #include +#include #if defined Q_OS_WIN32 - #include - #include - #include - #include - #include - #include - #include - #include - #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #else - #include +#include #endif namespace FS { -void ensureExists(const QDir &dir) +void ensureExists(const QDir& dir) { - if (!QDir().mkpath(dir.absolutePath())) - { - throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + - dir.absolutePath() + ")"); + if (!QDir().mkpath(dir.absolutePath())) { + throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")"); } } -void write(const QString &filename, const QByteArray &data) +void write(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) - { - throw FileSystemException("Couldn't open " + filename + " for writing: " + - file.errorString()); + if (!file.open(QSaveFile::WriteOnly)) { + throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } - if (data.size() != file.write(data)) - { - throw FileSystemException("Error writing data to " + filename + ": " + - file.errorString()); + if (data.size() != file.write(data)) { + throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } - if (!file.commit()) - { - throw FileSystemException("Error while committing data to " + filename + ": " + - file.errorString()); + if (!file.commit()) { + throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); } } -QByteArray read(const QString &filename) +QByteArray read(const QString& filename) { QFile file(filename); - if (!file.open(QFile::ReadOnly)) - { - throw FileSystemException("Unable to open " + filename + " for reading: " + - file.errorString()); + if (!file.open(QFile::ReadOnly)) { + throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString()); } const qint64 size = file.size(); QByteArray data(int(size), 0); const qint64 ret = file.read(data.data(), size); - if (ret == -1 || ret != size) - { - throw FileSystemException("Error reading data from " + filename + ": " + - file.errorString()); + if (ret == -1 || ret != size) { + throw FileSystemException("Error reading data from " + filename + ": " + file.errorString()); } return data; } @@ -138,12 +126,12 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } -bool copy::operator()(const QString &offset) +bool copy::operator()(const QString& offset) { - //NOTE always deep copy on windows. the alternatives are too messy. - #if defined Q_OS_WIN32 +// NOTE always deep copy on windows. the alternatives are too messy. +#if defined Q_OS_WIN32 m_followSymlinks = true; - #endif +#endif auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); @@ -152,52 +140,39 @@ bool copy::operator()(const QString &offset) if (!currentSrc.exists()) return false; - if(!m_followSymlinks && currentSrc.isSymLink()) - { + if (!m_followSymlinks && currentSrc.isSymLink()) { qDebug() << "creating symlink" << src << " - " << dst; - if (!ensureFilePathExists(dst)) - { + if (!ensureFilePathExists(dst)) { qWarning() << "Cannot create path!"; return false; } return QFile::link(currentSrc.symLinkTarget(), dst); - } - else if(currentSrc.isFile()) - { + } else if (currentSrc.isFile()) { qDebug() << "copying file" << src << " - " << dst; - if (!ensureFilePathExists(dst)) - { + if (!ensureFilePathExists(dst)) { qWarning() << "Cannot create path!"; return false; } return QFile::copy(src, dst); - } - else if(currentSrc.isDir()) - { + } else if (currentSrc.isDir()) { qDebug() << "recursing" << offset; - if (!ensureFolderPathExists(dst)) - { + if (!ensureFolderPathExists(dst)) { qWarning() << "Cannot create path!"; return false; } QDir currentDir(src); - for(auto & f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) - { + for (auto& f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) { auto inner_offset = PathCombine(offset, f); // ignore and skip stuff that matches the blacklist. - if(m_blacklist && m_blacklist->matches(inner_offset)) - { + if (m_blacklist && m_blacklist->matches(inner_offset)) { continue; } - if(!operator()(inner_offset)) - { + if (!operator()(inner_offset)) { qWarning() << "Failed to copy" << inner_offset; return false; } } - } - else - { + } else { qCritical() << "Copy ERROR: Unknown filesystem object:" << src; return false; } @@ -208,55 +183,41 @@ bool deletePath(QString path) { bool OK = true; QFileInfo finfo(path); - if(finfo.isFile()) { + if (finfo.isFile()) { return QFile::remove(path); } QDir dir(path); - if (!dir.exists()) - { + if (!dir.exists()) { return OK; } - auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | - QDir::AllDirs | QDir::Files, - QDir::DirsFirst); + auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst); - for(auto & info: allEntries) - { + for (auto& info : allEntries) { #if defined Q_OS_WIN32 QString nativePath = QDir::toNativeSeparators(info.absoluteFilePath()); auto wString = nativePath.toStdWString(); DWORD dwAttrs = GetFileAttributesW(wString.c_str()); // Windows: check for junctions, reparse points and other nasty things of that sort - if(dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) - { - if (info.isFile()) - { + if (dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) { + if (info.isFile()) { OK &= QFile::remove(info.absoluteFilePath()); - } - else if (info.isDir()) - { + } else if (info.isDir()) { OK &= dir.rmdir(info.absoluteFilePath()); } } #else // We do not trust Qt with reparse points, but do trust it with unix symlinks. - if(info.isSymLink()) - { + if (info.isSymLink()) { OK &= QFile::remove(info.absoluteFilePath()); } #endif - else if (info.isDir()) - { + else if (info.isDir()) { OK &= deletePath(info.absoluteFilePath()); - } - else if (info.isFile()) - { + } else if (info.isFile()) { OK &= QFile::remove(info.absoluteFilePath()); - } - else - { + } else { OK = false; qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath(); } @@ -265,22 +226,30 @@ bool deletePath(QString path) return OK; } +bool trash(QString path, QString *pathInTrash = nullptr) +{ +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + return false; +#else + return QFile::moveToTrash(path, pathInTrash); +#endif +} -QString PathCombine(const QString & path1, const QString & path2) +QString PathCombine(const QString& path1, const QString& path2) { - if(!path1.size()) + if (!path1.size()) return path2; - if(!path2.size()) + if (!path2.size()) return path1; return QDir::cleanPath(path1 + QDir::separator() + path2); } -QString PathCombine(const QString & path1, const QString & path2, const QString & path3) +QString PathCombine(const QString& path1, const QString& path2, const QString& path3) { return PathCombine(PathCombine(path1, path2), path3); } -QString PathCombine(const QString & path1, const QString & path2, const QString & path3, const QString & path4) +QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4) { return PathCombine(PathCombine(path1, path2, path3), path4); } @@ -292,17 +261,14 @@ QString AbsolutePath(QString path) QString ResolveExecutable(QString path) { - if (path.isEmpty()) - { + if (path.isEmpty()) { return QString(); } - if(!path.contains('/')) - { + if (!path.contains('/')) { path = QStandardPaths::findExecutable(path); } QFileInfo pathInfo(path); - if(!pathInfo.exists() || !pathInfo.isExecutable()) - { + if (!pathInfo.exists() || !pathInfo.isExecutable()) { return QString(); } return pathInfo.absoluteFilePath(); @@ -322,12 +288,9 @@ QString NormalizePath(QString path) QDir b(path); QString newAbsolute = b.absolutePath(); - if (newAbsolute.startsWith(currentAbsolute)) - { + if (newAbsolute.startsWith(currentAbsolute)) { return a.relativeFilePath(newAbsolute); - } - else - { + } else { return newAbsolute; } } @@ -336,10 +299,8 @@ QString badFilenameChars = "\"\\/?<>:;*|!+\r\n"; QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) { - for (int i = 0; i < string.length(); i++) - { - if (badFilenameChars.contains(string[i])) - { + for (int i = 0; i < string.length(); i++) { + if (badFilenameChars.contains(string[i])) { string[i] = replaceWith; } } @@ -351,15 +312,12 @@ QString DirNameFromString(QString string, QString inDir) int num = 0; QString baseName = RemoveInvalidFilenameChars(string, '-'); QString dirName; - do - { - if(num == 0) - { + do { + if (num == 0) { dirName = baseName; - } - else - { - dirName = baseName + QString::number(num);; + } else { + dirName = baseName + QString::number(num); + ; } // If it's over 9000 @@ -383,36 +341,31 @@ bool checkProblemticPathJava(QDir folder) bool called_coinit = false; -HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args) +HRESULT CreateLink(LPCCH linkPath, LPCWSTR targetPath, LPCWSTR args) { HRESULT hres; - if (!called_coinit) - { + if (!called_coinit) { hres = CoInitialize(NULL); called_coinit = true; - if (!SUCCEEDED(hres)) - { + if (!SUCCEEDED(hres)) { qWarning("Failed to initialize COM. Error 0x%08lX", hres); return hres; } } - IShellLinkA *link; - hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, - (LPVOID *)&link); + IShellLink* link; + hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&link); - if (SUCCEEDED(hres)) - { - IPersistFile *persistFile; + if (SUCCEEDED(hres)) { + IPersistFile* persistFile; link->SetPath(targetPath); link->SetArguments(args); - hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile); - if (SUCCEEDED(hres)) - { + hres = link->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); + if (SUCCEEDED(hres)) { WCHAR wstr[MAX_PATH]; MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH); @@ -433,8 +386,7 @@ QString getDesktopDir() } // Cross-platform Shortcut creation -bool createShortCut(QString location, QString dest, QStringList args, QString name, - QString icon) +bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon) { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) location = PathCombine(location, name + ".desktop"); @@ -459,8 +411,7 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na stream.flush(); f.close(); - f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | - QFileDevice::ExeOther); + f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); return true; #elif defined Q_OS_WIN diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index fd305b014..b46f32812 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -41,29 +41,27 @@ #include #include -namespace FS -{ +namespace FS { -class FileSystemException : public ::Exception -{ -public: - FileSystemException(const QString &message) : Exception(message) {} +class FileSystemException : public ::Exception { + public: + FileSystemException(const QString& message) : Exception(message) {} }; /** * write data to a file safely */ -void write(const QString &filename, const QByteArray &data); +void write(const QString& filename, const QByteArray& data); /** * read data from a file safely\ */ -QByteArray read(const QString &filename); +QByteArray read(const QString& filename); /** * Update the last changed timestamp of an existing file */ -bool updateTimestamp(const QString & filename); +bool updateTimestamp(const QString& filename); /** * Creates all the folders in a path for the specified path @@ -77,35 +75,31 @@ bool ensureFilePathExists(QString filenamepath); */ bool ensureFolderPathExists(QString filenamepath); -class copy -{ -public: - copy(const QString & src, const QString & dst) +class copy { + public: + copy(const QString& src, const QString& dst) { m_src.setPath(src); m_dst.setPath(dst); } - copy & followSymlinks(const bool follow) + copy& followSymlinks(const bool follow) { m_followSymlinks = follow; return *this; } - copy & blacklist(const IPathMatcher * filter) + copy& blacklist(const IPathMatcher* filter) { m_blacklist = filter; return *this; } - bool operator()() - { - return operator()(QString()); - } + bool operator()() { return operator()(QString()); } -private: - bool operator()(const QString &offset); + private: + bool operator()(const QString& offset); -private: + private: bool m_followSymlinks = true; - const IPathMatcher * m_blacklist = nullptr; + const IPathMatcher* m_blacklist = nullptr; QDir m_src; QDir m_dst; }; @@ -115,9 +109,14 @@ class copy */ bool deletePath(QString path); -QString PathCombine(const QString &path1, const QString &path2); -QString PathCombine(const QString &path1, const QString &path2, const QString &path3); -QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4); +/** + * Trash a folder / file + */ +bool trash(QString path, QString *pathInTrash); + +QString PathCombine(const QString& path1, const QString& path2); +QString PathCombine(const QString& path1, const QString& path2, const QString& path3); +QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4); QString AbsolutePath(QString path); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index fb7103dd3..4447a17c4 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -33,30 +33,32 @@ * limitations under the License. */ +#include #include #include -#include #include -#include -#include -#include -#include -#include #include -#include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include -#include "InstanceList.h" #include "BaseInstance.h" +#include "ExponentialSeries.h" +#include "FileSystem.h" +#include "InstanceList.h" #include "InstanceTask.h" -#include "settings/INISettingsObject.h" #include "NullInstance.h" -#include "minecraft/MinecraftInstance.h" -#include "FileSystem.h" -#include "ExponentialSeries.h" #include "WatchLock.h" +#include "minecraft/MinecraftInstance.h" +#include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 #include @@ -64,13 +66,12 @@ const static int GROUP_FILE_FORMAT_VERSION = 1; -InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent) +InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) { resumeWatch(); // Create aand normalize path - if (!QDir::current().exists(instDir)) - { + if (!QDir::current().exists(instDir)) { QDir::current().mkpath(instDir); } @@ -83,9 +84,7 @@ InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, m_watcher->addPath(m_instDir); } -InstanceList::~InstanceList() -{ -} +InstanceList::~InstanceList() {} Qt::DropActions InstanceList::supportedDragActions() const { @@ -99,7 +98,7 @@ Qt::DropActions InstanceList::supportedDropActions() const bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const { - if(data && data->hasFormat("application/x-instanceid")) { + if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; @@ -107,7 +106,7 @@ bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, bool InstanceList::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) { - if(data && data->hasFormat("application/x-instanceid")) { + if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; @@ -120,35 +119,33 @@ QStringList InstanceList::mimeTypes() const return types; } -QMimeData * InstanceList::mimeData(const QModelIndexList& indexes) const +QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const { auto mimeData = QAbstractListModel::mimeData(indexes); - if(indexes.size() == 1) { + if (indexes.size() == 1) { auto instanceId = data(indexes[0], InstanceIDRole).toString(); mimeData->setData("application/x-instanceid", instanceId.toUtf8()); } return mimeData; } - -int InstanceList::rowCount(const QModelIndex &parent) const +int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_instances.count(); } -QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const +QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); if (row < 0 || row >= m_instances.size()) return QModelIndex(); - return createIndex(row, column, (void *)m_instances.at(row).get()); + return createIndex(row, column, (void*)m_instances.at(row).get()); } -QVariant InstanceList::data(const QModelIndex &index, int role) const +QVariant InstanceList::data(const QModelIndex& index, int role) const { - if (!index.isValid()) - { + if (!index.isValid()) { return QVariant(); } BaseInstance *pdata = static_cast(index.internalPointer()); @@ -193,29 +190,25 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid()) - { + if (!index.isValid()) { return false; } - if(role != Qt::EditRole) - { + if (role != Qt::EditRole) { return false; } - BaseInstance *pdata = static_cast(index.internalPointer()); + BaseInstance* pdata = static_cast(index.internalPointer()); auto newName = value.toString(); - if(pdata->name() == newName) - { + if (pdata->name() == newName) { return true; } pdata->setName(newName); return true; } -Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const +Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const { Qt::ItemFlags f; - if (index.isValid()) - { + if (index.isValid()) { f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } return f; @@ -224,13 +217,11 @@ Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const GroupId InstanceList::getInstanceGroup(const InstanceId& id) const { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { return GroupId(); } auto iter = m_instanceGroupIndex.find(inst->id()); - if(iter != m_instanceGroupIndex.end()) - { + if (iter != m_instanceGroupIndex.end()) { return *iter; } return GroupId(); @@ -239,33 +230,27 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { qDebug() << "Attempt to set a null instance's group"; return; } bool changed = false; auto iter = m_instanceGroupIndex.find(inst->id()); - if(iter != m_instanceGroupIndex.end()) - { - if(*iter != name) - { + if (iter != m_instanceGroupIndex.end()) { + if (*iter != name) { *iter = name; changed = true; } - } - else - { + } else { changed = true; m_instanceGroupIndex[id] = name; } - if(changed) - { + if (changed) { m_groupNameCache.insert(name); auto idx = getInstIndex(inst.get()); - emit dataChanged(index(idx), index(idx), {GroupRole}); + emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); } } @@ -279,24 +264,20 @@ void InstanceList::deleteGroup(const QString& name) { bool removed = false; qDebug() << "Delete group" << name; - for(auto & instance: m_instances) - { - const auto & instID = instance->id(); + for (auto& instance : m_instances) { + const auto& instID = instance->id(); auto instGroupName = getInstanceGroup(instID); - if(instGroupName == name) - { + if (instGroupName == name) { m_instanceGroupIndex.remove(instID); qDebug() << "Remove" << instID << "from group" << name; removed = true; auto idx = getInstIndex(instance.get()); - if(idx > 0) - { - emit dataChanged(index(idx), index(idx), {GroupRole}); + if (idx > 0) { + emit dataChanged(index(idx), index(idx), { GroupRole }); } } } - if(removed) - { + if (removed) { saveGroupList(); } } @@ -306,23 +287,75 @@ bool InstanceList::isGroupCollapsed(const QString& group) return m_collapsedGroups.contains(group); } +bool InstanceList::trashInstance(const InstanceId& id) +{ + auto inst = getInstanceById(id); + if (!inst) { + qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; + return false; + } + + auto cachedGroupId = m_instanceGroupIndex[id]; + + qDebug() << "Will trash instance" << id; + QString trashedLoc; + + if (m_instanceGroupIndex.remove(id)) { + saveGroupList(); + } + + if (!FS::trash(inst->instanceRoot(), &trashedLoc)) { + qDebug() << "Trash of instance" << id << "has not been completely successfully..."; + return false; + } + + qDebug() << "Instance" << id << "has been trashed by the launcher."; + m_trashHistory.push({id, inst->instanceRoot(), trashedLoc, cachedGroupId}); + + return true; +} + +bool InstanceList::trashedSomething() { + return !m_trashHistory.empty(); +} + +void InstanceList::undoTrashInstance() { + if (m_trashHistory.empty()) { + qWarning() << "Nothing to recover from trash."; + return; + } + + auto top = m_trashHistory.pop(); + + while (QDir(top.polyPath).exists()) { + top.id += "1"; + top.polyPath += "1"; + } + + qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath; + QFile(top.trashPath).rename(top.polyPath); + + m_instanceGroupIndex[top.id] = top.groupName; + m_groupNameCache.insert(top.groupName); + + saveGroupList(); + emit instancesChanged(); +} + void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); - if(!inst) - { + if (!inst) { qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } - if(m_instanceGroupIndex.remove(id)) - { + if (m_instanceGroupIndex.remove(id)) { saveGroupList(); } qDebug() << "Will delete instance" << id; - if(!FS::deletePath(inst->instanceRoot())) - { + if (!FS::deletePath(inst->instanceRoot())) { qWarning() << "Deletion of instance" << id << "has not been completely successful ..."; return; } @@ -330,15 +363,13 @@ void InstanceList::deleteInstance(const InstanceId& id) qDebug() << "Instance" << id << "has been deleted by the launcher."; } -static QMap getIdMapping(const QList &list) +static QMap getIdMapping(const QList& list) { QMap out; int i = 0; - for(auto & item: list) - { + for (auto& item : list) { auto id = item->id(); - if(out.contains(id)) - { + if (out.contains(id)) { qWarning() << "Duplicate ID" << id << "in instance list"; } out[id] = std::make_pair(item, i); @@ -347,24 +378,21 @@ static QMap getIdMapping(const QList & return out; } -QList< InstanceId > InstanceList::discoverInstances() +QList InstanceList::discoverInstances() { qDebug() << "Discovering instances in" << m_instDir; QList out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); - while (iter.hasNext()) - { + while (iter.hasNext()) { QString subDir = iter.next(); QFileInfo dirInfo(subDir); if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) continue; // if it is a symlink, ignore it if it goes to the instance folder - if(dirInfo.isSymLink()) - { + if (dirInfo.isSymLink()) { QFileInfo targetInfo(dirInfo.symLinkTarget()); QFileInfo instDirInfo(m_instDir); - if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) - { + if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) { qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; continue; } @@ -388,74 +416,56 @@ InstanceList::InstListError InstanceList::loadList() QList newList; - for(auto & id: discoverInstances()) - { - if(existingIds.contains(id)) - { + for (auto& id : discoverInstances()) { + if (existingIds.contains(id)) { auto instPair = existingIds[id]; existingIds.remove(id); qDebug() << "Should keep and soft-reload" << id; - } - else - { + } else { InstancePtr instPtr = loadInstance(id); - if(instPtr) - { + if (instPtr) { newList.append(instPtr); } } } // TODO: looks like a general algorithm with a few specifics inserted. Do something about it. - if(!existingIds.isEmpty()) - { + if (!existingIds.isEmpty()) { // get the list of removed instances and sort it by their original index, from last to first auto deadList = existingIds.values(); - auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool - { - return a.second > b.second; - }; + auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; }; std::sort(deadList.begin(), deadList.end(), orderSortPredicate); // remove the contiguous ranges of rows int front_bookmark = -1; int back_bookmark = -1; int currentItem = -1; - auto removeNow = [&]() - { + auto removeNow = [&]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); endRemoveRows(); front_bookmark = -1; back_bookmark = currentItem; }; - for(auto & removedItem: deadList) - { + for (auto& removedItem : deadList) { auto instPtr = removedItem.first; instPtr->invalidate(); currentItem = removedItem.second; - if(back_bookmark == -1) - { + if (back_bookmark == -1) { // no bookmark yet back_bookmark = currentItem; - } - else if(currentItem == front_bookmark - 1) - { + } else if (currentItem == front_bookmark - 1) { // part of contiguous sequence, continue - } - else - { + } else { // seam between previous and current item removeNow(); } front_bookmark = currentItem; } - if(back_bookmark != -1) - { + if (back_bookmark != -1) { removeNow(); } } - if(newList.size()) - { + if (newList.size()) { add(newList); } m_dirty = false; @@ -466,26 +476,23 @@ InstanceList::InstListError InstanceList::loadList() void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; - for(auto const& itr : m_instances) - { + for (auto const& itr : m_instances) { totalPlayTime += itr.get()->totalTimePlayed(); } } void InstanceList::saveNow() { - for(auto & item: m_instances) - { + for (auto& item : m_instances) { item->saveNow(); } } -void InstanceList::add(const QList &t) +void InstanceList::add(const QList& t) { beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); m_instances.append(t); - for(auto & ptr : t) - { + for (auto& ptr : t) { connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); @@ -493,69 +500,61 @@ void InstanceList::add(const QList &t) void InstanceList::resumeWatch() { - if(m_watchLevel > 0) - { + if (m_watchLevel > 0) { qWarning() << "Bad suspend level resume in instance list"; return; } m_watchLevel++; - if(m_watchLevel > 0 && m_dirty) - { + if (m_watchLevel > 0 && m_dirty) { loadList(); } } void InstanceList::suspendWatch() { - m_watchLevel --; + m_watchLevel--; } void InstanceList::providerUpdated() { m_dirty = true; - if(m_watchLevel == 1) - { + if (m_watchLevel == 1) { loadList(); } } InstancePtr InstanceList::getInstanceById(QString instId) const { - if(instId.isEmpty()) + if (instId.isEmpty()) return InstancePtr(); - for(auto & inst: m_instances) - { - if (inst->id() == instId) - { + for (auto& inst : m_instances) { + if (inst->id() == instId) { return inst; } } return InstancePtr(); } -QModelIndex InstanceList::getInstanceIndexById(const QString &id) const +QModelIndex InstanceList::getInstanceIndexById(const QString& id) const { return index(getInstIndex(getInstanceById(id).get())); } -int InstanceList::getInstIndex(BaseInstance *inst) const +int InstanceList::getInstIndex(BaseInstance* inst) const { int count = m_instances.count(); - for (int i = 0; i < count; i++) - { - if (inst == m_instances[i].get()) - { + for (int i = 0; i < count; i++) { + if (inst == m_instances[i].get()) { return i; } } return -1; } -void InstanceList::propertiesChanged(BaseInstance *inst) +void InstanceList::propertiesChanged(BaseInstance* inst) { int i = getInstIndex(inst); - if (i != -1) - { + if (i != -1) { emit dataChanged(index(i), index(i)); updateTotalPlayTime(); } @@ -563,8 +562,7 @@ void InstanceList::propertiesChanged(BaseInstance *inst) InstancePtr InstanceList::loadInstance(const InstanceId& id) { - if(!m_groupsLoaded) - { + if (!m_groupsLoaded) { loadGroupList(); } @@ -592,50 +590,42 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) void InstanceList::saveGroupList() { qDebug() << "Will save group list now."; - if(!m_instancesProbed) - { + if (!m_instancesProbed) { qDebug() << "Group saving prevented because we don't know the full list of instances yet."; return; } WatchLock foo(m_watcher, m_instDir); QString groupFileName = m_instDir + "/instgroups.json"; QMap> reverseGroupMap; - for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) - { + for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { QString id = iter.key(); QString group = iter.value(); if (group.isEmpty()) continue; - if(!instanceSet.contains(id)) - { + if (!instanceSet.contains(id)) { qDebug() << "Skipping saving missing instance" << id << "to groups list."; continue; } - if (!reverseGroupMap.count(group)) - { + if (!reverseGroupMap.count(group)) { QSet set; set.insert(id); reverseGroupMap[group] = set; - } - else - { - QSet &set = reverseGroupMap[group]; + } else { + QSet& set = reverseGroupMap[group]; set.insert(id); } } QJsonObject toplevel; toplevel.insert("formatVersion", QJsonValue(QString("1"))); QJsonObject groupsArr; - for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) - { + for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) { auto list = iter.value(); auto name = iter.key(); QJsonObject groupObj; QJsonArray instanceArr; groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); - for (auto item : list) - { + for (auto item : list) { instanceArr.append(QJsonValue(item)); } groupObj.insert("instances", instanceArr); @@ -643,13 +633,10 @@ void InstanceList::saveGroupList() } toplevel.insert("groups", groupsArr); QJsonDocument doc(toplevel); - try - { + try { FS::write(groupFileName, doc.toJson()); qDebug() << "Group list saved."; - } - catch (const FS::FileSystemException &e) - { + } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write instance group file :" << e.cause(); } } @@ -665,12 +652,9 @@ void InstanceList::loadGroupList() return; QByteArray jsonData; - try - { + try { jsonData = FS::read(groupFileName); - } - catch (const FS::FileSystemException &e) - { + } catch (const FS::FileSystemException& e) { qCritical() << "Failed to read instance group file :" << e.cause(); return; } @@ -679,17 +663,15 @@ void InstanceList::loadGroupList() QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); // if the json was bad, fail - if (error.error != QJsonParseError::NoError) - { + if (error.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse instance group file: %1 at offset %2") - .arg(error.errorString(), QString::number(error.offset)) - .toUtf8(); + .arg(error.errorString(), QString::number(error.offset)) + .toUtf8(); return; } // if the root of the json wasn't an object, fail - if (!jsonDoc.isObject()) - { + if (!jsonDoc.isObject()) { qWarning() << "Invalid group file. Root entry should be an object."; return; } @@ -701,8 +683,7 @@ void InstanceList::loadGroupList() return; // Get the groups. if it's not an object, fail - if (!rootObj.value("groups").isObject()) - { + if (!rootObj.value("groups").isObject()) { qWarning() << "Invalid group list JSON: 'groups' should be an object."; return; } @@ -712,21 +693,20 @@ void InstanceList::loadGroupList() // Iterate through all the groups. QJsonObject groupMapping = rootObj.value("groups").toObject(); - for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) - { + for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) { QString groupName = iter.key(); // If not an object, complain and skip to the next one. - if (!iter.value().isObject()) - { + if (!iter.value().isObject()) { qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8(); continue; } QJsonObject groupObj = iter.value().toObject(); - if (!groupObj.value("instances").isArray()) - { - qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8(); + if (!groupObj.value("instances").isArray()) { + qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.") + .arg(groupName) + .toUtf8(); continue; } @@ -734,15 +714,14 @@ void InstanceList::loadGroupList() groupSet.insert(groupName); auto hidden = groupObj.value("hidden").toBool(false); - if(hidden) { + if (hidden) { m_collapsedGroups.insert(groupName); } // Iterate through the list of instances in the group. QJsonArray instancesArray = groupObj.value("instances").toArray(); - for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) - { + for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) { m_instanceGroupIndex[(*iter2).toString()] = groupName; } } @@ -757,13 +736,11 @@ void InstanceList::instanceDirContentsChanged(const QString& path) emit instancesChanged(); } -void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) +void InstanceList::on_InstFolderChanged(const Setting& setting, QVariant value) { QString newInstDir = QDir(value.toString()).canonicalPath(); - if(newInstDir != m_instDir) - { - if(m_groupsLoaded) - { + if (newInstDir != m_instDir) { + if (m_groupsLoaded) { saveGroupList(); } m_instDir = newInstDir; @@ -775,7 +752,7 @@ void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) { qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded"); - if(collapsed) { + if (collapsed) { m_collapsedGroups.insert(group); } else { m_collapsedGroups.remove(group); @@ -783,19 +760,14 @@ void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) saveGroupList(); } -class InstanceStaging : public Task -{ -Q_OBJECT +class InstanceStaging : public Task { + Q_OBJECT const unsigned minBackoff = 1; const unsigned maxBackoff = 16; -public: - InstanceStaging ( - InstanceList * parent, - Task * child, - const QString & stagingPath, - const QString& instanceName, - const QString& groupName ) - : backoff(minBackoff, maxBackoff) + + public: + InstanceStaging(InstanceList* parent, Task* child, const QString& stagingPath, const QString& instanceName, const QString& groupName) + : backoff(minBackoff, maxBackoff) { m_parent = parent; m_child.reset(child); @@ -810,62 +782,51 @@ Q_OBJECT connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); } - virtual ~InstanceStaging() {}; - + virtual ~InstanceStaging(){}; // FIXME/TODO: add ability to abort during instance commit retries bool abort() override { - if(m_child && m_child->canAbort()) - { + if (m_child && m_child->canAbort()) { return m_child->abort(); } return false; } bool canAbort() const override { - if(m_child && m_child->canAbort()) - { + if (m_child && m_child->canAbort()) { return true; } return false; } -protected: - virtual void executeTask() override - { - m_child->start(); - } - QStringList warnings() const override - { - return m_child->warnings(); - } + protected: + virtual void executeTask() override { m_child->start(); } + QStringList warnings() const override { return m_child->warnings(); } -private slots: + private slots: void childSucceded() { unsigned sleepTime = backoff(); - if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) - { + if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) { emitSucceeded(); return; } // we actually failed, retry? - if(sleepTime == maxBackoff) - { + if (sleepTime == maxBackoff) { emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime; m_backoffTimer.start(sleepTime * 500); } - void childFailed(const QString & reason) + void childFailed(const QString& reason) { m_parent->destroyStagingPath(m_stagingPath); emitFailed(reason); } -private: + private: /* * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. * Basically, it starts messing things up while the launcher is extracting/creating instances @@ -873,14 +834,14 @@ private slots: */ ExponentialSeries backoff; QString m_stagingPath; - InstanceList * m_parent; + InstanceList* m_parent; unique_qobject_ptr m_child; QString m_instanceName; QString m_groupName; QTimer m_backoffTimer; }; -Task * InstanceList::wrapInstanceTask(InstanceTask * task) +Task* InstanceList::wrapInstanceTask(InstanceTask* task) { auto stagingPath = getStagedInstancePath(); task->setStagingPath(stagingPath); @@ -895,8 +856,7 @@ QString InstanceList::getStagedInstancePath() QString relPath = FS::PathCombine(tempDir, key); QDir rootPath(m_instDir); auto path = FS::PathCombine(m_instDir, relPath); - if(!rootPath.mkpath(relPath)) - { + if (!rootPath.mkpath(relPath)) { return QString(); } #ifdef Q_OS_WIN32 @@ -913,8 +873,7 @@ bool InstanceList::commitStagedInstance(const QString& path, const QString& inst { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); - if(!dir.rename(path, destination)) - { + if (!dir.rename(path, destination)) { qWarning() << "Failed to move" << path << "to" << destination; return false; } @@ -933,7 +892,8 @@ bool InstanceList::destroyStagingPath(const QString& keyPath) return FS::deletePath(keyPath); } -int InstanceList::getTotalPlayTime() { +int InstanceList::getTotalPlayTime() +{ updateTotalPlayTime(); return totalPlayTime; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index bc6c3af0a..62282f04b 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include "BaseInstance.h" @@ -46,6 +48,12 @@ enum class GroupsState Dirty }; +struct TrashHistoryItem { + QString id; + QString polyPath; + QString trashPath; + QString groupName; +}; class InstanceList : public QAbstractListModel { @@ -102,6 +110,9 @@ class InstanceList : public QAbstractListModel void setInstanceGroup(const InstanceId & id, const GroupId& name); void deleteGroup(const GroupId & name); + bool trashInstance(const InstanceId &id); + bool trashedSomething(); + void undoTrashInstance(); void deleteInstance(const InstanceId & id); // Wrap an instance creation task in some more task machinery and make it ready to be used @@ -180,4 +191,6 @@ private slots: QSet instanceSet; bool m_groupsLoaded = false; bool m_instancesProbed = false; + + QStack m_trashHistory; }; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d58f158e8..c3d95599f 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -252,6 +252,9 @@ class MainWindow::Ui TranslatedAction actionViewInstanceFolder; TranslatedAction actionViewCentralModsFolder; + QMenu * editMenu = nullptr; + TranslatedAction actionUndoTrashInstance; + QMenu * helpMenu = nullptr; TranslatedToolButton helpMenuButton; TranslatedAction actionReportBug; @@ -335,6 +338,14 @@ class MainWindow::Ui actionSettings->setShortcut(QKeySequence::Preferences); all_actions.append(&actionSettings); + actionUndoTrashInstance = TranslatedAction(MainWindow); + connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), MainWindow, SLOT(undoTrashInstance())); + actionUndoTrashInstance->setObjectName(QStringLiteral("actionUndoTrashInstance")); + actionUndoTrashInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Undo Last Instance Deletion")); + actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + actionUndoTrashInstance->setShortcut(QKeySequence("Ctrl+Z")); + all_actions.append(&actionUndoTrashInstance); + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { actionReportBug = TranslatedAction(MainWindow); actionReportBug->setObjectName(QStringLiteral("actionReportBug")); @@ -508,6 +519,9 @@ class MainWindow::Ui fileMenu->addSeparator(); fileMenu->addAction(actionSettings); + editMenu = menuBar->addMenu(tr("&Edit")); + editMenu->addAction(actionUndoTrashInstance); + viewMenu = menuBar->addMenu(tr("&View")); viewMenu->setSeparatorsCollapsible(false); viewMenu->addAction(actionCAT); @@ -732,9 +746,10 @@ class MainWindow::Ui actionDeleteInstance = TranslatedAction(MainWindow); actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); - actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance...")); + actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance")); actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete}); + actionDeleteInstance->setAutoRepeat(false); all_actions.append(&actionDeleteInstance); actionCopyInstance = TranslatedAction(MainWindow); @@ -1150,6 +1165,11 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos) connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup())); actions.append(actionDeleteGroup); } + + QAction *actionUndoTrashInstance = new QAction("Undo last trash instance", this); + connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), SLOT(undoTrashInstance())); + actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); + actions.append(actionUndoTrashInstance); } QMenu myMenu; myMenu.addActions(actions); @@ -1832,6 +1852,11 @@ void MainWindow::deleteGroup() } } +void MainWindow::undoTrashInstance() +{ + APPLICATION->instances()->undoTrashInstance(); +} + void MainWindow::on_actionViewInstanceFolder_triggered() { QString str = APPLICATION->settings()->get("InstanceDir").toString(); @@ -1957,7 +1982,12 @@ void MainWindow::on_actionDeleteInstance_triggered() { return; } + auto id = m_selectedInstance->id(); + if (APPLICATION->instances()->trashInstance(id)) { + return; + } + auto response = CustomMessageBox::selectable( this, tr("CAREFUL!"), diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index d7930b5ab..dde3d02c4 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -145,6 +145,7 @@ private slots: void on_actionDeleteInstance_triggered(); void deleteGroup(); + void undoTrashInstance(); void on_actionExportInstance_triggered(); From 5f1efbeb67c1b150a317b99484bf63888a05b78a Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 1 Aug 2022 09:30:26 +0200 Subject: [PATCH 07/14] fix: work around ubuntu 22.04 openssl appimage issues by copying openssl libs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit terrible hack but it works™️ Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0599c1d94..a8b02b39a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -314,6 +314,9 @@ jobs: cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk cp -r /home/runner/work/PolyMC/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + + cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}//usr/lib/ LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server" From 9c9528838a514b26243d69b81018afc6fb5cfe91 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 1 Aug 2022 14:40:55 +0200 Subject: [PATCH 08/14] chore: update issue template to ask about Qt version Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bac73932e..3f00609e4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -27,7 +27,14 @@ body: attributes: label: Version of PolyMC description: The version of PolyMC used in the bug report. - placeholder: PolyMC 1.3.2 + placeholder: PolyMC 1.4.1 + validations: + required: true +- type: textarea + attributes: + label: Version of Qt + description: The version of Qt used in the bug report. You can find it in Help- About PolyMC- About Qt. + placeholder: Qt 6.3.0 validations: required: true - type: textarea From 9d78b2d259288c3cc863ccf698c0ecc00b273ced Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 1 Aug 2022 14:50:10 +0200 Subject: [PATCH 09/14] Update .github/ISSUE_TEMPLATE/bug_report.yml Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Co-authored-by: flow --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3f00609e4..ab3c8a29f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,7 +33,7 @@ body: - type: textarea attributes: label: Version of Qt - description: The version of Qt used in the bug report. You can find it in Help- About PolyMC- About Qt. + description: The version of Qt used in the bug report. You can find it in Help -> About PolyMC -> About Qt. placeholder: Qt 6.3.0 validations: required: true From abd090bd48a3cbf50091e629c0174f8802f4eedf Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 1 Aug 2022 20:21:19 +0200 Subject: [PATCH 10/14] fix: remove iconfix from libraries/README.MD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit someone forgor (💀) to remove it Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- libraries/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libraries/README.md b/libraries/README.md index 37f53385e..f225ade88 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -22,11 +22,6 @@ Hoedown is a revived fork of Sundown, the Markdown parser based on the original See [github repo](https://github.com/hoedown/hoedown). -## iconfix -This was originally part of the razor-qt project and the Qt toolkit, respecitvely. Its sole purpose is to reimplement Qt's icon loading logic to prevent it from using any platform plugins that could break icon loading. - -Licensed under LGPL 2.1 - ## javacheck Simple Java tool that prints the JVM details - version and platform bitness. From bca1fef2b25587a30c05502cf0231d8d337487f4 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 2 Aug 2022 10:19:47 +0200 Subject: [PATCH 11/14] chore: downgrade to Qt 6.3.0 on macos seems to fix some emoji-related issues Signed-off-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0599c1d94..53c8cd7d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: macosx_deployment_target: 10.14 qt_ver: 6 qt_host: mac - qt_version: '6.3.1' + qt_version: '6.3.0' qt_modules: 'qt5compat qtimageformats' qt_path: /Users/runner/work/PolyMC/Qt From 31ba1de53b2308cdaabda2fa94a7c1a259a8a078 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 31 Jul 2022 21:56:37 -0300 Subject: [PATCH 12/14] fix: remove orphaned metadata to avoid problems with auto-updating insts Just as my master has taught me. :gun: Signed-off-by: flow --- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index a2e055bac..9b70e7a1a 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -83,6 +83,17 @@ void ModFolderLoadTask::run() } } + // Remove orphan metadata to prevent issues + // See https://github.com/PolyMC/PolyMC/issues/996 + QMutableMapIterator iter(m_result->mods); + while (iter.hasNext()) { + auto mod = iter.next().value(); + if (mod->status() == ModStatus::NotInstalled) { + mod->destroy(m_index_dir, false); + iter.remove(); + } + } + emit succeeded(); } From be4fb65470d2a005560e069f7e0969e974767b00 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 3 Aug 2022 21:14:23 +0200 Subject: [PATCH 13/14] fix: Add root path detection on OpenBSD Signed-off-by: Sefa Eyeoglu --- launcher/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 2bd91fd70..97d419931 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -321,7 +321,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) { // Root path is used for updates and portable data -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr m_rootPath = foo.absolutePath(); #elif defined(Q_OS_WIN32) From 5a611c6ffc24fa2e48ec6adff158a2a560a63e19 Mon Sep 17 00:00:00 2001 From: fn2006 <92369097+fn2006@users.noreply.github.com> Date: Sun, 7 Aug 2022 18:38:53 +0100 Subject: [PATCH 14/14] Add Ely.by accounts (#17) * Initial Ely.by support * Fix profile pictures for Ely.by * Disable upload and delete skin buttons for Ely.by accounts * Port UltimMC's authlib injector to PollyMC --- launcher/Application.cpp | 1 + launcher/CMakeLists.txt | 15 +- launcher/minecraft/MinecraftInstance.cpp | 15 + launcher/minecraft/MinecraftInstance.h | 2 + launcher/minecraft/auth/AccountData.cpp | 14 +- launcher/minecraft/auth/AccountData.h | 3 +- launcher/minecraft/auth/AccountList.cpp | 2 +- launcher/minecraft/auth/MinecraftAccount.cpp | 26 ++ launcher/minecraft/auth/MinecraftAccount.h | 15 + launcher/minecraft/auth/Yggdrasil.cpp | 8 +- launcher/minecraft/auth/Yggdrasil.h | 4 +- launcher/minecraft/auth/flows/Elyby.cpp | 24 ++ launcher/minecraft/auth/flows/Elyby.h | 26 ++ .../minecraft/auth/steps/ElybyProfileStep.cpp | 93 ++++++ .../minecraft/auth/steps/ElybyProfileStep.h | 22 ++ launcher/minecraft/auth/steps/ElybyStep.cpp | 52 +++ launcher/minecraft/auth/steps/ElybyStep.h | 28 ++ .../minecraft/auth/steps/YggdrasilStep.cpp | 4 +- launcher/minecraft/launch/InjectAuthlib.cpp | 173 ++++++++++ launcher/minecraft/launch/InjectAuthlib.h | 75 +++++ launcher/ui/dialogs/ElybyLoginDialog.cpp | 119 +++++++ launcher/ui/dialogs/ElybyLoginDialog.h | 59 ++++ launcher/ui/dialogs/ElybyLoginDialog.ui | 77 +++++ launcher/ui/pages/global/AccountListPage.cpp | 24 +- .../ui/pages/global/AccountListPage.cpp.orig | 297 ------------------ launcher/ui/pages/global/AccountListPage.h | 1 + launcher/ui/pages/global/AccountListPage.ui | 6 + 27 files changed, 871 insertions(+), 314 deletions(-) create mode 100644 launcher/minecraft/auth/flows/Elyby.cpp create mode 100644 launcher/minecraft/auth/flows/Elyby.h create mode 100644 launcher/minecraft/auth/steps/ElybyProfileStep.cpp create mode 100644 launcher/minecraft/auth/steps/ElybyProfileStep.h create mode 100644 launcher/minecraft/auth/steps/ElybyStep.cpp create mode 100644 launcher/minecraft/auth/steps/ElybyStep.h create mode 100644 launcher/minecraft/launch/InjectAuthlib.cpp create mode 100644 launcher/minecraft/launch/InjectAuthlib.h create mode 100644 launcher/ui/dialogs/ElybyLoginDialog.cpp create mode 100644 launcher/ui/dialogs/ElybyLoginDialog.h create mode 100644 launcher/ui/dialogs/ElybyLoginDialog.ui delete mode 100644 launcher/ui/pages/global/AccountListPage.cpp.orig diff --git a/launcher/Application.cpp b/launcher/Application.cpp index aa4964ff7..a9658cc4e 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -869,6 +869,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); + m_metacache->addBase("injectors", QDir("injectors").absolutePath()); m_metacache->Load(); qDebug() << "<> Cache initialized."; } diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a4a1315d0..4231f2cb7 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -212,11 +212,15 @@ set(MINECRAFT_SOURCES minecraft/auth/flows/MSA.h minecraft/auth/flows/Offline.cpp minecraft/auth/flows/Offline.h + minecraft/auth/flows/Elyby.cpp + minecraft/auth/flows/Elyby.h - minecraft/auth/steps/OfflineStep.cpp - minecraft/auth/steps/OfflineStep.h minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h + minecraft/auth/steps/ElybyProfileStep.cpp + minecraft/auth/steps/ElybyProfileStep.h + minecraft/auth/steps/ElybyStep.cpp + minecraft/auth/steps/ElybyStep.h minecraft/auth/steps/GetSkinStep.cpp minecraft/auth/steps/GetSkinStep.h minecraft/auth/steps/LauncherLoginStep.cpp @@ -229,6 +233,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/MinecraftProfileStepMojang.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h + minecraft/auth/steps/OfflineStep.cpp + minecraft/auth/steps/OfflineStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp minecraft/auth/steps/XboxAuthorizationStep.h minecraft/auth/steps/XboxProfileStep.cpp @@ -264,6 +270,8 @@ set(MINECRAFT_SOURCES minecraft/launch/LauncherPartLaunch.h minecraft/launch/MinecraftServerTarget.cpp minecraft/launch/MinecraftServerTarget.h + minecraft/launch/InjectAuthlib.cpp + minecraft/launch/InjectAuthlib.h minecraft/launch/PrintInstanceInfo.cpp minecraft/launch/PrintInstanceInfo.h minecraft/launch/ReconstructAssets.cpp @@ -817,6 +825,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/CustomMessageBox.h ui/dialogs/EditAccountDialog.cpp ui/dialogs/EditAccountDialog.h + ui/dialogs/ElybyLoginDialog.cpp + ui/dialogs/ElybyLoginDialog.h ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.h ui/dialogs/IconPickerDialog.cpp @@ -953,6 +963,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/IconPickerDialog.ui ui/dialogs/MSALoginDialog.ui ui/dialogs/OfflineLoginDialog.ui + ui/dialogs/ElybyLoginDialog.ui ui/dialogs/AboutDialog.ui ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 5a6f8de0e..a8f8a82a1 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -348,6 +348,7 @@ QStringList MinecraftInstance::extraArguments() const if (!addn.isEmpty()) { list.append(addn); } + auto agents = m_components->getProfile()->getAgents(); for (auto agent : agents) { @@ -355,6 +356,13 @@ QStringList MinecraftInstance::extraArguments() const agent->library()->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, getLocalLibraryPath()); list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument())); } + + // TODO: figure out how polymc's javaagent system works and use it instead of this hack + if (m_injector) { + list.append("-javaagent:"+m_injector->javaArg); + list.append("-Dauthlibinjector.noShowServerName"); + } + return list; } @@ -972,7 +980,14 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt if(!session->demo) { process->appendStep(new ClaimAccount(pptr, session)); } + + // authlib patch + if (session->user_type == "elyby") + { + process->appendStep(new InjectAuthlib(pptr, &m_injector)); + } process->appendStep(new Update(pptr, Net::Mode::Online)); + } else { diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 8e1c67f29..fb5c6bffb 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -5,6 +5,7 @@ #include #include #include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/InjectAuthlib.h" class ModFolderModel; class WorldList; @@ -128,6 +129,7 @@ class MinecraftInstance: public BaseInstance mutable std::shared_ptr m_texture_pack_list; mutable std::shared_ptr m_world_list; mutable std::shared_ptr m_game_options; + mutable std::shared_ptr m_injector; }; typedef std::shared_ptr MinecraftInstancePtr; diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 44f7e2563..50ca155f9 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -352,6 +352,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { type = AccountType::Mojang; } else if (typeS == "Offline") { type = AccountType::Offline; + } else if (typeS == "Elyby") { + type = AccountType::Elyby; } else { qWarning() << "Failed to parse account data: type is not recognized."; return false; @@ -409,6 +411,9 @@ QJsonObject AccountData::saveState() const { else if (type == AccountType::Offline) { output["type"] = "Offline"; } + else if (type == AccountType::Elyby) { + output["type"] = "Elyby"; + } tokenToJSONV3(output, yggdrasilToken, "ygg"); profileToJSONV3(output, minecraftProfile, "profile"); @@ -428,14 +433,14 @@ QString AccountData::accessToken() const { } QString AccountData::clientToken() const { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::Elyby) { return QString(); } return yggdrasilToken.extra["clientToken"].toString(); } void AccountData::setClientToken(QString clientToken) { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::Elyby) { return; } yggdrasilToken.extra["clientToken"] = clientToken; @@ -449,7 +454,7 @@ void AccountData::generateClientTokenIfMissing() { } void AccountData::invalidateClientToken() { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::Elyby) { return; } yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); @@ -473,6 +478,9 @@ QString AccountData::accountDisplayString() const { case AccountType::Mojang: { return userName(); } + case AccountType::Elyby: { + return userName(); + } case AccountType::Offline: { return QObject::tr(""); } diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 092e1691d..c11aa146f 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -74,7 +74,8 @@ struct MinecraftProfile { enum class AccountType { MSA, Mojang, - Offline + Offline, + Elyby }; enum class AccountState { diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index b3b57c742..4f7f0a128 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -332,7 +332,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const } case MigrationColumn: { - if(account->isMSA() || account->isOffline()) { + if(!account->isMojang()) { return tr("N/A", "Can Migrate?"); } if (account->canMigrate()) { diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index a5c6f542e..0b11f7d6e 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -51,6 +51,7 @@ #include "flows/MSA.h" #include "flows/Mojang.h" #include "flows/Offline.h" +#include "flows/Elyby.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); @@ -106,6 +107,17 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) return account; } +MinecraftAccountPtr MinecraftAccount::createElyby(const QString &username) +{ + MinecraftAccountPtr account = new MinecraftAccount(); + account->data.type = AccountType::Elyby; + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + account->data.minecraftEntitlement.ownsMinecraft = true; + account->data.minecraftEntitlement.canPlayMinecraft = true; + return account; +} + QJsonObject MinecraftAccount::saveToJson() const { @@ -162,6 +174,17 @@ shared_qobject_ptr MinecraftAccount::loginOffline() { return m_currentTask; } +shared_qobject_ptr MinecraftAccount::loginElyby(QString password) { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new ElybyLogin(&data, password)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); + emit activityChanged(true); + return m_currentTask; +} + shared_qobject_ptr MinecraftAccount::refresh() { if(m_currentTask) { return m_currentTask; @@ -173,6 +196,9 @@ shared_qobject_ptr MinecraftAccount::refresh() { else if(data.type == AccountType::Offline) { m_currentTask.reset(new OfflineRefresh(&data)); } + else if(data.type == AccountType::Elyby) { + m_currentTask.reset(new ElybyRefresh(&data)); + } else { m_currentTask.reset(new MojangRefresh(&data)); } diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 7777f8465..923df0f57 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -95,6 +95,8 @@ class MinecraftAccount : static MinecraftAccountPtr createOffline(const QString &username); + static MinecraftAccountPtr createElyby(const QString &username); + static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); @@ -113,6 +115,8 @@ class MinecraftAccount : shared_qobject_ptr loginOffline(); + shared_qobject_ptr loginElyby(QString password); + shared_qobject_ptr refresh(); shared_qobject_ptr currentTask(); @@ -152,10 +156,18 @@ class MinecraftAccount : return data.type == AccountType::MSA; } + bool isMojang() const { + return data.type == AccountType::Mojang; + } + bool isOffline() const { return data.type == AccountType::Offline; } + bool isElyby() const { + return data.type == AccountType::Elyby; + } + bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; } @@ -180,6 +192,9 @@ class MinecraftAccount : case AccountType::Offline: { return "offline"; } + case AccountType::Elyby: { + return "elyby"; + } break; default: { return "unknown"; diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index 299784119..acc026bee 100644 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -55,7 +55,7 @@ void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { void Yggdrasil::executeTask() { } -void Yggdrasil::refresh() { +void Yggdrasil::refresh(QString baseUrl) { start(); /* * { @@ -84,13 +84,13 @@ void Yggdrasil::refresh() { req.insert("requestUser", false); QJsonDocument doc(req); - QUrl reqUrl("https://authserver.mojang.com/refresh"); + QUrl reqUrl(baseUrl + "refresh"); QByteArray requestData = doc.toJson(); sendRequest(reqUrl, requestData); } -void Yggdrasil::login(QString password) { +void Yggdrasil::login(QString password, QString baseUrl) { start(); /* * { @@ -129,7 +129,7 @@ void Yggdrasil::login(QString password) { QJsonDocument doc(req); - QUrl reqUrl("https://authserver.mojang.com/authenticate"); + QUrl reqUrl(baseUrl + "authenticate"); QNetworkRequest netRequest(reqUrl); QByteArray requestData = doc.toJson(); diff --git a/launcher/minecraft/auth/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h index 4f52a04c2..34eb18b22 100644 --- a/launcher/minecraft/auth/Yggdrasil.h +++ b/launcher/minecraft/auth/Yggdrasil.h @@ -40,8 +40,8 @@ class Yggdrasil : public AccountTask ); virtual ~Yggdrasil() = default; - void refresh(); - void login(QString password); + void refresh(QString baseUrl); + void login(QString password, QString baseUrl); struct Error { diff --git a/launcher/minecraft/auth/flows/Elyby.cpp b/launcher/minecraft/auth/flows/Elyby.cpp new file mode 100644 index 000000000..72c104728 --- /dev/null +++ b/launcher/minecraft/auth/flows/Elyby.cpp @@ -0,0 +1,24 @@ +#include "Elyby.h" + +#include "minecraft/auth/steps/ElybyStep.h" +#include "minecraft/auth/steps/ElybyProfileStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +ElybyRefresh::ElybyRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new ElybyStep(m_data, QString())); + m_steps.append(new ElybyProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +ElybyLogin::ElybyLogin( + AccountData *data, + QString password, + QObject *parent +): AuthFlow(data, parent), m_password(password) { + m_steps.append(new ElybyStep(m_data, m_password)); + m_steps.append(new ElybyProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/launcher/minecraft/auth/flows/Elyby.h b/launcher/minecraft/auth/flows/Elyby.h new file mode 100644 index 000000000..beec3e62a --- /dev/null +++ b/launcher/minecraft/auth/flows/Elyby.h @@ -0,0 +1,26 @@ +#pragma once +#include "AuthFlow.h" + +class ElybyRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit ElybyRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class ElybyLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit ElybyLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); + +private: + QString m_password; +}; diff --git a/launcher/minecraft/auth/steps/ElybyProfileStep.cpp b/launcher/minecraft/auth/steps/ElybyProfileStep.cpp new file mode 100644 index 000000000..8cd34ffa0 --- /dev/null +++ b/launcher/minecraft/auth/steps/ElybyProfileStep.cpp @@ -0,0 +1,93 @@ +#include "ElybyProfileStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "net/NetUtils.h" + +ElybyProfileStep::ElybyProfileStep(AccountData* data) : AuthStep(data) { + +} + +ElybyProfileStep::~ElybyProfileStep() noexcept = default; + +QString ElybyProfileStep::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void ElybyProfileStep::perform() { + if (m_data->minecraftProfile.id.isEmpty()) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile.")); + return; + } + + QUrl url = QUrl("https://authserver.ely.by/session/profile/" + m_data->minecraftProfile.id); + QNetworkRequest req = QNetworkRequest(url); + AuthRequest *request = new AuthRequest(this); + connect(request, &AuthRequest::finished, this, &ElybyProfileStep::onRequestDone); + request->get(req); +} + +void ElybyProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void ElybyProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + if (Net::isApplicationError(error)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } + else { + emit finished( + AccountTaskState::STATE_OFFLINE, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } + return; + } + if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/launcher/minecraft/auth/steps/ElybyProfileStep.h b/launcher/minecraft/auth/steps/ElybyProfileStep.h new file mode 100644 index 000000000..765d79e96 --- /dev/null +++ b/launcher/minecraft/auth/steps/ElybyProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class ElybyProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit ElybyProfileStep(AccountData *data); + virtual ~ElybyProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/ElybyStep.cpp b/launcher/minecraft/auth/steps/ElybyStep.cpp new file mode 100644 index 000000000..e81ebb099 --- /dev/null +++ b/launcher/minecraft/auth/steps/ElybyStep.cpp @@ -0,0 +1,52 @@ +#include "ElybyStep.h" + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/Yggdrasil.h" + +ElybyStep::ElybyStep(AccountData* data, QString password) : AuthStep(data), m_password(password) { + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &ElybyStep::onAuthFailed); + connect(m_yggdrasil, &Task::succeeded, this, &ElybyStep::onAuthSucceeded); + connect(m_yggdrasil, &Task::aborted, this, &ElybyStep::onAuthFailed); +} + +ElybyStep::~ElybyStep() noexcept = default; + +QString ElybyStep::describe() { + return tr("Logging in with Ely.by account."); +} + +void ElybyStep::rehydrate() { + // NOOP, for now. +} + +void ElybyStep::perform() { + if(m_password.size()) { + m_yggdrasil->login(m_password, "https://authserver.ely.by/auth/"); + } + else { + m_yggdrasil->refresh("https://authserver.ely.by/auth/"); + } +} + +void ElybyStep::onAuthSucceeded() { + emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Ely.by")); +} + +void ElybyStep::onAuthFailed() { + // TODO: hook these in again, expand to MSA + // m_error = m_yggdrasil->m_error; + // m_aborted = m_yggdrasil->m_aborted; + + auto state = m_yggdrasil->taskState(); + QString errorMessage = tr("Ely.by user authentication failed."); + + // NOTE: soft error in the first step means 'offline' + if(state == AccountTaskState::STATE_FAILED_SOFT) { + state = AccountTaskState::STATE_OFFLINE; + errorMessage = tr("Ely.by user authentication ended with a network error."); + } + emit finished(state, errorMessage); +} diff --git a/launcher/minecraft/auth/steps/ElybyStep.h b/launcher/minecraft/auth/steps/ElybyStep.h new file mode 100644 index 000000000..5bf8f52ca --- /dev/null +++ b/launcher/minecraft/auth/steps/ElybyStep.h @@ -0,0 +1,28 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +class Yggdrasil; + +class ElybyStep : public AuthStep { + Q_OBJECT + +public: + explicit ElybyStep(AccountData *data, QString password); + virtual ~ElybyStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onAuthSucceeded(); + void onAuthFailed(); + +private: + Yggdrasil *m_yggdrasil = nullptr; + QString m_password; +}; diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp index e1d331726..d46dce9b1 100644 --- a/launcher/minecraft/auth/steps/YggdrasilStep.cpp +++ b/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -24,10 +24,10 @@ void YggdrasilStep::rehydrate() { void YggdrasilStep::perform() { if(m_password.size()) { - m_yggdrasil->login(m_password); + m_yggdrasil->login(m_password, "https://authserver.mojang.com/"); } else { - m_yggdrasil->refresh(); + m_yggdrasil->refresh("https://authserver.mojang.com/"); } } diff --git a/launcher/minecraft/launch/InjectAuthlib.cpp b/launcher/minecraft/launch/InjectAuthlib.cpp new file mode 100644 index 000000000..51bc3834f --- /dev/null +++ b/launcher/minecraft/launch/InjectAuthlib.cpp @@ -0,0 +1,173 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InjectAuthlib.h" +#include +#include +#include +#include +#include + +InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector) : LaunchStep(parent) +{ + m_injector = injector; +} + +void InjectAuthlib::executeTask() +{ + if (m_aborted) + { + emitFailed(tr("Task aborted.")); + return; + } + + auto latestVersionInfo = QString("https://authlib-injector.yushi.moe/artifact/latest.json"); + auto netJob = new NetJob("Injector versions info download", APPLICATION->network()); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("injectors", "version.json"); + if (!m_offlineMode) + { + entry->setStale(true); + auto task = Net::Download::makeCached(QUrl(latestVersionInfo), entry); + netJob->addNetAction(task); + + jobPtr.reset(netJob); + QObject::connect(netJob, &NetJob::succeeded, this, &InjectAuthlib::onVersionDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &InjectAuthlib::onDownloadFailed); + jobPtr->start(); + } + else + { + onVersionDownloadSucceeded(); + } +} + +void InjectAuthlib::onVersionDownloadSucceeded() +{ + + QByteArray data; + try + { + data = FS::read(QDir("injectors").absoluteFilePath("version.json")); + } + catch (const Exception &e) + { + qCritical() << "Translations Download Failed: index file not readable"; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(data, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint at " << parse_error.offset << " reason: " << parse_error.errorString(); + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + if (!doc.isObject()) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint root is not object"; + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QString downloadUrl; + try + { + downloadUrl = Json::requireString(doc.object(), "download_url"); + } + catch (const JSONValidationError &e) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint download url is not string"; + qCritical() << e.cause(); + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QFileInfo fi(downloadUrl); + m_versionName = fi.fileName(); + + qDebug() << "Authlib injector version:" << m_versionName; + if (!m_offlineMode) + { + auto netJob = new NetJob("Injector download", APPLICATION->network()); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("injectors", m_versionName); + entry->setStale(true); + auto task = Net::Download::makeCached(QUrl(downloadUrl), entry); + netJob->addNetAction(task); + + jobPtr.reset(netJob); + QObject::connect(netJob, &NetJob::succeeded, this, &InjectAuthlib::onDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &InjectAuthlib::onDownloadFailed); + jobPtr->start(); + } + else + { + onDownloadSucceeded(); + } +} + +void InjectAuthlib::onDownloadSucceeded() +{ + QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg("ely.by"); + + qDebug() + << "Injecting " << injector; + auto inj = new AuthlibInjector(injector); + m_injector->reset(inj); + + jobPtr.reset(); + emitSucceeded(); +} + +void InjectAuthlib::onDownloadFailed(QString reason) +{ + jobPtr.reset(); + emitFailed(reason); +} + +void InjectAuthlib::proceed() +{ +} + +bool InjectAuthlib::canAbort() const +{ + if (jobPtr) + { + return jobPtr->canAbort(); + } + return true; +} + +bool InjectAuthlib::abort() +{ + m_aborted = true; + if (jobPtr) + { + if (jobPtr->canAbort()) + { + return jobPtr->abort(); + } + } + return true; +} diff --git a/launcher/minecraft/launch/InjectAuthlib.h b/launcher/minecraft/launch/InjectAuthlib.h new file mode 100644 index 000000000..5274f55d9 --- /dev/null +++ b/launcher/minecraft/launch/InjectAuthlib.h @@ -0,0 +1,75 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +struct AuthlibInjector +{ + QString javaArg; + + AuthlibInjector(const QString arg) + { + javaArg = std::move(arg); + qDebug() << "NEW INJECTOR" << javaArg; + } +}; + +typedef std::shared_ptr AuthlibInjectorPtr; + +// FIXME: stupid. should be defined by the instance type? or even completely abstracted away... +class InjectAuthlib : public LaunchStep +{ + Q_OBJECT +public: + InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr *injector); + virtual ~InjectAuthlib(){}; + + void executeTask() override; + bool canAbort() const override; + void proceed() override; + + void setAuthServer(QString server) + { + m_authServer = server; + }; + + void setOfflineMode(bool offline) { + m_offlineMode = offline; + } + +public slots: + bool abort() override; + +private slots: + void onVersionDownloadSucceeded(); + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + +private: + shared_qobject_ptr jobPtr; + bool m_aborted = false; + + bool m_offlineMode; + QString m_versionName; + QString m_authServer; + AuthlibInjectorPtr *m_injector; +}; diff --git a/launcher/ui/dialogs/ElybyLoginDialog.cpp b/launcher/ui/dialogs/ElybyLoginDialog.cpp new file mode 100644 index 000000000..b2a0520d5 --- /dev/null +++ b/launcher/ui/dialogs/ElybyLoginDialog.cpp @@ -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. + */ + +#include "ElybyLoginDialog.h" +#include "ui_ElybyLoginDialog.h" + +#include "minecraft/auth/AccountTask.h" + +#include + +ElybyLoginDialog::ElybyLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ElybyLoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +ElybyLoginDialog::~ElybyLoginDialog() +{ + delete ui; +} + +// Stage 1: User interaction +void ElybyLoginDialog::accept() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + // Setup the login task and start it + m_account = MinecraftAccount::createElyby(ui->userTextBox->text()); + m_loginTask = m_account->loginElyby(ui->passTextBox->text()); + connect(m_loginTask.get(), &Task::failed, this, &ElybyLoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &ElybyLoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &ElybyLoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &ElybyLoginDialog::onTaskProgress); + m_loginTask->start(); +} + +void ElybyLoginDialog::setUserInputsEnabled(bool enable) +{ + ui->userTextBox->setEnabled(enable); + ui->passTextBox->setEnabled(enable); + ui->buttonBox->setEnabled(enable); +} + +// Enable the OK button only when both textboxes contain something. +void ElybyLoginDialog::on_userTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty()); +} +void ElybyLoginDialog::on_passTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty()); +} + +void ElybyLoginDialog::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 ElybyLoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void ElybyLoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void ElybyLoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr ElybyLoginDialog::newAccount(QWidget *parent, QString msg) +{ + ElybyLoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return 0; +} diff --git a/launcher/ui/dialogs/ElybyLoginDialog.h b/launcher/ui/dialogs/ElybyLoginDialog.h new file mode 100644 index 000000000..4b81c0b8b --- /dev/null +++ b/launcher/ui/dialogs/ElybyLoginDialog.h @@ -0,0 +1,59 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" +#include "tasks/Task.h" + +namespace Ui +{ +class ElybyLoginDialog; +} + +class ElybyLoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~ElybyLoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + +private: + explicit ElybyLoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void accept(); + + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + + void on_userTextBox_textEdited(const QString &newText); + void on_passTextBox_textEdited(const QString &newText); + +private: + Ui::ElybyLoginDialog *ui; + MinecraftAccountPtr m_account; + Task::Ptr m_loginTask; +}; diff --git a/launcher/ui/dialogs/ElybyLoginDialog.ui b/launcher/ui/dialogs/ElybyLoginDialog.ui new file mode 100644 index 000000000..4b03ebf99 --- /dev/null +++ b/launcher/ui/dialogs/ElybyLoginDialog.ui @@ -0,0 +1,77 @@ + + + ElybyLoginDialog + + + + 0 + 0 + 421 + 198 + + + + + 0 + 0 + + + + Add Account + + + + + + Message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Email + + + + + + + QLineEdit::Password + + + Password + + + + + + + 24 + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 1e3136a5d..0349495a4 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -48,6 +48,7 @@ #include "ui/dialogs/OfflineLoginDialog.h" #include "ui/dialogs/LoginDialog.h" #include "ui/dialogs/MSALoginDialog.h" +#include "ui/dialogs/ElybyLoginDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/SkinUploadDialog.h" @@ -202,6 +203,22 @@ void AccountListPage::on_actionAddOffline_triggered() } } +void AccountListPage::on_actionAddElyby_triggered() +{ + MinecraftAccountPtr account = ElybyLoginDialog::newAccount( + this, + tr("Please enter your Ely.by account email and password to add your account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setDefaultAccount(account); + } + } +} + void AccountListPage::on_actionRemove_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); @@ -245,17 +262,20 @@ void AccountListPage::updateButtonStates() bool hasSelection = !selection.empty(); bool accountIsReady = false; bool accountIsOnline = false; + bool accountIsElyby = false; if (hasSelection) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); accountIsReady = !account->isActive(); accountIsOnline = !account->isOffline(); + accountIsElyby = account->isElyby(); + } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); - ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); - ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby); + ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby); ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if(m_accounts->defaultAccount().get() == nullptr) { diff --git a/launcher/ui/pages/global/AccountListPage.cpp.orig b/launcher/ui/pages/global/AccountListPage.cpp.orig deleted file mode 100644 index f0fe7f384..000000000 --- a/launcher/ui/pages/global/AccountListPage.cpp.orig +++ /dev/null @@ -1,297 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * PolyMC - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (c) 2022 Jamie Mansfield - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "AccountListPage.h" -#include "ui_AccountListPage.h" - -#include -#include - -#include - -#include "net/NetJob.h" - -#include "ui/dialogs/ProgressDialog.h" -#include "ui/dialogs/LoginDialog.h" -#include "ui/dialogs/MSALoginDialog.h" -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/SkinUploadDialog.h" - -#include "tasks/Task.h" -#include "minecraft/auth/AccountTask.h" -#include "minecraft/services/SkinDelete.h" - -#include "Application.h" - -#include "BuildConfig.h" - -AccountListPage::AccountListPage(QWidget *parent) - : QMainWindow(parent), ui(new Ui::AccountListPage) -{ - ui->setupUi(this); - ui->listView->setEmptyString(tr( - "Welcome!\n" - "If you're new here, you can click the \"Add\" button to add your Mojang or Minecraft account." - )); - ui->listView->setEmptyMode(VersionListView::String); - ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); - - m_accounts = APPLICATION->accounts(); - - ui->listView->setModel(m_accounts.get()); - ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::ProfileNameColumn, QHeaderView::Stretch); - ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::NameColumn, QHeaderView::Stretch); - ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::MigrationColumn, QHeaderView::ResizeToContents); - ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::TypeColumn, QHeaderView::ResizeToContents); - ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::StatusColumn, QHeaderView::ResizeToContents); - ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); - - // Expand the account column - - QItemSelectionModel *selectionModel = ui->listView->selectionModel(); - - connect(selectionModel, &QItemSelectionModel::selectionChanged, [this](const QItemSelection &sel, const QItemSelection &dsel) { - updateButtonStates(); - }); - connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu); - - connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); - connect(m_accounts.get(), &AccountList::listActivityChanged, this, &AccountListPage::listChanged); - connect(m_accounts.get(), &AccountList::defaultAccountChanged, this, &AccountListPage::listChanged); - - updateButtonStates(); - - // Xbox authentication won't work without a client identifier, so disable the button if it is missing - if (~APPLICATION->currentCapabilities() & Application::SupportsMSA) { - ui->actionAddMicrosoft->setVisible(false); - ui->actionAddMicrosoft->setToolTip(tr("No Microsoft Authentication client ID was set.")); - } -} - -AccountListPage::~AccountListPage() -{ - delete ui; -} - -void AccountListPage::retranslate() -{ - ui->retranslateUi(this); -} - -void AccountListPage::ShowContextMenu(const QPoint& pos) -{ - auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); - menu->exec(ui->listView->mapToGlobal(pos)); - delete menu; -} - -void AccountListPage::changeEvent(QEvent* event) -{ - if (event->type() == QEvent::LanguageChange) - { - ui->retranslateUi(this); - } - QMainWindow::changeEvent(event); -} - -QMenu * AccountListPage::createPopupMenu() -{ - QMenu* filteredMenu = QMainWindow::createPopupMenu(); - filteredMenu->removeAction(ui->toolBar->toggleViewAction() ); - return filteredMenu; -} - - -void AccountListPage::listChanged() -{ - updateButtonStates(); -} - -void AccountListPage::on_actionAddMojang_triggered() -{ - MinecraftAccountPtr account = LoginDialog::newAccount( - this, - tr("Please enter your Mojang account email and password to add your account.") - ); - - if (account) - { - m_accounts->addAccount(account); - if (m_accounts->count() == 1) { - m_accounts->setDefaultAccount(account); - } - } -} - -void AccountListPage::on_actionAddMicrosoft_triggered() -{ - if(BuildConfig.BUILD_PLATFORM == "osx64") { - CustomMessageBox::selectable( - this, - tr("Microsoft Accounts not available"), - //: %1 refers to the launcher itself - tr( - "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated %1.\n\n" - "Please update both your operating system and %1." - ).arg(BuildConfig.LAUNCHER_NAME), - QMessageBox::Warning - )->exec(); - return; - } - MinecraftAccountPtr account = MSALoginDialog::newAccount( - this, - tr("Please enter your Mojang account email and password to add your account.") - ); - - if (account) - { - m_accounts->addAccount(account); - if (m_accounts->count() == 1) { - m_accounts->setDefaultAccount(account); - } - } -} - -<<<<<<< HEAD -void AccountListPage::on_actionAddOffline_triggered() -{ - MinecraftAccountPtr account = OfflineLoginDialog::newAccount( - this, - tr("Please enter your desired username to add your offline account.") - ); - - if (account) - { - m_accounts->addAccount(account); - if (m_accounts->count() == 1) { - m_accounts->setDefaultAccount(account); - } - } -} - -======= ->>>>>>> 3144c3a0518a63832e1403bdf2dff3f062026e14 -void AccountListPage::on_actionRemove_triggered() -{ - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.size() > 0) - { - QModelIndex selected = selection.first(); - m_accounts->removeAccount(selected); - } -} - -void AccountListPage::on_actionRefresh_triggered() { - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.size() > 0) { - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - m_accounts->requestRefresh(account->internalId()); - } -} - - -void AccountListPage::on_actionSetDefault_triggered() -{ - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.size() > 0) - { - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - m_accounts->setDefaultAccount(account); - } -} - -void AccountListPage::on_actionNoDefault_triggered() -{ - m_accounts->setDefaultAccount(nullptr); -} - -void AccountListPage::updateButtonStates() -{ - // If there is no selection, disable buttons that require something selected. - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - bool hasSelection = selection.size() > 0; - bool accountIsReady = false; - if (hasSelection) - { - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - accountIsReady = !account->isActive(); - } - ui->actionRemove->setEnabled(accountIsReady); - ui->actionSetDefault->setEnabled(accountIsReady); - ui->actionUploadSkin->setEnabled(accountIsReady); - ui->actionDeleteSkin->setEnabled(accountIsReady); - ui->actionRefresh->setEnabled(accountIsReady); - - if(m_accounts->defaultAccount().get() == nullptr) { - ui->actionNoDefault->setEnabled(false); - ui->actionNoDefault->setChecked(true); - } - else { - ui->actionNoDefault->setEnabled(true); - ui->actionNoDefault->setChecked(false); - } -} - -void AccountListPage::on_actionUploadSkin_triggered() -{ - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.size() > 0) - { - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - SkinUploadDialog dialog(account, this); - dialog.exec(); - } -} - -void AccountListPage::on_actionDeleteSkin_triggered() -{ - QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); - if (selection.size() <= 0) - return; - - QModelIndex selected = selection.first(); - MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); - ProgressDialog prog(this); - auto deleteSkinTask = std::make_shared(this, account->accessToken()); - if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { - CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); - return; - } -} diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 9395e92bf..1812c5e50 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -85,6 +85,7 @@ public slots: void on_actionAddMojang_triggered(); void on_actionAddMicrosoft_triggered(); void on_actionAddOffline_triggered(); + void on_actionAddElyby_triggered(); void on_actionRemove_triggered(); void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index 469955b51..e5d76ec48 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -55,6 +55,7 @@ + @@ -109,6 +110,11 @@ Add &Offline + + + Add &Ely.by + + &Refresh