Compare commits

...

8 Commits

Author SHA1 Message Date
lizzie 40e4a3f0a3 windowed death 2026-08-14 03:35:58 +00:00
lizzie 8b58a4f438 eatshi 2026-08-14 00:32:50 +00:00
lizzie 10b8a1b3c7 no way 2026-08-14 00:11:57 +00:00
lizzie a01fcd51cb ra 2026-08-13 23:35:40 +00:00
lizzie 5d05c88d9b g 2026-08-13 22:51:38 +00:00
lizzie 850394bd8c fixine 2026-08-13 19:43:33 +00:00
lizzie e84ce480f1 saucier arg parsing! 2026-08-13 19:02:23 +00:00
lizzie 13c9a055f9 [eden-cli] merge eden-room unto eden-cli
Signed-off-by: lizzie <lizzie@eden-emu.dev>
2026-08-13 18:32:29 +00:00
22 changed files with 699 additions and 817 deletions
-10
View File
@@ -46,9 +46,6 @@ endif()
cmake_dependent_option(YUZU_STATIC_ROOM "Build a static room executable only (CI only)" OFF "LINUX" OFF)
if (YUZU_STATIC_ROOM)
set(YUZU_ROOM ON)
set(YUZU_ROOM_STANDALONE ON)
# disable e v e r y t h i n g
set(ENABLE_QT OFF)
set(YUZU_CMD OFF)
@@ -257,9 +254,6 @@ option(YUZU_LEGACY "Apply patches that improve compatibility with older GPUs (e.
option(NIGHTLY_BUILD "Use Nightly qualifiers in the update checker and build metadata" OFF)
cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID" OFF)
cmake_dependent_option(YUZU_ROOM_STANDALONE "Enable standalone room executable" ON "YUZU_ROOM" OFF)
cmake_dependent_option(YUZU_CMD "Compile the eden-cli executable" ON "NOT ANDROID" OFF)
cmake_dependent_option(YUZU_CRASH_DUMPS "Compile crash dump (Minidump) support" OFF "WIN32 OR LINUX" OFF)
@@ -300,10 +294,6 @@ if (ARCHITECTURE_arm64 AND (ANDROID OR LINUX))
add_compile_definitions(HAS_NCE=1)
endif()
if (YUZU_ROOM)
add_compile_definitions(YUZU_ROOM)
endif()
if (UNIX AND NOT (LINUX OR WIN32))
if(CXX_APPLE OR CXX_CLANG)
# libc++ has stop_token and jthread as experimental
+2 -2
View File
@@ -65,8 +65,6 @@ These options control executables and build flavors.
**Desktop only**:
- `YUZU_CMD` (ON) Compile the SDL-based frontend (eden-cli)
- `YUZU_ROOM` (OFF) Compile dedicated room functionality into the main executable
- `YUZU_ROOM_STANDALONE` (OFF) Compile a separate executable for room functionality
- `YUZU_STATIC_ROOM` (OFF) Compile the room executable *only* as a static, portable executable
- This is only usable on Alpine Linux.
@@ -101,5 +99,7 @@ The following options were a part of Eden at one point, but have since been reti
- `YUZU_USE_CPM` - This option once had a purpose, but that purpose has long since passed us by. *All* builds use CPMUtil to manage dependencies now.
- If you want to *force* the usage of system dependencies, use `-DCPMUTIL_FORCE_SYSTEM=ON`.
- `YUZU_USE_EXTERNAL_SDL` - This is now handled automatically. It was included even after CPM for purposes that have not applied for a very long time.
- `YUZU_ROOM` - Room functionality is now built-in into eden-cli unconditionally.
- `YUZU_ROOM_STANDALONE` - Likewise.
See `src/dynarmic/CMakeLists.txt` for additional options--usually, these don't need changed
+37 -22
View File
@@ -1,32 +1,47 @@
# 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.
- `--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`: 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`: 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.
- `-x/--filter`: Sets the debug log filter irrespective of settings.
- `-s/--singlecore`: Forces single-core regardless of settings.
- `-l/--log-file`: The file for storing the room log.
- `-H/--headless`: Force headless mode (no GUI). Currently only used for rooms.
## eden-cli
Room settings:
- `-N/--name`: The name of the room.
- `-D/--description`: The room description.
- `-S/--bind-address`: The bind address for the room.
- `-P/--port`: The port used for the room.
- `-M/--max-members`: The maximum number of players for this room.
- `-W/--password`: The password for the room.
- `-G/--preferred-game`: The preferred game for this room.
- `-I/--preferred-game-id`: The preferred game-id for this room.
- `-U/--username`: The username used for announce.
- `-T/--token`: The token used for announce.
- `-A/--web-api-url`: yuzu Web API url.
- `-B/--ban-list-file`: The file for storing the room ban list.
- `-h/--help`: Display this help and exit.
- `-v/--version`: Output version information and exit.,
- `--debug/-d`: Enter debug mode, allow gdb stub at port `1234`
- `--config/-c`: Specify alternate configuration file.
- `--fullscreen/-f`: Set fullscreen.
- `--help/-h`: Display help.
- `--game/-g`: Specify the game to run.
- `--multiplayer/-m`: Specify multiplayer options.
- `--program/-p`: Specify the program arguments to pass (optional).
- `--user/-u`: Specify the user index.
- `--version/-v`: Display version and quit.
- `--input-profile/-i`: Specifies input profile name to use (for player #0 only).
- `--null-render/-n`: Forces the usage of the "Null" render backend irrespective of settings.
- `--filter/-x`: Sets the debug log filter irrespective of settings.
- `--singlecore/-s`: Forces single-core regardless of settings.
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.
-9
View File
@@ -251,15 +251,6 @@ endif()
add_subdirectory(common)
add_subdirectory(network)
if (YUZU_ROOM)
add_subdirectory(dedicated_room)
endif()
if (YUZU_ROOM_STANDALONE)
add_subdirectory(yuzu_room_standalone)
set_target_properties(yuzu-room PROPERTIES OUTPUT_NAME "eden-room")
endif()
if (YUZU_STATIC_ROOM)
return()
endif()
+2
View File
@@ -133,6 +133,8 @@ add_library(
typed_address.h
uint128.h
unique_function.h
program_args.cpp
program_args.h
random.cpp
random.h
uuid.cpp
+280
View File
@@ -0,0 +1,280 @@
// 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 "common/string_util.h"
#include "network/room.h"
#ifdef _WIN32
// windows.h needs to be included before shellapi.h
#include <windows.h>
#include <shellapi.h>
#include "common/windows/timer_resolution.h"
#endif
#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"
"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);
}
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 -1;
}
#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", no_argument, 0, 0},
{"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>)
[[maybe_unused]] 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
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;
}
} 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;
}
}
+49
View File
@@ -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[]);
}
-27
View File
@@ -1,27 +0,0 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2017 Citra Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
add_library(yuzu-room STATIC EXCLUDE_FROM_ALL
yuzu_room.cpp
yuzu_room.h
yuzu_room.rc)
target_link_libraries(yuzu-room PRIVATE common network)
if (ENABLE_WEB_SERVICE)
target_compile_definitions(yuzu-room PRIVATE ENABLE_WEB_SERVICE)
target_link_libraries(yuzu-room PRIVATE web_service)
endif()
target_link_libraries(yuzu-room PRIVATE
OpenSSL::SSL
OpenSSL::Crypto)
if (MSVC)
target_link_libraries(yuzu-room PRIVATE getopt)
endif()
target_link_libraries(yuzu-room PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
create_target_directory_groups(yuzu-room)
-394
View File
@@ -1,394 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <chrono>
#include <fstream>
#include <iostream>
#include <memory>
#include <regex>
#include <string>
#include <thread>
#ifdef _WIN32
// windows.h needs to be included before shellapi.h
#include <windows.h>
#include <shellapi.h>
#endif
#include <openssl/evp.h>
#include "common/common_types.h"
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/scm_rev.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "network/announce_multiplayer_session.h"
#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
#undef _UNICODE
#include <getopt.h>
#ifndef _MSC_VER
#include <unistd.h>
#endif
#include "yuzu_room.h"
static void PrintHelp(const char* argv0) {
LOG_INFO(Network,
"Usage: {}"
" [options] <filename>\n"
"-n, --room-name The name of the room\n"
"-d, --room-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"
"-l, --log-file The file for storing the room log\n"
"-h, --help Display this help and exit\n"
"-v, --version Output version information and exit\n",
argv0);
}
static void PrintVersion() {
LOG_INFO(Network, "Eden dedicated room {} {} Libnetwork: {}", Common::g_scm_branch,
Common::g_scm_desc, Network::network_version);
}
/// The magic text at the beginning of a yuzu-room ban list file.
static constexpr char BanListMagic[] = "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 != BanListMagic) {
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 << BanListMagic << "\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";
}
}
/// Application entry point
void LaunchRoom(int argc, char** argv, bool called_by_option) {
int option_index = 0;
char* endarg;
char* new_argv0 = argv[0];
if (called_by_option) {
strncat(new_argv0, " --room", 8);
}
std::string room_name;
std::string room_description;
std::string password;
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;
u64 preferred_game_id = 0;
u32 port = Network::DefaultRoomPort;
u32 max_members = 16;
static struct option long_options[] = {
{"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'},
{"log-file", required_argument, 0, 'l'},
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'v'},
// Entry option
{"room", 0, 0, 0},
{0, 0, 0, 0},
};
Common::Log::Initialize();
Common::Log::SetColorConsoleBackendEnabled(true);
Common::Log::Start();
while (optind < argc) {
int arg = getopt_long(argc, argv, "n:d:s:p:m:w:g:u:t:a:i:l:hv", long_options, &option_index);
if (arg != -1) {
char carg = static_cast<char>(arg);
switch (carg) {
case 'n':
room_name.assign(optarg);
break;
case 'd':
room_description.assign(optarg);
break;
case 's':
bind_address.assign(optarg);
break;
case 'p':
port = strtoul(optarg, &endarg, 0);
break;
case 'm':
max_members = strtoul(optarg, &endarg, 0);
break;
case 'w':
password.assign(optarg);
break;
case 'g':
preferred_game.assign(optarg);
break;
case 'i':
preferred_game_id = strtoull(optarg, &endarg, 16);
break;
case 'u':
username.assign(optarg);
break;
case 't':
token.assign(optarg);
break;
case 'a':
web_api_url.assign(optarg);
break;
case 'b':
ban_list_file.assign(optarg);
break;
case 'l':
log_file.assign(optarg);
break;
case 'h':
PrintHelp(argv[0]);
std::exit(0);
case 'v':
PrintVersion();
std::exit(0);
default:
break;
}
}
}
if (room_name.empty()) {
LOG_ERROR(Network, "Room name is empty!");
PrintHelp(argv[0]);
std::exit(-1);
}
if (preferred_game.empty()) {
LOG_ERROR(Network, "Preferred game is empty!");
PrintHelp(argv[0]);
std::exit(-1);
}
if (preferred_game_id == 0) {
LOG_ERROR(Network,
"preferred-game-id not set!\nThis should get set to allow users to find your "
"room.\nSet with --preferred-game-id id");
}
if (max_members > Network::MaxConcurrentConnections || max_members < 2) {
LOG_ERROR(Network,
"max_members needs to be in the range 2 - {}!",
Network::MaxConcurrentConnections);
PrintHelp(argv[0]);
std::exit(-1);
}
if (bind_address.empty()) {
LOG_INFO(Network, "Bind address is empty: defaulting to 0.0.0.0");
}
if (port > UINT16_MAX) {
LOG_ERROR(Network, "Port needs to be in the range 0 - 65535!");
PrintHelp(argv[0]);
std::exit(-1);
}
if (ban_list_file.empty()) {
LOG_ERROR(Network,
"Ban list file not set!\nThis should get set to load and save room ban "
"list.\nSet with --ban-list-file <file>");
}
bool announce = true;
if (token.empty() && announce) {
announce = false;
LOG_INFO(Network, "Token is empty: Hosting a private room");
}
if (web_api_url.empty() && announce) {
announce = false;
LOG_INFO(Network, "Endpoint url is empty: Hosting a private room");
}
if (announce) {
if (username.empty()) {
LOG_INFO(Network, "Hosting a public room");
Settings::values.web_api_url = web_api_url;
PadToken(token);
Settings::values.eden_username = UsernameFromDisplayToken(token);
username = Settings::values.eden_username.GetValue();
Settings::values.eden_token = TokenFromDisplayToken(token);
} else {
LOG_INFO(Network, "Hosting a public room");
Settings::values.web_api_url = web_api_url;
Settings::values.eden_username = username;
Settings::values.eden_token = token;
}
}
// Load the ban list
Network::Room::BanList ban_list;
if (!ban_list_file.empty()) {
ban_list = LoadBanList(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 = preferred_game,
.id = preferred_game_id};
if (!room->Create(room_name, room_description, bind_address, static_cast<u16>(port),
password, max_members, 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 (!ban_list_file.empty()) {
SaveBanList(room->GetBanList(), ban_list_file);
}
room->Destroy();
}
Network::Shutdown();
std::exit(0);
}
-7
View File
@@ -1,7 +0,0 @@
// Copyright eden Emulator Project
// Licensed under GPLv3 or any later version
// Refer to the license.txt file included.
#pragma once
void LaunchRoom(int argc, char** argv, bool called_by_option);
-20
View File
@@ -1,20 +0,0 @@
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "winresrc.h"
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
YUZU_ICON ICON "../../dist/eden.ico"
/////////////////////////////////////////////////////////////////////////////
//
// RT_MANIFEST
//
0 RT_MANIFEST "../../dist/yuzu.manifest"
+208
View File
@@ -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,199 @@ void Room::Destroy() {
room_impl->room_information.name.clear();
}
/// 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;
}
} // namespace Network
+4 -1
View File
@@ -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
+3 -4
View File
@@ -398,6 +398,9 @@ endif()
target_link_libraries(yuzu PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(yuzu PRIVATE common core input_common frontend_common network video_core qt_common)
target_link_libraries(yuzu PRIVATE Boost::headers Qt6::Widgets Qt6::Charts Qt6::Concurrent)
target_link_libraries(yuzu PRIVATE
OpenSSL::SSL
OpenSSL::Crypto)
target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
if (ENABLE_OPENGL)
target_link_libraries(yuzu PRIVATE glad)
@@ -434,10 +437,6 @@ if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
target_link_libraries(yuzu PRIVATE dynarmic::dynarmic)
endif()
if (YUZU_ROOM)
target_link_libraries(yuzu PRIVATE yuzu-room)
endif()
if (NOT MSVC AND (APPLE OR NOT YUZU_STATIC_BUILD))
# needed for vma
target_compile_options(yuzu PRIVATE
+11 -19
View File
@@ -2,12 +2,10 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QApplication>
#include "common/program_args.h"
#include "network/room.h"
#include "startup_checks.h"
#if YUZU_ROOM
#include <cstring>
#include "dedicated_room/yuzu_room.h"
#endif
#ifdef __unix__
#include "qt_common/gui_settings.h"
#endif
@@ -78,20 +76,6 @@ static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() {
}
int main(int argc, char* argv[]) {
#if YUZU_ROOM
bool launch_room = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--room") == 0) {
launch_room = true;
}
}
if (launch_room) {
LaunchRoom(argc, argv, true);
return 0;
}
#endif
bool has_broken_vulkan = false;
bool is_child = false;
if (CheckEnvVars(&is_child)) {
@@ -200,7 +184,15 @@ 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);
if (!args.room_name.empty() || !args.room_description.empty()) {
LOG_INFO(Frontend, "Assuming (headless) room mode");
Network::LaunchRoomLoopWithArguments(args);
return -1;
}
MainWindow main_window{std::move(args), has_broken_vulkan};
// After settings have been loaded by GMainWindow, apply the filter
main_window.show();
+19 -59
View File
@@ -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();
+2 -1
View File
@@ -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);
+10 -1
View File
@@ -38,7 +38,16 @@ add_executable(yuzu-cmd
${OPENGL_SOURCES}
)
target_link_libraries(yuzu-cmd PRIVATE common core input_common frontend_common video_core)
target_link_libraries(yuzu-cmd PRIVATE common network core input_common frontend_common video_core)
if (ENABLE_WEB_SERVICE)
target_compile_definitions(yuzu-cmd PRIVATE ENABLE_WEB_SERVICE)
target_link_libraries(yuzu-cmd PRIVATE web_service)
endif()
target_link_libraries(yuzu-cmd PRIVATE
OpenSSL::SSL
OpenSSL::Crypto)
if (ENABLE_OPENGL)
target_link_libraries(yuzu-cmd PRIVATE glad)
endif()
+72 -205
View File
@@ -8,6 +8,10 @@
#include <memory>
#include <regex>
#include <string>
#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>
@@ -32,9 +36,14 @@
#include "core/loader/loader.h"
#include "frontend_common/config.h"
#include "input_common/main.h"
#include "network/announce_multiplayer_session.h"
#include "network/network.h"
#include "sdl_config.h"
#include "network/room.h"
#include "network/verify_user.h"
#include "yuzu_cmd/sdl_config.h"
#include "video_core/renderer_base.h"
#include "yuzu_cmd/emu_window/emu_window_sdl3.h"
#ifdef HAS_OPENGL
#include "yuzu_cmd/emu_window/emu_window_sdl3_gl.h"
@@ -64,25 +73,6 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif
static void PrintHelp(const char* argv0) {
std::cout << "Usage: " << argv0
<< " [options] <filename>\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"
"-v, --version Output version information and exit\n";
}
static void PrintVersion() {
std::cout << "Eden " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl;
}
static void OnStateChanged(const Network::RoomMember::State& state) {
switch (state) {
case Network::RoomMember::State::Idle:
@@ -181,216 +171,70 @@ static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) {
struct SdlState {
Core::System system{};
std::unique_ptr<EmuWindow_SDL3> emu_window;
// settings
Common::ProgramArguments opts = {};
};
extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
SdlState* state = new SdlState();
#ifdef _WIN32
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "wb", stdout);
freopen("CONOUT$", "wb", stderr);
}
#endif
Common::Log::Initialize();
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
std::string filepath;
std::optional<std::string> config_path{};
std::string program_args;
std::optional<int> selected_user{};
std::optional<u16> override_gdb_port{};
bool use_multiplayer = false;
bool fullscreen = false;
bool force_null_render = false;
bool force_single_core = false;
std::string nickname{};
std::string password{};
std::string address{};
std::string input_profile{};
std::optional<std::string> log_filter{};
u16 port = Network::DefaultRoomPort;
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'},
{0, 0, 0, 0},
// clang-format on
};
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':
override_gdb_port = uint16_t(atoi(optarg));
break;
case 'c':
config_path = optarg;
break;
case 'f':
fullscreen = true;
LOG_INFO(Frontend, "Starting in fullscreen mode...");
break;
case 'h':
PrintHelp(argv[0]);
return SDL_APP_FAILURE;
case 'g':
filepath = std::string(optarg);
break;
case 'i': {
input_profile = std::string(optarg);
break;
}
case 'm': {
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);
nickname = match[1];
password = match[2];
address = match[3];
if (!match[4].str().empty()) {
port = u16(std::strtoul(match[4].str().c_str(), nullptr, 0));
}
std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$");
if (!std::regex_match(nickname, nickname_re)) {
LOG_ERROR(Frontend, "Nickname is not valid. Must be 4 to 20 alphanumeric characters");
return SDL_APP_FAILURE;
}
if (address.empty()) {
LOG_ERROR(Frontend, "Address to room must not be empty");
return SDL_APP_FAILURE;
}
break;
}
case 'p':
program_args = argv[optind];
++optind;
break;
case 'u':
selected_user = atoi(optarg);
break;
case 'v':
PrintVersion();
return SDL_APP_FAILURE;
case 'n':
force_null_render = true;
break;
case 's':
force_single_core = true;
break;
case 'x':
log_filter = argv[optind];
++optind;
break;
}
} else {
#ifdef _WIN32
filepath = Common::UTF16ToUTF8(argv_w[optind]);
#else
filepath = argv[optind];
#endif
optind++;
}
}
SdlConfig config{config_path};
static SDL_AppResult ExecuteWithGUI(SdlState& state) {
SdlConfig config{state.opts.config_path};
// apply the log_filter setting
// the logger was initialized before and doesn't pick up the filter on its own
Common::Log::Filter filter;
filter.ParseFilterString(log_filter.value_or(Settings::values.log_filter.GetValue()));
filter.ParseFilterString(state.opts.log_filter.value_or(Settings::values.log_filter.GetValue()));
Common::Log::SetGlobalFilter(filter);
if (!program_args.empty()) {
Settings::values.program_args = program_args;
if (!state.opts.program_args.empty()) {
Settings::values.program_args = state.opts.program_args;
}
if (!input_profile.empty()) {
if (!state.opts.input_profile.empty()) {
auto& players = Settings::values.players.GetValue();
players[0].profile_name = input_profile;
players[0].profile_name = state.opts.input_profile;
}
if (selected_user.has_value()) {
Settings::values.current_user = std::clamp(*selected_user, 0, 7);
if (state.opts.selected_user.has_value()) {
Settings::values.current_user = std::clamp(*state.opts.selected_user, 0, 7);
}
if (override_gdb_port.has_value()) {
if (state.opts.override_gdb_port.has_value()) {
Settings::values.use_gdbstub = true;
Settings::values.gdbstub_port = *override_gdb_port;
Settings::values.gdbstub_port = *state.opts.override_gdb_port;
}
if (force_single_core) {
if (state.opts.force_single_core) {
Settings::values.use_multi_core = false;
}
if (force_null_render) {
if (state.opts.force_null_render) {
Settings::values.renderer_backend = Settings::RendererBackend::Null;
}
#ifdef _WIN32
LocalFree(argv_w);
#endif
if (filepath.empty()) {
if (state.opts.filepath.empty()) {
LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified");
return SDL_APP_FAILURE;
}
state->system.Initialize();
state.system.Initialize();
InputCommon::InputSubsystem input_subsystem{};
// Apply the command line arguments
state->system.ApplySettings();
state.system.ApplySettings();
switch (Settings::values.renderer_backend.GetValue()) {
#ifdef HAS_OPENGL
case Settings::RendererBackend::OpenGL_GLSL:
case Settings::RendererBackend::OpenGL_GLASM:
case Settings::RendererBackend::OpenGL_SPIRV:
state->emu_window = std::make_unique<EmuWindow_SDL3_GL>(&input_subsystem, state->system, fullscreen);
state.emu_window = std::make_unique<EmuWindow_SDL3_GL>(&input_subsystem, state.system, state.opts.fullscreen);
break;
#endif
case Settings::RendererBackend::Vulkan:
state->emu_window = std::make_unique<EmuWindow_SDL3_VK>(&input_subsystem, state->system, fullscreen);
state.emu_window = std::make_unique<EmuWindow_SDL3_VK>(&input_subsystem, state.system, state.opts.fullscreen);
break;
case Settings::RendererBackend::Null:
state->emu_window = std::make_unique<EmuWindow_SDL3_Null>(&input_subsystem, state->system, fullscreen);
state.emu_window = std::make_unique<EmuWindow_SDL3_Null>(&input_subsystem, state.system, state.opts.fullscreen);
break;
default:
LOG_CRITICAL(Frontend, "Invalid renderer backend");
@@ -399,23 +243,23 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
#ifdef _WIN32
Common::Windows::SetCurrentTimerResolutionToMaximum();
state->system.CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution());
state.system.CoreTiming().SetTimerResolutionNs(Common::Windows::GetCurrentTimerResolution());
#endif
state->system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
state->system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
state->system.GetFileSystemController().CreateFactories(*state->system.GetFilesystem());
state->system.GetUserChannel().clear();
state.system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
state.system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
state.system.GetFileSystemController().CreateFactories(*state.system.GetFilesystem());
state.system.GetUserChannel().clear();
Service::AM::FrontendAppletParameters load_parameters{
.applet_id = Service::AM::AppletId::Application,
};
const Core::SystemResultStatus load_result = state->system.Load(*state->emu_window, filepath, load_parameters);
const Core::SystemResultStatus load_result = state.system.Load(*state.emu_window, state.opts.filepath, load_parameters);
switch (load_result) {
case Core::SystemResultStatus::Success:
break; // Expected case
case Core::SystemResultStatus::ErrorGetLoader:
LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath);
LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", state.opts.filepath);
return SDL_APP_FAILURE;
case Core::SystemResultStatus::ErrorLoader:
LOG_CRITICAL(Frontend, "Failed to load ROM!");
@@ -437,14 +281,14 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
return SDL_APP_FAILURE;
}
if (use_multiplayer) {
if (state.opts.use_multiplayer) {
if (auto member = Network::GetRoomMember().lock()) {
member->BindOnChatMessageReceived(OnMessageReceived);
member->BindOnStatusMessageReceived(OnStatusMessageReceived);
member->BindOnStateChanged(OnStateChanged);
member->BindOnError(OnNetworkError);
LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, nickname);
member->Join(nickname, address.c_str(), port, 0, Network::NoPreferredIP, password);
LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", state.opts.address, state.opts.port, state.opts.nickname);
member->Join(state.opts.nickname, state.opts.address.c_str(), state.opts.port, 0, Network::NoPreferredIP, state.opts.password);
} else {
LOG_ERROR(Network, "Could not access RoomMember");
return SDL_APP_FAILURE;
@@ -452,22 +296,45 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
}
// Core is loaded, start the GPU (makes the GPU contexts current to this thread)
state->system.GPU().Start();
state->system.GetCpuManager().OnGpuReady();
state.system.GPU().Start();
state.system.GetCpuManager().OnGpuReady();
if (Settings::values.use_disk_shader_cache.GetValue()) {
state->system.Renderer().ReadRasterizer()->LoadDiskResources(
state->system.GetApplicationProcessProgramID(), std::stop_token{},
state.system.Renderer().ReadRasterizer()->LoadDiskResources(
state.system.GetApplicationProcessProgramID(), std::stop_token{},
[](VideoCore::LoadCallbackStage, size_t value, size_t total) {});
}
// don't do anything, SDL3 already exists for us :D
state->system.RegisterExitCallback([] {});
void(state->system.Run());
if (state->system.DebuggerEnabled())
state->system.InitializeDebugger();
state.system.RegisterExitCallback([] {});
void(state.system.Run());
if (state.system.DebuggerEnabled())
state.system.InitializeDebugger();
return SDL_APP_SUCCESS;
}
extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
SdlState* state = new SdlState();
#ifdef _WIN32
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "wb", stdout);
freopen("CONOUT$", "wb", stderr);
}
#endif
Common::Log::Initialize();
Common::Log::SetColorConsoleBackendEnabled(true);
Common::Log::Start();
Common::ParseArguments(state->opts, argc, argv);
if (!state->opts.room_name.empty() || !state->opts.room_description.empty()) {
LOG_INFO(Frontend, "Assuming (headless) room mode");
Network::LaunchRoomLoopWithArguments(state->opts);
return SDL_APP_FAILURE;
}
return ExecuteWithGUI(*state);
}
extern "C" SDL_AppResult SDL_AppIterate(void *appstate) {
SdlState *state = (SdlState *)appstate;
return state->emu_window->IsOpen() ? SDL_APP_CONTINUE : SDL_APP_SUCCESS;
-18
View File
@@ -1,18 +0,0 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
add_executable(yuzu_room_standalone yuzu_room_standalone.cpp)
set_target_properties(yuzu_room_standalone PROPERTIES OUTPUT_NAME "eden-room")
target_link_libraries(yuzu_room_standalone PRIVATE yuzu-room)
if(UNIX AND NOT APPLE)
install(TARGETS yuzu_room_standalone RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
endif()
if (YUZU_STATIC_ROOM)
target_link_options(yuzu_room_standalone PRIVATE "-static")
endif()
create_target_directory_groups(yuzu_room_standalone)
-9
View File
@@ -1,9 +0,0 @@
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
@@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "dedicated_room/yuzu_room.h"
int main(int argc, char* argv[])
{
LaunchRoom(argc, argv, false);
}