Merge remote-tracking branch 'upstream/develop' into develop

Signed-off-by: sneedium <sneed@sneedmc.org>
This commit is contained in:
sneedium 2024-03-24 01:33:10 -04:00
commit bc1c592f41
Signed by: sneedium
GPG Key ID: 906F66490FBE722F
126 changed files with 2337 additions and 323 deletions

2
.clangd Normal file
View File

@ -0,0 +1,2 @@
CompileFlags:
CompilationDatabase: build

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -45,6 +45,8 @@
#include <QTextStream>
#include <QUrl>
#include <system_error>
#if defined Q_OS_WIN32
#include <objbase.h>
#include <objidl.h>
@ -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);
if (m_blacklist && m_blacklist->matches(relative_path))
continue;
auto dst_path = PathCombine(dst, relative_path);
ensureFilePathExists(dst_path);
fs::copy(toStdString(src_path), toStdString(dst_path), opt, err);
if (err) {
qWarning() << "Failed to copy files:" << QString::fromStdString(err.message());
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;
}
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;
}
}

View File

@ -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);
}

View File

@ -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)
{

View File

@ -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;
}

View File

@ -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)
{

View File

@ -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."));

View File

@ -40,6 +40,7 @@
#include "FileSystem.h"
#include <QDebug>
#include <deque>
// ours
bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &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<QString, QString> MMCZip::findFolderOfFileInZip(QuaZip * zip, QSet<const QString> what, const QString &root)
{
QuaZipDir rootDir(zip, root);
std::deque<QString> pathsToTraverse;
pathsToTraverse.push_back(root);
while (!pathsToTraverse.empty())
{
QString currentPath = pathsToTraverse.front();
pathsToTraverse.pop_front();
QuaZipDir rootDir(zip, currentPath);
for(auto fileName: rootDir.entryList(QDir::Files))
{
if(fileName == what)
return root;
if (what.contains(fileName))
return {currentPath, fileName};
}
for(auto fileName: rootDir.entryList(QDir::Dirs))
{
QString result = findFolderOfFileInZip(zip, what, root + fileName);
if(!result.isEmpty())
{
return result;
pathsToTraverse.push_back(rootDir.path() + fileName);
}
}
return QString();
return {QString(), QString()};
}
// ours
@ -292,10 +297,15 @@ std::optional<QStringList> 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;

View File

@ -78,11 +78,11 @@ namespace MMCZip
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& 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<QString, QString> findFolderOfFileInZip(QuaZip * zip, QSet<const QString> what, const QString &root = QString(""));
/**
* Find a multiple files of the same name in archive by file name

View File

@ -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:
{

View File

@ -448,6 +448,16 @@ QList<QString> 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

View File

@ -91,4 +91,5 @@ int main(int argc, char *argv[])
case Application::Succeeded:
return 0;
}
return 0;
}

View File

@ -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();
}
}
bool Index::hasUid(const QString &uid) const

View File

@ -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<QString> requires;
auto reqArray = requireArray(obj, keyName);
auto iter = reqArray.begin();
while(iter != reqArray.end())

View File

@ -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();
}

View File

@ -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();

View File

@ -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();

View File

@ -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))

View File

@ -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<LaunchTask> 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)
{

View File

@ -173,6 +173,7 @@ protected: // data
mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
mutable std::shared_ptr<QString> m_authlibinjector_javaagent = std::make_shared<QString>();
};
typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr;

View File

@ -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())
{

View File

@ -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

View File

@ -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)
{

View File

@ -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();
}

View File

@ -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;

View File

@ -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();
}

View File

@ -109,6 +109,10 @@ public:
MinecraftAccountPtr defaultAccount() const;
void setDefaultAccount(MinecraftAccountPtr profileId);
bool anyAccountIsValid();
bool drmCheck()
{
return true;
}
bool isActive() const;

View File

@ -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?

View File

@ -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<AccountTask> MinecraftAccount::login(QString password) {
Q_ASSERT(m_currentTask.get() == nullptr);
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<AccountTask> 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();

View File

@ -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";
}

View File

@ -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(

View File

@ -90,6 +90,7 @@ public slots:
private:
void sendRequest(QUrl endpoint, QByteArray content);
QString getBaseUrl();
protected:
QNetworkReply *m_netReply = nullptr;

View File

@ -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));
}

View File

@ -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;
};

View File

@ -0,0 +1,58 @@
#include "AuthlibInjectorStep.h"
#include "Application.h"
#include <iostream>
#include <QNetworkRequest>
#include <QUuid>
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"));
}
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <QObject>
#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<QNetworkReply> m_reply;
};

View File

@ -19,4 +19,7 @@ public:
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
private:
QString baseUrl;
};

View File

@ -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);

View File

@ -19,4 +19,7 @@ public:
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
private:
QString getBaseUrl();
};

View File

@ -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() {
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;
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);
}

View File

@ -0,0 +1,80 @@
#include "ConfigureAuthlibInjector.h"
#include <launch/LaunchTask.h>
#include <QDir>
#include <QFileInfo>
#include <QJsonDocument>
#include <Qt>
#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<QString> 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<NetJob>("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<NetJob>("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() {}

View File

@ -0,0 +1,24 @@
#pragma once
#include <launch/LaunchStep.h>
#include <minecraft/auth/MinecraftAccount.h>
#include "net/NetJob.h"
class ConfigureAuthlibInjector: public LaunchStep
{
Q_OBJECT
public:
explicit ConfigureAuthlibInjector(LaunchTask *parent, QString authlibinjector_base_url, std::shared_ptr<QString> javaagent_arg);
virtual ~ConfigureAuthlibInjector() {};
void executeTask() override;
void finalize() override;
bool canAbort() const override
{
return false;
}
private:
std::unique_ptr<NetJob> m_job;
std::shared_ptr<QString> m_javaagent_arg;
QString m_authlibinjector_base_url;
};

View File

@ -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<MinecraftInstance>(m_parent->instance());
auto packProfile = instance->getPackProfile();

View File

@ -82,6 +82,8 @@ std::pair<int, bool> 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());

View File

@ -66,6 +66,7 @@ std::pair<int, bool> 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<int, bool> 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())

View File

@ -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<int, std::pair<Version, Version>> 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<int, bool> 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();

View File

@ -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;

View File

@ -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 {

View File

@ -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";
}
}

View File

@ -405,7 +405,8 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
QHash<QString, QString> 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())

View File

@ -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

View File

@ -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<File *,QByteArray *>();
@ -73,7 +72,6 @@ void Flame::FileResolvingTask::netJobFinished()
blockedProjects.insert(&out, output);
}
}
index++;
}
connect(job, &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished);

View File

@ -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;

View File

@ -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: {

View File

@ -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);

View File

@ -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;

View File

@ -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()) {

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file>
</qresource>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 24 24"
enable-background="new 0 0 24 24"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs80" />
<rect
fill="none"
width="24"
height="24"
id="rect66" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#FFFFFF"
d="M18.5,5h-1.1c0-0.6-0.6-1-1.4-1c-0.8,0-1.4,0.4-1.4,1h-1.1v2h5V5z"
id="path75" />
<g
id="g3784"
transform="matrix(0.73845672,0,0,0.73845672,4.237417,0.430303)"
style="fill:#e6e6e6;fill-opacity:1;stroke:#585858;stroke-opacity:1"><rect
style="fill:#e6e6e6;fill-opacity:1;stroke:#585858;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:#e6e6e6;fill-opacity:1;stroke:#585858;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#585858;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,8 +1,14 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/backgrounds">
<file alias="kitteh">catbgrnd2.png</file>
<file alias="catmas">catmas.png</file>
<file alias="cattiversary">cattiversary.png</file>
<file alias="defaultCatmas">catmas.png</file>
<file alias="defaultCattiversary">cattiversary.png</file>
<file alias="defaultCat">catbackground.png</file>
<file alias="jinxCat">jinxbg.png</file>
<file alias="jinxCatmas">jinxmas.png</file>
<file alias="jinxCattiversary">jinxversary.png</file>
<file alias="floppaCat">floppabg.png</file>
<file alias="floppaCatmas">floppaxmas.png</file>
<file alias="floppaCattiversary">floppaversary.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@ -39,6 +39,7 @@
<file>scalable/status-good.svg</file>
<file>scalable/status-running.svg</file>
<file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file>
</qresource>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
fill="#757575"
height="24"
viewBox="0 0 24 24"
width="24"
version="1.1"
id="svg1594"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1598" />
<g
id="g3784-3"
transform="matrix(1.0718486,0,0,1.0718486,-5.0666791,-4.7930816)"
style="fill:#757575;fill-opacity:1;stroke:#757575;stroke-opacity:1">
<rect
style="fill:#757575;fill-opacity:1;stroke:#757575;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197-6"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" />
<rect
style="fill:#757575;fill-opacity:1;stroke:#757575;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774-7"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#757575;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path1828-5"
cx="21.224644"
cy="21.072649"
r="0.95399094" />
</g>
<g
id="g3784"
transform="matrix(0.95836194,0,0,0.95836194,-3.2596703,-3.0150411)"
style="fill:#757575;fill-opacity:1;stroke:#ffffff;stroke-opacity:1">
<rect
style="fill:#757575;fill-opacity:1;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" />
<rect
style="fill:#757575;fill-opacity:1;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file>
</qresource>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
sodipodi:docname="storage.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1540"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="4.9939416"
inkscape:cx="20.925355"
inkscape:cy="15.618925"
inkscape:window-width="909"
inkscape:window-height="1064"
inkscape:window-x="1001"
inkscape:window-y="6"
inkscape:window-maximized="1"
inkscape:current-layer="Calque_1" /><defs
id="defs5152" />
<g
id="g3784"
transform="matrix(1.4769134,0,0,1.4769134,-7.5163878,-7.1393945)"
style="stroke:#3366cc;stroke-opacity:1"><rect
style="fill:none;stroke:#3366cc;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:none;stroke:#3366cc;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#3366cc;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file>
</qresource>

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs4421" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#3366CC"
d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6 v20C32,29.3,29.3,32,26,32z"
id="path4374" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#DAEEFF"
d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
id="path4376" />
<g
id="g4388">
</g>
<g
id="g4390">
</g>
<g
id="g4392">
</g>
<g
id="g4394">
</g>
<g
id="g4396">
</g>
<g
id="g4398">
</g>
<g
id="g4400">
</g>
<g
id="g4402">
</g>
<g
id="g4404">
</g>
<g
id="g4406">
</g>
<g
id="g4408">
</g>
<g
id="g4410">
</g>
<g
id="g4412">
</g>
<g
id="g4414">
</g>
<g
id="g4416">
</g>
<g
id="g3784"
transform="translate(0.07734108,0.33259869)"
style="stroke:#666666;stroke-opacity:1"><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#666666;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file>
</qresource>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
sodipodi:docname="storage.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview271"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="9.9878833"
inkscape:cx="15.468743"
inkscape:cy="20.524869"
inkscape:window-width="909"
inkscape:window-height="1064"
inkscape:window-x="1001"
inkscape:window-y="6"
inkscape:window-maximized="1"
inkscape:current-layer="g3784" /><defs
id="defs3662" />
<g
id="g3645">
<rect
x="6"
y="0"
fill="none"
width="20"
height="0"
id="rect3635" />
<polygon
fill="none"
points="0,6 0,6 0,26 0,26 0,9 "
id="polygon3637" />
<polygon
fill="none"
points="32,6 32,9 32,26 32,26 32,6 "
id="polygon3639" />
<path
fill="#39B54A"
d="M32,9V6c0-3.3-2.7-6-6-6H6C2.7,0,0,2.7,0,6v3H32z"
id="path3641" />
<path
fill="#8C6239"
d="M0,9v17c0,3.3,2.7,6,6,6h20c3.3,0,6-2.7,6-6V9H0z"
id="path3643" />
</g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#F2F2F2"
d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
id="path3647" />
<g
id="g3784"
transform="translate(0.07734108,0.33259869)"
style="stroke:#666666;stroke-opacity:1"><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#666666;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file>
</qresource>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs2339" />
<g
id="g2306">
</g>
<g
id="g2308">
</g>
<g
id="g2310">
</g>
<g
id="g2312">
</g>
<g
id="g2314">
</g>
<g
id="g2316">
</g>
<g
id="g2318">
</g>
<g
id="g2320">
</g>
<g
id="g2322">
</g>
<g
id="g2324">
</g>
<g
id="g2326">
</g>
<g
id="g2328">
</g>
<g
id="g2330">
</g>
<g
id="g2332">
</g>
<g
id="g2334">
</g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#f2f2f2"
d="m 26.077341,32.020895 h -20 c -3.3,0 -6.00000002,-2.7 -6.00000002,-6 V 6.0208954 c 0,-3.3 2.70000002,-5.99999999 6.00000002,-5.99999999 h 20 c 3.3,0 6,2.69999999 6,5.99999999 V 26.020895 c 0,3.3 -2.7,6 -6,6 z"
id="path659"
style="fill:#000000;fill-opacity:1" /><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4d4d4d"
d="m 28.077341,6.0208954 c 0,-1.1 -0.9,-2 -2,-2 h -20 c -1.1,0 -2,0.9 -2,2 V 26.020895 c 0,1.1 0.9,2 2,2 h 20 c 1.1,0 2,-0.9 2,-2 z"
id="path661"
style="fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-opacity:1" /><rect
style="fill:none;fill-opacity:1;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.270771"
y="6.9590669"
ry="2.1458371" /><rect
style="fill:none;stroke:#666666;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.270771"
y="17.769564"
ry="1.7275306" /><circle
style="fill:#666666;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="path1828"
cx="21.301985"
cy="21.093542"
r="0.95399094" /></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -32,6 +32,7 @@
<file>scalable/status-bad.svg</file>
<file>scalable/status-good.svg</file>
<file>scalable/status-yellow.svg</file>
<file>scalable/storage.svg</file>
<file>scalable/viewfolder.svg</file>
<file>scalable/worlds.svg</file>
</qresource>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Calque_1"
x="0px"
y="0px"
viewBox="0 0 32 32"
enable-background="new 0 0 32 32"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs706" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#F2F2F2"
d="M26,32H6c-3.3,0-6-2.7-6-6V6c0-3.3,2.7-6,6-6h20c3.3,0,6,2.7,6,6 v20C32,29.3,29.3,32,26,32z"
id="path659" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4D4D4D"
d="M28,6c0-1.1-0.9-2-2-2H6C4.9,4,4,4.9,4,6v20c0,1.1,0.9,2,2,2h20 c1.1,0,2-0.9,2-2V6z"
id="path661" />
<g
id="g673">
</g>
<g
id="g675">
</g>
<g
id="g677">
</g>
<g
id="g679">
</g>
<g
id="g681">
</g>
<g
id="g683">
</g>
<g
id="g685">
</g>
<g
id="g687">
</g>
<g
id="g689">
</g>
<g
id="g691">
</g>
<g
id="g693">
</g>
<g
id="g695">
</g>
<g
id="g697">
</g>
<g
id="g699">
</g>
<g
id="g701">
</g>
<rect
style="fill:none;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none"
id="rect1197"
width="17.458458"
height="17.458458"
x="7.1934299"
y="6.9381723"
ry="2.1458371" /><rect
style="fill:none;stroke:#ffffff;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none"
id="rect1774"
width="17.458458"
height="6.6479607"
x="7.1934299"
y="17.748669"
ry="1.7275306" /><circle
style="fill:#ffffff;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:none"
id="path1828"
cx="21.224644"
cy="21.072649"
r="0.95399094" /></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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)

View File

@ -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<QString, Language>& languages)
return;
}
int index = 1;
try
{
auto toplevel_doc = Json::requireDocument(data);
@ -292,7 +293,6 @@ void readIndex(const QString & path, QMap<QString, Language>& 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<Column>(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<Column>(index.column());
switch(column)
{
case Column::Language:
{
return lang.languageName();
}
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<Language>::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;

View File

@ -40,7 +40,7 @@ public:
void downloadIndex();
private:
Language *findLanguage(const QString & key);
QVector<Language>::iterator findLanguage(const QString & key);
void reloadLocalFiles();
void downloadTranslation(QString key);
void downloadNext();

View File

@ -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();

View File

@ -195,6 +195,8 @@ private slots:
void updateNewsLabel();
void updateCat();
void konamiTriggered();
void globalSettingsClosed();

View File

@ -4,29 +4,102 @@
namespace WinDarkmode {
/* See https://github.com/statiolake/neovim-qt/commit/da8eaba7f0e38b6b51f3bacd02a8cc2d1f7a34d8 */
void setDarkWinTitlebar(WId winid, bool darkmode)
template<int syscall_id, typename... arglist> __attribute((naked)) uint32_t __fastcall WinSyscall([[maybe_unused]] arglist... args)
{
HWND hwnd = reinterpret_cast<HWND>(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<fnAllowDarkModeForWindow>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(133)));
fnSetPreferredAppMode SetPreferredAppMode
= reinterpret_cast<fnSetPreferredAppMode>(GetProcAddress(hUxtheme, MAKEINTRESOURCEA(135)));
fnSetWindowCompositionAttribute SetWindowCompositionAttribute
= reinterpret_cast<fnSetWindowCompositionAttribute>(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<NtUserSetWindowCompositionAttribute_NT6_2>(hWnd, &data);
else if (IsWindows8_1_Only())
WinSyscall<NtUserSetWindowCompositionAttribute_NT6_3>(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
}
}

View File

@ -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 {

View File

@ -20,9 +20,10 @@
#include <QtWidgets/QPushButton>
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
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)
{

View File

@ -18,6 +18,7 @@
#include <QtWidgets/QDialog>
#include <QtCore/QEventLoop>
#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;
};

View File

@ -33,6 +33,13 @@
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="authlibInjectorBaseTextBox">
<property name="placeholderText">
<string>AuthlibInjector base URL (e.g. ely.by)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="placeholderText">

View File

@ -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("<a href='%1'>%2</a>").arg(article_entry->link, new_title));
ui->currentArticleContentBrowser->setText(article_entry->content);

View File

@ -220,10 +220,10 @@ bool AccessibleInstanceView::selectRow(int row)
}
break;
}
default: {
default:
qWarning() << "Unhandled QAbstractItemView selection type!";
break;
}
}
view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
return true;
@ -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;
}

View File

@ -40,10 +40,8 @@
#include <QMessageBox>
#include <QFileDialog>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTabBar>
#include <QValidator>
#include <QVariant>
#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);

View File

@ -4,6 +4,7 @@
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2022 Lenny McLennington <lenny@sneed.church>
* 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 <QWidget>
#include <QValidator>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include "ui/pages/BasePage.h"
#include <Application.h>
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

View File

@ -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."
"<br><br>"
"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") {

View File

@ -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();

View File

@ -54,6 +54,7 @@
</attribute>
<addaction name="actionAddMicrosoft"/>
<addaction name="actionAddMojang"/>
<addaction name="actionAddAuthlibInjector"/>
<addaction name="actionAddOffline"/>
<addaction name="actionRefresh"/>
<addaction name="actionRemove"/>
@ -68,6 +69,11 @@
<string>Add &amp;Mojang</string>
</property>
</action>
<action name="actionAddAuthlibInjector">
<property name="text">
<string>Add Authlib-&amp;Injector</string>
</property>
</action>
<action name="actionRemove">
<property name="text">
<string>Remo&amp;ve</string>

Some files were not shown because too many files have changed in this diff Show More