diff --git a/docs/user/CommandLine.md b/docs/user/CommandLine.md index 443ea5b34c..d38504937b 100644 --- a/docs/user/CommandLine.md +++ b/docs/user/CommandLine.md @@ -1,30 +1,23 @@ # User Handbook - Command Line -There are two main applications, an SDL-based app (`eden-cli`) and a Qt based app (`eden`); both accept command line arguments. +There are two main applications, an SDL-based app (`eden-cli`) and a Qt based app (`eden`); both accept command line arguments and share almost the same accepted arguments. ## eden - `./eden `: Running with a single argument and nothing else, will make the emulator look for the given file and load it, this behaviour is similar to `eden-cli`; allows dragging and dropping games into the application. -- `-g `: Alternate way to specify what to load, overrides. However let it be noted that arguments that use `-` will be treated as options/ignored, if your game, for some reason, starts with `-`, in order to safely handle it you may need to specify it as an argument. -- `-f`: Use fullscreen. -- `-u `: Select the index of the user to load as. -- `-input-profile `: Specifies input profile name to use (for player #0 only). -- `-qlaunch`: Launch QLaunch. -- `-hlaunch`: Launch homebrew launcher `nx-hbloader`. +- `--hlaunch`: Launch homebrew launcher `nx-hbloader`. - Requires a copy of Atmosphere to be extracted onto `sdmc`. - This is a shorthand for `/sdmc/atmosphere/hbl.nsp`. -- `-setup`: Launch setup applet. - -## eden-cli - +- `--setup`: Launch setup applet. +- `-q/--qlaunch`: Launch QLaunch. - `-d/--debug`: Enter debug mode, allow gdb stub at port `1234` - `-c/--config`: Specify alternate configuration file. - `-f/--fullscreen`: Set fullscreen. - `-h/--help`: Display help. -- `-g/--game`: Specify the game to run. +- `-g/--game`: Alternate way to specify what to load, overrides. However let it be noted that arguments that use `-` will be treated as options/ignored, if your game, for some reason, starts with `-`, in order to safely handle it you may need to specify it as an argument. - `-m/--multiplayer`: Specify multiplayer options. - `-p/--program`: Specify the program arguments to pass (optional). -- `-u/--user`: Specify the user index. +- `-u/--user`: Select the index of the user to load as. - `-v/--version`: Display version and quit. - `-i/--input-profile`: Specifies input profile name to use (for player #0 only). - `-n/--null-render`: Forces the usage of the "Null" render backend irrespective of settings. @@ -50,3 +43,5 @@ Room settings: - `-v/--version`: Output version information and exit., If the name and description of the room is specified, then it will default to headless mode if not already specified. + +Old settings `-hlaunch`, `-qlaunch` and `-setup` are recognized independently. They're kept for backwards compatibility with shortcuts made before the change. Using one of these makes the parser immediately halt and ignore every other option. diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 10e8cd0cc7..94a86aa0a5 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -133,6 +133,7 @@ add_library( typed_address.h uint128.h unique_function.h + program_args.h random.cpp random.h uuid.cpp diff --git a/src/common/program_args.cpp b/src/common/program_args.cpp new file mode 100644 index 0000000000..8ee2211da7 --- /dev/null +++ b/src/common/program_args.cpp @@ -0,0 +1,278 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include "common/assert.h" +#include "common/program_args.h" +#include "common/logging.h" +#include "common/scm_rev.h" +#include "network/room.h" + +#undef _UNICODE +#include +#ifndef _MSC_VER +#include +#endif + +namespace Common { + +static void PrintHelp(const char* argv0) { + LOG_INFO(Frontend, "Usage: {} [options] \n" + "Core options:\n" + "-c, --config Load the specified configuration file\n" + "-f, --fullscreen Start in fullscreen mode\n" + "-g, --game File path of the game to load\n" + "-h, --help Display this help and exit\n" + "-m, --multiplayer=nick:password@address:port Nickname, password, address and port for multiplayer\n" + "-p, --program Pass following string as arguments to executable\n" + "-u, --user Select a specific user profile from 0 to 7\n" + "-d, --debug Run the GDB stub on a port from 1 to 65535\n" + "-i, --input-profile Specifies input profile name to use (for player #0 only)\n" + "-n, --null-render Forces the usage of the \"Null\" render backend irrespective of settings\n" + "-x, --filter Sets the debug log filter irrespective of settings\n" + "-s, --singlecore Forces single-core regardless of settings\n" + "Shared options:\n" + "-l, --log-file The file for storing the room log\n" + "-H, --headless Force headless mode (no GUI). Currently only used for rooms\n" +#ifdef YUZU_ROOM + "Room options:\n" + "-N, --name The name of the room\n" + "-D, --description The room description\n" + "-S, --bind-address The bind address for the room\n" + "-P, --port The port used for the room\n" + "-M, --max-members The maximum number of players for this room\n" + "-W, --password The password for the room\n" + "-G, --preferred-game The preferred game for this room\n" + "-I, --preferred-game-id The preferred game-id for this room\n" + "-U, --username The username used for announce\n" + "-T, --token The token used for announce\n" + "-A, --web-api-url yuzu Web API url\n" + "-B, --ban-list-file The file for storing the room ban list\n" +#endif + "Misc. options:\n" + "-h, --help Display this help and exit\n" + "-v, --version Output version information and exit\n", + argv0); +} + +static void PrintVersion() { + LOG_INFO(Frontend, "Eden {} {}, Libnetwork: {}", Common::g_scm_branch, Common::g_scm_desc, Network::network_version); +} + +int ParseArguments(ProgramArguments& args, int argc, char *argv[]) { + int option_index = 0; +#ifdef _WIN32 + int argc_w; + auto argv_w = CommandLineToArgvW(GetCommandLineW(), &argc_w); + if (argv_w == nullptr) { + LOG_CRITICAL(Frontend, "Failed to get command line arguments"); + return SDL_APP_FAILURE; + } +#endif + + static struct option long_options[] = { + // clang-format off + {"debug", no_argument, 0, 'd'}, + {"config", required_argument, 0, 'c'}, + {"fullscreen", no_argument, 0, 'f'}, + {"help", no_argument, 0, 'h'}, + {"game", required_argument, 0, 'g'}, + {"multiplayer", required_argument, 0, 'm'}, + {"program", optional_argument, 0, 'p'}, + {"user", required_argument, 0, 'u'}, + {"version", no_argument, 0, 'v'}, + {"input-profile", no_argument, 0, 'i'}, + {"null-render", no_argument, 0, 'n'}, + {"singlecore", no_argument, 0, 's'}, + {"filter", no_argument, 0, 'x'}, + + {"log-file", required_argument, 0, 'l'}, + {"headless", required_argument, 0, 'H'}, +#ifdef YUZU_ROOM + {"room-name", required_argument, 0, 'N'}, + {"room-description", required_argument, 0, 'D'}, + {"bind-address", required_argument, 0, 'S'}, + {"port", required_argument, 0, 'P'}, + {"max-members", required_argument, 0, 'M'}, + {"password", required_argument, 0, 'W'}, + {"preferred-game", required_argument, 0, 'G'}, + {"preferred-game-id", required_argument, 0, 'I'}, + {"username", optional_argument, 0, 'U'}, + {"token", required_argument, 0, 'T'}, + {"web-api-url", required_argument, 0, 'A'}, + {"ban-list-file", required_argument, 0, 'B'}, + // Entry option + {"room", no_argument, 0, 0}, +#endif + {"hlaunch", no_argument, 0, 500}, + {"qlaunch", no_argument, 0, 'q'}, + {"setup", no_argument, 0, 502}, + {0, 0, 0, 0}, + // clang-format on + }; + + // Kept for compatibility! + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-hlaunch") == 0) { + args.should_launch_hlaunch = true; + return 0; + } else if (strcmp(argv[i], "-qlaunch") == 0) { + args.should_launch_qlaunch = true; + return 0; + } else if (strcmp(argv[i], "-setup") == 0) { + args.should_launch_setup = true; + return 0; + } + } + + // Preserves drag and drop functionality (i.e ./eden ) + char *endarg = nullptr; + while (optind < argc) { + int arg = getopt_long(argc, argv, "g:fhvcip::c:u:d:", long_options, &option_index); + if (arg != -1) { + switch (arg) { + case 'd': + args.override_gdb_port = uint16_t(atoi(optarg)); + break; + case 'c': + args.config_path = optarg; + break; + case 'f': + args.fullscreen = true; + LOG_INFO(Frontend, "Starting in fullscreen mode..."); + break; + case 'h': + PrintHelp(argv[0]); + return 0; + case 'g': + args.filepath = std::string(optarg); + break; + case 'i': { + args.input_profile = std::string(optarg); + break; + } + case 'm': { + args.use_multiplayer = true; + const std::string str_arg(optarg); + // regex to check if the format is nickname:password@ip:port + // with optional :password + const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$"); + if (!std::regex_match(str_arg, re)) { + LOG_ERROR(Frontend, "Wrong format for option --multiplayer"); + return -1; + } else { + std::smatch match; + std::regex_search(str_arg, match, re); + ASSERT(match.size() == 5); + args.nickname = match[1]; + args.password = match[2]; + args.address = match[3]; + if (!match[4].str().empty()) { + args.port = u16(std::strtoul(match[4].str().c_str(), nullptr, 0)); + } + std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$"); + if (!std::regex_match(args.nickname, nickname_re)) { + LOG_ERROR(Frontend, "Nickname is not valid. Must be 4 to 20 alphanumeric characters"); + return -1; + } else { + if (args.address.empty()) { + LOG_ERROR(Frontend, "Address to room must not be empty"); + return -1; + } + } + } + break; + } + case 'p': + args.program_args.assign(optarg); + break; + case 'u': + args.selected_user = atoi(optarg); + break; + case 'v': + PrintVersion(); + break; + case 'n': + args.force_null_render = true; + break; + case 's': + args.force_single_core = true; + break; + case 'x': + args.log_filter.emplace(optarg); + break; + // shared + case 'l': + args.log_file.assign(optarg); + break; + case 'H': + args.headless.emplace(true); + break; + // room +#ifdef YUZU_ROOM + case 'N': + args.room_name.assign(optarg); + break; + case 'D': + args.room_description.assign(optarg); + break; + case 'S': + args.bind_address.assign(optarg); + break; + case 'P': { + auto const value = strtoul(optarg, &endarg, 0); + if (value <= USHRT_MAX) { + args.port = value; + } else { + LOG_ERROR(Frontend, "port must be between 0-{}", USHRT_MAX); + } + break; + } + case 'M': { + auto const value = strtoul(optarg, &endarg, 0); + if (value >= 2 && value <= Network::MaxConcurrentConnections) { + args.max_members = value; + } else { + LOG_ERROR(Frontend, "max members must be between 2-{}", value, Network::MaxConcurrentConnections); + } + break; + } + case 'W': + args.password.assign(optarg); + break; + case 'G': + args.preferred_game.assign(optarg); + break; + case 'I': + args.preferred_game_id = strtoull(optarg, &endarg, 16); + break; + case 'U': + args.nickname.assign(optarg); + break; + case 'T': + args.token.assign(optarg); + break; + case 'A': + args.web_api_url.assign(optarg); + break; + case 'B': + args.ban_list_file.assign(optarg); + break; +#endif + } + } else { +#ifdef _WIN32 + args.filepath = Common::UTF16ToUTF8(argv_w[optind]); +#else + args.filepath = argv[optind]; +#endif + optind++; + } + } +#ifdef _WIN32 + LocalFree(argv_w); +#endif + return 0; +} + +} diff --git a/src/common/program_args.h b/src/common/program_args.h new file mode 100644 index 0000000000..fbe78b9da1 --- /dev/null +++ b/src/common/program_args.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include "common/common_types.h" + +namespace Common { + +inline constexpr u16 DEFAULT_ROOM_PORT = 24872; +struct ProgramArguments { + std::optional config_path{}; + std::optional log_filter{}; + std::string nickname{}; + std::string password{}; + std::string address{}; + std::string input_profile{}; + std::string filepath{}; + std::string program_args{}; + std::string room_name{}; + std::string room_description{}; + std::string preferred_game{}; + std::string username{}; + std::string token{}; + std::string web_api_url{}; + std::string ban_list_file{}; + std::string log_file = "eden-room.log"; + std::string bind_address{}; + std::optional selected_user{}; + std::optional override_gdb_port{}; + std::optional headless{}; + u64 preferred_game_id = 0; + u32 max_members = 16; + u16 port = DEFAULT_ROOM_PORT; + bool use_multiplayer = false; + bool fullscreen = false; + bool force_null_render = false; + bool force_single_core = false; + + bool should_launch_qlaunch = false; + bool should_launch_hlaunch = false; + bool should_launch_setup = false; +}; + +int ParseArguments(ProgramArguments& args, int argc, char *argv[]); + +} diff --git a/src/network/room.cpp b/src/network/room.cpp index 2069673bae..aa20324987 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -6,18 +6,31 @@ #include #include #include +#include #include #include #include #include #include #include +#include + +#include + +#include "common/fs/file.h" #include "common/polyfill_thread.h" #include "common/logging.h" +#include "common/settings.h" +#include "common/string_util.h" #include "enet/enet.h" +#include "network/announce_multiplayer_session.h" #include "network/packet.h" #include "network/room.h" +#include "network/network.h" #include "network/verify_user.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/verify_user_jwt.h" +#endif namespace Network { @@ -1142,4 +1155,201 @@ void Room::Destroy() { room_impl->room_information.name.clear(); } +#ifdef YUZU_ROOM +/// The magic text at the beginning of a yuzu-room ban list file. +static constexpr char BAN_LIST_MAGIC[] = "YuzuRoom-BanList-1"; +static constexpr char TOKEN_DELIMITER{':'}; + +static void PadToken(std::string& token) { + std::array output{}; + std::array roundtrip{}; + for (size_t i = 0; i < 3; i++) { + EVP_DecodeBlock(output.data(), reinterpret_cast(token.c_str()), token.size()); + EVP_EncodeBlock(output.data(), roundtrip.data(), roundtrip.size()); + if (memcmp(roundtrip.data(), token.data(), token.size()) == 0) { + break; + } + token.push_back('='); + } +} + +static std::string UsernameFromDisplayToken(const std::string& display_token) { + std::size_t outlen = 4 * ((display_token.length() + 2) / 3); + std::array output{}; + EVP_DecodeBlock(output.data(), reinterpret_cast(display_token.c_str()), display_token.length()); + std::string decoded_display_token(reinterpret_cast(&output), outlen); + return decoded_display_token.substr(0, decoded_display_token.find(TOKEN_DELIMITER)); +} + +static std::string TokenFromDisplayToken(const std::string& display_token) { + std::size_t outlen = 4 * ((display_token.length() + 2) / 3); + std::array output{}; + EVP_DecodeBlock(output.data(), reinterpret_cast(display_token.c_str()), display_token.length()); + std::string decoded_display_token(reinterpret_cast(&output), outlen); + return decoded_display_token.substr(decoded_display_token.find(TOKEN_DELIMITER) + 1); +} + +static Network::Room::BanList LoadBanList(const std::string& path) { + std::ifstream file; + Common::FS::OpenFileStream(file, path, std::ios_base::in); + if (!file || file.eof()) { + LOG_ERROR(Network, "Could not open ban list!"); + return {}; + } + std::string magic; + std::getline(file, magic); + if (magic != BAN_LIST_MAGIC) { + LOG_ERROR(Network, "Ban list is not valid!"); + return {}; + } + + // false = username ban list, true = ip ban list + bool ban_list_type = false; + Network::Room::UsernameBanList username_ban_list; + Network::Room::IPBanList ip_ban_list; + while (!file.eof()) { + std::string line; + std::getline(file, line); + line.erase(std::remove(line.begin(), line.end(), '\0'), line.end()); + line = Common::StripSpaces(line); + if (line.empty()) { + // An empty line marks start of the IP ban list + ban_list_type = true; + continue; + } + if (ban_list_type) { + ip_ban_list.emplace_back(line); + } else { + username_ban_list.emplace_back(line); + } + } + return {username_ban_list, ip_ban_list}; +} + +static void SaveBanList(const Network::Room::BanList& ban_list, const std::string& path) { + std::ofstream file; + Common::FS::OpenFileStream(file, path, std::ios_base::out); + if (!file) { + LOG_ERROR(Network, "Could not save ban list!"); + return; + } + file << BAN_LIST_MAGIC << "\n"; + // Username ban list + for (const auto& username : ban_list.first) + file << username << "\n"; + file << "\n"; + // IP ban list + for (const auto& ip : ban_list.second) + file << ip << "\n"; +} + +int LaunchRoomLoopWithArguments(Common::ProgramArguments& args) { + if (args.room_name.empty()) { + LOG_ERROR(Network, "Room name is empty!"); + return -1; + } + if (args.preferred_game.empty()) { + LOG_ERROR(Network, "Preferred game is empty!"); + return -1; + } + if (args.preferred_game_id == 0) { + LOG_WARNING(Network, + "preferred-game-id not set!\n" + "This should get set to allow users to find your room.\n" + "Set with --preferred-game-id id"); + } + if (args.bind_address.empty()) { + LOG_INFO(Network, "Bind address is empty: defaulting to 0.0.0.0"); + } + if (args.ban_list_file.empty()) { + LOG_WARNING(Network, + "Ban list file not set!\n" + "This should get set to load and save room ban list.\n" + "Set with --ban-list-file "); + } + bool announce = true; + if (args.token.empty() && announce) { + announce = false; + LOG_INFO(Network, "Token is empty: Hosting a private room"); + } + if (args.web_api_url.empty() && announce) { + announce = false; + LOG_INFO(Network, "Endpoint url is empty: Hosting a private room"); + } + if (announce) { + if (args.username.empty()) { + LOG_INFO(Network, "Hosting a public room"); + Settings::values.web_api_url = args.web_api_url; + PadToken(args.token); + Settings::values.eden_username = UsernameFromDisplayToken(args.token); + args.username = Settings::values.eden_username.GetValue(); + Settings::values.eden_token = TokenFromDisplayToken(args.token); + } else { + LOG_INFO(Network, "Hosting a public room"); + Settings::values.web_api_url = args.web_api_url; + Settings::values.eden_username = args.username; + Settings::values.eden_token = args.token; + } + } + + // Load the ban list + Network::Room::BanList ban_list; + if (!args.ban_list_file.empty()) { + ban_list = LoadBanList(args.ban_list_file); + } + + std::unique_ptr verify_backend; + if (announce) { +#ifdef ENABLE_WEB_SERVICE + verify_backend = + std::make_unique(Settings::values.web_api_url.GetValue()); +#else + LOG_INFO(Network, + "Eden Web Services is not available with this build: validation is disabled."); + verify_backend = std::make_unique(); +#endif + } else { + verify_backend = std::make_unique(); + } + + Network::Init(); + if (auto room = Network::GetRoom().lock()) { + AnnounceMultiplayerRoom::GameInfo preferred_game_info{ + .name = args.preferred_game, + .id = args.preferred_game_id + }; + if (!room->Create(args.room_name, args.room_description, args.bind_address, u16(args.port), + args.password, args.max_members, args.username, preferred_game_info, + std::move(verify_backend), ban_list)) { + LOG_INFO(Network, "Failed to create room: "); + return -1; + } + LOG_INFO(Network, "Room is open. Close with Q+Enter..."); + auto announce_session = std::make_unique(); + if (announce) { + announce_session->Start(); + } + while (room->GetState() == Network::Room::State::Open) { + std::string in; + std::cin >> in; + if (in.size() > 0) { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + if (announce) { + announce_session->Stop(); + } + announce_session.reset(); + // Save the ban list + if (!args.ban_list_file.empty()) { + SaveBanList(room->GetBanList(), args.ban_list_file); + } + room->Destroy(); + } + Network::Shutdown(); + return 0; +} +#endif + } // namespace Network diff --git a/src/network/room.h b/src/network/room.h index af7a30c6f6..1591cca2f2 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project @@ -10,6 +10,7 @@ #include #include #include +#include "common/program_args.h" #include "common/announce_multiplayer_room.h" #include "common/common_types.h" #include "common/socket_types.h" @@ -148,4 +149,6 @@ private: std::unique_ptr room_impl; }; +int LaunchRoomLoopWithArguments(Common::ProgramArguments& args); + } // namespace Network diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 353d358a82..10ff456502 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include +#include "common/program_args.h" #include "startup_checks.h" #ifdef __unix__ @@ -182,7 +183,10 @@ int main(int argc, char* argv[]) { // generating shaders setlocale(LC_ALL, "C"); - MainWindow main_window{has_broken_vulkan}; + Common::ProgramArguments args{}; + Common::ParseArguments(args, argc, argv); + + MainWindow main_window{std::move(args), has_broken_vulkan}; // After settings have been loaded by GMainWindow, apply the filter main_window.show(); diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index f18a989255..4d47176ca0 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -350,7 +350,7 @@ inline static bool isDarkMode() { } #endif // _WIN32 -MainWindow::MainWindow(bool has_broken_vulkan) +MainWindow::MainWindow(Common::ProgramArguments&& opts, bool has_broken_vulkan) : ui{std::make_unique()}, input_subsystem{std::make_shared()}, user_data_migrator{this} { QtCommon::Init(this); @@ -512,73 +512,33 @@ MainWindow::MainWindow(bool has_broken_vulkan) return; } - QString game_path; - bool should_launch_qlaunch = false; - bool should_launch_hlaunch = false; - bool should_launch_setup = false; - bool has_gamepath = false; - bool is_fullscreen = false; - - // Preserves drag/drop functionality - for (int i = 1; i < args.size(); ++i) { - if (args[i] == QStringLiteral("-f")) { - // Launch game in fullscreen mode - is_fullscreen = true; - } else if (args[i] == QStringLiteral("-u") && i < args.size() - 1) { - // Launch game with a specific user - int user_arg_idx = ++i; - bool argument_ok; - std::size_t selected_user = args[user_arg_idx].toUInt(&argument_ok); - if (!argument_ok) { - // try to look it up by username, only finds the first username that matches. - std::string const user_arg_str = args[user_arg_idx].toStdString(); - auto const user_idx = - QtCommon::system->GetProfileManager().GetUserIndex(user_arg_str); - if (user_idx != std::nullopt) { - selected_user = user_idx.value(); - } else { - LOG_ERROR(Frontend, "Invalid user argument '{}'", user_arg_str); - continue; - } - } - if (QtCommon::system->GetProfileManager().UserExistsIndex(selected_user)) { - Settings::values.current_user = s32(selected_user); - user_flag_cmd_line = true; - } else { - LOG_ERROR(Frontend, "Selected user {} doesn't exist", selected_user); - } - } else if (args[i] == QStringLiteral("-g") && i < args.size() - 1) { - // Launch game at path - game_path = args[++i]; - has_gamepath = true; - } else if (args[i] == QStringLiteral("-input-profile") && i < args.size() - 1) { - auto& players = Settings::values.players.GetValue(); - players[0].profile_name = args[++i].toStdString(); - } else if (args[i] == QStringLiteral("-qlaunch")) { - should_launch_qlaunch = true; - } else if (args[i] == QStringLiteral("-hlaunch")) { - should_launch_hlaunch = true; - } else if (args[i] == QStringLiteral("-setup")) { - should_launch_setup = true; + if (opts.selected_user) { + auto user_index = *opts.selected_user; + if (QtCommon::system->GetProfileManager().UserExistsIndex(user_index)) { + Settings::values.current_user = s32(user_index); + user_flag_cmd_line = true; } else { - game_path = args[i]; - has_gamepath = true; + LOG_ERROR(Frontend, "Selected user {} doesn't exist", user_index); } } - // Override fullscreen setting if gamepath or argument is provided - if (has_gamepath || is_fullscreen) { - ui->action_Fullscreen->setChecked(is_fullscreen); + if (!opts.input_profile.empty()) { + Settings::values.players.GetValue()[0].profile_name = opts.input_profile; } - if (should_launch_setup) { + // Override fullscreen setting if gamepath or argument is provided + if (!opts.filepath.empty() && opts.fullscreen) { + ui->action_Fullscreen->setChecked(opts.fullscreen); + } + + if (opts.should_launch_setup) { LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::Starter), std::nullopt); } else { - if (!game_path.isEmpty()) { - BootGame(game_path, ApplicationAppletParameters()); - } else if (should_launch_qlaunch) { + if (!opts.filepath.empty()) { + BootGame(QString::fromStdString(opts.filepath), ApplicationAppletParameters()); + } else if (opts.should_launch_qlaunch) { LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::QLaunch), std::nullopt); - } else if (should_launch_hlaunch) { + } else if (opts.should_launch_hlaunch) { std::filesystem::path const sd_dir = Common::FS::GetEdenPathString(Common::FS::EdenPath::SDMCDir); auto const hbl_path = (sd_dir / "atmosphere" / "hbl.nsp").string(); diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index 2b50e3af91..2d32d1f635 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -17,6 +17,7 @@ #include #include +#include "common/program_args.h" #include "common/common_types.h" #include "common/settings_enums.h" #include "frontend_common/content_manager.h" @@ -165,7 +166,7 @@ class MainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); - explicit MainWindow(bool has_broken_vulkan); + explicit MainWindow(Common::ProgramArguments&& args, bool has_broken_vulkan); ~MainWindow() override; bool DropAction(QDropEvent* event); diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 27dd5c1e40..f1549ec0e7 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -11,6 +11,7 @@ #include #include #include "common/fs/file.h" +#include "common/program_args.h" #include "common/settings_enums.h" #ifdef __EMSCRIPTEN__ #include @@ -39,9 +40,6 @@ #include "network/network.h" #include "network/room.h" #include "network/verify_user.h" -#ifdef ENABLE_WEB_SERVICE -#include "web_service/verify_user_jwt.h" -#endif #include "yuzu_cmd/sdl_config.h" #include "video_core/renderer_base.h" @@ -75,47 +73,6 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; } #endif -static void PrintHelp(const char* argv0) { - LOG_INFO(Frontend, "Usage: {} [options] \n" - "Core options:\n" - "-c, --config Load the specified configuration file\n" - "-f, --fullscreen Start in fullscreen mode\n" - "-g, --game File path of the game to load\n" - "-h, --help Display this help and exit\n" - "-m, --multiplayer=nick:password@address:port Nickname, password, address and port for multiplayer\n" - "-p, --program Pass following string as arguments to executable\n" - "-u, --user Select a specific user profile from 0 to 7\n" - "-d, --debug Run the GDB stub on a port from 1 to 65535\n" - "-i, --input-profile Specifies input profile name to use (for player #0 only)\n" - "-n, --null-render Forces the usage of the \"Null\" render backend irrespective of settings\n" - "-x, --filter Sets the debug log filter irrespective of settings\n" - "-s, --singlecore Forces single-core regardless of settings\n" - "Shared options:\n" - "-l, --log-file The file for storing the room log\n" - "-H, --headless Force headless mode (no GUI). Currently only used for rooms\n" - "Room options:\n" - "-N, --name The name of the room\n" - "-D, --description The room description\n" - "-S, --bind-address The bind address for the room\n" - "-P, --port The port used for the room\n" - "-M, --max-members The maximum number of players for this room\n" - "-W, --password The password for the room\n" - "-G, --preferred-game The preferred game for this room\n" - "-I, --preferred-game-id The preferred game-id for this room\n" - "-U, --username The username used for announce\n" - "-T, --token The token used for announce\n" - "-A, --web-api-url yuzu Web API url\n" - "-B, --ban-list-file The file for storing the room ban list\n" - "Misc. options:\n" - "-h, --help Display this help and exit\n" - "-v, --version Output version information and exit\n", - argv0); -} - -static void PrintVersion() { - LOG_INFO(Frontend, "Eden {} {}, Libnetwork: {}", Common::g_scm_branch, Common::g_scm_desc, Network::network_version); -} - static void OnStateChanged(const Network::RoomMember::State& state) { switch (state) { case Network::RoomMember::State::Idle: @@ -215,35 +172,7 @@ struct SdlState { Core::System system{}; std::unique_ptr emu_window; // settings - struct { - std::optional config_path{}; - std::optional log_filter{}; - std::string nickname{}; - std::string password{}; - std::string address{}; - std::string input_profile{}; - std::string filepath{}; - std::string program_args{}; - std::string room_name{}; - std::string room_description{}; - std::string preferred_game{}; - std::string username{}; - std::string token{}; - std::string web_api_url{}; - std::string ban_list_file{}; - std::string log_file = "eden-room.log"; - std::string bind_address{}; - std::optional selected_user{}; - std::optional override_gdb_port{}; - std::optional headless{}; - u64 preferred_game_id = 0; - u32 max_members = 16; - u16 port = Network::DefaultRoomPort; - bool use_multiplayer = false; - bool fullscreen = false; - bool force_null_render = false; - bool force_single_core = false; - } opts = {}; + Common::ProgramArguments opts = {}; }; static SDL_AppResult ExecuteWithGUI(SdlState& state) { @@ -281,10 +210,6 @@ static SDL_AppResult ExecuteWithGUI(SdlState& state) { Settings::values.renderer_backend = Settings::RendererBackend::Null; } -#ifdef _WIN32 - LocalFree(argv_w); -#endif - if (state.opts.filepath.empty()) { LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified"); return SDL_APP_FAILURE; @@ -388,207 +313,6 @@ static SDL_AppResult ExecuteWithGUI(SdlState& state) { return SDL_APP_SUCCESS; } -/// The magic text at the beginning of a yuzu-room ban list file. -static constexpr char BAN_LIST_MAGIC[] = "YuzuRoom-BanList-1"; -static constexpr char TOKEN_DELIMITER{':'}; - -static void PadToken(std::string& token) { - std::array output{}; - std::array roundtrip{}; - for (size_t i = 0; i < 3; i++) { - EVP_DecodeBlock(output.data(), reinterpret_cast(token.c_str()), token.size()); - EVP_EncodeBlock(output.data(), roundtrip.data(), roundtrip.size()); - if (memcmp(roundtrip.data(), token.data(), token.size()) == 0) { - break; - } - token.push_back('='); - } -} - -static std::string UsernameFromDisplayToken(const std::string& display_token) { - std::size_t outlen = 4 * ((display_token.length() + 2) / 3); - std::array output{}; - EVP_DecodeBlock(output.data(), reinterpret_cast(display_token.c_str()), display_token.length()); - std::string decoded_display_token(reinterpret_cast(&output), outlen); - return decoded_display_token.substr(0, decoded_display_token.find(TOKEN_DELIMITER)); -} - -static std::string TokenFromDisplayToken(const std::string& display_token) { - std::size_t outlen = 4 * ((display_token.length() + 2) / 3); - std::array output{}; - EVP_DecodeBlock(output.data(), reinterpret_cast(display_token.c_str()), display_token.length()); - std::string decoded_display_token(reinterpret_cast(&output), outlen); - return decoded_display_token.substr(decoded_display_token.find(TOKEN_DELIMITER) + 1); -} - -static Network::Room::BanList LoadBanList(const std::string& path) { - std::ifstream file; - Common::FS::OpenFileStream(file, path, std::ios_base::in); - if (!file || file.eof()) { - LOG_ERROR(Network, "Could not open ban list!"); - return {}; - } - std::string magic; - std::getline(file, magic); - if (magic != BAN_LIST_MAGIC) { - LOG_ERROR(Network, "Ban list is not valid!"); - return {}; - } - - // false = username ban list, true = ip ban list - bool ban_list_type = false; - Network::Room::UsernameBanList username_ban_list; - Network::Room::IPBanList ip_ban_list; - while (!file.eof()) { - std::string line; - std::getline(file, line); - line.erase(std::remove(line.begin(), line.end(), '\0'), line.end()); - line = Common::StripSpaces(line); - if (line.empty()) { - // An empty line marks start of the IP ban list - ban_list_type = true; - continue; - } - if (ban_list_type) { - ip_ban_list.emplace_back(line); - } else { - username_ban_list.emplace_back(line); - } - } - - return {username_ban_list, ip_ban_list}; -} - -static void SaveBanList(const Network::Room::BanList& ban_list, const std::string& path) { - std::ofstream file; - Common::FS::OpenFileStream(file, path, std::ios_base::out); - if (!file) { - LOG_ERROR(Network, "Could not save ban list!"); - return; - } - - file << BAN_LIST_MAGIC << "\n"; - - // Username ban list - for (const auto& username : ban_list.first) { - file << username << "\n"; - } - file << "\n"; - - // IP ban list - for (const auto& ip : ban_list.second) { - file << ip << "\n"; - } -} - -static SDL_AppResult ExecuteHeadlessRoom(SdlState& state) { - if (state.opts.room_name.empty()) { - LOG_ERROR(Network, "Room name is empty!"); - return SDL_APP_FAILURE; - } - if (state.opts.preferred_game.empty()) { - LOG_ERROR(Network, "Preferred game is empty!"); - return SDL_APP_FAILURE; - } - if (state.opts.preferred_game_id == 0) { - LOG_WARNING(Network, - "preferred-game-id not set!\n" - "This should get set to allow users to find your room.\n" - "Set with --preferred-game-id id"); - } - if (state.opts.bind_address.empty()) { - LOG_INFO(Network, "Bind address is empty: defaulting to 0.0.0.0"); - } - if (state.opts.ban_list_file.empty()) { - LOG_WARNING(Network, - "Ban list file not set!\n" - "This should get set to load and save room ban list.\n" - "Set with --ban-list-file "); - } - bool announce = true; - if (state.opts.token.empty() && announce) { - announce = false; - LOG_INFO(Network, "Token is empty: Hosting a private room"); - } - if (state.opts.web_api_url.empty() && announce) { - announce = false; - LOG_INFO(Network, "Endpoint url is empty: Hosting a private room"); - } - if (announce) { - if (state.opts.username.empty()) { - LOG_INFO(Network, "Hosting a public room"); - Settings::values.web_api_url = state.opts.web_api_url; - PadToken(state.opts.token); - Settings::values.eden_username = UsernameFromDisplayToken(state.opts.token); - state.opts.username = Settings::values.eden_username.GetValue(); - Settings::values.eden_token = TokenFromDisplayToken(state.opts.token); - } else { - LOG_INFO(Network, "Hosting a public room"); - Settings::values.web_api_url = state.opts.web_api_url; - Settings::values.eden_username = state.opts.username; - Settings::values.eden_token = state.opts.token; - } - } - - // Load the ban list - Network::Room::BanList ban_list; - if (!state.opts.ban_list_file.empty()) { - ban_list = LoadBanList(state.opts.ban_list_file); - } - - std::unique_ptr verify_backend; - if (announce) { -#ifdef ENABLE_WEB_SERVICE - verify_backend = - std::make_unique(Settings::values.web_api_url.GetValue()); -#else - LOG_INFO(Network, - "Eden Web Services is not available with this build: validation is disabled."); - verify_backend = std::make_unique(); -#endif - } else { - verify_backend = std::make_unique(); - } - - Network::Init(); - if (auto room = Network::GetRoom().lock()) { - AnnounceMultiplayerRoom::GameInfo preferred_game_info{ - .name = state.opts.preferred_game, - .id = state.opts.preferred_game_id - }; - if (!room->Create(state.opts.room_name, state.opts.room_description, state.opts.bind_address, u16(state.opts.port), - state.opts.password, state.opts.max_members, state.opts.username, preferred_game_info, - std::move(verify_backend), ban_list)) { - LOG_INFO(Network, "Failed to create room: "); - std::exit(-1); - } - LOG_INFO(Network, "Room is open. Close with Q+Enter..."); - auto announce_session = std::make_unique(); - if (announce) { - announce_session->Start(); - } - while (room->GetState() == Network::Room::State::Open) { - std::string in; - std::cin >> in; - if (in.size() > 0) { - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - if (announce) { - announce_session->Stop(); - } - announce_session.reset(); - // Save the ban list - if (!state.opts.ban_list_file.empty()) { - SaveBanList(room->GetBanList(), state.opts.ban_list_file); - } - room->Destroy(); - } - Network::Shutdown(); - return SDL_APP_FAILURE; -} - extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { SdlState* state = new SdlState(); @@ -603,198 +327,11 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { Common::Log::SetColorConsoleBackendEnabled(true); Common::Log::Start(); - int option_index = 0; -#ifdef _WIN32 - int argc_w; - auto argv_w = CommandLineToArgvW(GetCommandLineW(), &argc_w); - if (argv_w == nullptr) { - LOG_CRITICAL(Frontend, "Failed to get command line arguments"); - return SDL_APP_FAILURE; - } -#endif - - static struct option long_options[] = { - // clang-format off - {"debug", no_argument, 0, 'd'}, - {"config", required_argument, 0, 'c'}, - {"fullscreen", no_argument, 0, 'f'}, - {"help", no_argument, 0, 'h'}, - {"game", required_argument, 0, 'g'}, - {"multiplayer", required_argument, 0, 'm'}, - {"program", optional_argument, 0, 'p'}, - {"user", required_argument, 0, 'u'}, - {"version", no_argument, 0, 'v'}, - {"input-profile", no_argument, 0, 'i'}, - {"null-render", no_argument, 0, 'n'}, - {"singlecore", no_argument, 0, 's'}, - {"filter", no_argument, 0, 'x'}, - - {"log-file", required_argument, 0, 'l'}, - {"headless", required_argument, 0, 'H'}, - - {"room-name", required_argument, 0, 'N'}, - {"room-description", required_argument, 0, 'D'}, - {"bind-address", required_argument, 0, 'S'}, - {"port", required_argument, 0, 'P'}, - {"max-members", required_argument, 0, 'M'}, - {"password", required_argument, 0, 'W'}, - {"preferred-game", required_argument, 0, 'G'}, - {"preferred-game-id", required_argument, 0, 'I'}, - {"username", optional_argument, 0, 'U'}, - {"token", required_argument, 0, 'T'}, - {"web-api-url", required_argument, 0, 'A'}, - {"ban-list-file", required_argument, 0, 'B'}, - // Entry option - {"room", 0, 0, 0}, - {0, 0, 0, 0}, - // clang-format on - }; - - char *endarg = nullptr; - while (optind < argc) { - int arg = getopt_long(argc, argv, "g:fhvcip::c:u:d:", long_options, &option_index); - if (arg != -1) { - switch (char(arg)) { - case 'd': - state->opts.override_gdb_port = uint16_t(atoi(optarg)); - break; - case 'c': - state->opts.config_path = optarg; - break; - case 'f': - state->opts.fullscreen = true; - LOG_INFO(Frontend, "Starting in fullscreen mode..."); - break; - case 'h': - PrintHelp(argv[0]); - return SDL_APP_FAILURE; - case 'g': - state->opts.filepath = std::string(optarg); - break; - case 'i': { - state->opts.input_profile = std::string(optarg); - break; - } - case 'm': { - state->opts.use_multiplayer = true; - const std::string str_arg(optarg); - // regex to check if the format is nickname:password@ip:port - // with optional :password - const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$"); - if (!std::regex_match(str_arg, re)) { - std::cout << "Wrong format for option --multiplayer\n"; - PrintHelp(argv[0]); - return SDL_APP_FAILURE; - } - - std::smatch match; - std::regex_search(str_arg, match, re); - ASSERT(match.size() == 5); - state->opts.nickname = match[1]; - state->opts.password = match[2]; - state->opts.address = match[3]; - if (!match[4].str().empty()) { - state->opts.port = u16(std::strtoul(match[4].str().c_str(), nullptr, 0)); - } - std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$"); - if (!std::regex_match(state->opts.nickname, nickname_re)) { - LOG_ERROR(Frontend, "Nickname is not valid. Must be 4 to 20 alphanumeric characters"); - return SDL_APP_FAILURE; - } - if (state->opts.address.empty()) { - LOG_ERROR(Frontend, "Address to room must not be empty"); - return SDL_APP_FAILURE; - } - break; - } - case 'p': - state->opts.program_args.assign(optarg); - break; - case 'u': - state->opts.selected_user = atoi(optarg); - break; - case 'v': - PrintVersion(); - return SDL_APP_FAILURE; - case 'n': - state->opts.force_null_render = true; - break; - case 's': - state->opts.force_single_core = true; - break; - case 'x': - state->opts.log_filter.emplace(optarg); - break; - // shared - case 'l': - state->opts.log_file.assign(optarg); - break; - case 'H': - state->opts.headless.emplace(true); - break; - // room - case 'N': - state->opts.room_name.assign(optarg); - break; - case 'D': - state->opts.room_description.assign(optarg); - break; - case 'S': - state->opts.bind_address.assign(optarg); - break; - case 'P': { - auto const value = strtoul(optarg, &endarg, 0); - if (value <= USHRT_MAX) { - state->opts.port = value; - } else { - LOG_ERROR(Frontend, "port must be between 0-{}", USHRT_MAX); - } - break; - } - case 'M': { - auto const value = strtoul(optarg, &endarg, 0); - if (value >= 2 && value <= Network::MaxConcurrentConnections) { - state->opts.max_members = value; - } else { - LOG_ERROR(Frontend, "max members must be between 2-{}", value, Network::MaxConcurrentConnections); - } - break; - } - case 'W': - state->opts.password.assign(optarg); - break; - case 'G': - state->opts.preferred_game.assign(optarg); - break; - case 'I': - state->opts.preferred_game_id = strtoull(optarg, &endarg, 16); - break; - case 'U': - state->opts.nickname.assign(optarg); - break; - case 'T': - state->opts.token.assign(optarg); - break; - case 'A': - state->opts.web_api_url.assign(optarg); - break; - case 'B': - state->opts.ban_list_file.assign(optarg); - break; - } - } else { -#ifdef _WIN32 - state->opts.filepath = Common::UTF16ToUTF8(argv_w[optind]); -#else - state->opts.filepath = argv[optind]; -#endif - optind++; - } - } - + Common::ParseArguments(state->opts, argc, argv); if (!state->opts.room_name.empty() || !state->opts.room_description.empty()) { LOG_INFO(Frontend, "Assuming (headless) room mode"); - return ExecuteHeadlessRoom(*state); + Network::LaunchRoomLoopWithArguments(state->opts); + return SDL_APP_FAILURE; } return ExecuteWithGUI(*state); }