GH-1795 add terminal launch option to use a specific Minecraft profile

Used like this:
```
./MultiMC --launch 1.17.1 --profile MultiMCTest --server mc.hypixel.net
```
This commit is contained in:
Petr Mrázek 2021-10-31 21:42:06 +01:00
parent 393d17b8d3
commit 27f276ef13
12 changed files with 198 additions and 87 deletions

View File

@ -565,6 +565,8 @@ SET(LAUNCHER_SOURCES
Launcher.cpp Launcher.cpp
UpdateController.cpp UpdateController.cpp
UpdateController.h UpdateController.h
LauncherMessage.h
LauncherMessage.cpp
# GUI - general utilities # GUI - general utilities
DesktopServices.h DesktopServices.h

View File

@ -34,9 +34,11 @@ void LaunchController::executeTask()
login(); login();
} }
// FIXME: minecraft specific void LaunchController::decideAccount()
void LaunchController::login() { {
JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); if(m_accountToUse) {
return;
}
// Find an account to use. // Find an account to use.
std::shared_ptr<AccountList> accounts = LAUNCHER->accounts(); std::shared_ptr<AccountList> accounts = LAUNCHER->accounts();
@ -60,8 +62,8 @@ void LaunchController::login() {
} }
} }
MinecraftAccountPtr account = accounts->activeAccount(); m_accountToUse = accounts->activeAccount();
if (account.get() == nullptr) if (m_accountToUse == nullptr)
{ {
// If no default account is set, ask the user which one to use. // If no default account is set, ask the user which one to use.
ProfileSelectDialog selectDialog( ProfileSelectDialog selectDialog(
@ -73,16 +75,23 @@ void LaunchController::login() {
selectDialog.exec(); selectDialog.exec();
// Launch the instance with the selected account. // Launch the instance with the selected account.
account = selectDialog.selectedAccount(); m_accountToUse = selectDialog.selectedAccount();
// If the user said to use the account as default, do that. // If the user said to use the account as default, do that.
if (selectDialog.useAsGlobalDefault() && account.get() != nullptr) { if (selectDialog.useAsGlobalDefault() && m_accountToUse) {
accounts->setActiveAccount(account->profileId()); accounts->setActiveAccount(m_accountToUse->profileId());
} }
} }
}
void LaunchController::login() {
JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget);
decideAccount();
// if no account is selected, we bail // if no account is selected, we bail
if (!account.get()) if (!m_accountToUse)
{ {
emitFailed(tr("No account selected for launch.")); emitFailed(tr("No account selected for launch."));
return; return;
@ -102,10 +111,10 @@ void LaunchController::login() {
m_session->wants_online = m_online; m_session->wants_online = m_online;
std::shared_ptr<AccountTask> task; std::shared_ptr<AccountTask> task;
if(!password.isNull()) { if(!password.isNull()) {
task = account->login(m_session, password); task = m_accountToUse->login(m_session, password);
} }
else { else {
task = account->refresh(m_session); task = m_accountToUse->refresh(m_session);
} }
if (task) if (task)
{ {

View File

@ -4,6 +4,7 @@
#include <tools/BaseProfiler.h> #include <tools/BaseProfiler.h>
#include "minecraft/launch/MinecraftServerTarget.h" #include "minecraft/launch/MinecraftServerTarget.h"
#include "minecraft/auth/MinecraftAccount.h"
class InstanceWindow; class InstanceWindow;
class LaunchController: public Task class LaunchController: public Task
@ -15,39 +16,45 @@ public:
LaunchController(QObject * parent = nullptr); LaunchController(QObject * parent = nullptr);
virtual ~LaunchController(){}; virtual ~LaunchController(){};
void setInstance(InstancePtr instance) void setInstance(InstancePtr instance) {
{
m_instance = instance; m_instance = instance;
} }
InstancePtr instance()
{ InstancePtr instance() {
return m_instance; return m_instance;
} }
void setOnline(bool online)
{ void setOnline(bool online) {
m_online = online; m_online = online;
} }
void setProfiler(BaseProfilerFactory *profiler)
{ void setProfiler(BaseProfilerFactory *profiler) {
m_profiler = profiler; m_profiler = profiler;
} }
void setParentWidget(QWidget * widget)
{ void setParentWidget(QWidget * widget) {
m_parentWidget = widget; m_parentWidget = widget;
} }
void setServerToJoin(MinecraftServerTargetPtr serverToJoin)
{ void setServerToJoin(MinecraftServerTargetPtr serverToJoin) {
m_serverToJoin = std::move(serverToJoin); m_serverToJoin = std::move(serverToJoin);
} }
void setAccountToUse(MinecraftAccountPtr accountToUse) {
m_accountToUse = std::move(accountToUse);
}
QString id() QString id()
{ {
return m_instance->id(); return m_instance->id();
} }
bool abort() override; bool abort() override;
private: private:
void login(); void login();
void launchInstance(); void launchInstance();
void decideAccount();
private slots: private slots:
void readyForLaunch(); void readyForLaunch();
@ -62,6 +69,7 @@ private:
InstancePtr m_instance; InstancePtr m_instance;
QWidget * m_parentWidget = nullptr; QWidget * m_parentWidget = nullptr;
InstanceWindow *m_console = nullptr; InstanceWindow *m_console = nullptr;
MinecraftAccountPtr m_accountToUse = nullptr;
AuthSessionPtr m_session; AuthSessionPtr m_session;
shared_qobject_ptr<LaunchTask> m_launcher; shared_qobject_ptr<LaunchTask> m_launcher;
MinecraftServerTargetPtr m_serverToJoin; MinecraftServerTargetPtr m_serverToJoin;

View File

@ -23,6 +23,8 @@
#include "themes/BrightTheme.h" #include "themes/BrightTheme.h"
#include "themes/CustomTheme.h" #include "themes/CustomTheme.h"
#include "LauncherMessage.h"
#include "setupwizard/SetupWizard.h" #include "setupwizard/SetupWizard.h"
#include "setupwizard/LanguageWizardPage.h" #include "setupwizard/LanguageWizardPage.h"
#include "setupwizard/JavaWizardPage.h" #include "setupwizard/JavaWizardPage.h"
@ -227,8 +229,7 @@ Launcher::Launcher(int &argc, char **argv) : QApplication(argc, argv)
// --dir // --dir
parser.addOption("dir"); parser.addOption("dir");
parser.addShortOpt("dir", 'd'); parser.addShortOpt("dir", 'd');
parser.addDocumentation("dir", "Use the supplied folder as application root instead of " parser.addDocumentation("dir", "Use the supplied folder as application root instead of the binary location (use '.' for current)");
"the binary location (use '.' for current)");
// --launch // --launch
parser.addOption("launch"); parser.addOption("launch");
parser.addShortOpt("launch", 'l'); parser.addShortOpt("launch", 'l');
@ -236,8 +237,11 @@ Launcher::Launcher(int &argc, char **argv) : QApplication(argc, argv)
// --server // --server
parser.addOption("server"); parser.addOption("server");
parser.addShortOpt("server", 's'); parser.addShortOpt("server", 's');
parser.addDocumentation("server", "Join the specified server on launch " parser.addDocumentation("server", "Join the specified server on launch (only valid in combination with --launch)");
"(only valid in combination with --launch)"); // --profile
parser.addOption("profile");
parser.addShortOpt("profile", 'a');
parser.addDocumentation("profile", "Use the account specified by its profile name (only valid in combination with --launch)");
// --alive // --alive
parser.addSwitch("alive"); parser.addSwitch("alive");
parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after the launcher starts"); parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after the launcher starts");
@ -280,6 +284,7 @@ Launcher::Launcher(int &argc, char **argv) : QApplication(argc, argv)
} }
m_instanceIdToLaunch = args["launch"].toString(); m_instanceIdToLaunch = args["launch"].toString();
m_serverToJoin = args["server"].toString(); m_serverToJoin = args["server"].toString();
m_profileToUse = args["profile"].toString();
m_liveCheck = args["alive"].toBool(); m_liveCheck = args["alive"].toBool();
m_zipToImport = args["import"].toUrl(); m_zipToImport = args["import"].toUrl();
@ -346,6 +351,13 @@ Launcher::Launcher(int &argc, char **argv) : QApplication(argc, argv)
return; return;
} }
if(m_instanceIdToLaunch.isEmpty() && !m_profileToUse.isEmpty())
{
std::cerr << "--account can only be used in combination with --launch!" << std::endl;
m_status = Launcher::Failed;
return;
}
#if defined(Q_OS_MAC) #if defined(Q_OS_MAC)
// move user data to new location if on macOS and it still exists in Contents/MacOS // move user data to new location if on macOS and it still exists in Contents/MacOS
QDir fi(applicationDirPath()); QDir fi(applicationDirPath());
@ -419,30 +431,38 @@ Launcher::Launcher(int &argc, char **argv) : QApplication(argc, argv)
// FIXME: you can run the same binaries with multiple data dirs and they won't clash. This could cause issues for updates. // FIXME: you can run the same binaries with multiple data dirs and they won't clash. This could cause issues for updates.
m_peerInstance = new LocalPeer(this, appID); m_peerInstance = new LocalPeer(this, appID);
connect(m_peerInstance, &LocalPeer::messageReceived, this, &Launcher::messageReceived); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Launcher::messageReceived);
if(m_peerInstance->isClient()) if(m_peerInstance->isClient()) {
{
int timeout = 2000; int timeout = 2000;
if(m_instanceIdToLaunch.isEmpty()) if(m_instanceIdToLaunch.isEmpty())
{ {
m_peerInstance->sendMessage("activate", timeout); LauncherMessage activate;
activate.command = "activate";
m_peerInstance->sendMessage(activate.serialize(), timeout);
if(!m_zipToImport.isEmpty()) if(!m_zipToImport.isEmpty())
{ {
m_peerInstance->sendMessage("import " + m_zipToImport.toString(), timeout); LauncherMessage import;
import.command = "import";
import.args.insert("path", m_zipToImport.toString());
m_peerInstance->sendMessage(import.serialize(), timeout);
} }
} }
else else
{ {
LauncherMessage launch;
launch.command = "launch";
launch.args["id"] = m_instanceIdToLaunch;
if(!m_serverToJoin.isEmpty()) if(!m_serverToJoin.isEmpty())
{ {
m_peerInstance->sendMessage( launch.args["server"] = m_serverToJoin;
"launch-with-server " + m_instanceIdToLaunch + " " + m_serverToJoin, timeout);
} }
else if(!m_profileToUse.isEmpty())
{ {
m_peerInstance->sendMessage("launch " + m_instanceIdToLaunch, timeout); launch.args["profile"] = m_profileToUse;
} }
m_peerInstance->sendMessage(launch.serialize(), timeout);
} }
m_status = Launcher::Succeeded; m_status = Launcher::Succeeded;
return; return;
@ -977,18 +997,26 @@ void Launcher::performMainStartupAction()
if(inst) if(inst)
{ {
MinecraftServerTargetPtr serverToJoin = nullptr; MinecraftServerTargetPtr serverToJoin = nullptr;
MinecraftAccountPtr accountToUse = nullptr;
qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching";
if(!m_serverToJoin.isEmpty()) if(!m_serverToJoin.isEmpty())
{ {
// FIXME: validate the server string
serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin))); serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin)));
qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching with server" << m_serverToJoin; qDebug() << " Launching with server" << m_serverToJoin;
}
else
{
qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching";
} }
launch(inst, true, nullptr, serverToJoin); if(!m_profileToUse.isEmpty())
{
accountToUse = accounts()->getAccountByProfileName(m_profileToUse);
if(!accountToUse) {
return;
}
qDebug() << " Launching with account" << m_profileToUse;
}
launch(inst, true, nullptr, serverToJoin, accountToUse);
return; return;
} }
} }
@ -1032,7 +1060,7 @@ Launcher::~Launcher()
#endif #endif
} }
void Launcher::messageReceived(const QString& message) void Launcher::messageReceived(const QByteArray& message)
{ {
if(status() != Initialized) if(status() != Initialized)
{ {
@ -1040,7 +1068,10 @@ void Launcher::messageReceived(const QString& message)
return; return;
} }
QString command = message.section(' ', 0, 0); LauncherMessage received;
received.parse(message);
auto & command = received.command;
if(command == "activate") if(command == "activate")
{ {
@ -1048,52 +1079,54 @@ void Launcher::messageReceived(const QString& message)
} }
else if(command == "import") else if(command == "import")
{ {
QString arg = message.section(' ', 1); QString path = received.args["path"];
if(arg.isEmpty()) if(path.isEmpty())
{ {
qWarning() << "Received" << command << "message without a zip path/URL."; qWarning() << "Received" << command << "message without a zip path/URL.";
return; return;
} }
m_mainWindow->droppedURLs({ QUrl(arg) }); m_mainWindow->droppedURLs({ QUrl(path) });
} }
else if(command == "launch") else if(command == "launch")
{ {
QString arg = message.section(' ', 1); QString id = received.args["id"];
if(arg.isEmpty()) QString server = received.args["server"];
{ QString profile = received.args["profile"];
qWarning() << "Received" << command << "message without an instance ID.";
InstancePtr instance;
if(!id.isEmpty()) {
instance = instances()->getInstanceById(id);
if(!instance) {
qWarning() << "Launch command requires an valid instance ID. " << id << "resolves to nothing.";
return;
}
}
else {
qWarning() << "Launch command called without an instance ID...";
return; return;
} }
auto inst = instances()->getInstanceById(arg);
if(inst) MinecraftServerTargetPtr serverObject = nullptr;
{ if(!server.isEmpty()) {
launch(inst, true, nullptr); serverObject = std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(server));
} }
}
else if(command == "launch-with-server") MinecraftAccountPtr accountObject;
{ if(!profile.isEmpty()) {
QString instanceID = message.section(' ', 1, 1); accountObject = accounts()->getAccountByProfileName(profile);
QString serverToJoin = message.section(' ', 2, 2); if(!accountObject) {
if(instanceID.isEmpty()) qWarning() << "Launch command requires the specified profile to be valid. " << profile << "does not resolve to any account.";
{ return;
qWarning() << "Received" << command << "message without an instance ID."; }
return;
}
if(serverToJoin.isEmpty())
{
qWarning() << "Received" << command << "message without a server to join.";
return;
}
auto inst = instances()->getInstanceById(instanceID);
if(inst)
{
launch(
inst,
true,
nullptr,
std::make_shared<MinecraftServerTarget>(MinecraftServerTarget::parse(serverToJoin))
);
} }
launch(
instance,
true,
nullptr,
serverObject,
accountObject
);
} }
else else
{ {
@ -1189,7 +1222,8 @@ bool Launcher::launch(
InstancePtr instance, InstancePtr instance,
bool online, bool online,
BaseProfilerFactory *profiler, BaseProfilerFactory *profiler,
MinecraftServerTargetPtr serverToJoin MinecraftServerTargetPtr serverToJoin,
MinecraftAccountPtr accountToUse
) { ) {
if(m_updateRunning) if(m_updateRunning)
{ {
@ -1212,6 +1246,7 @@ bool Launcher::launch(
controller->setOnline(online); controller->setOnline(online);
controller->setProfiler(profiler); controller->setProfiler(profiler);
controller->setServerToJoin(serverToJoin); controller->setServerToJoin(serverToJoin);
controller->setAccountToUse(accountToUse);
if(window) if(window)
{ {
controller->setParentWidget(window); controller->setParentWidget(window);

View File

@ -156,13 +156,14 @@ public slots:
InstancePtr instance, InstancePtr instance,
bool online = true, bool online = true,
BaseProfilerFactory *profiler = nullptr, BaseProfilerFactory *profiler = nullptr,
MinecraftServerTargetPtr serverToJoin = nullptr MinecraftServerTargetPtr serverToJoin = nullptr,
MinecraftAccountPtr accountToUse = nullptr
); );
bool kill(InstancePtr instance); bool kill(InstancePtr instance);
private slots: private slots:
void on_windowClose(); void on_windowClose();
void messageReceived(const QString & message); void messageReceived(const QByteArray & message);
void controllerSucceeded(); void controllerSucceeded();
void controllerFailed(const QString & error); void controllerFailed(const QString & error);
void analyticsSettingChanged(const Setting &setting, QVariant value); void analyticsSettingChanged(const Setting &setting, QVariant value);
@ -229,6 +230,7 @@ private:
public: public:
QString m_instanceIdToLaunch; QString m_instanceIdToLaunch;
QString m_serverToJoin; QString m_serverToJoin;
QString m_profileToUse;
bool m_liveCheck = false; bool m_liveCheck = false;
QUrl m_zipToImport; QUrl m_zipToImport;
std::unique_ptr<QFile> logFile; std::unique_ptr<QFile> logFile;

View File

@ -0,0 +1,31 @@
#include "LauncherMessage.h"
#include <QJsonDocument>
#include <QJsonObject>
void LauncherMessage::parse(const QByteArray & input) {
auto doc = QJsonDocument::fromBinaryData(input);
auto root = doc.object();
command = root.value("command").toString();
args.clear();
auto parsedArgs = root.value("args").toObject();
for(auto iter = parsedArgs.begin(); iter != parsedArgs.end(); iter++) {
args[iter.key()] = iter.value().toString();
}
}
QByteArray LauncherMessage::serialize() {
QJsonObject root;
root.insert("command", command);
QJsonObject outArgs;
for (auto iter = args.begin(); iter != args.end(); iter++) {
outArgs[iter.key()] = iter.value();
}
root.insert("args", outArgs);
QJsonDocument out;
out.setObject(root);
return out.toBinaryData();
}

View File

@ -0,0 +1,13 @@
#pragma once
#include <QString>
#include <QMap>
#include <QByteArray>
struct LauncherMessage {
QString command;
QMap<QString, QString> args;
QByteArray serialize();
void parse(const QByteArray & input);
};

View File

@ -47,6 +47,16 @@ int AccountList::findAccountByProfileId(const QString& profileId) const {
return -1; return -1;
} }
MinecraftAccountPtr AccountList::getAccountByProfileName(const QString& profileName) const {
for (int i = 0; i < count(); i++) {
MinecraftAccountPtr account = at(i);
if (account->profileName() == profileName) {
return account;
}
}
return nullptr;
}
const MinecraftAccountPtr AccountList::at(int i) const const MinecraftAccountPtr AccountList::at(int i) const
{ {
return MinecraftAccountPtr(m_accounts.at(i)); return MinecraftAccountPtr(m_accounts.at(i));

View File

@ -62,6 +62,7 @@ public:
void addAccount(const MinecraftAccountPtr account); void addAccount(const MinecraftAccountPtr account);
void removeAccount(QModelIndex index); void removeAccount(QModelIndex index);
int findAccountByProfileId(const QString &profileId) const; int findAccountByProfileId(const QString &profileId) const;
MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const;
/*! /*!
* Sets the path to load/save the list file from/to. * Sets the path to load/save the list file from/to.

View File

@ -17,6 +17,7 @@
#include <QStringList> #include <QStringList>
// FIXME: the way this is written, it can't ever do any sort of validation and can accept total junk
MinecraftServerTarget MinecraftServerTarget::parse(const QString &fullAddress) { MinecraftServerTarget MinecraftServerTarget::parse(const QString &fullAddress) {
QStringList split = fullAddress.split(":"); QStringList split = fullAddress.split(":");

View File

@ -83,11 +83,11 @@ public:
LocalPeer(QObject *parent, const ApplicationId &appId); LocalPeer(QObject *parent, const ApplicationId &appId);
~LocalPeer(); ~LocalPeer();
bool isClient(); bool isClient();
bool sendMessage(const QString &message, int timeout); bool sendMessage(const QByteArray &message, int timeout);
ApplicationId applicationId() const; ApplicationId applicationId() const;
Q_SIGNALS: Q_SIGNALS:
void messageReceived(const QString &message); void messageReceived(const QByteArray &message);
protected Q_SLOTS: protected Q_SLOTS:
void receiveConnection(); void receiveConnection();

View File

@ -155,7 +155,7 @@ bool LocalPeer::isClient()
} }
bool LocalPeer::sendMessage(const QString &message, int timeout) bool LocalPeer::sendMessage(const QByteArray &message, int timeout)
{ {
if (!isClient()) if (!isClient())
return false; return false;
@ -177,7 +177,7 @@ bool LocalPeer::sendMessage(const QString &message, int timeout)
return false; return false;
} }
QByteArray uMsg(message.toUtf8()); QByteArray uMsg(message);
QDataStream ds(&socket); QDataStream ds(&socket);
ds.writeBytes(uMsg.constData(), uMsg.size()); ds.writeBytes(uMsg.constData(), uMsg.size());
@ -232,10 +232,9 @@ void LocalPeer::receiveConnection()
delete socket; delete socket;
return; return;
} }
QString message(QString::fromUtf8(uMsg));
socket->write(ack, qstrlen(ack)); socket->write(ack, qstrlen(ack));
socket->waitForBytesWritten(1000); socket->waitForBytesWritten(1000);
socket->waitForDisconnected(1000); // make sure client reads ack socket->waitForDisconnected(1000); // make sure client reads ack
delete socket; delete socket;
emit messageReceived(message); //### (might take a long time to return) emit messageReceived(uMsg); //### (might take a long time to return)
} }