diff --git a/Readme.md b/Readme.md index bea0e8b542..8f907ae749 100644 --- a/Readme.md +++ b/Readme.md @@ -173,6 +173,11 @@ Options: -b, --batch Run Dolphin without the user interface (Requires --exec or --nand-title) -c, --confirm Set Confirm on Stop + --netplay_host= + Host a netplay session on the specified port (Requires --exec) + --netplay_join= OR + Join a netplay session at the specified IP address and + port or using a host code -v VIDEO_BACKEND, --video_backend=VIDEO_BACKEND Specify a video backend -a AUDIO_EMULATION, --audio_emulation=AUDIO_EMULATION diff --git a/Source/Core/DolphinQt/Main.cpp b/Source/Core/DolphinQt/Main.cpp index 88400aa1a6..4aa46f1e9f 100644 --- a/Source/Core/DolphinQt/Main.cpp +++ b/Source/Core/DolphinQt/Main.cpp @@ -41,6 +41,7 @@ #include "DolphinQt/Updater.h" #include "UICommon/CommandLineParse.h" +#include "UICommon/GameFile.h" #include "UICommon/UICommon.h" static bool QtMsgAlertHandler(const char* caption, const char* text, bool yes_no, @@ -190,14 +191,22 @@ int main(int argc, char* argv[]) std::unique_ptr boot; bool game_specified = false; + std::optional netplay_game = std::nullopt; if (options.is_set("exec")) { const std::list paths_list = options.all("exec"); const std::vector paths{std::make_move_iterator(std::begin(paths_list)), std::make_move_iterator(std::end(paths_list))}; - boot = BootParameters::GenerateFromFile( - paths, BootSessionData(save_state_path, DeleteSavestateAfterBoot::No)); - game_specified = true; + if (options.is_set("netplay_host")) + { + netplay_game = UICommon::GameFile(paths[0]); + } + else + { + boot = BootParameters::GenerateFromFile( + paths, BootSessionData(save_state_path, DeleteSavestateAfterBoot::No)); + game_specified = true; + } } else if (options.is_set("nand_title")) { @@ -220,9 +229,48 @@ int main(int argc, char* argv[]) game_specified = true; } + // FIXME: All those if/else-if clauses except for the last `else` SEGFAULT. + // This is "fine" since they mean to exit dolphin anyways, but it probably shouldn't do that. int retval; - - if (save_state_path && !game_specified) + if (options.is_set("netplay_join") && options.is_set("netplay_host")) + { + ModalMessageBox::critical( + nullptr, QObject::tr("Error"), + QObject::tr("The --netplay_host and --netplay_join flags are mutually exclusive.")); + retval = 1; + } + else if (options.is_set("netplay_join") && game_specified) + { + ModalMessageBox::critical( + nullptr, QObject::tr("Error"), + QObject::tr("You cannot select the game for the NetPlay session you are joining.")); + retval = 1; + } + else if (options.is_set("netplay_join") && save_state_path) + { + ModalMessageBox::critical(nullptr, QObject::tr("Error"), + QObject::tr("NetPlay does not support NAND titles.")); + retval = 1; + } + else if (!netplay_game && options.is_set("netplay_host")) + { + ModalMessageBox::critical(nullptr, QObject::tr("Error"), + QObject::tr("You must specify a game to host with NetPlay.")); + retval = 1; + } + else if (netplay_game && game_specified) + { + ModalMessageBox::critical(nullptr, QObject::tr("Error"), + QObject::tr("NetPlay does not support NAND titles.")); + retval = 1; + } + else if (netplay_game && save_state_path) + { + ModalMessageBox::critical(nullptr, QObject::tr("Error"), + QObject::tr("NetPlay does not support savestates.")); + retval = 1; + } + else if (save_state_path && !game_specified) { ModalMessageBox::critical( nullptr, QObject::tr("Error"), @@ -250,7 +298,8 @@ int main(int argc, char* argv[]) Settings::Instance().ApplyStyle(); MainWindow win{Core::System::GetInstance(), std::move(boot), - static_cast(options.get("movie"))}; + static_cast(options.get("movie")), options.is_set("netplay_join"), + netplay_game}; #if defined(USE_ANALYTICS) && USE_ANALYTICS if (!Config::Get(Config::MAIN_ANALYTICS_PERMISSION_ASKED)) diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 93ad16a59e..62c74c2ae1 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -218,7 +218,8 @@ static std::vector StringListToStdVector(QStringList list) } MainWindow::MainWindow(Core::System& system, std::unique_ptr boot_parameters, - const std::string& movie_path) + const std::string& movie_path, const bool netplay_join, + const std::optional netplay_host) : QMainWindow(nullptr), m_system(system) { setWindowTitle(QString::fromStdString(Common::GetScmRevStr())); @@ -330,7 +331,15 @@ MainWindow::MainWindow(Core::System& system, std::unique_ptr boo Host::GetInstance()->SetMainWindowHandle(reinterpret_cast(winId())); - if (m_pending_boot != nullptr) + if (netplay_join) + { + NetPlayJoin(); + } + else if (netplay_host) + { + NetPlayHost(netplay_host.value()); + } + else if (m_pending_boot != nullptr) { StartGame(std::move(m_pending_boot)); m_pending_boot.reset(); diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index f9f0f1c95d..193b3b82c8 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -80,7 +80,8 @@ class MainWindow final : public QMainWindow public: explicit MainWindow(Core::System& system, std::unique_ptr boot_parameters, - const std::string& movie_path); + const std::string& movie_path, const bool netplay_join, + const std::optional netplay_host); ~MainWindow(); WindowSystemInfo GetWindowSystemInfo() const; diff --git a/Source/Core/UICommon/CommandLineParse.cpp b/Source/Core/UICommon/CommandLineParse.cpp index a37f0f2561..e3e0170c4f 100644 --- a/Source/Core/UICommon/CommandLineParse.cpp +++ b/Source/Core/UICommon/CommandLineParse.cpp @@ -3,6 +3,7 @@ #include "UICommon/CommandLineParse.h" +#include #include #include #include @@ -15,6 +16,7 @@ #include "Common/StringUtil.h" #include "Common/Version.h" #include "Core/Config/MainSettings.h" +#include "Core/Config/NetplaySettings.h" namespace CommandLineParse { @@ -22,7 +24,9 @@ class CommandLineConfigLayerLoader final : public Config::ConfigLayerLoader { public: CommandLineConfigLayerLoader(const std::list& args, const std::string& video_backend, - const std::string& audio_backend, bool batch, bool debugger) + const std::string& audio_backend, + const std::optional netplay_host, + const std::string& netplay_join, bool batch, bool debugger) : ConfigLayerLoader(Config::LayerType::CommandLine) { if (!video_backend.empty()) @@ -34,6 +38,54 @@ public: ValueToString(audio_backend == "HLE")); } + if (netplay_host.has_value()) + { + const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); + const bool is_traversal = traversal_choice == "traversal"; + + Config::Location config = is_traversal ? Config::NETPLAY_TRAVERSAL_PORT.GetLocation() : + Config::NETPLAY_HOST_PORT.GetLocation(); + + m_values.emplace_back(config, ValueToString(netplay_host.value())); + } + + if (!netplay_join.empty()) + { + std::vector join_parts = SplitString(netplay_join, ':'); + if (join_parts.size() < 2) + { + // The user is submitting a host code + const std::string& host_code = join_parts[0]; + m_values.emplace_back(Config::NETPLAY_TRAVERSAL_CHOICE.GetLocation(), "traversal"); + m_values.emplace_back(Config::NETPLAY_HOST_CODE.GetLocation(), host_code); + } + else + { + // Ther user is submitting an IP address + const std::string& host = join_parts[0]; + const std::string& port_str = join_parts[1]; + if (!std::all_of(port_str.begin(), port_str.end(), ::isdigit) || port_str.length() > 5) + { + fmt::println(std::cerr, "Error: the port must be a number between 0-{}.", + std::numeric_limits::max()); + std::exit(EXIT_FAILURE); + } + + const u64 port = std::stoul(port_str); + if (port > std::numeric_limits::max() || port < 1) + { + fmt::println(std::cerr, "Error: the port must be a number between 0-{}.", + std::numeric_limits::max()); + std::exit(EXIT_FAILURE); + } + + const u16 cast_port = static_cast(port); + + m_values.emplace_back(Config::NETPLAY_ADDRESS.GetLocation(), host); + m_values.emplace_back(Config::NETPLAY_CONNECT_PORT.GetLocation(), ValueToString(cast_port)); + } + } + // Batch mode hides the main window, and render to main hides the render window. To avoid a // situation where we would have no window at all, disable render to main when using batch mode. if (batch) @@ -116,6 +168,16 @@ std::unique_ptr CreateParser(ParserOptions options) .action("store_true") .help("Run Dolphin without the user interface (Requires --exec or --nand-title)"); parser->add_option("-c", "--confirm").action("store_true").help("Set Confirm on Stop"); + parser->add_option("--netplay_host") + .action("store") + .metavar("") + .type("int") + .help("Host a netplay session on the specified port (Requires --exec)"); + parser->add_option("--netplay_join") + .action("store") + .metavar(" OR ") + .type("string") + .help("Join a netplay session at the specified IP address and port or using a host code"); } parser->set_defaults("video_backend", ""); @@ -137,6 +199,10 @@ static void AddConfigLayer(const optparse::Values& options) Config::AddLayer(std::make_unique( std::move(config_args), static_cast(options.get("video_backend")), static_cast(options.get("audio_emulation")), + options.is_set("netplay_host") ? + std::optional(static_cast(options.get("netplay_host"))) : + std::nullopt, + static_cast(options.get("netplay_join")), static_cast(options.get("batch")), static_cast(options.get("debugger")))); }