diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 433e65cb2..1554b8f6b 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -202,7 +202,7 @@ jobs: macos_release: name: Release MacOS universal - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e11f6edf..072c6bf2f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -183,7 +183,7 @@ jobs: macos_release: name: Release MacOS universal - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 diff --git a/docs/compatibility.csv b/docs/compatibility.csv index 3528652c8..570c93618 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -1070,6 +1070,7 @@ 010017B0102A8000,"Emma: Lost in Memories",nvdec,playable,2021-01-28 16:19:10 010068300E08E000,"Enchanted in the Moonlight - Kiryu, Chikage & Yukinojo -",gpu;nvdec,ingame,2022-11-20 16:18:45 01007A4008486000,"Enchanting Mahjong Match",gpu,ingame,2020-04-17 22:01:31 +0100EF901E552000,"ENDER MAGNOLIA: Bloom in the Mist",deadlock,boots,2025-01-22 17:59:00 01004F3011F92000,"Endless Fables: Dark Moor",gpu;nvdec,ingame,2021-03-07 15:31:03 010067B017588000,"Endless Ocean™ Luminous",services-horizon;crash,ingame,2024-05-30 02:05:57 0100B8700BD14000,"Energy Cycle Edge",services,ingame,2021-11-30 05:02:31 @@ -2681,7 +2682,7 @@ 01005EA01C0FC000,"SONIC X SHADOW GENERATIONS",crash,ingame,2025-01-07 04:20:45 010064F00C212000,"Soul Axiom Rebooted",nvdec;slow,ingame,2020-09-04 12:41:01 0100F2100F0B2000,"Soul Searching",,playable,2020-07-09 18:39:07 -01008F2005154000,"South Park™: The Fractured but Whole™ - Standard Edition",slow;online-broken,playable,2024-07-08 17:47:28 +01008F2005154000,"South Park™: The Fractured but Whole™ - Standard Edition",slow;online-broken;vulkan-backend-bug;gpu,ingame,2025-01-21 17:35:10 0100B9F00C162000,"Space Blaze",,playable,2020-08-30 16:18:05 010005500E81E000,"Space Cows",UE4;crash,menus,2020-06-15 11:33:20 0100707011722000,"Space Elite Force",,playable,2020-11-27 15:21:05 diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs index e6fe74d53..fbb19767c 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs @@ -78,5 +78,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller /// Controller Rumble Settings /// public RumbleConfigController Rumble { get; set; } + + /// + /// Controller LED Settings + /// + public LedConfigController Led { get; set; } } } diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/LedConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/LedConfigController.cs new file mode 100644 index 000000000..21727d62e --- /dev/null +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/LedConfigController.cs @@ -0,0 +1,15 @@ +namespace Ryujinx.Common.Configuration.Hid.Controller +{ + public class LedConfigController + { + /// + /// Packed RGB int of the color + /// + public uint LedColor { get; set; } + + /// + /// Enable LED color changing by the emulator + /// + public bool EnableLed { get; set; } + } +} diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index dca634d9d..e0cb12026 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -14,6 +14,7 @@ namespace Ryujinx.Common { switch (currentBackend) { + case GraphicsBackend.Metal when !OperatingSystem.IsMacOS(): case GraphicsBackend.OpenGl when OperatingSystem.IsMacOS(): return GraphicsBackend.Vulkan; case GraphicsBackend.Vulkan or GraphicsBackend.OpenGl or GraphicsBackend.Metal: @@ -28,18 +29,28 @@ namespace Ryujinx.Common public static readonly string[] GreatMetalTitles = [ - "01006f8002326000", // Animal Crossings: New Horizons - "01009bf0072d4000", // Captain Toad: Treasure Tracker + "010076f0049a2000", // Bayonetta "0100a5c00d162000", // Cuphead "010023800d64a000", // Deltarune + "01003a30012c0000", // LEGO City Undercover "010028600EBDA000", // Mario 3D World "0100152000022000", // Mario Kart 8 Deluxe - "01005CA01580E000", // Persona 5 - "0100187003A36000", // Pokémon: Let's Go, Evoli! + "010075a016a3a000", // Persona 4 Arena Ultimax + "0100187003A36000", // Pokémon: Let's Go, Eevee! "010003f003a34000", // Pokémon: Let's Go, Pikachu! "01008C0016544000", // Sea of Stars "01006A800016E000", // Smash Ultimate - "0100000000010000", // Super Mario Odyessy + "01006bb00c6f0000", // The Legend of Zelda: Link's Awakening + + // These ones have small issues, but those happen on Vulkan as well: + "01006f8002326000", // Animal Crossings: New Horizons + "01009bf0072d4000", // Captain Toad: Treasure Tracker + "01009510001ca000", // Fast RMX + "01005CA01580E000", // Persona 5 Royale + "0100000000010000", // Super Mario Odyssey + + //Isaac claims it has a issue in level 2, but I am not able to replicate it on my M3. More testing would be appreciated: + "010015100b514000", // Super Mario Bros. Wonder ]; public static string GetDiscordGameAsset(string titleId) @@ -47,72 +58,87 @@ namespace Ryujinx.Common public static readonly string[] DiscordGameAssetKeys = [ + //All games are in Alphabetical order by Game name. + + //Dragon Quest Franchise "010008900705c000", // Dragon Quest Builders "010042000a986000", // Dragon Quest Builders 2 - "010055d009f78000", // Fire Emblem: Three Houses - "0100a12011cc8000", // Fire Emblem: Shadow Dragon + //Fire Emblem Franchise "0100a6301214e000", // Fire Emblem Engage + "0100a12011cc8000", // Fire Emblem: Shadow Dragon + "010055d009f78000", // Fire Emblem: Three Houses "0100f15003e64000", // Fire Emblem Warriors "010071f0143ea000", // Fire Emblem Warriors: Three Hopes - - "01007e3006dda000", // Kirby Star Allies + + //Kirby Franchise "01004d300c5ae000", // Kirby and the Forgotten Land - "01006b601380e000", // Kirby's Return to Dream Land Deluxe - "01003fb00c5a8000", // Super Kirby Clash - "0100227010460000", // Kirby Fighters 2 "0100a8e016236000", // Kirby's Dream Buffet - + "0100227010460000", // Kirby Fighters 2 + "01006b601380e000", // Kirby's Return to Dream Land Deluxe + "01007e3006dda000", // Kirby Star Allies + "01003fb00c5a8000", // Super Kirby Clash + + + //The Zelda Franchise + "01000b900d8b0000", // Cadence of Hyrule + "0100ae00096ea000", // Hyrule Warriors: Definitive Edition + "01002b00111a2000", // Hyrule Warriors: Age of Calamity "01007ef00011e000", // The Legend of Zelda: Breath of the Wild "01006bb00c6f0000", // The Legend of Zelda: Link's Awakening "01002da013484000", // The Legend of Zelda: Skyward Sword HD "0100f2c0115b6000", // The Legend of Zelda: Tears of the Kingdom "01008cf01baac000", // The Legend of Zelda: Echoes of Wisdom - "01000b900d8b0000", // Cadence of Hyrule - "0100ae00096ea000", // Hyrule Warriors: Definitive Edition - "01002b00111a2000", // Hyrule Warriors: Age of Calamity + + //Luigi Franchise "010048701995e000", // Luigi's Mansion 2 HD "0100dca0064a6000", // Luigi's Mansion 3 + //Metroid Franchise "010093801237c000", // Metroid Dread "010012101468c000", // Metroid Prime Remastered - "0100000000010000", // SUPER MARIO ODYSSEY - "0100ea80032ea000", // Super Mario Bros. U Deluxe - "01009b90006dc000", // Super Mario Maker 2 - "010049900f546000", // Super Mario 3D All-Stars - "010049900F546001", // ^ 64 - "010049900F546002", // ^ Sunshine - "010049900F546003", // ^ Galaxy - "010028600ebda000", // Super Mario 3D World + Bowser's Fury - "010015100b514000", // Super Mario Bros. Wonder - "0100152000022000", // Mario Kart 8 Deluxe - "010036b0034e4000", // Super Mario Party - "01006fe013472000", // Mario Party Superstars - "0100965017338000", // Super Mario Party Jamboree + //Monster Hunter Franchise + "0100770008dd8000", // Monster Hunter Generations Ultimate + "0100b04011742000", // Monster Hunter Rise + + //Mario Franchise "01006d0017f7a000", // Mario & Luigi: Brothership + "010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020 "010067300059a000", // Mario + Rabbids: Kingdom Battle "0100317013770000", // Mario + Rabbids: Sparks of Hope + "0100c9c00e25c000", // Mario Golf: Super Rush + "0100152000022000", // Mario Kart 8 Deluxe + "01006fe013472000", // Mario Party Superstars + "010019401051c000", // Mario Strikers: Battle League + "0100bde00862a000", // Mario Tennis Aces + "0100b99019412000", // Mario vs. Donkey Kong + "010049900f546000", // Super Mario 3D All-Stars + "010028600ebda000", // Super Mario 3D World + Bowser's Fury + "010049900F546001", // Super Mario 64 + "0100ea80032ea000", // Super Mario Bros. U Deluxe + "010015100b514000", // Super Mario Bros. Wonder + "010049900F546003", // Super Mario Galaxy + "01009b90006dc000", // Super Mario Maker 2 + "0100000000010000", // SUPER MARIO ODYSSEY + "010036b0034e4000", // Super Mario Party + "0100965017338000", // Super Mario Party Jamboree + "0100bc0018138000", // Super Mario RPG + "010049900F546002", // Super Mario Sunshine "0100a3900c3e2000", // Paper Mario: The Origami King "0100ecd018ebe000", // Paper Mario: The Thousand-Year Door - "0100bc0018138000", // Super Mario RPG - "0100bde00862a000", // Mario Tennis Aces - "0100c9c00e25c000", // Mario Golf: Super Rush - "010019401051c000", // Mario Strikers: Battle League - "010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020 - "0100b99019412000", // Mario vs. Donkey Kong + //Pikmin Franchise "0100aa80194b0000", // Pikmin 1 "0100d680194b2000", // Pikmin 2 "0100f4c009322000", // Pikmin 3 Deluxe "0100b7c00933a000", // Pikmin 4 + //The Pokémon Franchise "0100f4300bf2c000", // New Pokémon Snap "0100000011d90000", // Pokémon Brilliant Diamond "01001f5010dfa000", // Pokémon Legends: Arceus - "010003f003a34000", // Pokémon: Let's Go Pikachu! - "0100187003a36000", // Pokémon: Let's Go Eevee! "01003d200baa2000", // Pokémon Mystery Dungeon - Rescue Team DX "0100a3d008c5c000", // Pokémon Scarlet "01008db008c2c000", // Pokémon Shield @@ -120,24 +146,29 @@ namespace Ryujinx.Common "0100abf008968000", // Pokémon Sword "01008f6008c5e000", // Pokémon Violet "0100b3f000be2000", // Pokkén Tournament DX + "0100187003a36000", // Pokémon: Let's Go Eevee! + "010003f003a34000", // Pokémon: Let's Go Pikachu! - "01003bc0000a0000", // Splatoon 2 (US) + //Splatoon Franchise "0100f8f0000a2000", // Splatoon 2 (EU) "01003c700009c000", // Splatoon 2 (JP) + "01003bc0000a0000", // Splatoon 2 (US) "0100c2500fc20000", // Splatoon 3 "0100ba0018500000", // Splatoon 3: Splatfest World Premiere - "010040600c5ce000", // Tetris 99 - "0100277011f1a000", // Super Mario Bros. 35 - "0100ad9012510000", // PAC-MAN 99 + //NSO Membership games "0100ccf019c8c000", // F-ZERO 99 - "0100d870045b6000", // NES - Nintendo Switch Online - "01008d300c50c000", // SNES - Nintendo Switch Online - "0100c9a00ece6000", // N64 - Nintendo Switch Online - "0100e0601c632000", // N64 - Nintendo Switch Online 18+ "0100c62011050000", // GB - Nintendo Switch Online "010012f017576000", // GBA - Nintendo Switch Online + "0100c9a00ece6000", // N64 - Nintendo Switch Online + "0100e0601c632000", // N64 - Nintendo Switch Online 18+ + "0100d870045b6000", // NES - Nintendo Switch Online + "0100ad9012510000", // PAC-MAN 99 + "010040600c5ce000", // Tetris 99 + "01008d300c50c000", // SNES - Nintendo Switch Online + "0100277011f1a000", // Super Mario Bros. 35 + //Misc Nintendo 1st party games "01000320000cc000", // 1-2 Switch "0100300012f2a000", // Advance Wars 1+2: Re-Boot Camp "01006f8002326000", // Animal Crossing: New Horizons @@ -152,27 +183,32 @@ namespace Ryujinx.Common "01006a800016e000", // Super Smash Bros. Ultimate "0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore + //Bayonetta Franchise "010076f0049a2000", // Bayonetta "01007960049a0000", // Bayonetta 2 "01004a4010fea000", // Bayonetta 3 "0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon + //Persona Franchise "0100dcd01525a000", // Persona 3 Portable - "010062b01525c000", // Persona 4 Golden "010075a016a3a000", // Persona 4 Arena Ultimax + "010062b01525c000", // Persona 4 Golden "01005ca01580e000", // Persona 5 Royal "0100801011c3e000", // Persona 5 Strikers "010087701b092000", // Persona 5 Tactica - "01009aa000faa000", // Sonic Mania + //Sonic Franchise "01004ad014bf0000", // Sonic Frontiers + "01009aa000faa000", // Sonic Mania "01005ea01c0fc000", // SONIC X SHADOW GENERATIONS "01005ea01c0fc001", // ^ + //Xenoblade Franchise "0100ff500e34a000", // Xenoblade Chronicles - Definitive Edition "0100e95004038000", // Xenoblade Chronicles 2 "010074f013262000", // Xenoblade Chronicles 3 + //Misc Games "010056e00853a000", // A Hat in Time "0100fd1014726000", // Baldurs Gate: Dark Alliance "0100dbf01000a000", // Burnout Paradise Remastered @@ -185,8 +221,6 @@ namespace Ryujinx.Common "01003620068ea000", // Hand of Fate 2 "010085500130a000", // Lego City: Undercover "010073c01af34000", // LEGO Horizon Adventures - "0100770008dd8000", // Monster Hunter Generations Ultimate - "0100b04011742000", // Monster Hunter Rise "0100853015e86000", // No Man's Sky "01007bb017812000", // Portal "0100abd01785c000", // Portal 2 diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs index 3a02eb615..b50ea174d 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs @@ -118,7 +118,7 @@ namespace Ryujinx.Graphics.Gpu.Shader private static string GetDiskCachePath() { return GraphicsConfig.EnableShaderCache && GraphicsConfig.TitleId != null - ? Path.Combine(AppDataManager.GamesDirPath, GraphicsConfig.TitleId, "cache", "shader") + ? Path.Combine(AppDataManager.GamesDirPath, GraphicsConfig.TitleId.ToLower(), "cache", "shader") : null; } diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index ec0f58b01..a87453d00 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -710,9 +710,8 @@ namespace Ryujinx.HLE.FileSystem { updateNcasItem.Add((nca.Header.ContentType, entry.FullName)); } - else + else if (updateNcas.TryAdd(nca.Header.TitleId, new List<(NcaContentType, string)>())) { - updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>()); updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName)); } } @@ -898,9 +897,8 @@ namespace Ryujinx.HLE.FileSystem { updateNcasItem.Add((nca.Header.ContentType, entry.FullPath)); } - else + else if (updateNcas.TryAdd(nca.Header.TitleId, new List<(NcaContentType, string)>())) { - updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>()); updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath)); } diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs index aa57a0310..6fdfe1398 100644 --- a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs @@ -56,7 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, _serializerContext.ProfilesJson); return profilesJson.Profiles - .FindFirst(profile => profile.AccountState == AccountState.Open) + .FindFirst(profile => profile.UserId == profilesJson.LastOpened) .Convert(profileJson => new UserProfile(new UserId(profileJson.UserId), profileJson.Name, profileJson.Image, profileJson.LastModifiedTimestamp)); } diff --git a/src/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs b/src/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs index 65748be33..846c4dc4f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs @@ -244,6 +244,15 @@ namespace Ryujinx.HLE.HOS.Services.Settings return ResultCode.Success; } + [CommandCmif(68)] + // GetSerialNumber() -> buffer + public ResultCode GetSerialNumber(ServiceCtx context) + { + context.ResponseData.Write(Encoding.ASCII.GetBytes("RYU00000000000")); + + return ResultCode.Success; + } + [CommandCmif(77)] // GetDeviceNickName() -> buffer public ResultCode GetDeviceNickName(ServiceCtx context) diff --git a/src/Ryujinx.HLE/HOS/Services/Spl/IGeneralInterface.cs b/src/Ryujinx.HLE/HOS/Services/Spl/IGeneralInterface.cs index 4a2a910f8..ade67b9c0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Spl/IGeneralInterface.cs +++ b/src/Ryujinx.HLE/HOS/Services/Spl/IGeneralInterface.cs @@ -51,6 +51,9 @@ namespace Ryujinx.HLE.HOS.Services.Spl context.ResponseData.Write(configValue); + if (result == SmcResult.Success) + return ResultCode.Success; + return (ResultCode)((int)result << 9) | ResultCode.ModuleId; } diff --git a/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs b/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs index 12bfab4bb..ed22c3661 100644 --- a/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs +++ b/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Logging; +using SDL2; using System; using System.Collections.Generic; using System.Numerics; @@ -118,6 +119,11 @@ namespace Ryujinx.Input.SDL2 result |= GamepadFeaturesFlag.Rumble; } + if (SDL_GameControllerHasLED(_gamepadHandle) == SDL_bool.SDL_TRUE) + { + result |= GamepadFeaturesFlag.Led; + } + return result; } diff --git a/src/Ryujinx.Input/GamepadFeaturesFlag.cs b/src/Ryujinx.Input/GamepadFeaturesFlag.cs index 69ec23686..52e8519d1 100644 --- a/src/Ryujinx.Input/GamepadFeaturesFlag.cs +++ b/src/Ryujinx.Input/GamepadFeaturesFlag.cs @@ -24,5 +24,10 @@ namespace Ryujinx.Input /// Also named sixaxis /// Motion, + + /// + /// The LED on the back of modern PlayStation controllers (DualSense & DualShock 4). + /// + Led, } } diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 2642f603b..b2cae2348 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -674,7 +674,7 @@ namespace Ryujinx.Ava public async Task LoadGuestApplication(BlitStruct? customNacpData = null) { - InitializeSwitchInstance(); + InitEmulatedSwitch(); MainWindow.UpdateGraphicsConfig(); SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); @@ -757,6 +757,8 @@ namespace Ryujinx.Ava { romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs"); } + + Logger.Notice.Print(LogClass.Application, $"Loading unpacked content archive from '{ApplicationPath}'."); if (romFsFiles.Length > 0) { @@ -783,6 +785,8 @@ namespace Ryujinx.Ava } else if (File.Exists(ApplicationPath)) { + Logger.Notice.Print(LogClass.Application, $"Loading content archive from '{ApplicationPath}'."); + switch (Path.GetExtension(ApplicationPath).ToLowerInvariant()) { case ".xci": @@ -885,7 +889,7 @@ namespace Ryujinx.Ava Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); } - private void InitializeSwitchInstance() + private void InitEmulatedSwitch() { // Initialize KeySet. VirtualFileSystem.ReloadKeySet(); @@ -1040,7 +1044,7 @@ namespace Ryujinx.Ava _viewModel.WindowState = WindowState.FullScreen; } - if (_viewModel.WindowState is WindowState.FullScreen) + if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUI) { _viewModel.ShowMenuAndStatusBar = false; } diff --git a/src/Ryujinx/Assets/Styles/CheckboxMenuItemStyle.axaml b/src/Ryujinx/Assets/Styles/CheckboxMenuItemStyle.axaml new file mode 100644 index 000000000..13176c84f --- /dev/null +++ b/src/Ryujinx/Assets/Styles/CheckboxMenuItemStyle.axaml @@ -0,0 +1,13 @@ + + + + + diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 49ee5a01d..8a101d733 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -572,6 +572,31 @@ "zh_TW": "使用全螢幕模式啟動遊戲" } }, + { + "ID": "MenuBarOptionsStartGamesWithoutUI", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Start Games with UI Hidden", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "MenuBarOptionsStopEmulation", "Translations": { @@ -748,32 +773,7 @@ } }, { - "ID": "MenuBarTools", - "Translations": { - "ar_SA": "_الأدوات", - "de_DE": "", - "el_GR": "_Εργαλεία", - "en_US": "_Tools", - "es_ES": "_Herramientas", - "fr_FR": "_Outils", - "he_IL": "_כלים", - "it_IT": "_Strumenti", - "ja_JP": "ツール(_T)", - "ko_KR": "도구(_T)", - "no_NO": "_Verktøy", - "pl_PL": "_Narzędzia", - "pt_BR": "_Ferramentas", - "ru_RU": "_Инструменты", - "sv_SE": "V_erktyg", - "th_TH": "_เครื่องมือ", - "tr_TR": "_Araçlar", - "uk_UA": "_Інструменти", - "zh_CN": "工具(_T)", - "zh_TW": "工具(_T)" - } - }, - { - "ID": "MenuBarToolsInstallFirmware", + "ID": "MenuBarActionsInstallFirmware", "Translations": { "ar_SA": "تثبيت البرنامج الثابت", "de_DE": "Firmware installieren", @@ -798,7 +798,7 @@ } }, { - "ID": "MenuBarFileToolsInstallFirmwareFromFile", + "ID": "MenuBarActionsInstallFirmwareFromFile", "Translations": { "ar_SA": "تثبيت برنامج ثابت من XCI أو ZIP", "de_DE": "Firmware von einer XCI- oder einer ZIP-Datei installieren", @@ -823,7 +823,7 @@ } }, { - "ID": "MenuBarFileToolsInstallFirmwareFromDirectory", + "ID": "MenuBarActionsInstallFirmwareFromDirectory", "Translations": { "ar_SA": "تثبيت برنامج ثابت من مجلد", "de_DE": "Firmware aus einem Verzeichnis installieren", @@ -848,7 +848,7 @@ } }, { - "ID": "MenuBarToolsInstallKeys", + "ID": "MenuBarActionsInstallKeys", "Translations": { "ar_SA": "", "de_DE": "", @@ -873,7 +873,7 @@ } }, { - "ID": "MenuBarFileToolsInstallKeysFromFile", + "ID": "MenuBarFileActionsInstallKeysFromFile", "Translations": { "ar_SA": "", "de_DE": "", @@ -898,7 +898,7 @@ } }, { - "ID": "MenuBarFileToolsInstallKeysFromFolder", + "ID": "MenuBarFileActionsInstallKeysFromFolder", "Translations": { "ar_SA": "", "de_DE": "", @@ -923,7 +923,7 @@ } }, { - "ID": "MenuBarToolsManageFileTypes", + "ID": "MenuBarActionsManageFileTypes", "Translations": { "ar_SA": "إدارة أنواع الملفات", "de_DE": "Dateitypen verwalten", @@ -948,7 +948,7 @@ } }, { - "ID": "MenuBarToolsInstallFileTypes", + "ID": "MenuBarActionsInstallFileTypes", "Translations": { "ar_SA": "تثبيت أنواع الملفات", "de_DE": "Dateitypen installieren", @@ -973,7 +973,7 @@ } }, { - "ID": "MenuBarToolsUninstallFileTypes", + "ID": "MenuBarActionsUninstallFileTypes", "Translations": { "ar_SA": "إزالة أنواع الملفات", "de_DE": "Dateitypen deinstallieren", @@ -998,7 +998,7 @@ } }, { - "ID": "MenuBarToolsXCITrimmer", + "ID": "MenuBarActionsXCITrimmer", "Translations": { "ar_SA": "", "de_DE": "", @@ -2297,6 +2297,56 @@ "zh_TW": "從應用程式的目前配置中提取 RomFS 分區 (包含更新)" } }, + { + "ID": "GameListContextMenuExtractDataAocRomFS", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "DLC RomFS", + "es_ES": "", + "fr_FR": "RomFS de DLC", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameListContextMenuExtractDataAocRomFSToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Extract the RomFS from a selected DLC file", + "es_ES": "", + "fr_FR": "Extraire les RomFS d'un fichier DLC choisi", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "Pakk ut RomFS filene fra valgt DLC fil", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "GameListContextMenuExtractDataLogo", "Translations": { @@ -7572,6 +7622,31 @@ "zh_TW": "陀螺儀無感帶:" } }, + { + "ID": "ControllerSettingsLedColor", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Custom LED", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "ControllerSettingsSave", "Translations": { @@ -22896,6 +22971,31 @@ "zh_CN": "什么都没有", "zh_TW": "無法啟動 (Nothing)" } + }, + { + "ID": "ExtractAocListHeader", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Select a DLC to Extract", + "es_ES": "", + "fr_FR": "Choisissez un DLC à extraire", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "Velg en DLC og hente ut", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } } ] } diff --git a/src/Ryujinx/Common/ApplicationHelper.cs b/src/Ryujinx/Common/ApplicationHelper.cs index 88a991a3d..e5b4da728 100644 --- a/src/Ryujinx/Common/ApplicationHelper.cs +++ b/src/Ryujinx/Common/ApplicationHelper.cs @@ -1,6 +1,6 @@ -using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using Avalonia.Threading; +using Gommon; using LibHac; using LibHac.Account; using LibHac.Common; @@ -15,6 +15,7 @@ using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; @@ -296,13 +297,13 @@ namespace Ryujinx.Ava.Common extractorThread.Start(); } - public static void ExtractAoc(string destination, NcaSectionType ncaSectionType, string updateFilePath, string updateName) + public static void ExtractAoc(string destination, string updateFilePath, string updateName) { var cancellationToken = new CancellationTokenSource(); UpdateWaitWindow waitingDialog = new( RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), - LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, ncaSectionType, Path.GetFileName(updateFilePath)), + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, NcaSectionType.Data, Path.GetFileName(updateFilePath)), cancellationToken); Thread extractorThread = new(() => @@ -352,7 +353,7 @@ namespace Ryujinx.Ava.Common ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None; - int index = Nca.GetSectionIndexFromType(ncaSectionType, publicDataNca.Header.ContentType); + int index = Nca.GetSectionIndexFromType(NcaSectionType.Data, publicDataNca.Header.ContentType); try { @@ -410,44 +411,35 @@ namespace Ryujinx.Ava.Common } }) { - Name = "GUI.NcaSectionExtractorThread", + Name = "GUI.AocExtractorThread", IsBackground = true, }; extractorThread.Start(); } - public static async Task ExtractAoc(IStorageProvider storageProvider, NcaSectionType ncaSectionType, - string updateFilePath, string updateName) + public static async Task ExtractAoc(IStorageProvider storageProvider, string updateFilePath, string updateName) { - var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + Optional result = await storageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions { - Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], - AllowMultiple = false, + Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle] }); - if (result.Count == 0) - { - return; - } - - ExtractAoc(result[0].Path.LocalPath, ncaSectionType, updateFilePath, updateName); + if (!result.HasValue) return; + + ExtractAoc(result.Value.Path.LocalPath, updateFilePath, updateName); } public static async Task ExtractSection(IStorageProvider storageProvider, NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0) { - var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + Optional result = await storageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions { - Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle], - AllowMultiple = false, + Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle] }); - if (result.Count == 0) - { - return; - } + if (!result.HasValue) return; - ExtractSection(result[0].Path.LocalPath, ncaSectionType, titleFilePath, titleName, programIndex); + ExtractSection(result.Value.Path.LocalPath, ncaSectionType, titleFilePath, titleName, programIndex); } public static (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath, CancellationToken token) diff --git a/src/Ryujinx/Common/Models/DownloadableContentModel.cs b/src/Ryujinx/Common/Models/DownloadableContentModel.cs index ad9934bd2..de7a334ee 100644 --- a/src/Ryujinx/Common/Models/DownloadableContentModel.cs +++ b/src/Ryujinx/Common/Models/DownloadableContentModel.cs @@ -6,7 +6,7 @@ namespace Ryujinx.Ava.Common.Models public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci"; public string FileName => System.IO.Path.GetFileName(ContainerPath); - public string TitleIdStr => TitleId.ToString("x16"); + public string TitleIdStr => TitleId.ToString("x16").ToUpper(); public ulong TitleIdBase => TitleId & ~0x1FFFUL; } } diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs index 0d7e46285..f527e9811 100644 --- a/src/Ryujinx/Headless/Options.cs +++ b/src/Ryujinx/Headless/Options.cs @@ -149,7 +149,7 @@ namespace Ryujinx.Headless IgnoreMissingServices = configurationState.System.IgnoreMissingServices; if (NeedsOverride(nameof(IgnoreControllerApplet))) - IgnoreControllerApplet = configurationState.IgnoreApplet; + IgnoreControllerApplet = configurationState.System.IgnoreApplet; return; diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index b9aa402c5..cf0e6f7e6 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -230,13 +230,16 @@ namespace Ryujinx.Ava internal static void PrintSystemInfo() { Logger.Notice.Print(LogClass.Application, $"{RyujinxApp.FullAppName} Version: {Version}"); + Logger.Notice.Print(LogClass.Application, $".NET Runtime: {RuntimeInformation.FrameworkDescription}"); SystemInfo.Gather().Print(); - var enabledLogLevels = Logger.GetEnabledLevels().ToArray(); - - Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogLevels.Length is 0 - ? "" - : enabledLogLevels.JoinToString(", "))}"); + Logger.Notice.Print(LogClass.Application, $"Logs Enabled: { + Logger.GetEnabledLevels() + .FormatCollection( + x => x.ToString(), + separator: ", ", + emptyCollectionFallback: "") + }"); Logger.Notice.Print(LogClass.Application, AppDataManager.Mode == AppDataManager.LaunchMode.Custom diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 8c72d5a3c..c6a4840d2 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -123,12 +123,13 @@ MSBuild:Compile + - + @@ -150,6 +151,7 @@ + @@ -173,10 +175,5 @@ - - - UserSelectorDialog.axaml - Code - - + diff --git a/src/Ryujinx/RyujinxApp.axaml b/src/Ryujinx/RyujinxApp.axaml index aca69645a..636535ea4 100644 --- a/src/Ryujinx/RyujinxApp.axaml +++ b/src/Ryujinx/RyujinxApp.axaml @@ -16,5 +16,6 @@ + diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index a2cac35c7..d2fad58ac 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -6,7 +6,6 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Ava.UI.ViewModels.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Common; @@ -42,7 +41,7 @@ namespace Ryujinx.Ava.UI.Applet bool okPressed = false; - if (ConfigurationState.Instance.IgnoreApplet) + if (ConfigurationState.Instance.System.IgnoreApplet) return false; Dispatcher.UIThread.InvokeAsync(async () => diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 7708936ca..9fed95aa7 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -106,6 +106,10 @@ Click="ExtractApplicationRomFs_Click" Header="{ext:Locale GameListContextMenuExtractDataRomFS}" ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Controls/DlcSelectView.axaml.cs b/src/Ryujinx/UI/Controls/DlcSelectView.axaml.cs new file mode 100644 index 000000000..c011ca110 --- /dev/null +++ b/src/Ryujinx/UI/Controls/DlcSelectView.axaml.cs @@ -0,0 +1,51 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Common.Models; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.Utilities.AppLibrary; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class DlcSelectView : UserControl + { + public DlcSelectView() + { + InitializeComponent(); + } + +#nullable enable + public static async Task Show(ulong selectedTitleId, ApplicationLibrary appLibrary) +#nullable disable + { + DlcSelectViewModel viewModel = new(selectedTitleId, appLibrary); + + ContentDialog contentDialog = new() + { + PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue], + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, + Content = new DlcSelectView { DataContext = viewModel } + }; + + Style closeButton = new(x => x.Name("CloseButton")); + closeButton.Setters.Add(new Setter(WidthProperty, 80d)); + + Style closeButtonParent = new(x => x.Name("CommandSpace")); + closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, + Avalonia.Layout.HorizontalAlignment.Right)); + + contentDialog.Styles.Add(closeButton); + contentDialog.Styles.Add(closeButtonParent); + + await ContentDialogHelper.ShowAsync(contentDialog); + + return viewModel.SelectedDlc; + } + } +} diff --git a/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs index c27a3ac9b..4d021655e 100644 --- a/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs +++ b/src/Ryujinx/UI/Controls/NavigationDialogHost.axaml.cs @@ -69,7 +69,7 @@ namespace Ryujinx.Ava.UI.Controls VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient) { - var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); + NavigationDialogHost content = new(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); ContentDialog contentDialog = new() { Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle], diff --git a/src/Ryujinx/UI/Helpers/Commands.cs b/src/Ryujinx/UI/Helpers/Commands.cs index df24a7da1..868b49158 100644 --- a/src/Ryujinx/UI/Helpers/Commands.cs +++ b/src/Ryujinx/UI/Helpers/Commands.cs @@ -12,9 +12,9 @@ namespace Ryujinx.Ava.UI.Helpers public static RelayCommand CreateConditional(Action action, Func canExecute) => new(action, canExecute); - public static RelayCommand CreateWithArg(Action action) + public static RelayCommand Create(Action action) => new(action); - public static RelayCommand CreateConditionalWithArg(Action action, Predicate canExecute) + public static RelayCommand CreateConditional(Action action, Predicate canExecute) => new(action, canExecute); public static AsyncRelayCommand Create(Func action) @@ -24,11 +24,11 @@ namespace Ryujinx.Ava.UI.Helpers public static AsyncRelayCommand CreateSilentFail(Func action) => new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler); - public static AsyncRelayCommand CreateWithArg(Func action) + public static AsyncRelayCommand Create(Func action) => new(action, AsyncRelayCommandOptions.None); - public static AsyncRelayCommand CreateConcurrentWithArg(Func action) + public static AsyncRelayCommand CreateConcurrent(Func action) => new(action, AsyncRelayCommandOptions.AllowConcurrentExecutions); - public static AsyncRelayCommand CreateSilentFailWithArg(Func action) + public static AsyncRelayCommand CreateSilentFail(Func action) => new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler); public static AsyncRelayCommand CreateConditional(Func action, Func canExecute) @@ -38,11 +38,11 @@ namespace Ryujinx.Ava.UI.Helpers public static AsyncRelayCommand CreateSilentFailConditional(Func action, Func canExecute) => new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler); - public static AsyncRelayCommand CreateConditionalWithArg(Func action, Predicate canExecute) + public static AsyncRelayCommand CreateConditional(Func action, Predicate canExecute) => new(action, canExecute, AsyncRelayCommandOptions.None); - public static AsyncRelayCommand CreateConcurrentConditionalWithArg(Func action, Predicate canExecute) + public static AsyncRelayCommand CreateConcurrentConditional(Func action, Predicate canExecute) => new(action, canExecute, AsyncRelayCommandOptions.AllowConcurrentExecutions); - public static AsyncRelayCommand CreateSilentFailConditionalWithArg(Func action, Predicate canExecute) + public static AsyncRelayCommand CreateSilentFailConditional(Func action, Predicate canExecute) => new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler); } } diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs index 833670bdc..ea7dd34c3 100644 --- a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -1,3 +1,4 @@ +using Avalonia.Media; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; @@ -387,6 +388,30 @@ namespace Ryujinx.Ava.UI.Models.Input } } + private bool _enableLedChanging; + + public bool EnableLedChanging + { + get => _enableLedChanging; + set + { + _enableLedChanging = value; + OnPropertyChanged(); + } + } + + private Color _ledColor; + + public Color LedColor + { + get => _ledColor; + set + { + _ledColor = value; + OnPropertyChanged(); + } + } + private bool _enableMotion; public bool EnableMotion { @@ -483,12 +508,23 @@ namespace Ryujinx.Ava.UI.Models.Input WeakRumble = controllerInput.Rumble.WeakRumble; StrongRumble = controllerInput.Rumble.StrongRumble; } + + if (controllerInput.Led != null) + { + EnableLedChanging = controllerInput.Led.EnableLed; + uint rawColor = controllerInput.Led.LedColor; + byte alpha = (byte)(rawColor >> 24); + byte red = (byte)(rawColor >> 16); + byte green = (byte)(rawColor >> 8); + byte blue = (byte)(rawColor % 256); + LedColor = new Color(alpha, red, green, blue); + } } } public InputConfig GetConfig() { - var config = new StandardControllerInputConfig + StandardControllerInputConfig config = new() { Id = Id, Backend = InputBackendType.GamepadSDL2, @@ -540,6 +576,11 @@ namespace Ryujinx.Ava.UI.Models.Input WeakRumble = WeakRumble, StrongRumble = StrongRumble, }, + Led = new LedConfigController + { + EnableLed = EnableLedChanging, + LedColor = LedColor.ToUInt32() + }, Version = InputConfig.CurrentVersion, DeadzoneLeft = DeadzoneLeft, DeadzoneRight = DeadzoneRight, diff --git a/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs new file mode 100644 index 000000000..d50d8249a --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/DlcSelectViewModel.cs @@ -0,0 +1,25 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Ryujinx.Ava.Common.Models; +using Ryujinx.Ava.Utilities.AppLibrary; +using System.Linq; + +namespace Ryujinx.Ava.UI.ViewModels +{ + public partial class DlcSelectViewModel : BaseModel + { + [ObservableProperty] private DownloadableContentModel[] _dlcs; + #nullable enable + [ObservableProperty] private DownloadableContentModel? _selectedDlc; + #nullable disable + + public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary) + { + _dlcs = appLibrary.DownloadableContents.Items + .Where(x => x.Dlc.TitleIdBase == titleId) + .Select(x => x.Dlc) + .OrderBy(it => it.IsBundled ? 0 : 1) + .ThenBy(it => it.TitleId) + .ToArray(); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs index 482fe2981..0380ef598 100644 --- a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs @@ -37,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input [ObservableProperty] private SvgImage _image; - public readonly InputViewModel ParentModel; + public InputViewModel ParentModel { get; } public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config) { diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 05f479d9f..3d1bd5f4a 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -69,6 +69,9 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public bool IsRight { get; set; } public bool IsLeft { get; set; } + public bool HasLed => false; //temporary + //SelectedGamepad.Features.HasFlag(GamepadFeaturesFlag.Led); + public bool IsModified { get; set; } public event Action NotifyChangesEvent; diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index f88ed65d0..07cad41c5 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -488,6 +488,19 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public bool StartGamesWithoutUI + { + get => ConfigurationState.Instance.UI.StartNoUI; + set + { + ConfigurationState.Instance.UI.StartNoUI.Value = value; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + OnPropertyChanged(); + } + } + public bool ShowConsole { get => ConfigurationState.Instance.UI.ShowConsole; @@ -624,6 +637,7 @@ namespace Ryujinx.Ava.UI.ViewModels ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize], ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath], ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite], + ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel], _ => string.Empty, }; } @@ -694,6 +708,7 @@ namespace Ryujinx.Ava.UI.ViewModels public IHostUIHandler UiHandler { get; internal set; } public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite; public bool IsSortedByTitle => SortMode == ApplicationSort.Title; + public bool IsSortedByTitleId => SortMode == ApplicationSort.TitleId; public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer; public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed; public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed; @@ -726,6 +741,7 @@ namespace Ryujinx.Ava.UI.ViewModels ApplicationSort.FileSize => CreateComparer(IsAscending, app => app.FileSize), ApplicationSort.Path => CreateComparer(IsAscending, app => app.Path), ApplicationSort.Favorite => CreateComparer(IsAscending, app => new AppListFavoriteComparable(app)), + ApplicationSort.TitleId => CreateComparer(IsAscending, app => app.Id), _ => null, #pragma warning restore IDE0055 }; @@ -1195,6 +1211,11 @@ namespace Ryujinx.Ava.UI.ViewModels StartGamesInFullscreen = !StartGamesInFullscreen; } + public void ToggleStartGamesWithoutUI() + { + StartGamesWithoutUI = !StartGamesWithoutUI; + } + public void ToggleShowConsole() { ShowConsole = !ShowConsole; @@ -1641,10 +1662,7 @@ namespace Ryujinx.Ava.UI.ViewModels public async Task OpenAmiiboWindow() { - if (!IsAmiiboRequested) - return; - - if (AppHost.Device.System.SearchingForAmiibo(out int deviceId)) + if (AppHost.Device.System.SearchingForAmiibo(out int deviceId) && IsGameRunning) { string titleId = AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(); AmiiboWindow window = new(ShowAll, LastScannedAmiiboId, titleId); @@ -1662,10 +1680,7 @@ namespace Ryujinx.Ava.UI.ViewModels } public async Task OpenBinFile() { - if (!IsAmiiboRequested) - return; - - if (AppHost.Device.System.SearchingForAmiibo(out int deviceId)) + if (AppHost.Device.System.SearchingForAmiibo(out _) && IsGameRunning) { var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 2678bbf98..b2311cfc7 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -488,7 +488,6 @@ namespace Ryujinx.Ava.UI.ViewModels EnableDiscordIntegration = config.EnableDiscordIntegration; CheckUpdatesOnStart = config.CheckUpdatesOnStart; ShowConfirmExit = config.ShowConfirmExit; - IgnoreApplet = config.IgnoreApplet; RememberWindowState = config.RememberWindowState; ShowTitleBar = config.ShowTitleBar; HideCursor = (int)config.HideCursor.Value; @@ -532,6 +531,7 @@ namespace Ryujinx.Ava.UI.ViewModels EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; DramSize = config.System.DramSize; IgnoreMissingServices = config.System.IgnoreMissingServices; + IgnoreApplet = config.System.IgnoreApplet; // CPU EnablePptc = config.System.EnablePtc; @@ -591,7 +591,6 @@ namespace Ryujinx.Ava.UI.ViewModels config.EnableDiscordIntegration.Value = EnableDiscordIntegration; config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; config.ShowConfirmExit.Value = ShowConfirmExit; - config.IgnoreApplet.Value = IgnoreApplet; config.RememberWindowState.Value = RememberWindowState; config.ShowTitleBar.Value = ShowTitleBar; config.HideCursor.Value = (HideCursorMode)HideCursor; @@ -632,12 +631,10 @@ namespace Ryujinx.Ava.UI.ViewModels } config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); - config.Graphics.VSyncMode.Value = VSyncMode; - config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval; - config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval; config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; config.System.DramSize.Value = DramSize; config.System.IgnoreMissingServices.Value = IgnoreMissingServices; + config.System.IgnoreApplet.Value = IgnoreApplet; // CPU config.System.EnablePtc.Value = EnablePptc; @@ -646,6 +643,9 @@ namespace Ryujinx.Ava.UI.ViewModels config.System.UseHypervisor.Value = UseHypervisor; // Graphics + config.Graphics.VSyncMode.Value = VSyncMode; + config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval; + config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval; config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex; config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); config.Graphics.EnableShaderCache.Value = EnableShaderCache; diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml index 7daf23eb6..db7040f4b 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml @@ -4,6 +4,7 @@ xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" @@ -486,6 +487,37 @@ + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs index ee84fbc37..52a6d51b9 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs @@ -4,14 +4,11 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using DiscordRPC; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels.Input; using Ryujinx.Common.Configuration.Hid.Controller; -using Ryujinx.Common.Logging; using Ryujinx.Input; using Ryujinx.Input.Assigner; -using System; using StickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; namespace Ryujinx.Ava.UI.Views.Input diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 6bce21851..3252aa5cf 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -81,25 +81,16 @@ - - - - + Header="{ext:Locale MenuBarOptionsStartGamesInFullscreen}" + Classes="withCheckbox"> - - - - + + + + + + Header="{ext:Locale MenuBarOptionsShowConsole}" + Classes="withCheckbox"> - - - - - - - - + Icon="{ext:Icon fa-solid fa-language}" + Classes="withCheckbox"> - - - - + ToolTip.Tip="{ext:Locale OpenSettingsTooltip}" + Classes="withCheckbox"> - - - - + ToolTip.Tip="{ext:Locale OpenProfileManagerTooltip}" + Classes="withCheckbox"> + IsVisible="{Binding !EnableNonGameRunningControls}"> - - - - + + + + - - - + + + - - - + + + - + diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 521460012..9a63c022d 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -37,26 +37,20 @@ namespace Ryujinx.Ava.UI.Views.Main ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems(); ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems(); - MiiAppletMenuItem.Command = new AsyncRelayCommand(OpenMiiApplet); - CloseRyujinxMenuItem.Command = new RelayCommand(CloseWindow); - OpenSettingsMenuItem.Command = new AsyncRelayCommand(OpenSettings); - PauseEmulationMenuItem.Command = new RelayCommand(() => ViewModel.AppHost?.Pause()); - ResumeEmulationMenuItem.Command = new RelayCommand(() => ViewModel.AppHost?.Resume()); - StopEmulationMenuItem.Command = new AsyncRelayCommand(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted()); - CheatManagerMenuItem.Command = new AsyncRelayCommand(async () => - { - try - { - await OpenCheatManagerForCurrentApp(); - } catch {} - }); - InstallFileTypesMenuItem.Command = new AsyncRelayCommand(InstallFileTypes); - UninstallFileTypesMenuItem.Command = new AsyncRelayCommand(UninstallFileTypes); - XciTrimmerMenuItem.Command = new AsyncRelayCommand(() => XCITrimmerWindow.Show(ViewModel)); - AboutWindowMenuItem.Command = new AsyncRelayCommand(AboutWindow.Show); - CompatibilityListMenuItem.Command = new AsyncRelayCommand(CompatibilityList.Show); + MiiAppletMenuItem.Command = Commands.Create(OpenMiiApplet); + CloseRyujinxMenuItem.Command = Commands.Create(CloseWindow); + OpenSettingsMenuItem.Command = Commands.Create(OpenSettings); + PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause()); + ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume()); + StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted()); + CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp); + InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes); + UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes); + XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show); + AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show); + CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show); - UpdateMenuItem.Command = new AsyncRelayCommand(async () => + UpdateMenuItem.Command = Commands.Create(async () => { if (Updater.CanUpdate(true)) await Updater.BeginUpdateAsync(true); @@ -64,12 +58,12 @@ namespace Ryujinx.Ava.UI.Views.Main FaqMenuItem.Command = SetupGuideMenuItem.Command = - LdnGuideMenuItem.Command = new RelayCommand(OpenHelper.OpenUrl); + LdnGuideMenuItem.Command = Commands.Create(OpenHelper.OpenUrl); WindowSize720PMenuItem.Command = WindowSize1080PMenuItem.Command = WindowSize1440PMenuItem.Command = - WindowSize2160PMenuItem.Command = new RelayCommand(ChangeWindowSize); + WindowSize2160PMenuItem.Command = Commands.Create(ChangeWindowSize); } private IEnumerable GenerateToggleFileTypeItems() => diff --git a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml index 1c6895db1..cdc66a138 100644 --- a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml @@ -105,6 +105,12 @@ GroupName="Sort" IsChecked="{Binding IsSortedByTitle, Mode=OneTime}" Tag="Title" /> + + Text="Highly specific hacks & tricks to alleviate performance issues, crashing, or freezing. Will cause issues." /> + ToolTip.Tip="{ext:Locale AddGameDirTooltip}"> @@ -168,8 +167,7 @@ Grid.Column="1" MinWidth="90" Margin="10,0,0,0" - ToolTip.Tip="{ext:Locale AddAutoloadDirTooltip}" - Click="AddAutoloadDirButton_OnClick"> + ToolTip.Tip="{ext:Locale AddAutoloadDirTooltip}"> diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs index 3532e1855..9707b3193 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml.cs @@ -1,12 +1,17 @@ +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Platform.Storage; using Avalonia.VisualTree; +using Gommon; +using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.Utilities; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Settings { @@ -18,31 +23,39 @@ namespace Ryujinx.Ava.UI.Views.Settings { InitializeComponent(); ShowTitleBarBox.IsVisible = OperatingSystem.IsWindows(); + AddGameDirButton.Command = + Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirectories, true)); + AddAutoloadDirButton.Command = + Commands.Create(() => AddDirButton(AutoloadDirPathBox, ViewModel.AutoloadDirectories, false)); } - private async void AddGameDirButton_OnClick(object sender, RoutedEventArgs e) + private async Task AddDirButton(TextBox addDirBox, AvaloniaList directories, bool isGameList) { - string path = GameDirPathBox.Text; + string path = addDirBox.Text; - if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path)) + if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !directories.Contains(path)) { - ViewModel.GameDirectories.Add(path); - ViewModel.GameDirectoryChanged = true; + directories.Add(path); + + addDirBox.Clear(); + + if (isGameList) + ViewModel.GameDirectoryChanged = true; + else + ViewModel.AutoloadDirectoryChanged = true; } else { - if (this.GetVisualRoot() is Window window) - { - var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions - { - AllowMultiple = false, - }); + Optional folder = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(); - if (result.Count > 0) - { - ViewModel.GameDirectories.Add(result[0].Path.LocalPath); + if (folder.HasValue) + { + directories.Add(folder.Value.Path.LocalPath); + + if (isGameList) ViewModel.GameDirectoryChanged = true; - } + else + ViewModel.AutoloadDirectoryChanged = true; } } } @@ -63,33 +76,6 @@ namespace Ryujinx.Ava.UI.Views.Settings } } - private async void AddAutoloadDirButton_OnClick(object sender, RoutedEventArgs e) - { - string path = AutoloadDirPathBox.Text; - - if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.AutoloadDirectories.Contains(path)) - { - ViewModel.AutoloadDirectories.Add(path); - ViewModel.AutoloadDirectoryChanged = true; - } - else - { - if (this.GetVisualRoot() is Window window) - { - var result = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions - { - AllowMultiple = false, - }); - - if (result.Count > 0) - { - ViewModel.AutoloadDirectories.Add(result[0].Path.LocalPath); - ViewModel.AutoloadDirectoryChanged = true; - } - } - } - } - private void RemoveAutoloadDirButton_OnClick(object sender, RoutedEventArgs e) { int oldIndex = AutoloadDirsList.SelectedIndex; diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml index 8270e070a..e2c4fe16e 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml @@ -7,7 +7,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:pi="using:Projektanker.Icons.Avalonia" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:models="clr-namespace:Ryujinx.Ava.Common.Models" Width="500" @@ -15,9 +14,6 @@ mc:Ignorable="d" x:DataType="viewModels:DownloadableContentManagerViewModel" Focusable="True"> - - - - + @@ -113,22 +109,13 @@ Margin="10 0" HorizontalAlignment="Left" VerticalAlignment="Center" - Text="{Binding TitleId}" /> + Text="{Binding TitleIdStr}" /> -