diff --git a/.clangd b/.clangd new file mode 100644 index 00000000..5a804e9c --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + CompilationDatabase: build \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d001cfc..fa07c3f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,16 +26,16 @@ jobs: qt_ver: 6 qt_host: linux qt_version: '6.2.4' - qt_modules: 'qt5compat qtimageformats' + qt_modules: 'qt5compat qtimageformats qtcharts' - os: windows-2022 name: "Windows-Legacy" - msystem: mingw32 + msystem: mingw64 qt_ver: 5 - os: windows-2022 name: "Windows" - msystem: mingw32 + msystem: mingw64 qt_ver: 6 - os: macos-12 @@ -44,7 +44,7 @@ jobs: qt_ver: 6 qt_host: mac qt_version: '6.3.0' - qt_modules: 'qt5compat qtimageformats' + qt_modules: 'qt5compat qtimageformats qtcharts' - os: macos-12 name: macOS-Legacy @@ -52,7 +52,7 @@ jobs: qt_ver: 5 qt_host: mac qt_version: '5.15.2' - qt_modules: '' + qt_modules: 'qtcharts' runs-on: ${{ matrix.os }} @@ -97,6 +97,7 @@ jobs: qt${{ matrix.qt_ver }}-base:p qt${{ matrix.qt_ver }}-svg:p qt${{ matrix.qt_ver }}-imageformats:p + qt${{ matrix.qt_ver }}-charts:p quazip-qt${{ matrix.qt_ver }}:p ccache:p nsis:p @@ -154,7 +155,7 @@ jobs: - name: Install Qt (Linux) if: runner.os == 'Linux' && matrix.qt_ver != 6 run: | - sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 + sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5charts5-dev - name: Install Qt (macOS and AppImage) if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' @@ -280,7 +281,7 @@ jobs: cd ${{ env.INSTALL_DIR }} if [ "${{ matrix.qt_ver }}" == "5" ]; then - cp /mingw32/bin/libcrypto-1_1.dll /mingw32/bin/libssl-1_1.dll ./ + cp /mingw64/bin/libcrypto-3-x64.dll /mingw64/bin/libssl-3-x64.dll ./ fi - name: Package (Windows, portable) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ced1bcd..abc87c53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,13 @@ set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") +# Increases the stack size to 8MB for Windows, only when not building in Debug mode +# because we don't want random users being affected by stack overflows in release builds, +# but it's fine in debug builds for finding and fixing them +if((NOT CMAKE_BUILD_TYPE STREQUAL "Debug") AND WIN32) + set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}") +endif() + # Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") @@ -76,7 +83,7 @@ set(Launcher_NEWS_OPEN_URL "https://multimc.org/posts.html" CACHE STRING "URL th set(Launcher_HELP_URL "" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 5) +set(Launcher_VERSION_MAJOR 6) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") @@ -120,9 +127,6 @@ execute_process(COMMAND ./id.sh OUTPUT_VARIABLE Launcher_MSA_CLIENT_ID) set(Launcher_CURSEFORGE_API_KEY "" CACHE STRING "API key for the CurseForge platform") set(Launcher_CURSEFORGE_API_KEY_API_URL "https://cf.polymc.org/api" CACHE STRING "URL to fetch the Curseforge API key from.") -# Curseforge API Key -execute_process(COMMAND ./cf.sh OUTPUT_VARIABLE Launcher_CURSEFORGE_API_KEY) - #### Check the current Git commit and branch include(GetGitRevisionDescription) git_get_exact_tag(Launcher_GIT_TAG) @@ -141,7 +145,8 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}") include(QtVersionlessBackport) if(Launcher_QT_VERSION_MAJOR EQUAL 5) set(QT_VERSION_MAJOR 5) - find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml) + find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test + Xml Charts) if(NOT Launcher_FORCE_BUNDLED_LIBS) find_package(QuaZip-Qt5 1.3 QUIET) @@ -155,7 +160,8 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml Core5Compat) + find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent Network Test + Xml Charts Core5Compat) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) if(NOT Launcher_FORCE_BUNDLED_LIBS) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 87239577..21873db1 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -567,6 +567,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // The cat m_settings->registerSetting("TheCat", false); + m_settings->registerSetting("CatStyle", "BackgroundCat"); + m_settings->registerSetting("CatPosition", "top right"); m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("SelectedInstance", QString()); @@ -769,6 +771,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("authlibinjector", QDir("cache/authlibinjector").absolutePath()); m_metacache->Load(); qDebug() << "<> Cache initialized."; } @@ -1119,9 +1122,9 @@ void Application::setApplicationTheme(const QString& name, bool initial) #ifdef Q_OS_WIN if (m_mainWindow) { if (QString::compare(theme->id(), "dark") == 0) { - WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true); + WinDarkmode::setWindowDarkModeEnabled((HWND)m_mainWindow->winId(), true); } else { - WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false); + WinDarkmode::setWindowDarkModeEnabled((HWND)m_mainWindow->winId(), false); } } #endif @@ -1336,9 +1339,9 @@ MainWindow* Application::showMainWindow(bool minimized) m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); #ifdef Q_OS_WIN if (QString::compare(settings()->get("ApplicationTheme").toString(), "dark") == 0) { - WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), true); + WinDarkmode::setWindowDarkModeEnabled((HWND)m_mainWindow->winId(), true); } else { - WinDarkmode::setDarkWinTitlebar(m_mainWindow->winId(), false); + WinDarkmode::setWindowDarkModeEnabled((HWND)m_mainWindow->winId(), false); } #endif if(minimized) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index fa941af9..6903565d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -189,11 +189,15 @@ set(MINECRAFT_SOURCES minecraft/auth/flows/AuthFlow.h minecraft/auth/flows/Mojang.cpp minecraft/auth/flows/Mojang.h + minecraft/auth/flows/AuthlibInjector.cpp + minecraft/auth/flows/AuthlibInjector.h minecraft/auth/flows/MSA.cpp minecraft/auth/flows/MSA.h minecraft/auth/flows/Offline.cpp minecraft/auth/flows/Offline.h + minecraft/auth/steps/AuthlibInjectorStep.cpp + minecraft/auth/steps/AuthlibInjectorStep.h minecraft/auth/steps/OfflineStep.cpp minecraft/auth/steps/OfflineStep.h minecraft/auth/steps/EntitlementsStep.cpp @@ -233,6 +237,8 @@ set(MINECRAFT_SOURCES minecraft/launch/ClaimAccount.cpp minecraft/launch/ClaimAccount.h + minecraft/launch/ConfigureAuthlibInjector.cpp + minecraft/launch/ConfigureAuthlibInjector.h minecraft/launch/CreateGameFolders.cpp minecraft/launch/CreateGameFolders.h minecraft/launch/ModMinecraftJar.cpp @@ -675,6 +681,8 @@ SET(LAUNCHER_SOURCES ui/pages/instance/ServersPage.h ui/pages/instance/WorldListPage.cpp ui/pages/instance/WorldListPage.h + ui/pages/instance/StoragePage.cpp + ui/pages/instance/StoragePage.h # GUI - global settings pages ui/pages/global/AccountListPage.cpp @@ -889,6 +897,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/instance/VersionPage.ui ui/pages/instance/WorldListPage.ui ui/pages/instance/ScreenshotsPage.ui + ui/pages/instance/StoragePage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/VanillaPage.ui @@ -971,6 +980,7 @@ target_link_libraries(Launcher_logic Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Charts ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index c2db0dbc..4f787710 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -45,6 +45,8 @@ #include #include +#include + #if defined Q_OS_WIN32 #include #include @@ -174,7 +176,7 @@ bool copy::operator()(const QString& offset) auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); - std::error_code err; + std::error_code err{}; fs::copy_options opt = copy_opts::none; @@ -182,28 +184,49 @@ bool copy::operator()(const QString& offset) if (!m_followSymlinks) opt |= copy_opts::copy_symlinks; + const auto testAndCopy = [opt, &err](const QString& s, const QString& d) { + if (ensureFilePathExists(d)) { + fs::copy(toStdString(s), toStdString(d), opt, err); + } else { + // mkpath failed which means the destination directory doesn't exist. + err = std::make_error_code(std::errc::no_such_file_or_directory); + } + + if (err) { + qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); + qDebug() << "Source file:" << s; + qDebug() << "Destination file:" << d; + } + }; // We can't use copy_opts::recursive because we need to take into account the // blacklisted paths, so we iterate over the source directory, and if there's no blacklist // match, we copy the file. - QDir src_dir(src); - QDirIterator source_it(src, QDir::Filter::Files, QDirIterator::Subdirectories); + if (QDir src_dir(src); src_dir.exists()) { + QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); - while (source_it.hasNext()) { - auto src_path = source_it.next(); - auto relative_path = src_dir.relativeFilePath(src_path); + while (source_it.hasNext()) { + auto src_path = source_it.next(); + auto relative_path = src_dir.relativeFilePath(src_path); - if (m_blacklist && m_blacklist->matches(relative_path)) - continue; + auto dst_path = PathCombine(dst, relative_path); - auto dst_path = PathCombine(dst, relative_path); - ensureFilePathExists(dst_path); + if (m_blacklist && m_blacklist->matches(relative_path)) { + qDebug() << "Attempted to copy blacklisted file:"; + qDebug() << "Source file:" << src_path; + qDebug() << "Destination file:" << dst_path; + continue; + } - fs::copy(toStdString(src_path), toStdString(dst_path), opt, err); - if (err) { - qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); - qDebug() << "Source file:" << src_path; - qDebug() << "Destination file:" << dst_path; + testAndCopy(src_path, dst_path); + } + } else { // src_dir could still be a file, try to copy it directly. + if (m_blacklist && m_blacklist->matches(src)){ + qDebug() << "Attempted to copy blacklisted file:"; + qDebug() << "Source file:" << src; + qDebug() << "Destination file:" << dst; + } else { + testAndCopy(src, dst); } } @@ -340,7 +363,7 @@ QString getDesktopDir() // Cross-platform Shortcut creation bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon) { -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +#if !defined(Q_OS_WIN) && !defined(Q_OS_OSX) location = PathCombine(location, name + ".desktop"); QFile f(location); @@ -366,49 +389,35 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); return true; -#elif defined Q_OS_WIN - // TODO: Fix - // QFile file(PathCombine(location, name + ".lnk")); - // WCHAR *file_w; - // WCHAR *dest_w; - // WCHAR *args_w; - // file.fileName().toWCharArray(file_w); - // dest.toWCharArray(dest_w); - - // QString argStr; - // for (int i = 0; i < args.count(); i++) - // { - // argStr.append(args[i]); - // argStr.append(" "); - // } - // argStr.toWCharArray(args_w); - - // return SUCCEEDED(CreateLink(file_w, dest_w, args_w)); - return false; #else qWarning("Desktop Shortcuts not supported on your platform!"); return false; #endif } -bool overrideFolder(QString overwritten_path, QString override_path) +bool mergeFolders(QString dstpath, QString srcpath) { - using copy_opts = fs::copy_options; - - if (!FS::ensureFolderPathExists(overwritten_path)) - return false; - - std::error_code err; - fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing; - - fs::copy(toStdString(override_path), toStdString(overwritten_path), opt, err); - - if (err) { - qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path); - qCritical() << "Reason:" << QString::fromStdString(err.message()); + std::error_code ec; + fs::path fullSrcPath = srcpath.toStdString(); + fs::path fullDstPath = dstpath.toStdString(); + for (auto& entry : fs::recursive_directory_iterator(fullSrcPath)) + { + fs::path relativeChild = fs::relative(entry, fullSrcPath); + if (entry.is_directory()) + if (!fs::exists(fullDstPath / relativeChild)) + fs::create_directory(fullDstPath / relativeChild); + if (entry.is_regular_file()) + { + fs::path childDst = fullDstPath / relativeChild; + if (fs::exists(childDst)) + fs::remove(childDst); + fs::copy(entry, childDst, fs::copy_options::none, ec); + if (ec.value() != 0) + qCritical() << QString("File copy failed with: %1. File was %2 -> %3").arg(QString::fromStdString(ec.message()), entry.path().c_str(), childDst.c_str()); + } } - return err.value() == 0; + return ec.value() == 0; } } diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index b46f3281..7e66daff 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -154,5 +154,5 @@ QString getDesktopDir(); // Overrides one folder with the contents of another, preserving items exclusive to the first folder // Equivalent to doing QDir::rename, but allowing for overrides -bool overrideFolder(QString overwritten_path, QString override_path); +bool mergeFolders(QString dstpath, QString srcpath); } diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index b490620d..1102ab6e 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -164,23 +164,21 @@ void InstanceImportTask::processZipPack() } else { - QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); - QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); - - if (!mmcRoot.isNull()) - { - // process as MultiMC instance/pack - qDebug() << "MultiMC:" << mmcRoot; - root = mmcRoot; - m_modpackType = ModpackType::MultiMC; - } - else if(!flameRoot.isNull()) + auto [rootDirectory, fileName] = MMCZip::findFolderOfFileInZip(m_packZip.get(), {"manifest.json", "instance.cfg"}); + if(fileName == "manifest.json") { // process as Flame pack - qDebug() << "Flame:" << flameRoot; - root = flameRoot; + qDebug() << "Flame:" << rootDirectory; + root = rootDirectory; m_modpackType = ModpackType::Flame; } + else if (fileName == "instance.cfg") + { + // process as MultiMC instance/pack + qDebug() << "MultiMC:" << rootDirectory; + root = rootDirectory; + m_modpackType = ModpackType::MultiMC; + } } if(m_modpackType == ModpackType::Unknown) { diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 151a2251..0d894de1 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -904,7 +904,7 @@ bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& QString destination = FS::PathCombine(m_instDir, instID); if (should_override) { - if (!FS::overrideFolder(destination, path)) { + if (!FS::mergeFolders(destination, path)) { qWarning() << "Failed to override" << path << "to" << destination; return false; } diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index bf29377d..4b0318d7 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -16,6 +16,7 @@ #include "ui/pages/instance/WorldListPage.h" #include "ui/pages/instance/ServersPage.h" #include "ui/pages/instance/GameOptionsPage.h" +#include "ui/pages/instance/StoragePage.h" class InstancePageProvider : public QObject, public BasePageProvider { @@ -46,6 +47,7 @@ public: // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix.get())); + values.append(new StoragePage(onesix.get())); auto logMatcher = inst->getLogFileMatcher(); if(logMatcher) { diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index e61c5f67..2c5d8567 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -112,7 +112,18 @@ void LaunchController::decideAccount() } } + bool overrideAccount = m_instance->settings()->get("OverrideAccount").toBool(); + QString overrideAccountProfileId = m_instance->settings()->get("OverrideAccountProfileId").toString(); + m_accountToUse = accounts->defaultAccount(); + + if (overrideAccount) { + int overrideIndex = accounts->findAccountByProfileId(overrideAccountProfileId); + if (overrideIndex != -1) { + m_accountToUse = accounts->at(overrideIndex); + } + } + if (!m_accountToUse) { // If no default account is set, ask the user which one to use. @@ -179,7 +190,7 @@ void LaunchController::login() { switch(m_accountToUse->accountState()) { case AccountState::Offline: { m_session->wants_online = false; - // NOTE: fallthrough is intentional + [[fallthrough]]; } case AccountState::Online: { if(!m_session->wants_online) { @@ -212,7 +223,6 @@ void LaunchController::login() { APPLICATION->settings()->set("LastOfflinePlayerName", usedname); } m_session->MakeOffline(usedname); - // offline flavored game from here :3 } if(m_accountToUse->ownsMinecraft()) { if(!m_accountToUse->hasProfile()) { @@ -259,7 +269,7 @@ void LaunchController::login() { // This means some sort of soft error that we can fix with a refresh ... so let's refresh. case AccountState::Unchecked: { m_accountToUse->refresh(); - // NOTE: fallthrough intentional + [[fallthrough]]; } case AccountState::Working: { // refresh is in progress, we need to wait for it to finish to proceed. @@ -272,12 +282,11 @@ void LaunchController::login() { progDialog.execWithTask(task.get()); continue; } - // FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that - /* case AccountState::Queued: { + // FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that + qWarning() << "AccountState::Queued is not implemented"; return; } - */ case AccountState::Expired: { auto errorString = tr("The account has expired and needs to be logged into manually again."); QMessageBox::warning( @@ -314,6 +323,9 @@ void LaunchController::login() { emitFailed(errorString); return; } + default: { + qWarning() << "Invalid AccountState enum"; + } } } emitFailed(tr("Failed to launch.")); diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 9f4e968f..1ac95fc9 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -40,6 +40,7 @@ #include "FileSystem.h" #include +#include // ours bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, const FilterFunction filter) @@ -228,23 +229,27 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const } // ours -QString MMCZip::findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root) +std::pair MMCZip::findFolderOfFileInZip(QuaZip * zip, QSet what, const QString &root) { - QuaZipDir rootDir(zip, root); - for(auto fileName: rootDir.entryList(QDir::Files)) + std::deque pathsToTraverse; + pathsToTraverse.push_back(root); + while (!pathsToTraverse.empty()) { - if(fileName == what) - return root; - } - for(auto fileName: rootDir.entryList(QDir::Dirs)) - { - QString result = findFolderOfFileInZip(zip, what, root + fileName); - if(!result.isEmpty()) + QString currentPath = pathsToTraverse.front(); + pathsToTraverse.pop_front(); + QuaZipDir rootDir(zip, currentPath); + + for(auto fileName: rootDir.entryList(QDir::Files)) { - return result; + if (what.contains(fileName)) + return {currentPath, fileName}; + } + for(auto fileName: rootDir.entryList(QDir::Dirs)) + { + pathsToTraverse.push_back(rootDir.path() + fileName); } } - return QString(); + return {QString(), QString()}; } // ours @@ -292,10 +297,15 @@ std::optional MMCZip::extractSubDir(QuaZip *zip, const QString & su do { QString name = zip->getCurrentFileName(); - if(!name.startsWith(subdir)) + if(!QDir::cleanPath(name).startsWith(subdir)) { continue; } + if (QDir::isAbsolutePath(name) || QDir::cleanPath(name).startsWith("..")) + { + qDebug() << "extractSubDir: Skipping file that tries to place itself in an absolute location or in a parent directory."; + continue; + } name.remove(0, subdir.size()); auto original_name = name; diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index ce9775bd..2a38ecf4 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -78,11 +78,11 @@ namespace MMCZip bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); /** - * Find a single file in archive by file name (not path) + * Breath-first find a single file in archive by a list of file names (not path) * - * \return the path prefix where the file is + * \return pair of {file parent directory, file name} */ - QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString("")); + std::pair findFolderOfFileInZip(QuaZip * zip, QSet what, const QString &root = QString("")); /** * Find a multiple files of the same name in archive by file name diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 032f21f9..fa4ab5a4 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -211,6 +211,7 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const return tr("Latest"); } } + [[fallthrough]]; } default: { @@ -254,6 +255,7 @@ QVariant VersionProxyModel::data(const QModelIndex &index, int role) const } return pixmap; } + [[fallthrough]]; } default: { diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index e4a962b7..0b34fd15 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -448,6 +448,16 @@ QList JavaUtils::FindJavaPaths() scanJavaDir("/opt/jdks"); // flatpak scanJavaDir("/app/jdk"); + + // Default SDKMAN directory can be overwritten via SDKMAN_DIR env var (default $HOME/.sdkman) + // see https://sdkman.io/install + auto sdkmanInstallPath = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(QDir::homePath(), ".sdkman")); + scanJavaDir(FS::PathCombine(sdkmanInstallPath, "candidates/java")); + // Default ASDF directory can be overwritten via ASDF_DIR or ASDF_DATA_DIR env vars (default $HOME/.asdf) + // see https://asdf-vm.com/manage/configuration.html#asdf-dir + auto asdfDataPath = qEnvironmentVariable("ASDF_DATA_DIR", qEnvironmentVariable("ASDF_DIR", FS::PathCombine(QDir::homePath(), ".asdf"))); + scanJavaDir(FS::PathCombine(asdfDataPath, "installs/java")); + return addJavasFromEnv(javas); } #else diff --git a/launcher/main.cpp b/launcher/main.cpp index d1196865..cac33a35 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -91,4 +91,5 @@ int main(int argc, char *argv[]) case Application::Succeeded: return 0; } + return 0; } diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index 6802470d..c831a486 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -45,11 +45,9 @@ QVariant Index::data(const QModelIndex &index, int role) const switch (role) { case Qt::DisplayRole: - switch (index.column()) - { - case 0: return list->humanReadable(); - default: break; - } + if (index.column() == 0) + return list->humanReadable(); + break; case UidRole: return list->uid(); case NameRole: return list->name(); case ListPtrRole: return QVariant::fromValue(list); @@ -70,10 +68,7 @@ QVariant Index::headerData(int section, Qt::Orientation orientation, int role) c { return tr("Name"); } - else - { - return QVariant(); - } + return QVariant(); } bool Index::hasUid(const QString &uid) const diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp index 796da4bb..e6fd3206 100644 --- a/launcher/meta/JsonFormat.cpp +++ b/launcher/meta/JsonFormat.cpp @@ -56,10 +56,10 @@ static VersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj) version->setType(ensureString(obj, "type", QString())); version->setRecommended(ensureBoolean(obj, QString("recommended"), false)); version->setVolatile(ensureBoolean(obj, QString("volatile"), false)); - RequireSet requires, conflicts; - parseRequires(obj, &requires, "requires"); + RequireSet required, conflicts; + parseRequires(obj, &required, "requires"); parseRequires(obj, &conflicts, "conflicts"); - version->setRequires(requires, conflicts); + version->setRequires(required, conflicts); return version; } @@ -176,7 +176,6 @@ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName { if(obj.contains(keyName)) { - QSet requires; auto reqArray = requireArray(obj, keyName); auto iter = reqArray.begin(); while(iter != reqArray.end()) diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp index a8dc3169..60c06514 100644 --- a/launcher/meta/Version.cpp +++ b/launcher/meta/Version.cpp @@ -111,9 +111,9 @@ void Meta::Version::setTime(const qint64 time) emit timeChanged(); } -void Meta::Version::setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts) +void Meta::Version::setRequires(const Meta::RequireSet &required, const Meta::RequireSet &conflicts) { - m_requires = requires; + m_requires = required; m_conflicts = conflicts; emit requiresChanged(); } diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index dea8dc8a..ebfb2191 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -61,7 +61,7 @@ public: /* con/des */ { return m_time; } - const Meta::RequireSet &requires() const + const Meta::RequireSet &required() const { return m_requires; } @@ -87,7 +87,7 @@ public: /* con/des */ public: // for usage by format parsers only void setType(const QString &type); void setTime(const qint64 time); - void setRequires(const Meta::RequireSet &requires, const Meta::RequireSet &conflicts); + void setRequires(const Meta::RequireSet &required, const Meta::RequireSet &conflicts); void setVolatile(bool volatile_); void setRecommended(bool recommended); void setProvidesRecommendations(); diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index f609e94c..1a3e3b0e 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -77,7 +77,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const case ParentVersionRole: { // FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'. - auto & reqs = version->requires(); + auto & reqs = version->required(); auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req) { return req.uid == "net.minecraft"; @@ -92,7 +92,7 @@ QVariant VersionList::data(const QModelIndex &index, int role) const case UidRole: return version->uid(); case TimeRole: return version->time(); - case RequiresRole: return QVariant::fromValue(version->requires()); + case RequiresRole: return QVariant::fromValue(version->required()); case SortRole: return version->rawTime(); case VersionPtrRole: return QVariant::fromValue(version); case RecommendedRole: return version->isRecommended(); diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 7e5b6058..a8eed008 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -451,9 +451,9 @@ void Component::updateCachedData() m_cachedVolatile = file->m_volatile; changed = true; } - if(!deepCompare(m_cachedRequires, file->requires)) + if(!deepCompare(m_cachedRequires, file->required)) { - m_cachedRequires = file->requires; + m_cachedRequires = file->required; changed = true; } if(!deepCompare(m_cachedConflicts, file->conflicts)) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 3ca2e16d..15c14d80 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -59,6 +59,7 @@ #include "launch/steps/QuitAfterGameStop.h" #include "minecraft/launch/LauncherPartLaunch.h" +#include "minecraft/launch/ConfigureAuthlibInjector.h" #include "minecraft/launch/DirectJavaLaunch.h" #include "minecraft/launch/ModMinecraftJar.h" #include "minecraft/launch/ClaimAccount.h" @@ -188,6 +189,10 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // Account override + m_settings->registerSetting("OverrideAccount", false); + m_settings->registerSetting("OverrideAccountProfileId", ""); + qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); @@ -256,7 +261,7 @@ QString MinecraftInstance::getLocalLibraryPath() const bool MinecraftInstance::supportsDemo() const { Version instance_ver { getPackProfile()->getComponentVersion("net.minecraft") }; - // Demo mode was introduced in 1.3.1: https://minecraft.fandom.com/wiki/Demo_mode#History + // Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History // FIXME: Due to Version constraints atm, this can't handle well non-release versions return instance_ver >= Version("1.3.1"); } @@ -384,6 +389,11 @@ QStringList MinecraftInstance::javaArguments() { QStringList args; + if (!m_authlibinjector_javaagent->isNull()) + { + args.append(QString("-javaagent:%1").arg(*m_authlibinjector_javaagent)); + } + // custom args go first. we want to override them if we have our own here. args.append(extraArguments()); @@ -987,6 +997,12 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(step); } + *m_authlibinjector_javaagent = QString(); + if (!session->authlib_injector_base_url.isNull()) + { + process->appendStep(new ConfigureAuthlibInjector(pptr, session->authlib_injector_base_url, m_authlibinjector_javaagent)); + } + // if we aren't in offline mode,. if(session->status != AuthSession::PlayableOffline) { diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 1895d187..9fe79989 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -173,6 +173,7 @@ protected: // data 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_authlibinjector_javaagent = std::make_shared(); }; typedef std::shared_ptr MinecraftInstancePtr; diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index cec4a55b..4ae6afb0 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -266,7 +266,7 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc if (root.contains("requires")) { - Meta::parseRequires(root, &out->requires); + Meta::parseRequires(root, &out->required); } QString dependsOnMinecraftVersion = root.value("mcVersion").toString(); if(!dependsOnMinecraftVersion.isEmpty()) @@ -274,9 +274,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc Meta::Require mcReq; mcReq.uid = "net.minecraft"; mcReq.equalsVersion = dependsOnMinecraftVersion; - if (out->requires.count(mcReq) == 0) + if (out->required.count(mcReq) == 0) { - out->requires.insert(mcReq); + out->required.insert(mcReq); } } if (root.contains("conflicts")) @@ -368,9 +368,9 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch } root.insert("mods", array); } - if(!patch->requires.empty()) + if(!patch->required.empty()) { - Meta::serializeRequires(root, &patch->requires, "requires"); + Meta::serializeRequires(root, &patch->required, "requires"); } if(!patch->conflicts.empty()) { diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index f6ace9e8..f364ff56 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -138,7 +138,7 @@ public: /* data */ * SneedMC: set of packages this depends on * NOTE: this is shared with the meta format!!! */ - Meta::RequireSet requires; + Meta::RequireSet required; /** * SneedMC: set of packages this conflicts with diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 90fcf337..6be783c8 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -285,7 +285,7 @@ void World::readFromZip(const QFileInfo &file) { return; } - auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); + auto [location, _] = MMCZip::findFolderOfFileInZip(&zip, {"level.dat"}); is_valid = !location.isEmpty(); if (!is_valid) { diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 44f7e256..07428bb9 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -350,6 +350,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { type = AccountType::MSA; } else if (typeS == "Mojang") { type = AccountType::Mojang; + } else if (typeS == "Authlib-Injector") { + type = AccountType::AuthlibInjector; } else if (typeS == "Offline") { type = AccountType::Offline; } else { @@ -362,6 +364,10 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); } + if(type == AccountType::AuthlibInjector) { + authlibInjectorBaseUrl = data.value("authlibInjectorUrl").toString(); + } + if(type == AccountType::MSA) { auto clientIDV = data.value("msa-client-id"); if (clientIDV.isString()) { @@ -405,8 +411,10 @@ QJsonObject AccountData::saveState() const { tokenToJSONV3(output, userToken, "utoken"); tokenToJSONV3(output, xboxApiToken, "xrp-main"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); - } - else if (type == AccountType::Offline) { + } else if (type == AccountType::AuthlibInjector) { + output["type"] = "Authlib-Injector"; + output["authlibInjectorUrl"] = authlibInjectorBaseUrl; + } else if (type == AccountType::Offline) { output["type"] = "Offline"; } @@ -428,14 +436,14 @@ QString AccountData::accessToken() const { } QString AccountData::clientToken() const { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::AuthlibInjector) { return QString(); } return yggdrasilToken.extra["clientToken"].toString(); } void AccountData::setClientToken(QString clientToken) { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::AuthlibInjector) { return; } yggdrasilToken.extra["clientToken"] = clientToken; @@ -449,7 +457,7 @@ void AccountData::generateClientTokenIfMissing() { } void AccountData::invalidateClientToken() { - if(type != AccountType::Mojang) { + if(type != AccountType::Mojang && type != AccountType::AuthlibInjector) { return; } yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); @@ -470,6 +478,7 @@ QString AccountData::profileName() const { QString AccountData::accountDisplayString() const { switch(type) { + case AccountType::AuthlibInjector: case AccountType::Mojang: { return userName(); } diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 092e1691..42894f87 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -74,6 +74,7 @@ struct MinecraftProfile { enum class AccountType { MSA, Mojang, + AuthlibInjector, Offline }; @@ -85,6 +86,7 @@ enum class AccountState { Disabled, Errored, Expired, + Queued, Gone }; @@ -114,6 +116,9 @@ struct AccountData { QString lastError() const; AccountType type = AccountType::MSA; + QString authlibInjectorBaseUrl; + QString authlibInjectorApiLocation; + bool legacy = false; bool canMigrateToMSA = false; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index b3b57c74..a3036a0a 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -328,6 +328,12 @@ QVariant AccountList::data(const QModelIndex &index, int role) const case AccountState::Gone: { return tr("Gone", "Account status"); } + case AccountState::Queued: { + qWarning() << "Unhandled account state Queued"; + [[fallthrough]]; + } + default: + return tr("Unknown", "Account status"); } } @@ -341,8 +347,9 @@ QVariant AccountList::data(const QModelIndex &index, int role) const else { return tr("No", "Can Migrate?"); } + qWarning() << "Unhandled case in MigrationColumn"; + [[fallthrough]]; } - default: return QVariant(); } @@ -359,7 +366,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const case ProfileNameColumn: return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; } - + [[fallthrough]]; default: return QVariant(); } diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 7b68bce8..c5feaff5 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -109,6 +109,10 @@ public: MinecraftAccountPtr defaultAccount() const; void setDefaultAccount(MinecraftAccountPtr profileId); bool anyAccountIsValid(); + bool drmCheck() + { + return true; + } bool isActive() const; diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index a75df506..ed1ea100 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -38,8 +38,12 @@ struct AuthSession QString player_name; // profile ID QString uuid; - // 'legacy' or 'mojang', depending on account type + // 'legacy' or 'mojang' or 'authlib-injector', depending on account type QString user_type; + + // If not using authlib injector, this is blank. + QString authlib_injector_base_url; + // Did the auth server reply? bool auth_server_online = false; // Did the user request online mode? diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 73d570f1..36e2671a 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -50,7 +50,39 @@ #include "flows/MSA.h" #include "flows/Mojang.h" +#include "flows/AuthlibInjector.h" #include "flows/Offline.h" +#include "minecraft/auth/AccountData.h" + +// Basically the same as https://github.com/qt/qtbase/blob/5.12/src/corelib/plugin/quuid.cpp#L152C1-L173C2, but unfortunately they don't allow +// us to specify a byte array for the namespace, we only get to specify a fixed length Uuid so I have to copy it and modify it ever so slightly. +static QUuid createUuidFromName(const QByteArray &ns, const QByteArray &baseData, QCryptographicHash::Algorithm algorithm, int version) +{ + QByteArray hashResult; + + // create a scope so later resize won't reallocate + { + QCryptographicHash hash(algorithm); + hash.addData(ns); + hash.addData(baseData); + hashResult = hash.result(); + } + hashResult.resize(16); // Sha1 will be too long + + QUuid result = QUuid::fromRfc4122(hashResult); + + result.data3 &= 0x0FFF; + result.data3 |= (version << 12); + result.data4[0] &= 0x3F; + result.data4[0] |= 0x80; + + return result; +} + +static QUuid createUuidV3(const QByteArray &ns, const QByteArray &baseData) +{ + return createUuidFromName(ns, baseData, QCryptographicHash::Md5, 3); +} MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); @@ -82,6 +114,16 @@ MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username return account; } +MinecraftAccountPtr MinecraftAccount::createAuthlibInjectorFromUsername(const QString &username, QString baseUrl) +{ + MinecraftAccountPtr account = createFromUsername(username); + account->data.type = AccountType::AuthlibInjector; + account->data.authlibInjectorBaseUrl = baseUrl; + account->data.minecraftEntitlement.ownsMinecraft = true; + account->data.minecraftEntitlement.canPlayMinecraft = true; + return account; +} + MinecraftAccountPtr MinecraftAccount::createBlankMSA() { MinecraftAccountPtr account(new MinecraftAccount()); @@ -100,7 +142,7 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); account->data.minecraftEntitlement.ownsMinecraft = true; account->data.minecraftEntitlement.canPlayMinecraft = true; - account->data.minecraftProfile.id = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + account->data.minecraftProfile.id = createUuidV3("OfflinePlayer:", username.toUtf8()).toString().remove(QRegularExpression("[{}-]")); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Katabasis::Validity::Certain; return account; @@ -132,7 +174,14 @@ QPixmap MinecraftAccount::getFace() const { shared_qobject_ptr MinecraftAccount::login(QString password) { Q_ASSERT(m_currentTask.get() == nullptr); - m_currentTask.reset(new MojangLogin(&data, password)); + if (data.type == AccountType::Mojang) + { + m_currentTask.reset(new MojangLogin(&data, password)); + } + else if (data.type == AccountType::AuthlibInjector) + { + m_currentTask.reset(new AuthlibInjectorLogin(&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")); }); @@ -173,6 +222,9 @@ shared_qobject_ptr MinecraftAccount::refresh() { else if(data.type == AccountType::Offline) { m_currentTask.reset(new OfflineRefresh(&data)); } + else if(data.type == AccountType::AuthlibInjector) { + m_currentTask.reset(new AuthlibInjectorRefresh(&data)); + } else { m_currentTask.reset(new MojangRefresh(&data)); } @@ -300,8 +352,9 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) session->player_name = data.profileName(); // profile ID session->uuid = data.profileId(); - // 'legacy' or 'mojang', depending on account type + // 'legacy' or 'mojang', or 'authlib-injector' depending on account type session->user_type = typeString(); + session->authlib_injector_base_url = data.authlibInjectorBaseUrl; if (!session->access_token.isEmpty()) { session->session = "token:" + data.accessToken() + ":" + data.profileId(); diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 3e0d3e82..e4063ae4 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -91,6 +91,8 @@ public: /* construction */ static MinecraftAccountPtr createFromUsername(const QString &username); + static MinecraftAccountPtr createAuthlibInjectorFromUsername(const QString &username, QString baseUrl); + static MinecraftAccountPtr createBlankMSA(); static MinecraftAccountPtr createOffline(const QString &username); @@ -177,6 +179,10 @@ public: /* queries */ return "msa"; } break; + case AccountType::AuthlibInjector: { + return "authlib-injector"; + } + break; case AccountType::Offline: { return "offline"; } diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index 29978411..e94bf49b 100644 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -27,6 +27,25 @@ #include "Application.h" + +QString Yggdrasil::getBaseUrl() +{ + switch (m_data->type) + { + case AccountType::Mojang: { + return "https://authserver.mojang.com"; + } + case AccountType::AuthlibInjector: { + return m_data->authlibInjectorApiLocation + "/authserver"; + } + // Silence warnings about unhandled enum values for values we know shouldn't be handled. + case AccountType::MSA: + case AccountType::Offline: + break; + } + return ""; +} + Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) : AccountTask(data, parent) { @@ -84,7 +103,7 @@ void Yggdrasil::refresh() { req.insert("requestUser", false); QJsonDocument doc(req); - QUrl reqUrl("https://authserver.mojang.com/refresh"); + QUrl reqUrl = getBaseUrl() + "/refresh"; QByteArray requestData = doc.toJson(); sendRequest(reqUrl, requestData); @@ -129,7 +148,8 @@ void Yggdrasil::login(QString password) { QJsonDocument doc(req); - QUrl reqUrl("https://authserver.mojang.com/authenticate"); + QUrl reqUrl = getBaseUrl() + "/authenticate"; + qDebug() << "baseurl = " << getBaseUrl() << "requrl = " << reqUrl; QNetworkRequest netRequest(reqUrl); QByteArray requestData = doc.toJson(); @@ -273,6 +293,7 @@ void Yggdrasil::processReply() { AccountTaskState::STATE_FAILED_GONE, tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") ); + break; } default: changeState( diff --git a/launcher/minecraft/auth/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h index 4f52a04c..4c5ba763 100644 --- a/launcher/minecraft/auth/Yggdrasil.h +++ b/launcher/minecraft/auth/Yggdrasil.h @@ -90,6 +90,7 @@ public slots: private: void sendRequest(QUrl endpoint, QByteArray content); + QString getBaseUrl(); protected: QNetworkReply *m_netReply = nullptr; diff --git a/launcher/minecraft/auth/flows/AuthlibInjector.cpp b/launcher/minecraft/auth/flows/AuthlibInjector.cpp new file mode 100644 index 00000000..840087d2 --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthlibInjector.cpp @@ -0,0 +1,29 @@ +#include "AuthlibInjector.h" + +#include "minecraft/auth/steps/AuthlibInjectorStep.h" +#include "minecraft/auth/steps/MinecraftProfileStepMojang.h" +#include "minecraft/auth/steps/YggdrasilStep.h" +#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/MigrationEligibilityStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +AuthlibInjectorRefresh::AuthlibInjectorRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new AuthlibInjectorStep(m_data)); + m_steps.append(new YggdrasilStep(m_data, QString())); + m_steps.append(new MinecraftProfileStepMojang(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +AuthlibInjectorLogin::AuthlibInjectorLogin( + AccountData *data, + QString password, + QObject *parent +): AuthFlow(data, parent), m_password(password) { + m_steps.append(new AuthlibInjectorStep(m_data)); + m_steps.append(new YggdrasilStep(m_data, m_password)); + m_steps.append(new MinecraftProfileStepMojang(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/launcher/minecraft/auth/flows/AuthlibInjector.h b/launcher/minecraft/auth/flows/AuthlibInjector.h new file mode 100644 index 00000000..567eb1ee --- /dev/null +++ b/launcher/minecraft/auth/flows/AuthlibInjector.h @@ -0,0 +1,26 @@ +#pragma once +#include "AuthFlow.h" + +class AuthlibInjectorRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit AuthlibInjectorRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class AuthlibInjectorLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit AuthlibInjectorLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); + +private: + QString m_password; +}; diff --git a/launcher/minecraft/auth/steps/AuthlibInjectorStep.cpp b/launcher/minecraft/auth/steps/AuthlibInjectorStep.cpp new file mode 100644 index 00000000..7e2e420b --- /dev/null +++ b/launcher/minecraft/auth/steps/AuthlibInjectorStep.cpp @@ -0,0 +1,58 @@ +#include "AuthlibInjectorStep.h" +#include "Application.h" + +#include + +#include +#include + +AuthlibInjectorStep::AuthlibInjectorStep(AccountData* data) : AuthStep(data) { +} + +AuthlibInjectorStep::~AuthlibInjectorStep() noexcept = default; + +QString AuthlibInjectorStep::describe() { + return tr("Fetching authlib injector API URL"); +} + + +void AuthlibInjectorStep::perform() { + // Default to the same as the base URL + QUrl url; + url.setScheme("https"); + url.setAuthority(m_data->authlibInjectorBaseUrl); + qDebug() << url << url.toString() << url.isLocalFile(); + m_data->authlibInjectorApiLocation = url.toString(); + QNetworkRequest request = QNetworkRequest(url); + m_reply.reset( APPLICATION->network()->get(request)); + connect(m_reply.get(), &QNetworkReply::finished, this, &AuthlibInjectorStep::onRequestDone); + qDebug() << "Fetching authlib injector API URL"; +} + +void AuthlibInjectorStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void AuthlibInjectorStep::onRequestDone() { + if (m_reply->hasRawHeader("x-authlib-injector-api-location")) + { + QString authlibInjectorApiLocationHeader = m_reply->rawHeader("x-authlib-injector-api-location"); + QUrl url = authlibInjectorApiLocationHeader; + if (!url.isValid()) + { + qDebug() << "Invalid Authlib Injector API URL specified by server: " << authlibInjectorApiLocationHeader; + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Invalid authlib injector API URL")); + } + else + { + m_data->authlibInjectorApiLocation = authlibInjectorApiLocationHeader; + qDebug() << "Authlib injector API URL: " << m_data->authlibInjectorApiLocation; + emit finished(AccountTaskState::STATE_WORKING, tr("Fetched authlib injector API URL")); + } + } + else + { + qDebug() << "Authlib injector API URL not found"; + emit finished(AccountTaskState::STATE_WORKING, tr("Authlib injector API URL not found, defaulting to the supplied base URL")); + } +} diff --git a/launcher/minecraft/auth/steps/AuthlibInjectorStep.h b/launcher/minecraft/auth/steps/AuthlibInjectorStep.h new file mode 100644 index 00000000..2abaf06b --- /dev/null +++ b/launcher/minecraft/auth/steps/AuthlibInjectorStep.h @@ -0,0 +1,24 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class AuthlibInjectorStep : public AuthStep { + Q_OBJECT + +public: + explicit AuthlibInjectorStep(AccountData *data); + virtual ~AuthlibInjectorStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(); +private: + std::unique_ptr m_reply; +}; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/launcher/minecraft/auth/steps/MinecraftProfileStep.h index 8ef3395c..9f008002 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.h +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.h @@ -19,4 +19,7 @@ public: private slots: void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + +private: + QString baseUrl; }; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp index 6a1eb7a0..ac38ca38 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp @@ -6,8 +6,24 @@ #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" -MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) { +MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {} +QString MinecraftProfileStepMojang::getBaseUrl() +{ + switch (m_data->type) + { + case AccountType::Mojang: { + return "https://sessionserver.mojang.com"; + } + case AccountType::AuthlibInjector: { + return m_data->authlibInjectorApiLocation + "/sessionserver"; + } + // Silence warnings about unhandled enum values for values we know shouldn't be handled. + case AccountType::MSA: + case AccountType::Offline: + break; + } + return ""; } MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default; @@ -24,7 +40,7 @@ void MinecraftProfileStepMojang::perform() { } // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts - QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id); + QUrl url = getBaseUrl() + "/session/minecraft/profile/" + m_data->minecraftProfile.id; QNetworkRequest req = QNetworkRequest(url); AuthRequest *request = new AuthRequest(this); connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone); diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h index e06b30ab..04091fc1 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h @@ -19,4 +19,7 @@ public: private slots: void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + +private: + QString getBaseUrl(); }; diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp index e1d33172..a8adfa66 100644 --- a/launcher/minecraft/auth/steps/YggdrasilStep.cpp +++ b/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -1,5 +1,6 @@ #include "YggdrasilStep.h" +#include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "minecraft/auth/Yggdrasil.h" @@ -15,7 +16,14 @@ YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(dat YggdrasilStep::~YggdrasilStep() noexcept = default; QString YggdrasilStep::describe() { - return tr("Logging in with Mojang account."); + switch(m_data->type) { + case(AccountType::Mojang): + return tr("Logging in with Mojang account."); + case AccountType::AuthlibInjector: + return tr("Logging in with %1 account.").arg(m_data->authlibInjectorBaseUrl); + default: + break; + } } void YggdrasilStep::rehydrate() { @@ -32,7 +40,9 @@ void YggdrasilStep::perform() { } void YggdrasilStep::onAuthSucceeded() { - emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang")); + emit m_data->type == AccountType::Mojang + ? finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang")) + : finished(AccountTaskState::STATE_WORKING, tr("Logged in with %1").arg(m_data->authlibInjectorBaseUrl)); } void YggdrasilStep::onAuthFailed() { @@ -41,12 +51,32 @@ void YggdrasilStep::onAuthFailed() { // m_aborted = m_yggdrasil->m_aborted; auto state = m_yggdrasil->taskState(); - QString errorMessage = tr("Mojang user authentication failed."); + QString errorMessage = m_data->type == AccountType::Mojang + ? tr("Mojang user authentication failed.") + : tr("%1 user authentication failed").arg(m_data->authlibInjectorBaseUrl); // NOTE: soft error in the first step means 'offline' if(state == AccountTaskState::STATE_FAILED_SOFT) { state = AccountTaskState::STATE_OFFLINE; - errorMessage = tr("Mojang user authentication ended with a network error."); + switch(m_data->type) { + case AccountType::Mojang: + { + errorMessage = tr("Mojang user authentication ended with a network error."); + break; + } + case AccountType::AuthlibInjector: + { + if(m_data->authlibInjectorBaseUrl.isEmpty()) + { + errorMessage = tr("User authentication ended with a network error, did specify a url?"); + } else { + errorMessage = tr("%1 user authentication ended with a network error").arg(m_data->authlibInjectorBaseUrl); + } + break; + } + default: + break; + } } emit finished(state, errorMessage); } diff --git a/launcher/minecraft/launch/ConfigureAuthlibInjector.cpp b/launcher/minecraft/launch/ConfigureAuthlibInjector.cpp new file mode 100644 index 00000000..ceb3d72f --- /dev/null +++ b/launcher/minecraft/launch/ConfigureAuthlibInjector.cpp @@ -0,0 +1,80 @@ +#include "ConfigureAuthlibInjector.h" +#include +#include +#include +#include +#include + +#include "Application.h" +#include "minecraft/auth/AccountList.h" +#include "net/ChecksumValidator.h" +#include "net/Download.h" +#include "net/HttpMetaCache.h" +#include "net/NetAction.h" + +ConfigureAuthlibInjector::ConfigureAuthlibInjector(LaunchTask* parent, + QString authlibinjector_base_url, + std::shared_ptr javaagent_arg) + : LaunchStep(parent), m_javaagent_arg{ javaagent_arg }, m_authlibinjector_base_url{ authlibinjector_base_url } +{} + +void ConfigureAuthlibInjector::executeTask() +{ + auto downloadFailed = [this] (QString reason) { + return emitFailed(QString("Download failed: %1").arg(reason)); + }; + auto entry = APPLICATION->metacache()->resolveEntry("authlibinjector", "latest.json"); + + entry->setStale(true); + m_job = std::make_unique("Download authlibinjector latest.json", APPLICATION->network()); + auto latestJsonDl = + Net::Download::makeCached(QUrl("https://authlib-injector.yushi.moe/artifact/latest.json"), entry, Net::Download::Option::NoOptions); + m_job->addNetAction(latestJsonDl); + connect(m_job.get(), &NetJob::succeeded, this, [this, entry, downloadFailed] { + QFile authlibInjectorLatestJson = entry->getFullPath(); + authlibInjectorLatestJson.open(QIODevice::ReadOnly); + if (!authlibInjectorLatestJson.isOpen()) + return emitFailed(QString("Failed to open authlib-injector info json: %1").arg(authlibInjectorLatestJson.errorString())); + + QJsonParseError json_parse_error; + QJsonDocument doc = QJsonDocument::fromJson(authlibInjectorLatestJson.readAll(), &json_parse_error); + if (json_parse_error.error != QJsonParseError::NoError) + return emitFailed(QString("Failed to parse authlib-injector info json: %1").arg(json_parse_error.errorString())); + + if (!doc.isObject()) + return emitFailed(QString("Failed to parse authlib-injector info json: not a json object")); + QJsonObject obj = doc.object(); + + QString authlibInjectorJarUrl = obj["download_url"].toString(); + if (authlibInjectorJarUrl.isNull()) + return emitFailed(QString("Failed to parse authlib-injector info json: download url missing")); + + QString sha256Sum = obj["checksums"].toObject()["sha256"].toString(); + if (sha256Sum.isNull()) + return emitFailed("Failed to parse authlib-injector info json: sha256 checksum missing"); + + auto sha256SumRaw = QByteArray::fromHex(sha256Sum.toLatin1()); + + QString filename = QFileInfo(authlibInjectorJarUrl).fileName(); + auto javaAgentEntry = APPLICATION->metacache()->resolveEntry("authlibinjector", filename); + m_job = std::make_unique("Download authlibinjector java agent", APPLICATION->network()); + auto javaAgentDl = Net::Download::makeCached(QUrl(authlibInjectorJarUrl), javaAgentEntry, Net::Download::Option::MakeEternal); + javaAgentDl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha256, sha256SumRaw)); + m_job->addNetAction(javaAgentDl); + connect(m_job.get(), &NetJob::succeeded, this, [this, javaAgentEntry] { + auto path = javaAgentEntry->getFullPath(); + qDebug() << path; + *m_javaagent_arg = QString("%1=%2").arg(path).arg(m_authlibinjector_base_url); + emitSucceeded(); + }); + connect(m_job.get(), &NetJob::failed, this, downloadFailed); + m_job->start(); + }, + // This slot can't run instantly because it needs to wait for the netjob's code to stop running + // Since it will destroy the old netjob by reassigning the unique_ptr + Qt::QueuedConnection); + connect(m_job.get(), &NetJob::failed, this, downloadFailed); + m_job->start(); +} + +void ConfigureAuthlibInjector::finalize() {} diff --git a/launcher/minecraft/launch/ConfigureAuthlibInjector.h b/launcher/minecraft/launch/ConfigureAuthlibInjector.h new file mode 100644 index 00000000..8bfc5748 --- /dev/null +++ b/launcher/minecraft/launch/ConfigureAuthlibInjector.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include "net/NetJob.h" + +class ConfigureAuthlibInjector: public LaunchStep +{ + Q_OBJECT +public: + explicit ConfigureAuthlibInjector(LaunchTask *parent, QString authlibinjector_base_url, std::shared_ptr javaagent_arg); + virtual ~ConfigureAuthlibInjector() {}; + + void executeTask() override; + void finalize() override; + bool canAbort() const override + { + return false; + } +private: + std::unique_ptr m_job; + std::shared_ptr m_javaagent_arg; + QString m_authlibinjector_base_url; +}; diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index 99809f82..280c7f9a 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -39,6 +39,9 @@ #include "minecraft/PackProfile.h" #include "minecraft/MinecraftInstance.h" +#undef major +#undef minor + void VerifyJavaInstall::executeTask() { auto instance = std::dynamic_pointer_cast(m_parent->instance()); auto packProfile = instance->getPackProfile(); diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 39023f69..5d5f6696 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -82,6 +82,8 @@ std::pair Mod::compare(const Resource& other, SortType type) const auto res = Resource::compare(other, type); if (res.first != 0) return res; + // FIXME: Determine if this is a legitimate fallthrough + [[fallthrough]]; } case SortType::VERSION: { auto this_ver = Version(version()); diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 0fbcfd7c..824f65a1 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -66,6 +66,7 @@ std::pair Resource::compare(const Resource& other, SortType type) con return { 1, type == SortType::ENABLED }; if (!enabled() && other.enabled()) return { -1, type == SortType::ENABLED }; + [[fallthrough]]; case SortType::NAME: { QString this_name{ name() }; QString other_name{ other.name() }; @@ -76,6 +77,7 @@ std::pair Resource::compare(const Resource& other, SortType type) con auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive); if (compare_result != 0) return { compare_result, type == SortType::NAME }; + [[fallthrough]]; } case SortType::DATE: if (dateTimeChanged() > other.dateTimeChanged()) diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 3fc10a2f..88447e77 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -9,13 +9,20 @@ #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" // Values taken from: -// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta static const QMap> s_pack_format_versions = { - { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, - { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, - { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, - { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, + { 1, { Version("1.6.1"), Version("1.8.9") } }, + { 2, { Version("1.9"), Version("1.10.2") } }, + { 3, { Version("1.11"), Version("1.12.2") } }, + { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, + { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, + { 8, { Version("1.18"), Version("1.18.2") } }, { 9, { Version("1.19"), Version("1.19.2") } }, + { 12, { Version("1.19.3"), Version("1.19.3") } }, + { 13, { Version("1.19.4"), Version("1.19.4") } }, + { 14, { Version("1.20"), Version("1.20") } } }; void ResourcePack::setPackFormat(int new_format_id) @@ -85,6 +92,7 @@ std::pair ResourcePack::compare(const Resource& other, SortType type) auto res = Resource::compare(other, type); if (res.first != 0) return res; + [[fallthrough]]; } case SortType::PACK_FORMAT: { auto this_ver = packFormat(); diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index 03121908..4a58ae9b 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -49,7 +49,7 @@ class ResourcePack : public Resource { mutable QMutex m_data_lock; /* The 'version' of a resource pack, as defined in the pack.mcmeta file. - * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + * See https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta */ int m_pack_format = 0; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 4f87bc13..9ce9a0e7 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -115,7 +115,7 @@ void processZIP(ResourcePack& pack) zip.close(); } -// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta void processMCMeta(ResourcePack& pack, QByteArray&& raw_data) { try { diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp index c7987875..68cf2ba9 100644 --- a/launcher/minecraft/services/SkinUpload.cpp +++ b/launcher/minecraft/services/SkinUpload.cpp @@ -42,12 +42,13 @@ QByteArray getVariant(SkinUpload::Model model) { switch (model) { - default: - qDebug() << "Unknown skin type!"; case SkinUpload::STEVE: return "CLASSIC"; case SkinUpload::ALEX: return "SLIM"; + default: + qDebug() << "Unknown skin type!"; + return "CLASSIC"; } } diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 234330a7..d292067e 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -405,7 +405,8 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask() QHash addonIds; for (auto const& hash : m_mods.keys()) { if (m_temp_versions.contains(hash)) { - auto const& data = m_temp_versions.find(hash).value(); + auto const& dataObj = m_temp_versions.find(hash); + auto const& data = dataObj.value(); auto id_str = data.addonId.toString(); if (!id_str.isEmpty()) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index a553eafd..91ed1ec3 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -351,7 +351,7 @@ QString PackInstallTask::getVersionForLoader(QString uid) if(m_version.loader.recommended || m_version.loader.latest) { for (int i = 0; i < vlist->versions().size(); i++) { auto version = vlist->versions().at(i); - auto reqs = version->requires(); + auto reqs = version->required(); // filter by minecraft version, if the loader depends on a certain version. // not all mod loaders depend on a given Minecraft version, so we won't do this diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 74bf126b..036f02ad 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -37,7 +37,6 @@ void Flame::FileResolvingTask::executeTask() void Flame::FileResolvingTask::netJobFinished() { setProgress(1, 3); - int index = 0; // job to check modrinth for blocked projects auto job = new NetJob("Modrinth check", m_network); blockedProjects = QMap(); @@ -73,7 +72,6 @@ void Flame::FileResolvingTask::netJobFinished() blockedProjects.insert(&out, output); } } - index++; } connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished); diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 8dd3a846..a92ec37a 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -158,7 +158,7 @@ void FlameCheckUpdate::executeTask() pack.addonId = mod->metadata()->project_id; pack.websiteUrl = mod->homeurl(); for (auto& author : mod->authors()) - pack.authors.append({ author }); + pack.authors.append({ author, "" }); pack.description = mod->description(); pack.provider = ModPlatform::Provider::FLAME; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 98e3a42c..859acc40 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -420,7 +420,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) switch (result.type) { case Flame::File::Type::Folder: { logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); - // fall-through intentional, we treat these as plain old mods and dump them wherever. + [[fallthrough]]; } case Flame::File::Type::SingleFile: case Flame::File::Type::Mod: { diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 97ce1dc6..3e4cc30c 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -135,8 +135,6 @@ void PackInstallTask::resolveMods() m_file_id_map.clear(); Flame::Manifest manifest; - int index = 0; - for (auto const& file : m_version.files) { if (!file.serverOnly && file.url.isEmpty()) { if (file.curseforge.file_id <= 0) { @@ -154,8 +152,6 @@ void PackInstallTask::resolveMods() } else { m_file_id_map.append(-1); } - - index++; } m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index e2d27547..856f12ac 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -150,7 +150,7 @@ void ModrinthCheckUpdate::executeTask() pack.addonId = mod->metadata()->project_id; pack.websiteUrl = mod->homeurl(); for (auto& author : mod->authors()) - pack.authors.append({ author }); + pack.authors.append({ author, "" }); pack.description = mod->description(); pack.provider = ModPlatform::Provider::MODRINTH; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index ddeea224..bd4c9461 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -195,8 +195,8 @@ bool ModrinthCreationTask::createInstance() Override::createOverrides("client-overrides", parent_folder, client_override_path); // Apply the overrides - if (!FS::overrideFolder(mcPath, client_override_path)) { - setError(tr("Could not rename the client overrides folder:\n") + "client overrides"); + if (!FS::mergeFolders(mcPath, client_override_path)) { + setError(tr("Could not overwrite / create new files:\n") + "client overrides"); return false; } } @@ -305,6 +305,11 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< Modrinth::File file; file.path = Json::requireString(modInfo, "path"); + if (QDir::isAbsolutePath(file.path) || QDir::cleanPath(file.path).startsWith("..")) { + qDebug() << "Skipped file that tries to place itself in an absolute location or in a parent directory."; + continue; + } + auto env = Json::ensureObject(modInfo, "env"); // 'env' field is optional if (!env.isEmpty()) { diff --git a/launcher/resources/OSX/OSX.qrc b/launcher/resources/OSX/OSX.qrc index 3eca8e19..e4cd53ef 100644 --- a/launcher/resources/OSX/OSX.qrc +++ b/launcher/resources/OSX/OSX.qrc @@ -32,6 +32,7 @@ scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg + scalable/storage.svg scalable/viewfolder.svg scalable/worlds.svg diff --git a/launcher/resources/OSX/scalable/storage.svg b/launcher/resources/OSX/scalable/storage.svg new file mode 100644 index 00000000..e4aace13 --- /dev/null +++ b/launcher/resources/OSX/scalable/storage.svg @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/launcher/resources/backgrounds/backgrounds.qrc b/launcher/resources/backgrounds/backgrounds.qrc index 52921512..cdb84a23 100644 --- a/launcher/resources/backgrounds/backgrounds.qrc +++ b/launcher/resources/backgrounds/backgrounds.qrc @@ -1,8 +1,14 @@ - catbgrnd2.png - catmas.png - cattiversary.png + catmas.png + cattiversary.png + catbackground.png + jinxbg.png + jinxmas.png + jinxversary.png + floppabg.png + floppaxmas.png + floppaversary.png diff --git a/launcher/resources/backgrounds/catbackground.png b/launcher/resources/backgrounds/catbackground.png new file mode 100644 index 00000000..e9de7f27 Binary files /dev/null and b/launcher/resources/backgrounds/catbackground.png differ diff --git a/launcher/resources/backgrounds/floppabg.png b/launcher/resources/backgrounds/floppabg.png new file mode 100644 index 00000000..c1dfc8c6 Binary files /dev/null and b/launcher/resources/backgrounds/floppabg.png differ diff --git a/launcher/resources/backgrounds/floppaversary.png b/launcher/resources/backgrounds/floppaversary.png new file mode 100644 index 00000000..f315f15a Binary files /dev/null and b/launcher/resources/backgrounds/floppaversary.png differ diff --git a/launcher/resources/backgrounds/floppaxmas.png b/launcher/resources/backgrounds/floppaxmas.png new file mode 100644 index 00000000..a7b12809 Binary files /dev/null and b/launcher/resources/backgrounds/floppaxmas.png differ diff --git a/launcher/resources/backgrounds/jinxbg.png b/launcher/resources/backgrounds/jinxbg.png new file mode 100644 index 00000000..6a0b47cb Binary files /dev/null and b/launcher/resources/backgrounds/jinxbg.png differ diff --git a/launcher/resources/backgrounds/jinxmas.png b/launcher/resources/backgrounds/jinxmas.png new file mode 100644 index 00000000..7fb23f7c Binary files /dev/null and b/launcher/resources/backgrounds/jinxmas.png differ diff --git a/launcher/resources/backgrounds/jinxversary.png b/launcher/resources/backgrounds/jinxversary.png new file mode 100644 index 00000000..d28af287 Binary files /dev/null and b/launcher/resources/backgrounds/jinxversary.png differ diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index b63e578b..1f6a8240 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -39,6 +39,7 @@ scalable/status-good.svg scalable/status-running.svg scalable/status-yellow.svg + scalable/storage.svg scalable/viewfolder.svg scalable/worlds.svg diff --git a/launcher/resources/flat/scalable/storage.svg b/launcher/resources/flat/scalable/storage.svg new file mode 100644 index 00000000..d8042aea --- /dev/null +++ b/launcher/resources/flat/scalable/storage.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + diff --git a/launcher/resources/iOS/iOS.qrc b/launcher/resources/iOS/iOS.qrc index f05cd67c..d2373742 100644 --- a/launcher/resources/iOS/iOS.qrc +++ b/launcher/resources/iOS/iOS.qrc @@ -32,6 +32,7 @@ scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg + scalable/storage.svg scalable/viewfolder.svg scalable/worlds.svg diff --git a/launcher/resources/iOS/scalable/storage.svg b/launcher/resources/iOS/scalable/storage.svg new file mode 100644 index 00000000..712524f1 --- /dev/null +++ b/launcher/resources/iOS/scalable/storage.svg @@ -0,0 +1,60 @@ + + + + + + diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc index 456963b7..39078e66 100644 --- a/launcher/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -32,6 +32,7 @@ scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg + scalable/storage.svg scalable/viewfolder.svg scalable/worlds.svg diff --git a/launcher/resources/pe_blue/scalable/storage.svg b/launcher/resources/pe_blue/scalable/storage.svg new file mode 100644 index 00000000..a54eb8e4 --- /dev/null +++ b/launcher/resources/pe_blue/scalable/storage.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc index 92a78b5c..d448d006 100644 --- a/launcher/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -32,6 +32,7 @@ scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg + scalable/storage.svg scalable/viewfolder.svg scalable/worlds.svg diff --git a/launcher/resources/pe_colored/scalable/storage.svg b/launcher/resources/pe_colored/scalable/storage.svg new file mode 100644 index 00000000..77695ff3 --- /dev/null +++ b/launcher/resources/pe_colored/scalable/storage.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc index 929b310d..53910098 100644 --- a/launcher/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -32,6 +32,7 @@ scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg + scalable/storage.svg scalable/viewfolder.svg scalable/worlds.svg diff --git a/launcher/resources/pe_dark/scalable/storage.svg b/launcher/resources/pe_dark/scalable/storage.svg new file mode 100644 index 00000000..aec7d82e --- /dev/null +++ b/launcher/resources/pe_dark/scalable/storage.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc index 25fde872..7bf31dcc 100644 --- a/launcher/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -32,6 +32,7 @@ scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg + scalable/storage.svg scalable/viewfolder.svg scalable/worlds.svg diff --git a/launcher/resources/pe_light/scalable/storage.svg b/launcher/resources/pe_light/scalable/storage.svg new file mode 100644 index 00000000..92f3bb4e --- /dev/null +++ b/launcher/resources/pe_light/scalable/storage.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index ce08a6a2..c4b7724c 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -101,9 +101,8 @@ void ConcurrentTask::startNext() setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); updateState(); - QCoreApplication::processEvents(); - - next->start(); + QMetaObject::invokeMethod( + this, [=] { next->start(); }, Qt::QueuedConnection); } void ConcurrentTask::subTaskSucceeded(Task::Ptr task) diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 2f57de3a..4aeb859a 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -226,7 +226,9 @@ void TranslationsModel::indexReceived() reloadLocalFiles(); auto language = d->m_system_locale; - if (!findLanguage(language)) + auto languageIterator = findLanguage(language); + + if (languageIterator == decltype(languageIterator){}) { language = d->m_system_language; } @@ -259,7 +261,6 @@ void readIndex(const QString & path, QMap& languages) return; } - int index = 1; try { auto toplevel_doc = Json::requireDocument(data); @@ -292,7 +293,6 @@ void readIndex(const QString & path, QMap& languages) lang.file_size = Json::requireInteger(langObj, "size"); languages.insert(lang.key, lang); - index++; } } catch (Json::JsonException & e) @@ -418,8 +418,6 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const return QVariant(); int row = index.row(); - auto column = static_cast(index.column()); - if (row < 0 || row >= d->m_languages.size()) return QVariant(); @@ -428,22 +426,19 @@ QVariant TranslationsModel::data(const QModelIndex& index, int role) const { case Qt::DisplayRole: { + auto column = static_cast(index.column()); switch(column) { - case Column::Language: - { + case Column::Language: return lang.languageName(); - } - case Column::Completeness: - { + case Column::Completeness: return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); - } + default: + return QVariant(); } } case Qt::ToolTipRole: - { return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total").arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total)); - } case Qt::UserRole: return lang.key; default: @@ -495,7 +490,7 @@ int TranslationsModel::columnCount(const QModelIndex& parent) const return 2; } -Language * TranslationsModel::findLanguage(const QString& key) +QVector::iterator TranslationsModel::findLanguage(const QString& key) { auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language & lang) { @@ -503,7 +498,7 @@ Language * TranslationsModel::findLanguage(const QString& key) }); if(found == d->m_languages.end()) { - return nullptr; + return {}; } else { @@ -514,21 +509,21 @@ Language * TranslationsModel::findLanguage(const QString& key) bool TranslationsModel::selectLanguage(QString key) { QString &langCode = key; - auto langPtr = findLanguage(key); + auto langIterator = findLanguage(key); if (langCode.isEmpty()) { d->no_language_set = true; } - if(!langPtr) + if (langIterator == decltype(langIterator){}) { qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; langCode = defaultLangCode; } else { - langCode = langPtr->key; + langCode = langIterator->key; } // uninstall existing translators if there are any @@ -580,7 +575,7 @@ bool TranslationsModel::selectLanguage(QString key) d->m_qt_translator.reset(); } - if(langPtr->localFileType == FileType::PO) + if(langIterator->localFileType == FileType::PO) { qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po")); @@ -603,7 +598,7 @@ bool TranslationsModel::selectLanguage(QString key) d->m_app_translator.reset(); } } - else if(langPtr->localFileType == FileType::QM) + else if(langIterator->localFileType == FileType::QM) { d->m_app_translator.reset(new QTranslator()); if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path())) @@ -635,7 +630,7 @@ bool TranslationsModel::selectLanguage(QString key) QModelIndex TranslationsModel::selectedIndex() { auto found = findLanguage(d->m_selectedLanguage); - if(found) + if(found != decltype(found){}) { // QVector iterator freely converts to pointer to contained type return index(found - d->m_languages.begin(), 0, QModelIndex()); @@ -673,7 +668,7 @@ void TranslationsModel::updateLanguage(QString key) return; } auto found = findLanguage(key); - if(!found) + if(found == decltype(found){}) { qWarning() << "Cannot update invalid language" << key; return; @@ -692,7 +687,7 @@ void TranslationsModel::downloadTranslation(QString key) return; } auto lang = findLanguage(key); - if(!lang) + if(lang == decltype(lang){}) { qWarning() << "Will not download an unknown translation" << key; return; diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h index 3abf84e6..b7292c2d 100644 --- a/launcher/translations/TranslationsModel.h +++ b/launcher/translations/TranslationsModel.h @@ -40,7 +40,7 @@ public: void downloadIndex(); private: - Language *findLanguage(const QString & key); + QVector::iterator findLanguage(const QString & key); void reloadLocalFiles(); void downloadTranslation(QString key); void downloadNext(); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 681e84b1..e1e9d640 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1483,27 +1483,40 @@ void MainWindow::setCatBackground(bool enabled) { QDateTime now = QDateTime::currentDateTime(); QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); - QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); - QString cat; - if(non_stupid_abs(now.daysTo(xmas)) <= 4) { - cat = "catmas"; + QDateTime christmasStart(QDate(now.date().year(), 12, 25), QTime(0, 0)); + QDateTime christmasEnd(QDate(now.date().year(), 1, 7), QTime(0, 0)); //end at midnight of the 7th + + QString cat = "default"; + QString catStyleOpt = APPLICATION->settings()->get("CatStyle").toString(); + if(catStyleOpt == "Floppa") + cat = "floppa"; + else if(catStyleOpt == "Jinx") + cat = "jinx"; + + if(christmasStart <= now || now < christmasEnd) { + cat += "Catmas"; } else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { - cat = "cattiversary"; + cat += "Cattiversary"; } else { - cat = "kitteh"; + cat += "Cat"; } + + auto cat_position = APPLICATION->settings()->get("CatPosition").toString().toLower().trimmed(); + if (cat_position != "top left" && cat_position != "bottom left" && cat_position != "bottom right" && cat_position != "top right") + cat_position = "top right"; + view->setStyleSheet(QString(R"( InstanceView { background-image: url(:/backgrounds/%1); background-attachment: fixed; background-clip: padding; - background-position: top right; + background-position: %2; background-repeat: none; background-color:palette(base); -})").arg(cat)); +})").arg(cat, cat_position)); } else { @@ -1569,8 +1582,7 @@ void MainWindow::finalizeInstance(InstancePtr inst) { view->updateGeometries(); setSelectedInstanceById(inst->id()); - if (APPLICATION->accounts()->anyAccountIsValid()) - { + if (APPLICATION->accounts()->drmCheck()) { ProgressDialog loadDialog(this); auto update = inst->createUpdateTask(Net::Mode::Online); connect(update.get(), &Task::failed, [this](QString reason) @@ -1583,9 +1595,7 @@ void MainWindow::finalizeInstance(InstancePtr inst) loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(update.get()); } - } - else - { + } else { CustomMessageBox::selectable( this, tr("Error"), @@ -1803,6 +1813,7 @@ void MainWindow::globalSettingsClosed() updateMainToolBar(); updateToolsMenu(); updateStatusCenter(); + updateCat(); // This needs to be done to prevent UI elements disappearing in the event the config is changed // but PolyMC exits abnormally, causing the window state to never be saved: APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); @@ -2153,6 +2164,11 @@ void MainWindow::updateStatusCenter() } } +void MainWindow::updateCat() +{ + setCatBackground(APPLICATION->settings()->get("TheCat").toBool()); +} + void MainWindow::refreshCurrentInstance(bool running) { auto current = view->selectionModel()->currentIndex(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 638d39e6..2b6d8520 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -195,6 +195,8 @@ private slots: void updateNewsLabel(); + void updateCat(); + void konamiTriggered(); void globalSettingsClosed(); diff --git a/launcher/ui/WinDarkmode.cpp b/launcher/ui/WinDarkmode.cpp index eac68e4f..0f160f2c 100644 --- a/launcher/ui/WinDarkmode.cpp +++ b/launcher/ui/WinDarkmode.cpp @@ -4,29 +4,102 @@ namespace WinDarkmode { -/* See https://github.com/statiolake/neovim-qt/commit/da8eaba7f0e38b6b51f3bacd02a8cc2d1f7a34d8 */ -void setDarkWinTitlebar(WId winid, bool darkmode) +template __attribute((naked)) uint32_t __fastcall WinSyscall([[maybe_unused]] arglist... args) { - HWND hwnd = reinterpret_cast(winid); - BOOL dark = (BOOL) darkmode; + asm volatile("mov %%rcx, %%r10; movl %0, %%eax; syscall; ret" + :: "i"(syscall_id)); +} - HMODULE hUxtheme = LoadLibraryExW(L"uxtheme.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); - HMODULE hUser32 = GetModuleHandleW(L"user32.dll"); - fnAllowDarkModeForWindow AllowDarkModeForWindow - = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133))); - fnSetPreferredAppMode SetPreferredAppMode - = reinterpret_cast(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135))); - fnSetWindowCompositionAttribute SetWindowCompositionAttribute - = reinterpret_cast(GetProcAddress(hUser32, "SetWindowCompositionAttribute")); +VOID ApplyStringProp(HWND hWnd, LPCWSTR lpString, WORD Property) +{ + WORD Prop = (uint16_t)(uint64_t)GetPropW(hWnd, (LPCWSTR)(uint64_t)Property); + if (Prop) + { + DeleteAtom(Prop); + RemovePropW(hWnd, (LPCWSTR)(uint64_t)Property); + } + if (lpString) + { + ATOM v = AddAtomW(lpString); + if (v) + SetPropW(hWnd, (LPCWSTR)(uint64_t)Property, (HANDLE)(uint64_t)v); + } +} - SetPreferredAppMode(AllowDark); - AllowDarkModeForWindow(hwnd, dark); +VOID AllowDarkModeForWindow(HWND hWnd, BOOL Enable) +{ + if (hWnd) + { + ApplyStringProp(hWnd, Enable ? L"Enabled" : NULL, 0xA91E); + } + return; +} + +BOOL IsWindows11() +{ + HMODULE hKern32 = GetModuleHandleW(L"kernel32.dll"); + return GetProcAddress(hKern32, "Wow64SetThreadDefaultGuestMachine") != NULL; // Win11 21h2+ +} + +BOOL IsWindows10_Only() +{ + HMODULE hKern32 = GetModuleHandleW(L"kernel32.dll"); + HMODULE hNtuser = GetModuleHandleW(L"ntdll.dll"); + return GetProcAddress(hKern32, "SetThreadSelectedCpuSets") != NULL + && GetProcAddress(hNtuser, "ZwSetInformationCpuPartition") == NULL; +} + +BOOL IsWindows8_0_Only() +{ + HMODULE hKern32 = GetModuleHandleW(L"kernel32.dll"); + return GetProcAddress(hKern32, "CreateFile2") != NULL // Export added in 6.2 (8) + && GetProcAddress(hKern32, "AppXFreeMemory") != NULL; // Export added in 6.2 (8), removed in 6.3 (8.1) +} + +BOOL IsWindows8_1_Only() +{ + HMODULE hKern32 = GetModuleHandleW(L"kernel32.dll"); + return GetProcAddress(hKern32, "CalloutOnFiberStack") != NULL // Export added in 6.3 (8.1), Removed in 10.0.10586 + && GetProcAddress(hKern32, "SetThreadSelectedCpuSets") == NULL; // Export added in 10.0 (10) +} + +void setWindowDarkModeEnabled(HWND hWnd, bool Enabled) +{ + AllowDarkModeForWindow(hWnd, Enabled); + BOOL DarkEnabled = (BOOL)Enabled; WINDOWCOMPOSITIONATTRIBDATA data = { WCA_USEDARKMODECOLORS, - &dark, - sizeof(dark) + &DarkEnabled, + sizeof(DarkEnabled) }; - SetWindowCompositionAttribute(hwnd, &data); + +#ifdef _WIN64 + constexpr int NtUserSetWindowCompositionAttribute_NT6_2 = 0x13b4; + constexpr int NtUserSetWindowCompositionAttribute_NT6_3 = 0x13e5; + + if (IsWindows8_0_Only()) + WinSyscall(hWnd, &data); + else if (IsWindows8_1_Only()) + WinSyscall(hWnd, &data); + else if (IsWindows10_Only() || IsWindows11()) + { + ((fnSetWindowCompositionAttribute)(PVOID)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute")) + (hWnd, &data); + // Verified this ordinal is the same through Win11 22H2 (5/8/2023) + ((fnSetPreferredAppMode)(PVOID)GetProcAddress(GetModuleHandleW(L"uxtheme.dll"), MAKEINTRESOURCEA(135))) + (AppMode_AllowDark); + } +#else + if (IsWindows10_Only()) + { + ((fnSetWindowCompositionAttribute)(PVOID)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowCompositionAttribute")) + (hWnd, &data); + // Verified this ordinal is the same through Win11 22H2 (5/8/2023) + ((fnSetPreferredAppMode)(PVOID)GetProcAddress(GetModuleHandleW(L"uxtheme.dll"), MAKEINTRESOURCEA(135))) + (AppMode_AllowDark); + } +#endif + } } diff --git a/launcher/ui/WinDarkmode.h b/launcher/ui/WinDarkmode.h index 5b567c6b..e697ddbc 100644 --- a/launcher/ui/WinDarkmode.h +++ b/launcher/ui/WinDarkmode.h @@ -6,14 +6,14 @@ namespace WinDarkmode { -void setDarkWinTitlebar(WId winid, bool darkmode); +void setWindowDarkModeEnabled(HWND hWnd, bool Enabled); enum PreferredAppMode { - Default, - AllowDark, - ForceDark, - ForceLight, - Max + AppMode_Default, + AppMode_AllowDark, + AppMode_ForceDark, + AppMode_ForceLight, + AppMode_Max }; enum WINDOWCOMPOSITIONATTRIB { diff --git a/launcher/ui/dialogs/LoginDialog.cpp b/launcher/ui/dialogs/LoginDialog.cpp index 30394b72..f9ddd188 100644 --- a/launcher/ui/dialogs/LoginDialog.cpp +++ b/launcher/ui/dialogs/LoginDialog.cpp @@ -20,9 +20,10 @@ #include -LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LoginDialog) +LoginDialog::LoginDialog(QWidget *parent, AccountType type) : QDialog(parent), ui(new Ui::LoginDialog), m_accountType{type} { ui->setupUi(this); + ui->authlibInjectorBaseTextBox->setVisible(m_accountType == AccountType::AuthlibInjector); ui->progressBar->setVisible(false); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); @@ -42,7 +43,14 @@ void LoginDialog::accept() ui->progressBar->setVisible(true); // Setup the login task and start it - m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text()); + if (m_accountType == AccountType::Mojang) + { + m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text()); + } + else if (m_accountType == AccountType::AuthlibInjector) + { + m_account = MinecraftAccount::createAuthlibInjectorFromUsername(ui->userTextBox->text(), ui->authlibInjectorBaseTextBox->text()); + } m_loginTask = m_account->login(ui->passTextBox->text()); connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded); @@ -106,10 +114,9 @@ void LoginDialog::onTaskProgress(qint64 current, qint64 total) ui->progressBar->setValue(current); } -// Public interface -MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg) +MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg, AccountType type) { - LoginDialog dlg(parent); + LoginDialog dlg(parent, type); dlg.ui->label->setText(msg); if (dlg.exec() == QDialog::Accepted) { diff --git a/launcher/ui/dialogs/LoginDialog.h b/launcher/ui/dialogs/LoginDialog.h index f8101ff5..0a15ce5b 100644 --- a/launcher/ui/dialogs/LoginDialog.h +++ b/launcher/ui/dialogs/LoginDialog.h @@ -18,6 +18,7 @@ #include #include +#include "minecraft/auth/AccountData.h" #include "minecraft/auth/MinecraftAccount.h" #include "tasks/Task.h" @@ -33,10 +34,13 @@ class LoginDialog : public QDialog public: ~LoginDialog(); - static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + /* + * @param type: Mojang or Authlib + */ + static MinecraftAccountPtr newAccount(QWidget *parent, QString message, AccountType type = AccountType::Mojang); private: - explicit LoginDialog(QWidget *parent = 0); + explicit LoginDialog(QWidget *parent = 0, AccountType type = AccountType::Mojang); void setUserInputsEnabled(bool enable); @@ -56,4 +60,5 @@ private: Ui::LoginDialog *ui; MinecraftAccountPtr m_account; Task::Ptr m_loginTask; + AccountType m_accountType; }; diff --git a/launcher/ui/dialogs/LoginDialog.ui b/launcher/ui/dialogs/LoginDialog.ui index 8fa4a45d..1cbdfccd 100644 --- a/launcher/ui/dialogs/LoginDialog.ui +++ b/launcher/ui/dialogs/LoginDialog.ui @@ -33,6 +33,13 @@ + + + + AuthlibInjector base URL (e.g. ely.by) + + + diff --git a/launcher/ui/dialogs/NewsDialog.cpp b/launcher/ui/dialogs/NewsDialog.cpp index d3b21627..91d32312 100644 --- a/launcher/ui/dialogs/NewsDialog.cpp +++ b/launcher/ui/dialogs/NewsDialog.cpp @@ -30,7 +30,8 @@ NewsDialog::~NewsDialog() void NewsDialog::selectedArticleChanged(const QString& new_title) { - auto const& article_entry = m_entries.constFind(new_title).value(); + auto const& article_entry_ptr = m_entries.constFind(new_title); + auto const& article_entry = article_entry_ptr.value(); ui->articleTitleLabel->setText(QString("%2").arg(article_entry->link, new_title)); ui->currentArticleContentBrowser->setText(article_entry->content); diff --git a/launcher/ui/instanceview/AccessibleInstanceView.cpp b/launcher/ui/instanceview/AccessibleInstanceView.cpp index 7de3ac72..b3d37c56 100644 --- a/launcher/ui/instanceview/AccessibleInstanceView.cpp +++ b/launcher/ui/instanceview/AccessibleInstanceView.cpp @@ -218,11 +218,11 @@ bool AccessibleInstanceView::selectRow(int row) if ((!row || !view()->selectionModel()->isRowSelected(row - 1, view()->rootIndex())) && !view()->selectionModel()->isRowSelected(row + 1, view()->rootIndex())) { view()->clearSelection(); } - break; - } - default: { break; } + default: + qWarning() << "Unhandled QAbstractItemView selection type!"; + break; } view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); @@ -248,7 +248,7 @@ bool AccessibleInstanceView::selectColumn(int column) if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) { return false; } - // fallthrough intentional + [[fallthrough]]; } case QAbstractItemView::ContiguousSelection: { if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) { @@ -279,7 +279,7 @@ bool AccessibleInstanceView::unselectRow(int row) QItemSelection selection(index, index); auto selectionModel = view()->selectionModel(); - switch (view()->selectionMode()) { + switch (const auto selectionType = view()->selectionMode()) { case QAbstractItemView::SingleSelection: // no unselect if (selectedRowCount() == 1) { @@ -298,8 +298,13 @@ bool AccessibleInstanceView::unselectRow(int row) //the ones which are down the current row will be deselected selection = QItemSelection(index, view()->model()->index(rowCount() - 1, 0, view()->rootIndex())); } + break; } + case QAbstractItemView::NoSelection: + break; default: { + // FIXME: See if MultiSelection / ExtendedSelection need to be handled + qWarning() << "Unhandled QAbstractItemView selection type!" << selectionType; break; } } @@ -342,6 +347,7 @@ bool AccessibleInstanceView::unselectColumn(int column) //of the current row, the ones which are at the right will be deselected selection = QItemSelection(index, model->index(0, columnCount() - 1, view()->rootIndex())); } + break; default: break; } diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 6ee352c0..818743f4 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -40,10 +40,8 @@ #include #include -#include #include #include -#include #include #include "settings/SettingsObject.h" @@ -82,9 +80,9 @@ APIPage::APIPage(QWidget *parent) : connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder); // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); - ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); - ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID)); - ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey)); + ui->baseURLEntry->setValidator(new TrimmedRegExValidator(validUrlRegExp, ui->baseURLEntry)); + ui->msaClientID->setValidator(new TrimmedRegExValidator(validMSAClientID, ui->msaClientID)); + ui->flameKey->setValidator(new TrimmedRegExValidator(validFlameKey, ui->flameKey)); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 82d34a9c..1694d9f3 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -4,6 +4,7 @@ * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * Copyright (c) 2022 Lenny McLennington + * Copyright (c) 2022 jdp_ (https://github.com/jdpatdiscord) * * 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 @@ -38,14 +39,29 @@ #pragma once #include +#include +#include +#include #include "ui/pages/BasePage.h" #include -namespace Ui { +namespace Ui +{ class APIPage; } +class TrimmedRegExValidator : public QRegularExpressionValidator +{ + using QRegularExpressionValidator::QRegularExpressionValidator; + + virtual QValidator::State validate(QString& input, int& npos) const override + { + input = input.trimmed(); + return QRegularExpressionValidator::validate(input, npos); + } +}; + class APIPage : public QWidget, public BasePage { Q_OBJECT diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 2ce6af2d..84cd1651 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -157,6 +157,36 @@ void AccountListPage::on_actionAddMojang_triggered() } } +void AccountListPage::on_actionAddAuthlibInjector_triggered() +{ + if (!m_accounts->drmCheck()) { + QMessageBox::warning( + this, + tr("Error"), + tr( + "You must add a Microsoft or Mojang account that owns Minecraft before you can add an Authlib Injector account." + "

" + "If you have lost your account you can contact Microsoft for support." + ) + ); + return; + } + + MinecraftAccountPtr account = LoginDialog::newAccount( + this, + tr("Please enter the AuthlibInjector base URL, and enter your account email and password to add your account."), + AccountType::AuthlibInjector + ); + + 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") { diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 9395e92b..e6003e20 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -83,6 +83,7 @@ public: public slots: void on_actionAddMojang_triggered(); + void on_actionAddAuthlibInjector_triggered(); void on_actionAddMicrosoft_triggered(); void on_actionAddOffline_triggered(); void on_actionRemove_triggered(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index 469955b5..5b2efcbb 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -54,6 +54,7 @@ + @@ -68,6 +69,11 @@ Add &Mojang
+ + + Add Authlib-&Injector + + Remo&ve diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 6ccffed4..c03d46c0 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -96,7 +96,7 @@ MiB - 128 + 1 65536 diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 74142ef3..945a091d 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -207,6 +207,16 @@ void LauncherPage::applySettings() break; } + if(s->get("CatStyle") != ui->themeComboBoxCat->currentText()) + { + s->set("CatStyle", ui->themeComboBoxCat->currentText()); + } + + if(s->get("CatPosition") != ui->themeComboBoxCatPosition->currentData()) + { + s->set("CatPosition", ui->themeComboBoxCatPosition->currentData()); + } + if(original != s->get("IconTheme")) { APPLICATION->setIconTheme(s->get("IconTheme").toString()); @@ -308,6 +318,32 @@ void LauncherPage::loadSettings() } idx++; } + + //TODO: Don't make it hardcoded via strings + auto currentCatStyle = s->get("CatStyle").toString(); + ui->themeComboBoxCat->addItem("BackgroundCat"); + ui->themeComboBoxCat->addItem("Jinx"); + ui->themeComboBoxCat->addItem("Floppa"); + if(currentCatStyle == "Floppa") + ui->themeComboBoxCat->setCurrentIndex(2); + else if(currentCatStyle == "Jinx") + ui->themeComboBoxCat->setCurrentIndex(1); + else + ui->themeComboBoxCat->setCurrentIndex(0); + + auto currentCatPosition = s->get("CatPosition").toString(); + ui->themeComboBoxCatPosition->addItem(tr("Top Left"), "top left"); + ui->themeComboBoxCatPosition->addItem(tr("Top Right"), "top right"); + ui->themeComboBoxCatPosition->addItem(tr("Bottom Left"), "bottom left"); + ui->themeComboBoxCatPosition->addItem(tr("Bottom Right"), "bottom right"); + if (currentCatPosition == "top left") + ui->themeComboBoxCatPosition->setCurrentIndex(0); + else if (currentCatPosition == "bottom left") + ui->themeComboBoxCatPosition->setCurrentIndex(2); + else if (currentCatPosition == "bottom right") + ui->themeComboBoxCatPosition->setCurrentIndex(3); + else + ui->themeComboBoxCatPosition->setCurrentIndex(1); } // Toolbar/menu bar settings (not applicable if native menu bar is present) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 0123e251..1fd7e45d 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -202,7 +202,7 @@ - + &Icons @@ -270,7 +270,7 @@ - + &Colors @@ -292,6 +292,37 @@ + + + + &Cat Type + + + themeComboBoxCat + + + + + + + Qt::StrongFocus + + + + + + + + + + Cat Position + + + + + + +
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 5da7f19f..153885e4 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -53,12 +53,20 @@ #include "java/JavaUtils.h" #include "FileSystem.h" +#include "minecraft/auth/AccountList.h" + InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst) { m_settings = inst->settings(); + m_accounts = APPLICATION->accounts(); ui->setupUi(this); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + // This would just be set in the .ui file, if only Qt's uic would just ignore propreties that don't exist. SAD! + ui->accountComboBox->setPlaceholderText(tr("No account selected")); +#endif + auto sysMB = Sys::getSystemRam() / Sys::mebibyte; ui->maxMemSpinBox->setMaximum(sysMB); connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); @@ -275,6 +283,18 @@ void InstanceSettingsPage::applySettings() m_settings->reset("JoinServerOnLaunchAddress"); } + // Account override settings + bool accountOverride = ui->accountGroupBox->isChecked(); + m_settings->set("OverrideAccount", accountOverride); + if (accountOverride && ui->accountComboBox->currentData().isValid()) + { + m_settings->set("OverrideAccountProfileId", ui->accountComboBox->currentData().toString()); + } + else + { + m_settings->reset("OverrideAccountProfileId"); + } + // FIXME: This should probably be called by a signal instead m_instance->updateRuntimeContext(); } @@ -372,6 +392,45 @@ void InstanceSettingsPage::loadSettings() ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); + + ui->accountGroupBox->setChecked(m_settings->get("OverrideAccount").toBool()); + if (m_accounts->count() > 0) { + for (int i = 0; i < m_accounts->count(); i++) { + MinecraftAccountPtr account = m_accounts->at(i); + + QString profileLabel = account->profileName(); + QString profileId = account->profileId(); + QString profileType = account->typeString(); + profileType[0] = profileType[0].toUpper(); + QPixmap profileFace = account->getFace(); + QIcon profileIcon = !profileFace.isNull() ? QIcon(profileFace) : APPLICATION->getThemedIcon("noaccount"); + + ui->accountComboBox->addItem(profileIcon, QString("%1 (%2)").arg(profileLabel, profileType), profileId); + } + + QString accountProfileId = m_settings->get("OverrideAccountProfileId").toString(); + int index = -1; + if (ui->accountGroupBox->isChecked()) { + // Try find the account by the override profile id setting + // It'll be -1 if the account doesn't exist, which will just + // Set the combobox to the "no option selected" state + index = ui->accountComboBox->findData(accountProfileId); + } + ui->accountComboBox->setCurrentIndex(index); + } else { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + ui->accountComboBox->setPlaceholderText(tr("No accounts available")); + ui->accountComboBox->setCurrentIndex(-1); +#else + ui->accountComboBox->addItem(tr("No accounts available")); +#endif + ui->accountComboBox->setDisabled(true); + } + + // If there isn't a currently selected account, the override should show as disabled + if (ui->accountComboBox->currentIndex() == -1) { + ui->accountGroupBox->setChecked(false); + } } void InstanceSettingsPage::on_javaDetectBtn_clicked() diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 97d1296f..aa23070a 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -93,5 +93,6 @@ private: Ui::InstanceSettingsPage *ui; BaseInstance *m_instance; SettingsObjectPtr m_settings; + shared_qobject_ptr m_accounts; unique_qobject_ptr checker; }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 8b3c3370..b12dd707 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -7,7 +7,7 @@ 0 0 691 - 581 + 629 @@ -151,7 +151,7 @@ MiB - 128 + 1 65536 @@ -595,6 +595,48 @@ + + + + Set an account to use + + + true + + + false + + + + + + + + + 0 + 0 + + + + Game account: + + + + + + + + 0 + 0 + + + + + + + + + diff --git a/launcher/ui/pages/instance/StoragePage.cpp b/launcher/ui/pages/instance/StoragePage.cpp new file mode 100644 index 00000000..da10d284 --- /dev/null +++ b/launcher/ui/pages/instance/StoragePage.cpp @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Slendi + * + * 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 . + */ + +#include "StoragePage.h" +#include +#include +#include +#include "ui_StoragePage.h" + +qint64 getDirectorySize(QString path) +{ + QDirIterator it(path, QDirIterator::Subdirectories); + qint64 fileSizeTotal = 0; + while (it.hasNext()) { + it.next(); + fileSizeTotal += it.fileInfo().size(); + } + return fileSizeTotal; +} + +void clearDirectoryInner(QString path) +{ + QDir(path).removeRecursively(); +} + +StoragePage::StoragePage(BaseInstance* inst, QWidget* parent) : QWidget(parent), ui(new Ui::StoragePage), m_inst(inst) +{ + ui->setupUi(this); + + m_confirmation_box = new QMessageBox(this); + m_confirmation_box->setWindowTitle("Confirmation"); + m_confirmation_box->setIcon(QMessageBox::Warning); + m_confirmation_box->setText("Are you sure you want to proceed?"); + m_confirmation_box->setStandardButtons(QMessageBox::Yes); + m_confirmation_box->addButton(QMessageBox::No); + m_confirmation_box->setDefaultButton(QMessageBox::No); + + m_series = new QPieSeries(this); + m_series->setLabelsVisible(); + m_series->setLabelsPosition(QPieSlice::LabelInsideHorizontal); + m_chart_view = new QChartView(this); + m_chart = new QChart(); + m_chart->setParent(this); + m_chart->addSeries(m_series); + m_chart->setBackgroundVisible(false); + m_chart->setMargins(QMargins(0, 0, 0, 0)); + m_chart->legend()->setAlignment(Qt::AlignLeft); + m_chart->legend()->setLabelColor(QApplication::palette().text().color()); + m_chart_view->setRenderHint(QPainter::Antialiasing); + m_chart_view->installEventFilter(this); + + ui->verticalLayout->addWidget(m_chart_view); + ui->retranslateUi(this); + + updateCalculations(); + + connect(ui->button_goto_resouce_packs, &QPushButton::clicked, this, [&] { m_container->selectPage("resourcepacks"); }); + connect(ui->button_goto_mods, &QPushButton::clicked, this, [&] { m_container->selectPage("mods"); }); + connect(ui->button_goto_worlds, &QPushButton::clicked, this, [&] { m_container->selectPage("worlds"); }); + connect(ui->button_goto_screenshots, &QPushButton::clicked, this, [&] { m_container->selectPage("screenshots"); }); + connect(ui->button_goto_other_logs, &QPushButton::clicked, this, [&] { m_container->selectPage("logs"); }); + + connect(ui->button_clear_screenshots, &QPushButton::clicked, this, &StoragePage::handleClearScreenshotsButton); + connect(ui->button_clear_logs, &QPushButton::clicked, this, &StoragePage::handleClearLogsButton); + connect(ui->button_clear_all, &QPushButton::clicked, this, &StoragePage::handleClearAllButton); +} + +StoragePage::~StoragePage() +{ + delete ui; +} + +bool StoragePage::apply() +{ + return true; +} + +void StoragePage::retranslate() +{ + ui->retranslateUi(this); +} + +void StoragePage::handleClearScreenshotsButton() +{ + if (m_confirmation_box->exec() != QMessageBox::Yes) + return; + + auto path = m_inst->gameRoot() + "/screenshots"; + QDir(path).removeRecursively(); + updateCalculations(); +} + +void StoragePage::handleClearLogsButton() +{ + if (m_confirmation_box->exec() != QMessageBox::Yes) + return; + + auto path = m_inst->gameRoot() + "/logs"; + QDir(path).removeRecursively(); + updateCalculations(); +} + +void StoragePage::handleClearAllButton() +{ + if (m_confirmation_box->exec() != QMessageBox::Yes) + return; + + handleClearScreenshotsButton(); + handleClearLogsButton(); +} + +void StoragePage::updateCalculations() +{ + auto size_resource_packs = + getDirectorySize((m_inst->gameRoot() + "/texturepacks")) + getDirectorySize((m_inst->gameRoot() + "/resourcepacks")); + auto size_mods = getDirectorySize(m_inst->modsRoot()); + auto size_saves = getDirectorySize((m_inst->gameRoot() + "/saves")); + auto size_screenshots = getDirectorySize((m_inst->gameRoot() + "/screenshots")); + auto size_logs = getDirectorySize((m_inst->gameRoot() + "/logs")); + + auto storage_info = QStorageInfo(QDir(m_inst->gameRoot())); + auto size_remaining = storage_info.bytesAvailable(); + auto size_used = storage_info.bytesTotal() - size_remaining; + + auto locale = this->locale(); + ui->label_resource_packs->setText(locale.formattedDataSize(size_resource_packs)); + ui->label_mods->setText(locale.formattedDataSize(size_mods)); + ui->label_saves->setText(locale.formattedDataSize(size_saves)); + ui->label_screenshots->setText(locale.formattedDataSize(size_screenshots)); + ui->label_logs->setText(locale.formattedDataSize(size_logs)); + ui->label_combined->setText(locale.formattedDataSize(size_resource_packs + size_mods + size_saves + size_screenshots + size_logs)); + + ui->label_used->setText(locale.formattedDataSize(size_used)); + ui->label_remaining->setText(locale.formattedDataSize(size_remaining)); + + m_series->clear(); + m_series->append("Resource packs", size_resource_packs); + m_series->append("Mods", size_mods); + m_series->append("Saves", size_saves); + m_series->append("Screenshots", size_screenshots); + m_series->append("Logs", size_logs); + + m_chart_view->setChart(m_chart); + for (auto slice : m_series->slices()) + slice->setLabel(slice->label() + " " + QString("%1%").arg(100 * slice->percentage(), 0, 'f', 1)); +} + +bool StoragePage::eventFilter(QObject *object, QEvent *event) +{ + if(event->type() == QEvent::PaletteChange) + { + m_chart->legend()->setLabelColor(QApplication::palette().text().color()); + return true; + } + return false; +} diff --git a/launcher/ui/pages/instance/StoragePage.h b/launcher/ui/pages/instance/StoragePage.h new file mode 100644 index 00000000..294ca377 --- /dev/null +++ b/launcher/ui/pages/instance/StoragePage.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Slendi + * + * 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: + */ + +#pragma once + +#include +#include +#include +#include + +#include "BaseInstance.h" +#include "ui/pages/BasePage.h" +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +using namespace QtCharts; +#endif + +namespace Ui +{ +class StoragePage; +} + +class StoragePage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit StoragePage(BaseInstance *inst, QWidget *parent = 0); + virtual ~StoragePage(); + virtual QString displayName() const override + { + return tr("Storage"); + } + virtual QIcon icon() const override + { + auto icon = APPLICATION->getThemedIcon("storage"); + if(icon.isNull()) + icon = APPLICATION->getThemedIcon("news"); + return icon; + } + virtual QString id() const override + { + return "storage"; + } + virtual bool apply() override; + virtual QString helpPage() const override + { + return "Storage"; + } + void retranslate() override; + + void handleClearScreenshotsButton(); + void handleClearLogsButton(); + void handleClearAllButton(); + + void updateCalculations(); + +protected: + bool eventFilter(QObject *object, QEvent *event) override; + +private: + Ui::StoragePage *ui; + BaseInstance *m_inst; + + QPieSeries *m_series; + QChart *m_chart; + QChartView *m_chart_view; + + QMessageBox *m_confirmation_box; +}; diff --git a/launcher/ui/pages/instance/StoragePage.ui b/launcher/ui/pages/instance/StoragePage.ui new file mode 100644 index 00000000..fba6050f --- /dev/null +++ b/launcher/ui/pages/instance/StoragePage.ui @@ -0,0 +1,276 @@ + + + StoragePage + + + + 0 + 0 + 731 + 538 + + + + + + + + + Clear Screenshots + + + + + + + 0 B + + + + + + + Disk space used + + + + + + + 0 B + + + + + + + 0 B + + + + + + + Disk space remaining + + + + + + + 0 B + + + + + + + 0 B + + + + + + + + 0 + 0 + + + + Resource packs + + + + + + + + 0 + 0 + + + + Worlds + + + + + + + false + + + + + + true + + + + + + + + 0 + 0 + + + + Screenshots + + + + + + + Clear All + + + + + + + + 0 + 0 + + + + Combined + + + + + + + 0 B + + + + + + + + 0 + 0 + + + + Logs + + + + + + + 0 B + + + + + + + + 0 + 0 + + + + 0 B + + + + + + + false + + + + + + true + + + + + + + + 0 + 0 + + + + Mods + + + + + + + Clear Logs + + + + + + + true + + + Go to Resource packs + + + false + + + + + + + true + + + Go to Mods + + + false + + + + + + + true + + + Go to Worlds + + + false + + + + + + + Go to Other Logs + + + + + + + Go to Screenshots + + + + + + + + + + diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 5dc220ef..b929ad13 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -313,28 +313,22 @@ void WorldListPage::mceditError() void WorldListPage::mceditState(LoggedProcess::State state) { - bool failed = false; switch(state) { case LoggedProcess::NotRunning: case LoggedProcess::Starting: return; + case LoggedProcess::Running: + case LoggedProcess::Finished: + m_mceditStarting = false; + return; case LoggedProcess::FailedToStart: case LoggedProcess::Crashed: case LoggedProcess::Aborted: - { - failed = true; - } - case LoggedProcess::Running: - case LoggedProcess::Finished: - { - m_mceditStarting = false; - break; - } - } - if(failed) - { - mceditError(); + mceditError(); + return; + default: + qWarning() << "Invalid MCEdit state"; } } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp index 03196685..c896fecf 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp @@ -65,7 +65,7 @@ QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionListPtr vlist, // select recommended build for (int i = 0; i < vlist->versions().size(); i++) { auto version = vlist->versions().at(i); - auto reqs = version->requires(); + auto reqs = version->required(); // filter by minecraft version, if the loader depends on a certain version. if (minecraftVersion != nullptr) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 81bc5f29..ebec5c1b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -179,10 +179,9 @@ void ModpackListModel::refresh() performPaginatedSearch(); } -static auto sortFromIndex(int index) -> QString +static QString sortFromIndex(int index, bool& unhandled) { switch(index){ - default: case 0: return "relevance"; case 1: @@ -193,6 +192,10 @@ static auto sortFromIndex(int index) -> QString return "newest"; case 4: return "updated"; + default: + unhandled = true; + qWarning() << QString("Unhandled case in sortFromIndex (%i)").arg(QString::number(index)); + break; } return {}; @@ -200,11 +203,11 @@ static auto sortFromIndex(int index) -> QString void ModpackListModel::searchWithTerm(const QString& term, const int sort) { - if(sort > 5 || sort < 0) + bool unhandled = false; + auto sort_str = sortFromIndex(sort, unhandled); + if (unhandled) return; - auto sort_str = sortFromIndex(sort); - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) { return; } @@ -312,6 +315,8 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) void ModpackListModel::searchRequestFailed(QString reason) { + qWarning() << QString("searchRequestFailed reason: %1").arg(reason); + auto failed_action = jobPtr->getFailedActions().at(0); if (!failed_action->m_reply) { // Network error diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index 14683778..b1851878 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -69,6 +69,7 @@ bool JavaWizardPage::validatePage() case JavaSettingsWidget::ValidationStatus::AllOK: { settings->set("JavaPath", m_java_widget->javaPath()); + [[fallthrough]]; } case JavaSettingsWidget::ValidationStatus::JavaBad: { diff --git a/launcher/ui/widgets/Common.cpp b/launcher/ui/widgets/Common.cpp index 097bb6d4..3c576d3a 100644 --- a/launcher/ui/widgets/Common.cpp +++ b/launcher/ui/widgets/Common.cpp @@ -1,13 +1,17 @@ #include "Common.h" +#include + // Origin: Qt // More specifically, this is a trimmed down version on the algorithm in: // https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#846 -QList> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height) +QStringList viewItemTextLayout(QTextLayout& textLayout, QSize bounds, qreal& height) { - QList> lines; + QStringList result; height = 0; + QFontMetrics fontMetrics{ textLayout.font() }; + textLayout.beginLayout(); QString str = textLayout.text(); @@ -19,15 +23,23 @@ QList> viewItemTextLayout(QTextLayout& textLayout, int if (line.textLength() == 0) break; - line.setLineWidth(lineWidth); - line.setPosition(QPointF(0, height)); - + line.setLineWidth(bounds.width()); height += line.height(); - lines.append(std::make_pair(line.naturalTextWidth(), str.mid(line.textStart(), line.textLength()))); + // If the *next* line has enough space to be drawn, then we don't need to elide this line. + if (height + fontMetrics.lineSpacing() < bounds.height()) { + result.append(str.mid(line.textStart(), line.textLength())); + } else { + // Otherwise, if *this* line has enough space to be drawn, draw it elided. + if (height < bounds.height()) { + result.append(fontMetrics.elidedText(str.mid(line.textStart()), Qt::ElideRight, bounds.width())); + } + // And end it here, since we know there's not enough space to draw the next line. + break; + } } textLayout.endLayout(); - return lines; + return result; } diff --git a/launcher/ui/widgets/Common.h b/launcher/ui/widgets/Common.h index b3dd5ca8..876261a9 100644 --- a/launcher/ui/widgets/Common.h +++ b/launcher/ui/widgets/Common.h @@ -6,4 +6,4 @@ * Returns a list of pairs, each containing the width of that line and that line's string, respectively. * The total height of those lines is set in the last argument, 'height'. */ -QList> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height); +QStringList viewItemTextLayout(QTextLayout& textLayout, QSize bounds, qreal& height); diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index fdc581b4..da435481 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -107,14 +107,14 @@ QString InfoFrame::renderColorCodes(QString input) { // // TODO: Wrap links inside tags - // https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes + // https://minecraft.wiki/w/Formatting_codes#Color_codes const QMap color_codes_map = { {'0', "#000000"}, {'1', "#0000AA"}, {'2', "#00AA00"}, {'3', "#00AAAA"}, {'4', "#AA0000"}, {'5', "#AA00AA"}, {'6', "#FFAA00"}, {'7', "#AAAAAA"}, {'8', "#555555"}, {'9', "#5555FF"}, {'a', "#55FF55"}, {'b', "#55FFFF"}, {'c', "#FF5555"}, {'d', "#FF55FF"}, {'e', "#FFFF55"}, {'f', "#FFFFFF"} }; - // https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes + // https://minecraft.wiki/w/Formatting_codes#Formatting_codes const QMap formatting_codes_map = { {'l', "b"}, {'m', "s"}, {'n', "u"}, {'o', "i"} }; diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 01be88d9..69302055 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -5,6 +5,8 @@ #include #include +#include + ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {} void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const @@ -34,6 +36,13 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o icon_width = icon_size.width(); icon_height = icon_size.height(); + float desired_dim = rect.height() - 10; + + auto scaleRatio = icon_width > icon_height ? desired_dim / icon_width : desired_dim / icon_height; + + icon_width *= scaleRatio; + icon_height *= scaleRatio; + icon_x_margin = (rect.height() - icon_width) / 2; icon_y_margin = (rect.height() - icon_height) / 2; } @@ -47,9 +56,8 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o opt.icon.paint(painter, x, y, icon_width, icon_height); } - // Change the rect so that funther painting is easier - auto remaining_width = rect.width() - icon_width - 2 * icon_x_margin; - rect.setRect(rect.x() + icon_width + 2 * icon_x_margin, rect.y(), remaining_width, rect.height()); + // Change the rect so that further painting is easier + rect.setTopLeft(QPoint(rect.x() + icon_width + 2 * icon_x_margin, rect.y() + 4)); { // Title painting auto title = index.data(UserDataTypes::TITLE).toString(); @@ -66,10 +74,15 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o font.setPointSize(font.pointSize() + 2); painter->setFont(font); - // On the top, aligned to the left after the icon - painter->drawText(rect.x(), rect.y() + QFontMetrics(font).height(), title); + QFontMetrics fontMetrics{font}; + QRect titleRect(rect.topLeft() + QPoint(0, fontMetrics.ascent() - fontMetrics.height()), QSize(rect.width(), fontMetrics.height())); + // On the top, aligned to the left after the icon + painter->drawText(titleRect, title, QTextOption(Qt::AlignTop)); painter->restore(); + + // Change the rect again so it takes up the space below the title text + rect.setTop(titleRect.bottom()); } { // Description painting @@ -78,22 +91,45 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o QTextLayout text_layout(description, opt.font); qreal height = 0; - auto cut_text = viewItemTextLayout(text_layout, remaining_width, height); + auto cut_text = viewItemTextLayout(text_layout, rect.size(), height); - // Get first line unconditionally - description = cut_text.first().second; - // Get second line, elided if needed - if (cut_text.size() > 1) { - if (cut_text.size() > 2) - description += opt.fontMetrics.elidedText(cut_text.at(1).second, opt.textElideMode, cut_text.at(1).first); - else - description += cut_text.at(1).second; - } + description = cut_text.join("\n"); - // On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare) - painter->drawText(rect.x(), rect.y() + rect.height() - 2.2 * opt.fontMetrics.height(), remaining_width, - 2 * opt.fontMetrics.height(), Qt::TextWordWrap, description); + QRect descriptionRect = rect; + painter->drawText(descriptionRect, Qt::TextWordWrap, description); } painter->restore(); } + +QSize ProjectItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + int height = 0; + + // 2px spacing between top and title + height += 2; + { // Ensure enough space for one line with the title font + auto font = option.font; + if (index.data(UserDataTypes::SELECTED).toBool()) { + font.setBold(true); + font.setUnderline(true); + } + + font.setPointSize(font.pointSize() + 2); + + // Ensure enough space for the title + height += QFontMetrics{ font }.height(); + } + + { // Ensure enough space for 2 lines of description text + height += QFontMetrics{ option.font }.lineSpacing() * 2; + } + + QSize indexSizeHint = index.data(Qt::SizeHintRole).toSize(); + + if (indexSizeHint.isValid()) { + return QSize(indexSizeHint.width(), std::max(indexSizeHint.height(), height)); + } + + return QSize(0, height); +} diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h index f668edf6..f3bf3d52 100644 --- a/launcher/ui/widgets/ProjectItem.h +++ b/launcher/ui/widgets/ProjectItem.h @@ -21,5 +21,6 @@ class ProjectItemDelegate final : public QStyledItemDelegate { ProjectItemDelegate(QWidget* parent); void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; + QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override; }; diff --git a/libraries/javacheck/CMakeLists.txt b/libraries/javacheck/CMakeLists.txt index fd545d2b..7fb05b42 100644 --- a/libraries/javacheck/CMakeLists.txt +++ b/libraries/javacheck/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.9.4) project(launcher Java) -find_package(Java 1.7 REQUIRED COMPONENTS Development) +find_package(Java 1.8 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT JavaCheck) -set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) +set(CMAKE_JAVA_COMPILE_FLAGS -target 8 -source 8 -Xlint:deprecation -Xlint:unchecked) set(SRC JavaCheck.java diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index c4dfa5b7..aea15c1f 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.9.4) project(launcher Java) -find_package(Java 1.7 REQUIRED COMPONENTS Development) +find_package(Java 1.8 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT org.polymc.EntryPoint) -set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) +set(CMAKE_JAVA_COMPILE_FLAGS -target 8 -source 8 -Xlint:deprecation -Xlint:unchecked) set(SRC org/polymc/EntryPoint.java diff --git a/libraries/murmur2/src/MurmurHash2.cpp b/libraries/murmur2/src/MurmurHash2.cpp index b625efb1..0d62837e 100644 --- a/libraries/murmur2/src/MurmurHash2.cpp +++ b/libraries/murmur2/src/MurmurHash2.cpp @@ -89,8 +89,10 @@ void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev) switch (prev.len) { case 3: prev.h ^= data[2] << 16; + [[fallthrough]]; case 2: prev.h ^= data[1] << 8; + [[fallthrough]]; case 1: prev.h ^= data[0]; prev.h *= m; diff --git a/libraries/systeminfo/src/sys_unix.cpp b/libraries/systeminfo/src/sys_unix.cpp index 3c63e73a..05269bb1 100644 --- a/libraries/systeminfo/src/sys_unix.cpp +++ b/libraries/systeminfo/src/sys_unix.cpp @@ -66,16 +66,16 @@ uint64_t Sys::getSystemRam() // ignore rest of the line file.ignore(std::numeric_limits::max(), '\n'); } -#elif defined(Q_OS_FREEBSD) +#elif defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) char buff[512]; - FILE *fp = popen("sysctl hw.physmem", "r"); + FILE *fp = popen("sysctl -n hw.physmem", "r"); if (fp != NULL) { while(fgets(buff, 512, fp) != NULL) { std::string str(buff); - uint64_t mem = std::stoull(str.substr(12, std::string::npos)); - return mem * 1024ull; + uint64_t mem = std::stoull(str); + return mem; } } #endif diff --git a/nix/default.nix b/nix/default.nix index 05fe0150..be54026c 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -17,7 +17,7 @@ , msaClientID ? "" , extraJDKs ? [ ] , extra-cmake-modules - +, qtcharts # flake , self , version @@ -51,7 +51,7 @@ mkDerivation rec { src = lib.cleanSource self; nativeBuildInputs = [ cmake extra-cmake-modules ninja jdk ghc_filesystem file wrapQtAppsHook ]; - buildInputs = [ qtbase quazip zlib ]; + buildInputs = [ qtbase quazip zlib qtcharts ]; dontWrapQtApps = true; diff --git a/program_info/sneedmc.manifest b/program_info/sneedmc.manifest index 51af6c39..c2274fdf 100644 --- a/program_info/sneedmc.manifest +++ b/program_info/sneedmc.manifest @@ -10,7 +10,7 @@ - + Custom Minecraft launcher for managing multiple installs. diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 4efb90ac..47a963b0 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -147,6 +147,42 @@ slots: f(); } + void test_copy_with_dot_hidden() + { + QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); + auto f = [&folder]() + { + QTemporaryDir tempDir; + tempDir.setAutoRemove(true); + qDebug() << "From:" << folder << "To:" << tempDir.path(); + + QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); + qDebug() << tempDir.path(); + qDebug() << target_dir.path(); + FS::copy c(folder, target_dir.path()); + c(); + + auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; + + for (auto entry: target_dir.entryList(filter)) { + qDebug() << entry; + } + + QVERIFY(target_dir.entryList(filter).contains(".secret_folder")); + target_dir.cd(".secret_folder"); + QVERIFY(target_dir.entryList(filter).contains(".secret_file.txt")); + }; + + // first try variant without trailing / + QVERIFY(!folder.endsWith('/')); + f(); + + // then variant with trailing / + folder.append('/'); + QVERIFY(folder.endsWith('/')); + f(); + } + void test_getDesktop() { QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); diff --git a/tests/testdata/FileSystem/test_folder/.secret_folder/.secret_file.txt b/tests/testdata/FileSystem/test_folder/.secret_folder/.secret_file.txt new file mode 100644 index 00000000..65e37085 Binary files /dev/null and b/tests/testdata/FileSystem/test_folder/.secret_folder/.secret_file.txt differ