firmware stage

This commit is contained in:
GreemDev 2025-11-21 03:36:10 -06:00
parent bbd11d19d6
commit 94cb992083
14 changed files with 355 additions and 54 deletions

View file

@ -25041,6 +25041,156 @@
"zh_CN": "", "zh_CN": "",
"zh_TW": "" "zh_TW": ""
} }
},
{
"ID": "SetupWizardFirmwarePageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Switch Firmware",
"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": "SetupWizardFirmwarePageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder or .zip/.xci containing your dumped Nintendo Switch firmware:",
"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": "SetupWizardFirmwarePageFolderPopupTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder containing your dumped & extracted Switch firmware.",
"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": "SetupWizardFirmwarePageFilePopupTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the file containing your dumped Switch firmware.",
"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": "SetupWizardFirmwarePageFileBrowse",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select .zip or .xci",
"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": "SetupWizardFirmwarePageFolderBrowse",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select Extracted Folder",
"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": ""
}
} }
] ]
} }

View file

@ -90,8 +90,12 @@ namespace Ryujinx.Common
} }
public void Dispose() public void Dispose()
{
try
{ {
_queue.CompleteAdding(); _queue.CompleteAdding();
} catch (ObjectDisposedException) {}
_cts.Cancel(); _cts.Cancel();
_workerThread.Join(); _workerThread.Join();

View file

@ -9,7 +9,7 @@ namespace Ryujinx.Ava.Systems.SetupWizard
/// <summary> /// <summary>
/// Define the logic and flow of this <see cref="BaseSetupWizard"/>. /// Define the logic and flow of this <see cref="BaseSetupWizard"/>.
/// </summary> /// </summary>
public abstract ValueTask Start(); public abstract Task Start();
protected ValueTask<bool> FirstPage() protected ValueTask<bool> FirstPage()
{ {

View file

@ -21,7 +21,7 @@ namespace Ryujinx.Ava.Systems.SetupWizard
[ObservableProperty] [ObservableProperty]
public partial object? Content { get; set; } public partial object? Content { get; set; }
[ObservableProperty] public partial object? HelpContent { get; set; } = "test"; [ObservableProperty] public partial object? HelpContent { get; set; }
[ObservableProperty] [ObservableProperty]
public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext]; public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext];

View file

@ -59,8 +59,6 @@
Content="{ext:Locale SetupWizardActionBack}" Content="{ext:Locale SetupWizardActionBack}"
Margin="10,0,0,0" Margin="10,0,0,0"
Command="{Binding MoveBackCommand}" /> Command="{Binding MoveBackCommand}" />
<StackPanel Orientation="Horizontal">
</StackPanel>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
HorizontalAlignment="Right" HorizontalAlignment="Right"

View file

@ -65,10 +65,11 @@ namespace Ryujinx.Ava.UI.Helpers
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose)); _notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
} }
public static void ShowError(string message) => public static void ShowError(string message, bool waitingExit = false) =>
ShowError( ShowError(
LocaleManager.Instance[LocaleKeys.DialogErrorTitle], LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}" $"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
waitingExit: waitingExit
); );
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>

View file

@ -0,0 +1,27 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:markup="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:pages="clr-namespace:Ryujinx.UI.SetupWizard.Pages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="pages:SetupFirmwarePageViewModel"
x:Class="Ryujinx.UI.SetupWizard.Pages.SetupFirmwarePage">
<StackPanel>
<TextBlock Text="{markup:Locale SetupWizardFirmwarePageDescription}"/>
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
<TextBox Name="FirmwarePathField" Margin="0, 10, 0, 5" Text="{Binding FirmwareSourcePath}" IsReadOnly="True" />
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="3.5">
<Button
Content="{markup:Locale SetupWizardFirmwarePageFolderBrowse}"
Command="{Binding BrowseFolderCommand}"
CommandParameter="{Binding #FirmwarePathField}"/>
<Button
Content="{markup:Locale SetupWizardFirmwarePageFileBrowse}"
Command="{Binding BrowseFileCommand}"
CommandParameter="{Binding #FirmwarePathField}"/>
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View file

@ -0,0 +1,12 @@
using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.UI.SetupWizard.Pages
{
public partial class SetupFirmwarePage : RyujinxControl<SetupFirmwarePageViewModel>
{
public SetupFirmwarePage()
{
InitializeComponent();
}
}
}

View file

@ -0,0 +1,69 @@
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Gommon;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Ryujinx.UI.SetupWizard.Pages
{
public partial class SetupFirmwarePageViewModel : BaseModel
{
[ObservableProperty]
public partial string FirmwareSourcePath { get; set; }
[RelayCommand]
private static async Task BrowseFile(TextBox tb)
{
Optional<IStorageFile> result = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFilePickerAsync(new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageFilePopupTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.FileDialogAllTypes])
{
Patterns = ["*.xci", "*.zip"],
AppleUniformTypeIdentifiers = ["com.ryujinx.xci", "public.zip-archive"],
MimeTypes = ["application/x-nx-xci", "application/zip"],
},
new("XCI")
{
Patterns = ["*.xci"],
AppleUniformTypeIdentifiers = ["com.ryujinx.xci"],
MimeTypes = ["application/x-nx-xci"],
},
new("ZIP")
{
Patterns = ["*.zip"],
AppleUniformTypeIdentifiers = ["public.zip-archive"],
MimeTypes = ["application/zip"],
}
}
});
if (result.TryGet(out IStorageFile firmwareFile))
{
tb.Text = firmwareFile.TryGetLocalPath();
}
}
[RelayCommand]
private static async Task BrowseFolder(TextBox tb)
{
Optional<IStorageFolder> result = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageFolderPopupTitle]
});
if (result.TryGet(out IStorageFolder firmwareFolder))
{
tb.Text = firmwareFolder.TryGetLocalPath();
}
}
}
}

View file

@ -2,8 +2,10 @@ using Avalonia.Controls;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard.Pages namespace Ryujinx.Ava.UI.SetupWizard.Pages
@ -11,22 +13,19 @@ namespace Ryujinx.Ava.UI.SetupWizard.Pages
public partial class SetupKeysPageViewModel : BaseModel public partial class SetupKeysPageViewModel : BaseModel
{ {
[ObservableProperty] [ObservableProperty]
public partial string? KeysFolderPath { get; set; } public partial string KeysFolderPath { get; set; }
[RelayCommand] [RelayCommand]
private static async Task Browse(TextBox tb) private static async Task Browse(TextBox tb)
{ {
var result = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Optional<IStorageFolder> result = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions
Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle],
AllowMultiple = false
}) switch {
[var target] => target.TryGetLocalPath(),
_ => null
};
if (result is not null)
{ {
tb.Text = result; Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle]
});
if (result.TryGet(out IStorageFolder keyFolder))
{
tb.Text = keyFolder.TryGetLocalPath();
} }
} }
} }

View file

@ -45,7 +45,7 @@ namespace Ryujinx.Ava.UI.SetupWizard
LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, directory); LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, directory);
} }
NotificationHelper.ShowError(message); NotificationHelper.ShowError(message, waitingExit: true);
} }
finally finally
{ {
@ -57,7 +57,7 @@ namespace Ryujinx.Ava.UI.SetupWizard
} }
catch (MissingKeyException ex) catch (MissingKeyException ex)
{ {
NotificationHelper.ShowError(ex.ToString()); NotificationHelper.ShowError(ex.ToString(), waitingExit: true);
return Result.Failure(NoKeysFoundInFolder.Shared); return Result.Failure(NoKeysFoundInFolder.Shared);
} }
catch (Exception ex) catch (Exception ex)

View file

@ -3,27 +3,49 @@ using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.SetupWizard; using Ryujinx.Ava.Systems.SetupWizard;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.SetupWizard.Pages; using Ryujinx.Ava.UI.SetupWizard.Pages;
using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem;
using Ryujinx.UI.SetupWizard.Pages;
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard namespace Ryujinx.Ava.UI.SetupWizard
{ {
public partial class RyujinxSetupWizard(ContentPresenter presenter, MainWindowViewModel mwvm, Action onClose) : BaseSetupWizard(presenter) public partial class RyujinxSetupWizard(ContentPresenter presenter, MainWindowViewModel mwvm, Action onClose)
: BaseSetupWizard(presenter)
{ {
private bool _configWasModified = false; private bool _configWasModified = false;
public bool HasFirmware => mwvm.ContentManager.GetCurrentFirmwareVersion() != null; public bool HasFirmware => mwvm.ContentManager.GetCurrentFirmwareVersion() != null;
public override async ValueTask Start() public override async Task Start()
{ {
RyujinxSetupWizardWindow.IsUsingSetupWizard = true; RyujinxSetupWizardWindow.IsUsingSetupWizard = true;
Start: Start:
await FirstPage(); await FirstPage();
Keys: Keys:
if (!await SetupKeys())
goto Start;
Firmware:
if (!await SetupFirmware())
goto Keys;
Return:
onClose();
if (_configWasModified)
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
}
private async ValueTask<bool> SetupKeys()
{
if (!mwvm.VirtualFileSystem.HasKeySet) if (!mwvm.VirtualFileSystem.HasKeySet)
{ {
Retry: Retry:
@ -34,7 +56,7 @@ namespace Ryujinx.Ava.UI.SetupWizard
.Show(); .Show();
if (!result) if (!result)
goto Start; return false;
if (!Directory.Exists(kpvm.KeysFolderPath)) if (!Directory.Exists(kpvm.KeysFolderPath))
goto Retry; goto Retry;
@ -46,35 +68,55 @@ namespace Ryujinx.Ava.UI.SetupWizard
} }
} }
Firmware: return true;
// ReSharper disable once ConditionIsAlwaysTrueOrFalse }
// i know its always false thats the fucking point, its not done
if (!HasFirmware && false)
{
if (!mwvm.VirtualFileSystem.HasKeySet)
goto Keys;
private async ValueTask<bool> SetupFirmware()
{
if (!HasFirmware)
{
Retry: Retry:
SetupKeysPageViewModel kpvm = new(); SetupFirmwarePageViewModel fwvm = new();
bool result = await NextPage() bool result = await NextPage()
.WithTitle(LocaleKeys.SetupWizardKeysPageTitle) .WithTitle(LocaleKeys.SetupWizardFirmwarePageTitle)
.WithContent<SetupKeysPage>(kpvm) .WithContent<SetupFirmwarePage>(fwvm)
.Show(); .Show();
if (!result) if (!result)
goto Keys; return false;
if (!Directory.Exists(kpvm.KeysFolderPath)) if (!Directory.Exists(fwvm.FirmwareSourcePath))
goto Retry; goto Retry;
await mwvm.HandleKeysInstallation(kpvm.KeysFolderPath); try
{
mwvm.ContentManager.InstallFirmware(fwvm.FirmwareSourcePath);
SystemVersion installedFwVer = mwvm.ContentManager.GetCurrentFirmwareVersion();
NotificationHelper.ShowInformation(
"Firmware installed",
$"Installed firmware version {installedFwVer.VersionString}."
);
mwvm.RefreshFirmwareStatus(installedFwVer);
// Purge Applet Cache.
DirectoryInfo miiEditorCacheFolder = new(
Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")
);
if (miiEditorCacheFolder.Exists)
{
miiEditorCacheFolder.Delete(true);
}
}
catch (Exception e)
{
NotificationHelper.ShowError(e.Message, waitingExit: true);
goto Retry;
}
} }
Return: return true;
onClose();
if (_configWasModified)
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
} }
} }
} }

View file

@ -1377,8 +1377,8 @@ namespace Ryujinx.Ava.UI.ViewModels
Patterns = ["*.zip"], Patterns = ["*.zip"],
AppleUniformTypeIdentifiers = ["public.zip-archive"], AppleUniformTypeIdentifiers = ["public.zip-archive"],
MimeTypes = ["application/zip"], MimeTypes = ["application/zip"],
}, }
}, }
}); });
if (result.HasValue) if (result.HasValue)
@ -1758,12 +1758,11 @@ namespace Ryujinx.Ava.UI.ViewModels
public static void UpdateGameMetadata(string titleId, TimeSpan playTime) public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime));
public void RefreshFirmwareStatus() public void RefreshFirmwareStatus(SystemVersion version = null)
{ {
SystemVersion version = null;
try try
{ {
version = ContentManager.GetCurrentFirmwareVersion(); version ??= ContentManager.GetCurrentFirmwareVersion();
} }
catch (Exception) catch (Exception)
{ {

View file

@ -149,7 +149,7 @@ namespace Ryujinx.Ava.UI.Windows
Task windowTask = ShowAsync( Task windowTask = ShowAsync(
RyujinxSetupWizardWindow.CreateWindow(ViewModel, out BaseSetupWizard wiz), RyujinxSetupWizardWindow.CreateWindow(ViewModel, out BaseSetupWizard wiz),
this); this);
_ = wiz.Start().AsTask(); _ = wiz.Start();
await windowTask; await windowTask;
} }
}); });