Merge branch 'master' into 20-empty-nca-lockup

This commit is contained in:
Shyanne
2025-12-28 14:16:39 -05:00
88 changed files with 1369 additions and 1311 deletions

View File

@@ -8,6 +8,8 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Common.Locale
@@ -158,52 +160,86 @@ namespace Ryujinx.Ava.Common.Locale
LocaleChanged?.Invoke();
}
private static LocalesJson? _localeData;
private static LocalesData? _localeData;
private static Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode)
{
Dictionary<LocaleKeys, string> localeStrings = new();
_localeData ??= EmbeddedResources.ReadAllText("Ryujinx/Assets/Locale.json")
.Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson));
foreach (LocalesEntry locale in _localeData.Value.Locales)
if (_localeData is null)
{
if (locale.Translations.Count < _localeData.Value.Languages.Count)
Dictionary<string, LocalesJson> locales = [];
foreach (string uri in EmbeddedResources.GetAllAvailableResources("Ryujinx/Assets/Locales", ".json"))
{
throw new Exception(
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
string path = uri[..^".json".Length];
path = path.Replace('.', '/');
path = path.Append(".json");
locales.TryAdd(Path.GetFileName(path), EmbeddedResources.ReadAllText(path)
.Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson)));
}
if (locale.Translations.Count > _localeData.Value.Languages.Count)
_localeData = new LocalesData
{
throw new Exception(
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
Languages = EmbeddedResources.ReadAllText("Ryujinx/Assets/Languages.json")
.Into(it => JsonHelper.Deserialize(it, LanguagesJsonContext.Default.LanguagesJson)).Languages.Keys.ToList(),
LocalesFiles = locales
};
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
continue;
}
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val)
? val
: locale.Translations[DefaultLanguageCode];
if (string.IsNullOrEmpty(str))
foreach (LocalesJson file in _localeData.Value.LocalesFiles.Values)
{
foreach (LocalesEntry locale in file.Locales)
{
throw new Exception(
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
}
if (locale.Translations.Count < _localeData.Value.Languages.Count)
{
throw new Exception(
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
localeStrings[localeKey] = str;
if (locale.Translations.Count > _localeData.Value.Languages.Count)
{
throw new Exception(
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
continue;
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val)
? val
: locale.Translations[DefaultLanguageCode];
if (string.IsNullOrEmpty(str))
{
throw new Exception(
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
}
localeStrings[localeKey] = str;
}
}
return localeStrings;
}
}
public struct LocalesJson
public struct LocalesData
{
public List<string> Languages { get; set; }
public Dictionary<string, LocalesJson> LocalesFiles { get; set; }
}
public struct LanguagesJson
{
public Dictionary<string, string> Languages { get; set; }
}
public struct LocalesJson
{
public List<LocalesEntry> Locales { get; set; }
}
@@ -216,4 +252,8 @@ namespace Ryujinx.Ava.Common.Locale
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(LocalesJson))]
internal partial class LocalesJsonContext : JsonSerializerContext;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(LanguagesJson))]
internal partial class LanguagesJsonContext : JsonSerializerContext;
}

View File

@@ -580,5 +580,10 @@ namespace Ryujinx.Headless
{
return AccountSaveDataManager.GetLastUsedUser();
}
public void TakeScreenshot()
{
throw new NotImplementedException();
}
}
}

View File

@@ -17,6 +17,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless;
using Ryujinx.SDL3.Common;
@@ -46,7 +47,7 @@ namespace Ryujinx.Ava
public static int Main(string[] args)
{
Version = ReleaseInformation.Version;
if (OperatingSystem.IsWindows())
{
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
@@ -55,8 +56,11 @@ namespace Ryujinx.Ava
return 0;
}
if (Environment.CurrentDirectory.StartsWithIgnoreCase("C:\\Program Files") ||
Environment.CurrentDirectory.StartsWithIgnoreCase("C:\\Program Files (x86)"))
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var 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);
return 0;
@@ -73,11 +77,23 @@ namespace Ryujinx.Ava
}
}
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
// TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception.
// This is undesirable and causes very odd behavior during development (the process stops responding,
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.
// This needs to be investigated, but calling prctl() is better than modifying system-wide settings or leaving this be.
if (!coreDumpArg)
{
OsUtils.SetCoreDumpable(false);
}
PreviewerDetached = true;
if (args.Length > 0 && args[0] is "--no-gui" or "nogui")
if (noGuiArg)
{
HeadlessRyujinx.Entrypoint(args[1..]);
HeadlessRyujinx.Entrypoint(args);
return 0;
}
@@ -112,6 +128,14 @@ namespace Ryujinx.Ava
: [Win32RenderingMode.Software]
});
private static bool ConsumeCommandLineArgument(ref string[] args, string targetArgument)
{
List<string> argList = [.. args];
bool found = argList.Remove(targetArgument);
args = argList.ToArray();
return found;
}
private static void Initialize(string[] args)
{
// Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched
@@ -177,7 +201,6 @@ namespace Ryujinx.Ava
}
}
public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false)
{
if (string.IsNullOrEmpty(gameId))

View File

@@ -134,7 +134,7 @@
</ItemGroup>
<ItemGroup>
<None Remove="Assets\locales.json" />
<None Remove="Assets\**\*.json" />
<None Remove="Assets\Styles\Styles.xaml" />
<None Remove="Assets\Styles\Themes.xaml" />
<None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
@@ -156,8 +156,8 @@
<EmbeddedResource Include="..\..\docs\compatibility.csv" LogicalName="RyujinxGameCompatibilityList">
<Link>Assets\RyujinxGameCompatibility.csv</Link>
</EmbeddedResource>
<EmbeddedResource Include="..\..\assets\locales.json">
<Link>Assets\Locale.json</Link>
<EmbeddedResource Include="..\..\assets\**\*.json">
<LinkBase>Assets</LinkBase>
</EmbeddedResource>
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
@@ -178,6 +178,6 @@
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\assets\locales.json" />
<AdditionalFiles Include="..\..\assets\Locales\*.json" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration
{
[JsonConverter(typeof(TypedStringEnumConverter<AudioBackend>))]
[JsonConverter(typeof(JsonStringEnumConverter<AudioBackend>))]
public enum AudioBackend
{
Dummy,

View File

@@ -1,10 +1,9 @@
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.SystemState;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.System
{
[JsonConverter(typeof(TypedStringEnumConverter<Language>))]
[JsonConverter(typeof(JsonStringEnumConverter<Language>))]
public enum Language
{
Japanese,

View File

@@ -1,10 +1,9 @@
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.SystemState;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.System
{
[JsonConverter(typeof(TypedStringEnumConverter<Region>))]
[JsonConverter(typeof(JsonStringEnumConverter<Region>))]
public enum Region
{
Japan,

View File

@@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.UI
{
[JsonConverter(typeof(TypedStringEnumConverter<FocusLostType>))]
[JsonConverter(typeof(JsonStringEnumConverter<FocusLostType>))]
public enum FocusLostType
{
DoNothing,

View File

@@ -1,9 +1,8 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.UI
{
[JsonConverter(typeof(TypedStringEnumConverter<UpdaterType>))]
[JsonConverter(typeof(JsonStringEnumConverter<UpdaterType>))]
public enum UpdaterType
{
Off,

View File

@@ -327,5 +327,10 @@ namespace Ryujinx.Ava.UI.Applet
return profile;
}
public void TakeScreenshot()
{
_parent.ViewModel.AppHost.ScreenshotRequested = true;
}
}
}

View File

@@ -56,6 +56,8 @@ namespace Ryujinx.Ava
if (OperatingSystem.IsMacOS())
{
// Switches macOS key held behavior to repeat the input key instead of showing the character accents menu (like doing on an iOS keyboard would).
// https://macos-defaults.com/keyboard/applepressandholdenabled.html
Process.Start("/usr/bin/defaults", "write org.ryujinx.Ryujinx ApplePressAndHoldEnabled -bool false");
}
}

View File

@@ -1333,7 +1333,10 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public void TakeScreenshot() => AppHost.ScreenshotRequested = true;
public void TakeScreenshot()
{
AppHost.ScreenshotRequested = true;
}
public void HideUi() => ShowMenuAndStatusBar = false;

View File

@@ -85,29 +85,16 @@ namespace Ryujinx.Ava.UI.Views.Main
private static IEnumerable<MenuItem> GenerateLanguageMenuItems()
{
const string LocalePath = "Ryujinx/Assets/Locale.json";
const string LanguagesPath = "Ryujinx/Assets/Languages.json";
string languageJson = EmbeddedResources.ReadAllText(LocalePath);
string languageJson = EmbeddedResources.ReadAllText(LanguagesPath);
string currentLanguageCode = LocaleManager.Instance.CurrentLanguageCode;
LocalesJson locales = JsonHelper.Deserialize(languageJson, LocalesJsonContext.Default.LocalesJson);
LanguagesJson languages = JsonHelper.Deserialize(languageJson, LanguagesJsonContext.Default.LanguagesJson);
foreach (string language in locales.Languages)
foreach ((string code, string language) in languages.Languages)
{
int index = locales.Locales.FindIndex(x => x.ID == "Language");
string languageName;
if (index == -1)
{
languageName = language;
}
else
{
string tr = locales.Locales[index].Translations[language];
languageName = string.IsNullOrEmpty(tr)
? language
: tr;
}
string languageName = string.IsNullOrEmpty(language) ? code : language;
MenuItem menuItem = new()
{