mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-17 05:43:15 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1eea736615 |
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h4 align="center"><b>Eden</b> is a free and opensource (FOSS) Switch 1 emulator, derived from Yuzu and Sudachi - started by developer Camille LaVey.
|
||||
<h4 align="center"><b>Eden</b> is a free and opensource (FOSS) Switch 1 emulator, derived from Yuzu, Citron, and Sudachi - started by developer Camille LaVey.
|
||||
It's written in C++ with portability in mind, with builds for Windows, Linux, macOS, Android, FreeBSD and more.
|
||||
</h4>
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Design Overview
|
||||
|
||||
Modern game consoles require heavy power to be emulated appropriatedly. This is why the emulator uses an approach known as HLE (High-Level-Emulation), in a nuthsell: Instead of accurately emulating every subsystem that forms part of a component, emulate the resulting visible I/O interface instead.
|
||||
|
||||
For example, take a disk write, instead of emulating a proper SD card we instead use the C++ standard library for I/O. Additionally we use the abstractions provided by the `fs` service to "lie" to programs about certain SD card properties. Notably this includes making up sizes for the fake SD card, giving "realistic" values or expected outputs for a given card, and so on. And instead of writing to an actual SD card, the emulator simply writes to a file.
|
||||
|
||||
This also means grand part of the emulator consists of just re-implementing firmware but using HLE primitives; for example audio doesn't go to an emulated audio device, but rather gets processed on the fly by a dedicated service and then passed to SDL3/cubeb/etc.
|
||||
|
||||
As such, many of the systems implemented are not 100% accurate to the original software, but they're "good enough" to pass as being so. While we do strive to maintain high compatibility (especially with homebrew), there are realistic limitations to these approaches.
|
||||
|
||||
No formal fuzzing or formal verification has been done on the emulator as a whole, this means there may be hundreds of bugs hiding in each subsystem. As such, output may also not correctly match due to those aforementioned issues.
|
||||
|
||||
## src/android/
|
||||
|
||||
Entire Android frontend, written mostly in Kotlin and generally having asinine hacks (thanks Android) due to the particularly horrific (and particular way) to do things.
|
||||
|
||||
## src/audio_core/
|
||||
|
||||
Handles everything related to audio, this is where most of the filtering and processing occurs (CPU intensive task!).
|
||||
|
||||
## src/common/
|
||||
|
||||
The [common](../src/common) folder contains just your basic pollyfills for whatever missing functionality. We heavily encourage new PRs to make use of one of the dependencies, or the standard C++ library. Minimizing the amount of things we reinvent the wheel for is always a good thing.
|
||||
|
||||
## src/core/
|
||||
|
||||
The [core folder](../src/core) is the main heart of the emulator, while we don't officially support having other frontends out of the box (RetroArch, for example); any prospective developer can reuse this library (compiled as one by CMake) to stitch up their own RetroArch core, for example.
|
||||
|
||||
## src/dynarmic/
|
||||
|
||||
See [Dynarmic](./dynarmic).
|
||||
|
||||
## src/hid_core/
|
||||
|
||||
Input, joysticks, controllers and everything related to input devices.
|
||||
|
||||
## src/shader_recompiler/
|
||||
|
||||
Dedicated shader recompiler to translate Maxwell assembly code to either SPIR-V, GLSL or GLASM.
|
||||
|
||||
## src/video_core/
|
||||
|
||||
Most of the things here have their own dedicated section. In short this is basically the entire Tegra NVIDIA Maxwell GPU emulation. Additionally it includes some [extra effects](../src/video_core/host_ahders) to emulate MSAA, D24 copies or as polyfill.
|
||||
|
||||
Available backends are: Null, Vulkan, and OpenGL.
|
||||
|
||||
See [NVIDIA GPU](./NvidiaGpu.md).
|
||||
+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
|
||||
|
||||
@@ -20,6 +20,7 @@ This contains documentation created by developers. This contains build instructi
|
||||
- **[Driver Bugs](./DriverBugs.md)**
|
||||
- **[Building Older Commits](./build/OlderCommits.md)**
|
||||
- Subsystems:
|
||||
- **[Design Overview](./DesignOverview.md)**
|
||||
- **[Dynarmic](./dynarmic/README.md)**
|
||||
- **[HOS Kernel](./HosKernel.md)**
|
||||
- **[Settings](./Settings.md)**
|
||||
|
||||
+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.
|
||||
|
||||
@@ -77,3 +77,41 @@ Use this guide to get starting using the Eden emulator.
|
||||
8. Double-Click the main window to add the folder containing your games.
|
||||
9. Go to *Emulation > Configure > Input* and set up your controller. Click **OK** to close the dialog window.
|
||||
10. Double-Click a game to run it.
|
||||
|
||||
## macOS
|
||||
|
||||
Current macOS support is still experimental and very reliant on MoltenVK developments, plans have shifted to properly provide support for KosmicKrisp and similar new GPU endeavours, but macOS users still are bound to MoltenVK itself.
|
||||
|
||||
Users of macOS may wish to use [Asahi Linux](https://wiki.gentoo.org/wiki/Project:Asahi/Guide) for the rising KosmicKrisp support.
|
||||
|
||||
As of writing, neither macOS nor Asahi has support for NCE; additionally Asahi has extraneous paging bugs with fastmem.
|
||||
|
||||
### Allowing Eden to Run on MacOS
|
||||
|
||||
Use this guide when you need to allow Eden to run on a Mac system, but are being blocked by Apple Security policy.
|
||||
|
||||
**Click [Here](https://evilperson1337.notion.site/Allowing-Eden-to-Run-on-MacOS-2b057c2edaf681fea63dc81027efeffd) for a version of this guide with images & visual elements.**
|
||||
|
||||
---
|
||||
|
||||
##### Pre-Requisites
|
||||
|
||||
- Permissions to modify settings in MacOS
|
||||
|
||||
---
|
||||
|
||||
#### Why am I Seeing This?
|
||||
|
||||
Recent versions of MacOS (Catalina & newer) introduced the **Gatekeeper** security functionality, requiring software to be signed by Apple or a trusted (aka - paying) developer. If the signature isn't on the list of trusted ones, it will stop the program from executing and display the message above.
|
||||
|
||||
---
|
||||
|
||||
#### Steps
|
||||
|
||||
1. Open the *System Settings* panel.
|
||||
2. Navigate to *Privacy & Security*.
|
||||
3. Scroll down and observe the following message under the **Security** settings.
|
||||
4. Select **Open Anyway** to tell your Mac that you trust the application.
|
||||
5. You will now get another window appearing to verify you want to open Eden. Select **Open Anyway**.
|
||||
6. You will be prompted for your password to authorize the request. Enter the credentials of an account that has permission to modify settings and press **OK**.
|
||||
7. Eden will now open and any subsequent launches of the program will not prompt this.
|
||||
|
||||
+3
-1
@@ -10,13 +10,15 @@ A copy of this handbook is [available online](https://git.eden-emu.dev/eden-emu/
|
||||
|
||||
- **[The Basics](Basics.md)**
|
||||
- **[Quickstart](./QuickStart.md)**
|
||||
- **[Windows](./QuickStart.md#windows)**
|
||||
- **[Steamdeck](./QuickStart.md#steamdeck)**
|
||||
- **[macOS](./QuickStart.md#macos)**
|
||||
- **[Settings](./Settings.md)**
|
||||
- **[Controllers](./Controllers.md)**
|
||||
- **[Controller profiles](./Controllers.md#configuring-controller-profiles)**
|
||||
- **[Audio](Audio.md)**
|
||||
- **[Graphics](Graphics.md)**
|
||||
- **[Installing Mods](./Mods.md)**
|
||||
- **[Run On macOS](./RunOnMacOS.md)**
|
||||
- **[Data, Savefiles and Storage](Storage.md)**
|
||||
- **[Orphaned Profiles](Orphaned.md)**
|
||||
- **[Troubleshooting](./Troubleshoot.md)**
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# User Handbook - Run on macOS
|
||||
|
||||
Current macOS support is still experimental and very reliant on MoltenVK developments, plans have shifted to properly provide support for KosmicKrisp and similar new GPU endeavours, but macOS users still are bound to MoltenVK itself.
|
||||
|
||||
Users of macOS may wish to use [Asahi Linux](https://wiki.gentoo.org/wiki/Project:Asahi/Guide) for the rising KosmicKrisp support.
|
||||
|
||||
As of writing, neither macOS nor Asahi has support for NCE; additionally Asahi has extraneous paging bugs with fastmem.
|
||||
|
||||
## Allowing Eden to Run on MacOS
|
||||
|
||||
Use this guide when you need to allow Eden to run on a Mac system, but are being blocked by Apple Security policy.
|
||||
|
||||
**Click [Here](https://evilperson1337.notion.site/Allowing-Eden-to-Run-on-MacOS-2b057c2edaf681fea63dc81027efeffd) for a version of this guide with images & visual elements.**
|
||||
|
||||
---
|
||||
|
||||
#### Pre-Requisites
|
||||
|
||||
- Permissions to modify settings in MacOS
|
||||
|
||||
---
|
||||
|
||||
### Why am I Seeing This?
|
||||
|
||||
Recent versions of MacOS (Catalina & newer) introduced the **Gatekeeper** security functionality, requiring software to be signed by Apple or a trusted (aka - paying) developer. If the signature isn't on the list of trusted ones, it will stop the program from executing and display the message above.
|
||||
|
||||
---
|
||||
|
||||
### Steps
|
||||
|
||||
1. Open the *System Settings* panel.
|
||||
2. Navigate to *Privacy & Security*.
|
||||
3. Scroll down and observe the following message under the **Security** settings.
|
||||
4. Select **Open Anyway** to tell your Mac that you trust the application.
|
||||
5. You will now get another window appearing to verify you want to open Eden. Select **Open Anyway**.
|
||||
6. You will be prompted for your password to authorize the request. Enter the credentials of an account that has permission to modify settings and press **OK**.
|
||||
7. Eden will now open and any subsequent launches of the program will not prompt this.
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -156,9 +154,7 @@ if(WIN32)
|
||||
windows/timer_resolution.h)
|
||||
target_link_libraries(common PRIVATE ntdll)
|
||||
endif()
|
||||
if (MSVC)
|
||||
target_link_libraries(common PRIVATE getopt)
|
||||
endif()
|
||||
|
||||
if(NOT WIN32)
|
||||
target_sources(common PRIVATE signal_chain.cpp signal_chain.h)
|
||||
endif()
|
||||
|
||||
@@ -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[]);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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