From 7b0576db71e57e663e3c59ef80939fdb0b0c8be7 Mon Sep 17 00:00:00 2001 From: Xpl0itR Date: Fri, 31 Jan 2020 18:21:46 +0000 Subject: [PATCH] Fix application list (#891) * Fix application list * Convert file extensions to lowercase before comparing * AcK's requested changes * fixed bug found by gdkchan's requested changes * Account for mismatch between LibHac.TitleLanguage and ...System.Language --- Ryujinx.HLE/HOS/Horizon.cs | 14 +- Ryujinx/Ui/ApplicationAddedEventArgs.cs | 6 +- .../Ui/ApplicationCountUpdatedEventArgs.cs | 10 + Ryujinx/Ui/ApplicationLibrary.cs | 348 ++++++++++-------- Ryujinx/Ui/MainWindow.cs | 36 +- Ryujinx/Ui/SwitchSettings.cs | 2 - 6 files changed, 248 insertions(+), 168 deletions(-) create mode 100644 Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index e4e089434..7254a5ed5 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -617,19 +617,19 @@ namespace Ryujinx.HLE.HOS metaData.TitleName = nacp.Titles.ToArray().FirstOrDefault(x => x.Name[0] != 0).Name.ToString(); } - metaData.Aci0.TitleId = nacp.PresenceGroupId; - - if (metaData.Aci0.TitleId == 0) + if (nacp.PresenceGroupId != 0) + { + metaData.Aci0.TitleId = nacp.PresenceGroupId; + } + else if (nacp.SaveDataOwnerId.Value != 0) { metaData.Aci0.TitleId = nacp.SaveDataOwnerId.Value; } - - if (metaData.Aci0.TitleId == 0) + else if (nacp.AddOnContentBaseId != 0) { metaData.Aci0.TitleId = nacp.AddOnContentBaseId - 0x1000; } - - if (metaData.Aci0.TitleId.ToString("x16") == "fffffffffffff000") + else { metaData.Aci0.TitleId = 0000000000000000; } diff --git a/Ryujinx/Ui/ApplicationAddedEventArgs.cs b/Ryujinx/Ui/ApplicationAddedEventArgs.cs index 85a2f5a18..5e09389b5 100644 --- a/Ryujinx/Ui/ApplicationAddedEventArgs.cs +++ b/Ryujinx/Ui/ApplicationAddedEventArgs.cs @@ -4,8 +4,6 @@ namespace Ryujinx.Ui { public class ApplicationAddedEventArgs : EventArgs { - public ApplicationData AppData { get; set; } - public int NumAppsFound { get; set; } - public int NumAppsLoaded { get; set; } + public ApplicationData AppData { get; set; } } -} +} \ No newline at end of file diff --git a/Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs b/Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs new file mode 100644 index 000000000..78e2e50ce --- /dev/null +++ b/Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ryujinx.Ui +{ + public class ApplicationCountUpdatedEventArgs : EventArgs + { + public int NumAppsFound { get; set; } + public int NumAppsLoaded { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs index 9d0f2617d..3707cc634 100644 --- a/Ryujinx/Ui/ApplicationLibrary.cs +++ b/Ryujinx/Ui/ApplicationLibrary.cs @@ -26,7 +26,8 @@ namespace Ryujinx.Ui { public class ApplicationLibrary { - public static event EventHandler ApplicationAdded; + public static event EventHandler ApplicationAdded; + public static event EventHandler ApplicationCountUpdated; private static readonly byte[] _nspIcon = GetResourceBytes("Ryujinx.Ui.assets.NSPIcon.png"); private static readonly byte[] _xciIcon = GetResourceBytes("Ryujinx.Ui.assets.XCIIcon.png"); @@ -36,12 +37,14 @@ namespace Ryujinx.Ui private static VirtualFileSystem _virtualFileSystem; private static Language _desiredTitleLanguage; + private static bool _loadingError; public static void LoadApplications(List appDirs, VirtualFileSystem virtualFileSystem, Language desiredTitleLanguage) { int numApplicationsFound = 0; int numApplicationsLoaded = 0; + _loadingError = false; _virtualFileSystem = virtualFileSystem; _desiredTitleLanguage = desiredTitleLanguage; @@ -49,7 +52,7 @@ namespace Ryujinx.Ui List applications = new List(); foreach (string appDir in appDirs) { - if (Directory.Exists(appDir) == false) + if (!Directory.Exists(appDir)) { Logger.PrintWarning(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); @@ -58,67 +61,16 @@ namespace Ryujinx.Ui foreach (string app in Directory.GetFiles(appDir, "*.*", SearchOption.AllDirectories)) { - if ((Path.GetExtension(app) == ".xci") || - (Path.GetExtension(app) == ".nro") || - (Path.GetExtension(app) == ".nso") || - (Path.GetFileName(app) == "hbl.nsp")) + if ((Path.GetExtension(app).ToLower() == ".nsp") || + (Path.GetExtension(app).ToLower() == ".pfs0")|| + (Path.GetExtension(app).ToLower() == ".xci") || + (Path.GetExtension(app).ToLower() == ".nca") || + (Path.GetExtension(app).ToLower() == ".nro") || + (Path.GetExtension(app).ToLower() == ".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(_virtualFileSystem.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, $"{app}: 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") - { - try - { - Nca nca = new Nca(_virtualFileSystem.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, $"{app}: The header key is incorrect or missing and therefore the NCA header content type check has failed."); - } - - applications.Add(app); - numApplicationsFound++; - } } } @@ -135,15 +87,16 @@ namespace Ryujinx.Ui using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read)) { - if ((Path.GetExtension(applicationPath) == ".nsp") || - (Path.GetExtension(applicationPath) == ".pfs0") || - (Path.GetExtension(applicationPath) == ".xci")) + if ((Path.GetExtension(applicationPath).ToLower() == ".nsp") || + (Path.GetExtension(applicationPath).ToLower() == ".pfs0") || + (Path.GetExtension(applicationPath).ToLower() == ".xci")) { try { PartitionFileSystem pfs; - - if (Path.GetExtension(applicationPath) == ".xci") + bool isExeFs = false; + + if (Path.GetExtension(applicationPath).ToLower() == ".xci") { Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); @@ -152,13 +105,41 @@ namespace Ryujinx.Ui else { pfs = new PartitionFileSystem(file.AsStorage()); + + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") + { + pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure(); + + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType == NcaContentType.Program && !nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + hasMainNca = true; + + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (!hasMainNca && !isExeFs) + { + numApplicationsFound--; + + continue; + } } - // Store the ControlFS in variable called controlFs - IFileSystem controlFs = GetControlFs(pfs); - - // If this is null then this is probably not a normal NSP, it's probably an ExeFS as an NSP - if (controlFs == null) + if (isExeFs) { applicationIcon = _nspIcon; @@ -174,6 +155,9 @@ namespace Ryujinx.Ui } else { + // Store the ControlFS in variable called controlFs + IFileSystem controlFs = GetControlFs(pfs); + // Creates NACP class from the NACP file controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp", OpenMode.Read).ThrowIfFailure(); @@ -182,31 +166,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; - - if (string.IsNullOrWhiteSpace(titleName)) - { - titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; - } - - titleId = controlData.PresenceGroupId.ToString("x16"); - - 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; - } + GetNameIdDeveloper(controlData, out titleName, out titleId, out developer); // Read the icon from the ControlFS and store it as a byte array try @@ -244,25 +204,35 @@ namespace Ryujinx.Ui if (applicationIcon == null) { - applicationIcon = Path.GetExtension(applicationPath) == ".xci" ? _xciIcon : _nspIcon; + applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; } } } } catch (MissingKeyException exception) { - applicationIcon = Path.GetExtension(applicationPath) == ".xci" ? _xciIcon : _nspIcon; + applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; Logger.PrintWarning(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); } catch (InvalidDataException) { - applicationIcon = Path.GetExtension(applicationPath) == ".xci" ? _xciIcon : _nspIcon; + applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; Logger.PrintWarning(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); } + catch (Exception exception) + { + Logger.PrintError(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + Logger.PrintDebug(LogClass.Application, exception.ToString()); + + numApplicationsFound--; + _loadingError = true; + + continue; + } } - else if (Path.GetExtension(applicationPath) == ".nro") + else if (Path.GetExtension(applicationPath).ToLower() == ".nro") { BinaryReader reader = new BinaryReader(file); @@ -273,67 +243,87 @@ namespace Ryujinx.Ui return reader.ReadBytes(size); } - file.Seek(24, SeekOrigin.Begin); - int assetOffset = reader.ReadInt32(); - - if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + try { - byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + file.Seek(24, SeekOrigin.Begin); - long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); - long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + int assetOffset = reader.ReadInt32(); - ulong nacpOffset = reader.ReadUInt64(); - ulong nacpSize = reader.ReadUInt64(); - - // Reads and stores game icon as byte array - 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))) + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") { - // Creates NACP class from the memory stream - Nacp controlData = new Nacp(stream); + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); - // Get the title name, title ID, developer name and version number from the NACP - version = controlData.DisplayVersion; + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); - titleName = controlData.Descriptions[(int)_desiredTitleLanguage].Title; + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); - if (string.IsNullOrWhiteSpace(titleName)) + // Reads and stores game icon as byte array + 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))) { - titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; - } + // Creates NACP class from the memory stream + Nacp controlData = new Nacp(stream); - titleId = controlData.PresenceGroupId.ToString("x16"); + // Get the title name, title ID, developer name and version number from the NACP + version = controlData.DisplayVersion; - 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; + GetNameIdDeveloper(controlData, out titleName, out titleId, out developer); } } + else + { + applicationIcon = _nroIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } } - else + catch { - applicationIcon = _nroIcon; + Logger.PrintError(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + + continue; } } - // If its an NCA or NSO we just set defaults - else if ((Path.GetExtension(applicationPath) == ".nca") || (Path.GetExtension(applicationPath) == ".nso")) + else if (Path.GetExtension(applicationPath).ToLower() == ".nca") { - applicationIcon = Path.GetExtension(applicationPath) == ".nca" ? _ncaIcon : _nsoIcon; + try + { + Nca nca = new Nca(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType != NcaContentType.Program || nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + numApplicationsFound--; + + continue; + } + } + catch (InvalidDataException) + { + Logger.PrintWarning(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); + } + catch + { + Logger.PrintError(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + _loadingError = true; + + continue; + } + + applicationIcon = _ncaIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + // If its an NSO we just set defaults + else if (Path.GetExtension(applicationPath).ToLower() == ".nso") + { + applicationIcon = _nsoIcon; titleName = Path.GetFileNameWithoutExtension(applicationPath); } } @@ -373,12 +363,30 @@ namespace Ryujinx.Ui numApplicationsLoaded++; OnApplicationAdded(new ApplicationAddedEventArgs() - { - AppData = data, + { + AppData = data + }); + + OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs() + { NumAppsFound = numApplicationsFound, NumAppsLoaded = numApplicationsLoaded }); } + + OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs() + { + NumAppsFound = numApplicationsFound, + NumAppsLoaded = numApplicationsLoaded + }); + + if (_loadingError) + { + Gtk.Application.Invoke(delegate + { + GtkDialog.CreateErrorDialog("One or more files encountered were not of a valid type, check logs for more info."); + }); + } } protected static void OnApplicationAdded(ApplicationAddedEventArgs e) @@ -386,6 +394,11 @@ namespace Ryujinx.Ui ApplicationAdded?.Invoke(null, e); } + protected static void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e) + { + ApplicationCountUpdated?.Invoke(null, e); + } + private static byte[] GetResourceBytes(string resourceName) { Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); @@ -433,7 +446,7 @@ namespace Ryujinx.Ui internal static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { string metadataFolder = Path.Combine(_virtualFileSystem.GetBasePath(), "games", titleId, "gui"); - string metadataFile = Path.Combine(metadataFolder, "metadata.json"); + string metadataFile = Path.Combine(metadataFolder, "metadata.json"); IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase }); @@ -497,5 +510,50 @@ namespace Ryujinx.Ui return readableString; } + + private static void GetNameIdDeveloper(Nacp controlData, out string titleName, out string titleId, out string developer) + { + Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); + + NacpDescription nacpDescription = controlData.Descriptions.ToList().Find(x => x.Language == desiredTitleLanguage); + + if (nacpDescription != null) + { + titleName = nacpDescription.Title; + developer = nacpDescription.Developer; + } + else + { + titleName = null; + developer = null; + } + + if (string.IsNullOrWhiteSpace(titleName)) + { + titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; + } + + if (string.IsNullOrWhiteSpace(developer)) + { + developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer; + } + + if (controlData.PresenceGroupId != 0) + { + titleId = controlData.PresenceGroupId.ToString("x16"); + } + else if (controlData.SaveDataOwnerId != 0) + { + titleId = controlData.SaveDataOwnerId.ToString("x16"); + } + else if (controlData.AddOnContentBaseId != 0) + { + titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); + } + else + { + titleId = "0000000000000000"; + } + } } } diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 02fd801b6..61c59d356 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -72,7 +72,8 @@ namespace Ryujinx.Ui DeleteEvent += Window_Close; - ApplicationLibrary.ApplicationAdded += Application_Added; + ApplicationLibrary.ApplicationAdded += Application_Added; + ApplicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated; _gameTable.ButtonReleaseEvent += Row_Clicked; @@ -135,9 +136,7 @@ namespace Ryujinx.Ui _tableStore.SetSortColumnId(0, SortType.Descending); UpdateColumns(); -#pragma warning disable CS4014 UpdateGameTable(); -#pragma warning restore CS4014 Task.Run(RefreshFirmwareLabel); } @@ -209,7 +208,7 @@ namespace Ryujinx.Ui return instance; } - internal static async Task UpdateGameTable() + internal static void UpdateGameTable() { if (_updatingGameTable) { @@ -220,10 +219,16 @@ namespace Ryujinx.Ui _tableStore.Clear(); - await Task.Run(() => ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, - _virtualFileSystem, ConfigurationState.Instance.System.Language)); + Thread applicationLibraryThread = new Thread(() => + { + ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, + _virtualFileSystem, ConfigurationState.Instance.System.Language); - _updatingGameTable = false; + _updatingGameTable = false; + }); + applicationLibraryThread.Name = "GUI.ApplicationLibraryThread"; + applicationLibraryThread.IsBackground = true; + applicationLibraryThread.Start(); } internal void LoadApplication(string path) @@ -423,9 +428,22 @@ namespace Ryujinx.Ui args.AppData.FileExtension, args.AppData.FileSize, args.AppData.Path); + }); + } + private void ApplicationCount_Updated(object sender, ApplicationCountUpdatedEventArgs args) + { + Application.Invoke(delegate + { _progressLabel.Text = $"{args.NumAppsLoaded}/{args.NumAppsFound} Games Loaded"; - _progressBar.Value = (float)args.NumAppsLoaded / args.NumAppsFound; + float barValue = 0; + + if (args.NumAppsFound != 0) + { + barValue = (float)args.NumAppsLoaded / args.NumAppsFound; + } + + _progressBar.Value = barValue; }); } @@ -838,9 +856,7 @@ namespace Ryujinx.Ui private void RefreshList_Pressed(object sender, ButtonReleaseEventArgs args) { -#pragma warning disable CS4014 UpdateGameTable(); -#pragma warning restore CS4014 } private static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b) diff --git a/Ryujinx/Ui/SwitchSettings.cs b/Ryujinx/Ui/SwitchSettings.cs index 8bd164d81..72ac49604 100644 --- a/Ryujinx/Ui/SwitchSettings.cs +++ b/Ryujinx/Ui/SwitchSettings.cs @@ -438,9 +438,7 @@ namespace Ryujinx.Ui MainWindow.SaveConfig(); MainWindow.ApplyTheme(); -#pragma warning disable CS4014 MainWindow.UpdateGameTable(); -#pragma warning restore CS4014 Dispose(); }