Add support for multi game XCIs (#5638)

* Add default values to ApplicationData directly

* Refactor application loading

It should now be possible to load multi game XCIs.
Included updates won't be detected for now.
Opening a game from the command line currently only opens the first one.

* Only include program NCAs where at least one tuple item is not null

* Get application data by title id and add programIndex check back

* Refactor application loading again and remove duplicate code

* Actually use patch ncas for updates

* Fix number of applications found with multi game xcis

* Don't load bundled updates from multi game xcis

* Change ApplicationData.TitleId type to ulong & Add TitleIdString property

* Use cnmt files and ContentCollection to load programs

* Ava: Add updates and DLCs from gamecarts

* Get the cnmt file from its NCA

* Ava: Identify bundled updates in updater window

* Fix the (hopefully) last few bugs

* Add idOffset parameter to GetNcaByType

* Handle missing file for dlc.json

* Ava: Shorten error message for invalid files

* Gtk: Add additional string for bundled updates in TitleUpdateWindow

* Hopefully fix DLC issues

* Apply formatting

* Finally fix DLC issues

* Adjust property names and fileSize field

* Read the correct update file

* Fix wrong casing for application id strings

* Rename TitleId to ApplicationId

* Address review comments

* Fix formatting issues

* Apply suggestions from code review

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Gracefully fail when loading pfs for update and dlc window

* Fix applications with multiple programs

* Fix DLCWindow crash on GTK

* Fix some GUI issues

* Remove IsXci again

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
This commit is contained in:
TSRBerry 2023-11-11 21:56:57 +01:00 committed by GitHub
parent 55557525b1
commit 5c3cfb84c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1168 additions and 816 deletions

View file

@ -39,6 +39,7 @@ using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading;
@ -70,7 +71,7 @@ namespace Ryujinx.Ui
private bool _gameLoaded;
private bool _ending;
private string _currentEmulatedGamePath = null;
private ApplicationData _currentApplicationData = null;
private string _lastScannedAmiiboId = "";
private bool _lastScannedAmiiboShowAll = false;
@ -181,8 +182,12 @@ namespace Ryujinx.Ui
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
_userChannelPersistence = new UserChannelPersistence();
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
// Instantiate GUI objects.
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem);
_applicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel);
_uiHandler = new GtkHostUiHandler(this);
_deviceExitStatus = new AutoResetEvent(false);
@ -784,7 +789,7 @@ namespace Ryujinx.Ui
}
}
private bool LoadApplication(string path, bool isFirmwareTitle)
private bool LoadApplication(string path, ulong titleId, bool isFirmwareTitle)
{
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
@ -858,7 +863,7 @@ namespace Ryujinx.Ui
case ".xci":
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
return _emulationContext.LoadXci(path);
return _emulationContext.LoadXci(path, titleId);
case ".nca":
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
@ -867,7 +872,7 @@ namespace Ryujinx.Ui
case ".pfs0":
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
return _emulationContext.LoadNsp(path);
return _emulationContext.LoadNsp(path, titleId);
default:
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
try
@ -888,7 +893,7 @@ namespace Ryujinx.Ui
return false;
}
public void RunApplication(string path, bool startFullscreen = false)
public void RunApplication(ApplicationData application, bool startFullscreen = false)
{
if (_gameLoaded)
{
@ -910,14 +915,14 @@ namespace Ryujinx.Ui
bool isFirmwareTitle = false;
if (path.StartsWith("@SystemContent"))
if (application.Path.StartsWith("@SystemContent"))
{
path = VirtualFileSystem.SwitchPathToSystemPath(path);
application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path);
isFirmwareTitle = true;
}
if (!LoadApplication(path, isFirmwareTitle))
if (!LoadApplication(application.Path, application.Id, isFirmwareTitle))
{
_emulationContext.Dispose();
SwitchToGameTable();
@ -927,7 +932,7 @@ namespace Ryujinx.Ui
SetupProgressUiHandlers();
_currentEmulatedGamePath = path;
_currentApplicationData = application;
_deviceExitStatus.Reset();
@ -1168,7 +1173,7 @@ namespace Ryujinx.Ui
_tableStore.AppendValues(
args.AppData.Favorite,
new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
$"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
$"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}",
args.AppData.Developer,
args.AppData.Version,
args.AppData.TimePlayedString,
@ -1256,9 +1261,22 @@ namespace Ryujinx.Ui
{
_gameTableSelection.GetSelected(out TreeIter treeIter);
string path = (string)_tableStore.GetValue(treeIter, 9);
ApplicationData application = new()
{
Favorite = (bool)_tableStore.GetValue(treeIter, 0),
Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0],
Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
Developer = (string)_tableStore.GetValue(treeIter, 3),
Version = (string)_tableStore.GetValue(treeIter, 4),
TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
FileExtension = (string)_tableStore.GetValue(treeIter, 7),
FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
Path = (string)_tableStore.GetValue(treeIter, 9),
ControlHolder = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10),
};
RunApplication(path);
RunApplication(application);
}
private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
@ -1316,13 +1334,22 @@ namespace Ryujinx.Ui
return;
}
string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString();
string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0];
string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
ApplicationData application = new()
{
Favorite = (bool)_tableStore.GetValue(treeIter, 0),
Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0],
Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
Developer = (string)_tableStore.GetValue(treeIter, 3),
Version = (string)_tableStore.GetValue(treeIter, 4),
TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
FileExtension = (string)_tableStore.GetValue(treeIter, 7),
FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
Path = (string)_tableStore.GetValue(treeIter, 9),
ControlHolder = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10),
};
BlitStruct<ApplicationControlProperty> controlData = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10);
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
_ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application);
}
private void Load_Application_File(object sender, EventArgs args)
@ -1344,7 +1371,12 @@ namespace Ryujinx.Ui
if (fileChooser.Run() == (int)ResponseType.Accept)
{
RunApplication(fileChooser.Filename);
ApplicationData applicationData = new()
{
Path = fileChooser.Filename,
};
RunApplication(applicationData);
}
}
@ -1354,7 +1386,13 @@ namespace Ryujinx.Ui
if (fileChooser.Run() == (int)ResponseType.Accept)
{
RunApplication(fileChooser.Filename);
ApplicationData applicationData = new()
{
Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename),
Path = fileChooser.Filename,
};
RunApplication(applicationData);
}
}
@ -1369,7 +1407,14 @@ namespace Ryujinx.Ui
{
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
RunApplication(contentPath);
ApplicationData applicationData = new()
{
Name = "miiEdit",
Id = 0x0100000000001009ul,
Path = contentPath,
};
RunApplication(applicationData);
}
private void Open_Ryu_Folder(object sender, EventArgs args)
@ -1645,13 +1690,13 @@ namespace Ryujinx.Ui
{
_userChannelPersistence.ShouldRestart = false;
RunApplication(_currentEmulatedGamePath);
RunApplication(_currentApplicationData);
}
else
{
// otherwise, clear state.
_userChannelPersistence = new UserChannelPersistence();
_currentEmulatedGamePath = null;
_currentApplicationData = null;
_actionMenu.Sensitive = false;
_firmwareInstallFile.Sensitive = true;
_firmwareInstallDirectory.Sensitive = true;
@ -1713,7 +1758,7 @@ namespace Ryujinx.Ui
_emulationContext.Processes.ActiveApplication.ProgramId,
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
_currentEmulatedGamePath);
_currentApplicationData.Path);
window.Destroyed += CheatWindow_Destroyed;
window.Show();