Compare commits

..

3 Commits

Author SHA1 Message Date
Mabel
81468c1d25 Discord Rich Presence: Tomodachi Life LTD and Animal Crossing New Horizons (#103)
Adds Discord Rich Presence for Tomodachi Life: Living the Dream, Tomodachi Life: Living the Dream - Welcome Version, and Animal Crossing: New Horizons

Tomodachi Life Rich Presence uses your total Mii count, and your island level
![image](/attachments/46c20fba-f092-4f8c-af8d-c71340d94e78)
Animal Crossing New Horizons Rich Presence uses your island name
![image](/attachments/f9dfbeaa-86a3-4989-b74a-d65b1d8e6260)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/103
2026-05-20 13:38:57 +00:00
Max
004a12005e UI: Included more launch checks (#4)
- Added Onedrive folder check for Windows.
- Added iCloud and Downloads folder checks for macOS.
- Added sudo checks for macOS/Linux.
- Added dialogue prompts for macOS/Linux.
- Added unofficial build warning for detected Flatpak install.

These checks have console fallbacks in case the GUI decides it doesn't want to work that day.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/4
2026-05-20 13:37:05 +00:00
Mabel
a3eda287b5 Fix Clipboard Copy Operation Crash (#108)
Fixes a COM exception crash related to clipboard copy events from changes in Avalonia 11.3

Solves [Ryubing/Issues#294](https://github.com/Ryubing/Issues/issues/294) caused by Avalonia 11.3's changes, see [AvaloniaUI/Avalonia#20007](https://github.com/AvaloniaUI/Avalonia/issues/20007) for more information

I've only tested this on Windows, no idea if this has issues in MacOS or Linux, or if it's even a problem there at all.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/108
2026-05-20 13:22:56 +00:00
5 changed files with 211 additions and 6 deletions

View File

@@ -46,6 +46,7 @@ namespace Ryujinx.Ava
private const uint MbIconwarning = 0x30; private const uint MbIconwarning = 0x30;
[STAThread]
public static int Main(string[] args) public static int Main(string[] args)
{ {
Version = ReleaseInformation.Version; Version = ReleaseInformation.Version;
@@ -54,17 +55,39 @@ namespace Ryujinx.Ava
{ {
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
{ {
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning); Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run on an outdated version of Windows. Exiting...");
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
"You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n",
$"Ryujinx {Version}", MbIconwarning);
return 0; return 0;
} }
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string onedriveFiles = Environment.GetEnvironmentVariable("Onedrive");
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); string onedriveConsumerFiles = Environment.GetEnvironmentVariable("OnedriveConsumer");
string onedriveCommercialFiles = Environment.GetEnvironmentVariable("OnedriveCommercial");
// Apparently not everyone has OneDrive shoved onto their system.
if ((onedriveFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveFiles))
|| (onedriveConsumerFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveConsumerFiles))
|| (onedriveCommercialFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveCommercialFiles)))
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from a OneDrive folder. Exiting...");
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
"Ryujinx is not intended to be run from a OneDrive folder. Please move it out and relaunch.",
$"Ryujinx {Version}", MbIconwarning);
return 0;
}
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) || if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) ||
Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86)) Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86))
{ {
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning); Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Program Files folder. Exiting...");
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
"Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.",
$"Ryujinx {Version}", MbIconwarning);
return 0; return 0;
} }
@@ -74,10 +97,70 @@ namespace Ryujinx.Ava
// ...but this reads like it checks if the current is in/has the Windows admin role? lol // ...but this reads like it checks if the current is in/has the Windows admin role? lol
if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
{ {
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.", $"Ryujinx {Version}", MbIconwarning); Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.",
$"Ryujinx {Version}", MbIconwarning);
return 0; return 0;
} }
} }
else // Unix
{
// sudo check
[DllImport("libc")]
static extern uint geteuid();
bool root = geteuid().Equals(0);
if (OperatingSystem.IsMacOS())
{
if (root)
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
"Ryujinx is not intended to be run as administrator.", "Ok");
return 0;
}
string downloadFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
if (Environment.CurrentDirectory.StartsWithIgnoreCase(downloadFiles))
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Downloads folder. Exiting...");
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
"Ryujinx is not intended to be run from the Downloads folder.", "Ok");
return 0;
}
string icloudFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library/Mobile Documents/com~apple~CloudDocs");
if (Environment.CurrentDirectory.StartsWithIgnoreCase(icloudFiles))
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the iCloud folder. Exiting...");
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
"Ryujinx is not intended to be run from the iCloud folder.", "Ok");
return 0;
}
}
if (OperatingSystem.IsLinux())
{
if (root)
{
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
LinuxSDLInterop.SimpleMessageBox($"Ryujinx {Version}", "Ryujinx is not intended to be run as administrator.");
return 0;
}
string container = Environment.GetEnvironmentVariable("container");
if (container is not null && container.EqualsIgnoreCase("flatpak"))
{
Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
Logger.Warning?.PrintMsg(LogClass.Application, "This is very likely an unofficial build, Ryujinx does NOT have a flatpak!");
Logger.Info?.PrintMsg(LogClass.Application, "Please visit https://ryujinx.app/ for our official builds.");
Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
}
}
}
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui"); bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps"); bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
@@ -310,7 +393,7 @@ namespace Ryujinx.Ava
"never" => HideCursorMode.Never, "never" => HideCursorMode.Never,
"onidle" => HideCursorMode.OnIdle, "onidle" => HideCursorMode.OnIdle,
"always" => HideCursorMode.Always, "always" => HideCursorMode.Always,
_ => ConfigurationState.Instance.HideCursor, _ => ConfigurationState.Instance.HideCursor
}; };
// Check if memoryManagerMode was overridden. // Check if memoryManagerMode was overridden.

View File

@@ -1070,6 +1070,23 @@ namespace Ryujinx.Ava.Systems.PlayReport
_ => FormattedValue.ForceReset _ => FormattedValue.ForceReset
}; };
private static FormattedValue TomodachiLifeLTD_Status(SingleValue value)
{
MessagePackObject messagePackObject = value.Matched.PackedValue;
MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary();
int miiCount = messagePackObjectDictionary["MiiNum"].AsInt32();
int fountainLevel = messagePackObjectDictionary["FountainLevel"].AsInt32();
return $"Looking after {"Mii".ToQuantity(miiCount)}, with an island level of {fountainLevel}";
}
private static FormattedValue AnimalCrossingNewHorizons_AppCommon(SingleValue value)
{
MessagePackObject messagePackObject = value.Matched.PackedValue;
MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary();
return $"Living on {messagePackObjectDictionary["LandName"].AsString()} Island";
}
} }
} }

View File

@@ -119,6 +119,19 @@ namespace Ryujinx.Ava.Systems.PlayReport
"based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.") "based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.")
.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame) .AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
) )
.AddSpec(
[ "010051f0207b2000", "0100ca502552a000" ], // Tomodachi Life: Living the Dream + Demo
spec => spec
.WithDescription(
"based on your total Mii count and island level.")
.AddValueFormatter("Common", TomodachiLifeLTD_Status)
)
.AddSpec(
"01006f8002326000", // Animal Crossing New Horizons
spec => spec
.WithDescription("based on your island name.")
.AddValueFormatter("AppCmn", AnimalCrossingNewHorizons_AppCommon)
)
); );
private static string Playing(string game) => $"Playing {game}"; private static string Playing(string game) => $"Playing {game}";

View File

@@ -0,0 +1,30 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.UI.Helpers
{
public class LinuxSDLInterop
{
// TODO: add a parameter for prompt style
// TODO: look into adding text for the button
// TODO: check success of prompt box
public static int SimpleMessageBox(string caption, string text)
{
const string sdl = "SDL2";
[DllImport(sdl)]
static extern int SDL_Init(uint flags);
[DllImport(sdl, CallingConvention = CallingConvention.Cdecl)]
static extern int SDL_ShowSimpleMessageBox(uint flags, string title, string message, IntPtr window);
[DllImport(sdl)]
static extern void SDL_Quit();
SDL_Init(0);
SDL_ShowSimpleMessageBox(32 /* 32 = warning style */, caption, text, IntPtr.Zero);
SDL_Quit();
return 0;
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.UI.Helpers
{
public class macOSNativeInterop
{
// TODO: add a parameter for prompt style
// TODO: check success of prompt box
public static int SimpleMessageBox(string caption, string text, string button)
{
// Grab what we need to make the message box.
const string ObjCRuntime = "/usr/lib/libobjc.A.dylib";
const string FoundationFramework = "/System/Library/Frameworks/Foundation.framework/Foundation";
const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit";
[DllImport(ObjCRuntime, EntryPoint = "sel_registerName")]
static extern IntPtr GetSelector(string name);
[DllImport(ObjCRuntime, EntryPoint = "objc_getClass")]
static extern IntPtr GetClass(string name);
[DllImport(FoundationFramework, EntryPoint = "objc_msgSend")]
static extern IntPtr SendMessage(IntPtr target, IntPtr selector);
[DllImport(FoundationFramework, EntryPoint = "objc_msgSend")]
static extern IntPtr SendMessageWithParameter(IntPtr target, IntPtr selector, IntPtr param);
[DllImport(ObjCRuntime)]
static extern IntPtr dlopen(string path, int mode);
dlopen(AppKitFramework, 0x1); // have to invoke AppKit so that NSAlert doesn't return a null pointer
IntPtr NSStringClass = GetClass("NSString");
IntPtr Selector = GetSelector("stringWithUTF8String:");
IntPtr SharedApp = SendMessage(GetClass("NSApplication"), GetSelector("sharedApplication"));
IntPtr NSAlert = SendMessage(GetClass("NSAlert"), GetSelector("alloc"));
IntPtr AlertInstance = SendMessage(NSAlert, GetSelector("init"));
// Create caption, text, and button text.
IntPtr boxCaption = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(caption));
IntPtr boxText = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(text));
IntPtr boxButton = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(button));
// Set up the window.
SendMessageWithParameter(SharedApp, GetSelector("setActivationPolicy:"), IntPtr.Zero); // Give it a window.
SendMessageWithParameter(SharedApp, GetSelector("activateIgnoringOtherApps:"), (IntPtr) 1); // Force it to the front.
// Set up the message box.
SendMessageWithParameter(AlertInstance, GetSelector("setAlertStyle:"), IntPtr.Zero); // Set style to warning.
SendMessageWithParameter(AlertInstance, GetSelector("setMessageText:"), boxCaption);
SendMessageWithParameter(AlertInstance, GetSelector("setInformativeText:"), boxText);
SendMessageWithParameter(AlertInstance, GetSelector("addButtonWithTitle:"), boxButton);
// Send prompt to user, then clean up.
SendMessage(AlertInstance, GetSelector("runModal"));
SendMessage(AlertInstance, GetSelector("release"));
return 0;
}
}
}