Merge branch 'MultiMC:develop' into develop

This commit is contained in:
Davide Pierotti 2023-05-16 09:21:40 +02:00 committed by GitHub
commit ea262c45ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1920 additions and 410 deletions

20
.github/workflows/dispatch.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Dispatcher
on:
push:
branches: ['6']
jobs:
dispatch:
name: Dispatch
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Extract branch name
shell: bash
run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >>$GITHUB_OUTPUT
id: extract_branch
- name: Dispatch to workflows
run: |
curl -H "Accept: application/vnd.github.everest-preview+json" \
-H "Authorization: token ${{ secrets.DISPATCH_TOKEN }}" \
--request POST \
--data '{"event_type": "push_to_main_repo", "client_payload": { "branch": "${{ steps.extract_branch.outputs.branch }}" }}' https://api.github.com/repos/MultiMC/Build/dispatches

View File

@ -195,7 +195,7 @@ if(Launcher_LAYOUT_REAL STREQUAL "mac-bundle")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}")
set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns)
set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2015-2021 ${Launcher_Copyright}")
set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2015-2023 ${Launcher_Copyright}")
# directories to look for dependencies
set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})

View File

@ -12,7 +12,7 @@ If you want to contribute, talk to us on [Discord](https://discord.gg/multimc) f
While blindly submitting PRs is definitely possible, they're not necessarily going to get accepted.
We aren't looking for flashy features, but expanding upon the existing feature set without distruption or endangering future viability of the project is OK.
We aren't looking for flashy features, but expanding upon the existing feature set without disruption or endangering the future viability of the project is OK.
### Building
If you want to build the launcher yourself, check [BUILD.md](BUILD.md) for build instructions.
@ -40,9 +40,9 @@ Unless required by applicable law or agreed to in writing, software distributed
## Forking/Redistributing/Custom builds policy
We keep Launcher open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.
The license gives you access to the source MultiMC is build from, but:
- Not the name, logo and other branding.
- Not the API tokens required to talk to services the launcher depends on.
The license gives you access to the source MultiMC is built from, but not:
- The name, logo and other branding.
- The API tokens required to talk to services that the launcher depends on.
Because of the nature of the agreements required to interact with the Microsoft identity platform, it's impossible for us to continue allowing everyone to build the code as 'MultiMC'. The source code has been debranded and now builds as `DevLauncher` by default.

View File

@ -57,7 +57,7 @@ QString Config::printableVersionString() const
QString vstr = QString("%1.%2.%3").arg(QString::number(VERSION_MAJOR), QString::number(VERSION_MINOR), QString::number(VERSION_HOTFIX));
// If the build is not a main release, append the channel
if(VERSION_CHANNEL != "stable")
if(VERSION_CHANNEL != "develop")
{
vstr += "-" + VERSION_CHANNEL;
}

View File

@ -100,6 +100,12 @@ public:
QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/";
QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/";
/**
* The build that is reported to the Technic API.
*/
QString TECHNIC_API_BUILD = "multimc";
/**
* \brief Converts the Version to a string.
* \return The version number in string format (major.minor.revision.build).

View File

@ -575,7 +575,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
FS::updateTimestamp(m_rootPath);
#endif
qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT;
qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2023 " << BuildConfig.LAUNCHER_COPYRIGHT;
qDebug() << "Version : " << BuildConfig.printableVersionString();
qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT;
qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC;
@ -627,7 +627,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this));
// Updates
m_settings->registerSetting("UpdateChannel", BuildConfig.VERSION_CHANNEL);
m_settings->registerSetting("AutoUpdate", true);
// Theming
@ -718,6 +717,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("ShowGameTime", true);
m_settings->registerSetting("ShowGlobalGameTime", true);
m_settings->registerSetting("RecordGameTime", true);
m_settings->registerSetting("ShowGameTimeHours", false);
// Minecraft launch method
m_settings->registerSetting("MCLaunchMethod", "LauncherPart");
@ -811,7 +811,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM);
auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json";
qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl;
m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_CHANNEL, BuildConfig.VERSION_BUILD));
m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_BUILD));
qDebug() << "<> Updater started.";
}

View File

@ -1,4 +1,5 @@
/* Copyright 2013-2021 MultiMC Contributors
* Copyright 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -55,6 +56,14 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s
m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr);
m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr);
// Managed Packs
m_settings->registerSetting("ManagedPack", false);
m_settings->registerSetting("ManagedPackType", "");
m_settings->registerSetting("ManagedPackID", "");
m_settings->registerSetting("ManagedPackName", "");
m_settings->registerSetting("ManagedPackVersionID", "");
m_settings->registerSetting("ManagedPackVersionName", "");
}
QString BaseInstance::getPreLaunchCommand()
@ -72,6 +81,46 @@ QString BaseInstance::getPostExitCommand()
return settings()->get("PostExitCommand").toString();
}
bool BaseInstance::isManagedPack()
{
return settings()->get("ManagedPack").toBool();
}
QString BaseInstance::getManagedPackType()
{
return settings()->get("ManagedPackType").toString();
}
QString BaseInstance::getManagedPackID()
{
return settings()->get("ManagedPackID").toString();
}
QString BaseInstance::getManagedPackName()
{
return settings()->get("ManagedPackName").toString();
}
QString BaseInstance::getManagedPackVersionID()
{
return settings()->get("ManagedPackVersionID").toString();
}
QString BaseInstance::getManagedPackVersionName()
{
return settings()->get("ManagedPackVersionName").toString();
}
void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version)
{
settings()->set("ManagedPack", true);
settings()->set("ManagedPackType", type);
settings()->set("ManagedPackID", id);
settings()->set("ManagedPackName", name);
settings()->set("ManagedPackVersionID", versionId);
settings()->set("ManagedPackVersionName", version);
}
int BaseInstance::getConsoleMaxLines() const
{
auto lineSetting = settings()->getSetting("ConsoleMaxLines");
@ -264,6 +313,11 @@ QString BaseInstance::windowTitle() const
return BuildConfig.LAUNCHER_NAME + ": " + name().replace(QRegExp("[ \n\r\t]+"), " ");
}
QString BaseInstance::instanceTitle() const
{
return name().replace(QRegExp("[ \n\r\t]+"), " ");
}
// FIXME: why is this here? move it to MinecraftInstance!!!
QStringList BaseInstance::extraArguments() const
{

View File

@ -1,4 +1,5 @@
/* Copyright 2013-2021 MultiMC Contributors
* Copyright 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -109,6 +110,8 @@ public:
/// Value used for instance window titles
QString windowTitle() const;
QString instanceTitle() const;
QString iconKey() const;
void setIconKey(QString val);
@ -119,6 +122,14 @@ public:
QString getPostExitCommand();
QString getWrapperCommand();
bool isManagedPack();
QString getManagedPackType();
QString getManagedPackID();
QString getManagedPackName();
QString getManagedPackVersionID();
QString getManagedPackVersionName();
void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version);
/// guess log level from a line of game log
virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level)
{

View File

@ -509,6 +509,8 @@ set(TECHNIC_SOURCES
modplatform/technic/SingleZipPackInstallTask.cpp
modplatform/technic/SolderPackInstallTask.h
modplatform/technic/SolderPackInstallTask.cpp
modplatform/technic/SolderPackManifest.h
modplatform/technic/SolderPackManifest.cpp
modplatform/technic/TechnicPackProcessor.h
modplatform/technic/TechnicPackProcessor.cpp
)
@ -525,6 +527,10 @@ set(ATLAUNCHER_SOURCES
set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthPackManifest.cpp
modplatform/modrinth/ModrinthPackManifest.h
modplatform/modrinth/ModrinthInstanceExportTask.h
modplatform/modrinth/ModrinthInstanceExportTask.cpp
modplatform/modrinth/ModrinthHashLookupRequest.h
modplatform/modrinth/ModrinthHashLookupRequest.cpp
)
add_unit_test(Index
@ -782,6 +788,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/SkinUploadDialog.h
ui/dialogs/CreateShortcutDialog.cpp
ui/dialogs/CreateShortcutDialog.h
ui/dialogs/ModrinthExportDialog.cpp
ui/dialogs/ModrinthExportDialog.h
# GUI - widgets
ui/widgets/Common.cpp
@ -880,6 +888,7 @@ qt5_wrap_ui(LAUNCHER_UI
ui/dialogs/LoginDialog.ui
ui/dialogs/EditAccountDialog.ui
ui/dialogs/CreateShortcutDialog.ui
ui/dialogs/ModrinthExportDialog.ui
)
qt5_add_resources(LAUNCHER_RESOURCES

View File

@ -311,7 +311,14 @@ void InstanceImportTask::processModrinth() {
auto jsonFiles = Json::requireIsArrayOf<QJsonObject>(obj, "files", "modrinth.index.json");
for(auto & obj: jsonFiles) {
Modrinth::File file;
file.path = Json::requireString(obj, "path");
auto dirtyPath = Json::requireString(obj, "path");
dirtyPath.replace('\\', '/');
auto simplifiedPath = QDir::cleanPath(dirtyPath);
QFileInfo fileInfo (simplifiedPath);
if(simplifiedPath.startsWith("../") || simplifiedPath.contains("/../") || fileInfo.isAbsolute()) {
throw JSONValidationError("Invalid path found in modpack files:\n\n" + simplifiedPath);
}
file.path = simplifiedPath;
// env doesn't have to be present, in that case mod is required
auto env = Json::ensureObject(obj, "env");

View File

@ -78,6 +78,14 @@ QJsonObject requireObject(const QJsonDocument &doc, const QString &what)
}
return doc.object();
}
QJsonObject requireObject(const QJsonValueRef &node, const QString &what)
{
if (!node.isObject())
{
throw JsonException(what + " is not an object");
}
return node.toObject();
}
QJsonArray requireArray(const QJsonDocument &doc, const QString &what)
{
if (!doc.isArray())

View File

@ -41,6 +41,8 @@ QJsonDocument requireDocument(const QString &filename, const QString &what = "Do
/// @throw JsonException
QJsonObject requireObject(const QJsonDocument &doc, const QString &what = "Document");
/// @throw JsonException
QJsonObject requireObject(const QJsonValueRef &node, const QString &what = "Node");
/// @throw JsonException
QJsonArray requireArray(const QJsonDocument &doc, const QString &what = "Document");
/////////////////// WRITING ////////////////////

View File

@ -9,6 +9,8 @@
#include "ui/dialogs/ProgressDialog.h"
#include "ui/dialogs/EditAccountDialog.h"
#include "ui/dialogs/ProfileSetupDialog.h"
#include "ui/dialogs/LoginDialog.h"
#include "ui/dialogs/MSALoginDialog.h"
#include <QLineEdit>
#include <QInputDialog>
@ -223,16 +225,60 @@ void LaunchController::login() {
}
*/
case AccountState::Expired: {
auto errorString = tr("The account has expired and needs to be logged into manually again.");
QMessageBox::warning(
auto errorString = tr("The account has expired and needs to be logged into manually. Press OK to log in again.");
auto button = QMessageBox::warning(
m_parentWidget,
tr("Account refresh failed"),
errorString,
QMessageBox::StandardButton::Ok,
QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel,
QMessageBox::StandardButton::Ok
);
emitFailed(errorString);
return;
if (button == QMessageBox::StandardButton::Ok) {
auto accounts = APPLICATION->accounts();
bool isDefault = accounts->defaultAccount() == m_accountToUse;
bool msa = m_accountToUse->isMSA();
accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(m_accountToUse->profileId())));
MinecraftAccountPtr newAccount = nullptr;
if (msa) {
if(BuildConfig.BUILD_PLATFORM == "osx64") {
CustomMessageBox::selectable(
m_parentWidget,
tr("Microsoft Accounts not available"),
tr(
"Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n"
"Please update both your operating system and MultiMC."
),
QMessageBox::Warning
)->exec();
emitFailed(tr("Attempted to re-login to a Microsoft account on an unsupported platform"));
return;
}
newAccount = MSALoginDialog::newAccount(
m_parentWidget,
tr("Please enter your Mojang account email and password to add your account.")
);
} else {
newAccount = LoginDialog::newAccount(
m_parentWidget,
tr("Please enter your Mojang account email and password to add your account.")
);
}
if (newAccount) {
accounts->addAccount(newAccount);
if (isDefault) {
accounts->setDefaultAccount(newAccount);
}
m_accountToUse = nullptr;
decideAccount();
continue;
} else {
emitFailed(tr("Account expired and re-login attempt failed"));
return;
}
} else {
emitFailed(errorString);
return;
}
}
case AccountState::Gone: {
auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to.");

View File

@ -36,3 +36,7 @@ QString Time::prettifyDuration(int64_t duration) {
}
return QObject::tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes);
}
QString Time::prettifyDurationHours(int64_t duration) {
return QString("%1").arg(duration / 3600.0, 0, 'f', 0);
}

View File

@ -21,5 +21,6 @@
namespace Time {
QString prettifyDuration(int64_t duration);
QString prettifyDurationHours(int64_t duration);
}

View File

@ -48,6 +48,7 @@
#include "MinecraftLoadAndCheck.h"
#include "minecraft/gameoptions/GameOptions.h"
#include "minecraft/update/FoldersTask.h"
#include "minecraft/VersionFilterData.h"
#define IBUS "@im=ibus"
@ -425,10 +426,17 @@ QStringList MinecraftInstance::processMinecraftArgs(
if (serverToJoin && !serverToJoin->address.isEmpty())
{
args_pattern += " --server " + serverToJoin->address;
args_pattern += " --port " + QString::number(serverToJoin->port);
if (m_components->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate)
{
args_pattern += " --quickPlayMultiplayer " + serverToJoin->address + ":" + QString::number(serverToJoin->port);
}
else
{
args_pattern += " --server " + serverToJoin->address;
args_pattern += " --port " + QString::number(serverToJoin->port);
}
}
QMap<QString, QString> token_mapping;
// yggdrasil!
if(session) {
@ -489,6 +497,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
if (serverToJoin && !serverToJoin->address.isEmpty())
{
launchScript += "useQuickPlay " + QString::number(m_components->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate) + "\n";
launchScript += "serverAddress " + serverToJoin->address + "\n";
launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n";
}
@ -513,6 +522,8 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
.arg(settings()->get("MinecraftWinHeight").toInt());
launchScript += "windowTitle " + windowTitle() + "\n";
launchScript += "windowParams " + windowParams + "\n";
launchScript += "instanceTitle " + instanceTitle() + "\n";
launchScript += "instanceIconId " + iconKey() + "\n";
}
// legacy auth
@ -785,11 +796,19 @@ QString MinecraftInstance::getStatusbarDescription()
if(m_settings->get("ShowGameTime").toBool())
{
if (lastTimePlayed() > 0) {
description.append(tr(", last played for %1").arg(Time::prettifyDuration(lastTimePlayed())));
if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) {
description.append(tr(", last played for %1 hours").arg(Time::prettifyDurationHours(lastTimePlayed())));
} else {
description.append(tr(", last played for %1").arg(Time::prettifyDuration(lastTimePlayed())));
}
}
if (totalTimePlayed() > 0) {
description.append(tr(", total played for %1").arg(Time::prettifyDuration(totalTimePlayed())));
if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) {
description.append(tr(", total played for %1 hours").arg(Time::prettifyDurationHours(totalTimePlayed())));
} else {
description.append(tr(", total played for %1").arg(Time::prettifyDuration(totalTimePlayed())));
}
}
}
if(hasCrashed())

View File

@ -66,7 +66,8 @@ VersionFilterData::VersionFilterData()
"net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl",
"org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform"};
java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00");
java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00");
java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00");
java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00");
java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00");
java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00");
quickPlayBeginsDate = timeFromS3Time("2023-04-05T12:05:17+00:00");
}

View File

@ -27,5 +27,7 @@ struct VersionFilterData
QDateTime java16BeginsDate;
// release data of first version to require Java 17 (1.18 Pre Release 2)
QDateTime java17BeginsDate;
// release date of first version to use --quickPlayMultiplayer instead of --server/--port for directly joining servers
QDateTime quickPlayBeginsDate;
};
extern VersionFilterData g_VersionFilterData;

View File

@ -39,10 +39,11 @@
namespace ATLauncher {
PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version)
PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version)
{
m_support = support;
m_pack = pack;
m_pack_name = packName;
m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), "");
m_version_name = version;
}
@ -60,7 +61,7 @@ void PackInstallTask::executeTask()
qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId();
auto *netJob = new NetJob("ATLauncher::VersionFetch", APPLICATION->network());
auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json")
.arg(m_pack).arg(m_version_name);
.arg(m_pack_safe_name).arg(m_version_name);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
@ -96,6 +97,10 @@ void PackInstallTask::onDownloadSucceeded()
}
m_version = version;
// Display install message if one exists
if (!m_version.messages.install.isEmpty())
m_support->displayMessage(m_version.messages.install);
auto vlist = APPLICATION->metadataIndex()->get("net.minecraft");
if(!vlist)
{
@ -303,7 +308,7 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared
auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");
auto f = std::make_shared<VersionFile>();
f->name = m_pack + " " + m_version_name + " (libraries)";
f->name = m_pack_name + " " + m_version_name + " (libraries)";
for(const auto & lib : m_version.libraries) {
auto libName = detectLibrary(lib);
@ -408,7 +413,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<
}
auto f = std::make_shared<VersionFile>();
f->name = m_pack + " " + m_version_name;
f->name = m_pack_name + " " + m_version_name;
if(!mainClass.isEmpty() && !mainClasses.contains(mainClass)) {
f->mainClass = mainClass;
}
@ -450,9 +455,9 @@ void PackInstallTask::installConfigs()
setStatus(tr("Downloading configs..."));
jobPtr = new NetJob(tr("Config download"), APPLICATION->network());
auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name);
auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name);
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip")
.arg(m_pack).arg(m_version_name);
.arg(m_pack_safe_name).arg(m_version_name);
auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path);
entry->setStale(true);
@ -526,7 +531,7 @@ void PackInstallTask::downloadMods()
QVector<QString> selectedMods;
if (!optionalMods.isEmpty()) {
setStatus(tr("Selecting optional mods..."));
selectedMods = m_support->chooseOptionalMods(optionalMods);
selectedMods = m_support->chooseOptionalMods(m_version, optionalMods);
}
setStatus(tr("Downloading mods..."));
@ -810,6 +815,7 @@ void PackInstallTask::install()
instance.setName(m_instName);
instance.setIconKey(m_instIcon);
instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name);
instanceSettings->resumeSave();
jarmods.clear();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020-2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2021 Petr Mrazek <peterix@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -37,7 +37,7 @@ public:
/**
* Requests a user interaction to select which optional mods should be installed.
*/
virtual QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) = 0;
virtual QVector<QString> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) = 0;
/**
* Requests a user interaction to select a component version from a given version list
@ -45,6 +45,11 @@ public:
*/
virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0;
/**
* Requests a user interaction to display a message.
*/
virtual void displayMessage(QString message) = 0;
};
class PackInstallTask : public InstanceTask
@ -52,7 +57,7 @@ class PackInstallTask : public InstanceTask
Q_OBJECT
public:
explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version);
explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version);
virtual ~PackInstallTask(){}
bool canAbort() const override { return true; }
@ -94,7 +99,8 @@ private:
NetJob::Ptr jobPtr;
QByteArray response;
QString m_pack;
QString m_pack_name;
QString m_pack_safe_name;
QString m_version_name;
PackVersion m_version;

View File

@ -178,6 +178,8 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) {
p.depends.append(Json::requireValueString(depends));
}
}
p.colour = Json::ensureString(obj, QString("colour"), "");
p.warning = Json::ensureString(obj, QString("warning"), "");
p.client = Json::ensureBoolean(obj, QString("client"), false);
@ -197,6 +199,12 @@ static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments & a,
a.depends = Json::ensureString(obj, "depends", "");
}
static void loadVersionMessages(ATLauncher::VersionMessages & m, QJsonObject & obj)
{
m.install = Json::ensureString(obj, "install", "");
m.update = Json::ensureString(obj, "update", "");
}
void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
{
v.version = Json::requireString(obj, "version");
@ -244,4 +252,25 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
auto configsObj = Json::requireObject(obj, "configs");
loadVersionConfigs(v.configs, configsObj);
}
if(obj.contains("colours")) {
auto colourObj = Json::requireObject(obj, "colours");
for (const auto &key : colourObj.keys()) {
v.colours[key] = Json::requireValueString(colourObj.value(key), "colour");
}
}
if(obj.contains("warnings")) {
auto warningsObj = Json::requireObject(obj, "warnings");
for (const auto &key : warningsObj.keys()) {
v.warnings[key] = Json::requireValueString(warningsObj.value(key), "warning");
}
}
if(obj.contains("messages")) {
auto messages = Json::requireObject(obj, "messages");
loadVersionMessages(v.messages, messages);
}
}

View File

@ -16,8 +16,10 @@
#pragma once
#include <QMap>
#include <QString>
#include <QVector>
#include <QMap>
#include <QJsonObject>
namespace ATLauncher
@ -109,6 +111,8 @@ struct VersionMod
bool library;
QString group;
QVector<QString> depends;
QString colour;
QString warning;
bool client;
@ -134,6 +138,12 @@ struct PackVersionExtraArguments
QString depends;
};
struct VersionMessages
{
QString install;
QString update;
};
struct PackVersion
{
QString version;
@ -146,6 +156,10 @@ struct PackVersion
QVector<VersionLibrary> libraries;
QVector<VersionMod> mods;
VersionConfigs configs;
QMap<QString, QString> colours;
QMap<QString, QString> warnings;
VersionMessages messages;
};
void loadVersion(PackVersion & v, QJsonObject & obj);

View File

@ -0,0 +1,124 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#include <QJsonArray>
#include <QJsonDocument>
#include "ModrinthHashLookupRequest.h"
#include "BuildConfig.h"
#include "Json.h"
namespace Modrinth
{
HashLookupRequest::HashLookupRequest(QList<HashLookupData> hashes, QList<HashLookupResponseData> *output) : NetAction(), m_hashes(hashes), m_output(output)
{
m_url = "https://api.modrinth.com/v2/version_files";
m_status = Job_NotStarted;
}
void HashLookupRequest::startImpl()
{
finished = false;
m_status = Job_InProgress;
QNetworkRequest request(m_url);
request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject requestObject;
QJsonArray hashes;
for (const auto &data : m_hashes) {
hashes.append(data.hash);
}
requestObject.insert("hashes", hashes);
requestObject.insert("algorithm", QJsonValue("sha512"));
QNetworkReply *rep = m_network->post(request, QJsonDocument(requestObject).toJson());
m_reply.reset(rep);
connect(rep, &QNetworkReply::uploadProgress, this, &HashLookupRequest::downloadProgress);
connect(rep, &QNetworkReply::finished, this, &HashLookupRequest::downloadFinished);
connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError)));
}
void HashLookupRequest::downloadError(QNetworkReply::NetworkError error)
{
qCritical() << "Modrinth hash lookup request failed with error" << m_reply->errorString() << "Server reply:\n" << m_reply->readAll();
if (finished) {
qCritical() << "Double finished ModrinthHashLookupRequest!";
return;
}
m_status = Job_Failed;
finished = true;
m_reply.reset();
emit failed(m_index_within_job);
}
void HashLookupRequest::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
m_total_progress = bytesTotal;
m_progress = bytesReceived;
emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
}
void HashLookupRequest::downloadFinished()
{
if (finished) {
qCritical() << "Double finished ModrinthHashLookupRequest!";
return;
}
QByteArray data = m_reply->readAll();
m_reply.reset();
try {
auto document = Json::requireDocument(data);
auto rootObject = Json::requireObject(document);
for (const auto &hashData : m_hashes) {
if (rootObject.contains(hashData.hash)) {
auto versionObject = Json::requireObject(rootObject, hashData.hash);
auto files = Json::requireIsArrayOf<QJsonObject>(versionObject, "files");
QJsonObject file;
for (const auto &fileJson : files) {
auto hashes = Json::requireObject(fileJson, "hashes");
QString sha512 = Json::requireString(hashes, "sha512");
if (sha512 == hashData.hash) {
file = fileJson;
}
}
m_output->append(HashLookupResponseData {
hashData.fileInfo,
true,
file
});
} else {
m_output->append(HashLookupResponseData {
hashData.fileInfo,
false,
QJsonObject()
});
}
}
m_status = Job_Finished;
finished = true;
emit succeeded(m_index_within_job);
} catch (const Json::JsonException &e) {
qCritical() << "Failed to parse Modrinth hash lookup response: " << e.cause();
m_status = Job_Failed;
finished = true;
emit failed(m_index_within_job);
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#pragma once
#include <QFileInfo>
#include <QJsonObject>
#include "net/NetAction.h"
namespace Modrinth
{
struct HashLookupData
{
QFileInfo fileInfo;
QString hash;
};
struct HashLookupResponseData
{
QFileInfo fileInfo;
bool found;
QJsonObject fileJson;
};
class HashLookupRequest : public NetAction
{
public:
using Ptr = shared_qobject_ptr<HashLookupRequest>;
explicit HashLookupRequest(QList<HashLookupData> hashes, QList<HashLookupResponseData> *output);
static Ptr make(QList<HashLookupData> hashes, QList<HashLookupResponseData> *output) {
return Ptr(new HashLookupRequest(hashes, output));
}
protected slots:
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
void downloadError(QNetworkReply::NetworkError error) override;
void downloadFinished() override;
void downloadReadyRead() override {}
public slots:
void startImpl() override;
private:
QList<HashLookupData> m_hashes;
std::shared_ptr<QList<HashLookupResponseData>> m_output;
bool finished = true;
};
}

View File

@ -0,0 +1,252 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#include <QDir>
#include <QDirIterator>
#include <QCryptographicHash>
#include <QMap>
#include "Json.h"
#include "ModrinthInstanceExportTask.h"
#include "net/NetJob.h"
#include "Application.h"
#include "ui/dialogs/ModrinthExportDialog.h"
#include "JlCompress.h"
#include "FileSystem.h"
#include "ModrinthHashLookupRequest.h"
namespace Modrinth
{
InstanceExportTask::InstanceExportTask(InstancePtr instance, ExportSettings settings) : m_instance(instance), m_settings(settings) {}
void InstanceExportTask::executeTask()
{
setStatus(tr("Finding files to look up on Modrinth..."));
QDir modsDir(m_instance->gameRoot() + "/mods");
modsDir.setFilter(QDir::Files);
modsDir.setNameFilters(QStringList() << "*.jar");
QDir resourcePacksDir(m_instance->gameRoot() + "/resourcepacks");
resourcePacksDir.setFilter(QDir::Files);
resourcePacksDir.setNameFilters(QStringList() << "*.zip");
QDir shaderPacksDir(m_instance->gameRoot() + "/shaderpacks");
shaderPacksDir.setFilter(QDir::Files);
shaderPacksDir.setNameFilters(QStringList() << "*.zip");
QStringList filesToResolve;
if (modsDir.exists()) {
QDirIterator modsIterator(modsDir);
while (modsIterator.hasNext()) {
filesToResolve << modsIterator.next();
}
}
if (m_settings.includeResourcePacks && resourcePacksDir.exists()) {
QDirIterator resourcePacksIterator(resourcePacksDir);
while (resourcePacksIterator.hasNext()) {
filesToResolve << resourcePacksIterator.next();
}
}
if (m_settings.includeShaderPacks && shaderPacksDir.exists()) {
QDirIterator shaderPacksIterator(shaderPacksDir);
while (shaderPacksIterator.hasNext()) {
filesToResolve << shaderPacksIterator.next();
}
}
if (!m_settings.datapacksPath.isEmpty()) {
QDir datapacksDir(m_instance->gameRoot() + "/" + m_settings.datapacksPath);
datapacksDir.setFilter(QDir::Files);
datapacksDir.setNameFilters(QStringList() << "*.zip");
if (datapacksDir.exists()) {
QDirIterator datapacksIterator(datapacksDir);
while (datapacksIterator.hasNext()) {
filesToResolve << datapacksIterator.next();
}
}
}
m_netJob = new NetJob(tr("Modrinth pack export"), APPLICATION->network());
QList<HashLookupData> hashes;
qint64 progress = 0;
setProgress(progress, filesToResolve.length());
for (const QString &filePath: filesToResolve) {
qDebug() << "Attempting to resolve file hash from Modrinth API: " << filePath;
QFile file(filePath);
if (file.open(QFile::ReadOnly)) {
QByteArray contents = file.readAll();
QCryptographicHash hasher(QCryptographicHash::Sha512);
hasher.addData(contents);
QString hash = hasher.result().toHex();
hashes.append(HashLookupData {
QFileInfo(file),
hash
});
progress++;
setProgress(progress, filesToResolve.length());
}
}
m_response.reset(new QList<HashLookupResponseData>);
m_netJob->addNetAction(HashLookupRequest::make(hashes, m_response.get()));
connect(m_netJob.get(), &NetJob::succeeded, this, &InstanceExportTask::lookupSucceeded);
connect(m_netJob.get(), &NetJob::failed, this, &InstanceExportTask::lookupFailed);
connect(m_netJob.get(), &NetJob::progress, this, &InstanceExportTask::lookupProgress);
m_netJob->start();
setStatus(tr("Looking up files on Modrinth..."));
}
void InstanceExportTask::lookupSucceeded()
{
setStatus(tr("Creating modpack metadata..."));
QList<ExportFile> resolvedFiles;
QFileInfoList failedFiles;
for (const auto &file : *m_response) {
if (file.found) {
try {
auto url = Json::requireString(file.fileJson, "url");
auto hashes = Json::requireObject(file.fileJson, "hashes");
QString sha512Hash = Json::requireString(hashes, "sha512");
QString sha1Hash = Json::requireString(hashes, "sha1");
ExportFile fileData;
QDir gameDir(m_instance->gameRoot());
fileData.path = gameDir.relativeFilePath(file.fileInfo.absoluteFilePath());
fileData.download = url;
fileData.sha512 = sha512Hash;
fileData.sha1 = sha1Hash;
fileData.fileSize = file.fileInfo.size();
resolvedFiles << fileData;
} catch (const Json::JsonException &e) {
qDebug() << "File " << file.fileInfo.absoluteFilePath() << " failed to process for reason " << e.cause() << ", adding to overrides";
failedFiles << file.fileInfo;
}
} else {
failedFiles << file.fileInfo;
}
}
QJsonObject indexJson;
indexJson.insert("formatVersion", QJsonValue(1));
indexJson.insert("game", QJsonValue("minecraft"));
indexJson.insert("versionId", QJsonValue(m_settings.version));
indexJson.insert("name", QJsonValue(m_settings.name));
if (!m_settings.description.isEmpty()) {
indexJson.insert("summary", QJsonValue(m_settings.description));
}
QJsonArray files;
for (const auto &file : resolvedFiles) {
QJsonObject fileObj;
fileObj.insert("path", file.path);
QJsonObject hashes;
hashes.insert("sha512", file.sha512);
hashes.insert("sha1", file.sha1);
fileObj.insert("hashes", hashes);
QJsonArray downloads;
downloads.append(file.download);
fileObj.insert("downloads", downloads);
fileObj.insert("fileSize", QJsonValue(file.fileSize));
files.append(fileObj);
}
indexJson.insert("files", files);
QJsonObject dependencies;
dependencies.insert("minecraft", m_settings.gameVersion);
if (!m_settings.forgeVersion.isEmpty()) {
dependencies.insert("forge", m_settings.forgeVersion);
}
if (!m_settings.fabricVersion.isEmpty()) {
dependencies.insert("fabric-loader", m_settings.fabricVersion);
}
if (!m_settings.quiltVersion.isEmpty()) {
dependencies.insert("quilt-loader", m_settings.quiltVersion);
}
indexJson.insert("dependencies", dependencies);
setStatus(tr("Copying files to modpack..."));
QTemporaryDir tmp;
if (tmp.isValid()) {
Json::write(indexJson, tmp.path() + "/modrinth.index.json");
if (!failedFiles.isEmpty()) {
QDir tmpDir(tmp.path());
QDir gameDir(m_instance->gameRoot());
for (const auto &file : failedFiles) {
QString src = file.absoluteFilePath();
tmpDir.mkpath("overrides/" + gameDir.relativeFilePath(file.absolutePath()));
QString dest = tmpDir.path() + "/overrides/" + gameDir.relativeFilePath(src);
if (!QFile::copy(file.absoluteFilePath(), dest)) {
emitFailed(tr("Failed to copy file %1 to overrides").arg(src));
return;
}
}
if (m_settings.includeGameConfig) {
tmpDir.mkdir("overrides");
QFile::copy(gameDir.absoluteFilePath("options.txt"), tmpDir.absoluteFilePath("overrides/options.txt"));
}
if (m_settings.includeModConfigs) {
tmpDir.mkdir("overrides");
FS::copy copy(m_instance->gameRoot() + "/config", tmpDir.absoluteFilePath("overrides/config"));
copy();
}
}
setStatus(tr("Zipping modpack..."));
if (!JlCompress::compressDir(m_settings.exportPath, tmp.path())) {
emitFailed(tr("Failed to create zip file"));
return;
}
} else {
emitFailed(tr("Failed to create temporary directory"));
return;
}
qDebug() << "Successfully exported Modrinth pack to " << m_settings.exportPath;
emitSucceeded();
}
void InstanceExportTask::lookupFailed(const QString &reason)
{
emitFailed(reason);
}
void InstanceExportTask::lookupProgress(qint64 current, qint64 total)
{
setProgress(current, total);
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#pragma once
#include "tasks/Task.h"
#include "BaseInstance.h"
#include "net/NetJob.h"
#include "ui/dialogs/ModrinthExportDialog.h"
#include "ModrinthHashLookupRequest.h"
namespace Modrinth
{
struct ExportSettings
{
QString version;
QString name;
QString description;
bool includeGameConfig;
bool includeModConfigs;
bool includeResourcePacks;
bool includeShaderPacks;
QString datapacksPath;
QString gameVersion;
QString forgeVersion;
QString fabricVersion;
QString quiltVersion;
QString exportPath;
};
// Using the existing Modrinth::File struct from the importer doesn't actually make much sense here (doesn't support multiple hashes, hash is a byte array rather than a string, no file size, etc)
struct ExportFile
{
QString path;
QString sha512;
QString sha1;
QString download;
qint64 fileSize;
};
class InstanceExportTask : public Task
{
Q_OBJECT
public:
explicit InstanceExportTask(InstancePtr instance, ExportSettings settings);
protected:
//! Entry point for tasks.
virtual void executeTask() override;
private slots:
void lookupSucceeded();
void lookupFailed(const QString &reason);
void lookupProgress(qint64 current, qint64 total);
private:
InstancePtr m_instance;
ExportSettings m_settings;
std::shared_ptr<QList<HashLookupResponseData>> m_response;
NetJob::Ptr m_netJob;
};
}

View File

@ -1,4 +1,5 @@
/* Copyright 2013-2021 MultiMC Contributors
* Copyright 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,14 +21,18 @@
#include <QtConcurrentRun>
#include <MMCZip.h>
#include "TechnicPackProcessor.h"
#include "SolderPackManifest.h"
#include "net/ChecksumValidator.h"
Technic::SolderPackInstallTask::SolderPackInstallTask(
shared_qobject_ptr<QNetworkAccessManager> network,
const QUrl &sourceUrl,
const QString &version,
const QString &minecraftVersion
) {
m_sourceUrl = sourceUrl;
m_minecraftVersion = minecraftVersion;
m_version = version;
m_network = network;
}
@ -41,34 +46,12 @@ bool Technic::SolderPackInstallTask::abort() {
void Technic::SolderPackInstallTask::executeTask()
{
setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString()));
m_filesNetJob = new NetJob(tr("Finding recommended version"), m_network);
m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded);
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
m_filesNetJob->start();
}
setStatus(tr("Resolving modpack files"));
void Technic::SolderPackInstallTask::versionSucceeded()
{
try
{
QJsonDocument doc = Json::requireDocument(m_response);
QJsonObject obj = Json::requireObject(doc);
QString version = Json::requireString(obj, "recommended", "__placeholder__");
m_sourceUrl = m_sourceUrl.toString() + '/' + version;
}
catch (const JSONValidationError &e)
{
emitFailed(e.cause());
m_filesNetJob.reset();
return;
}
setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString()));
m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network);
m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
auto sourceUrl = QString("%1/%2").arg(m_sourceUrl.toString(), m_version);
m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response));
auto job = m_filesNetJob.get();
connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
@ -77,38 +60,47 @@ void Technic::SolderPackInstallTask::versionSucceeded()
void Technic::SolderPackInstallTask::fileListSucceeded()
{
setStatus(tr("Downloading modpack:"));
QStringList modUrls;
try
{
QJsonDocument doc = Json::requireDocument(m_response);
QJsonObject obj = Json::requireObject(doc);
QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
if (!minecraftVersion.isEmpty())
m_minecraftVersion = minecraftVersion;
QJsonArray mods = Json::requireArray(obj, "mods", "'mods'");
for (auto mod: mods)
{
QJsonObject modObject = Json::requireValueObject(mod);
modUrls.append(Json::requireString(modObject, "url", "'url'"));
}
setStatus(tr("Downloading modpack"));
QJsonParseError parse_error {};
QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << m_response;
return;
}
catch (const JSONValidationError &e)
{
emitFailed(e.cause());
auto obj = doc.object();
TechnicSolder::PackBuild build;
try {
TechnicSolder::loadPackBuild(build, obj);
}
catch (const JSONValidationError& e) {
emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
m_filesNetJob.reset();
return;
}
if (!build.minecraft.isEmpty())
m_minecraftVersion = build.minecraft;
m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network);
int i = 0;
for (auto &modUrl: modUrls)
for (const auto &mod : build.mods)
{
auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path));
auto dl = Net::Download::makeFile(mod.url, path);
if (!mod.md5.isEmpty()) {
auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
}
m_filesNetJob->addNetAction(dl);
i++;
}
m_modCount = modUrls.size();
m_modCount = build.mods.size();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
@ -206,6 +198,4 @@ void Technic::SolderPackInstallTask::extractFinished()
void Technic::SolderPackInstallTask::extractAborted()
{
emitFailed(tr("Instance import has been aborted."));
return;
}

View File

@ -1,4 +1,5 @@
/* Copyright 2013-2021 MultiMC Contributors
* Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -27,7 +28,7 @@ namespace Technic
{
Q_OBJECT
public:
explicit SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, const QUrl &sourceUrl, const QString &minecraftVersion);
explicit SolderPackInstallTask(shared_qobject_ptr<QNetworkAccessManager> network, const QUrl &sourceUrl, const QString& version, const QString &minecraftVersion);
bool canAbort() const override { return true; }
bool abort() override;
@ -37,7 +38,6 @@ namespace Technic
virtual void executeTask() override;
private slots:
void versionSucceeded();
void fileListSucceeded();
void downloadSucceeded();
void downloadFailed(QString reason);
@ -52,6 +52,7 @@ namespace Technic
NetJob::Ptr m_filesNetJob;
QUrl m_sourceUrl;
QString m_version;
QString m_minecraftVersion;
QByteArray m_response;
QTemporaryDir m_outputDir;

View File

@ -0,0 +1,56 @@
/*
* Copyright 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SolderPackManifest.h"
#include "Json.h"
namespace TechnicSolder {
void loadPack(Pack& v, QJsonObject& obj)
{
v.recommended = Json::requireString(obj, "recommended");
v.latest = Json::requireString(obj, "latest");
auto builds = Json::requireArray(obj, "builds");
for (const auto buildRaw : builds) {
auto build = Json::requireValueString(buildRaw);
v.builds.append(build);
}
}
static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj)
{
b.name = Json::requireString(obj, "name");
b.version = Json::requireString(obj, "version");
b.md5 = Json::requireString(obj, "md5");
b.url = Json::requireString(obj, "url");
}
void loadPackBuild(PackBuild& v, QJsonObject& obj)
{
v.minecraft = Json::requireString(obj, "minecraft");
auto mods = Json::requireArray(obj, "mods");
for (const auto modRaw : mods) {
auto modObj = Json::requireValueObject(modRaw);
PackBuildMod mod;
loadPackBuildMod(mod, modObj);
v.mods.append(mod);
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QString>
#include <QVector>
#include <QJsonObject>
namespace TechnicSolder {
struct Pack {
QString recommended;
QString latest;
QVector<QString> builds;
};
void loadPack(Pack& v, QJsonObject& obj);
struct PackBuildMod {
QString name;
QString version;
QString md5;
QString url;
};
struct PackBuild {
QString minecraft;
QVector<PackBuildMod> mods;
};
void loadPackBuild(PackBuild& v, QJsonObject& obj);
}

View File

@ -1,4 +1,4 @@
/* Copyright 2013-2021 MultiMC Contributors
/* Copyright 2013-2023 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -122,6 +122,13 @@ void Download::downloadError(QNetworkReply::NetworkError error)
qCritical() << "Aborted " << m_url.toString();
m_status = Job_Aborted;
}
else if(error == QNetworkReply::ContentNotFoundError && (m_options & Option::AllowNotFound))
{
// The Modrinth API returns a 404 when a hash was not found when performing reverse hash lookup, we don't want to treat this as a failure
qDebug() << "Received 404 from " << m_url.toString() << ", continuing...";
m_status = Job_Finished;
return;
}
else
{
if(m_options & Option::AcceptLocalFiles)

View File

@ -1,4 +1,4 @@
/* Copyright 2013-2021 MultiMC Contributors
/* Copyright 2013-2023 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,7 +32,8 @@ public: /* types */
enum class Option
{
NoOptions = 0,
AcceptLocalFiles = 1
AcceptLocalFiles = 1,
AllowNotFound = 2
};
Q_DECLARE_FLAGS(Options, Option)

View File

@ -10,7 +10,7 @@
#include "Application.h"
NotificationChecker::NotificationChecker(QObject *parent)
: QObject(parent)
: QObject(parent), m_appVersionChannel("develop")
{
}
@ -19,11 +19,6 @@ void NotificationChecker::setNotificationsUrl(const QUrl &notificationsUrl)
m_notificationsUrl = notificationsUrl;
}
void NotificationChecker::setApplicationChannel(QString channel)
{
m_appVersionChannel = channel;
}
void NotificationChecker::setApplicationFullVersion(QString version)
{
m_appFullVersion = version;

View File

@ -14,7 +14,6 @@ public:
void setNotificationsUrl(const QUrl &notificationsUrl);
void setApplicationPlatform(QString platform);
void setApplicationChannel(QString channel);
void setApplicationFullVersion(QString version);
struct NotificationEntry

View File

@ -192,7 +192,6 @@ void readIndex(const QString & path, QMap<QString, Language>& languages)
return;
}
int index = 1;
try
{
auto toplevel_doc = Json::requireDocument(data);
@ -225,7 +224,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)

View File

@ -1,4 +1,4 @@
/* Copyright 2013-2021 MultiMC Contributors
/* Copyright 2013-2023 MultiMC Contributors
*
* Authors: Andrew Okin
* Peterix
@ -85,6 +85,7 @@
#include "ui/dialogs/NotificationDialog.h"
#include "ui/dialogs/CreateShortcutDialog.h"
#include "ui/dialogs/ExportInstanceDialog.h"
#include "ui/dialogs/ModrinthExportDialog.h"
#include "UpdateController.h"
#include "KonamiCode.h"
@ -849,14 +850,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow
// if automatic update checks are allowed, start one.
if (APPLICATION->settings()->get("AutoUpdate").toBool() && updatesAllowed)
{
updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), false);
updater->checkForUpdate(false);
}
}
{
auto checker = new NotificationChecker();
checker->setNotificationsUrl(QUrl(BuildConfig.NOTIFICATION_URL));
checker->setApplicationChannel(BuildConfig.VERSION_CHANNEL);
checker->setApplicationPlatform(BuildConfig.BUILD_PLATFORM);
checker->setApplicationFullVersion(BuildConfig.FULL_VERSION_STR);
m_notificationChecker.reset(checker);
@ -975,6 +975,33 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
void MainWindow::updateToolsMenu()
{
QToolButton *exportButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionExportInstance));
exportButton->setPopupMode(QToolButton::MenuButtonPopup);
QMenu *exportMenu = ui->actionExportInstance->menu();
if (exportMenu) {
exportMenu->clear();
} else {
exportMenu = new QMenu();
}
exportMenu->addSeparator()->setText(tr("Format"));
QAction *mmcExport = exportMenu->addAction(BuildConfig.LAUNCHER_NAME);
QAction *modrinthExport = exportMenu->addAction(tr("Modrinth (WIP)"));
connect(mmcExport, &QAction::triggered, this, &MainWindow::on_actionExportInstance_triggered);
connect(modrinthExport, &QAction::triggered, [this]()
{
if (m_selectedInstance) {
ModrinthExportDialog dlg(m_selectedInstance, this);
dlg.exec();
}
});
ui->actionExportInstance->setMenu(exportMenu);
QToolButton *launchButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance));
QToolButton *launchOfflineButton = dynamic_cast<QToolButton*>(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstanceOffline));
@ -1639,7 +1666,7 @@ void MainWindow::checkForUpdates()
if(BuildConfig.UPDATER_ENABLED)
{
auto updater = APPLICATION->updateChecker();
updater->checkForUpdate(APPLICATION->settings()->get("UpdateChannel").toString(), true);
updater->checkForUpdate(true);
}
else
{
@ -2006,6 +2033,10 @@ void MainWindow::updateStatusCenter()
int timePlayed = APPLICATION->instances()->getTotalPlayTime();
if (timePlayed > 0) {
m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed)));
if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) {
m_statusCenter->setText(tr("Total playtime: %1 hours").arg(Time::prettifyDurationHours(timePlayed)));
} else {
m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed)));
}
}
}

View File

@ -60,9 +60,12 @@ CreateShortcutDialog::~CreateShortcutDialog()
void CreateShortcutDialog::on_shortcutPathBrowse_clicked()
{
QString linkExtension;
#ifdef Q_OS_UNIX
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
linkExtension = ui->createScriptCheckBox->isChecked() ? "sh" : "desktop";
#endif
#ifdef Q_OS_MAC
linkExtension = "command";
#endif
#ifdef Q_OS_WIN
linkExtension = ui->createScriptCheckBox->isChecked() ? "bat" : "lnk";
#endif
@ -104,20 +107,20 @@ void CreateShortcutDialog::updateDialogState()
}
}
QString CreateShortcutDialog::getLaunchCommand()
QString CreateShortcutDialog::getLaunchCommand(bool escapeQuotesTwice)
{
return "\"" + QDir::toNativeSeparators(QCoreApplication::applicationFilePath()) + "\""
+ getLaunchArgs();
return "\"" + QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).replace('"', escapeQuotesTwice ? "\\\\\"" : "\\\"") + "\""
+ getLaunchArgs(escapeQuotesTwice);
}
QString CreateShortcutDialog::getLaunchArgs()
QString CreateShortcutDialog::getLaunchArgs(bool escapeQuotesTwice)
{
return " -d \"" + QDir::toNativeSeparators(QDir::currentPath()) + "\""
+ " -l " + m_instance->id()
+ (ui->joinServerCheckBox->isChecked() ? " -s " + ui->joinServer->text() : "")
+ (ui->useProfileCheckBox->isChecked() ? " -a " + ui->profileComboBox->currentText() : "")
return " -d \"" + QDir::toNativeSeparators(QDir::currentPath()).replace('"', escapeQuotesTwice ? "\\\\\"" : "\\\"") + "\""
+ " -l \"" + m_instance->id() + "\""
+ (ui->joinServerCheckBox->isChecked() ? " -s \"" + ui->joinServer->text() + "\"" : "")
+ (ui->useProfileCheckBox->isChecked() ? " -a \"" + ui->profileComboBox->currentText() + "\"" : "")
+ (ui->launchOfflineCheckBox->isChecked() ? " -o" : "")
+ (ui->offlineUsernameCheckBox->isChecked() ? " -n " + ui->offlineUsername->text() : "");
+ (ui->offlineUsernameCheckBox->isChecked() ? " -n \"" + ui->offlineUsername->text() + "\"" : "");
}
void CreateShortcutDialog::createShortcut()
@ -134,7 +137,7 @@ void CreateShortcutDialog::createShortcut()
{
shortcutText = "#!/bin/sh\n"
// FIXME: is there a way to use the launcher script instead of the raw binary here?
"cd \"" + QDir::currentPath() + "\"\n"
"cd \"" + QDir::currentPath().replace('"', "\\\"") + "\"\n"
+ getLaunchCommand() + " &\n";
} else
// freedesktop.org desktop entry
@ -149,7 +152,7 @@ void CreateShortcutDialog::createShortcut()
shortcutText = "[Desktop Entry]\n"
"Type=Application\n"
"Name=" + m_instance->name() + " - " + BuildConfig.LAUNCHER_DISPLAYNAME + "\n"
+ "Exec=" + getLaunchCommand() + "\n"
+ "Exec=" + getLaunchCommand(true) + "\n"
+ "Path=" + QDir::currentPath() + "\n"
+ "Icon=" + QDir::currentPath() + "/icons/shortcut-icon.png\n";
@ -159,7 +162,7 @@ void CreateShortcutDialog::createShortcut()
// Windows batch script implementation
shortcutText = "@ECHO OFF\r\n"
"CD \"" + QDir::toNativeSeparators(QDir::currentPath()) + "\"\r\n"
"START /B " + getLaunchCommand() + "\r\n";
"START /B \"\" " + getLaunchCommand() + "\r\n";
#endif
QFile shortcutFile(ui->shortcutPath->text());
if (shortcutFile.open(QIODevice::WriteOnly))

View File

@ -39,8 +39,8 @@ private:
Ui::CreateShortcutDialog *ui;
InstancePtr m_instance;
QString getLaunchCommand();
QString getLaunchArgs();
QString getLaunchCommand(bool escapeQuotesTwice = false);
QString getLaunchArgs(bool escapeQuotesTwice = false);
void createShortcut();

View File

@ -1,4 +1,4 @@
/* Copyright 2013-2021 MultiMC Contributors
/* Copyright 2013-2022 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@
#include <QtWidgets/QPushButton>
#include <QUrl>
#include <QClipboard>
MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog)
{
@ -34,6 +35,7 @@ MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MS
int MSALoginDialog::exec() {
setUserInputsEnabled(false);
ui->progressBar->setVisible(true);
ui->copyCodeButton->setVisible(false);
// Setup the login task and start it
m_account = MinecraftAccount::createBlankMSA();
@ -68,6 +70,8 @@ void MSALoginDialog::externalLoginTick() {
void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) {
ui->copyCodeButton->setVisible(true);
m_externalLoginElapsed = 0;
m_externalLoginTimeout = expiresIn;
@ -81,9 +85,12 @@ void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString&
QString urlString = uri.toString();
QString linkString = QString("<a href=\"%1\">%2</a>").arg(urlString, urlString);
ui->label->setText(tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code));
m_code = code;
}
void MSALoginDialog::hideVerificationUriAndCode() {
ui->copyCodeButton->setVisible(false);
m_externalLoginTimer.stop();
}
@ -139,3 +146,8 @@ MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg)
}
return 0;
}
void MSALoginDialog::on_copyCodeButton_clicked()
{
QApplication::clipboard()->setText(m_code);
}

View File

@ -49,6 +49,7 @@ slots:
void onTaskProgress(qint64 current, qint64 total);
void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn);
void hideVerificationUriAndCode();
void on_copyCodeButton_clicked();
void externalLoginTick();
@ -57,6 +58,7 @@ private:
MinecraftAccountPtr m_account;
shared_qobject_ptr<AccountTask> m_loginTask;
QTimer m_externalLoginTimer;
QString m_code;
int m_externalLoginElapsed = 0;
int m_externalLoginTimeout = 0;
};

View File

@ -49,14 +49,25 @@ aaaaa</string>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="copyCodeButton">
<property name="text">
<string>Copy Code</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>

View File

@ -0,0 +1,142 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#include <QFileDialog>
#include <QStandardPaths>
#include <QProgressDialog>
#include <QMessageBox>
#include "ModrinthExportDialog.h"
#include "ui_ModrinthExportDialog.h"
#include "BaseInstance.h"
#include "modplatform/modrinth/ModrinthInstanceExportTask.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "ProgressDialog.h"
#include "CustomMessageBox.h"
ModrinthExportDialog::ModrinthExportDialog(InstancePtr instance, QWidget *parent) :
QDialog(parent), ui(new Ui::ModrinthExportDialog), m_instance(instance)
{
ui->setupUi(this);
ui->name->setText(m_instance->name());
ui->version->setText("1.0");
}
void ModrinthExportDialog::updateDialogState()
{
ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled(
!ui->name->text().isEmpty()
&& !ui->version->text().isEmpty()
&& ui->file->text().endsWith(".mrpack")
&& (
!ui->includeDatapacks->isChecked()
|| (!ui->datapacksPath->text().isEmpty() && QDir(m_instance->gameRoot() + "/" + ui->datapacksPath->text()).exists())
)
);
}
void ModrinthExportDialog::on_fileBrowseButton_clicked()
{
QFileDialog dialog(this, tr("Select modpack file"), QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
dialog.setDefaultSuffix("mrpack");
dialog.setNameFilter("Modrinth modpacks (*.mrpack)");
dialog.setAcceptMode(QFileDialog::AcceptSave);
dialog.setFileMode(QFileDialog::AnyFile);
dialog.selectFile(ui->name->text() + ".mrpack");
if (dialog.exec()) {
ui->file->setText(dialog.selectedFiles().at(0));
}
updateDialogState();
}
void ModrinthExportDialog::on_datapackPathBrowse_clicked()
{
QFileDialog dialog(this, tr("Select global datapacks folder"), m_instance->gameRoot());
dialog.setAcceptMode(QFileDialog::AcceptOpen);
dialog.setFileMode(QFileDialog::DirectoryOnly);
if (dialog.exec()) {
ui->datapacksPath->setText(QDir(m_instance->gameRoot()).relativeFilePath(dialog.selectedFiles().at(0)));
}
updateDialogState();
}
void ModrinthExportDialog::accept()
{
Modrinth::ExportSettings settings;
settings.name = ui->name->text();
settings.version = ui->version->text();
settings.description = ui->description->text();
settings.includeGameConfig = ui->includeGameConfig->isChecked();
settings.includeModConfigs = ui->includeModConfigs->isChecked();
settings.includeResourcePacks = ui->includeResourcePacks->isChecked();
settings.includeShaderPacks = ui->includeShaderPacks->isChecked();
if (ui->includeDatapacks->isChecked()) {
settings.datapacksPath = ui->datapacksPath->text();
}
MinecraftInstancePtr minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(m_instance);
minecraftInstance->getPackProfile()->reload(Net::Mode::Offline);
for (int i = 0; i < minecraftInstance->getPackProfile()->rowCount(); i++) {
auto component = minecraftInstance->getPackProfile()->getComponent(i);
if (component->isCustom()) {
CustomMessageBox::selectable(
this,
tr("Warning"),
tr("Instance contains a custom component: %1\nThis cannot be exported to a Modrinth pack; the exported pack may not work correctly!")
.arg(component->getName()),
QMessageBox::Warning
)->exec();
}
}
settings.gameVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.minecraft");
settings.forgeVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.minecraftforge");
settings.fabricVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader");
settings.quiltVersion = minecraftInstance->getPackProfile()->getComponentVersion("org.quiltmc.quilt-loader");
settings.exportPath = ui->file->text();
auto *task = new Modrinth::InstanceExportTask(m_instance, settings);
connect(task, &Task::failed, [this](QString reason)
{
QString text;
if (reason.length() > 1000) {
text = reason.left(1000) + "...";
} else {
text = reason;
}
CustomMessageBox::selectable(parentWidget(), tr("Error"), text, QMessageBox::Critical)->show();
});
connect(task, &Task::succeeded, [this, task]()
{
QStringList warnings = task->warnings();
if(warnings.count())
{
CustomMessageBox::selectable(parentWidget(), tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
}
});
ProgressDialog loadDialog(this);
loadDialog.setSkipButton(true, tr("Abort"));
loadDialog.execWithTask(task);
QDialog::accept();
}
ModrinthExportDialog::~ModrinthExportDialog()
{
delete ui;
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2023 arthomnix
*
* This source is subject to the Microsoft Public License (MS-PL).
* Please see the COPYING.md file for more information.
*/
#pragma once
#include <QDialog>
#include "ExportInstanceDialog.h"
QT_BEGIN_NAMESPACE
namespace Ui
{
class ModrinthExportDialog;
}
QT_END_NAMESPACE
class ModrinthExportDialog : public QDialog
{
Q_OBJECT
public:
explicit ModrinthExportDialog(InstancePtr instance, QWidget *parent = nullptr);
~ModrinthExportDialog() override;
private slots:
void on_fileBrowseButton_clicked();
void on_datapackPathBrowse_clicked();
void accept() override;
void updateDialogState();
private:
Ui::ModrinthExportDialog *ui;
InstancePtr m_instance;
};

View File

@ -0,0 +1,333 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ModrinthExportDialog</class>
<widget class="QDialog" name="ModrinthExportDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>835</width>
<height>559</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>ModrinthExportDialog</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>821</width>
<height>541</height>
</rect>
</property>
<layout class="QVBoxLayout" name="mainLayout">
<item>
<widget class="QLabel" name="label">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>Export Modrinth modpack</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="metadataGroupBox">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>200</height>
</size>
</property>
<property name="title">
<string>Metadata</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>801</width>
<height>151</height>
</rect>
</property>
<layout class="QVBoxLayout" name="metadataVLayout">
<item>
<layout class="QFormLayout" name="metadataFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="versionLabel">
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="descriptionLabel">
<property name="text">
<string>Description</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="version"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="description"/>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QGroupBox" name="optionsGroupBox">
<property name="title">
<string>Export Options</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget_3">
<property name="geometry">
<rect>
<x>9</x>
<y>29</y>
<width>801</width>
<height>221</height>
</rect>
</property>
<layout class="QVBoxLayout" name="optionsLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<widget class="QLabel" name="fileLabel">
<property name="text">
<string>File</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="file"/>
</item>
<item>
<widget class="QPushButton" name="fileBrowseButton">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="includeGameConfig">
<property name="text">
<string>Include Minecraft config</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="includeModConfigs">
<property name="text">
<string>Include mod configs</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="includeResourcePacks">
<property name="text">
<string>Include resource packs</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="includeShaderPacks">
<property name="text">
<string>Include shader packs</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="includeDatapacks">
<property name="toolTip">
<string>Use this if your modpack contains a mod which adds global datapacks.</string>
</property>
<property name="text">
<string>Include global datapacks folder:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="datapacksPath"/>
</item>
<item>
<widget class="QPushButton" name="datapackPathBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>340</x>
<y>532</y>
</hint>
<hint type="destinationlabel">
<x>338</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>340</x>
<y>532</y>
</hint>
<hint type="destinationlabel">
<x>338</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>name</sender>
<signal>textChanged(QString)</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>updateDialogState()</slot>
<hints>
<hint type="sourcelabel">
<x>395</x>
<y>90</y>
</hint>
<hint type="destinationlabel">
<x>339</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>version</sender>
<signal>textChanged(QString)</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>updateDialogState()</slot>
<hints>
<hint type="sourcelabel">
<x>395</x>
<y>129</y>
</hint>
<hint type="destinationlabel">
<x>339</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>file</sender>
<signal>textChanged(QString)</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>updateDialogState()</slot>
<hints>
<hint type="sourcelabel">
<x>309</x>
<y>329</y>
</hint>
<hint type="destinationlabel">
<x>339</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>datapacksPath</sender>
<signal>textChanged(QString)</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>updateDialogState()</slot>
<hints>
<hint type="sourcelabel">
<x>532</x>
<y>472</y>
</hint>
<hint type="destinationlabel">
<x>417</x>
<y>279</y>
</hint>
</hints>
</connection>
<connection>
<sender>includeDatapacks</sender>
<signal>stateChanged(int)</signal>
<receiver>ModrinthExportDialog</receiver>
<slot>updateDialogState()</slot>
<hints>
<hint type="sourcelabel">
<x>183</x>
<y>472</y>
</hint>
<hint type="destinationlabel">
<x>417</x>
<y>279</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>updateDialogState()</slot>
</slots>
</ui>

View File

@ -11,14 +11,13 @@
UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog)
{
ui->setupUi(this);
auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
if(hasUpdate)
{
ui->label->setText(tr("A new %1 update is available!").arg(channel));
ui->label->setText(tr("A new update is available!"));
}
else
{
ui->label->setText(tr("No %1 updates found. You are running the latest version.").arg(channel));
ui->label->setText(tr("No updates found. You are running the latest version."));
ui->btnUpdateNow->setHidden(true);
ui->btnUpdateLater->setText(tr("Close"));
}
@ -33,19 +32,10 @@ UpdateDialog::~UpdateDialog()
void UpdateDialog::loadChangelog()
{
auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
dljob = new NetJob("Changelog", APPLICATION->network());
QString url;
if(channel == "stable")
{
url = QString("https://raw.githubusercontent.com/MultiMC/Launcher/%1/changelog.md").arg(channel);
m_changelogType = CHANGELOG_MARKDOWN;
}
else
{
url = QString("https://api.github.com/repos/MultiMC/Launcher/compare/%1...%2").arg(BuildConfig.GIT_COMMIT, channel);
m_changelogType = CHANGELOG_COMMITS;
}
url = QString("https://api.github.com/repos/MultiMC/Launcher/compare/%1...develop").arg(BuildConfig.GIT_COMMIT);
m_changelogType = CHANGELOG_COMMITS;
dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData));
connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded);
connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed);
@ -65,7 +55,6 @@ QString reprocessMarkdown(QByteArray markdown)
QString reprocessCommits(QByteArray json)
{
auto channel = APPLICATION->settings()->get("UpdateChannel").toString();
try
{
QString result;
@ -119,7 +108,7 @@ QString reprocessCommits(QByteArray json)
if(status == "identical")
{
return QObject::tr("<p>There are no code changes between your current version and latest %1.</p>").arg(channel);
return QObject::tr("<p>There are no code changes between your current version and the latest.</p>");
}
else if(status == "ahead")
{

View File

@ -56,23 +56,12 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch
m_languageModel = APPLICATION->translations();
loadSettings();
if(BuildConfig.UPDATER_ENABLED)
{
QObject::connect(APPLICATION->updateChecker().get(), &UpdateChecker::channelListLoaded, this, &LauncherPage::refreshUpdateChannelList);
if (APPLICATION->updateChecker()->hasChannels())
{
refreshUpdateChannelList();
}
else
{
APPLICATION->updateChecker()->updateChanList(false);
}
}
else
// Updater
if(!BuildConfig.UPDATER_ENABLED)
{
ui->updateSettingsBox->setHidden(true);
}
// Analytics
if(BuildConfig.ANALYTICS_ID.isEmpty())
{
@ -163,78 +152,6 @@ void LauncherPage::on_migrateDataFolderMacBtn_clicked()
qApp->quit();
}
void LauncherPage::refreshUpdateChannelList()
{
// Stop listening for selection changes. It's going to change a lot while we update it and
// we don't need to update the
// description label constantly.
QObject::disconnect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
SLOT(updateChannelSelectionChanged(int)));
QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList();
ui->updateChannelComboBox->clear();
int selection = -1;
for (int i = 0; i < channelList.count(); i++)
{
UpdateChecker::ChannelListEntry entry = channelList.at(i);
// When it comes to selection, we'll rely on the indexes of a channel entry being the
// same in the
// combo box as it is in the update checker's channel list.
// This probably isn't very safe, but the channel list doesn't change often enough (or
// at all) for
// this to be a big deal. Hope it doesn't break...
ui->updateChannelComboBox->addItem(entry.name);
// If the update channel we just added was the selected one, set the current index in
// the combo box to it.
if (entry.id == m_currentUpdateChannel)
{
qDebug() << "Selected index" << i << "channel id" << m_currentUpdateChannel;
selection = i;
}
}
ui->updateChannelComboBox->setCurrentIndex(selection);
// Start listening for selection changes again and update the description label.
QObject::connect(ui->updateChannelComboBox, SIGNAL(currentIndexChanged(int)), this,
SLOT(updateChannelSelectionChanged(int)));
refreshUpdateChannelDesc();
// Now that we've updated the channel list, we can enable the combo box.
// It starts off disabled so that if the channel list hasn't been loaded, it will be
// disabled.
ui->updateChannelComboBox->setEnabled(true);
}
void LauncherPage::updateChannelSelectionChanged(int index)
{
refreshUpdateChannelDesc();
}
void LauncherPage::refreshUpdateChannelDesc()
{
// Get the channel list.
QList<UpdateChecker::ChannelListEntry> channelList = APPLICATION->updateChecker()->getChannelList();
int selectedIndex = ui->updateChannelComboBox->currentIndex();
if (selectedIndex < 0)
{
return;
}
if (selectedIndex < channelList.count())
{
// Find the channel list entry with the given index.
UpdateChecker::ChannelListEntry selected = channelList.at(selectedIndex);
// Set the description text.
ui->updateChannelDescLabel->setText(selected.description);
// Set the currently selected channel ID.
m_currentUpdateChannel = selected.id;
}
}
void LauncherPage::applySettings()
{
auto s = APPLICATION->settings();
@ -246,7 +163,6 @@ void LauncherPage::applySettings()
// Updates
s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked());
s->set("UpdateChannel", m_currentUpdateChannel);
auto original = s->get("IconTheme").toString();
//FIXME: make generic
switch (ui->themeComboBox->currentIndex())
@ -333,7 +249,6 @@ void LauncherPage::loadSettings()
auto s = APPLICATION->settings();
// Updates
ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool());
m_currentUpdateChannel = s->get("UpdateChannel").toString();
//FIXME: make generic
auto theme = s->get("IconTheme").toString();
if (theme == "pe_dark")

View File

@ -69,31 +69,14 @@ slots:
void on_iconsDirBrowseBtn_clicked();
void on_migrateDataFolderMacBtn_clicked();
/*!
* Updates the list of update channels in the combo box.
*/
void refreshUpdateChannelList();
/*!
* Updates the channel description label.
*/
void refreshUpdateChannelDesc();
/*!
* Updates the font preview
*/
void refreshFontPreview();
void updateChannelSelectionChanged(int index);
private:
Ui::LauncherPage *ui;
/*!
* Stores the currently selected update channel.
*/
QString m_currentUpdateChannel;
// default format for the font preview...
QTextCharFormat *defaultFormat;

View File

@ -58,33 +58,6 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="updateChannelLabel">
<property name="text">
<string>Up&amp;date Channel:</string>
</property>
<property name="buddy">
<cstring>updateChannelComboBox</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="updateChannelComboBox">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="updateChannelDescLabel">
<property name="text">
<string>No channel selected.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -555,7 +528,6 @@
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>autoUpdateCheckBox</tabstop>
<tabstop>updateChannelComboBox</tabstop>
<tabstop>instDirTextBox</tabstop>
<tabstop>instDirBrowseBtn</tabstop>
<tabstop>modsDirTextBox</tabstop>

View File

@ -1,4 +1,4 @@
/* Copyright 2013-2021 MultiMC Contributors
/* Copyright 2013-2022 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -71,6 +71,7 @@ void MinecraftPage::applySettings()
s->set("ShowGameTime", ui->showGameTime->isChecked());
s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked());
s->set("RecordGameTime", ui->recordGameTime->isChecked());
s->set("ShowGameTimeHours", ui->showGameTimeHours->isChecked());
}
void MinecraftPage::loadSettings()
@ -88,4 +89,5 @@ void MinecraftPage::loadSettings()
ui->showGameTime->setChecked(s->get("ShowGameTime").toBool());
ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool());
ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
ui->showGameTimeHours->setChecked(s->get("ShowGameTimeHours").toBool());
}

View File

@ -161,6 +161,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showGameTimeHours">
<property name="text">
<string>Show time spent playing in hours only</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -314,6 +314,20 @@ void ScreenshotsPage::on_actionUpload_triggered()
if (selection.isEmpty())
return;
auto uploadText = tr("Upload screenshot to imgur.com?");
if (selection.size() > 1)
uploadText = tr("Upload %1 screenshots to imgur.com?").arg(selection.size());
auto response = CustomMessageBox::selectable(
this,
tr("Upload?"),
uploadText,
QMessageBox::Question,
QMessageBox::Yes | QMessageBox::No
)->exec();
if (response == QMessageBox::No)
return;
QList<ScreenShot::Ptr> uploaded;
auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network()));
if(selection.size() < 2)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,8 +17,10 @@
#include "AtlOptionalModDialog.h"
#include "ui_AtlOptionalModDialog.h"
AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
: QAbstractListModel(parent), m_mods(mods) {
#include <QMessageBox>
AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
: QAbstractListModel(parent), m_version(version), m_mods(mods) {
// fill mod index
for (int i = 0; i < m_mods.size(); i++) {
@ -71,6 +73,11 @@ QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const
return mod.description;
}
}
else if (role == Qt::ForegroundRole) {
if (!mod.colour.isEmpty() && m_version.colours.contains(mod.colour)) {
return QColor(QString("#%1").arg(m_version.colours[mod.colour]));
}
}
else if (role == Qt::CheckStateRole) {
if (index.column() == EnabledColumn) {
return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked;
@ -134,7 +141,21 @@ void AtlOptionalModListModel::clearAll() {
}
void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) {
setMod(mod, index, !m_selection[mod.name]);
auto enable = !m_selection[mod.name];
// If there is a warning for the mod, display that first (if we would be enabling the mod)
if (enable && !mod.warning.isEmpty() && m_version.warnings.contains(mod.warning)) {
auto message = QString("%1<br><br>%2")
.arg(m_version.warnings[mod.warning], tr("Are you sure that you want to enable this mod?"));
// fixme: avoid casting here
auto result = QMessageBox::warning((QWidget*) this->parent(), tr("Warning"), message, QMessageBox::Yes | QMessageBox::No);
if (result != QMessageBox::Yes) {
return;
}
}
setMod(mod, index, enable);
}
void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) {
@ -199,11 +220,11 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool
}
AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods)
AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods)
: QDialog(parent), ui(new Ui::AtlOptionalModDialog) {
ui->setupUi(this);
listModel = new AtlOptionalModListModel(this, mods);
listModel = new AtlOptionalModListModel(this, version, mods);
ui->treeView->setModel(listModel);
ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -36,7 +36,7 @@ public:
DescriptionColumn,
};
AtlOptionalModListModel(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods);
QVector<QString> getResult();
@ -58,7 +58,9 @@ private:
void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true);
private:
ATLauncher::PackVersion m_version;
QVector<ATLauncher::VersionMod> m_mods;
QMap<QString, bool> m_selection;
QMap<QString, int> m_index;
QMap<QString, QVector<QString>> m_dependants;
@ -68,7 +70,7 @@ class AtlOptionalModDialog : public QDialog {
Q_OBJECT
public:
AtlOptionalModDialog(QWidget *parent, QVector<ATLauncher::VersionMod> mods);
AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods);
~AtlOptionalModDialog() override;
QVector<QString> getResult() {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020-2022 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2021 Philip T <me@phit.link>
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -26,6 +26,8 @@
#include <BuildConfig.h>
#include <QMessageBox>
AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), ui(new Ui::AtlPage), dialog(dialog)
{
@ -89,7 +91,7 @@ void AtlPage::suggestCurrent()
return;
}
dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.safeName, selectedVersion));
dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.name, selectedVersion));
auto editedLogoName = selected.safeName;
auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower());
listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo)
@ -145,8 +147,8 @@ void AtlPage::onVersionSelectionChanged(QString data)
suggestCurrent();
}
QVector<QString> AtlPage::chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) {
AtlOptionalModDialog optionalModDialog(this, mods);
QVector<QString> AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) {
AtlOptionalModDialog optionalModDialog(this, version, mods);
optionalModDialog.exec();
return optionalModDialog.getResult();
}
@ -186,3 +188,8 @@ QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVers
vselect.exec();
return vselect.selectedVersion()->descriptor();
}
void AtlPage::displayMessage(QString message)
{
QMessageBox::information(this, tr("Installing"), message);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright 2020-2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -64,7 +64,8 @@ private:
void suggestCurrent();
QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override;
QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) override;
QVector<QString> chooseOptionalMods(ATLauncher::PackVersion version, QVector<ATLauncher::VersionMod> mods) override;
void displayMessage(QString message) override;
private slots:
void triggerSearch();

View File

@ -1,4 +1,5 @@
/* Copyright 2020-2021 MultiMC Contributors
* Copyright 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,6 +18,7 @@
#include <QList>
#include <QString>
#include <QVector>
namespace Technic {
struct Modpack {
@ -36,6 +38,11 @@ struct Modpack {
QString websiteUrl;
QString author;
QString description;
QString currentVersion;
bool versionsLoaded = false;
QString recommended;
QVector<QString> versions;
};
}

View File

@ -1,4 +1,5 @@
/* Copyright 2020-2021 MultiMC Contributors
* Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,6 +16,7 @@
#include "TechnicModel.h"
#include "Application.h"
#include "BuildConfig.h"
#include "Json.h"
#include <QIcon>
@ -94,13 +96,24 @@ void Technic::ListModel::performSearch()
NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network());
QString searchUrl = "";
if (currentSearchTerm.isEmpty()) {
searchUrl = "https://api.technicpack.net/trending?build=multimc";
searchUrl = QString("%1trending?build=%2")
.arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD);
searchMode = List;
}
else
{
else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) {
searchUrl = QString("https://%1?build=%2")
.arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD);
searchMode = Single;
}
else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) {
searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD);
searchMode = Single;
}
else {
searchUrl = QString(
"https://api.technicpack.net/search?build=multimc&q=%1"
).arg(currentSearchTerm);
"%1search?build=%2&q=%3"
).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm);
searchMode = List;
}
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
@ -125,26 +138,58 @@ void Technic::ListModel::searchRequestFinished()
QList<Modpack> newList;
try {
auto root = Json::requireObject(doc);
auto objs = Json::requireArray(root, "modpacks");
for (auto technicPack: objs) {
Modpack pack;
auto technicPackObject = Json::requireValueObject(technicPack);
pack.name = Json::requireString(technicPackObject, "name");
pack.slug = Json::requireString(technicPackObject, "slug");
if (pack.slug == "vanilla")
continue;
auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null");
if(rawURL == "null") {
pack.logoUrl = "null";
pack.logoName = "null";
switch (searchMode) {
case List: {
auto objs = Json::requireArray(root, "modpacks");
for (auto technicPack: objs) {
Modpack pack;
auto technicPackObject = Json::requireValueObject(technicPack);
pack.name = Json::requireString(technicPackObject, "name");
pack.slug = Json::requireString(technicPackObject, "slug");
if (pack.slug == "vanilla")
continue;
auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null");
if(rawURL == "null") {
pack.logoUrl = "null";
pack.logoName = "null";
}
else {
pack.logoUrl = rawURL;
pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
}
pack.broken = false;
newList.append(pack);
}
break;
}
else {
pack.logoUrl = rawURL;
pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
case Single: {
if (root.contains("error")) {
// Invalid API url
break;
}
Modpack pack;
pack.name = Json::requireString(root, "displayName");
pack.slug = Json::requireString(root, "name");
if (root.contains("icon")) {
auto iconObj = Json::requireObject(root, "icon");
auto iconUrl = Json::requireString(iconObj, "url");
pack.logoUrl = iconUrl;
pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0);
}
else {
pack.logoUrl = "null";
pack.logoName = "null";
}
pack.broken = false;
newList.append(pack);
break;
}
pack.broken = false;
newList.append(pack);
}
}
catch (const JSONValidationError &err)

View File

@ -1,4 +1,5 @@
/* Copyright 2020-2021 MultiMC Contributors
* Copyright 2021 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -63,6 +64,10 @@ private:
ResetRequested,
Finished
} searchState = None;
enum SearchMode {
List,
Single,
} searchMode = List;
NetJob::Ptr jobPtr;
QByteArray response;
};

View File

@ -1,4 +1,5 @@
/* Copyright 2013-2022 MultiMC Contributors
* Copyright 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,9 +22,11 @@
#include "ui/dialogs/NewInstanceDialog.h"
#include "BuildConfig.h"
#include "TechnicModel.h"
#include "modplatform/technic/SingleZipPackInstallTask.h"
#include "modplatform/technic/SolderPackInstallTask.h"
#include "modplatform/technic/SolderPackManifest.h"
#include "Json.h"
#include "Application.h"
@ -36,7 +39,9 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent)
ui->searchEdit->installEventFilter(this);
model = new Technic::ListModel(this);
ui->packView->setModel(model);
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged);
}
bool TechnicPage::eventFilter(QObject* watched, QEvent* event)
@ -74,13 +79,14 @@ void TechnicPage::triggerSearch() {
void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second)
{
ui->versionSelectionBox->clear();
if(!first.isValid())
{
if(isOpened)
{
dialog->setSuggestedPack();
}
//ui->frame->clear();
return;
}
@ -113,17 +119,19 @@ void TechnicPage::suggestCurrent()
}
NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network());
std::shared_ptr<QByteArray> response = std::make_shared<QByteArray>();
QString slug = current.slug;
netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.technicpack.net/modpack/%1?build=multimc").arg(slug), response.get()));
QObject::connect(netJob, &NetJob::succeeded, this, [this, response, slug]
netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response));
QObject::connect(netJob, &NetJob::succeeded, this, [this, slug]
{
jobPtr.reset();
if (current.slug != slug)
{
return;
}
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
QJsonParseError parse_error {};
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
QJsonObject obj = doc.object();
if(parse_error.error != QJsonParseError::NoError)
{
@ -165,10 +173,14 @@ void TechnicPage::suggestCurrent()
current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__");
current.author = Json::ensureString(obj, "user", QString(), "__placeholder__");
current.description = Json::ensureString(obj, "description", QString(), "__placeholder__");
current.currentVersion = Json::ensureString(obj, "version", QString(), "__placeholder__");
current.metadataLoaded = true;
metadataLoaded();
});
netJob->start();
jobPtr = netJob;
jobPtr->start();
}
// expects current.metadataLoaded to be true
@ -178,26 +190,119 @@ void TechnicPage::metadataLoaded()
QString name = current.name;
if (current.websiteUrl.isEmpty())
// This allows injecting HTML here.
text = name;
text = name.toHtmlEscaped();
else
// URL not properly escaped for inclusion in HTML. The name allows for injecting HTML.
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
text = "<a href=\"" + current.websiteUrl.toHtmlEscaped() + "\">" + name.toHtmlEscaped() + "</a>";
if (!current.author.isEmpty()) {
// This allows injecting HTML here
text += tr(" by ") + current.author;
text += "<br>" + tr(" by ") + current.author.toHtmlEscaped();
}
text += "<br><br>";
ui->packDescription->setHtml(text + current.description);
// Strip trailing forward-slashes from Solder URL's
if (current.isSolder) {
while (current.url.endsWith('/')) current.url.chop(1);
}
// Display versions from Solder
if (!current.isSolder) {
// If the pack isn't a Solder pack, it only has the single version
ui->versionSelectionBox->addItem(current.currentVersion);
}
else if (current.versionsLoaded) {
// reverse foreach, so that the newest versions are first
for (auto i = current.versions.size(); i--;) {
ui->versionSelectionBox->addItem(current.versions.at(i));
}
ui->versionSelectionBox->setCurrentText(current.recommended);
}
else {
// For now, until the versions are pulled from the Solder instance, display the current
// version so we can display something quicker
ui->versionSelectionBox->addItem(current.currentVersion);
auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network());
auto url = QString("%1/modpack/%2").arg(current.url, current.slug);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response));
QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded);
jobPtr = netJob;
jobPtr->start();
}
selectVersion();
}
void TechnicPage::selectVersion() {
if (!isOpened) {
return;
}
if (current.broken) {
dialog->setSuggestedPack();
return;
}
if (!current.isSolder)
{
dialog->setSuggestedPack(current.name, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion));
dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion));
}
else
{
while (current.url.endsWith('/')) current.url.chop(1);
dialog->setSuggestedPack(current.name, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, current.minecraftVersion));
dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, selectedVersion, current.minecraftVersion));
}
}
void TechnicPage::onSolderLoaded() {
jobPtr.reset();
auto fallback = [this]() {
current.versionsLoaded = true;
current.versions.clear();
current.versions.append(current.currentVersion);
};
current.versions.clear();
QJsonParseError parse_error {};
auto doc = QJsonDocument::fromJson(response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString();
qWarning() << response;
fallback();
return;
}
auto obj = doc.object();
TechnicSolder::Pack pack;
try {
TechnicSolder::loadPack(pack, obj);
}
catch (const JSONValidationError &err) {
qCritical() << "Couldn't parse Solder pack metadata:" << err.cause();
fallback();
return;
}
current.versionsLoaded = true;
current.recommended = pack.recommended;
current.versions << pack.builds;
// Finally, let's reload :)
ui->versionSelectionBox->clear();
metadataLoaded();
}
void TechnicPage::onVersionSelectionChanged(QString data) {
if (data.isNull() || data.isEmpty()) {
selectedVersion = "";
return;
}
selectedVersion = data;
selectVersion();
}

View File

@ -1,4 +1,5 @@
/* Copyright 2013-2021 MultiMC Contributors
* Copyright 2021-2022 Jamie Mansfield <jmansfield@cadixdev.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +20,7 @@
#include "ui/pages/BasePage.h"
#include <Application.h>
#include "net/NetJob.h"
#include "tasks/Task.h"
#include "TechnicData.h"
@ -65,14 +67,22 @@ public:
private:
void suggestCurrent();
void metadataLoaded();
void selectVersion();
private slots:
void triggerSearch();
void onSelectionChanged(QModelIndex first, QModelIndex second);
void onSolderLoaded();
void onVersionSelectionChanged(QString data);
private:
Ui::TechnicPage *ui = nullptr;
NewInstanceDialog* dialog = nullptr;
Technic::ListModel* model = nullptr;
Technic::Modpack current;
QString selectedVersion;
NetJob::Ptr jobPtr;
QByteArray response;
};

View File

@ -10,52 +10,44 @@
<height>405</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter ...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="rightMargin">
<number>0</number>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="2">
<widget class="QComboBox" name="versionSelectionBox"/>
</item>
<item row="0" column="1">
<widget class="QTextBrowser" name="packDescription"/>
<widget class="QLabel" name="label">
<property name="text">
<string>Version selected:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QListView" name="packView">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QListView" name="packView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
@ -67,14 +59,27 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QTextBrowser" name="packDescription"/>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter ...</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>searchEdit</tabstop>
<tabstop>searchButton</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -26,11 +26,11 @@
#include "BuildConfig.h"
#include "sys.h"
UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild)
UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, int currentBuild)
{
m_network = nam;
m_channelUrl = channelUrl;
m_currentChannel = currentChannel;
m_currentChannel = "develop";
m_currentBuild = currentBuild;
}
@ -44,9 +44,10 @@ bool UpdateChecker::hasChannels() const
return !m_channels.isEmpty();
}
void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate)
void UpdateChecker::checkForUpdate(bool notifyNoUpdate)
{
qDebug() << "Checking for updates.";
QString updateChannel = "develop";
// If the channel list hasn't loaded yet, load it and defer checking for updates until
// later.
@ -54,7 +55,6 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate)
{
qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check.";
m_checkUpdateWaiting = true;
m_deferredUpdateChannel = updateChannel;
updateChanList(notifyNoUpdate);
return;
}
@ -67,13 +67,13 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate)
// Find the desired channel within the channel list and get its repo URL. If if cannot be
// found, error.
QString stableUrl;
QString developUrl;
m_newRepoUrl = "";
for (ChannelListEntry entry : m_channels)
{
qDebug() << "channelEntry = " << entry.id;
if(entry.id == "stable") {
stableUrl = entry.url;
if(entry.id == "develop") {
developUrl = entry.url;
}
if (entry.id == updateChannel) {
m_newRepoUrl = entry.url;
@ -88,8 +88,8 @@ void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate)
qDebug() << "m_repoUrl = " << m_newRepoUrl;
if (m_newRepoUrl.isEmpty()) {
qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl;
m_newRepoUrl = stableUrl;
qWarning() << "m_repoUrl was empty. defaulting to 'develop': " << developUrl;
m_newRepoUrl = developUrl;
}
// If nothing applies, error
@ -255,7 +255,7 @@ void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate)
// If we're waiting to check for updates, do that now.
if (m_checkUpdateWaiting) {
checkForUpdate(m_deferredUpdateChannel, notifyNoUpdate);
checkForUpdate(notifyNoUpdate);
}
emit channelListLoaded();

View File

@ -23,8 +23,8 @@ class UpdateChecker : public QObject
Q_OBJECT
public:
UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild);
void checkForUpdate(QString updateChannel, bool notifyNoUpdate);
UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, int currentBuild);
void checkForUpdate(bool notifyNoUpdate);
/*!
* Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake).
@ -107,11 +107,6 @@ private:
*/
bool m_checkUpdateWaiting = false;
/*!
* if m_checkUpdateWaiting, this is the last used update channel
*/
QString m_deferredUpdateChannel;
int m_currentBuild = -1;
QString m_currentChannel;
QString m_currentRepoUrl;

View File

@ -42,38 +42,32 @@ slots:
void tst_ChannelListParsing_data()
{
QTest::addColumn<QString>("channel");
QTest::addColumn<QString>("channelUrl");
QTest::addColumn<bool>("hasChannels");
QTest::addColumn<bool>("valid");
QTest::addColumn<QList<UpdateChecker::ChannelListEntry> >("result");
QTest::newRow("garbage")
<< QString()
<< findTestDataUrl("data/garbageChannels.json")
<< false
<< false
<< QList<UpdateChecker::ChannelListEntry>();
QTest::newRow("errors")
<< QString()
<< findTestDataUrl("data/errorChannels.json")
<< false
<< true
<< QList<UpdateChecker::ChannelListEntry>();
QTest::newRow("no channels")
<< QString()
<< findTestDataUrl("data/noChannels.json")
<< false
<< true
<< QList<UpdateChecker::ChannelListEntry>();
QTest::newRow("one channel")
<< QString("develop")
<< findTestDataUrl("data/oneChannel.json")
<< true
<< true
<< (QList<UpdateChecker::ChannelListEntry>() << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "http://example.org/stuff"});
QTest::newRow("several channels")
<< QString("develop")
<< findTestDataUrl("data/channels.json")
<< true
<< true
@ -84,15 +78,13 @@ slots:
}
void tst_ChannelListParsing()
{
QFETCH(QString, channel);
QFETCH(QString, channelUrl);
QFETCH(bool, hasChannels);
QFETCH(bool, valid);
QFETCH(QList<UpdateChecker::ChannelListEntry>, result);
shared_qobject_ptr<QNetworkAccessManager> nam = new QNetworkAccessManager();
UpdateChecker checker(nam, channelUrl, channel, 0);
UpdateChecker checker(nam, channelUrl, 0);
QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded()));
QVERIFY(channelListLoadedSpy.isValid());
@ -116,12 +108,11 @@ slots:
void tst_UpdateChecking()
{
QString channel = "develop";
QString channelUrl = findTestDataUrl("data/channels.json");
int currentBuild = 2;
shared_qobject_ptr<QNetworkAccessManager> nam = new QNetworkAccessManager();
UpdateChecker checker(nam, channelUrl, channel, currentBuild);
UpdateChecker checker(nam, channelUrl, currentBuild);
QSignalSpy updateAvailableSpy(&checker, SIGNAL(updateAvailable(GoUpdate::Status)));
QVERIFY(updateAvailableSpy.isValid());
@ -133,7 +124,7 @@ slots:
qDebug() << "CWD:" << QDir::current().absolutePath();
checker.m_channels[0].url = findTestDataUrl("data/");
checker.checkForUpdate(channel, false);
checker.checkForUpdate(false);
QVERIFY(updateAvailableSpy.wait());

View File

@ -1,4 +1,4 @@
/* Copyright 2012-2021 MultiMC Contributors
/* Copyright 2012-2023 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -42,6 +42,9 @@ public class OneSixLauncher implements Launcher
private String windowTitle;
private String windowParams;
private String instanceTitle;
private String instanceIconId;
// secondary parameters
private int winSizeW;
private int winSizeH;
@ -50,6 +53,7 @@ public class OneSixLauncher implements Launcher
private String serverAddress;
private String serverPort;
private boolean useQuickPlay;
// the much abused system classloader, for convenience (for further abuse)
private ClassLoader cl;
@ -68,8 +72,16 @@ public class OneSixLauncher implements Launcher
windowTitle = params.firstSafe("windowTitle", "Minecraft");
windowParams = params.firstSafe("windowParams", "854x480");
instanceTitle = params.firstSafe("instanceTitle", "Minecraft");
instanceIconId = params.firstSafe("instanceIconId", "default");
// NOTE: this is included for the CraftPresence mod
System.setProperty("multimc.instance.title", instanceTitle);
System.setProperty("multimc.instance.icon", instanceIconId);
serverAddress = params.firstSafe("serverAddress", null);
serverPort = params.firstSafe("serverPort", null);
useQuickPlay = params.firstSafe("useQuickPlay").startsWith("1");
cwd = System.getProperty("user.dir");
@ -175,10 +187,18 @@ public class OneSixLauncher implements Launcher
if (serverAddress != null)
{
mcparams.add("--server");
mcparams.add(serverAddress);
mcparams.add("--port");
mcparams.add(serverPort);
if (useQuickPlay)
{
mcparams.add("--quickPlayMultiplayer");
mcparams.add(serverAddress + ":" + serverPort);
}
else
{
mcparams.add("--server");
mcparams.add(serverAddress);
mcparams.add("--port");
mcparams.add(serverPort);
}
}
// Get the Minecraft Class.