diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 79e9550696..aa01bfc97d 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -107,7 +107,7 @@ namespace Ryujinx.HLE.HOS public string TitleName { get; private set; } - public string TitleID { get; private set; } + public string TitleId { get; private set; } public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; } @@ -499,7 +499,7 @@ namespace Ryujinx.HLE.HOS Nacp controlData = new Nacp(controlFile.AsStream()); TitleName = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title; - TitleID = metaData.Aci0.TitleId.ToString("x16"); + TitleId = metaData.Aci0.TitleId.ToString("x16"); if (string.IsNullOrWhiteSpace(TitleName)) { @@ -515,7 +515,7 @@ namespace Ryujinx.HLE.HOS } else { - TitleID = metaData.Aci0.TitleId.ToString("x16"); + TitleId = metaData.Aci0.TitleId.ToString("x16"); } } @@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS } } - TitleID = metaData.Aci0.TitleId.ToString("x16"); + TitleId = metaData.Aci0.TitleId.ToString("x16"); LoadNso("rtld"); LoadNso("main"); @@ -658,7 +658,7 @@ namespace Ryujinx.HLE.HOS ContentManager.LoadEntries(); TitleName = metaData.TitleName; - TitleID = metaData.Aci0.TitleId.ToString("x16"); + TitleId = metaData.Aci0.TitleId.ToString("x16"); ProgramLoader.LoadStaticObjects(this, metaData, new IExecutable[] { staticObject }); } diff --git a/Ryujinx.HLE/HOS/Services/Arp/ApplicationLaunchProperty.cs b/Ryujinx.HLE/HOS/Services/Arp/ApplicationLaunchProperty.cs index c1c6d26dc9..4962e3ffdc 100644 --- a/Ryujinx.HLE/HOS/Services/Arp/ApplicationLaunchProperty.cs +++ b/Ryujinx.HLE/HOS/Services/Arp/ApplicationLaunchProperty.cs @@ -33,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Services.Arp return new ApplicationLaunchProperty { - TitleId = BitConverter.ToInt64(StringUtils.HexToBytes(context.Device.System.TitleID), 0), + TitleId = BitConverter.ToInt64(StringUtils.HexToBytes(context.Device.System.TitleId), 0), Version = 0x00, BaseGameStorageId = (byte)StorageId.NandSystem, UpdateGameStorageId = (byte)StorageId.None diff --git a/Ryujinx.sln.DotSettings b/Ryujinx.sln.DotSettings index 579d97a459..ed35825493 100644 --- a/Ryujinx.sln.DotSettings +++ b/Ryujinx.sln.DotSettings @@ -4,9 +4,15 @@ UseExplicitType UseExplicitType <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy> + True True True + True + True + True + True True + True True True True diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs index 858c7e398c..8148758710 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -23,19 +23,20 @@ namespace Ryujinx Application.Init(); + string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "system", "prod.keys"); + string userProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch", "prod.keys"); + if (!File.Exists(appDataPath) && !File.Exists(userProfilePath)) + { + MainWindow.CreateErrorDialog($"Key file was not found. Please refer to `KEYS.md` for more info"); + } + Application gtkApplication = new Application("Ryujinx.Ryujinx", GLib.ApplicationFlags.None); - MainWindow mainWindow = new MainWindow(args, gtkApplication); + MainWindow mainWindow = new MainWindow(gtkApplication); gtkApplication.Register(GLib.Cancellable.Current); gtkApplication.AddWindow(mainWindow); mainWindow.Show(); - if (!File.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "system", "prod.keys"))) - { - Logger.PrintWarning(LogClass.Application, "Key file was not found"); - MainWindow.CreateErrorDialog($"Key file was not found. Please refer to `KEYS.md` for more info"); - } - if (args.Length == 1) { mainWindow.LoadApplication(args[0]); @@ -51,7 +52,7 @@ namespace Ryujinx private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { - var exception = e.ExceptionObject as Exception; + Exception exception = e.ExceptionObject as Exception; Logger.PrintError(LogClass.Emulation, $"Unhandled exception caught: {exception}"); diff --git a/Ryujinx/Ui/AboutWindow.cs b/Ryujinx/Ui/AboutWindow.cs index 66d8b05849..8fd1c0b1dc 100644 --- a/Ryujinx/Ui/AboutWindow.cs +++ b/Ryujinx/Ui/AboutWindow.cs @@ -22,6 +22,7 @@ namespace Ryujinx.UI private static Info Information { get; set; } #pragma warning disable CS0649 +#pragma warning disable IDE0044 [GUI] Window _aboutWin; [GUI] Label _versionText; [GUI] Image _ryujinxLogo; @@ -30,6 +31,7 @@ namespace Ryujinx.UI [GUI] Image _discordLogo; [GUI] Image _twitterLogo; #pragma warning restore CS0649 +#pragma warning restore IDE0044 public AboutWindow() : this(new Builder("Ryujinx.Ui.AboutWindow.glade")) { } @@ -103,7 +105,7 @@ namespace Ryujinx.UI OpenUrl("https://twitter.com/RyujinxEmu"); } - private void ContributersButton_Pressed(object sender, ButtonPressEventArgs args) + private void ContributorsButton_Pressed(object sender, ButtonPressEventArgs args) { OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a"); } diff --git a/Ryujinx/Ui/AboutWindow.glade b/Ryujinx/Ui/AboutWindow.glade index 28a8007203..8a38bd9cc7 100644 --- a/Ryujinx/Ui/AboutWindow.glade +++ b/Ryujinx/Ui/AboutWindow.glade @@ -523,11 +523,11 @@ Andy A (BaronKiko) - + True False start - + True diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs index df1b4b12ab..71199da5b0 100644 --- a/Ryujinx/Ui/ApplicationLibrary.cs +++ b/Ryujinx/Ui/ApplicationLibrary.cs @@ -14,8 +14,8 @@ using System.Reflection; using System.Text; using Utf8Json; using Utf8Json.Resolvers; -using SystemState = Ryujinx.HLE.HOS.SystemState; using ApplicationData = Ryujinx.UI.ApplicationLibrary.ApplicationData; +using SystemState = Ryujinx.HLE.HOS.SystemState; namespace Ryujinx.UI { @@ -38,18 +38,14 @@ namespace Ryujinx.UI public string Path { get; set; } } - private static byte[] RyujinxNspIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSPIcon.png"); - private static byte[] RyujinxXciIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxXCIIcon.png"); - private static byte[] RyujinxNcaIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNCAIcon.png"); - private static byte[] RyujinxNroIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNROIcon.png"); - private static byte[] RyujinxNsoIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSOIcon.png"); + private static readonly byte[] _ryujinxNspIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSPIcon.png"); + private static readonly byte[] _ryujinxXciIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxXCIIcon.png"); + private static readonly byte[] _ryujinxNcaIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNCAIcon.png"); + private static readonly byte[] _ryujinxNroIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNROIcon.png"); + private static readonly byte[] _ryujinxNsoIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSOIcon.png"); - private static Keyset KeySet; - private static SystemState.TitleLanguage DesiredTitleLanguage; - - private const double SecondsPerMinute = 60.0; - private const double SecondsPerHour = SecondsPerMinute * 60; - private const double SecondsPerDay = SecondsPerHour * 24; + private static Keyset _keySet; + private static SystemState.TitleLanguage _desiredTitleLanguage; private struct ApplicationMetadata { @@ -58,19 +54,19 @@ namespace Ryujinx.UI public string LastPlayed { get; set; } } - private static ApplicationMetadata AppMetadata; + private static ApplicationMetadata _appMetadata; - public static void LoadApplications(List AppDirs, Keyset keySet, SystemState.TitleLanguage desiredTitleLanguage) + public static void LoadApplications(List appDirs, Keyset keySet, SystemState.TitleLanguage desiredTitleLanguage) { - float numApplicationsFound = 0; - float numApplicationsLoaded = 0; + int numApplicationsFound = 0; + int numApplicationsLoaded = 0; - KeySet = keySet; - DesiredTitleLanguage = desiredTitleLanguage; + _keySet = keySet; + _desiredTitleLanguage = desiredTitleLanguage; // Builds the applications list with paths to found applications List applications = new List(); - foreach (string appDir in AppDirs) + foreach (string appDir in appDirs) { if (Directory.Exists(appDir) == false) { @@ -83,20 +79,56 @@ namespace Ryujinx.UI foreach (string app in apps) { if ((Path.GetExtension(app) == ".xci") || - (Path.GetExtension(app) == ".nsp") || - (Path.GetExtension(app) == ".pfs0")|| (Path.GetExtension(app) == ".nro") || (Path.GetExtension(app) == ".nso")) { applications.Add(app); numApplicationsFound++; } + else if ((Path.GetExtension(app) == ".nsp") || (Path.GetExtension(app) == ".pfs0")) + { + try + { + bool hasMainNca = false; + + PartitionFileSystem nsp = new PartitionFileSystem(new FileStream(app, FileMode.Open, FileAccess.Read).AsStorage()); + foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca")) + { + nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure(); + Nca nca = new Nca(_keySet, ncaFile.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType == NcaContentType.Program && !nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + hasMainNca = true; + } + } + + if (!hasMainNca) continue; + } + catch (InvalidDataException) + { + Logger.PrintWarning(LogClass.Application, "The header key is incorrect or missing and therefore the NCA header content type check has failed."); + } + + applications.Add(app); + numApplicationsFound++; + } else if (Path.GetExtension(app) == ".nca") { - Nca nca = new Nca(KeySet, new FileStream(app, FileMode.Open, FileAccess.Read).AsStorage()); - if (nca.Header.ContentType != NcaContentType.Program) + try { - continue; + Nca nca = new Nca(_keySet, new FileStream(app, FileMode.Open, FileAccess.Read).AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType != NcaContentType.Program || nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + continue; + } + } + catch (InvalidDataException) + { + Logger.PrintWarning(LogClass.Application, "The header key is incorrect or missing and therefore the NCA header content type check has failed."); } applications.Add(app); @@ -108,7 +140,7 @@ namespace Ryujinx.UI // Loops through applications list, creating a struct and then firing an event containing the struct for each application foreach (string applicationPath in applications) { - double filesize = new FileInfo(applicationPath).Length * 0.000000000931; + double fileSize = new FileInfo(applicationPath).Length * 0.000000000931; string titleName = null; string titleId = null; string developer = null; @@ -123,12 +155,12 @@ namespace Ryujinx.UI { try { - IFileSystem controlFs = null; + IFileSystem controlFs; // Store the ControlFS in variable called controlFs if (Path.GetExtension(applicationPath) == ".xci") { - Xci xci = new Xci(KeySet, file.AsStorage()); + Xci xci = new Xci(_keySet, file.AsStorage()); controlFs = GetControlFs(xci.OpenPartition(XciPartitionType.Secure)); } @@ -145,7 +177,7 @@ namespace Ryujinx.UI // Get the title name, title ID, developer name and version number from the NACP version = controlData.DisplayVersion; - titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title; + titleName = controlData.Descriptions[(int)_desiredTitleLanguage].Title; if (string.IsNullOrWhiteSpace(titleName)) { @@ -164,7 +196,7 @@ namespace Ryujinx.UI titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); } - developer = controlData.Descriptions[(int)DesiredTitleLanguage].Developer; + developer = controlData.Descriptions[(int)_desiredTitleLanguage].Developer; if (string.IsNullOrWhiteSpace(developer)) { @@ -174,13 +206,11 @@ namespace Ryujinx.UI // Read the icon from the ControlFS and store it as a byte array try { - controlFs.OpenFile(out IFile icon, $"/icon_{DesiredTitleLanguage}.dat", OpenMode.Read).ThrowIfFailure(); + controlFs.OpenFile(out IFile icon, $"/icon_{_desiredTitleLanguage}.dat", OpenMode.Read).ThrowIfFailure(); - using (MemoryStream stream = new MemoryStream()) - { - icon.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } + using MemoryStream stream = new MemoryStream(); + icon.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); } catch (HorizonResultException) { @@ -229,81 +259,73 @@ namespace Ryujinx.UI version = "?"; applicationIcon = NspOrXciIcon(applicationPath); - Logger.PrintWarning(LogClass.Application, $"The file is not an NCA file or the header key is incorrect. Errored File: {applicationPath}"); - } - catch (Exception exception) - { - Logger.PrintWarning(LogClass.Application, $"This warning usualy means that you have a DLC in one of you game directories\n{exception}"); - - continue; + Logger.PrintWarning(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); } } else if (Path.GetExtension(applicationPath) == ".nro") { BinaryReader reader = new BinaryReader(file); - byte[] Read(long Position, int Size) + byte[] Read(long position, int size) { - file.Seek(Position, SeekOrigin.Begin); + file.Seek(position, SeekOrigin.Begin); - return reader.ReadBytes(Size); + return reader.ReadBytes(size); } file.Seek(24, SeekOrigin.Begin); - int AssetOffset = reader.ReadInt32(); + int assetOffset = reader.ReadInt32(); - if (Encoding.ASCII.GetString(Read(AssetOffset, 4)) == "ASET") + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") { - byte[] IconSectionInfo = Read(AssetOffset + 8, 0x10); + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); - long iconOffset = BitConverter.ToInt64(IconSectionInfo, 0); - long iconSize = BitConverter.ToInt64(IconSectionInfo, 8); + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); ulong nacpOffset = reader.ReadUInt64(); ulong nacpSize = reader.ReadUInt64(); // Reads and stores game icon as byte array - applicationIcon = Read(AssetOffset + iconOffset, (int)iconSize); + applicationIcon = Read(assetOffset + iconOffset, (int)iconSize); // Creates memory stream out of byte array which is the NACP - using (MemoryStream stream = new MemoryStream(Read(AssetOffset + (int)nacpOffset, (int)nacpSize))) + using MemoryStream stream = new MemoryStream(Read(assetOffset + (int)nacpOffset, (int)nacpSize)); + // Creates NACP class from the memory stream + Nacp controlData = new Nacp(stream); + + // Get the title name, title ID, developer name and version number from the NACP + version = controlData.DisplayVersion; + + titleName = controlData.Descriptions[(int)_desiredTitleLanguage].Title; + + if (string.IsNullOrWhiteSpace(titleName)) { - // Creates NACP class from the memory stream - Nacp controlData = new Nacp(stream); + titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; + } - // Get the title name, title ID, developer name and version number from the NACP - version = controlData.DisplayVersion; + titleId = controlData.PresenceGroupId.ToString("x16"); - titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title; + if (string.IsNullOrWhiteSpace(titleId)) + { + titleId = controlData.SaveDataOwnerId.ToString("x16"); + } - if (string.IsNullOrWhiteSpace(titleName)) - { - titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; - } + if (string.IsNullOrWhiteSpace(titleId)) + { + titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); + } - titleId = controlData.PresenceGroupId.ToString("x16"); + developer = controlData.Descriptions[(int)_desiredTitleLanguage].Developer; - if (string.IsNullOrWhiteSpace(titleId)) - { - titleId = controlData.SaveDataOwnerId.ToString("x16"); - } - - if (string.IsNullOrWhiteSpace(titleId)) - { - titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); - } - - developer = controlData.Descriptions[(int)DesiredTitleLanguage].Developer; - - if (string.IsNullOrWhiteSpace(developer)) - { - developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer; - } + if (string.IsNullOrWhiteSpace(developer)) + { + developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer; } } else { - applicationIcon = RyujinxNroIcon; + applicationIcon = _ryujinxNroIcon; titleName = "Application"; titleId = "0000000000000000"; developer = "Unknown"; @@ -315,11 +337,11 @@ namespace Ryujinx.UI { if (Path.GetExtension(applicationPath) == ".nca") { - applicationIcon = RyujinxNcaIcon; + applicationIcon = _ryujinxNcaIcon; } else if (Path.GetExtension(applicationPath) == ".nso") { - applicationIcon = RyujinxNsoIcon; + applicationIcon = _ryujinxNsoIcon; } string fileName = Path.GetFileName(applicationPath); @@ -336,20 +358,20 @@ namespace Ryujinx.UI } } - (bool, string, string) metaData = GetMetadata(titleId); + (bool fav, string timePlayed, string lastPlayed) metaData = GetMetadata(titleId); ApplicationData data = new ApplicationData() { - Favorite = metaData.Item1, + Favorite = metaData.fav, Icon = applicationIcon, TitleName = titleName, TitleId = titleId, Developer = developer, Version = version, - TimePlayed = metaData.Item2, - LastPlayed = metaData.Item3, + TimePlayed = metaData.timePlayed, + LastPlayed = metaData.lastPlayed, FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1), - FileSize = (filesize < 1) ? (filesize * 1024).ToString("0.##") + "MB" : filesize.ToString("0.##") + "GB", + FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB", Path = applicationPath, }; @@ -379,29 +401,29 @@ namespace Ryujinx.UI return resourceByteArray; } - private static IFileSystem GetControlFs(PartitionFileSystem Pfs) + private static IFileSystem GetControlFs(PartitionFileSystem pfs) { Nca controlNca = null; - // Add keys to keyset if needed - foreach (DirectoryEntryEx ticketEntry in Pfs.EnumerateEntries("/", "*.tik")) + // Add keys to key set if needed + foreach (DirectoryEntryEx ticketEntry in pfs.EnumerateEntries("/", "*.tik")) { - Result result = Pfs.OpenFile(out IFile ticketFile, ticketEntry.FullPath, OpenMode.Read); + Result result = pfs.OpenFile(out IFile ticketFile, ticketEntry.FullPath, OpenMode.Read); if (result.IsSuccess()) { Ticket ticket = new Ticket(ticketFile.AsStream()); - KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet))); + _keySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_keySet))); } } // Find the Control NCA and store it in variable called controlNca - foreach (DirectoryEntryEx fileEntry in Pfs.EnumerateEntries("/", "*.nca")) + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) { - Pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure(); + pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure(); - Nca nca = new Nca(KeySet, ncaFile.AsStorage()); + Nca nca = new Nca(_keySet, ncaFile.AsStorage()); if (nca.Header.ContentType == NcaContentType.Control) { @@ -413,72 +435,80 @@ namespace Ryujinx.UI return controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); } - private static (bool, string, string) GetMetadata(string TitleId) + private static (bool fav, string timePlayed, string lastPlayed) GetMetadata(string titleId) { - string metadataFolder = Path.Combine(new VirtualFileSystem().GetBasePath(), "games", TitleId, "gui"); + string metadataFolder = Path.Combine(new VirtualFileSystem().GetBasePath(), "games", titleId, "gui"); string metadataFile = Path.Combine(metadataFolder, "metadata.json"); - IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); + IJsonFormatterResolver resolver = CompositeResolver.Create(StandardResolver.AllowPrivateSnakeCase); if (!File.Exists(metadataFile)) { Directory.CreateDirectory(metadataFolder); - AppMetadata = new ApplicationMetadata + _appMetadata = new ApplicationMetadata { Favorite = false, TimePlayed = 0, LastPlayed = "Never" }; - byte[] saveData = JsonSerializer.Serialize(AppMetadata, resolver); + byte[] saveData = JsonSerializer.Serialize(_appMetadata, resolver); File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson()); } using (Stream stream = File.OpenRead(metadataFile)) { - AppMetadata = JsonSerializer.Deserialize(stream, resolver); + _appMetadata = JsonSerializer.Deserialize(stream, resolver); } - string timePlayed; - - if (AppMetadata.TimePlayed < SecondsPerMinute) - { - timePlayed = $"{AppMetadata.TimePlayed}s"; - } - else if (AppMetadata.TimePlayed < SecondsPerHour) - { - timePlayed = $"{Math.Round(AppMetadata.TimePlayed / SecondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins"; - } - else if (AppMetadata.TimePlayed < SecondsPerDay) - { - timePlayed = $"{Math.Round(AppMetadata.TimePlayed / SecondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs"; - } - else - { - timePlayed = $"{Math.Round(AppMetadata.TimePlayed / SecondsPerDay, 2, MidpointRounding.AwayFromZero)} days"; - } - - return (AppMetadata.Favorite, timePlayed, AppMetadata.LastPlayed); + return (_appMetadata.Favorite, ConvertSecondsToReadableString(_appMetadata.TimePlayed), _appMetadata.LastPlayed); } private static byte[] NspOrXciIcon(string applicationPath) { if (Path.GetExtension(applicationPath) == ".xci") { - return RyujinxXciIcon; + return _ryujinxXciIcon; } else { - return RyujinxNspIcon; + return _ryujinxNspIcon; } } + + private static string ConvertSecondsToReadableString(double seconds) + { + const int secondsPerMinute = 60; + const int secondsPerHour = secondsPerMinute * 60; + const int secondsPerDay = secondsPerHour * 24; + string readableString; + + if (seconds < secondsPerMinute) + { + readableString = $"{seconds}s"; + } + else if (seconds < secondsPerHour) + { + readableString = $"{Math.Round(seconds / secondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins"; + } + else if (seconds < secondsPerDay) + { + readableString = $"{Math.Round(seconds / secondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs"; + } + else + { + readableString = $"{Math.Round(seconds / secondsPerDay, 2, MidpointRounding.AwayFromZero)} days"; + } + + return readableString; + } } public class ApplicationAddedEventArgs : EventArgs { public ApplicationData AppData { get; set; } - public float NumAppsFound { get; set; } - public float NumAppsLoaded { get; set; } + public int NumAppsFound { get; set; } + public int NumAppsLoaded { get; set; } } } diff --git a/Ryujinx/Ui/GLScreen.cs b/Ryujinx/Ui/GLScreen.cs index c5ad1d5959..eb7b2d66e6 100644 --- a/Ryujinx/Ui/GLScreen.cs +++ b/Ryujinx/Ui/GLScreen.cs @@ -300,8 +300,8 @@ namespace Ryujinx.UI string titleNameSection = string.IsNullOrWhiteSpace(_device.System.TitleName) ? string.Empty : " | " + _device.System.TitleName; - string titleIDSection = string.IsNullOrWhiteSpace(_device.System.TitleID) ? string.Empty - : " | " + _device.System.TitleID.ToUpper(); + string titleIDSection = string.IsNullOrWhiteSpace(_device.System.TitleId) ? string.Empty + : " | " + _device.System.TitleId.ToUpper(); _newTitle = $"Ryujinx{titleNameSection}{titleIDSection} | Host FPS: {hostFps:0.0} | Game FPS: {gameFps:0.0} | " + $"Game Vsync: {(_device.EnableDeviceVsync ? "On" : "Off")}"; diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 39f2266a5d..dac7308d44 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -49,20 +49,22 @@ namespace Ryujinx.UI private static ListStore _tableStore; - private static Task UpdateGameTableTask; + private static Task _updateGameTableTask; - private static bool _gameLoaded = false; - private static bool _ending = false; + private static bool _gameLoaded; + private static bool _ending; - private static TreeViewColumn favColumn; - private static TreeViewColumn appColumn; - private static TreeViewColumn devColumn; - private static TreeViewColumn versionColumn; - private static TreeViewColumn timePlayedColumn; - private static TreeViewColumn lastPlayedColumn; - private static TreeViewColumn fileExtColumn; - private static TreeViewColumn fileSizeColumn; - private static TreeViewColumn pathColumn; + private static TreeViewColumn _favColumn; + private static TreeViewColumn _appColumn; + private static TreeViewColumn _devColumn; + private static TreeViewColumn _versionColumn; + private static TreeViewColumn _timePlayedColumn; + private static TreeViewColumn _lastPlayedColumn; + private static TreeViewColumn _fileExtColumn; + private static TreeViewColumn _fileSizeColumn; + private static TreeViewColumn _pathColumn; + + private static TreeView _treeView; private struct ApplicationMetadata { @@ -78,6 +80,7 @@ namespace Ryujinx.UI public static RichPresence DiscordPresence; #pragma warning disable CS0649 +#pragma warning disable IDE0044 [GUI] Window _mainWin; [GUI] CheckMenuItem _fullScreen; [GUI] MenuItem _stopEmulation; @@ -95,10 +98,11 @@ namespace Ryujinx.UI [GUI] Label _progressLabel; [GUI] LevelBar _progressBar; #pragma warning restore CS0649 +#pragma warning restore IDE0044 - public MainWindow(string[] args, Application gtkApplication) : this(new Builder("Ryujinx.Ui.MainWindow.glade"), args, gtkApplication) { } + public MainWindow(Application gtkApplication) : this(new Builder("Ryujinx.Ui.MainWindow.glade"), gtkApplication) { } - private MainWindow(Builder builder, string[] args, Application gtkApplication) : base(builder.GetObject("_mainWin").Handle) + private MainWindow(Builder builder, Application gtkApplication) : base(builder.GetObject("_mainWin").Handle) { builder.Autoconnect(this); @@ -114,6 +118,8 @@ namespace Ryujinx.UI _gtkApplication = gtkApplication; + _treeView = _gameTable; + Configuration.Load(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json")); Configuration.InitialConfigure(_device); @@ -181,10 +187,10 @@ namespace Ryujinx.UI internal static void ApplyTheme() { - CssProvider cssProvider = new CssProvider(); - if (SwitchSettings.SwitchConfig.EnableCustomTheme) { + CssProvider cssProvider = new CssProvider(); + if (File.Exists(SwitchSettings.SwitchConfig.CustomThemePath) && (System.IO.Path.GetExtension(SwitchSettings.SwitchConfig.CustomThemePath) == ".css")) { cssProvider.LoadFromPath(SwitchSettings.SwitchConfig.CustomThemePath); @@ -193,9 +199,9 @@ namespace Ryujinx.UI { Logger.PrintWarning(LogClass.Application, $"The \"custom_theme_path\" section in \"Config.json\" contains an invalid path: \"{SwitchSettings.SwitchConfig.CustomThemePath}\""); } - } - StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800); + StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800); + } } private void UpdateColumns() @@ -221,36 +227,39 @@ namespace Ryujinx.UI foreach (TreeViewColumn column in _gameTable.Columns) { - if (column.Title == "Fav") { favColumn = column; } - else if (column.Title == "Application") { appColumn = column; } - else if (column.Title == "Developer") { devColumn = column; } - else if (column.Title == "Version") { versionColumn = column; } - else if (column.Title == "Time Played") { timePlayedColumn = column; } - else if (column.Title == "Last Played") { lastPlayedColumn = column; } - else if (column.Title == "File Ext") { fileExtColumn = column; } - else if (column.Title == "File Size") { fileSizeColumn = column; } - else if (column.Title == "Path") { pathColumn = column; } + if (column.Title == "Fav") { _favColumn = column; } + else if (column.Title == "Application") { _appColumn = column; } + else if (column.Title == "Developer") { _devColumn = column; } + else if (column.Title == "Version") { _versionColumn = column; } + else if (column.Title == "Time Played") { _timePlayedColumn = column; } + else if (column.Title == "Last Played") { _lastPlayedColumn = column; } + else if (column.Title == "File Ext") { _fileExtColumn = column; } + else if (column.Title == "File Size") { _fileSizeColumn = column; } + else if (column.Title == "Path") { _pathColumn = column; } } - if (SwitchSettings.SwitchConfig.GuiColumns.FavColumn) { favColumn.SortColumnId = 0; } - if (SwitchSettings.SwitchConfig.GuiColumns.IconColumn) { appColumn.SortColumnId = 2; } - if (SwitchSettings.SwitchConfig.GuiColumns.AppColumn) { devColumn.SortColumnId = 3; } - if (SwitchSettings.SwitchConfig.GuiColumns.DevColumn) { versionColumn.SortColumnId = 4; } - if (SwitchSettings.SwitchConfig.GuiColumns.TimePlayedColumn) { timePlayedColumn.SortColumnId = 5; } - if (SwitchSettings.SwitchConfig.GuiColumns.LastPlayedColumn) { lastPlayedColumn.SortColumnId = 6; } - if (SwitchSettings.SwitchConfig.GuiColumns.FileExtColumn) { fileExtColumn.SortColumnId = 7; } - if (SwitchSettings.SwitchConfig.GuiColumns.FileSizeColumn) { fileSizeColumn.SortColumnId = 8; } - if (SwitchSettings.SwitchConfig.GuiColumns.PathColumn) { pathColumn.SortColumnId = 9; } + if (SwitchSettings.SwitchConfig.GuiColumns.FavColumn) { _favColumn.SortColumnId = 0; } + if (SwitchSettings.SwitchConfig.GuiColumns.IconColumn) { _appColumn.SortColumnId = 2; } + if (SwitchSettings.SwitchConfig.GuiColumns.AppColumn) { _devColumn.SortColumnId = 3; } + if (SwitchSettings.SwitchConfig.GuiColumns.DevColumn) { _versionColumn.SortColumnId = 4; } + if (SwitchSettings.SwitchConfig.GuiColumns.TimePlayedColumn) { _timePlayedColumn.SortColumnId = 5; } + if (SwitchSettings.SwitchConfig.GuiColumns.LastPlayedColumn) { _lastPlayedColumn.SortColumnId = 6; } + if (SwitchSettings.SwitchConfig.GuiColumns.FileExtColumn) { _fileExtColumn.SortColumnId = 7; } + if (SwitchSettings.SwitchConfig.GuiColumns.FileSizeColumn) { _fileSizeColumn.SortColumnId = 8; } + if (SwitchSettings.SwitchConfig.GuiColumns.PathColumn) { _pathColumn.SortColumnId = 9; } } internal static async Task UpdateGameTable() { - if (UpdateGameTableTask != null && !UpdateGameTableTask.IsCompleted) return; + if (_updateGameTableTask != null && !_updateGameTableTask.IsCompleted) return; _tableStore.Clear(); + _treeView.HeadersClickable = false; - UpdateGameTableTask = Task.Run(() => ApplicationLibrary.LoadApplications(SwitchSettings.SwitchConfig.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage)); - await UpdateGameTableTask; + _updateGameTableTask = Task.Run(() => ApplicationLibrary.LoadApplications(SwitchSettings.SwitchConfig.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage)); + await _updateGameTableTask; + + _treeView.HeadersClickable = true; } internal void LoadApplication(string path) @@ -322,7 +331,7 @@ namespace Ryujinx.UI #if MACOS_BUILD CreateGameWindow(); #else - new Thread(new ThreadStart(CreateGameWindow)).Start(); + new Thread(CreateGameWindow).Start(); #endif _gameLoaded = true; @@ -330,12 +339,12 @@ namespace Ryujinx.UI if (DiscordIntegrationEnabled) { - if (File.ReadAllLines(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RPsupported.dat")).Contains(_device.System.TitleID)) + if (File.ReadAllLines(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RPsupported.dat")).Contains(_device.System.TitleId)) { - DiscordPresence.Assets.LargeImageKey = _device.System.TitleID; + DiscordPresence.Assets.LargeImageKey = _device.System.TitleId; } - string state = _device.System.TitleID; + string state = _device.System.TitleId; if (state == null) { @@ -363,35 +372,35 @@ namespace Ryujinx.UI DiscordClient.SetPresence(DiscordPresence); } - string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleID, "gui"); + string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleId, "gui"); string metadataFile = System.IO.Path.Combine(metadataFolder, "metadata.json"); IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); - ApplicationMetadata AppMetadata = new ApplicationMetadata(); + ApplicationMetadata appMetadata; if (!File.Exists(metadataFile)) { Directory.CreateDirectory(metadataFolder); - AppMetadata = new ApplicationMetadata + appMetadata = new ApplicationMetadata { Favorite = false, TimePlayed = 0, LastPlayed = "Never" }; - byte[] data = JsonSerializer.Serialize(AppMetadata, resolver); + byte[] data = JsonSerializer.Serialize(appMetadata, resolver); File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson()); } using (Stream stream = File.OpenRead(metadataFile)) { - AppMetadata = JsonSerializer.Deserialize(stream, resolver); + appMetadata = JsonSerializer.Deserialize(stream, resolver); } - AppMetadata.LastPlayed = DateTime.UtcNow.ToString(); + appMetadata.LastPlayed = DateTime.UtcNow.ToString(); - byte[] saveData = JsonSerializer.Serialize(AppMetadata, resolver); + byte[] saveData = JsonSerializer.Serialize(appMetadata, resolver); File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson()); } } @@ -416,35 +425,35 @@ namespace Ryujinx.UI if (_gameLoaded) { - string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleID, "gui"); + string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleId, "gui"); string metadataFile = System.IO.Path.Combine(metadataFolder, "metadata.json"); IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); - ApplicationMetadata AppMetadata = new ApplicationMetadata(); + ApplicationMetadata appMetadata; if (!File.Exists(metadataFile)) { Directory.CreateDirectory(metadataFolder); - AppMetadata = new ApplicationMetadata + appMetadata = new ApplicationMetadata { Favorite = false, TimePlayed = 0, LastPlayed = "Never" }; - byte[] data = JsonSerializer.Serialize(AppMetadata, resolver); + byte[] data = JsonSerializer.Serialize(appMetadata, resolver); File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson()); } using (Stream stream = File.OpenRead(metadataFile)) { - AppMetadata = JsonSerializer.Deserialize(stream, resolver); + appMetadata = JsonSerializer.Deserialize(stream, resolver); } - AppMetadata.TimePlayed += Math.Round(DateTime.UtcNow.Subtract(DateTime.Parse(AppMetadata.LastPlayed)).TotalSeconds, MidpointRounding.AwayFromZero); + appMetadata.TimePlayed += Math.Round(DateTime.UtcNow.Subtract(DateTime.Parse(appMetadata.LastPlayed)).TotalSeconds, MidpointRounding.AwayFromZero); - byte[] saveData = JsonSerializer.Serialize(AppMetadata, resolver); + byte[] saveData = JsonSerializer.Serialize(appMetadata, resolver); File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson()); } @@ -482,7 +491,7 @@ namespace Ryujinx.UI { _tableStore.AppendValues(e.AppData.Favorite, new Gdk.Pixbuf(e.AppData.Icon, 75, 75), $"{e.AppData.TitleName}\n{e.AppData.TitleId.ToUpper()}", e.AppData.Developer, e.AppData.Version, e.AppData.TimePlayed, e.AppData.LastPlayed, e.AppData.FileExtension, e.AppData.FileSize, e.AppData.Path); _progressLabel.Text = $"{e.NumAppsLoaded}/{e.NumAppsFound} Games Loaded"; - _progressBar.Value = e.NumAppsLoaded / e.NumAppsFound; + _progressBar.Value = (float)e.NumAppsLoaded / e.NumAppsFound; } private void FavToggle_Toggled(object sender, ToggledArgs args) @@ -492,27 +501,27 @@ namespace Ryujinx.UI string metadataPath = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", titleid, "gui", "metadata.json"); IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); - ApplicationMetadata AppMetadata = new ApplicationMetadata(); + ApplicationMetadata appMetadata; using (Stream stream = File.OpenRead(metadataPath)) { - AppMetadata = JsonSerializer.Deserialize(stream, resolver); + appMetadata = JsonSerializer.Deserialize(stream, resolver); } if ((bool)_tableStore.GetValue(treeIter, 0)) { _tableStore.SetValue(treeIter, 0, false); - AppMetadata.Favorite = false; + appMetadata.Favorite = false; } else { _tableStore.SetValue(treeIter, 0, true); - AppMetadata.Favorite = true; + appMetadata.Favorite = true; } - byte[] saveData = JsonSerializer.Serialize(AppMetadata, resolver); + byte[] saveData = JsonSerializer.Serialize(appMetadata, resolver); File.WriteAllText(metadataPath, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson()); } @@ -597,12 +606,12 @@ namespace Ryujinx.UI private void Settings_Pressed(object sender, EventArgs args) { - SwitchSettings SettingsWin = new SwitchSettings(_device); + SwitchSettings settingsWin = new SwitchSettings(_device); _gtkApplication.Register(GLib.Cancellable.Current); - _gtkApplication.AddWindow(SettingsWin); + _gtkApplication.AddWindow(settingsWin); - SettingsWin.Show(); + settingsWin.Show(); } private void Update_Pressed(object sender, EventArgs args) @@ -621,12 +630,12 @@ namespace Ryujinx.UI private void About_Pressed(object sender, EventArgs args) { - AboutWindow AboutWin = new AboutWindow(); + AboutWindow aboutWin = new AboutWindow(); _gtkApplication.Register(GLib.Cancellable.Current); - _gtkApplication.AddWindow(AboutWin); + _gtkApplication.AddWindow(aboutWin); - AboutWin.Show(); + aboutWin.Show(); } private void Fav_Toggled(object sender, EventArgs args) @@ -785,7 +794,7 @@ namespace Ryujinx.UI bValue = bValue.Substring(0, bValue.Length - 1); } - if (float.Parse(aValue) > float.Parse(bValue)) return -1; + if (float.Parse(aValue) > float.Parse(bValue)) return -1; else if (float.Parse(bValue) > float.Parse(aValue)) return 1; else return 0; } @@ -808,15 +817,15 @@ namespace Ryujinx.UI if (aValue.Substring(aValue.Length - 2) == "GB") { - aValue = (float.Parse(aValue.Substring(0, aValue.Length - 2)) * 1024).ToString(); + aValue = (float.Parse(aValue[0..^2]) * 1024).ToString(); } - else aValue = aValue.Substring(0, aValue.Length - 2); + else aValue = aValue[0..^2]; if (bValue.Substring(bValue.Length - 2) == "GB") { - bValue = (float.Parse(bValue.Substring(0, bValue.Length - 2)) * 1024).ToString(); + bValue = (float.Parse(bValue[0..^2]) * 1024).ToString(); } - else bValue = bValue.Substring(0, bValue.Length - 2); + else bValue = bValue[0..^2]; if (float.Parse(aValue) > float.Parse(bValue)) return -1; else if (float.Parse(bValue) > float.Parse(aValue)) return 1; diff --git a/Ryujinx/Ui/SwitchSettings.cs b/Ryujinx/Ui/SwitchSettings.cs index 2431e34cf5..14030b239b 100644 --- a/Ryujinx/Ui/SwitchSettings.cs +++ b/Ryujinx/Ui/SwitchSettings.cs @@ -15,13 +15,14 @@ namespace Ryujinx.UI { internal static Configuration SwitchConfig { get; set; } - private HLE.Switch _device; + private readonly HLE.Switch _device; private static ListStore _gameDirsBoxStore; private static bool _listeningForKeypress; #pragma warning disable CS0649 +#pragma warning disable IDE0044 [GUI] Window _settingsWin; [GUI] CheckButton _errorLogToggle; [GUI] CheckButton _warningLogToggle; @@ -79,8 +80,9 @@ namespace Ryujinx.UI [GUI] ToggleButton _r1; [GUI] ToggleButton _zR1; #pragma warning restore CS0649 +#pragma warning restore IDE0044 - public static void ConfigureSettings(Configuration Instance) { SwitchConfig = Instance; } + public static void ConfigureSettings(Configuration instance) { SwitchConfig = instance; } public SwitchSettings(HLE.Switch device) : this(new Builder("Ryujinx.Ui.SwitchSettings.glade"), device) { } @@ -200,18 +202,18 @@ namespace Ryujinx.UI _listeningForKeypress = true; - void On_KeyPress(object Sender, KeyPressEventArgs KeyPressed) + void On_KeyPress(object o, KeyPressEventArgs keyPressed) { - string key = KeyPressed.Event.Key.ToString(); + string key = keyPressed.Event.Key.ToString(); string capKey = key.First().ToString().ToUpper() + key.Substring(1); if (Enum.IsDefined(typeof(OpenTK.Input.Key), capKey)) { button.Label = capKey; } - else if (GdkToOpenTKInput.ContainsKey(key)) + else if (GdkToOpenTkInput.ContainsKey(key)) { - button.Label = GdkToOpenTKInput[key]; + button.Label = GdkToOpenTkInput[key]; } else { @@ -233,21 +235,20 @@ namespace Ryujinx.UI private void Controller_Changed(object sender, EventArgs args, string controllerType, Image controllerImage) { - if (controllerType == "ProController") + switch (controllerType) { - controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ProCon.png", 500, 500); - } - else if (controllerType == "NpadLeft") - { - controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.BlueCon.png", 500, 500); - } - else if (controllerType == "NpadRight") - { - controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.RedCon.png", 500, 500); - } - else - { - controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyCon.png", 500, 500); + case "ProController": + controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ProCon.png", 500, 500); + break; + case "NpadLeft": + controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.BlueCon.png", 500, 500); + break; + case "NpadRight": + controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.RedCon.png", 500, 500); + break; + default: + controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyCon.png", 500, 500); + break; } } @@ -383,7 +384,9 @@ namespace Ryujinx.UI Configuration.Configure(_device, SwitchConfig); MainWindow.ApplyTheme(); +#pragma warning disable CS4014 MainWindow.UpdateGameTable(); +#pragma warning restore CS4014 Dispose(); } @@ -392,7 +395,7 @@ namespace Ryujinx.UI Dispose(); } - public readonly Dictionary GdkToOpenTKInput = new Dictionary() + public readonly Dictionary GdkToOpenTkInput = new Dictionary() { { "Key_0", "Number0" }, { "Key_1", "Number1" },