mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-14 21:04:41 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a897331edf | |||
| 6af5e176ed | |||
| 3eca67617a |
@@ -46,6 +46,9 @@ 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)
|
||||
@@ -254,6 +257,9 @@ 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)
|
||||
@@ -294,6 +300,10 @@ 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
@@ -65,6 +65,8 @@ 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.
|
||||
|
||||
@@ -99,7 +101,5 @@ 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
|
||||
|
||||
+22
-37
@@ -1,47 +1,32 @@
|
||||
# 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 and share almost the same accepted arguments.
|
||||
There are two main applications, an SDL-based app (`eden-cli`) and a Qt based app (`eden`); both accept command line 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.
|
||||
- `--hlaunch`: Launch homebrew launcher `nx-hbloader`.
|
||||
- `-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`.
|
||||
- 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.
|
||||
- `-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.
|
||||
- `-setup`: Launch setup applet.
|
||||
|
||||
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.,
|
||||
## eden-cli
|
||||
|
||||
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.
|
||||
- `--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.
|
||||
|
||||
@@ -251,6 +251,15 @@ 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()
|
||||
|
||||
-1
@@ -30,7 +30,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
||||
ENABLE_BUFFER_HISTORY("enable_buffer_history"),
|
||||
USE_OPTIMIZED_VERTEX_BUFFERS("use_optimized_vertex_buffers"),
|
||||
ENABLE_GPU_BUFFER_READBACK("enable_gpu_buffer_readback"),
|
||||
SYNC_MEMORY_OPERATIONS("sync_memory_operations"),
|
||||
BUFFER_REORDER_DISABLE("disable_buffer_reorder"),
|
||||
RENDERER_DEBUG("debug"),
|
||||
|
||||
-7
@@ -808,13 +808,6 @@ abstract class SettingsItem(
|
||||
descriptionId = R.string.enable_buffer_history_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.ENABLE_GPU_BUFFER_READBACK,
|
||||
titleId = R.string.enable_gpu_buffer_readback,
|
||||
descriptionId = R.string.enable_gpu_buffer_readback_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.USE_OPTIMIZED_VERTEX_BUFFERS,
|
||||
|
||||
-1
@@ -294,7 +294,6 @@ class SettingsFragmentPresenter(
|
||||
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
|
||||
add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
|
||||
add(BooleanSetting.ENABLE_BUFFER_HISTORY.key)
|
||||
add(BooleanSetting.ENABLE_GPU_BUFFER_READBACK.key)
|
||||
add(BooleanSetting.USE_OPTIMIZED_VERTEX_BUFFERS.key)
|
||||
|
||||
add(HeaderSetting(R.string.hacks))
|
||||
|
||||
@@ -499,8 +499,6 @@
|
||||
<string name="renderer_reactive_flushing_description">يحسن دقة العرض في بعض الألعاب على حساب الأداء.</string>
|
||||
<string name="enable_buffer_history">تمكين سجل التخزين المؤقت</string>
|
||||
<string name="enable_buffer_history_description">يُتيح هذا الخيار الوصول إلى حالات التخزين المؤقت السابقة. وقد يُحسّن جودة العرض وثبات الأداء في بعض الألعاب.</string>
|
||||
<string name="enable_gpu_buffer_readback">تفعيل قراءة مخزن وحدة معالجة الرسومات</string>
|
||||
<string name="enable_gpu_buffer_readback_description">يحافظ هذا النظام على بيانات المخزن المؤقت المُعدّلة بواسطة وحدة معالجة الرسومات عن طريق قراءتها مرة أخرى قبل التحميل. تتطلب بعض الألعاب ذلك لعرض بعض التأثيرات بشكل صحيح. قد يُسبب ذلك مشاكل إذا لم يتمكن الجهاز من التعامل مع عبء العمل الإضافي.</string>
|
||||
<string name="use_optimized_vertex_buffers">مخازن الرؤوس المُحسّنة</string>
|
||||
<string name="use_optimized_vertex_buffers_description">يُتيح ربطًا مُحسَّنًا لمخازن الرؤوس لتحسين الأداء. يتطلب برامج تشغيل Mesa 26.0+ Turnip/ برامج تشغيل QCOM. قد يتعطل على برامج تشغيل Turnip القديمة (25.3 وما دون).</string>
|
||||
|
||||
|
||||
@@ -491,8 +491,6 @@
|
||||
<string name="renderer_reactive_flushing_description">Mejora la precisión de renderizado en algunos juegos, pero reduce el rendimiento.</string>
|
||||
<string name="enable_buffer_history">Activar el historial del búfer</string>
|
||||
<string name="enable_buffer_history_description">Permite el acceso al estado del búfer anterior. Esta opción puede mejorar la calidad de renderizado y la consistencia en el rendimiento de algunos juegos.</string>
|
||||
<string name="enable_gpu_buffer_readback">Activar la lectura del buffer de la GPU</string>
|
||||
<string name="enable_gpu_buffer_readback_description">Conserva los datos del búfer modificados por la GPU leyéndolos antes de subirlos.\nAlgunos juegos requieren esto para renderizar correctamente ciertos efectos.\nPuede causar problemas si el hardware no puede soportar la carga de trabajo adicional.</string>
|
||||
<string name="use_optimized_vertex_buffers">Búferes de vértices optimizados</string>
|
||||
<string name="use_optimized_vertex_buffers_description">Permite la optimización del enlace del búfer de vértices para un mejor rendimiento. Requiere controladores Mesa 26.0+ Turnip/ controladores QCOM. Fallará con controladores Turnip más antiguos (versión 25.3 o inferior).</string>
|
||||
|
||||
|
||||
@@ -489,8 +489,6 @@
|
||||
<string name="renderer_reactive_flushing_description">通过牺牲性能来提升某些游戏的渲染精度。</string>
|
||||
<string name="enable_buffer_history">启用缓冲区历史</string>
|
||||
<string name="enable_buffer_history_description">启用对先前缓冲区状态的访问。此选项可在某些游戏中提升渲染质量并保持性能的一致性。</string>
|
||||
<string name="enable_gpu_buffer_readback">启用 GPU 缓冲区回读</string>
|
||||
<string name="enable_gpu_buffer_readback_description">在上传前回读经由 GPU 修改过的缓冲区数据,以将其保留。一些游戏会用到这项设定以正确渲染某些效果。如果硬件无法处理额外的工作负载,则可能会导致问题。</string>
|
||||
<string name="use_optimized_vertex_buffers">优化顶点缓冲区</string>
|
||||
<string name="use_optimized_vertex_buffers_description">启用经过优化的顶点缓冲区绑定以提升性能。需要 Mesa 26.0 及以上版本的 Turnip 或 QCOM 驱动程序。若使用较旧版本的 Turnip 驱动 (25.3 及以下版本) 则会导致崩溃。</string>
|
||||
|
||||
|
||||
@@ -505,8 +505,6 @@
|
||||
<string name="renderer_reactive_flushing_description">Improves rendering accuracy in some games at the cost of performance.</string>
|
||||
<string name="enable_buffer_history">Enable buffer history</string>
|
||||
<string name="enable_buffer_history_description">Enables access to previous buffer states. This option may improve rendering quality and performance consistency in some games.</string>
|
||||
<string name="enable_gpu_buffer_readback">Enable GPU Buffer Readback</string>
|
||||
<string name="enable_gpu_buffer_readback_description">Preserves GPU-modified buffer data by reading it back before uploads. Some games require this to render certain effects properly. May cause issues if the hardware cannot handle the additional workload.</string>
|
||||
<string name="use_optimized_vertex_buffers">Optimized Vertex Buffers</string>
|
||||
<string name="use_optimized_vertex_buffers_description">Enables optimized vertex buffer binding for improved performance. Requires Mesa 26.0+ Turnip drivers/ QCOM drivers. Will crash on older Turnip drivers (25.3 and below).</string>
|
||||
|
||||
|
||||
@@ -133,8 +133,6 @@ add_library(
|
||||
typed_address.h
|
||||
uint128.h
|
||||
unique_function.h
|
||||
program_args.cpp
|
||||
program_args.h
|
||||
random.cpp
|
||||
random.h
|
||||
uuid.cpp
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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[]);
|
||||
|
||||
}
|
||||
@@ -576,13 +576,6 @@ struct Values {
|
||||
false,
|
||||
#endif
|
||||
"rescale_hack", Category::RendererHacks};
|
||||
SwitchableSetting<bool> enable_gpu_buffer_readback{linkage,
|
||||
false,
|
||||
"enable_gpu_buffer_readback",
|
||||
Category::RendererAdvanced,
|
||||
Specialization::Default,
|
||||
true,
|
||||
true};
|
||||
|
||||
SwitchableSetting<bool> use_asynchronous_shaders{linkage, false, "use_asynchronous_shaders",
|
||||
Category::RendererHacks};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,394 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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"
|
||||
@@ -6,31 +6,18 @@
|
||||
#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 {
|
||||
|
||||
@@ -1155,199 +1142,4 @@ 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
|
||||
|
||||
+1
-4
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
|
||||
@@ -10,7 +10,6 @@
|
||||
#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"
|
||||
@@ -149,6 +148,4 @@ private:
|
||||
std::unique_ptr<RoomImpl> room_impl;
|
||||
};
|
||||
|
||||
int LaunchRoomLoopWithArguments(Common::ProgramArguments& args);
|
||||
|
||||
} // namespace Network
|
||||
|
||||
@@ -223,8 +223,6 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
|
||||
tr("Controls the DMA read mode.\nUnsafe is faster, while Safe is more stable and can fix issues in some games.\nDefault follows the GPU Accuracy setting."));
|
||||
INSERT(Settings, gpu_fence_behavior, tr("GPU Fence Behavior:"),
|
||||
tr("Controls the GPU fence synchronization behavior.\nImmediate is the fastest option, but can introduce some issues.\nBalanced offers better compatibility and may fix issues in some games.\nAccurate further improves compatibility at the cost of some performance.\nStrict is the slowest option, but can fix issues that require stricter synchronization.\nDefault follows the GPU Accuracy setting."));
|
||||
INSERT(Settings, enable_gpu_buffer_readback, tr("Enable GPU buffer readback"),
|
||||
tr("Preserves GPU-modified data by reading it back before uploading.\nSome games require this to render certain effects properly."));
|
||||
INSERT(Settings, use_asynchronous_shaders, tr("Enable asynchronous shader compilation"),
|
||||
tr("May reduce shader stutter."));
|
||||
INSERT(Settings, gpu_clock, tr("GPU Clocks"),
|
||||
|
||||
@@ -1448,8 +1448,8 @@ BufferId BufferCache<P>::FindBuffer(DAddr device_addr, u32 size) {
|
||||
const BufferId buffer_id = page_table[page];
|
||||
if (buffer_id) {
|
||||
Buffer& buffer = slot_buffers[buffer_id];
|
||||
WaitForGpuFenceIfNeeded(buffer);
|
||||
if (buffer.IsInBounds(device_addr, size)) {
|
||||
SynchronizeBufferIfNeeded(buffer);
|
||||
return buffer_id;
|
||||
}
|
||||
}
|
||||
@@ -1457,16 +1457,28 @@ BufferId BufferCache<P>::FindBuffer(DAddr device_addr, u32 size) {
|
||||
}
|
||||
|
||||
template <class P>
|
||||
void BufferCache<P>::WaitForGpuFenceIfNeeded(Buffer& buffer) {
|
||||
if constexpr (!IS_OPENGL) {
|
||||
const bool gpu_fence_accurate = Settings::IsGPUFenceBehaviorAccurate();
|
||||
const bool gpu_fence_strict = Settings::IsGPUFenceBehaviorStrict();
|
||||
if (gpu_fence_accurate || gpu_fence_strict) {
|
||||
const u64 gpu_tick_delay = gpu_fence_strict ? 0 : 3;
|
||||
const u64 buffer_tick = buffer.getWriteTick();
|
||||
const u64 gpu_tick = runtime.KnownGpuTick();
|
||||
if (buffer_tick > gpu_tick + gpu_tick_delay) {
|
||||
runtime.Wait(buffer_tick);
|
||||
void BufferCache<P>::SynchronizeBufferIfNeeded(Buffer& buffer) {
|
||||
const bool gpu_fence_accurate = Settings::IsGPUFenceBehaviorAccurate();
|
||||
const bool gpu_fence_strict = Settings::IsGPUFenceBehaviorStrict();
|
||||
const bool should_sync = gpu_fence_accurate || gpu_fence_strict;
|
||||
if (should_sync) {
|
||||
if (gpu_fence_accurate) {
|
||||
if constexpr (!IS_OPENGL) {
|
||||
const bool should_wait = buffer.getWriteTick() > runtime.KnownGpuTick() + 3;
|
||||
if (should_wait) {
|
||||
runtime.Wait(buffer.getWriteTick());
|
||||
}
|
||||
}
|
||||
} else if (gpu_fence_strict) {
|
||||
bool should_download = true;
|
||||
if constexpr (!IS_OPENGL) {
|
||||
should_download = buffer.getWriteTick() > runtime.KnownGpuTick();
|
||||
if (should_download) {
|
||||
runtime.Wait(buffer.getWriteTick());
|
||||
}
|
||||
}
|
||||
if (should_download) {
|
||||
DownloadBufferMemory(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1688,9 +1700,6 @@ void BufferCache<P>::ImmediateUploadMemory([[maybe_unused]] Buffer& buffer,
|
||||
if (immediate_buffer.empty()) {
|
||||
immediate_buffer = ImmediateBuffer(largest_copy);
|
||||
}
|
||||
if (Settings::values.enable_gpu_buffer_readback.GetValue()) {
|
||||
DownloadBufferMemory(buffer, device_addr, copy.size);
|
||||
}
|
||||
device_memory.ReadBlockUnsafe(device_addr, immediate_buffer.data(), copy.size);
|
||||
upload_span = immediate_buffer.subspan(0, copy.size);
|
||||
}
|
||||
@@ -1709,9 +1718,6 @@ void BufferCache<P>::MappedUploadMemory([[maybe_unused]] Buffer& buffer,
|
||||
for (BufferCopy& copy : copies) {
|
||||
u8* const src_pointer = staging_pointer.data() + copy.src_offset;
|
||||
const DAddr device_addr = buffer.CpuAddr() + copy.dst_offset;
|
||||
if (Settings::values.enable_gpu_buffer_readback.GetValue()) {
|
||||
DownloadBufferMemory(buffer, device_addr, copy.size);
|
||||
}
|
||||
device_memory.ReadBlockUnsafe(device_addr, src_pointer, copy.size);
|
||||
// Apply the staging offset
|
||||
copy.src_offset += upload_staging.offset;
|
||||
|
||||
@@ -435,6 +435,8 @@ private:
|
||||
|
||||
bool SynchronizeBuffer(Buffer& buffer, DAddr device_addr, u32 size);
|
||||
|
||||
void SynchronizeBufferIfNeeded(Buffer& buffer);
|
||||
|
||||
void UploadMemory(Buffer& buffer, u64 total_size_bytes, u64 largest_copy,
|
||||
std::span<BufferCopy> copies);
|
||||
|
||||
|
||||
@@ -398,9 +398,6 @@ 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)
|
||||
@@ -437,6 +434,10 @@ 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
|
||||
|
||||
+19
-11
@@ -2,10 +2,12 @@
|
||||
// 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
|
||||
@@ -76,6 +78,20 @@ 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)) {
|
||||
@@ -184,15 +200,7 @@ int main(int argc, char* argv[]) {
|
||||
// generating shaders
|
||||
setlocale(LC_ALL, "C");
|
||||
|
||||
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};
|
||||
MainWindow main_window{has_broken_vulkan};
|
||||
// After settings have been loaded by GMainWindow, apply the filter
|
||||
main_window.show();
|
||||
|
||||
|
||||
+58
-18
@@ -350,7 +350,7 @@ inline static bool isDarkMode() {
|
||||
}
|
||||
#endif // _WIN32
|
||||
|
||||
MainWindow::MainWindow(Common::ProgramArguments&& opts, bool has_broken_vulkan)
|
||||
MainWindow::MainWindow(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,33 +512,73 @@ MainWindow::MainWindow(Common::ProgramArguments&& opts, bool has_broken_vulkan)
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
} else {
|
||||
LOG_ERROR(Frontend, "Selected user {} doesn't exist", user_index);
|
||||
game_path = args[i];
|
||||
has_gamepath = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.input_profile.empty()) {
|
||||
Settings::values.players.GetValue()[0].profile_name = opts.input_profile;
|
||||
}
|
||||
|
||||
// Override fullscreen setting if gamepath or argument is provided
|
||||
if (!opts.filepath.empty() && opts.fullscreen) {
|
||||
ui->action_Fullscreen->setChecked(opts.fullscreen);
|
||||
if (has_gamepath || is_fullscreen) {
|
||||
ui->action_Fullscreen->setChecked(is_fullscreen);
|
||||
}
|
||||
|
||||
if (opts.should_launch_setup) {
|
||||
if (should_launch_setup) {
|
||||
LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::Starter), std::nullopt);
|
||||
} else {
|
||||
if (!opts.filepath.empty()) {
|
||||
BootGame(QString::fromStdString(opts.filepath), ApplicationAppletParameters());
|
||||
} else if (opts.should_launch_qlaunch) {
|
||||
if (!game_path.isEmpty()) {
|
||||
BootGame(game_path, ApplicationAppletParameters());
|
||||
} else if (should_launch_qlaunch) {
|
||||
LaunchFirmwareApplet(u64(Service::AM::AppletProgramId::QLaunch), std::nullopt);
|
||||
} else if (opts.should_launch_hlaunch) {
|
||||
} else if (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,7 +17,6 @@
|
||||
#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"
|
||||
@@ -166,7 +165,7 @@ class MainWindow : public QMainWindow {
|
||||
public:
|
||||
void filterBarSetChecked(bool state);
|
||||
void UpdateUITheme();
|
||||
explicit MainWindow(Common::ProgramArguments&& args, bool has_broken_vulkan);
|
||||
explicit MainWindow(bool has_broken_vulkan);
|
||||
~MainWindow() override;
|
||||
|
||||
bool DropAction(QDropEvent* event);
|
||||
|
||||
@@ -38,16 +38,7 @@ add_executable(yuzu-cmd
|
||||
${OPENGL_SOURCES}
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
target_link_libraries(yuzu-cmd PRIVATE common core input_common frontend_common video_core)
|
||||
if (ENABLE_OPENGL)
|
||||
target_link_libraries(yuzu-cmd PRIVATE glad)
|
||||
endif()
|
||||
|
||||
+205
-72
@@ -8,10 +8,6 @@
|
||||
#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>
|
||||
@@ -36,14 +32,9 @@
|
||||
#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 "network/room.h"
|
||||
#include "network/verify_user.h"
|
||||
|
||||
#include "yuzu_cmd/sdl_config.h"
|
||||
#include "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"
|
||||
@@ -73,6 +64,25 @@ __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:
|
||||
@@ -171,70 +181,216 @@ static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) {
|
||||
struct SdlState {
|
||||
Core::System system{};
|
||||
std::unique_ptr<EmuWindow_SDL3> emu_window;
|
||||
// settings
|
||||
Common::ProgramArguments opts = {};
|
||||
};
|
||||
|
||||
static SDL_AppResult ExecuteWithGUI(SdlState& state) {
|
||||
SdlConfig config{state.opts.config_path};
|
||||
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};
|
||||
|
||||
// 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(state.opts.log_filter.value_or(Settings::values.log_filter.GetValue()));
|
||||
filter.ParseFilterString(log_filter.value_or(Settings::values.log_filter.GetValue()));
|
||||
Common::Log::SetGlobalFilter(filter);
|
||||
|
||||
if (!state.opts.program_args.empty()) {
|
||||
Settings::values.program_args = state.opts.program_args;
|
||||
if (!program_args.empty()) {
|
||||
Settings::values.program_args = program_args;
|
||||
}
|
||||
|
||||
if (!state.opts.input_profile.empty()) {
|
||||
if (!input_profile.empty()) {
|
||||
auto& players = Settings::values.players.GetValue();
|
||||
players[0].profile_name = state.opts.input_profile;
|
||||
players[0].profile_name = input_profile;
|
||||
}
|
||||
|
||||
if (state.opts.selected_user.has_value()) {
|
||||
Settings::values.current_user = std::clamp(*state.opts.selected_user, 0, 7);
|
||||
if (selected_user.has_value()) {
|
||||
Settings::values.current_user = std::clamp(*selected_user, 0, 7);
|
||||
}
|
||||
|
||||
if (state.opts.override_gdb_port.has_value()) {
|
||||
if (override_gdb_port.has_value()) {
|
||||
Settings::values.use_gdbstub = true;
|
||||
Settings::values.gdbstub_port = *state.opts.override_gdb_port;
|
||||
Settings::values.gdbstub_port = *override_gdb_port;
|
||||
}
|
||||
|
||||
if (state.opts.force_single_core) {
|
||||
if (force_single_core) {
|
||||
Settings::values.use_multi_core = false;
|
||||
}
|
||||
|
||||
if (state.opts.force_null_render) {
|
||||
if (force_null_render) {
|
||||
Settings::values.renderer_backend = Settings::RendererBackend::Null;
|
||||
}
|
||||
|
||||
if (state.opts.filepath.empty()) {
|
||||
#ifdef _WIN32
|
||||
LocalFree(argv_w);
|
||||
#endif
|
||||
|
||||
if (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, state.opts.fullscreen);
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_GL>(&input_subsystem, state->system, fullscreen);
|
||||
break;
|
||||
#endif
|
||||
case Settings::RendererBackend::Vulkan:
|
||||
state.emu_window = std::make_unique<EmuWindow_SDL3_VK>(&input_subsystem, state.system, state.opts.fullscreen);
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_VK>(&input_subsystem, state->system, fullscreen);
|
||||
break;
|
||||
case Settings::RendererBackend::Null:
|
||||
state.emu_window = std::make_unique<EmuWindow_SDL3_Null>(&input_subsystem, state.system, state.opts.fullscreen);
|
||||
state->emu_window = std::make_unique<EmuWindow_SDL3_Null>(&input_subsystem, state->system, fullscreen);
|
||||
break;
|
||||
default:
|
||||
LOG_CRITICAL(Frontend, "Invalid renderer backend");
|
||||
@@ -243,23 +399,23 @@ static SDL_AppResult ExecuteWithGUI(SdlState& state) {
|
||||
|
||||
#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, state.opts.filepath, load_parameters);
|
||||
const Core::SystemResultStatus load_result = state->system.Load(*state->emu_window, 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 {}!", state.opts.filepath);
|
||||
LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath);
|
||||
return SDL_APP_FAILURE;
|
||||
case Core::SystemResultStatus::ErrorLoader:
|
||||
LOG_CRITICAL(Frontend, "Failed to load ROM!");
|
||||
@@ -281,14 +437,14 @@ static SDL_AppResult ExecuteWithGUI(SdlState& state) {
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
|
||||
if (state.opts.use_multiplayer) {
|
||||
if (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 {}", 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);
|
||||
LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, nickname);
|
||||
member->Join(nickname, address.c_str(), port, 0, Network::NoPreferredIP, password);
|
||||
} else {
|
||||
LOG_ERROR(Network, "Could not access RoomMember");
|
||||
return SDL_APP_FAILURE;
|
||||
@@ -296,45 +452,22 @@ static SDL_AppResult ExecuteWithGUI(SdlState& state) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
|
||||
int main()
|
||||
{
|
||||
cout << "Hello World!" << endl;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user