misc: move Models & Helpers into Common & Avalonia projects

This commit is contained in:
Evan Husted
2024-12-29 19:09:28 -06:00
parent 9baaa2b8f8
commit 6caab1aa37
65 changed files with 141 additions and 146 deletions

View File

@@ -0,0 +1,10 @@
using System;
namespace Ryujinx.Ava.Utilities.AppLibrary
{
public class ApplicationCountUpdatedEventArgs : EventArgs
{
public int NumAppsFound { get; set; }
public int NumAppsLoaded { get; set; }
}
}

View File

@@ -0,0 +1,172 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Loader;
using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.UI.Common.Helper;
using System;
using System.IO;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.AppLibrary
{
public class ApplicationData
{
public static Func<string> LocalizedNever { get; set; } = () => "Never";
public bool Favorite { get; set; }
public byte[] Icon { get; set; }
public string Name { get; set; } = "Unknown";
public ulong Id { get; set; }
public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0";
public int PlayerCount { get; set; }
public int GameCount { get; set; }
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }
public long FileSize { get; set; }
public string Path { get; set; }
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
public bool HasControlHolder => ControlHolder.ByteSpan.Length > 0;
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n") ?? LocalizedNever();
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
[JsonIgnore] public string IdString => Id.ToString("x16");
[JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL;
[JsonIgnore] public string IdBaseString => IdBase.ToString("x16");
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
{
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
if (!System.IO.Path.Exists(titleFilePath))
{
Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
return string.Empty;
}
string extension = System.IO.Path.GetExtension(titleFilePath).ToLower();
if (extension is ".nsp" or ".xci")
{
IFileSystem pfs;
if (extension == ".xci")
{
Xci xci = new(virtualFileSystem.KeySet, file.AsStorage());
pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
pfs = pfsTemp;
}
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new(virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
if (nca.Header.ContentType != NcaContentType.Program)
{
continue;
}
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
patchNca = nca;
}
else
{
mainNca = nca;
}
}
}
else if (extension == ".nca")
{
mainNca = new Nca(virtualFileSystem.KeySet, file.AsStorage());
}
if (mainNca == null)
{
Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA was not present in the selected file");
return string.Empty;
}
(Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _);
if (updatePatchNca != null)
{
patchNca = updatePatchNca;
}
IFileSystem codeFs = null;
if (patchNca == null)
{
if (mainNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystem(NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid);
}
}
else
{
if (patchNca.CanOpenSection(NcaSectionType.Code))
{
codeFs = mainNca.OpenFileSystemWithPatch(patchNca, NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid);
}
}
if (codeFs == null)
{
Logger.Error?.Print(LogClass.Loader, "No ExeFS found in NCA");
return string.Empty;
}
const string MainExeFs = "main";
if (!codeFs.FileExists($"/{MainExeFs}"))
{
Logger.Error?.Print(LogClass.Loader, "No main binary ExeFS found in ExeFS");
return string.Empty;
}
using var nsoFile = new UniqueRef<IFile>();
codeFs.OpenFile(ref nsoFile.Ref, $"/{MainExeFs}".ToU8Span(), OpenMode.Read).ThrowIfFailure();
NsoReader reader = new();
reader.Initialize(nsoFile.Release().AsStorage().AsFile(OpenMode.Read)).ThrowIfFailure();
return Convert.ToHexString(reader.Header.ModuleId.ItemsRo.ToArray()).Replace("-", string.Empty).ToUpper()[..16];
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.AppLibrary
{
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(ApplicationMetadata))]
internal partial class ApplicationJsonSerializerContext : JsonSerializerContext
{
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
using System;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.AppLibrary
{
public class ApplicationMetadata
{
public string Title { get; set; }
public bool Favorite { get; set; }
[JsonPropertyName("timespan_played")]
public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero;
[JsonPropertyName("last_played_utc")]
public DateTime? LastPlayed { get; set; } = null;
[JsonPropertyName("time_played")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public double TimePlayedOld { get; set; }
[JsonPropertyName("last_played")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string LastPlayedOld { get; set; }
/// <summary>
/// Updates <see cref="LastPlayed"/>. Call this before launching a game.
/// </summary>
public void UpdatePreGame()
{
LastPlayed = DateTime.UtcNow;
}
/// <summary>
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
/// </summary>
public void UpdatePostGame()
{
DateTime? prevLastPlayed = LastPlayed;
UpdatePreGame();
if (!prevLastPlayed.HasValue)
{
return;
}
TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
}
}
}

View File

@@ -0,0 +1,42 @@
using LibHac.Ns;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.AppLibrary
{
public struct LdnGameData
{
public string Id { get; set; }
public int PlayerCount { get; set; }
public int MaxPlayerCount { get; set; }
public string GameName { get; set; }
public string TitleId { get; set; }
public string Mode { get; set; }
public string Status { get; set; }
public IEnumerable<string> Players { get; set; }
public static Array GetArrayForApp(
IEnumerable<LdnGameData> receivedData, ref ApplicationControlProperty acp)
{
LibHac.Common.FixedArrays.Array8<ulong> communicationId = acp.LocalCommunicationId;
return new Array(receivedData.Where(game =>
communicationId.Items.Contains(Convert.ToUInt64(game.TitleId, 16))
));
}
public class Array
{
private readonly LdnGameData[] _ldnDatas;
internal Array(IEnumerable<LdnGameData> receivedData)
{
_ldnDatas = receivedData.ToArray();
}
public int PlayerCount => _ldnDatas.Sum(it => it.PlayerCount);
public int GameCount => _ldnDatas.Length;
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
namespace Ryujinx.Ava.Utilities.AppLibrary
{
public class LdnGameDataReceivedEventArgs : EventArgs
{
public IEnumerable<LdnGameData> LdnData { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Utilities.AppLibrary
{
[JsonSerializable(typeof(IEnumerable<LdnGameData>))]
internal partial class LdnGameDataSerializerContext : JsonSerializerContext;
}

View File

@@ -0,0 +1,60 @@
using LibHac.Common;
using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem;
namespace Ryujinx.Ava.Utilities
{
public readonly struct AppletMetadata
{
private readonly ContentManager _contentManager;
public string Name { get; }
public ulong ProgramId { get; }
public string Version { get; }
public AppletMetadata(ContentManager contentManager, string name, ulong programId, string version = "1.0.0")
: this(name, programId, version)
{
_contentManager = contentManager;
}
public AppletMetadata(string name, ulong programId, string version = "1.0.0")
{
Name = name;
ProgramId = programId;
Version = version;
}
public string GetContentPath(ContentManager contentManager)
=> (contentManager ?? _contentManager)
.GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program);
public bool CanStart(ContentManager contentManager, out ApplicationData appData,
out BlitStruct<ApplicationControlProperty> appControl)
{
contentManager ??= _contentManager;
if (contentManager == null)
{
appData = null;
appControl = new BlitStruct<ApplicationControlProperty>(0);
return false;
}
appData = new() { Name = Name, Id = ProgramId, Path = GetContentPath(contentManager) };
if (string.IsNullOrEmpty(appData.Path))
{
appControl = new BlitStruct<ApplicationControlProperty>(0);
return false;
}
appControl = StructHelpers.CreateCustomNacpData(Name, Version);
return true;
}
}
}

View File

@@ -0,0 +1,135 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using Path = System.IO.Path;
namespace Ryujinx.Ava.Utilities
{
public static class DownloadableContentsHelper
{
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase)
{
var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase);
if (!File.Exists(downloadableContentJsonPath))
{
return [];
}
try
{
var downloadableContentContainerList = JsonHelper.DeserializeFromFile(downloadableContentJsonPath,
_serializerContext.ListDownloadableContentContainer);
return LoadDownloadableContents(vfs, downloadableContentContainerList);
}
catch
{
Logger.Error?.Print(LogClass.Configuration, "Downloadable Content JSON failed to deserialize.");
return [];
}
}
public static void SaveDownloadableContentsJson(ulong applicationIdBase, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
{
DownloadableContentContainer container = default;
List<DownloadableContentContainer> downloadableContentContainerList = new();
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)
{
if (container.ContainerPath != dlc.ContainerPath)
{
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
{
downloadableContentContainerList.Add(container);
}
container = new DownloadableContentContainer
{
ContainerPath = dlc.ContainerPath,
DownloadableContentNcaList = [],
};
}
container.DownloadableContentNcaList.Add(new DownloadableContentNca
{
Enabled = isEnabled,
TitleId = dlc.TitleId,
FullPath = dlc.FullPath,
});
}
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
{
downloadableContentContainerList.Add(container);
}
var downloadableContentJsonPath = PathToGameDLCJson(applicationIdBase);
JsonHelper.SerializeToFile(downloadableContentJsonPath, downloadableContentContainerList, _serializerContext.ListDownloadableContentContainer);
}
private static List<(DownloadableContentModel, bool IsEnabled)> LoadDownloadableContents(VirtualFileSystem vfs, List<DownloadableContentContainer> downloadableContentContainers)
{
var result = new List<(DownloadableContentModel, bool IsEnabled)>();
foreach (DownloadableContentContainer downloadableContentContainer in downloadableContentContainers)
{
if (!File.Exists(downloadableContentContainer.ContainerPath))
{
continue;
}
using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, vfs);
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
using UniqueRef<IFile> ncaFile = new();
partitionFileSystem.OpenFile(ref ncaFile.Ref, downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryOpenNca(vfs, ncaFile.Get.AsStorage());
if (nca == null)
{
continue;
}
var content = new DownloadableContentModel(nca.Header.TitleId,
downloadableContentContainer.ContainerPath,
downloadableContentNca.FullPath);
result.Add((content, downloadableContentNca.Enabled));
}
}
return result;
}
private static Nca TryOpenNca(VirtualFileSystem vfs, IStorage ncaStorage)
{
try
{
return new Nca(vfs.KeySet, ncaStorage);
}
catch (Exception) { }
return null;
}
private static string PathToGameDLCJson(ulong applicationIdBase)
{
return Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "dlc.json");
}
}
}

View File

@@ -0,0 +1,106 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.UI;
using Ryujinx.HLE.FileSystem;
using System;
using System.IO;
namespace Ryujinx.Ava.Utilities
{
/// <summary>
/// Ensure installation validity
/// </summary>
public static class SetupValidator
{
public static bool IsFirmwareValid(ContentManager contentManager, out UserError error)
{
error = contentManager.GetCurrentFirmwareVersion() != null
? UserError.Success
: UserError.NoFirmware;
return error is UserError.Success;
}
public static bool CanFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out SystemVersion firmwareVersion)
{
try
{
firmwareVersion = contentManager.VerifyFirmwarePackage(baseApplicationPath);
}
catch (Exception)
{
firmwareVersion = null;
}
return error == UserError.NoFirmware && Path.GetExtension(baseApplicationPath).ToLowerInvariant() == ".xci" && firmwareVersion != null;
}
public static bool TryFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out UserError outError)
{
if (error == UserError.NoFirmware)
{
string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant();
// If the target app to start is a XCI, try to install firmware from it
if (baseApplicationExtension == ".xci")
{
SystemVersion firmwareVersion;
try
{
firmwareVersion = contentManager.VerifyFirmwarePackage(baseApplicationPath);
}
catch (Exception)
{
firmwareVersion = null;
}
// The XCI is a valid firmware package, try to install the firmware from it!
if (firmwareVersion != null)
{
try
{
Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}");
contentManager.InstallFirmware(baseApplicationPath);
Logger.Info?.Print(LogClass.Application, $"System version {firmwareVersion.VersionString} successfully installed.");
outError = UserError.Success;
return true;
}
catch
{
// ignored
}
}
}
}
outError = error;
return false;
}
public static bool CanStartApplication(ContentManager contentManager, string baseApplicationPath, out UserError error)
{
if (Directory.Exists(baseApplicationPath) || File.Exists(baseApplicationPath))
{
string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant();
// NOTE: We don't force homebrew developers to install a system firmware.
if (baseApplicationExtension is ".nro" or ".nso")
{
error = UserError.Success;
return true;
}
return IsFirmwareValid(contentManager, out error);
}
error = UserError.ApplicationNotFound;
return false;
}
}
}

View File

@@ -0,0 +1,173 @@
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using ShellLink;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.Utilities
{
public static class ShortcutHelper
{
[SupportedOSPlatform("windows")]
private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
iconPath += ".ico";
MemoryStream iconDataStream = new(iconData);
using var image = SKBitmap.Decode(iconDataStream);
image.Resize(new SKImageInfo(128, 128), SKFilterQuality.High);
SaveBitmapAsIcon(image, iconPath);
var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId), iconPath, 0);
shortcut.StringData.NameString = cleanedAppName;
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
}
[SupportedOSPlatform("linux")]
private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh");
var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.desktop");
iconPath += ".png";
var image = SKBitmap.Decode(iconData);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var file = File.OpenWrite(iconPath);
data.SaveTo(file);
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId)}");
}
[SupportedOSPlatform("macos")]
private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
var plistFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.plist");
var shortcutScript = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-launch-script.sh");
// Macos .App folder
string contentFolderPath = Path.Combine("/Applications", cleanedAppName + ".app", "Contents");
string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS");
if (!Directory.Exists(scriptFolderPath))
{
Directory.CreateDirectory(scriptFolderPath);
}
// Runner script
const string ScriptName = "runner.sh";
string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
using StreamWriter scriptFile = new(scriptPath);
scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId));
// Set execute permission
FileInfo fileInfo = new(scriptPath);
fileInfo.UnixFileMode |= UnixFileMode.UserExecute;
// img
string resourceFolderPath = Path.Combine(contentFolderPath, "Resources");
if (!Directory.Exists(resourceFolderPath))
{
Directory.CreateDirectory(resourceFolderPath);
}
const string IconName = "icon.png";
var image = SKBitmap.Decode(iconData);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var file = File.OpenWrite(Path.Combine(resourceFolderPath, IconName));
data.SaveTo(file);
// plist file
using StreamWriter outputFile = new(Path.Combine(contentFolderPath, "Info.plist"));
outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName);
}
public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData)
{
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars()));
if (OperatingSystem.IsWindows())
{
string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath);
return;
}
if (OperatingSystem.IsLinux())
{
string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
Directory.CreateDirectory(iconPath);
CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
return;
}
if (OperatingSystem.IsMacOS())
{
CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName);
return;
}
throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
}
private static string GetArgsString(string appFilePath, string applicationId)
{
// args are first defined as a list, for easier adjustments in the future
var argsList = new List<string>();
if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg))
{
argsList.Add("--root-data-dir");
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
}
if (appFilePath.ToLower().EndsWith(".xci"))
{
argsList.Add("--application-id");
argsList.Add($"\"{applicationId}\"");
}
argsList.Add($"\"{appFilePath}\"");
return string.Join(" ", argsList);
}
/// <summary>
/// Creates an Icon (.ico) file using the source bitmap image at the specified file path.
/// </summary>
/// <param name="source">The source bitmap image that will be saved as an .ico file</param>
/// <param name="filePath">The location that the new .ico file will be saved too (Make sure to include '.ico' in the path).</param>
[SupportedOSPlatform("windows")]
private static void SaveBitmapAsIcon(SKBitmap source, string filePath)
{
// Code Modified From https://stackoverflow.com/a/11448060/368354 by Benlitz
byte[] header = { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 22, 0, 0, 0 };
using FileStream fs = new(filePath, FileMode.Create);
fs.Write(header);
// Writing actual data
using var data = source.Encode(SKEncodedImageFormat.Png, 100);
data.SaveTo(fs);
// Getting data length (file length minus header)
long dataLength = fs.Length - header.Length;
// Write it in the correct place
fs.Seek(14, SeekOrigin.Begin);
fs.WriteByte((byte)dataLength);
fs.WriteByte((byte)(dataLength >> 8));
fs.WriteByte((byte)(dataLength >> 16));
fs.WriteByte((byte)(dataLength >> 24));
}
}
}

View File

@@ -0,0 +1,24 @@
using Ryujinx.HLE.Loaders.Processes;
namespace Ryujinx.Ava.Utilities
{
public static class TitleHelper
{
public static string ActiveApplicationTitle(ProcessResult activeProcess, string applicationVersion, string pauseString = "")
{
if (activeProcess == null)
return string.Empty;
string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : $" {activeProcess.Name}";
string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $" v{activeProcess.DisplayVersion}";
string titleIdSection = $" ({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
string appTitle = $"Ryujinx {applicationVersion} -{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
return !string.IsNullOrEmpty(pauseString)
? appTitle + $" ({pauseString})"
: appTitle;
}
}
}

View File

@@ -0,0 +1,151 @@
using LibHac.Common;
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.HLE.Utilities;
using Ryujinx.UI.Common.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers;
using TitleUpdateMetadata = Ryujinx.Common.Configuration.TitleUpdateMetadata;
namespace Ryujinx.Ava.Utilities
{
public static class TitleUpdatesHelper
{
private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase)
{
var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
if (!File.Exists(titleUpdatesJsonPath))
{
return [];
}
try
{
var titleUpdateWindowData = JsonHelper.DeserializeFromFile(titleUpdatesJsonPath, _serializerContext.TitleUpdateMetadata);
return LoadTitleUpdates(vfs, titleUpdateWindowData, applicationIdBase);
}
catch
{
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {applicationIdBase:x16} at {titleUpdatesJsonPath}");
return [];
}
}
public static void SaveTitleUpdatesJson(ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates)
{
var titleUpdateWindowData = new TitleUpdateMetadata
{
Selected = string.Empty,
Paths = [],
};
foreach ((TitleUpdateModel update, bool isSelected) in updates)
{
titleUpdateWindowData.Paths.Add(update.Path);
if (isSelected)
{
if (!string.IsNullOrEmpty(titleUpdateWindowData.Selected))
{
Logger.Error?.Print(LogClass.Application,
$"Tried to save two updates as 'IsSelected' for {applicationIdBase:x16}");
return;
}
titleUpdateWindowData.Selected = update.Path;
}
}
var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
}
private static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase)
{
var result = new List<(TitleUpdateModel, bool IsSelected)>();
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
foreach (string path in titleUpdateMetadata.Paths)
{
if (!File.Exists(path))
continue;
try
{
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, vfs);
Dictionary<ulong, ContentMetaData> updates =
pfs.GetContentData(ContentMetaType.Patch, vfs, checkLevel);
if (!updates.TryGetValue(applicationIdBase, out ContentMetaData content))
continue;
Nca patchNca = content.GetNcaByType(vfs.KeySet, ContentType.Program);
Nca controlNca = content.GetNcaByType(vfs.KeySet, ContentType.Control);
if (controlNca is null || patchNca is null)
continue;
ApplicationControlProperty controlData = new();
using UniqueRef<IFile> nacpFile = new();
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None)
.OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None)
.ThrowIfFailure();
var displayVersion = controlData.DisplayVersionString.ToString();
var update = new TitleUpdateModel(content.ApplicationId, content.Version.Version,
displayVersion, path);
result.Add((update, path == titleUpdateMetadata.Selected));
}
catch (MissingKeyException exception)
{
Logger.Warning?.Print(LogClass.Application,
$"Your key set is missing a key with the name: {exception.Name}");
}
catch (InvalidDataException)
{
Logger.Warning?.Print(LogClass.Application,
$"The header key is incorrect or missing and therefore the NCA header content type check has failed. Malformed File: {path}");
}
catch (IOException exception)
{
Logger.Warning?.Print(LogClass.Application, exception.Message);
}
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application,
$"The file encountered was not of a valid type. File: '{path}' Error: {exception}");
}
}
return result;
}
private static string PathToGameUpdatesJson(ulong applicationIdBase)
=> Path.Combine(AppDataManager.GamesDirPath, applicationIdBase.ToString("x16"), "updates.json");
}
}