mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-21 15:28:19 +00:00
saucier arg parsing!
This commit is contained in:
@@ -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 <path>`: 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 <path>`: 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 <number>`: Select the index of the user to load as.
|
||||
- `-input-profile <name>`: 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 `<eden folder>/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.
|
||||
|
||||
@@ -133,6 +133,7 @@ add_library(
|
||||
typed_address.h
|
||||
uint128.h
|
||||
unique_function.h
|
||||
program_args.h
|
||||
random.cpp
|
||||
random.h
|
||||
uuid.cpp
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <regex>
|
||||
#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 <getopt.h>
|
||||
#ifndef _MSC_VER
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Common {
|
||||
|
||||
static void PrintHelp(const char* argv0) {
|
||||
LOG_INFO(Frontend, "Usage: {} [options] <filename>\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 <game path>)
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
inline constexpr u16 DEFAULT_ROOM_PORT = 24872;
|
||||
struct ProgramArguments {
|
||||
std::optional<std::string> config_path{};
|
||||
std::optional<std::string> 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<int> selected_user{};
|
||||
std::optional<u16> override_gdb_port{};
|
||||
std::optional<bool> 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[]);
|
||||
|
||||
}
|
||||
@@ -6,18 +6,31 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <regex>
|
||||
#include <shared_mutex>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <fstream>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
|
||||
#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<unsigned char, 512> output{};
|
||||
std::array<unsigned char, 2048> roundtrip{};
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
EVP_DecodeBlock(output.data(), reinterpret_cast<const unsigned char*>(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<unsigned char, 512> output{};
|
||||
EVP_DecodeBlock(output.data(), reinterpret_cast<const unsigned char*>(display_token.c_str()), display_token.length());
|
||||
std::string decoded_display_token(reinterpret_cast<char*>(&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<unsigned char, 512> output{};
|
||||
EVP_DecodeBlock(output.data(), reinterpret_cast<const unsigned char*>(display_token.c_str()), display_token.length());
|
||||
std::string decoded_display_token(reinterpret_cast<char*>(&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 <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<Network::VerifyUser::Backend> verify_backend;
|
||||
if (announce) {
|
||||
#ifdef ENABLE_WEB_SERVICE
|
||||
verify_backend =
|
||||
std::make_unique<WebService::VerifyUserJWT>(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<Network::VerifyUser::NullBackend>();
|
||||
#endif
|
||||
} else {
|
||||
verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
|
||||
}
|
||||
|
||||
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<Core::AnnounceMultiplayerSession>();
|
||||
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
|
||||
|
||||
+4
-1
@@ -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 <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#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<RoomImpl> room_impl;
|
||||
};
|
||||
|
||||
int LaunchRoomLoopWithArguments(Common::ProgramArguments& args);
|
||||
|
||||
} // namespace Network
|
||||
|
||||
+5
-1
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QApplication>
|
||||
#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();
|
||||
|
||||
|
||||
+19
-59
@@ -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<Ui::MainWindow>()},
|
||||
input_subsystem{std::make_shared<InputCommon::InputSubsystem>()}, 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();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <QTranslator>
|
||||
#include <qaction.h>
|
||||
|
||||
#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);
|
||||
|
||||
+5
-468
@@ -11,6 +11,7 @@
|
||||
#include <SDL3/SDL_init.h>
|
||||
#include <openssl/evp.h>
|
||||
#include "common/fs/file.h"
|
||||
#include "common/program_args.h"
|
||||
#include "common/settings_enums.h"
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
@@ -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] <filename>\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<EmuWindow_SDL3> emu_window;
|
||||
// settings
|
||||
struct {
|
||||
std::optional<std::string> config_path{};
|
||||
std::optional<std::string> 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<int> selected_user{};
|
||||
std::optional<u16> override_gdb_port{};
|
||||
std::optional<bool> 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<unsigned char, 512> output{};
|
||||
std::array<unsigned char, 2048> roundtrip{};
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
EVP_DecodeBlock(output.data(), reinterpret_cast<const unsigned char*>(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<unsigned char, 512> output{};
|
||||
EVP_DecodeBlock(output.data(), reinterpret_cast<const unsigned char*>(display_token.c_str()), display_token.length());
|
||||
std::string decoded_display_token(reinterpret_cast<char*>(&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<unsigned char, 512> output{};
|
||||
EVP_DecodeBlock(output.data(), reinterpret_cast<const unsigned char*>(display_token.c_str()), display_token.length());
|
||||
std::string decoded_display_token(reinterpret_cast<char*>(&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 <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<Network::VerifyUser::Backend> verify_backend;
|
||||
if (announce) {
|
||||
#ifdef ENABLE_WEB_SERVICE
|
||||
verify_backend =
|
||||
std::make_unique<WebService::VerifyUserJWT>(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<Network::VerifyUser::NullBackend>();
|
||||
#endif
|
||||
} else {
|
||||
verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
|
||||
}
|
||||
|
||||
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<Core::AnnounceMultiplayerSession>();
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user