From 27f276ef13f49b4d21b6c17db5d4cd5e404ea259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 31 Oct 2021 21:42:06 +0100 Subject: [PATCH] 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 ``` --- launcher/CMakeLists.txt | 2 + launcher/LaunchController.cpp | 31 ++-- launcher/LaunchController.h | 32 ++-- launcher/Launcher.cpp | 147 +++++++++++------- launcher/Launcher.h | 6 +- launcher/LauncherMessage.cpp | 31 ++++ launcher/LauncherMessage.h | 13 ++ launcher/minecraft/auth/AccountList.cpp | 10 ++ launcher/minecraft/auth/AccountList.h | 1 + .../launch/MinecraftServerTarget.cpp | 1 + libraries/LocalPeer/include/LocalPeer.h | 4 +- libraries/LocalPeer/src/LocalPeer.cpp | 7 +- 12 files changed, 198 insertions(+), 87 deletions(-) create mode 100644 launcher/LauncherMessage.cpp create mode 100644 launcher/LauncherMessage.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 691ff004..eba8f8a1 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -565,6 +565,8 @@ SET(LAUNCHER_SOURCES Launcher.cpp UpdateController.cpp UpdateController.h + LauncherMessage.h + LauncherMessage.cpp # GUI - general utilities DesktopServices.h diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index a865caab..1c1e41e6 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -34,9 +34,11 @@ void LaunchController::executeTask() login(); } -// FIXME: minecraft specific -void LaunchController::login() { - JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget); +void LaunchController::decideAccount() +{ + if(m_accountToUse) { + return; + } // Find an account to use. std::shared_ptr accounts = LAUNCHER->accounts(); @@ -60,8 +62,8 @@ void LaunchController::login() { } } - MinecraftAccountPtr account = accounts->activeAccount(); - if (account.get() == nullptr) + m_accountToUse = accounts->activeAccount(); + if (m_accountToUse == nullptr) { // If no default account is set, ask the user which one to use. ProfileSelectDialog selectDialog( @@ -73,16 +75,23 @@ void LaunchController::login() { selectDialog.exec(); // 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 (selectDialog.useAsGlobalDefault() && account.get() != nullptr) { - accounts->setActiveAccount(account->profileId()); + if (selectDialog.useAsGlobalDefault() && m_accountToUse) { + 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 (!account.get()) + if (!m_accountToUse) { emitFailed(tr("No account selected for launch.")); return; @@ -102,10 +111,10 @@ void LaunchController::login() { m_session->wants_online = m_online; std::shared_ptr task; if(!password.isNull()) { - task = account->login(m_session, password); + task = m_accountToUse->login(m_session, password); } else { - task = account->refresh(m_session); + task = m_accountToUse->refresh(m_session); } if (task) { diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index 5f177e00..7ed4b09e 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -4,6 +4,7 @@ #include #include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/auth/MinecraftAccount.h" class InstanceWindow; class LaunchController: public Task @@ -15,39 +16,45 @@ public: LaunchController(QObject * parent = nullptr); virtual ~LaunchController(){}; - void setInstance(InstancePtr instance) - { + void setInstance(InstancePtr instance) { m_instance = instance; } - InstancePtr instance() - { + + InstancePtr instance() { return m_instance; } - void setOnline(bool online) - { + + void setOnline(bool online) { m_online = online; } - void setProfiler(BaseProfilerFactory *profiler) - { + + void setProfiler(BaseProfilerFactory *profiler) { m_profiler = profiler; } - void setParentWidget(QWidget * widget) - { + + void setParentWidget(QWidget * widget) { m_parentWidget = widget; } - void setServerToJoin(MinecraftServerTargetPtr serverToJoin) - { + + void setServerToJoin(MinecraftServerTargetPtr serverToJoin) { m_serverToJoin = std::move(serverToJoin); } + + void setAccountToUse(MinecraftAccountPtr accountToUse) { + m_accountToUse = std::move(accountToUse); + } + QString id() { return m_instance->id(); } + bool abort() override; private: void login(); void launchInstance(); + void decideAccount(); private slots: void readyForLaunch(); @@ -62,6 +69,7 @@ private: InstancePtr m_instance; QWidget * m_parentWidget = nullptr; InstanceWindow *m_console = nullptr; + MinecraftAccountPtr m_accountToUse = nullptr; AuthSessionPtr m_session; shared_qobject_ptr m_launcher; MinecraftServerTargetPtr m_serverToJoin; diff --git a/launcher/Launcher.cpp b/launcher/Launcher.cpp index 5036b7ff..a69fb8f3 100644 --- a/launcher/Launcher.cpp +++ b/launcher/Launcher.cpp @@ -23,6 +23,8 @@ #include "themes/BrightTheme.h" #include "themes/CustomTheme.h" +#include "LauncherMessage.h" + #include "setupwizard/SetupWizard.h" #include "setupwizard/LanguageWizardPage.h" #include "setupwizard/JavaWizardPage.h" @@ -227,8 +229,7 @@ Launcher::Launcher(int &argc, char **argv) : QApplication(argc, argv) // --dir parser.addOption("dir"); parser.addShortOpt("dir", 'd'); - parser.addDocumentation("dir", "Use the supplied folder as application root instead of " - "the binary location (use '.' for current)"); + parser.addDocumentation("dir", "Use the supplied folder as application root instead of the binary location (use '.' for current)"); // --launch parser.addOption("launch"); parser.addShortOpt("launch", 'l'); @@ -236,8 +237,11 @@ Launcher::Launcher(int &argc, char **argv) : QApplication(argc, argv) // --server parser.addOption("server"); parser.addShortOpt("server", 's'); - parser.addDocumentation("server", "Join the specified server on launch " - "(only valid in combination with --launch)"); + parser.addDocumentation("server", "Join the specified server on 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 parser.addSwitch("alive"); 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_serverToJoin = args["server"].toString(); + m_profileToUse = args["profile"].toString(); m_liveCheck = args["alive"].toBool(); m_zipToImport = args["import"].toUrl(); @@ -346,6 +351,13 @@ Launcher::Launcher(int &argc, char **argv) : QApplication(argc, argv) 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) // move user data to new location if on macOS and it still exists in Contents/MacOS 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. m_peerInstance = new LocalPeer(this, appID); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Launcher::messageReceived); - if(m_peerInstance->isClient()) - { + if(m_peerInstance->isClient()) { int timeout = 2000; if(m_instanceIdToLaunch.isEmpty()) { - m_peerInstance->sendMessage("activate", timeout); + LauncherMessage activate; + activate.command = "activate"; + m_peerInstance->sendMessage(activate.serialize(), timeout); 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 { + LauncherMessage launch; + launch.command = "launch"; + launch.args["id"] = m_instanceIdToLaunch; + if(!m_serverToJoin.isEmpty()) { - m_peerInstance->sendMessage( - "launch-with-server " + m_instanceIdToLaunch + " " + m_serverToJoin, timeout); + launch.args["server"] = m_serverToJoin; } - 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; return; @@ -977,18 +997,26 @@ void Launcher::performMainStartupAction() if(inst) { MinecraftServerTargetPtr serverToJoin = nullptr; + MinecraftAccountPtr accountToUse = nullptr; + qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; if(!m_serverToJoin.isEmpty()) { + // FIXME: validate the server string serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin))); - qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching with server" << m_serverToJoin; - } - else - { - qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; + qDebug() << " Launching with server" << m_serverToJoin; } - 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; } } @@ -1032,7 +1060,7 @@ Launcher::~Launcher() #endif } -void Launcher::messageReceived(const QString& message) +void Launcher::messageReceived(const QByteArray& message) { if(status() != Initialized) { @@ -1040,7 +1068,10 @@ void Launcher::messageReceived(const QString& message) return; } - QString command = message.section(' ', 0, 0); + LauncherMessage received; + received.parse(message); + + auto & command = received.command; if(command == "activate") { @@ -1048,52 +1079,54 @@ void Launcher::messageReceived(const QString& message) } else if(command == "import") { - QString arg = message.section(' ', 1); - if(arg.isEmpty()) + QString path = received.args["path"]; + if(path.isEmpty()) { qWarning() << "Received" << command << "message without a zip path/URL."; return; } - m_mainWindow->droppedURLs({ QUrl(arg) }); + m_mainWindow->droppedURLs({ QUrl(path) }); } else if(command == "launch") { - QString arg = message.section(' ', 1); - if(arg.isEmpty()) - { - qWarning() << "Received" << command << "message without an instance ID."; + QString id = received.args["id"]; + QString server = received.args["server"]; + QString profile = received.args["profile"]; + + 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; } - auto inst = instances()->getInstanceById(arg); - if(inst) - { - launch(inst, true, nullptr); + + MinecraftServerTargetPtr serverObject = nullptr; + if(!server.isEmpty()) { + serverObject = std::make_shared(MinecraftServerTarget::parse(server)); } - } - else if(command == "launch-with-server") - { - QString instanceID = message.section(' ', 1, 1); - QString serverToJoin = message.section(' ', 2, 2); - if(instanceID.isEmpty()) - { - 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::parse(serverToJoin)) - ); + + MinecraftAccountPtr accountObject; + if(!profile.isEmpty()) { + accountObject = accounts()->getAccountByProfileName(profile); + if(!accountObject) { + qWarning() << "Launch command requires the specified profile to be valid. " << profile << "does not resolve to any account."; + return; + } } + + launch( + instance, + true, + nullptr, + serverObject, + accountObject + ); } else { @@ -1189,7 +1222,8 @@ bool Launcher::launch( InstancePtr instance, bool online, BaseProfilerFactory *profiler, - MinecraftServerTargetPtr serverToJoin + MinecraftServerTargetPtr serverToJoin, + MinecraftAccountPtr accountToUse ) { if(m_updateRunning) { @@ -1212,6 +1246,7 @@ bool Launcher::launch( controller->setOnline(online); controller->setProfiler(profiler); controller->setServerToJoin(serverToJoin); + controller->setAccountToUse(accountToUse); if(window) { controller->setParentWidget(window); diff --git a/launcher/Launcher.h b/launcher/Launcher.h index f4a20122..8d97525f 100644 --- a/launcher/Launcher.h +++ b/launcher/Launcher.h @@ -156,13 +156,14 @@ public slots: InstancePtr instance, bool online = true, BaseProfilerFactory *profiler = nullptr, - MinecraftServerTargetPtr serverToJoin = nullptr + MinecraftServerTargetPtr serverToJoin = nullptr, + MinecraftAccountPtr accountToUse = nullptr ); bool kill(InstancePtr instance); private slots: void on_windowClose(); - void messageReceived(const QString & message); + void messageReceived(const QByteArray & message); void controllerSucceeded(); void controllerFailed(const QString & error); void analyticsSettingChanged(const Setting &setting, QVariant value); @@ -229,6 +230,7 @@ private: public: QString m_instanceIdToLaunch; QString m_serverToJoin; + QString m_profileToUse; bool m_liveCheck = false; QUrl m_zipToImport; std::unique_ptr logFile; diff --git a/launcher/LauncherMessage.cpp b/launcher/LauncherMessage.cpp new file mode 100644 index 00000000..4cc56e22 --- /dev/null +++ b/launcher/LauncherMessage.cpp @@ -0,0 +1,31 @@ +#include "LauncherMessage.h" + +#include +#include + +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(); +} diff --git a/launcher/LauncherMessage.h b/launcher/LauncherMessage.h new file mode 100644 index 00000000..024b16a5 --- /dev/null +++ b/launcher/LauncherMessage.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include + +struct LauncherMessage { + QString command; + QMap args; + + QByteArray serialize(); + void parse(const QByteArray & input); +}; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 76af0ac0..a76cac55 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -47,6 +47,16 @@ int AccountList::findAccountByProfileId(const QString& profileId) const { 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 { return MinecraftAccountPtr(m_accounts.at(i)); diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index ed08bb1d..e275eb17 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -62,6 +62,7 @@ public: void addAccount(const MinecraftAccountPtr account); void removeAccount(QModelIndex index); int findAccountByProfileId(const QString &profileId) const; + MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const; /*! * Sets the path to load/save the list file from/to. diff --git a/launcher/minecraft/launch/MinecraftServerTarget.cpp b/launcher/minecraft/launch/MinecraftServerTarget.cpp index 569273b6..0f98f356 100644 --- a/launcher/minecraft/launch/MinecraftServerTarget.cpp +++ b/launcher/minecraft/launch/MinecraftServerTarget.cpp @@ -17,6 +17,7 @@ #include +// 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) { QStringList split = fullAddress.split(":"); diff --git a/libraries/LocalPeer/include/LocalPeer.h b/libraries/LocalPeer/include/LocalPeer.h index a24e4775..3619ed5d 100644 --- a/libraries/LocalPeer/include/LocalPeer.h +++ b/libraries/LocalPeer/include/LocalPeer.h @@ -83,11 +83,11 @@ public: LocalPeer(QObject *parent, const ApplicationId &appId); ~LocalPeer(); bool isClient(); - bool sendMessage(const QString &message, int timeout); + bool sendMessage(const QByteArray &message, int timeout); ApplicationId applicationId() const; Q_SIGNALS: - void messageReceived(const QString &message); + void messageReceived(const QByteArray &message); protected Q_SLOTS: void receiveConnection(); diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index 129f3abc..cb218466 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -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()) return false; @@ -177,7 +177,7 @@ bool LocalPeer::sendMessage(const QString &message, int timeout) return false; } - QByteArray uMsg(message.toUtf8()); + QByteArray uMsg(message); QDataStream ds(&socket); ds.writeBytes(uMsg.constData(), uMsg.size()); @@ -232,10 +232,9 @@ void LocalPeer::receiveConnection() delete socket; return; } - QString message(QString::fromUtf8(uMsg)); socket->write(ack, qstrlen(ack)); socket->waitForBytesWritten(1000); socket->waitForDisconnected(1000); // make sure client reads ack delete socket; - emit messageReceived(message); //### (might take a long time to return) + emit messageReceived(uMsg); //### (might take a long time to return) }