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;
[STAThread]
public static int Main(string[] args)
{
Version = ReleaseInformation.Version;
@@ -54,17 +55,39 @@ namespace Ryujinx.Ava
{
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;
}
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
string onedriveFiles = Environment.GetEnvironmentVariable("Onedrive");
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) ||
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;
}
@@ -74,10 +97,70 @@ namespace Ryujinx.Ava
// ...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))
{
_ = 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;
}
}
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 coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
@@ -310,7 +393,7 @@ namespace Ryujinx.Ava
"never" => HideCursorMode.Never,
"onidle" => HideCursorMode.OnIdle,
"always" => HideCursorMode.Always,
_ => ConfigurationState.Instance.HideCursor,
_ => ConfigurationState.Instance.HideCursor
};
// Check if memoryManagerMode was overridden.

View File

@@ -1070,6 +1070,23 @@ namespace Ryujinx.Ava.Systems.PlayReport
_ => 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.")
.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}";

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;
}
}
}