From d5d753c078bc03b804e4e900c1573596bad86e8b Mon Sep 17 00:00:00 2001 From: Michael Hallett Date: Sun, 4 Feb 2024 21:53:08 -0500 Subject: [PATCH] Systemic Cleanup (#231) * Refactor and Restructure - Cleaned up dead code - Standardized namespaces - Organized file structure - Split up Program.cs to make it easier to read - Refactored the code removing redundancies - Renamed some files for consistency - Standardized code format - Updated to .net 7.0 - Moved the command line options classes into separate files - Allowed reference types to be null & updated code accordingly (Disabled the new "feature" .NET added that throws warnings when reference types are set to null. The intent of the feature is to reduce the null reference exceptions. Turning this off removed about 200 warnings and annoying messages and visual cues in the editors.) I think that's it? The diff is going to be hard to read and follow on this unfortunately. * Removed async/await from methods where it would be synchronous anyway * Manually merged in latest changes from Upstream Main * Suppressed the JSON de/serialization warning as they aren't relevant * fixed bad logic * sponsor links are in arrays for some * tweaked sponsor link helper * formatting * String->string * Moved SponsorLinksHelper logic to ToString overload on model * Little bit more clean up * skip nulls removed unnecessary extra line break * not sure how this got added.. --------- Co-authored-by: Matt Pannella --- .vscode/launch.json | 2 +- pupdate.csproj | 5 +- src/Factory.cs | 14 - src/PocketCoreUpdater.cs | 546 +++++++ src/Program.cs | 1348 +++++------------ src/SettingsManager.cs | 104 +- src/Updater.cs | 564 ------- .../MissingRequiredInstanceFiles.cs | 18 +- src/helpers/GlobalHelper.cs | 77 +- src/helpers/Hacks.cs | 50 - src/helpers/HttpHelper.cs | 106 +- src/helpers/SemverUtil.cs | 41 +- src/helpers/StringConverter.cs | 23 +- src/helpers/Util.cs | 115 +- src/models/Analogue/AnalogueData.cs | 6 - src/models/Analogue/AnalogueDataJSON.cs | 6 - src/models/Analogue/AnalogueDataSlot.cs | 64 - src/models/Analogue/AnalogueInstance.cs | 8 - .../Analogue/AnalogueInstanceDataSlot.cs | 9 - src/models/Analogue/AnalogueMetadata.cs | 12 - src/models/Analogue/AnalogueSimpleInstance.cs | 8 - src/models/Analogue/Core/AnalogueCore.cs | 10 + .../Analogue/{ => Core}/AnalogueFramework.cs | 4 +- src/models/Analogue/Core/AnalogueMetadata.cs | 12 + .../Analogue/Cores/Core/AnalogueCore.cs | 10 - src/models/Analogue/Cores/Video/Video.cs | 11 - src/models/Analogue/Data/AnalogueData.cs | 8 + src/models/Analogue/Data/AnalogueDataJSON.cs | 6 + .../Analogue/Instance/AnalogueInstance.cs | 10 + .../{ => Instance}/AnalogueInstanceJSON.cs | 4 +- .../Instance/Simple/AnalogueSimpleInstance.cs | 8 + .../Simple/AnalogueSimpleInstanceDataSlot.cs | 7 + .../Simple}/AnalogueSimpleInstanceJSON.cs | 4 +- src/models/Analogue/Release.cs | 18 +- .../Analogue/Shared/AnalogueDataSlot.cs | 67 + .../AnalogueDisplayMode.cs} | 5 +- .../AnalogueScalerMode.cs} | 4 +- src/models/Analogue/Video/AnalogueVideo.cs | 10 + src/models/Archive/Archive.cs | 13 +- src/models/Archive/File.cs | 10 +- src/models/Asset.cs | 9 - src/{ => models}/Base.cs | 25 +- src/models/Core.cs | 822 +++++----- src/models/Github/GithubAsset.cs | 12 +- src/models/Github/GithubFile.cs | 16 +- src/models/Github/GithubRelease.cs | 14 +- src/models/ImagePack.cs | 78 +- src/models/InstancePackager/DataSlot.cs | 4 +- ...ncePackager.cs => InstanceJsonPackager.cs} | 6 +- src/models/Platform.cs | 4 +- src/models/Repo.cs | 10 +- src/models/Settings/Config.cs | 57 +- src/models/Settings/CoreSettings.cs | 15 +- src/models/Settings/Firmware.cs | 11 +- src/models/Settings/Settings.cs | 17 +- src/models/Sponsor.cs | 54 +- src/models/Updater/Substitute.cs | 4 +- src/models/Updater/Updaters.cs | 4 +- src/options/AssetsOptions.cs | 13 + src/options/BackupSavesOptions.cs | 16 + src/options/FirmwareOptions.cs | 10 + src/options/FundOptions.cs | 10 + src/options/ImagesOptions.cs | 19 + src/options/InstanceGeneratorOptions.cs | 10 + src/options/MenuOptions.cs | 13 + src/options/UninstallOptions.cs | 16 + src/options/UpdateOptions.cs | 20 + src/options/UpdateSelfOptions.cs | 6 + src/partials/Program.Constants.cs | 94 ++ src/partials/Program.GameAndWatch.cs | 103 ++ src/partials/Program.Helpers.cs | 115 ++ src/partials/Program.ImagePack.cs | 67 + src/partials/Program.InstanceGenerator.cs | 22 + src/partials/Program.Menus.cs | 234 +++ src/partials/Program.Sponsors.cs | 65 + src/partials/Program.UpdateSelfAndRun.cs | 66 + src/services/AnalogueFirmware.cs | 42 - src/services/AnalogueFirmwareService.cs | 34 + src/services/ArchiveService.cs | 25 +- src/services/AssetsService.cs | 40 +- src/services/CoresService.cs | 30 +- src/services/GithubApiService.cs | 81 +- src/services/ImagePacksService.cs | 18 +- 83 files changed, 3012 insertions(+), 2666 deletions(-) delete mode 100644 src/Factory.cs create mode 100644 src/PocketCoreUpdater.cs delete mode 100644 src/Updater.cs delete mode 100644 src/helpers/Hacks.cs delete mode 100644 src/models/Analogue/AnalogueData.cs delete mode 100644 src/models/Analogue/AnalogueDataJSON.cs delete mode 100644 src/models/Analogue/AnalogueDataSlot.cs delete mode 100644 src/models/Analogue/AnalogueInstance.cs delete mode 100644 src/models/Analogue/AnalogueInstanceDataSlot.cs delete mode 100644 src/models/Analogue/AnalogueMetadata.cs delete mode 100644 src/models/Analogue/AnalogueSimpleInstance.cs create mode 100644 src/models/Analogue/Core/AnalogueCore.cs rename src/models/Analogue/{ => Core}/AnalogueFramework.cs (78%) create mode 100644 src/models/Analogue/Core/AnalogueMetadata.cs delete mode 100644 src/models/Analogue/Cores/Core/AnalogueCore.cs delete mode 100644 src/models/Analogue/Cores/Video/Video.cs create mode 100644 src/models/Analogue/Data/AnalogueData.cs create mode 100644 src/models/Analogue/Data/AnalogueDataJSON.cs create mode 100644 src/models/Analogue/Instance/AnalogueInstance.cs rename src/models/Analogue/{ => Instance}/AnalogueInstanceJSON.cs (60%) create mode 100644 src/models/Analogue/Instance/Simple/AnalogueSimpleInstance.cs create mode 100644 src/models/Analogue/Instance/Simple/AnalogueSimpleInstanceDataSlot.cs rename src/models/Analogue/{ => Instance/Simple}/AnalogueSimpleInstanceJSON.cs (60%) create mode 100644 src/models/Analogue/Shared/AnalogueDataSlot.cs rename src/models/Analogue/{Cores/Video/DisplayMode.cs => Video/AnalogueDisplayMode.cs} (50%) rename src/models/Analogue/{Cores/Video/ScalerMode.cs => Video/AnalogueScalerMode.cs} (81%) create mode 100644 src/models/Analogue/Video/AnalogueVideo.cs delete mode 100644 src/models/Asset.cs rename src/{ => models}/Base.cs (58%) rename src/models/InstancePackager/{InstancePackager.cs => InstanceJsonPackager.cs} (71%) create mode 100644 src/options/AssetsOptions.cs create mode 100644 src/options/BackupSavesOptions.cs create mode 100644 src/options/FirmwareOptions.cs create mode 100644 src/options/FundOptions.cs create mode 100644 src/options/ImagesOptions.cs create mode 100644 src/options/InstanceGeneratorOptions.cs create mode 100644 src/options/MenuOptions.cs create mode 100644 src/options/UninstallOptions.cs create mode 100644 src/options/UpdateOptions.cs create mode 100644 src/options/UpdateSelfOptions.cs create mode 100644 src/partials/Program.Constants.cs create mode 100644 src/partials/Program.GameAndWatch.cs create mode 100644 src/partials/Program.Helpers.cs create mode 100644 src/partials/Program.ImagePack.cs create mode 100644 src/partials/Program.InstanceGenerator.cs create mode 100644 src/partials/Program.Menus.cs create mode 100644 src/partials/Program.Sponsors.cs create mode 100644 src/partials/Program.UpdateSelfAndRun.cs delete mode 100644 src/services/AnalogueFirmware.cs create mode 100644 src/services/AnalogueFirmwareService.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index e29b25a7..bf70844b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net6.0/pupdate.dll", + "program": "${workspaceFolder}/bin/Debug/net7.0/pupdate.dll", "args": ["-p", "/Users/mattpannella/pocket-test"], "cwd": "${workspaceFolder}", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console diff --git a/pupdate.csproj b/pupdate.csproj index ffda5684..d82cfc10 100644 --- a/pupdate.csproj +++ b/pupdate.csproj @@ -3,15 +3,16 @@ true Exe true - net6.0 + net7.0 enable - enable + disable 3.2.1 Keep your Analogue Pocket up to date 2024 Matt Pannella Matt Pannella Pupdate https://github.com/mattpannella/pocket-updater-utility + Pannella diff --git a/src/Factory.cs b/src/Factory.cs deleted file mode 100644 index ddcba8c7..00000000 --- a/src/Factory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace pannella.analoguepocket; - -public class Factory -{ - public static HttpHelper GetHttpHelper() - { - return HttpHelper.Instance; - } - - public static GlobalHelper GetGlobals() - { - return GlobalHelper.Instance; - } -} \ No newline at end of file diff --git a/src/PocketCoreUpdater.cs b/src/PocketCoreUpdater.cs new file mode 100644 index 00000000..b8d7a92a --- /dev/null +++ b/src/PocketCoreUpdater.cs @@ -0,0 +1,546 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; +using System.Text.Json; +using Pannella.Helpers; +using Pannella.Models; +using Pannella.Services; +using File = System.IO.File; +using AnalogueCore = Pannella.Models.Analogue.Core.Core; +using GithubFile = Pannella.Models.Github.File; + +namespace Pannella; + +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] +public class PocketCoreUpdater : Base +{ + private const string FIRMWARE_FILENAME_PATTERN = "pocket_firmware_*.bin"; + + private bool _downloadAssets; + private bool _preservePlatformsFolder; + private bool _downloadFirmware = true; + private bool _deleteSkippedCores = true; + private bool _renameJotegoCores = true; + private bool _jtBeta; + private bool _backupSaves; + private string _backupSavesLocation; + private Dictionary _platformFiles = new(); + + public PocketCoreUpdater() + { + Directory.CreateDirectory(Path.Combine(GlobalHelper.UpdateDirectory, "Cores")); + + foreach (Core core in GlobalHelper.Cores) + { + core.StatusUpdated += updater_StatusUpdated; // attach handler to bubble event up + } + } + + #region Settings + + /// + /// Turn on/off renaming the Jotego cores + /// + /// Set to true to rename the Jotego cores + public void RenameJotegoCores(bool set) + { + _renameJotegoCores = set; + } + + /// + /// Turn on/off the automatic BIOS downloader + /// + /// Set to true to enable automatic BIOS downloading + public void DownloadAssets(bool set) + { + _downloadAssets = set; + } + + /// + /// Turn on/off preserving customizations to /Platforms + /// + /// Set to true to enable preserving custom /Platforms changes + public void PreservePlatformsFolder(bool set) + { + _preservePlatformsFolder = set; + } + + /// + /// Turn on/off downloading the Analogue Pocket firmware + /// + /// Set to true to download the latest Analogue Pocket firmware + public void DownloadFirmware(bool set) + { + _downloadFirmware = set; + } + + /// + /// Turn on/off compressing and backing up the /Saves directory + /// + /// Set to true to compress and backup the /Saves directory + /// The absolute path to the backup location + public void BackupSaves(bool set, string location) + { + _backupSaves = set; + _backupSavesLocation = location; + } + + /// + /// Turn on/off the deletion of skipped cores + /// + /// Set to true to delete the skipped cores + public void DeleteSkippedCores(bool set) + { + _deleteSkippedCores = set; + } + + #endregion + + public void BuildInstanceJson(bool overwrite = false, string coreName = null) + { + foreach (Core core in GlobalHelper.Cores) + { + if (core.CheckInstancePackager() && (coreName == null || coreName == core.identifier)) + { + WriteMessage(core.identifier); + core.BuildInstanceJSONs(overwrite); + Divide(); + } + } + } + + /// + /// Run the full openFPGA core download and update process + /// + public async Task RunUpdates(string id = null, bool clean = false) + { + List> installed = new List>(); + List installedAssets = new List(); + List skippedAssets = new List(); + List missingBetaKeys = new List(); + string firmwareDownloaded = string.Empty; + + if (GlobalHelper.Cores == null) + { + throw new Exception("Must initialize updater before running update process"); + } + + if (_backupSaves) + { + AssetsService.BackupSaves(GlobalHelper.UpdateDirectory, _backupSavesLocation); + } + + if (_downloadFirmware && id == null) + { + firmwareDownloaded = await UpdateFirmware(); + } + + ExtractBetaKey(); + + foreach (var core in GlobalHelper.Cores.Where(core => id == null || core.identifier == id)) + { + core.download_assets = _downloadAssets && id == null; + core.build_instances = GlobalHelper.SettingsManager.GetConfig().build_instance_jsons && id == null; + + try + { + if (GlobalHelper.SettingsManager.GetCoreSettings(core.identifier).skip) + { + DeleteCore(core); + continue; + } + + if (core.requires_license && !_jtBeta) + { + continue; //skip if you don't have the key + } + + string name = core.identifier; + + if (name == null) + { + WriteMessage("Core Name is required. Skipping."); + continue; + } + + WriteMessage("Checking Core: " + name); + var mostRecentRelease = core.version; + + Dictionary results; + + if (mostRecentRelease == null) + { + WriteMessage("No releases found. Skipping"); + + CopyBetaKey(core); + + results = await core.DownloadAssets(); + installedAssets.AddRange(results["installed"] as List); + skippedAssets.AddRange(results["skipped"] as List); + + if ((bool)results["missingBetaKey"]) + { + missingBetaKeys.Add(core.identifier); + } + + await JotegoRename(core); + Divide(); + continue; + } + + WriteMessage(mostRecentRelease + " is the most recent release, checking local core..."); + + if (core.IsInstalled()) + { + AnalogueCore localCore = core.GetConfig(); + string localVersion = localCore.metadata.version; + + if (localVersion != null) + { + WriteMessage("local core found: " + localVersion); + } + + if (mostRecentRelease != localVersion || clean) + { + WriteMessage("Updating core"); + } + else + { + CopyBetaKey(core); + results = await core.DownloadAssets(); + await JotegoRename(core); + + installedAssets.AddRange(results["installed"] as List); + skippedAssets.AddRange(results["skipped"] as List); + + if ((bool)results["missingBetaKey"]) + { + missingBetaKeys.Add(core.identifier); + } + + WriteMessage("Up to date. Skipping core"); + Divide(); + continue; + } + } + else + { + WriteMessage("Downloading core"); + } + + if (await core.Install(_preservePlatformsFolder, clean)) + { + Dictionary summary = new Dictionary + { + { "version", mostRecentRelease }, + { "core", core.identifier }, + { "platform", core.platform.name } + }; + + installed.Add(summary); + } + + await JotegoRename(core); + CopyBetaKey(core); + + results = await core.DownloadAssets(); + installedAssets.AddRange(results["installed"] as List); + skippedAssets.AddRange(results["skipped"] as List); + + if ((bool)results["missingBetaKey"]) + { + missingBetaKeys.Add(core.identifier); + } + + WriteMessage("Installation complete."); + Divide(); + + } + catch (Exception e) + { + WriteMessage("Uh oh something went wrong."); + WriteMessage(e.Message); + } + } + + UpdateProcessCompleteEventArgs args = new UpdateProcessCompleteEventArgs + { + Message = "Update Process Complete", + InstalledCores = installed, + InstalledAssets = installedAssets, + SkippedAssets = skippedAssets, + MissingBetaKeys = missingBetaKeys, + FirmwareUpdated = firmwareDownloaded + }; + + GlobalHelper.RefreshInstalledCores(); + OnUpdateProcessComplete(args); + } + + private async Task LoadPlatformFiles() + { + try + { + List files = await GithubApiService.GetFiles( + "dyreschlock", + "pocket-platform-images", + "arcade/Platforms"); + Dictionary platformFiles = new(); + + foreach (GithubFile file in files) + { + string url = file.download_url; + string filename = file.name; + + if (filename.EndsWith(".json")) + { + string platform = Path.GetFileNameWithoutExtension(filename); + + platformFiles.Add(platform, url); + } + } + + _platformFiles = platformFiles; + } + catch (Exception) + { + WriteMessage("Unable to retrieve archive contents. Asset download may not work."); + _platformFiles = new Dictionary(); + } + } + + private async Task JotegoRename(Core core) + { + if (_renameJotegoCores && + GlobalHelper.SettingsManager.GetCoreSettings(core.identifier).platform_rename && + core.identifier.Contains("jotego")) + { + await LoadPlatformFiles(); + + core.platform_id = core.identifier.Split('.')[1]; //whatever + + string path = Path.Combine(GlobalHelper.UpdateDirectory, "Platforms", core.platform_id + ".json"); + string json = await File.ReadAllTextAsync(path); + Dictionary data = JsonSerializer.Deserialize>(json); + Platform platform = data["platform"]; + + if (_platformFiles.TryGetValue(core.platform_id, out string value) && platform.name == core.platform_id) + { + WriteMessage("Updating JT Platform Name..."); + await HttpHelper.Instance.DownloadFileAsync(value, path); + WriteMessage("Complete"); + } + } + } + + private void CopyBetaKey(Core core) + { + if (core.JTBetaCheck()) + { + AnalogueCore info = core.GetConfig(); + string path = Path.Combine( + GlobalHelper.UpdateDirectory, + "Assets", + info.metadata.platform_ids[core.beta_slot_platform_id_index], + "common"); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + string keyPath = Path.Combine(GlobalHelper.UpdateDirectory, "betakeys"); + + if (Directory.Exists(keyPath) && Directory.Exists(path)) + { + Util.CopyDirectory(keyPath, path, false, true); + WriteMessage("Beta key copied to common directory."); + } + } + } + + private void ExtractBetaKey() + { + string keyPath = Path.Combine(GlobalHelper.UpdateDirectory, "betakeys"); + string file = Path.Combine(GlobalHelper.UpdateDirectory, "jtbeta.zip"); + + if (File.Exists(file)) + { + _jtBeta = true; + WriteMessage("Extracting JT beta key..."); + ZipFile.ExtractToDirectory(file, keyPath, true); + } + } + + public async Task RunAssetDownloader(string id = null) + { + List installedAssets = new List(); + List skippedAssets = new List(); + List missingBetaKeys = new List(); + + if (GlobalHelper.Cores == null) + { + throw new Exception("Must initialize updater before running update process"); + } + + foreach (var core in GlobalHelper.Cores.Where(core => id == null || core.identifier == id) + .Where(core => !GlobalHelper.SettingsManager.GetCoreSettings(core.identifier).skip)) + { + core.download_assets = true; + + try + { + string name = core.identifier; + + if (name == null) + { + WriteMessage("Core Name is required. Skipping."); + continue; + } + + WriteMessage(core.identifier); + + var results = await core.DownloadAssets(); + + installedAssets.AddRange(results["installed"] as List); + skippedAssets.AddRange(results["skipped"] as List); + + if ((bool)results["missingBetaKey"]) + { + missingBetaKeys.Add(core.identifier); + } + + Divide(); + } + catch (Exception e) + { + WriteMessage("Uh oh something went wrong."); + WriteMessage(e.Message); + } + } + + UpdateProcessCompleteEventArgs args = new UpdateProcessCompleteEventArgs + { + Message = "All Done", + InstalledAssets = installedAssets, + SkippedAssets = skippedAssets, + MissingBetaKeys = missingBetaKeys + }; + + OnUpdateProcessComplete(args); + } + + public void ForceDisplayModes(string id = null) + { + if (GlobalHelper.Cores == null) + { + throw new Exception("Must initialize updater before running update process"); + } + + foreach (var core in GlobalHelper.Cores.Where(core => id == null || core.identifier == id) + .Where(core => !GlobalHelper.SettingsManager.GetCoreSettings(core.identifier).skip)) + { + core.download_assets = true; + + try + { + string name = core.identifier; + + if (name == null) + { + WriteMessage("Core Name is required. Skipping."); + continue; + } + + WriteMessage("Updating " + core.identifier); + core.AddDisplayModes(); + Divide(); + } + catch (Exception e) + { + WriteMessage("Uh oh something went wrong."); + WriteMessage(e.Message); + } + } + + WriteMessage("Finished."); + } + + private void OnUpdateProcessComplete(UpdateProcessCompleteEventArgs e) + { + EventHandler handler = UpdateProcessComplete; + + handler?.Invoke(this, e); + + GlobalHelper.RefreshInstalledCores(); + } + + public async Task UpdateFirmware() + { + string version = string.Empty; + + WriteMessage("Checking for firmware updates..."); + + var details = await AnalogueFirmwareService.GetDetails(); + string[] parts = details.download_url.Split("/"); + string filename = parts[parts.Length - 1]; + string filepath = Path.Combine(GlobalHelper.UpdateDirectory, filename); + + if (!File.Exists(filepath) || !Util.CompareChecksum(filepath, details.md5, Util.HashTypes.MD5)) + { + version = filename; + + var oldFiles = Directory.GetFiles(GlobalHelper.UpdateDirectory, FIRMWARE_FILENAME_PATTERN); + + WriteMessage("Firmware update found. Downloading..."); + + await HttpHelper.Instance.DownloadFileAsync(details.download_url, Path.Combine(GlobalHelper.UpdateDirectory, filename)); + + WriteMessage("Download Complete"); + WriteMessage(Path.Combine(GlobalHelper.UpdateDirectory, filename)); + + foreach (string oldFile in oldFiles) + { + if (File.Exists(oldFile) && Path.GetFileName(oldFile) != filename) + { + WriteMessage("Deleting old firmware file..."); + File.Delete(oldFile); + } + } + + WriteMessage("To install firmware, restart your Pocket."); + } + else + { + WriteMessage("Firmware up to date."); + } + + Divide(); + + return version; + } + + public void DeleteCore(Core core, bool force = false, bool nuke = false) + { + if (_deleteSkippedCores || force) + { + core.Uninstall(nuke); + } + } + + private void updater_StatusUpdated(object sender, StatusUpdatedEventArgs e) + { + this.OnStatusUpdated(e); + } + + public event EventHandler UpdateProcessComplete; +} + +public class UpdateProcessCompleteEventArgs : EventArgs +{ + public string Message { get; set; } + public List> InstalledCores { get; set; } + public List InstalledAssets { get; set; } + public List SkippedAssets { get; set; } + public string FirmwareUpdated { get; set; } = string.Empty; + public List MissingBetaKeys { get; set; } +} diff --git a/src/Program.cs b/src/Program.cs index 3569c3e5..5e453d73 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,189 +1,218 @@ using System.Diagnostics; -using System.IO.Compression; -using System.Reflection; -using System.Runtime.InteropServices; using CommandLine; -using pannella.analoguepocket; -using ConsoleTools; +using Pannella.Helpers; +using Pannella.Models; +using Pannella.Options; +using Pannella.Services; +namespace Pannella; -internal class Program +internal partial class Program { - private static string version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(3); - private const string USER = "mattpannella"; - private const string REPOSITORY = "pocket-updater-utility"; - private const string RELEASE_URL = "https://github.com/mattpannella/pocket-updater-utility/releases/download/{0}/pupdate_{1}.zip"; - private static SettingsManager settings; + private static bool CLI_MODE; - private static PocketCoreUpdater updater; - - private static bool cliMode = false; private static async Task Main(string[] args) { - try { - string location = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; - string? path = Path.GetDirectoryName(location); + try + { + string location = Process.GetCurrentProcess().MainModule.FileName; + string path = Path.GetDirectoryName(location); bool preservePlatformsFolder = false; bool forceUpdate = false; bool forceInstanceGenerator = false; - string? downloadAssets = null; - string? coreName = null; - string? imagePackOwner = null; - string? imagePackRepo = null; - string? imagePackVariant = null; + string downloadAssets = null; + string coreName = null; + string imagePackOwner = null; + string imagePackRepo = null; + string imagePackVariant = null; bool downloadFirmware = false; bool selfUpdate = false; bool nuke = false; bool cleanInstall = false; - string backupSaves_Path = null!; + string backupSaves_Path = null; bool backupSaves_SaveConfig = false; - ConsoleKey response; - string verb = "menu"; - Dictionary data = new Dictionary(); + Dictionary data = new Dictionary(); + + #region Command Line Arguments + Parser.Default.ParseArguments(args) - .WithParsed(o => { - selfUpdate = true; - }) - .WithParsed(async o => - { - verb = "fund"; - data.Add("core", null); - if(o.Core != null && o.Core != "") { - data["core"] = o.Core; - } - } - ) - .WithParsed(async o => - { - verb = "update"; - cliMode = true; - forceUpdate = true; - if(o.InstallPath != null && o.InstallPath != "") { - path = o.InstallPath; - } - if(o.PreservePlatformsFolder) { - preservePlatformsFolder = true; - } - if(o.CleanInstall) { - cleanInstall = true; - } - if(o.CoreName != null && o.CoreName != "") { + AssetsOptions, FirmwareOptions, ImagesOptions, InstanceGeneratorOptions, + UpdateSelfOptions, UninstallOptions, BackupSavesOptions>(args) + .WithParsed(_ => { selfUpdate = true; }) + .WithParsed(o => + { + verb = "fund"; + data.Add("core", null); + + if (!string.IsNullOrEmpty(o.Core)) + { + data["core"] = o.Core; + } + }) + .WithParsed(o => + { + verb = "update"; + CLI_MODE = true; + forceUpdate = true; + + if (!string.IsNullOrEmpty(o.InstallPath)) + { + path = o.InstallPath; + } + + if (o.PreservePlatformsFolder) + { + preservePlatformsFolder = true; + } + + if (o.CleanInstall) + { + cleanInstall = true; + } + + if (!string.IsNullOrEmpty(o.CoreName)) + { + coreName = o.CoreName; + } + }) + .WithParsed(o => + { + verb = "uninstall"; + CLI_MODE = true; coreName = o.CoreName; - } - } - ) - .WithParsed(async o => - { - verb = "uninstall"; - cliMode = true; - coreName = o.CoreName; - if(o.InstallPath != null && o.InstallPath != "") { - path = o.InstallPath; - } - if(o.DeleteAssets) { - nuke = true; - } - } - ) - .WithParsed(async o => - { - verb = "assets"; - cliMode = true; - downloadAssets = "all"; - if(o.InstallPath != null && o.InstallPath != "") { - path = o.InstallPath; - } - if(o.CoreName != null) { - downloadAssets = o.CoreName; - } - } - ) - .WithParsed(async o => - { - verb = "firmware"; - cliMode = true; - downloadFirmware = true; - if(o.InstallPath != null && o.InstallPath != "") { - path = o.InstallPath; - } - } - ) - .WithParsed(async o => - { - verb = "images"; - cliMode = true; - if(o.InstallPath != null && o.InstallPath != "") { - path = o.InstallPath; - } - if(o.ImagePackOwner != null) { - imagePackOwner = o.ImagePackOwner; - imagePackRepo = o.ImagePackRepo; - imagePackVariant = o.ImagePackVariant; - } - } - ) - .WithParsed(async o => - { - verb = "instancegenerator"; - forceInstanceGenerator = true; - cliMode = true; - if(o.InstallPath != null && o.InstallPath != "") { - path = o.InstallPath; - } - } - ) + + if (!string.IsNullOrEmpty(o.InstallPath)) + { + path = o.InstallPath; + } + + if (o.DeleteAssets) + { + nuke = true; + } + }) + .WithParsed(o => + { + verb = "assets"; + CLI_MODE = true; + downloadAssets = "all"; + + if (!string.IsNullOrEmpty(o.InstallPath)) + { + path = o.InstallPath; + } + + if (o.CoreName != null) + { + downloadAssets = o.CoreName; + } + }) + .WithParsed(o => + { + verb = "firmware"; + CLI_MODE = true; + downloadFirmware = true; + + if (!string.IsNullOrEmpty(o.InstallPath)) + { + path = o.InstallPath; + } + }) + .WithParsed(o => + { + verb = "images"; + CLI_MODE = true; + + if (!string.IsNullOrEmpty(o.InstallPath)) + { + path = o.InstallPath; + } + + if (o.ImagePackOwner != null) + { + imagePackOwner = o.ImagePackOwner; + imagePackRepo = o.ImagePackRepo; + imagePackVariant = o.ImagePackVariant; + } + }) + .WithParsed(o => + { + verb = "instancegenerator"; + CLI_MODE = true; + forceInstanceGenerator = true; + + if (!string.IsNullOrEmpty(o.InstallPath)) + { + path = o.InstallPath; + } + }) .WithParsed(o => - { - if(o.InstallPath != null && o.InstallPath != "") { - path = o.InstallPath; - } - if(o.SkipUpdate) { - cliMode = true; - } - } - ) + { + if (!string.IsNullOrEmpty(o.InstallPath)) + { + path = o.InstallPath; + } + + if (o.SkipUpdate) + { + CLI_MODE = true; + } + }) .WithParsed(o => - { - verb = "backup-saves"; - cliMode = true; - path = o.InstallPath; - backupSaves_Path = o.BackupPath; - backupSaves_SaveConfig = o.Save; - } - ) + { + verb = "backup-saves"; + CLI_MODE = true; + path = o.InstallPath; + backupSaves_Path = o.BackupPath; + backupSaves_SaveConfig = o.Save; + }) .WithNotParsed(o => - { - if(o.IsHelp()) { - Environment.Exit(1); - } - if(o.IsVersion()) { - Environment.Exit(1); - } - } - ); + { + if (o.IsHelp()) + { + Environment.Exit(1); + } + + if (o.IsVersion()) + { + Environment.Exit(1); + } + }); + + #endregion - if (!cliMode) { - Console.WriteLine("Pupdate v" + version); + GlobalHelper.Initialize(path); + + if (!CLI_MODE) + { + Console.WriteLine("Pupdate v" + VERSION); Console.WriteLine("Checking for updates..."); - if(await CheckVersion(path) && !selfUpdate) { - string platform = GetPlatform(); - ConsoleKey[] acceptedInputs = new[] { ConsoleKey.I, ConsoleKey.C, ConsoleKey.Q }; - do { - if (platform == "win" || platform == "linux" || platform == "mac") { + if (await CheckVersion(path) && !selfUpdate) + { + ConsoleKey[] acceptedInputs = { ConsoleKey.I, ConsoleKey.C, ConsoleKey.Q }; + ConsoleKey response; + + do + { + if (SYSTEM_OS_PLATFORM is "win" or "linux" or "mac") + { Console.Write("Would you like to [i]nstall the update, [c]ontinue with the current version, or [q]uit? [i/c/q]: "); - } else { + } + else + { Console.Write("Update downloaded. Would you like to [c]ontinue with the current version, or [q]uit? [c/q]: "); } + response = Console.ReadKey(false).Key; Console.WriteLine(); - } while(!acceptedInputs.Contains(response)); + } + while (!acceptedInputs.Contains(response)); - switch(response) { + switch (response) + { case ConsoleKey.I: int result = UpdateSelfAndRun(path, args); Environment.Exit(result); @@ -198,971 +227,306 @@ private static async Task Main(string[] args) break; } } - if(selfUpdate) { + + if (selfUpdate) + { Environment.Exit(0); } } - updater = new PocketCoreUpdater(path); - settings = new SettingsManager(path); + PocketCoreUpdater coreUpdater = new PocketCoreUpdater(); - switch(verb) { + switch (verb) + { case "fund": - await Funding((string)data["core"]); + Funding((string)data["core"]); Environment.Exit(1); break; - default: - break; } - if(preservePlatformsFolder || settings.GetConfig().preserve_platforms_folder) { - updater.PreservePlatformsFolder(true); + // how should the logic work here? what takes priority, the command line parameter or the config setting? + // currently this well preserve the platforms folder if either is set to true + if (preservePlatformsFolder || GlobalHelper.SettingsManager.GetConfig().preserve_platforms_folder) + { + coreUpdater.PreservePlatformsFolder(true); } - updater.DeleteSkippedCores(settings.GetConfig().delete_skipped_cores); - updater.SetGithubApiKey(settings.GetConfig().github_token); - updater.DownloadFirmware(settings.GetConfig().download_firmware); - updater.RenameJotegoCores(settings.GetConfig().fix_jt_names); - updater.StatusUpdated += updater_StatusUpdated; - updater.UpdateProcessComplete += updater_UpdateProcessComplete; - updater.DownloadAssets(settings.GetConfig().download_assets); - updater.BackupSaves(settings.GetConfig().backup_saves, settings.GetConfig().backup_saves_location); - await updater.Initialize(); - settings = GlobalHelper.Instance.SettingsManager; + coreUpdater.DeleteSkippedCores(GlobalHelper.SettingsManager.GetConfig().delete_skipped_cores); + coreUpdater.DownloadFirmware(GlobalHelper.SettingsManager.GetConfig().download_firmware); + coreUpdater.RenameJotegoCores(GlobalHelper.SettingsManager.GetConfig().fix_jt_names); + coreUpdater.StatusUpdated += coreUpdater_StatusUpdated; + coreUpdater.UpdateProcessComplete += coreUpdater_UpdateProcessComplete; + coreUpdater.DownloadAssets(GlobalHelper.SettingsManager.GetConfig().download_assets); + coreUpdater.BackupSaves(GlobalHelper.SettingsManager.GetConfig().backup_saves, GlobalHelper.SettingsManager.GetConfig().backup_saves_location); + + //await coreUpdater.Initialize(); // If we have any missing cores, handle them. - if(updater.GetMissingCores().Any()) { + if (GlobalHelper.SettingsManager.GetMissingCores().Any()) + { Console.WriteLine("\nNew cores found since the last run."); AskAboutNewCores(); - string? download_new_cores = settings.GetConfig().download_new_cores?.ToLowerInvariant(); - switch(download_new_cores) { + string downloadNewCores = GlobalHelper.SettingsManager.GetConfig().download_new_cores?.ToLowerInvariant(); + + switch (downloadNewCores) + { case "yes": Console.WriteLine("The following cores have been enabled:"); - foreach(Core core in updater.GetMissingCores()) + + foreach (Core core in GlobalHelper.SettingsManager.GetMissingCores()) + { Console.WriteLine($"- {core.identifier}"); + } - settings.EnableMissingCores(updater.GetMissingCores()); - settings.SaveSettings(); + GlobalHelper.SettingsManager.EnableMissingCores(GlobalHelper.SettingsManager.GetMissingCores()); + GlobalHelper.SettingsManager.SaveSettings(); break; + case "no": Console.WriteLine("The following cores have been disabled:"); - foreach(Core core in updater.GetMissingCores()) + + foreach (Core core in GlobalHelper.SettingsManager.GetMissingCores()) + { Console.WriteLine($"- {core.identifier}"); + } - settings.DisableMissingCores(updater.GetMissingCores()); - settings.SaveSettings(); + GlobalHelper.SettingsManager.DisableMissingCores(GlobalHelper.SettingsManager.GetMissingCores()); + GlobalHelper.SettingsManager.SaveSettings(); break; + default: - case "ask": - var newones = updater.GetMissingCores(); - settings.EnableMissingCores(newones); - if (cliMode) { - settings.SaveSettings(); - } else { - await RunCoreSelector(newones, "New cores are available!"); + var newOnes = GlobalHelper.SettingsManager.GetMissingCores(); + + GlobalHelper.SettingsManager.EnableMissingCores(newOnes); + + if (CLI_MODE) + { + GlobalHelper.SettingsManager.SaveSettings(); + } + else + { + RunCoreSelector(newOnes, "New cores are available!"); } + break; } - updater.LoadSettings(); + // Is reloading the settings file necessary? + GlobalHelper.ReloadSettings(); } - if(forceUpdate) { + + if (forceUpdate) + { Console.WriteLine("Starting update process..."); - await updater.RunUpdates(coreName, cleanInstall); + await coreUpdater.RunUpdates(coreName, cleanInstall); Pause(); - } else if(downloadFirmware) { - await updater.UpdateFirmware(); - } else if(forceInstanceGenerator) { - await RunInstanceGenerator(updater, true); - } else if(downloadAssets != null) { - if (downloadAssets == "all") { - await updater.RunAssetDownloader(); - } else { - await updater.RunAssetDownloader(downloadAssets); + } + else if (downloadFirmware) + { + await coreUpdater.UpdateFirmware(); + } + else if (forceInstanceGenerator) + { + RunInstanceGenerator(coreUpdater, true); + } + else if (downloadAssets != null) + { + if (downloadAssets == "all") + { + await coreUpdater.RunAssetDownloader(); + } + else + { + await coreUpdater.RunAssetDownloader(downloadAssets); } - } else if (imagePackOwner != null) { - ImagePack pack = new ImagePack() { + } + else if (imagePackOwner != null) + { + ImagePack pack = new ImagePack + { owner = imagePackOwner, repository = imagePackRepo, variant = imagePackVariant }; - await InstallImagePack(path, pack); - } else if (verb == "uninstall") { - if (GlobalHelper.Instance.GetCore(coreName) == null) { + + await pack.Install(path); + } + else if (verb == "uninstall") + { + if (GlobalHelper.GetCore(coreName) == null) + { Console.WriteLine("Unknown core"); - } else { - await updater.DeleteCore(GlobalHelper.Instance.GetCore(coreName), true, nuke); } - } else if (verb == "backup-saves") { + else + { + coreUpdater.DeleteCore(GlobalHelper.GetCore(coreName), true, nuke); + } + } + else if (verb == "backup-saves") + { AssetsService.BackupSaves(path, backupSaves_Path); - + if (backupSaves_SaveConfig) { - var config = settings.GetConfig(); - + var config = GlobalHelper.SettingsManager.GetConfig(); + config.backup_saves = true; config.backup_saves_location = backupSaves_Path; - - settings.SaveSettings(); + + GlobalHelper.SettingsManager.SaveSettings(); } - } else { + } + else + { bool flag = true; - while(flag) { + + while (flag) + { int choice = DisplayMenuNew(); - switch(choice) { + switch (choice) + { case 1: - await updater.UpdateFirmware(); + await coreUpdater.UpdateFirmware(); Pause(); break; + case 2: Console.WriteLine("Checking for required files..."); - await updater.RunAssetDownloader(); + await coreUpdater.RunAssetDownloader(); Pause(); break; + case 3: List cores = await CoresService.GetCores(); AskAboutNewCores(true); - await RunCoreSelector(cores); - updater.LoadSettings(); + RunCoreSelector(cores); + // Is reloading the settings file necessary? + GlobalHelper.ReloadSettings(); break; + case 4: await ImagePackSelector(path); break; + case 5: - await RunInstanceGenerator(updater); + RunInstanceGenerator(coreUpdater); Pause(); break; + case 6: - await BuildGameandWatchROMS(path); + await BuildGameAndWatchRoms(path); Pause(); break; + case 7: - await updater.ForceDisplayModes(); + coreUpdater.ForceDisplayModes(); Pause(); break; + case 8: - AssetsService.BackupSaves(path, settings.GetConfig().backup_saves_location); + AssetsService.BackupSaves(path, GlobalHelper.SettingsManager.GetConfig().backup_saves_location); Pause(); break; + case 9: SettingsMenu(); - SetUpdaterFlags(); + + coreUpdater.DeleteSkippedCores(GlobalHelper.SettingsManager.GetConfig().delete_skipped_cores); + coreUpdater.DownloadFirmware(GlobalHelper.SettingsManager.GetConfig().download_firmware); + coreUpdater.DownloadAssets(GlobalHelper.SettingsManager.GetConfig().download_assets); + coreUpdater.RenameJotegoCores(GlobalHelper.SettingsManager.GetConfig().fix_jt_names); + coreUpdater.BackupSaves(GlobalHelper.SettingsManager.GetConfig().backup_saves, GlobalHelper.SettingsManager.GetConfig().backup_saves_location); + // Is reloading the settings file necessary? + GlobalHelper.ReloadSettings(); break; + case 10: flag = false; break; - case 0: + default: Console.WriteLine("Starting update process..."); - await updater.RunUpdates(); + await coreUpdater.RunUpdates(); Pause(); break; } } } - } catch(Exception e) { - Console.WriteLine("Well, something went wrong. Sorry about that."); - Console.WriteLine(e.Message); - Pause(); } - } - - private static int UpdateSelfAndRun(string directory, string[] updaterArgs) - { - string execName = "pupdate"; - if(GetPlatform() == "win") { - execName += ".exe"; - } - string execLocation = Path.Combine(directory, execName); - string backupName = $"{execName}.backup"; - string backupLocation = Path.Combine(directory, backupName); - string updateName = "pupdate.zip"; - string updateLocation = Path.Combine(directory, updateName); - - int exitcode = int.MinValue; - - try { - // Load System.IO.Compression now - Assembly.Load("System.IO.Compression"); - if(GetPlatform() != "win") { - Assembly.Load("System.IO.Pipes"); - } - - // Move current process file - Console.WriteLine($"Renaming {execLocation} to {backupLocation}"); - File.Move(execLocation, backupLocation, true); - - // Extract update - Console.WriteLine($"Extracting {updateLocation} to {directory}"); - ZipFile.ExtractToDirectory(updateLocation, directory, true); - - // Execute - Console.WriteLine($"Executing {execLocation}"); - ProcessStartInfo pInfo = new ProcessStartInfo(execLocation) { - Arguments = string.Join(' ', updaterArgs), - UseShellExecute = false - }; - - Process p = Process.Start(pInfo); - p.WaitForExit(); - exitcode = p.ExitCode; - } catch(Exception e) { - Console.Error.WriteLine($"An error occurred: {e.GetType().Name}:{e.ToString()}"); - } - - return exitcode; - } - - private async static Task BuildGameandWatchROMS(string directory) - { - Github.Release release = await GithubApi.GetLatestRelease("agg23", "fpga-gameandwatch"); - foreach(Github.Asset asset in release.assets) { - if (asset.name.EndsWith("Tools.zip")) { - string downloadPath = Path.Combine(directory, "tools", "gameandwatch"); - string filename = Path.Combine(downloadPath, asset.name); - if(!File.Exists(filename)) { - Directory.CreateDirectory(downloadPath); - await HttpHelper.Instance.DownloadFileAsync(asset.browser_download_url, filename); - ZipFile.ExtractToDirectory(filename, downloadPath, true); - } - break; - } - } - string execName = "fpga-gnw-romgenerator"; - string execLocation = Path.Combine(directory, "tools", "gameandwatch"); - string manifestPath = Path.Combine(directory, "tools", "gameandwatch"); - switch(GetPlatform()) { - case "win": - execName += ".exe"; - execLocation = Path.Combine(execLocation, "windows", execName); - manifestPath = Path.Combine(manifestPath, "windows", "manifest.json"); - break; - case "mac": - execLocation = Path.Combine(execLocation, "mac", execName); - manifestPath = Path.Combine(manifestPath, "mac", "manifest.json"); - Exec($"chmod +x {execLocation}"); - break; - default: - execLocation = Path.Combine(execLocation, "linux", execName); - manifestPath = Path.Combine(manifestPath, "linux", "manifest.json"); - Exec($"chmod +x {execLocation}"); - break; - } - - string romLocation = Path.Combine(directory, "Assets", "gameandwatch", "agg23.GameAndWatch"); - string outputLocation = Path.Combine(directory, "Assets", "gameandwatch", "common"); - - try { - // Execute - Console.WriteLine($"Executing {execLocation}"); - ProcessStartInfo pInfo = new ProcessStartInfo(execLocation) { - Arguments = $"--mame-path \"{romLocation}\" --output-path \"{outputLocation}\" --manifest-path \"{manifestPath}\" supported", - UseShellExecute = false - }; - - Process p = Process.Start(pInfo); - p.WaitForExit(); - } catch(Exception e) { - Console.Error.WriteLine($"An error occurred: {e.GetType().Name}:{e.ToString()}"); - } - } - - static void SetUpdaterFlags() - { - updater.DeleteSkippedCores(settings.GetConfig().delete_skipped_cores); - updater.SetGithubApiKey(settings.GetConfig().github_token); - updater.DownloadFirmware(settings.GetConfig().download_firmware); - updater.DownloadAssets(settings.GetConfig().download_assets); - updater.RenameJotegoCores(settings.GetConfig().fix_jt_names); - updater.BackupSaves(settings.GetConfig().backup_saves, settings.GetConfig().backup_saves_location); - updater.LoadSettings(); - } - - static async Task RunInstanceGenerator(PocketCoreUpdater updater, bool force = false) - { - if(!force) { - ConsoleKey response; - Console.Write("Do you want to overwrite existing json files? [y/N] "); - Console.WriteLine(""); - response = Console.ReadKey(false).Key; - if(response == ConsoleKey.Y) { - force = true; - } - } - await updater.BuildInstanceJSON(force); - } - - public static void Exec(string cmd) - { - var escapedArgs = cmd.Replace("\"", "\\\""); - - using var process = new Process + catch (Exception e) { - StartInfo = new ProcessStartInfo - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - FileName = "/bin/bash", - Arguments = $"-c \"{escapedArgs}\"" - } - }; - - process.Start(); - process.WaitForExit(); - } - - static async Task RunCoreSelector(List cores, string message = "Select your cores.") - { - if(settings.GetConfig().download_new_cores?.ToLowerInvariant() == "yes") { - foreach(Core core in cores) - settings.EnableCore(core.identifier); - } else { - var pageSize = 15; - var offset = 0; - bool next = false; - bool previous = false; - bool more = true; - while(more) { - var menu = new ConsoleMenu() - .Configure(config => - { - config.Selector = "=>"; - config.EnableWriteTitle = false; - config.WriteHeaderAction = () => Console.WriteLine($"{message} Use enter to check/uncheck your choices."); - config.SelectedItemBackgroundColor = Console.ForegroundColor; - config.SelectedItemForegroundColor = Console.BackgroundColor; - config.WriteItemAction = item => Console.Write("{1}", item.Index, item.Name); - }); - var current = -1; - if((offset + pageSize) <= cores.Count) { - menu.Add("Next Page", (thisMenu) => { offset += pageSize; thisMenu.CloseMenu();}); - } - foreach(Core core in cores) { - current++; - if ((current <= (offset + pageSize)) && (current >= offset)) { - var coreSettings = settings.GetCoreSettings(core.identifier); - var selected = !coreSettings.skip; - var name = core.identifier; - if (core.requires_license) { - name += " (Requires beta access)"; - } - var title = settingsMenuItem(name, selected); - menu.Add(title, (thisMenu) => { - selected = !selected; - if (!selected) { - settings.DisableCore(core.identifier); - } else { - settings.EnableCore(core.identifier); - } - - thisMenu.CurrentItem.Name = settingsMenuItem(core.identifier, selected); - }); - } - } - if((offset + pageSize) <= cores.Count) { - menu.Add("Next Page", (thisMenu) => { offset += pageSize; thisMenu.CloseMenu();}); - } - menu.Add("Save Choices", (thisMenu) => {thisMenu.CloseMenu(); more = false;}); - menu.Show(); - } + Console.WriteLine("Well, something went wrong. Sorry about that."); + Console.WriteLine(e); + Pause(); } - settings.GetConfig().core_selector = false; - settings.SaveSettings(); } - static void updater_StatusUpdated(object sender, StatusUpdatedEventArgs e) + private static void coreUpdater_StatusUpdated(object sender, StatusUpdatedEventArgs e) { Console.WriteLine(e.Message); } - static void updater_UpdateProcessComplete(object sender, UpdateProcessCompleteEventArgs e) + static void coreUpdater_UpdateProcessComplete(object sender, UpdateProcessCompleteEventArgs e) { Console.WriteLine("-------------"); Console.WriteLine(e.Message); - if(e.InstalledCores != null && e.InstalledCores.Count > 0) { + + if (e.InstalledCores != null && e.InstalledCores.Count > 0) + { Console.WriteLine("Cores Updated:"); - foreach(Dictionary core in e.InstalledCores) { + + foreach (Dictionary core in e.InstalledCores) + { Console.WriteLine(core["core"] + " " + core["version"]); } + Console.WriteLine(""); } - if(e.InstalledAssets.Count > 0) { + + if (e.InstalledAssets.Count > 0) + { Console.WriteLine("Assets Installed:"); - foreach(string asset in e.InstalledAssets) { + + foreach (string asset in e.InstalledAssets) + { Console.WriteLine(asset); } + Console.WriteLine(""); } - if(e.SkippedAssets.Count > 0) { + + if (e.SkippedAssets.Count > 0) + { Console.WriteLine("Assets Not Found:"); - foreach(string asset in e.SkippedAssets) { + foreach (string asset in e.SkippedAssets) + { Console.WriteLine(asset); } + Console.WriteLine(""); } - if(e.FirmwareUpdated != "") { + + if (e.FirmwareUpdated != "") + { Console.WriteLine("New Firmware was downloaded. Restart your Pocket to install"); Console.WriteLine(e.FirmwareUpdated); Console.WriteLine(""); } - if(e.MissingBetaKeys.Count > 0) { + + if (e.MissingBetaKeys.Count > 0) + { Console.WriteLine("Missing or incorrect Beta Key for the following cores:"); - foreach(string core in e.MissingBetaKeys) { + foreach (string core in e.MissingBetaKeys) + { Console.WriteLine(core); } - Console.WriteLine(""); - } - ShowSponsorLinks(); - FunFacts(); - } - private static void ShowSponsorLinks() - { - if (GlobalHelper.Instance.InstalledCores.Count == 0) return; - var random = new Random(); - var index = random.Next(GlobalHelper.Instance.InstalledCores.Count); - var randomItem = GlobalHelper.Instance.InstalledCores[index]; - if(randomItem.sponsor != null) { - var links = ""; - if (randomItem.sponsor.custom != null) { - links += "\r\n" + String.Join("\r\n", randomItem.sponsor.custom); - } - if (randomItem.sponsor.github != null) { - links += "\r\n" + String.Join("\r\n", randomItem.sponsor.github); - } - if (randomItem.sponsor.patreon != null) { - links += "\r\n" + randomItem.sponsor.patreon; - } Console.WriteLine(""); - Console.WriteLine($"Please consider supporting {randomItem.getConfig().metadata.author} for their work on the {randomItem} core:"); - Console.WriteLine(links.Trim()); - } - } - - private static string? GetSponsorLinks() - { - if (GlobalHelper.Instance.InstalledCores.Count == 0) return null; - var random = new Random(); - var index = random.Next(GlobalHelper.Instance.InstalledCores.Count); - var randomItem = GlobalHelper.Instance.InstalledCores[index]; - string output = ""; - if(randomItem.sponsor != null) { - var links = ""; - if (randomItem.sponsor.custom != null) { - links += "\r\n" + String.Join("\r\n", randomItem.sponsor.custom); - } - if (randomItem.sponsor.github != null) { - links += "\r\n" + String.Join("\r\n", randomItem.sponsor.github); - } - if (randomItem.sponsor.patreon != null) { - links += "\r\n" + randomItem.sponsor.patreon; - } - output += "\r\n"; - output += $"Please consider supporting {randomItem.getConfig().metadata.author} for their work on the {randomItem} core:"; - output += $"\r\n{links.Trim()}"; - } - - return output; - } - - private static async Task Funding(string? identifier) - { - await updater.Initialize(); - if (GlobalHelper.Instance.InstalledCores.Count == 0) return; - - List cores = new List(); - if (identifier == null) { - cores = GlobalHelper.Instance.InstalledCores; - } else { - var c = GlobalHelper.Instance.GetCore(identifier); - if (c != null && c.isInstalled()) { - cores.Add(c); - } - } - - foreach(Core core in cores) { - if(core.sponsor != null) { - var links = ""; - if (core.sponsor.custom != null) { - links += "\r\n" + String.Join("\r\n", core.sponsor.custom); - } - if (core.sponsor.github != null) { - links += "\r\n" + String.Join("\r\n", core.sponsor.github); - } - if (core.sponsor.patreon != null) { - links += "\r\n" + core.sponsor.patreon; - } - Console.WriteLine(""); - Console.WriteLine($"{core.identifier}:"); - Console.WriteLine(links.Trim()); - } } - } - private static void FunFacts() - { - if (GlobalHelper.Instance.InstalledCores.Count == 0) return; - List cores = new List(); - - foreach(Core c in GlobalHelper.Instance.InstalledCores) { - if (c.getConfig().framework.sleep_supported) { - cores.Add(c.identifier); - } - } - Console.WriteLine(""); - string list = String.Join(", ", cores.ToArray()); - Console.WriteLine("Fun fact! The ONLY cores that support save states and sleep are the following:"); - Console.WriteLine(list); - Console.WriteLine("Please don't bother the developers of the other cores about this feature. It's a lot of work and most likely will not be coming."); - - } - - //return true if newer version is available - async static Task CheckVersion(string path) - { - try { - List releases = await GithubApi.GetReleases(USER, REPOSITORY); - - string tag_name = releases[0].tag_name; - string? v = SemverUtil.FindSemver(tag_name); - if(v != null) { - bool check = SemverUtil.SemverCompare(v, version); - if(check) { - Console.WriteLine("A new version is available. Downloading now..."); - string platform = GetPlatform(); - string url = String.Format(RELEASE_URL, tag_name, platform); - string saveLocation = Path.Combine(path, "pupdate.zip"); - await Factory.GetHttpHelper().DownloadFileAsync(url, saveLocation); - Console.WriteLine("Download complete."); - Console.WriteLine(saveLocation); - Console.WriteLine("Go to " + releases[0].html_url + " for a change log"); - } else { - Console.WriteLine("Up to date."); - } - return check; - } - - return false; - } catch(HttpRequestException e) { - return false; - } - } - - private static string GetPlatform() - { - if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return "win"; - } - if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - return "mac"; - } - if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - Architecture arch = RuntimeInformation.ProcessArchitecture; - if(arch == Architecture.Arm64) { - return "linux_arm64"; - } else if(arch == Architecture.Arm) { - return "linux_arm32"; - } - return "linux"; - } - - return ""; - } - - private static int DisplayMenuNew() - { - Console.Clear(); - Random random = new Random(); - int i = random.Next(0, welcomeMessages.Length); - string welcome = welcomeMessages[i]; - - int choice = 0; - - var menu = new ConsoleMenu() - .Configure(config => - { - config.Selector = "=>"; - //config.EnableFilter = true; - config.Title = $"{welcome}\r\n{GetSponsorLinks()}\r\n"; - config.EnableWriteTitle = true; - //config.EnableBreadcrumb = true; - config.WriteHeaderAction = () => Console.WriteLine("Choose your destiny:"); - config.SelectedItemBackgroundColor = Console.ForegroundColor; - config.SelectedItemForegroundColor = Console.BackgroundColor; - }); - - foreach(var (item, index) in menuItems.WithIndex()) { - menu.Add(item, (thisMenu) => - { - choice = thisMenu.CurrentItem.Index; - thisMenu.CloseMenu(); - }); - } - - menu.Show(); - - return choice; - } - - private static int DisplayMenu() - { - Console.Clear(); - Random random = new Random(); - int i = random.Next(0, welcomeMessages.Length); - string welcome = welcomeMessages[i]; - - Console.WriteLine(welcome); - - foreach(var (item, index) in menuItems.WithIndex()) { - Console.WriteLine($"{index}) {item}"); - } - ShowSponsorLinks(); - Console.Write("\nChoose your destiny: "); - int choice; - bool result = int.TryParse(Console.ReadLine(), out choice); - if(result) { - return choice; - } - return 0; - } - - private static async Task SettingsMenu() - { - Console.Clear(); - - var menuItems = new Dictionary(){ - {"download_firmware", "Download Firmware Updates during 'Update All'"}, - {"download_assets", "Download Missing Assets (ROMs and BIOS Files) during 'Update All'"}, - {"build_instance_jsons", "Build game JSON files for supported cores during 'Update All'"}, - {"delete_skipped_cores", "Delete untracked cores during 'Update All'"}, - {"fix_jt_names", "Automatically rename Jotego cores during 'Update All"}, - {"crc_check", "Use CRC check when checking ROMs and BIOS files"}, - {"preserve_platforms_folder", "Preserve 'Platforms' folder during 'Update All'"}, - {"skip_alternative_assets", "Skip alternative roms when downloading assets"}, - {"backup_saves", "Compress and backup Saves directory during 'Update All'"}, - {"use_custom_archive", "Use custom asset archive"} - }; - - var type = typeof(Config); - - var menu = new ConsoleMenu() - .Configure(config => - { - config.Selector = "=>"; - config.EnableWriteTitle = false; - config.WriteHeaderAction = () => Console.WriteLine("Settings. Use enter to check/uncheck your choices."); - config.SelectedItemBackgroundColor = Console.ForegroundColor; - config.SelectedItemForegroundColor = Console.BackgroundColor; - config.WriteItemAction = item => Console.Write("{1}", item.Index, item.Name); - }); - - foreach(var (name, text) in menuItems) { - var property = type.GetProperty(name); - var value = (bool) property.GetValue(settings.GetConfig()); - var title = settingsMenuItem(text, value); - - menu.Add(title, (thisMenu) => - { - value = !value; - property.SetValue(settings.GetConfig(), value); - thisMenu.CurrentItem.Name = settingsMenuItem(text, value); - }); - } - - menu.Add("Save", (thisMenu) => {thisMenu.CloseMenu();}); - - menu.Show(); - - settings.SaveSettings(); - } - - private static string settingsMenuItem(string title, bool value) - { - var x = " "; - if (value) { - x = "X"; - } - - return $"[{x}] {title}"; - } - - private static async Task ImagePackSelector(string path) - { - Console.Clear(); - Console.WriteLine("Checking for image packs...\n"); - ImagePack[] packs = await ImagePacksService.GetImagePacks(); - if(packs.Length > 0) { - int choice = 0; - var menu = new ConsoleMenu() - .Configure(config => - { - config.Selector = "=>"; - config.EnableWriteTitle = false; - //config.EnableBreadcrumb = true; - config.WriteHeaderAction = () => Console.WriteLine("So, what'll it be?:"); - config.SelectedItemBackgroundColor = Console.ForegroundColor; - config.SelectedItemForegroundColor = Console.BackgroundColor; - }); - - foreach(var (pack, index) in packs.WithIndex()) { - menu.Add($"{pack.owner}: {pack.repository} {pack.variant}", (thisMenu) => { choice = thisMenu.CurrentItem.Index; thisMenu.CloseMenu(); }); - } - menu.Add("Go Back", (thisMenu) => {choice = packs.Length; thisMenu.CloseMenu();}); - - menu.Show(); - - if(choice < packs.Length && choice >= 0) { - await InstallImagePack(path, packs[choice]); - Pause(); - } else if(choice == packs.Length) { - return; - } else { - Console.WriteLine("you fucked up"); - Pause(); - } - } else { - Console.WriteLine("None found. Have a nice day"); - Pause(); - } - } - - private static async Task InstallImagePack(string path, ImagePack pack) - { - await pack.Install(path); - } - - private static void PauseExit(int exitcode = 0) - { - Console.WriteLine("Press any key to exit."); - Console.ReadLine(); //wait for input so the console doesn't auto close in windows - Environment.Exit(exitcode); - } - - private static void Pause() - { - if(cliMode) return; - Console.WriteLine("Press any key to continue."); - Console.ReadKey(true); - } - - private static void AskAboutNewCores(bool force = false) - { - while(settings.GetConfig().download_new_cores == null || force) { - force = false; - - Console.WriteLine("Would you like to, by default, install new cores? [Y]es, [N]o, [A]sk for each:"); - ConsoleKey response = Console.ReadKey(false).Key; - settings.GetConfig().download_new_cores = response switch { - ConsoleKey.Y => "yes", - ConsoleKey.N => "no", - ConsoleKey.A => "ask", - _ => null - }; - } + Console.WriteLine(GetRandomSponsorLinks()); + FunFacts(); } - - private static string[] menuItems = { - "Update All", - "Update Firmware", - "Download Required Assets", - "Select Cores", - "Download Platform Image Packs", - "Generate Instance JSON Files", - "Generate Game and Watch ROMS", - "Enable All Display Modes", - "Backup Saves Directory", - "Settings", - "Exit" - }; - - private static string[] welcomeMessages = { - @" - _____ _ _ ___ _____ _ -| __ | |___ _____ ___ _ _ ___ _ _ ___ ___ ___| | _| ___ ___ | __|___ _| | -| __ -| | .'| | -_| | | | . | | | _|_ -| -_| | _| | . | _| | | | . | . | -|_____|_|__,|_|_|_|___| |_ |___|___|_| |___|___|_|_| |___|_| |_____|___|___| - |___| ", - @" - _ _ _ _ _ _____ _ _ -| | | |___| |___ ___ _____ ___ | |_ ___ | __| |___ _ _ ___ ___| |_ ___ _ _ _ ___ -| | | | -_| | _| . | | -_| | _| . | | __| | .'| | | . | _| _| . | | | | | -|_____|___|_|___|___|_|_|_|___| |_| |___| |__| |_|__,|\_/|___|_| |_| |___|_____|_|_| - ", - @" - _ ___ _ _ _ _ - ___| |_ ___ | _|_|___ _| |___ _ _ ___ _ _ ___ ___ _ _ ___| |_ _ _ _| |___ _ _ ___ -|_ -| | -_| | _| | | . |_ -| | | | . | | | | _| _| | |_ -| _| | | | . | .'| | | -_| -|___|_|_|___| |_| |_|_|_|___|___| |_ |___|___| |___|_| |___|___|_| |_ | |___|__,|\_/|___| - |___| |___| ", - @" - _____ _ _ _ ___ _ _ -|_ _| |_|_|___ |_|___ ___ | _|___ ___ ___ _ _ ___ ___ ___| |_ ___ _ _ ___ ___ ___| |_ - | | | | |_ -| | |_ -| | .'| | _| .'| | _| | | | _| -_|_ -| _| .'| | | _| .'| | _| - |_| |_|_|_|___| |_|___| |__,| |_| |__,|_|_|___|_ | |_| |___|___|_| |__,|___|_| |__,|_|_|_| - |___| ", - @" __ - _ _ _ _ _ _ _ _____ _ _ _____ _ _ | | -| | | |___| |___ ___ _____ ___ | |_ ___ | |_| |_ ___ | __ | |___ ___| |_ | |___ ___| |_ ___| |_| | -| | | | -_| | _| . | | -_| | _| . | | _| | -_| | __ -| | .'| _| '_| | | | | .'| _| '_| -_| _|__| -|_____|___|_|___|___|_|_|_|___| |_| |___| |_| |_|_|___| |_____|_|__,|___|_,_| |_|_|_|__,|_| |_,_|___|_| |__| - ", - @" - _____ _ _ _____ _ -| | |_ ___| |_ _ _ |_ _|___ ___| |_ _ _ -|- -| _| _| | | |_ | | | .'|_ -| _| | |_ -|_____|_| |___|_|_|_ |_| |_| |__,|___|_| |_ |_| - |___| |___| ", - @" _ _____ - _ _ _ _ _ | | _ _ |___ | -| | | | |_ ___| |_|_|___ ___ _ _ ___ | |_ _ _ _ _|_|___| _| -| | | | | .'| _| | _| -_| | | | .'| | . | | | | | | |_| -|_____|_|_|__,|_| |_| |___| |_ |__,| |___|___|_ |_|_|_|_| - |___| |___| ", - @" _____ - _ _ _ _ _ _ |___ | -| | | | |_ ___| |_ |_|___ ___ _____ ___ ___| _| -| | | | | .'| _| | |_ -| | .'| | | .'| |_| -|_____|_|_|__,|_| |_|___| |__,| |_|_|_|__,|_|_|_| - ", - @" - _____ _ _ _____ _ _ _ -| __|_|___ ___|_|___ ___ | |___|_| |___ _| | -| __| |_ -|_ -| | . | | | | | | .'| | | -_| . | -|__| |_|___|___|_|___|_|_| |_|_|_|__,|_|_|___|___| - ", - @" - _____ _ -| | |_____| |_ ___ ___ ___ -| | | | . | .'|_ -| .'| -|_____|_|_|_|___|__,|___|__,| - ", - @" _ - __ _ | | -| | ___| |_|_|___ _____ ___ ___ ___ _ _ -| |__| -_| _| |_ -| | | . |_ -| -_| | | -|_____|___|_| |___| |_|_|_|___|___|___|_ | - |___|", -@" _=,_ - o_/6 /#\ - \__ |##/ - ='|--\ - / #'-. - \#|_ _'-. / - |/ \_( # |'' - C/ ,--___/" - }; -} -[Verb("menu", isDefault: true, HelpText = "Interactive Main Menu")] -public class MenuOptions -{ - [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] - public string? InstallPath { get; set; } - - [Option('s', "skip-update", HelpText = "Skip the self update check", Required = false)] - public bool SkipUpdate { get; set; } -} - -[Verb("update", HelpText = "Run update all. (You can configure via the settings menu)")] -public class UpdateOptions -{ - [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] - public string? InstallPath { get; set; } - - [Option ('c', "core", Required = false, HelpText = "The core you want to update.")] - public string CoreName { get; set; } - - [Option('f', "platformsfolder", Required = false, HelpText = "Preserve the Platforms folder, so customizations aren't overwritten by updates.")] - public bool PreservePlatformsFolder { get; set; } - - [Option('r', "clean", Required = false, HelpText = "Clean install. Remove all existing core files, before updating")] - public bool CleanInstall { get; set; } - -} - -[Verb("uninstall", HelpText = "Delete a core")] -public class UninstallOptions -{ - [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] - public string? InstallPath { get; set; } - - [Option ('c', "core", Required = true, HelpText = "The core you want to delete.")] - public string CoreName { get; set; } - - [Option('a', "assets", Required = false, HelpText = "Delete the core specific Assets folder")] - public bool DeleteAssets { get; set; } -} - -[Verb("assets", HelpText = "Run the asset downloader")] -public class AssetsOptions -{ - [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] - public string? InstallPath { get; set; } - - [Option ('c', "core", Required = false, HelpText = "The core you want to download assets for.")] - public string CoreName { get; set; } -} - -[Verb("instancegenerator", HelpText = "Run the instance JSON generator")] -public class InstancegeneratorOptions -{ - [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] - public string? InstallPath { get; set; } -} - -[Verb("images", HelpText = "Download image packs")] -public class ImagesOptions -{ - [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] - public string? InstallPath { get; set; } - - [Option('o', "owner", Required = true, HelpText = "Image pack repo username")] - public string ImagePackOwner { get; set; } - - [Option('i', "imagepack", Required = true, HelpText = "Github repo name for image pack")] - public string ImagePackRepo { get; set; } - - [Option('v', "variant", Required = false, HelpText = "The optional variant")] - public string? ImagePackVariant { get; set; } -} - -[Verb("firmware", HelpText = "Check for Pocket firmware updates")] -public class FirmwareOptions -{ - [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] - public string? InstallPath { get; set; } -} - -[Verb("fund", HelpText = "List sponsor links")] -public class FundOptions -{ - [Option('c', "core", HelpText = "The core to check funding links for", Required = false)] - public string? Core { get; set; } -} - -[Verb("update-self", HelpText = "Update this utility")] -public class UpdateSelfOptions -{ -} - -[Verb("backup-saves", HelpText = "Create a compressed zip file of the Saves directory.")] -public class BackupSavesOptions -{ - [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] - public string? InstallPath { get; set; } - - [Option('l', "location", HelpText = "Absolute path to backup location", Required = true)] - public string BackupPath { get; set; } = null!; - - [Option('s', "save", HelpText = "Save settings to the config file", Required = false)] - public bool Save { get; set; } -} - -public static class EnumExtension -{ - public static IEnumerable<(T item, int index)> WithIndex(this IEnumerable self) - => self.Select((item, index) => (item, index)); } diff --git a/src/SettingsManager.cs b/src/SettingsManager.cs index 869fed05..88cc7cfb 100644 --- a/src/SettingsManager.cs +++ b/src/SettingsManager.cs @@ -1,14 +1,16 @@ -using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.IO; +using Pannella.Models; +using Pannella.Models.Settings; -namespace pannella.analoguepocket; +namespace Pannella; +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public class SettingsManager { - private Settings _settings; - private string _settingsFile; - private List _newCores = new(); + private readonly Settings _settings; + private readonly string _settingsFile; + private readonly List _newCores = new(); private const string OLD_DEFAULT = "pocket-roms"; private const string NEW_DEFAULT = "openFPGA-Files"; @@ -16,9 +18,10 @@ public class SettingsManager private const string OLD_SETTINGS_FILENAME = "pocket_updater_settings.json"; private const string SETTINGS_FILENAME = "pupdate_settings.json"; - public SettingsManager(string settingsPath, List? cores = null) + public SettingsManager(string settingsPath, List cores = null) { _settings = new Settings(); + string file = Path.Combine(settingsPath, SETTINGS_FILENAME); string oldFile = Path.Combine(settingsPath, OLD_SETTINGS_FILENAME); string json = null!; @@ -37,120 +40,96 @@ public SettingsManager(string settingsPath, List? cores = null) { _settings = JsonSerializer.Deserialize(json); - //hack to force people over to new default :) - if(_settings.config.archive_name == OLD_DEFAULT) { + // hack to force people over to new default :) + if (_settings.config.archive_name == OLD_DEFAULT) + { _settings.config.archive_name = NEW_DEFAULT; } } - //bandaid to fix old settings files - if(_settings.config == null) { - _settings.config = new Config(); - } + // bandaid to fix old settings files + _settings.config ??= new Config(); _settingsFile = file; - if(cores != null) { + if (cores != null) + { InitializeCoreSettings(cores); } SaveSettings(); } - //loop through every core, and add any missing ones to the settings file + // loop through every core, and add any missing ones to the settings file public void InitializeCoreSettings(List cores) { - if(_settings.coreSettings == null) { - _settings.coreSettings = new Dictionary(); - } - foreach(Core core in cores) + _settings.coreSettings ??= new Dictionary(); + + foreach (Core core in cores) { - if(!_settings.coreSettings.ContainsKey(core.identifier)) { + if (!_settings.coreSettings.ContainsKey(core.identifier)) + { _newCores.Add(core); } } } public List GetMissingCores() => _newCores; + public void EnableMissingCores(List cores) { foreach (var core in cores) + { EnableCore(core.identifier); + } } + public void DisableMissingCores(List cores) { foreach (var core in cores) + { DisableCore(core.identifier); + } } - public bool SaveSettings() + public void SaveSettings() { var options = new JsonSerializerOptions { WriteIndented = true }; File.WriteAllText(_settingsFile, JsonSerializer.Serialize(_settings, options)); - - return true; } public void DisableCore(string name) { - if(_settings.coreSettings.ContainsKey(name)) + if (_settings.coreSettings.TryGetValue(name, out CoreSettings value)) { - _settings.coreSettings[name].skip = true; + value.skip = true; } else { - CoreSettings core = new CoreSettings(); - core.skip = true; + CoreSettings core = new CoreSettings { skip = true }; + _settings.coreSettings.Add(name, core); } } public void EnableCore(string name) { - if (_settings.coreSettings.ContainsKey(name)) + if (_settings.coreSettings.TryGetValue(name, out CoreSettings value)) { - _settings.coreSettings[name].skip = false; + value.skip = false; } else { - CoreSettings core = new CoreSettings(); - core.skip = false; - _settings.coreSettings.Add(name, core); - } - } + CoreSettings core = new CoreSettings { skip = false }; - public void UpdateCore(CoreSettings core, string name) - { - if (_settings.coreSettings.ContainsKey(name)) - { - _settings.coreSettings[name] = core; - } - else - { _settings.coreSettings.Add(name, core); } } public CoreSettings GetCoreSettings(string name) { - if (_settings.coreSettings.ContainsKey(name)) - { - return _settings.coreSettings[name]; - } - else - { - return new CoreSettings(); - } - } - - public Firmware GetCurrentFirmware() - { - return _settings.firmware; - } - - public void SetFirmwareVersion(string version) - { - _settings.firmware.version = version; - SaveSettings(); + return _settings.coreSettings.TryGetValue(name, out CoreSettings value) + ? value + : new CoreSettings(); } public Config GetConfig() @@ -158,9 +137,10 @@ public Config GetConfig() return _settings.config; } + // This is used by the RetroDriven Pocket Updater Windows Application + // ReSharper disable once UnusedMember.Global public void UpdateConfig(Config config) { _settings.config = config; } - } diff --git a/src/Updater.cs b/src/Updater.cs deleted file mode 100644 index 4467d50d..00000000 --- a/src/Updater.cs +++ /dev/null @@ -1,564 +0,0 @@ -using System; -using System.IO; -using System.IO.Compression; -using System.Text.Json; -using System.Text.RegularExpressions; - -namespace pannella.analoguepocket; - -public class PocketCoreUpdater : Base -{ - private const string FIRMWARE_FILENAME_PATTERN = "pocket_firmware_*.bin"; - private const string FIRMWARE_URL = "https://www.analogue.co/support/pocket/firmware/latest"; - private static readonly Regex BIN_REGEX = new Regex(@"(?inx) - ]* - href \s* = \s* - (? ['""] ) - (? [^'""]*\.bin ) - \k - [^>]* >"); - private bool _downloadAssets = false; - private bool _preservePlatformsFolder = false; - - private string _githubApiKey = ""; - - private bool _downloadFirmware = true; - private bool _deleteSkippedCores = true; - private bool _useConsole = false; - private bool _renameJotegoCores = true; - private bool _jtBeta = false; - private bool _backupSaves = false; - private string _backupSavesLocation; - - private Dictionary _platformFiles = new Dictionary(); - - /// - /// Constructor - /// - /// The directory to install/update openFPGA cores in. - /// Path to settings json file - public PocketCoreUpdater(string updateDirectory, string? settingsPath = null) - { - Factory.GetGlobals().UpdateDirectory = updateDirectory; - Directory.CreateDirectory(Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores")); - - if(settingsPath != null) { - Factory.GetGlobals().SettingsPath = settingsPath; - } else { - Factory.GetGlobals().SettingsPath = updateDirectory; - } - } - - public async Task Initialize() - { - LoadSettings(); - await LoadPlatformFiles(); - await LoadCores(); - await RefreshInstalledCores(); - await LoadArchive(); - await LoadBlacklist(); - } - - private async Task RefreshInstalledCores() - { - var installedCores = new List(); - foreach(Core c in GlobalHelper.Instance.Cores) { - if(c.isInstalled()) { - installedCores.Add(c); - } - } - GlobalHelper.Instance.InstalledCores = installedCores; - } - - private async Task LoadPlatformFiles() - { - try { - List files = await GithubApi.GetFiles("dyreschlock", "pocket-platform-images", "arcade/Platforms", - GlobalHelper.Instance.SettingsManager.GetConfig().github_token); - Dictionary platformFiles = new Dictionary(); - foreach(Github.File file in files) { - string url = file.download_url; - string filename = file.name; - if (filename.EndsWith(".json")) { - string platform = Path.GetFileNameWithoutExtension(filename); - platformFiles.Add(platform, url); - } - } - _platformFiles = platformFiles; - } catch (Exception e) { - _writeMessage("Unable to retrieve archive contents. Asset download may not work."); - _platformFiles = new Dictionary(); - } - } - - private async Task LoadArchive() - { - _writeMessage("Loading Assets Index..."); - if(Factory.GetGlobals().SettingsManager.GetConfig().use_custom_archive) { - var custom = Factory.GetGlobals().SettingsManager.GetConfig().custom_archive; - Uri baseUrl = new Uri(custom["url"]); - Uri url = new Uri(baseUrl, custom["index"]); - - Factory.GetGlobals().ArchiveFiles = await ArchiveService.GetFilesCustom(url.ToString()); - } else { - Factory.GetGlobals().ArchiveFiles = await ArchiveService.GetFiles(Factory.GetGlobals().SettingsManager.GetConfig().archive_name); - } - } - - private async Task LoadBlacklist() - { - Factory.GetGlobals().Blacklist = await AssetsService.GetBlacklist(); - } - - public async Task LoadCores() - { - Factory.GetGlobals().Cores = await CoresService.GetCores(); - Factory.GetGlobals().SettingsManager.InitializeCoreSettings(Factory.GetGlobals().Cores); - foreach(Core core in Factory.GetGlobals().Cores) { - core.StatusUpdated += updater_StatusUpdated; //attach handler to bubble event up - } - } - - public List GetCores() - { - return Factory.GetGlobals().Cores; - } - - public void LoadSettings() - { - Factory.GetGlobals().SettingsManager = new SettingsManager(Factory.GetGlobals().SettingsPath, Factory.GetGlobals().Cores); - } - - public List GetMissingCores() => Factory.GetGlobals().SettingsManager?.GetMissingCores() ?? new List(); - - /// - /// Turn on/off printing progress messages to the console - /// - /// Set to true to turn on console messages - public void PrintToConsole(bool set) - { - _useConsole = set; - } - - public void RenameJotegoCores(bool set) - { - _renameJotegoCores = set; - } - - /// - /// Turn on/off the automatic BIOS downloader - /// - /// Set to true to enable automatic BIOS downloading - public void DownloadAssets(bool set) - { - _downloadAssets = set; - } - - /// - /// Turn on/off preserving customizations to /Platforms - /// - /// Set to true to enable preserving custom /Platforms changes - public void PreservePlatformsFolder(bool set) - { - _preservePlatformsFolder = set; - } - - public void DownloadFirmware(bool set) - { - _downloadFirmware = set; - } - - public void BackupSaves(bool set, string location) - { - _backupSaves = set; - _backupSavesLocation = location; - } - - //get api and local cores - private async Task> getAllCores() - { - List cores = Factory.GetGlobals().Cores; - List local = await GetLocalCores(); - foreach(Core core in local) { - core.StatusUpdated += updater_StatusUpdated; //attach handler to bubble event up - } - cores.AddRange(local); - - return cores; - } - - public async Task BuildInstanceJSON(bool overwrite = false, string? corename = null) - { - List cores = await getAllCores(); - foreach(Core core in Factory.GetGlobals().Cores) { - if(core.CheckInstancePackager() && (corename == null || corename == core.identifier)) { - _writeMessage(core.identifier); - core.BuildInstanceJSONs(overwrite); - Divide(); - } - } - } - - /// - /// Run the full openFPGA core download and update process - /// - public async Task RunUpdates(string? id = null, bool clean = false) - { - List> installed = new List>(); - List installedAssets = new List(); - List skippedAssets = new List(); - List missingBetaKeys = new List(); - Dictionary results = new Dictionary(); - string firmwareDownloaded = ""; - if(Factory.GetGlobals().Cores == null) { - throw new Exception("Must initialize updater before running update process"); - } - - if (_backupSaves) - { - AssetsService.BackupSaves(Factory.GetGlobals().UpdateDirectory, _backupSavesLocation); - } - - if(_downloadFirmware && id == null) { - firmwareDownloaded = await UpdateFirmware(); - } - - await ExtractBetaKey(); - - List cores = await getAllCores(); - string json; - foreach(Core core in Factory.GetGlobals().Cores) { - if(id != null && core.identifier != id) { - continue; - } - - core.downloadAssets = (_downloadAssets && (id==null)); - core.buildInstances = (Factory.GetGlobals().SettingsManager.GetConfig().build_instance_jsons && (id==null)); - try { - if(Factory.GetGlobals().SettingsManager.GetCoreSettings(core.identifier).skip) { - await DeleteCore(core); - continue; - } - - if (core.requires_license && !_jtBeta) { - continue; //skip if you don't have the key - } - - string name = core.identifier; - if(name == null) { - _writeMessage("Core Name is required. Skipping."); - continue; - } - - _writeMessage("Checking Core: " + name); - var mostRecentRelease = core.version; - - if(mostRecentRelease == null) { - _writeMessage("No releases found. Skipping"); - await CopyBetaKey(core); - results = await core.DownloadAssets(); - installedAssets.AddRange(results["installed"] as List); - skippedAssets.AddRange(results["skipped"] as List); - if((bool)results["missingBetaKey"]) { - missingBetaKeys.Add(core.identifier); - } - await JotegoRename(core); - Divide(); - continue; - } - - _writeMessage(mostRecentRelease + " is the most recent release, checking local core..."); - if (core.isInstalled()) { - Analogue.Cores.Core.Core localCore = core.getConfig(); - string localVersion = localCore.metadata.version; - - if(localVersion != null) { - _writeMessage("local core found: " + localVersion); - } - - if (mostRecentRelease != localVersion || clean){ - _writeMessage("Updating core"); - } else { - await CopyBetaKey(core); - results = await core.DownloadAssets(); - await JotegoRename(core); - installedAssets.AddRange(results["installed"] as List); - skippedAssets.AddRange(results["skipped"] as List); - if((bool)results["missingBetaKey"]) { - missingBetaKeys.Add(core.identifier); - } - _writeMessage("Up to date. Skipping core"); - Divide(); - continue; - } - } else { - _writeMessage("Downloading core"); - } - - if(await core.Install(clean)) { - Dictionary summary = new Dictionary(); - summary.Add("version", mostRecentRelease); - summary.Add("core", core.identifier); - summary.Add("platform", core.platform.name); - installed.Add(summary); - } - await JotegoRename(core); - await CopyBetaKey(core); - results = await core.DownloadAssets(); - installedAssets.AddRange(results["installed"] as List); - skippedAssets.AddRange(results["skipped"] as List); - if((bool)results["missingBetaKey"]) { - missingBetaKeys.Add(core.identifier); - } - _writeMessage("Installation complete."); - Divide(); - - } catch(Exception e) { - _writeMessage("Uh oh something went wrong."); - _writeMessage(e.Message); - } - } - - UpdateProcessCompleteEventArgs args = new UpdateProcessCompleteEventArgs(); - args.Message = "Update Process Complete"; - args.InstalledCores = installed; - args.InstalledAssets = installedAssets; - args.SkippedAssets = skippedAssets; - args.MissingBetaKeys = missingBetaKeys; - args.FirmwareUpdated = firmwareDownloaded; - OnUpdateProcessComplete(args); - } - - private async Task JotegoRename(Core core) - { - if(_renameJotegoCores && Factory.GetGlobals().SettingsManager.GetCoreSettings(core.identifier).platform_rename - && core.identifier.Contains("jotego")) { - core.platform_id = core.identifier.Split('.')[1]; //whatever - string path = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Platforms", core.platform_id + ".json"); - string json = File.ReadAllText(path); - Dictionary data = JsonSerializer.Deserialize>(json); - Platform platform = data["platform"]; - if(_platformFiles.ContainsKey(core.platform_id) && platform.name == core.platform_id) { - _writeMessage("Updating JT Platform Name..."); - await Factory.GetHttpHelper().DownloadFileAsync(_platformFiles[core.platform_id], path); - _writeMessage("Complete"); - } - } - } - - private async Task CopyBetaKey(Core core) - { - if(core.JTBetaCheck()) { - Analogue.Cores.Core.Core info = core.getConfig(); - string path = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Assets", info.metadata.platform_ids[core.betaSlotPlatformIdIndex], "common"); - if(!Directory.Exists(path)) { - Directory.CreateDirectory(path); - } - string keyPath = Path.Combine(Factory.GetGlobals().UpdateDirectory, "betakeys"); - if(Directory.Exists(keyPath) && Directory.Exists(path)) { - Util.CopyDirectory(keyPath, path, false, true); - _writeMessage("Beta key copied to common directory."); - } - } - } - - private async Task ExtractBetaKey() - { - string keyPath = Path.Combine(Factory.GetGlobals().UpdateDirectory, "betakeys"); - string file = Path.Combine(Factory.GetGlobals().UpdateDirectory, "jtbeta.zip"); - if(File.Exists(file)) { - _jtBeta = true; - _writeMessage("Extracting JT beta key..."); - ZipFile.ExtractToDirectory(file, keyPath, true); - } - } - - public async Task RunAssetDownloader(string? id = null) - { - List installedAssets = new List(); - List skippedAssets = new List(); - List missingBetaKeys = new List(); - Dictionary results = new Dictionary(); - if(Factory.GetGlobals().Cores == null) { - throw new Exception("Must initialize updater before running update process"); - } - List cores = await getAllCores(); - foreach(Core core in Factory.GetGlobals().Cores) { - if(id != null && core.identifier != id) { - continue; - } - - if(Factory.GetGlobals().SettingsManager.GetCoreSettings(core.identifier).skip) { - continue; - } - - core.downloadAssets = true; - try { - string name = core.identifier; - if(name == null) { - _writeMessage("Core Name is required. Skipping."); - continue; - } - _writeMessage(core.identifier); - results = await core.DownloadAssets(); - installedAssets.AddRange(results["installed"] as List); - skippedAssets.AddRange(results["skipped"] as List); - if((bool)results["missingBetaKey"]) { - missingBetaKeys.Add(core.identifier); - } - Divide(); - } catch(Exception e) { - _writeMessage("Uh oh something went wrong."); - _writeMessage(e.Message); - } - } - - UpdateProcessCompleteEventArgs args = new UpdateProcessCompleteEventArgs(); - args.Message = "All Done"; - args.InstalledAssets = installedAssets; - args.SkippedAssets = skippedAssets; - args.MissingBetaKeys = missingBetaKeys; - OnUpdateProcessComplete(args); - } - - public async Task ForceDisplayModes(string? id = null) - { - if(Factory.GetGlobals().Cores == null) { - throw new Exception("Must initialize updater before running update process"); - } - List cores = await getAllCores(); - foreach(Core core in Factory.GetGlobals().Cores) { - if(id != null && core.identifier != id) { - continue; - } - - if(Factory.GetGlobals().SettingsManager.GetCoreSettings(core.identifier).skip) { - continue; - } - - core.downloadAssets = true; - try { - string name = core.identifier; - if(name == null) { - _writeMessage("Core Name is required. Skipping."); - continue; - } - _writeMessage("Updating " + core.identifier); - await core.AddDisplayModes(); - Divide(); - } catch(Exception e) { - _writeMessage("Uh oh something went wrong."); - _writeMessage(e.Message); - } - } - _writeMessage("Finished."); - } - - private void Divide() - { - _writeMessage("-------------"); - } - - public async Task> GetLocalCores() - { - string coresDirectory = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores"); - string[] directories = Directory.GetDirectories(coresDirectory,"*", SearchOption.TopDirectoryOnly); - List all = new List(); - foreach(string name in directories) { - string n = Path.GetFileName(name); - var matches = Factory.GetGlobals().Cores.Where(i=>i.identifier == n); - if(matches.Count() == 0) { - Core c = new Core { - identifier = n - }; - c.platform = c.ReadPlatformFile(); - all.Add(c); - } - } - - return all; - } - - public void SetGithubApiKey(string key) - { - _githubApiKey = key; - } - - protected virtual void OnUpdateProcessComplete(UpdateProcessCompleteEventArgs e) - { - EventHandler handler = UpdateProcessComplete; - if(handler != null) - { - handler(this, e); - } - RefreshInstalledCores(); - } - - public async Task UpdateFirmware() - { - string version = ""; - _writeMessage("Checking for firmware updates..."); - var details = await AnalogueFirmware.GetDetails(); - - string[] parts = details.download_url.Split("/"); - string filename = parts[parts.Length-1]; - string filepath = Path.Combine(Factory.GetGlobals().UpdateDirectory, filename); - if(!File.Exists(filepath) || !Util.CompareChecksum(filepath, details.md5, Util.HashTypes.MD5)) { - version = filename; - var oldfiles = Directory.GetFiles(Factory.GetGlobals().UpdateDirectory, FIRMWARE_FILENAME_PATTERN); - _writeMessage("Firmware update found. Downloading..."); - await Factory.GetHttpHelper().DownloadFileAsync(details.download_url, Path.Combine(Factory.GetGlobals().UpdateDirectory, filename)); - _writeMessage("Download Complete"); - _writeMessage(Path.Combine(Factory.GetGlobals().UpdateDirectory, filename)); - foreach (string oldfile in oldfiles) { - if (File.Exists(oldfile) && Path.GetFileName(oldfile) != filename) { - _writeMessage("Deleting old firmware file..."); - File.Delete(oldfile); - } - } - _writeMessage("To install firmware, restart your Pocket."); - } else { - _writeMessage("Firmware up to date."); - } - Divide(); - return version; - } - - public void DeleteSkippedCores(bool value) - { - _deleteSkippedCores = value; - } - - public async Task DeleteCore(Core core, bool force = false, bool nuke = false) - { - if(!_deleteSkippedCores || !force) { - return; - } - - core.Uninstall(nuke); - } - - private void updater_StatusUpdated(object sender, StatusUpdatedEventArgs e) - { - this.OnStatusUpdated(e); - } - public event EventHandler? UpdateProcessComplete; - - public void SetDownloadProgressHandler(EventHandler handler) - { - Factory.GetHttpHelper().DownloadProgressUpdate += handler; - } -} - -public class UpdateProcessCompleteEventArgs : EventArgs -{ - /// - /// Some kind of results - /// - public string Message { get; set; } - public List> InstalledCores { get; set; } - public List InstalledAssets { get; set; } - public List SkippedAssets { get; set; } - public string FirmwareUpdated { get; set; } = ""; - public List MissingBetaKeys { get; set; } -} diff --git a/src/exceptions/MissingRequiredInstanceFiles.cs b/src/exceptions/MissingRequiredInstanceFiles.cs index 8ed62583..fd4dcb60 100644 --- a/src/exceptions/MissingRequiredInstanceFiles.cs +++ b/src/exceptions/MissingRequiredInstanceFiles.cs @@ -1,20 +1,12 @@ -namespace pannella.analoguepocket; - -using System; +namespace Pannella; public class MissingRequiredInstanceFiles : Exception { - public MissingRequiredInstanceFiles() - { - } + public MissingRequiredInstanceFiles() { } public MissingRequiredInstanceFiles(string message) - : base(message) - { - } + : base(message) { } public MissingRequiredInstanceFiles(string message, Exception inner) - : base(message, inner) - { - } -} \ No newline at end of file + : base(message, inner) { } +} diff --git a/src/helpers/GlobalHelper.cs b/src/helpers/GlobalHelper.cs index 516ab7c2..a2e18370 100644 --- a/src/helpers/GlobalHelper.cs +++ b/src/helpers/GlobalHelper.cs @@ -1,42 +1,63 @@ -using System.IO; -using System.Net.Http; +using Pannella.Models; +using Pannella.Models.Archive; +using Pannella.Services; -namespace pannella.analoguepocket; +namespace Pannella.Helpers; -public class GlobalHelper +public static class GlobalHelper { - private static GlobalHelper instance = null; - private static object syncLock = new object(); - public archiveorg.Archive ArchiveFiles { get; set; } - public SettingsManager? SettingsManager { get; set ;} - public string UpdateDirectory { get; set; } - public string SettingsPath { get; set; } - public string[] Blacklist { get; set; } - public List? Cores { get; set; } - public List? InstalledCores { get; set; } - - private GlobalHelper() - { - - } + public static Archive ArchiveFiles { get; private set; } + public static SettingsManager SettingsManager { get; private set ;} + public static string UpdateDirectory { get; private set; } + public static string[] Blacklist { get; private set; } + public static List Cores { get; private set; } + public static List InstalledCores { get; private set; } - public static GlobalHelper Instance + private static bool isInitialized; + + public static async void Initialize(string path) { - get + if (!isInitialized) { - lock (syncLock) + isInitialized = true; + UpdateDirectory = path; + SettingsManager = new SettingsManager(path); + Cores = await CoresService.GetCores(); + SettingsManager.InitializeCoreSettings(Cores); + RefreshInstalledCores(); + Blacklist = await AssetsService.GetBlacklist(); + + Console.WriteLine("Loading Assets Index..."); + + if (SettingsManager.GetConfig().use_custom_archive) { - if (GlobalHelper.instance == null) { - GlobalHelper.instance = new GlobalHelper(); - } + var custom = SettingsManager.GetConfig().custom_archive; + Uri baseUrl = new Uri(custom["url"]); + Uri url = new Uri(baseUrl, custom["index"]); - return GlobalHelper.instance; + ArchiveFiles = await ArchiveService.GetFilesCustom(url.ToString()); + } + else + { + ArchiveFiles = await ArchiveService.GetFiles(SettingsManager.GetConfig().archive_name); } + + RefreshInstalledCores(); } - } + } + + public static void ReloadSettings() + { + SettingsManager = new SettingsManager(UpdateDirectory, Cores); + } + + public static void RefreshInstalledCores() + { + InstalledCores = Cores.Where(c => c.IsInstalled()).ToList(); + } - public Core? GetCore(string identifier) + public static Core GetCore(string identifier) { - return instance.Cores.Find(i => i.identifier == identifier); + return Cores.Find(i => i.identifier == identifier); } } diff --git a/src/helpers/Hacks.cs b/src/helpers/Hacks.cs deleted file mode 100644 index 746df410..00000000 --- a/src/helpers/Hacks.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.IO; - -namespace pannella.analoguepocket; - -public class Hacks -{ - private static string gamegearCore = @"{ - ""core"": { - ""magic"": ""APF_VER_1"", - ""metadata"": { - ""platform_ids"": [ - ""gg"" - ], - ""shortname"": ""GG"", - ""description"": ""GG Core"", - ""author"": ""Spiritualized"", - ""url"": """", - ""version"": ""1.3.0"", - ""date_release"": ""2022-08-25"" - }, - ""framework"": { - ""target_product"": ""Analogue Pocket"", - ""version_required"": ""1.1"", - ""sleep_supported"": true, - ""dock"": { - ""supported"": true, - ""analog_output"": false - }, - ""hardware"": { - ""link_port"": false, - ""cartridge_adapter"": -1 - } - }, - ""cores"": [ - { - ""name"": ""default"", - ""id"": 0, - ""filename"": ""gg.rev"" - } - ] - } -}"; - - public static void GamegearFix(string path) - { - string corefile = Path.Combine(path, "Cores", "Spiritualized.GG", "core.json"); - File.WriteAllText(corefile, gamegearCore, System.Text.Encoding.Default); - } - -} diff --git a/src/helpers/HttpHelper.cs b/src/helpers/HttpHelper.cs index f862f62b..4f6068b3 100644 --- a/src/helpers/HttpHelper.cs +++ b/src/helpers/HttpHelper.cs @@ -1,18 +1,16 @@ -using System.IO; -using System.Net.Http; - -namespace pannella.analoguepocket; +namespace Pannella.Helpers; public class HttpHelper { - private static HttpHelper instance = null; - private static object syncLock = new object(); - private HttpClient client = null; + private static HttpHelper instance; + private static readonly object syncLock = new(); + private HttpClient client; + public event EventHandler DownloadProgressUpdate; private HttpHelper() { - createClient(); + this.CreateClient(); } public static HttpHelper Instance @@ -21,30 +19,33 @@ public static HttpHelper Instance { lock (syncLock) { - if (HttpHelper.instance == null) { - HttpHelper.instance = new HttpHelper(); - } - - return HttpHelper.instance; + return instance ??= new HttpHelper(); } } } - - public async Task DownloadFileAsync(string uri, string outputPath, int timeout = 100) - { + public async Task DownloadFileAsync(string uri, string outputPath, int timeout = 100) + { bool console = false; - try { + + try + { var test = Console.WindowWidth; + console = true; - } catch (Exception) { } + } + catch (Exception) + { + // Do Nothing. + } using var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(timeout)); - Uri? uriResult; - if (!Uri.TryCreate(uri, UriKind.Absolute, out uriResult)) + if (!Uri.TryCreate(uri, UriKind.Absolute, out _)) + { throw new InvalidOperationException("URI is invalid."); + } using HttpResponseMessage r = await this.client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cts.Token); @@ -59,23 +60,31 @@ public async Task DownloadFileAsync(string uri, string outputPath, int timeout = while (isMoreToRead) { var read = await stream.ReadAsync(buffer, 0, buffer.Length); + if (read == 0) { isMoreToRead = false; - if (console) { + + if (console) + { Console.Write("\r"); } } else { readSoFar += read; + var progress = (double)readSoFar / totalSize; - if (console) { + + if (console) + { var progressWidth = Console.WindowWidth - 14; var progressBarWidth = (int)(progress * progressWidth); var progressBar = new string('=', progressBarWidth); var emptyProgressBar = new string(' ', progressWidth - progressBarWidth); + Console.Write($"\r{progressBar}{emptyProgressBar}] {(progress * 100):0.00}%"); + if (readSoFar == totalSize) { Console.CursorLeft = 0; @@ -83,52 +92,57 @@ public async Task DownloadFileAsync(string uri, string outputPath, int timeout = Console.CursorLeft = 0; } } - DownloadProgressEventArgs args = new DownloadProgressEventArgs(); - args.progress = progress; + + DownloadProgressEventArgs args = new() + { + Progress = progress + }; + OnDownloadProgressUpdate(args); + await fileStream.WriteAsync(buffer, 0, read); } } } - public async Task GetHTML(string uri, bool allowRedirect = true) - { - Uri? uriResult; - - if (!Uri.TryCreate(uri, UriKind.Absolute, out uriResult)) + public async Task GetHTML(string uri, bool allowRedirect = true) + { + if (!Uri.TryCreate(uri, UriKind.Absolute, out _)) + { throw new InvalidOperationException("URI is invalid."); + } - if(!allowRedirect) { - createClient(false); + if (!allowRedirect) + { + this.CreateClient(false); } var response = await this.client.GetAsync(uri); string html = await response.Content.ReadAsStringAsync(); - if(!allowRedirect) { - createClient(); + if (!allowRedirect) + { + this.CreateClient(); } - + return html; - } + } - private void createClient(bool allowRedirect = true) - { - this.client = new HttpClient(new HttpClientHandler() { AllowAutoRedirect = allowRedirect }); - this.client.Timeout = TimeSpan.FromMinutes(10); //10min - } + private void CreateClient(bool allowRedirect = true) + { + this.client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = allowRedirect }); + this.client.Timeout = TimeSpan.FromMinutes(10); // 10min + } - protected virtual void OnDownloadProgressUpdate(DownloadProgressEventArgs e) + private void OnDownloadProgressUpdate(DownloadProgressEventArgs e) { EventHandler handler = DownloadProgressUpdate; - if(handler != null) - { - handler(this, e); - } + + handler?.Invoke(this, e); } } public class DownloadProgressEventArgs : EventArgs { - public double progress = 0; + public double Progress; } diff --git a/src/helpers/SemverUtil.cs b/src/helpers/SemverUtil.cs index abb8481a..13c7adbe 100644 --- a/src/helpers/SemverUtil.cs +++ b/src/helpers/SemverUtil.cs @@ -1,36 +1,41 @@ -namespace pannella.analoguepocket; - using System.Text.RegularExpressions; +namespace Pannella.Helpers; + public class SemverUtil { private const string SEMVER_FINDER = @"\D*(\d+(\.\d+)*\.\d+)\D*"; - public static string? FindSemver(string input) + public static string FindSemver(string input) { Regex r = new Regex(SEMVER_FINDER); Match matches = r.Match(input); - if(matches == null || matches.Groups.Count <= 1) { + + if (matches.Groups.Count <= 1) + { return null; } + var semver = matches.Groups[1].Value; - //TODO throw some error if it doesn't find a semver in the tag + + // TODO: throw some error if it doesn't find a semver in the tag + semver = CompleteSemver(semver); return semver; } - /// - /////even though its technically not a valid semver, allow use of 2 part versions, and just add a .0 to complete the 3rd part - /// - public static string CompleteSemver(string version) + // Even though its technically not a valid semver, allow use of 2 part versions, + // and just add a .0 to complete the 3rd part + private static string CompleteSemver(string version) { string[] parts = version.Split("."); - if(parts.Length == 2) { + if (parts.Length == 2) + { version += ".0"; } - + return version; } @@ -38,22 +43,18 @@ public static bool SemverCompare(string semverA, string semverB) { Version verA = Version.Parse(semverA); Version verB = Version.Parse(semverB); - - switch(verA.CompareTo(verB)) + + switch (verA.CompareTo(verB)) { case 0: case -1: return false; + case 1: return true; + default: return true; } } - - public static bool IsActuallySemver(string potentiallySemver) - { - Version? ver = null; - return Version.TryParse(potentiallySemver, out ver); - } -} \ No newline at end of file +} diff --git a/src/helpers/StringConverter.cs b/src/helpers/StringConverter.cs index 63aef1a9..df5bc635 100644 --- a/src/helpers/StringConverter.cs +++ b/src/helpers/StringConverter.cs @@ -1,25 +1,22 @@ -namespace pannella; using System.Text.Json; +using System.Text.Json.Serialization; -public class StringConverter : System.Text.Json.Serialization.JsonConverter +namespace Pannella.Helpers; + +public class StringConverter : JsonConverter { public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Number) - { - var stringValue = reader.GetInt32(); - return stringValue.ToString(); - } - else if (reader.TokenType == JsonTokenType.String) + return reader.TokenType switch { - return reader.GetString(); - } - - throw new System.Text.Json.JsonException(); + JsonTokenType.Number => reader.GetInt32().ToString(), + JsonTokenType.String => reader.GetString(), + _ => throw new JsonException() + }; } public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) { writer.WriteStringValue(value); } -} \ No newline at end of file +} diff --git a/src/helpers/Util.cs b/src/helpers/Util.cs index f900cedc..c271effa 100644 --- a/src/helpers/Util.cs +++ b/src/helpers/Util.cs @@ -1,14 +1,16 @@ -using System.IO; -using Force.Crc32; using System.Security.Cryptography; -namespace pannella.analoguepocket; +using Force.Crc32; + +namespace Pannella.Helpers; public class Util { - private static string _platformsDirectory = "Platforms"; - private static string _temp = "imagesbackup"; + private const string PLATFORMS_DIRECTORY = "Platforms"; + private static readonly string[] BAD_DIRS = { "__MACOSX" }; - public enum HashTypes { + + public enum HashTypes + { CRC32, MD5 } @@ -20,7 +22,9 @@ public static void CopyDirectory(string sourceDir, string destinationDir, bool r // Check if the source directory exists if (!dir.Exists) + { throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + } // Cache directories before we start copying DirectoryInfo[] dirs = dir.GetDirectories(); @@ -32,6 +36,7 @@ public static void CopyDirectory(string sourceDir, string destinationDir, bool r foreach (FileInfo file in dir.GetFiles()) { string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath, overwrite); } @@ -41,6 +46,7 @@ public static void CopyDirectory(string sourceDir, string destinationDir, bool r foreach (DirectoryInfo subDir in dirs) { string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir, true, overwrite); } } @@ -49,82 +55,95 @@ public static void CopyDirectory(string sourceDir, string destinationDir, bool r public static void CleanDir(string source, bool preservePlatformsFolder = false, string platform = "") { // Clean up any bad directories (like Mac OS directories). - foreach(var dir in BAD_DIRS) { - try { + foreach (var dir in BAD_DIRS) + { + try + { Directory.Delete(Path.Combine(source, dir), true); } - catch { } + catch + { + // Ignore + } } - if(preservePlatformsFolder) { - string existing = Path.Combine(Factory.GetGlobals().UpdateDirectory, _platformsDirectory, platform + ".json"); - if(File.Exists(existing)) { - try { - string dir = Path.Combine(source, _platformsDirectory); + if (preservePlatformsFolder) + { + string existing = Path.Combine(GlobalHelper.UpdateDirectory, PLATFORMS_DIRECTORY, platform + ".json"); + + if (File.Exists(existing)) + { + try + { + string dir = Path.Combine(source, PLATFORMS_DIRECTORY); + Directory.Delete(dir, true); } - catch { } + catch + { + // Ignore + } } } // Clean files. - var files = Directory.EnumerateFiles(source).Where(file => isBadFile(Path.GetFileName(file))); - foreach(var file in files) { - try { + var files = Directory.EnumerateFiles(source).Where(file => IsBadFile(Path.GetFileName(file))); + + foreach (var file in files) + { + try + { File.Delete(file); } - catch { } + catch + { + // Ignore + } } // Recurse through subdirectories. var dirs = Directory.GetDirectories(source); - foreach(var dir in dirs) { - CleanDir(Path.Combine(source, Path.GetFileName(dir))); - } - static bool isBadFile(string name) + foreach (var dir in dirs) { - if (name.StartsWith('.')) return true; - if (name.EndsWith(".mra")) return true; - if (name.EndsWith(".txt")) return true; - return false; + CleanDir(Path.Combine(source, Path.GetFileName(dir))); } } - public static string GetCRC32(string filepath) + private static bool IsBadFile(string name) { - if(File.Exists(filepath)) { - var checksum = Crc32Algorithm.Compute(File.ReadAllBytes(filepath)); - return checksum.ToString("x8"); - } else { - throw new Exception("File doesn't exist. Cannot compute checksum"); - } + return name.StartsWith('.') || name.EndsWith(".mra") || name.EndsWith(".txt"); } - public static string GetMD5(string filepath) + public static bool CompareChecksum(string filepath, string checksum, HashTypes type = HashTypes.CRC32) { - if(File.Exists(filepath)) { - var checksum = MD5.HashData(File.ReadAllBytes(filepath)); - return Convert.ToHexString(checksum); - } else { - throw new Exception("File doesn't exist. Cannot compute checksum"); + if (!File.Exists(filepath)) + { + throw new Exception("File doesn't exist. Cannot compute checksum."); } - } - public static bool CompareChecksum(string filepath, string checksum, HashTypes type = HashTypes.CRC32) - { string hash; - switch(type) { + + switch (type) + { case HashTypes.MD5: - hash = GetMD5(filepath); + { + var newChecksum = MD5.HashData(File.ReadAllBytes(filepath)); + + hash = Convert.ToHexString(newChecksum); break; + } + case HashTypes.CRC32: default: - hash = GetCRC32(filepath); + { + var newChecksum = Crc32Algorithm.Compute(File.ReadAllBytes(filepath)); + + hash = newChecksum.ToString("x8"); break; + } } - + return hash.Equals(checksum, StringComparison.CurrentCultureIgnoreCase); } - } diff --git a/src/models/Analogue/AnalogueData.cs b/src/models/Analogue/AnalogueData.cs deleted file mode 100644 index 035fbfa7..00000000 --- a/src/models/Analogue/AnalogueData.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace pannella.analoguepocket.Analogue; - -public class Data -{ - public DataSlot[] data_slots { get; set; } -} \ No newline at end of file diff --git a/src/models/Analogue/AnalogueDataJSON.cs b/src/models/Analogue/AnalogueDataJSON.cs deleted file mode 100644 index a4c27df4..00000000 --- a/src/models/Analogue/AnalogueDataJSON.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace pannella.analoguepocket.Analogue; - -public class DataJSON -{ - public Data? data { get; set; } -} \ No newline at end of file diff --git a/src/models/Analogue/AnalogueDataSlot.cs b/src/models/Analogue/AnalogueDataSlot.cs deleted file mode 100644 index 776a0fc6..00000000 --- a/src/models/Analogue/AnalogueDataSlot.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace pannella.analoguepocket.Analogue; - -using System.Collections; - -public class DataSlot -{ - public string id { get; set; } - public string name { get; set; } - public bool required { get; set; } - public string parameters { get; set; } - - public string? filename { get; set; } - - public string[]? alternate_filenames{ get; set; } - - public string? md5 { get; set; } - - private BitArray? getBits() - { - if(parameters == null) { - return null; - } - int p = 0; - if(parameters.StartsWith("0x")) { - p = Convert.ToInt32(parameters, 16); - } else { - p = Int32.Parse(parameters); - } - - byte[] bytes = System.BitConverter.GetBytes(p); - BitArray bits = new BitArray(bytes); - - return bits; - } - - public bool isCoreSpecific() - { - var bits = getBits(); - - if(bits == null) { - return false; - } - - return bits[1]; - } - - public int getPlatformIdIndex() - { - var bits = getBits(); - - if(bits == null) { - return 0; - } - - var temp = new BitArray(2); - temp[1] = bits[25]; - temp[0] = bits[24]; - - int[] index = new int[1]; - temp.CopyTo(index, 0); - - return index[0]; - } -} \ No newline at end of file diff --git a/src/models/Analogue/AnalogueInstance.cs b/src/models/Analogue/AnalogueInstance.cs deleted file mode 100644 index c2d153d9..00000000 --- a/src/models/Analogue/AnalogueInstance.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace pannella.analoguepocket.Analogue; - -public class Instance -{ - public DataSlot[] data_slots { get; set; } - public string data_path { get; set; } = ""; - public string magic { get; set; } = "APF_VER_1"; -} \ No newline at end of file diff --git a/src/models/Analogue/AnalogueInstanceDataSlot.cs b/src/models/Analogue/AnalogueInstanceDataSlot.cs deleted file mode 100644 index bfe67615..00000000 --- a/src/models/Analogue/AnalogueInstanceDataSlot.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace pannella.analoguepocket.Analogue; - -using System.Collections; - -public class InstanceDataSlot -{ - public string id { get; set; } - public string? filename { get; set; } -} \ No newline at end of file diff --git a/src/models/Analogue/AnalogueMetadata.cs b/src/models/Analogue/AnalogueMetadata.cs deleted file mode 100644 index c23380a2..00000000 --- a/src/models/Analogue/AnalogueMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace pannella.analoguepocket.Analogue; - -public class Metadata -{ - public string[]? platform_ids { get; set; } - public string? shortname { get; set; } - public string? description { get; set; } - public string? author { get; set; } - public string? url { get; set; } - public string? version { get; set; } - public string? date_release { get; set; } -} \ No newline at end of file diff --git a/src/models/Analogue/AnalogueSimpleInstance.cs b/src/models/Analogue/AnalogueSimpleInstance.cs deleted file mode 100644 index db2184e8..00000000 --- a/src/models/Analogue/AnalogueSimpleInstance.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace pannella.analoguepocket.Analogue; - -public class SimpleInstance -{ - public InstanceDataSlot[] data_slots { get; set; } - public string data_path { get; set; } = ""; - public string magic { get; set; } = "APF_VER_1"; -} \ No newline at end of file diff --git a/src/models/Analogue/Core/AnalogueCore.cs b/src/models/Analogue/Core/AnalogueCore.cs new file mode 100644 index 00000000..5710b4b3 --- /dev/null +++ b/src/models/Analogue/Core/AnalogueCore.cs @@ -0,0 +1,10 @@ +namespace Pannella.Models.Analogue.Core; + +public class Core +{ + public Metadata metadata { get; set; } + + public string magic { get; set; } + + public Framework framework { get; set; } +} diff --git a/src/models/Analogue/AnalogueFramework.cs b/src/models/Analogue/Core/AnalogueFramework.cs similarity index 78% rename from src/models/Analogue/AnalogueFramework.cs rename to src/models/Analogue/Core/AnalogueFramework.cs index 8a2637cf..8a28eef5 100644 --- a/src/models/Analogue/AnalogueFramework.cs +++ b/src/models/Analogue/Core/AnalogueFramework.cs @@ -1,8 +1,8 @@ -namespace pannella.analoguepocket.Analogue; +namespace Pannella.Models.Analogue.Core; public class Framework { public string target_product { get; set; } public string version_required { get; set; } public bool sleep_supported { get; set; } -} \ No newline at end of file +} diff --git a/src/models/Analogue/Core/AnalogueMetadata.cs b/src/models/Analogue/Core/AnalogueMetadata.cs new file mode 100644 index 00000000..635bb773 --- /dev/null +++ b/src/models/Analogue/Core/AnalogueMetadata.cs @@ -0,0 +1,12 @@ +namespace Pannella.Models.Analogue.Core; + +public class Metadata +{ + public string[] platform_ids { get; set; } + public string shortname { get; set; } + public string description { get; set; } + public string author { get; set; } + public string url { get; set; } + public string version { get; set; } + public string date_release { get; set; } +} diff --git a/src/models/Analogue/Cores/Core/AnalogueCore.cs b/src/models/Analogue/Cores/Core/AnalogueCore.cs deleted file mode 100644 index 9e2b0578..00000000 --- a/src/models/Analogue/Cores/Core/AnalogueCore.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace pannella.analoguepocket.Analogue.Cores.Core; - -public class Core -{ - public Metadata? metadata { get; set; } - public string? magic { get; set; } - - public Framework? framework { get; set; } - -} \ No newline at end of file diff --git a/src/models/Analogue/Cores/Video/Video.cs b/src/models/Analogue/Cores/Video/Video.cs deleted file mode 100644 index 12acd054..00000000 --- a/src/models/Analogue/Cores/Video/Video.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace pannella.analoguepocket.Analogue.Cores.Video; - -public class Video -{ - public string? magic { get; set; } - - public List? scaler_modes { get; set; } - - public List? display_modes { get; set; } - -} \ No newline at end of file diff --git a/src/models/Analogue/Data/AnalogueData.cs b/src/models/Analogue/Data/AnalogueData.cs new file mode 100644 index 00000000..814143ab --- /dev/null +++ b/src/models/Analogue/Data/AnalogueData.cs @@ -0,0 +1,8 @@ +using Pannella.Models.Analogue.Shared; + +namespace Pannella.Models.Analogue.Data; + +public class Data +{ + public DataSlot[] data_slots { get; set; } +} diff --git a/src/models/Analogue/Data/AnalogueDataJSON.cs b/src/models/Analogue/Data/AnalogueDataJSON.cs new file mode 100644 index 00000000..830bbf09 --- /dev/null +++ b/src/models/Analogue/Data/AnalogueDataJSON.cs @@ -0,0 +1,6 @@ +namespace Pannella.Models.Analogue.Data; + +public class DataJSON +{ + public Data data { get; set; } +} diff --git a/src/models/Analogue/Instance/AnalogueInstance.cs b/src/models/Analogue/Instance/AnalogueInstance.cs new file mode 100644 index 00000000..2c981b1b --- /dev/null +++ b/src/models/Analogue/Instance/AnalogueInstance.cs @@ -0,0 +1,10 @@ +using Pannella.Models.Analogue.Shared; + +namespace Pannella.Models.Analogue.Instance; + +public class Instance +{ + public DataSlot[] data_slots { get; set; } + public string data_path { get; set; } = string.Empty; + public string magic { get; set; } = "APF_VER_1"; +} diff --git a/src/models/Analogue/AnalogueInstanceJSON.cs b/src/models/Analogue/Instance/AnalogueInstanceJSON.cs similarity index 60% rename from src/models/Analogue/AnalogueInstanceJSON.cs rename to src/models/Analogue/Instance/AnalogueInstanceJSON.cs index 97df9e68..b8a43eb8 100644 --- a/src/models/Analogue/AnalogueInstanceJSON.cs +++ b/src/models/Analogue/Instance/AnalogueInstanceJSON.cs @@ -1,6 +1,6 @@ -namespace pannella.analoguepocket.Analogue; +namespace Pannella.Models.Analogue.Instance; public class InstanceJSON { public Instance instance { get; set; } -} \ No newline at end of file +} diff --git a/src/models/Analogue/Instance/Simple/AnalogueSimpleInstance.cs b/src/models/Analogue/Instance/Simple/AnalogueSimpleInstance.cs new file mode 100644 index 00000000..ec00500c --- /dev/null +++ b/src/models/Analogue/Instance/Simple/AnalogueSimpleInstance.cs @@ -0,0 +1,8 @@ +namespace Pannella.Models.Analogue.Instance.Simple; + +public class SimpleInstance +{ + public SimpleDataSlot[] data_slots { get; set; } + public string data_path { get; set; } = string.Empty; + public string magic { get; set; } = "APF_VER_1"; +} diff --git a/src/models/Analogue/Instance/Simple/AnalogueSimpleInstanceDataSlot.cs b/src/models/Analogue/Instance/Simple/AnalogueSimpleInstanceDataSlot.cs new file mode 100644 index 00000000..953525e2 --- /dev/null +++ b/src/models/Analogue/Instance/Simple/AnalogueSimpleInstanceDataSlot.cs @@ -0,0 +1,7 @@ +namespace Pannella.Models.Analogue.Instance.Simple; + +public class SimpleDataSlot +{ + public string id { get; set; } + public string filename { get; set; } +} diff --git a/src/models/Analogue/AnalogueSimpleInstanceJSON.cs b/src/models/Analogue/Instance/Simple/AnalogueSimpleInstanceJSON.cs similarity index 60% rename from src/models/Analogue/AnalogueSimpleInstanceJSON.cs rename to src/models/Analogue/Instance/Simple/AnalogueSimpleInstanceJSON.cs index be49cfb6..7c1b7326 100644 --- a/src/models/Analogue/AnalogueSimpleInstanceJSON.cs +++ b/src/models/Analogue/Instance/Simple/AnalogueSimpleInstanceJSON.cs @@ -1,6 +1,6 @@ -namespace pannella.analoguepocket.Analogue; +namespace Pannella.Models.Analogue.Instance.Simple; public class SimpleInstanceJSON { public SimpleInstance instance { get; set; } -} \ No newline at end of file +} diff --git a/src/models/Analogue/Release.cs b/src/models/Analogue/Release.cs index f8b433a6..f8040589 100644 --- a/src/models/Analogue/Release.cs +++ b/src/models/Analogue/Release.cs @@ -1,12 +1,12 @@ -namespace pannella.analoguepocket.Analogue; +namespace Pannella.Models.Analogue; public class ReleaseDetails { - public string? url { get; set; } - public string? version { get; set; } - public string? published_at { get; set; } - public string? download_url { get; set; } - public string? md5 { get; set; } - public string? file_size { get; set; } - public string? release_notes_html { get; set; } -} \ No newline at end of file + public string url { get; set; } + public string version { get; set; } + public string published_at { get; set; } + public string download_url { get; set; } + public string md5 { get; set; } + public string file_size { get; set; } + public string release_notes_html { get; set; } +} diff --git a/src/models/Analogue/Shared/AnalogueDataSlot.cs b/src/models/Analogue/Shared/AnalogueDataSlot.cs new file mode 100644 index 00000000..c80147df --- /dev/null +++ b/src/models/Analogue/Shared/AnalogueDataSlot.cs @@ -0,0 +1,67 @@ +using System.Collections; + +namespace Pannella.Models.Analogue.Shared; + +public class DataSlot +{ + public string id { get; set; } + public string name { get; set; } + public bool required { get; set; } + public string parameters { get; set; } + public string filename { get; set; } + public string[] alternate_filenames { get; set; } + public string md5 { get; set; } + + private BitArray GetBits() + { + if (parameters == null) + { + return null; + } + + int p; + + if (parameters.StartsWith("0x")) + { + p = Convert.ToInt32(parameters, 16); + } + else + { + p = int.Parse(parameters); + } + + byte[] bytes = BitConverter.GetBytes(p); + BitArray bits = new BitArray(bytes); + + return bits; + } + + public bool IsCoreSpecific() + { + var bits = this.GetBits(); + + return bits != null && bits[1]; + } + + public int GetPlatformIdIndex() + { + var bits = this.GetBits(); + + if (bits == null) + { + return 0; + } + + var temp = new BitArray(2) + { + [1] = bits[25], + [0] = bits[24] + }; + + int[] index = new int[1]; + + temp.CopyTo(index, 0); + + return index[0]; + } +} diff --git a/src/models/Analogue/Cores/Video/DisplayMode.cs b/src/models/Analogue/Video/AnalogueDisplayMode.cs similarity index 50% rename from src/models/Analogue/Cores/Video/DisplayMode.cs rename to src/models/Analogue/Video/AnalogueDisplayMode.cs index 2a87a29e..f76b0b91 100644 --- a/src/models/Analogue/Cores/Video/DisplayMode.cs +++ b/src/models/Analogue/Video/AnalogueDisplayMode.cs @@ -1,7 +1,6 @@ -namespace pannella.analoguepocket.Analogue.Cores.Video; +namespace Pannella.Models.Analogue.Video; public class DisplayMode { public string id { get; set; } - -} \ No newline at end of file +} diff --git a/src/models/Analogue/Cores/Video/ScalerMode.cs b/src/models/Analogue/Video/AnalogueScalerMode.cs similarity index 81% rename from src/models/Analogue/Cores/Video/ScalerMode.cs rename to src/models/Analogue/Video/AnalogueScalerMode.cs index dc77ab22..b8468fdf 100644 --- a/src/models/Analogue/Cores/Video/ScalerMode.cs +++ b/src/models/Analogue/Video/AnalogueScalerMode.cs @@ -1,4 +1,4 @@ -namespace pannella.analoguepocket.Analogue.Cores.Video; +namespace Pannella.Models.Analogue.Video; public class ScalerMode { @@ -8,4 +8,4 @@ public class ScalerMode public int aspect_h { get; set; } public int rotation { get; set; } public int mirror { get; set; } -} \ No newline at end of file +} diff --git a/src/models/Analogue/Video/AnalogueVideo.cs b/src/models/Analogue/Video/AnalogueVideo.cs new file mode 100644 index 00000000..2d4483fd --- /dev/null +++ b/src/models/Analogue/Video/AnalogueVideo.cs @@ -0,0 +1,10 @@ +namespace Pannella.Models.Analogue.Video; + +public class Video +{ + public string magic { get; set; } + + public List scaler_modes { get; set; } + + public List display_modes { get; set; } +} diff --git a/src/models/Archive/Archive.cs b/src/models/Archive/Archive.cs index 51eac6f3..ad3a1673 100644 --- a/src/models/Archive/Archive.cs +++ b/src/models/Archive/Archive.cs @@ -1,16 +1,15 @@ -namespace archiveorg; - -using System.Linq; +namespace Pannella.Models.Archive; public class Archive { public int files_count { get; set; } public int item_last_updated { get; set; } - public archiveorg.File[] files { get; set; } + public File[] files { get; set; } - public archiveorg.File? GetFile(string filename) + public File GetFile(string filename) { - archiveorg.File? file = files.Where(file => file.name == filename).FirstOrDefault() as archiveorg.File; + File file = this.files.FirstOrDefault(file => file.name == filename); + return file; } -} \ No newline at end of file +} diff --git a/src/models/Archive/File.cs b/src/models/Archive/File.cs index c98992d5..0346d945 100644 --- a/src/models/Archive/File.cs +++ b/src/models/Archive/File.cs @@ -1,9 +1,9 @@ -namespace archiveorg; +namespace Pannella.Models.Archive; public class File { - public string? name { get; set; } - public string? md5 { get; set; } + public string name { get; set; } + public string md5 { get; set; } public string crc32 { get; set; } - public string? source { get; set; } -} \ No newline at end of file + public string source { get; set; } +} diff --git a/src/models/Asset.cs b/src/models/Asset.cs deleted file mode 100644 index 249e264d..00000000 --- a/src/models/Asset.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace pannella.analoguepocket; - -public class Asset -{ - public string platform{ get; set; } = ""; - public string? filename { get; set; } - public bool core_specific { get; set; } - public List? extensions { get; set; } -} \ No newline at end of file diff --git a/src/Base.cs b/src/models/Base.cs similarity index 58% rename from src/Base.cs rename to src/models/Base.cs index e033dc13..ed9e9426 100644 --- a/src/Base.cs +++ b/src/models/Base.cs @@ -1,28 +1,31 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models; public class Base { protected const string ARCHIVE_BASE_URL = "https://archive.org/download"; - public event EventHandler? StatusUpdated; + + public event EventHandler StatusUpdated; + protected void Divide() { - _writeMessage("-------------"); + WriteMessage("-------------"); } - protected void _writeMessage(string message) + protected void WriteMessage(string message) { - StatusUpdatedEventArgs args = new StatusUpdatedEventArgs(); - args.Message = message; + StatusUpdatedEventArgs args = new StatusUpdatedEventArgs + { + Message = message + }; + OnStatusUpdated(args); } - protected virtual void OnStatusUpdated(StatusUpdatedEventArgs e) + protected void OnStatusUpdated(StatusUpdatedEventArgs e) { EventHandler handler = StatusUpdated; - if(handler != null) - { - handler(this, e); - } + + handler?.Invoke(this, e); } } diff --git a/src/models/Core.cs b/src/models/Core.cs index cfb52e11..3554662d 100644 --- a/src/models/Core.cs +++ b/src/models/Core.cs @@ -1,121 +1,149 @@ -namespace pannella.analoguepocket; - -using System.IO; +using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.IO.Compression; -using System.Net.Http; +using System.Net; using System.Text.Json; -using System.Collections; - +using Pannella.Helpers; +using Pannella.Models.Analogue.Data; +using Pannella.Models.Analogue.Instance; +using Pannella.Models.Analogue.Instance.Simple; +using Pannella.Models.Analogue.Video; +using Pannella.Models.InstancePackager; +using Pannella.Models.Updater; +using AnalogueCore = Pannella.Models.Analogue.Core.Core; +using ArchiveFile = Pannella.Models.Archive.File; +using DataSlot = Pannella.Models.Analogue.Shared.DataSlot; +using InstancePackagerDataSlot = Pannella.Models.InstancePackager.DataSlot; + +namespace Pannella.Models; + +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public class Core : Base { public string identifier { get; set; } - public Repo? repository { get; set; } - public Platform? platform { get; set; } + public Repo repository { get; set; } + public Platform platform { get; set; } public string platform_id { get; set; } - public Sponsor? sponsor { get; set; } - public string? download_url { get; set; } - public string? release_date { get; set; } - public string? version { get; set; } - public string? betaSlotId = null; - public int betaSlotPlatformIdIndex = 0; - + public Sponsor sponsor { get; set; } + public string download_url { get; set; } + public string release_date { get; set; } + public string version { get; set; } + public string beta_slot_id; + public int beta_slot_platform_id_index; public bool requires_license { get; set; } = false; - private const string ZIP_FILE_NAME = "core.zip"; - public bool downloadAssets { get; set; } = Factory.GetGlobals().SettingsManager.GetConfig().download_assets; - public bool buildInstances { get; set; } = Factory.GetGlobals().SettingsManager.GetConfig().build_instance_jsons; + public bool download_assets { get; set; } = GlobalHelper.SettingsManager.GetConfig().download_assets; + + public bool build_instances { get; set; } = GlobalHelper.SettingsManager.GetConfig().build_instance_jsons; - private string[] allModes = { - "0x10", "0x20", "0x30", "0x31", "0x32", "0x40", "0x41", "0x42", "0x51", "0x52", "0xE0" - }; + private const string ZIP_FILE_NAME = "core.zip"; - private string[] gbModes = { - "0x21", "0x22", "0x23" - }; + private static string[] ALL_MODES = { "0x10", "0x20", "0x30", "0x31", "0x32", "0x40", "0x41", "0x42", "0x51", "0x52", "0xE0" }; + private static string[] GB_MODES = { "0x21", "0x22", "0x23" }; public override string ToString() { return platform.name; } - public async Task Install(bool clean = false) + public async Task Install(bool preservePlatformsFolder, bool clean = false) { - if(this.repository == null) { - _writeMessage("Core installed manually. Skipping."); + if (this.repository == null) + { + WriteMessage("Core installed manually. Skipping."); + return false; } - if (clean && this.isInstalled()) { + + if (clean && this.IsInstalled()) + { Delete(); } - //iterate through assets to find the zip release - if(await _installGithubAsset()) { - await this.ReplaceCheck(); + + // iterate through assets to find the zip release + if (await InstallGithubAsset(preservePlatformsFolder)) + { + this.ReplaceCheck(); + return true; - } else { - return false; } + + return false; } - private async Task _installGithubAsset() + private async Task InstallGithubAsset(bool preservePlatformsFolder) { - bool updated = false; - if (this.download_url == null) { - _writeMessage("No release URL found..."); - return updated; + if (this.download_url == null) + { + WriteMessage("No release URL found..."); + + return false; } - _writeMessage("Downloading file " + this.download_url + "..."); - string zipPath = Path.Combine(Factory.GetGlobals().UpdateDirectory, ZIP_FILE_NAME); - string extractPath = Factory.GetGlobals().UpdateDirectory; - await Factory.GetHttpHelper().DownloadFileAsync(this.download_url, zipPath); - _writeMessage("Extracting..."); + WriteMessage("Downloading file " + this.download_url + "..."); + + string zipPath = Path.Combine(GlobalHelper.UpdateDirectory, ZIP_FILE_NAME); + string extractPath = GlobalHelper.UpdateDirectory; + + await HttpHelper.Instance.DownloadFileAsync(this.download_url, zipPath); + + WriteMessage("Extracting..."); + string tempDir = Path.Combine(extractPath, "temp", this.identifier); + ZipFile.ExtractToDirectory(zipPath, tempDir, true); // Clean problematic directories and files. - Util.CleanDir(tempDir, Factory.GetGlobals().SettingsManager.GetConfig().preserve_platforms_folder, this.platform_id); + Util.CleanDir(tempDir, preservePlatformsFolder, this.platform_id); // Move the files into place and delete our core's temp directory. - _writeMessage("Installing..."); + WriteMessage("Installing..."); Util.CopyDirectory(tempDir, extractPath, true, true); Directory.Delete(tempDir, true); // See if the temp directory itself can be removed. - // Probably not needed if we aren't going to multithread this, but this is an async function so let's future proof. + // Probably not needed if we aren't going to multi-thread this, but this is an async function so let's future proof. if (!Directory.GetFiles(Path.Combine(extractPath, "temp")).Any()) + { Directory.Delete(Path.Combine(extractPath, "temp")); + } - updated = true; - File.Delete(zipPath); - return updated; + return true; } - private bool checkUpdateDirectory() + private static void CheckUpdateDirectory() { - if(!Directory.Exists(Factory.GetGlobals().UpdateDirectory)) { + if (!Directory.Exists(GlobalHelper.UpdateDirectory)) + { throw new Exception("Unable to access update directory"); } - - return true; } - public void Delete(bool nuke = false) + private void Delete(bool nuke = false) { - List folders = new List{"Cores", "Presets", "Settings"}; - foreach(string folder in folders) { - string path = Path.Combine(Factory.GetGlobals().UpdateDirectory, folder, this.identifier); - if(Directory.Exists(path)) { - _writeMessage("Deleting " + path); + List folders = new List { "Cores", "Presets", "Settings" }; + + foreach (string folder in folders) + { + string path = Path.Combine(GlobalHelper.UpdateDirectory, folder, this.identifier); + + if (Directory.Exists(path)) + { + WriteMessage("Deleting " + path); Directory.Delete(path, true); } } - if(nuke) { - string path = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Assets", this.platform_id, this.identifier); - if (Directory.Exists(path)) { - _writeMessage("Deleting " + path); + + if (nuke) + { + string path = Path.Combine(GlobalHelper.UpdateDirectory, "Assets", this.platform_id, this.identifier); + + if (Directory.Exists(path)) + { + WriteMessage("Deleting " + path); Directory.Delete(path, true); } } @@ -123,123 +151,108 @@ public void Delete(bool nuke = false) public void Uninstall(bool nuke = false) { - _writeMessage("Uninstalling " + this.identifier); - + WriteMessage("Uninstalling " + this.identifier); + Delete(nuke); - Factory.GetGlobals().SettingsManager.DisableCore(this.identifier); - Factory.GetGlobals().SettingsManager.SaveSettings(); + GlobalHelper.SettingsManager.DisableCore(this.identifier); + GlobalHelper.SettingsManager.SaveSettings(); - _writeMessage("Finished"); + WriteMessage("Finished"); Divide(); } - public Platform? ReadPlatformFile() + public Platform ReadPlatformFile() { - var info = this.getConfig(); + var info = this.GetConfig(); + if (info == null) { return this.platform; } - - string UpdateDirectory = Factory.GetGlobals().UpdateDirectory; - //cores with multiple platforms won't work...not sure any exist right now? - string platformsFolder = Path.Combine(UpdateDirectory, "Platforms"); + string updateDirectory = GlobalHelper.UpdateDirectory; + // cores with multiple platforms won't work...not sure any exist right now? + string platformsFolder = Path.Combine(updateDirectory, "Platforms"); string dataFile = Path.Combine(platformsFolder, info.metadata.platform_ids[0] + ".json"); - var p = JsonSerializer.Deserialize>(File.ReadAllText(dataFile)); - + var p = JsonSerializer.Deserialize>(File.ReadAllText(dataFile)); + return p["platform"]; } - public bool UpdatePlatform(string title, string category = null) + public async Task> DownloadAssets() { - var info = this.getConfig(); - if (info == null) + List installed = new List(); + List skipped = new List(); + bool missingBetaKey = false; + + if (!this.download_assets || !GlobalHelper.SettingsManager.GetCoreSettings(this.identifier).download_assets) { - return false; + return new Dictionary + { + { "installed", installed }, + { "skipped", skipped }, + { "missingBetaKey", false } + }; } - - string UpdateDirectory = Factory.GetGlobals().UpdateDirectory; - //cores with multiple platforms won't work...not sure any exist right now? - string platformsFolder = Path.Combine(UpdateDirectory, "Platforms"); - string dataFile = Path.Combine(platformsFolder, info.metadata.platform_ids[0] + ".json"); - if (!File.Exists(dataFile)) + CheckUpdateDirectory(); + WriteMessage("Looking for Assets"); + AnalogueCore info = this.GetConfig(); + string updateDirectory = GlobalHelper.UpdateDirectory; + // cores with multiple platforms won't work...not sure any exist right now? + string instancesDirectory = Path.Combine(updateDirectory, "Assets", info.metadata.platform_ids[0], this.identifier); + var options = new JsonSerializerOptions { Converters = { new StringConverter() } }; + + DataJSON dataJson = ReadDataJSON(); + + if (this.beta_slot_id != null) { - return false; + // what to do? } - if (platform.name != title || platform.category != category) + if (dataJson.data.data_slots.Length > 0) { - - Dictionary platform = new Dictionary(); - this.platform.name = title; - if (category != null) + foreach (DataSlot slot in dataJson.data.data_slots) { - this.platform.category = category; - } - platform.Add("platform", this.platform); - string json = JsonSerializer.Serialize(platform); - - File.WriteAllText(dataFile, json); - Factory.GetGlobals().SettingsManager.GetConfig().preserve_platforms_folder = true; - Factory.GetGlobals().SettingsManager.SaveSettings(); - } - - return true; - } - - public async Task> DownloadAssets() - { - List installed = new List(); - List skipped = new List(); - bool missingBetaKey = false; - if(!downloadAssets || !Factory.GetGlobals().SettingsManager.GetCoreSettings(this.identifier).download_assets) { - return new Dictionary{ - {"installed", installed }, - {"skipped", skipped }, - {"missingBetaKey", missingBetaKey } - }; - } - checkUpdateDirectory(); - _writeMessage("Looking for Assets"); - Analogue.Cores.Core.Core info = this.getConfig(); - string UpdateDirectory = Factory.GetGlobals().UpdateDirectory; - //cores with multiple platforms won't work...not sure any exist right now? - string instancesDirectory = Path.Combine(UpdateDirectory, "Assets", info.metadata.platform_ids[0], this.identifier); - var options = new JsonSerializerOptions - { - Converters = { new StringConverter() } - }; - - Analogue.DataJSON data = ReadDataJSON(); - if (betaSlotId != null) { + if (slot.filename != null && !slot.filename.EndsWith(".sav") && + !GlobalHelper.Blacklist.Contains(slot.filename)) + { + string path = Path.Combine(updateDirectory, "Assets", info.metadata.platform_ids[0]); - } - if(data.data.data_slots.Length > 0) { - foreach(Analogue.DataSlot slot in data.data.data_slots) { - if(slot.filename != null && !slot.filename.EndsWith(".sav") && !Factory.GetGlobals().Blacklist.Contains(slot.filename)) { - string path = Path.Combine(UpdateDirectory, "Assets", info.metadata.platform_ids[0]); - if(slot.isCoreSpecific()) { + if (slot.IsCoreSpecific()) + { path = Path.Combine(path, this.identifier); - } else { + } + else + { path = Path.Combine(path, "common"); } - List files = new List(); - files.Add(slot.filename); - if (slot.alternate_filenames != null) { + + List files = new List { slot.filename }; + + if (slot.alternate_filenames != null) + { files.AddRange(slot.alternate_filenames); } - foreach (string f in files) { + + foreach (string f in files) + { string filepath = Path.Combine(path, f); - if(File.Exists(filepath) && CheckCRC(filepath)) { - _writeMessage("Already installed: " + f); - } else { - if(await DownloadAsset(f, filepath)) { - installed.Add(filepath.Replace(UpdateDirectory, "")); - } else { - skipped.Add(filepath.Replace(UpdateDirectory, "")); + + if (File.Exists(filepath) && CheckCRC(filepath)) + { + WriteMessage("Already installed: " + f); + } + else + { + if (await DownloadAsset(f, filepath)) + { + installed.Add(filepath.Replace(updateDirectory, "")); + } + else + { + skipped.Add(filepath.Replace(updateDirectory, "")); } } } @@ -247,138 +260,186 @@ public async Task> DownloadAssets() } } - if(this.identifier == "Mazamars312.NeoGeo" || this.identifier == "Mazamars312.NeoGeo_Overdrive") { - return new Dictionary{ - {"installed", installed }, - {"skipped", skipped }, - {"missingBetaKey", false } - }; //nah + if (this.identifier is "Mazamars312.NeoGeo" or "Mazamars312.NeoGeo_Overdrive") + { + return new Dictionary + { + { "installed", installed }, + { "skipped", skipped }, + { "missingBetaKey", false } + }; } - if(CheckInstancePackager()) { + if (CheckInstancePackager()) + { BuildInstanceJSONs(); - return new Dictionary{ - {"installed", installed }, - {"skipped", skipped }, - {"missingBetaKey", missingBetaKey } + + return new Dictionary + { + { "installed", installed }, + { "skipped", skipped }, + { "missingBetaKey", false } }; } - - if(Directory.Exists(instancesDirectory)) { - string[] files = Directory.GetFiles(instancesDirectory,"*.json", SearchOption.AllDirectories); - foreach(string file in files) { - try { - //skip mac ._ files - if(File.GetAttributes(file).HasFlag(FileAttributes.Hidden)) { + + if (Directory.Exists(instancesDirectory)) + { + string[] files = Directory.GetFiles(instancesDirectory, "*.json", SearchOption.AllDirectories); + + foreach (string file in files) + { + try + { + // skip mac ._ files + if (File.GetAttributes(file).HasFlag(FileAttributes.Hidden)) + { continue; } - if(Factory.GetGlobals().SettingsManager.GetConfig().skip_alternative_assets && file.Contains(Path.Combine(instancesDirectory, "_alternatives"))) { + + if (GlobalHelper.SettingsManager.GetConfig().skip_alternative_assets && + file.Contains(Path.Combine(instancesDirectory, "_alternatives"))) + { continue; } - Analogue.InstanceJSON instance = JsonSerializer.Deserialize(File.ReadAllText(file), options); - if(instance.instance.data_slots.Length > 0) { - string data_path = instance.instance.data_path; - foreach(Analogue.DataSlot slot in instance.instance.data_slots) { - var plat = info.metadata.platform_ids[betaSlotPlatformIdIndex]; - if(!CheckBetaMD5(slot, plat)) { - _writeMessage("Invalid or missing beta key."); + + InstanceJSON instanceJson = JsonSerializer.Deserialize(File.ReadAllText(file), options); + + if (instanceJson.instance.data_slots.Length > 0) + { + string dataPath = instanceJson.instance.data_path; + + foreach (DataSlot slot in instanceJson.instance.data_slots) + { + var plat = info.metadata.platform_ids[this.beta_slot_platform_id_index]; + + if (!CheckBetaMD5(slot, plat)) + { + WriteMessage("Invalid or missing beta key."); missingBetaKey = true; } - if(!Factory.GetGlobals().Blacklist.Contains(slot.filename) && !slot.filename.EndsWith(".sav")) { - string path = Path.Combine(UpdateDirectory, "Assets", info.metadata.platform_ids[0], "common", data_path, slot.filename); - if(File.Exists(path) && CheckCRC(path)) { - _writeMessage("Already installed: " + slot.filename); - } else { - if(await DownloadAsset(slot.filename, path)) { - installed.Add(path.Replace(UpdateDirectory, "")); - } else { - skipped.Add(path.Replace(UpdateDirectory, "")); + + if (!GlobalHelper.Blacklist.Contains(slot.filename) && + !slot.filename.EndsWith(".sav")) + { + string path = Path.Combine(updateDirectory, "Assets", info.metadata.platform_ids[0], + "common", dataPath, slot.filename); + + if (File.Exists(path) && CheckCRC(path)) + { + WriteMessage("Already installed: " + slot.filename); + } + else + { + if (await DownloadAsset(slot.filename, path)) + { + installed.Add(path.Replace(updateDirectory, "")); + } + else + { + skipped.Add(path.Replace(updateDirectory, "")); } } } } } - } catch (Exception e) { - _writeMessage("Error while processing " + file); - _writeMessage(e.Message); + } + catch (Exception e) + { + WriteMessage("Error while processing " + file); + WriteMessage(e.Message); } } } - Dictionary results = new Dictionary{ - {"installed", installed }, - {"skipped", skipped }, - {"missingBetaKey", missingBetaKey } + + Dictionary results = new Dictionary + { + { "installed", installed }, + { "skipped", skipped }, + { "missingBetaKey", missingBetaKey } }; return results; } - public Analogue.Cores.Core.Core? getConfig() + public AnalogueCore GetConfig() { - checkUpdateDirectory(); - string file = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores", this.identifier, "core.json"); + CheckUpdateDirectory(); + + string file = Path.Combine(GlobalHelper.UpdateDirectory, "Cores", this.identifier, "core.json"); + if (!File.Exists(file)) { return null; } + string json = File.ReadAllText(file); - var options = new JsonSerializerOptions() - { - AllowTrailingCommas = true - }; - Analogue.Cores.Core.Core? config = JsonSerializer.Deserialize>(json, options)["core"]; + var options = new JsonSerializerOptions { AllowTrailingCommas = true }; + AnalogueCore config = JsonSerializer.Deserialize>(json, options)["core"]; return config; } - public Updater.Substitute[]? getSubstitutes() + public Substitute[] GetSubstitutes() { - checkUpdateDirectory(); - string file = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores", this.identifier, "updaters.json"); + CheckUpdateDirectory(); + + string file = Path.Combine(GlobalHelper.UpdateDirectory, "Cores", this.identifier, "updaters.json"); + if (!File.Exists(file)) { return null; } - string json = File.ReadAllText(file); - Updater.Updaters? config = JsonSerializer.Deserialize(json); - if (config == null) { - return null; - } + string json = File.ReadAllText(file); + Updaters config = JsonSerializer.Deserialize(json); - return config.previous; + return config?.previous; } - public bool isInstalled() + public bool IsInstalled() { - checkUpdateDirectory(); - string localCoreFile = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores", this.identifier, "core.json"); + CheckUpdateDirectory(); + + string localCoreFile = Path.Combine(GlobalHelper.UpdateDirectory, "Cores", this.identifier, "core.json"); + return File.Exists(localCoreFile); } private async Task DownloadAsset(string filename, string destination) { - if(Factory.GetGlobals().ArchiveFiles != null) { - archiveorg.File? file = Factory.GetGlobals().ArchiveFiles.GetFile(filename); - if(file == null) { - _writeMessage("Unable to find " + filename + " in archive"); + if (GlobalHelper.ArchiveFiles != null) + { + ArchiveFile file = GlobalHelper.ArchiveFiles.GetFile(filename); + + if (file == null) + { + WriteMessage("Unable to find " + filename + " in archive"); return false; } } - try { + try + { string url = BuildAssetUrl(filename); int count = 0; - do { - _writeMessage("Downloading " + filename); - await Factory.GetHttpHelper().DownloadFileAsync(url, destination, 600); - _writeMessage("Finished downloading " + filename); + + do + { + WriteMessage("Downloading " + filename); + await HttpHelper.Instance.DownloadFileAsync(url, destination, 600); + WriteMessage("Finished downloading " + filename); count++; - } while(count < 3 && !CheckCRC(destination)); - } catch(HttpRequestException e) { - if(e.StatusCode == System.Net.HttpStatusCode.NotFound) { - _writeMessage("Unable to find " + filename + " in archive"); - } else { - _writeMessage("There was a problem downloading " + filename); + } + while (count < 3 && !CheckCRC(destination)); + } + catch (HttpRequestException e) + { + if (e.StatusCode == HttpStatusCode.NotFound) + { + WriteMessage("Unable to find " + filename + " in archive"); + } + else + { + WriteMessage("There was a problem downloading " + filename); } return false; @@ -387,48 +448,53 @@ private async Task DownloadAsset(string filename, string destination) return true; } - private string BuildAssetUrl(string filename) + private static string BuildAssetUrl(string filename) { - if(Factory.GetGlobals().SettingsManager.GetConfig().use_custom_archive) { - var custom = Factory.GetGlobals().SettingsManager.GetConfig().custom_archive; + if (GlobalHelper.SettingsManager.GetConfig().use_custom_archive) + { + var custom = GlobalHelper.SettingsManager.GetConfig().custom_archive; Uri baseUrl = new Uri(custom["url"]); Uri url = new Uri(baseUrl, filename); return url.ToString(); - } else { - return ARCHIVE_BASE_URL + "/" + Factory.GetGlobals().SettingsManager.GetConfig().archive_name + "/" + filename; } + + return ARCHIVE_BASE_URL + "/" + GlobalHelper.SettingsManager.GetConfig().archive_name + "/" + filename; } private bool CheckCRC(string filepath) { - if(Factory.GetGlobals().ArchiveFiles == null || !Factory.GetGlobals().SettingsManager.GetConfig().crc_check) { + if (GlobalHelper.ArchiveFiles == null || !GlobalHelper.SettingsManager.GetConfig().crc_check) + { return true; } + string filename = Path.GetFileName(filepath); - archiveorg.File? file = Factory.GetGlobals().ArchiveFiles.GetFile(filename); - if(file == null) { + ArchiveFile file = GlobalHelper.ArchiveFiles.GetFile(filename); + + if (file == null) + { return true; //no checksum to compare to } - if(Util.CompareChecksum(filepath, file.crc32)) { + if (Util.CompareChecksum(filepath, file.crc32)) + { return true; } - _writeMessage(filename + ": Bad checksum!"); + WriteMessage(filename + ": Bad checksum!"); return false; } - //return false if a beta ley is required and missing or wrong - private bool CheckBetaMD5(Analogue.DataSlot slot, string platform) + // return false if a beta ley is required and missing or wrong + private bool CheckBetaMD5(DataSlot slot, string platform) { - if(slot.md5 != null && (betaSlotId != null && slot.id == betaSlotId)) { - string UpdateDirectory = Factory.GetGlobals().UpdateDirectory; - string path = Path.Combine(UpdateDirectory, "Assets", platform); + if (slot.md5 != null && (this.beta_slot_id != null && slot.id == this.beta_slot_id)) + { + string updateDirectory = GlobalHelper.UpdateDirectory; + string path = Path.Combine(updateDirectory, "Assets", platform); string filepath = Path.Combine(path, "common", slot.filename); - if(!File.Exists(filepath)) { - return false; - } - return Util.CompareChecksum(filepath, slot.md5, Util.HashTypes.MD5); + + return File.Exists(filepath) && Util.CompareChecksum(filepath, slot.md5, Util.HashTypes.MD5); } return true; @@ -436,115 +502,157 @@ private bool CheckBetaMD5(Analogue.DataSlot slot, string platform) public void BuildInstanceJSONs(bool overwrite = true) { - if(!buildInstances) { + if (!this.build_instances) + { return; } - string instancePackagerFile = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores", this.identifier, "instance-packager.json"); - if(!File.Exists(instancePackagerFile)) { + + string instancePackagerFile = Path.Combine(GlobalHelper.UpdateDirectory, "Cores", this.identifier, "instance-packager.json"); + + if (!File.Exists(instancePackagerFile)) + { return; } - _writeMessage("Building instance json files."); - InstancePackager packager = JsonSerializer.Deserialize(File.ReadAllText(instancePackagerFile)); - string commonPath = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Assets", packager.platform_id, "common"); - string outputDir = Path.Combine(Factory.GetGlobals().UpdateDirectory, packager.output); + + WriteMessage("Building instance json files."); + InstanceJsonPackager jsonPackager = JsonSerializer.Deserialize(File.ReadAllText(instancePackagerFile)); + string commonPath = Path.Combine(GlobalHelper.UpdateDirectory, "Assets", jsonPackager.platform_id, "common"); bool warning = false; - foreach(string dir in Directory.GetDirectories(commonPath, "*", SearchOption.AllDirectories)) { - Analogue.SimpleInstanceJSON instancejson = new Analogue.SimpleInstanceJSON(); - Analogue.SimpleInstance instance = new Analogue.SimpleInstance(); + + foreach (string dir in Directory.GetDirectories(commonPath, "*", SearchOption.AllDirectories)) + { + SimpleInstanceJSON simpleInstanceJson = new SimpleInstanceJSON(); + SimpleInstance instance = new SimpleInstance(); string dirName = Path.GetFileName(dir); - try { + + try + { instance.data_path = dir.Replace(commonPath + Path.DirectorySeparatorChar, "") + "/"; - List slots = new List(); + + List slots = new(); string jsonFileName = dirName + ".json"; - foreach(DataSlot slot in packager.data_slots) { + + foreach (InstancePackagerDataSlot slot in jsonPackager.data_slots) + { string[] files = Directory.GetFiles(dir, slot.filename); int index = slot.id; - switch(slot.sort) { + + switch (slot.sort) + { case "single": case "ascending": Array.Sort(files); break; + case "descending": - IComparer myComparer = new myReverserClass(); + IComparer myComparer = new ReverseComparer(); Array.Sort(files, myComparer); break; } - if(slot.required && files.Count() == 0) { + + if (slot.required && !files.Any()) + { throw new MissingRequiredInstanceFiles("Missing required files."); } - foreach(string file in files) { - if(File.GetAttributes(file).HasFlag(FileAttributes.Hidden)) { + + foreach (string file in files) + { + if (File.GetAttributes(file).HasFlag(FileAttributes.Hidden)) + { continue; } - Analogue.InstanceDataSlot current = new Analogue.InstanceDataSlot(); + + SimpleDataSlot current = new(); string filename = Path.GetFileName(file); - if(slot.as_filename) { + + if (slot.as_filename) + { jsonFileName = Path.GetFileNameWithoutExtension(file) + ".json"; } + current.id = index.ToString(); current.filename = filename; index++; slots.Add(current); } } - var limit = (JsonElement)packager.slot_limit["count"]; - if (slots.Count == 0 || (packager.slot_limit != null && slots.Count > limit.GetInt32())) { - _writeMessage("Unable to build " + jsonFileName); + + var limit = (JsonElement)jsonPackager.slot_limit["count"]; + + if (slots.Count == 0 || (jsonPackager.slot_limit != null && slots.Count > limit.GetInt32())) + { + WriteMessage("Unable to build " + jsonFileName); warning = true; continue; } + instance.data_slots = slots.ToArray(); - instancejson.instance = instance; - var options = new JsonSerializerOptions() - { - WriteIndented = true - }; + simpleInstanceJson.instance = instance; + + var options = new JsonSerializerOptions { WriteIndented = true }; string[] parts = dir.Split(commonPath); parts = parts[1].Split(jsonFileName.Remove(jsonFileName.Length - 5)); - string subdir = ""; - if(parts[0].Length > 1) { - subdir = parts[0].Trim(Path.DirectorySeparatorChar); + string subDirectory = string.Empty; + + if (parts[0].Length > 1) + { + subDirectory = parts[0].Trim(Path.DirectorySeparatorChar); } - string outputfile = Path.Combine(Factory.GetGlobals().UpdateDirectory, packager.output, subdir, jsonFileName); - if(!overwrite && File.Exists(outputfile)) { - _writeMessage(jsonFileName + " already exists."); - } else { - string json = JsonSerializer.Serialize(instancejson, options); - _writeMessage("Saving " + jsonFileName); - FileInfo file = new System.IO.FileInfo(outputfile); + + string outputFile = Path.Combine(GlobalHelper.UpdateDirectory, jsonPackager.output, subDirectory, jsonFileName); + + if (!overwrite && File.Exists(outputFile)) + { + WriteMessage(jsonFileName + " already exists."); + } + else + { + string json = JsonSerializer.Serialize(simpleInstanceJson, options); + + WriteMessage("Saving " + jsonFileName); + + FileInfo file = new FileInfo(outputFile); + file.Directory.Create(); // If the directory already exists, this method does nothing. - File.WriteAllText(outputfile, json); + + File.WriteAllText(outputFile, json); } - } catch(MissingRequiredInstanceFiles) { - //do nothin - } catch(Exception e) { - _writeMessage("Unable to build " + dirName); + } + catch (MissingRequiredInstanceFiles) + { + // Do nothing. + } + catch (Exception) + { + WriteMessage("Unable to build " + dirName); } } - if (warning) { - var message = (JsonElement)packager.slot_limit["message"]; - _writeMessage(message.GetString()); + + if (warning) + { + var message = (JsonElement)jsonPackager.slot_limit["message"]; + + WriteMessage(message.GetString()); } - _writeMessage("Finished"); + + WriteMessage("Finished"); } public bool CheckInstancePackager() { - string instancePackagerFile = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores", this.identifier, "instance-packager.json"); + string instancePackagerFile = Path.Combine(GlobalHelper.UpdateDirectory, "Cores", this.identifier, "instance-packager.json"); + return File.Exists(instancePackagerFile); } - public Analogue.DataJSON ReadDataJSON() + private DataJSON ReadDataJSON() { - string UpdateDirectory = Factory.GetGlobals().UpdateDirectory; - string coreDirectory = Path.Combine(UpdateDirectory, "Cores", this.identifier); + string updateDirectory = GlobalHelper.UpdateDirectory; + string coreDirectory = Path.Combine(updateDirectory, "Cores", this.identifier); string dataFile = Path.Combine(coreDirectory, "data.json"); - var options = new JsonSerializerOptions - { - Converters = { new StringConverter() } - }; + var options = new JsonSerializerOptions { Converters = { new StringConverter() } }; - Analogue.DataJSON data = JsonSerializer.Deserialize(File.ReadAllText(dataFile), options); + DataJSON data = JsonSerializer.Deserialize(File.ReadAllText(dataFile), options); return data; } @@ -552,28 +660,35 @@ public Analogue.DataJSON ReadDataJSON() public bool JTBetaCheck() { var data = ReadDataJSON(); - bool check = data.data.data_slots.Any(x=>x.name=="JTBETA"); + bool check = data.data.data_slots.Any(x => x.name == "JTBETA"); + + if (check) + { + var slot = data.data.data_slots.First(x => x.name == "JTBETA"); - if (check) { - var slot = data.data.data_slots.Where(x=>x.name=="JTBETA").First(); - betaSlotId = slot.id; - betaSlotPlatformIdIndex = slot.getPlatformIdIndex(); + this.beta_slot_id = slot.id; + this.beta_slot_platform_id_index = slot.GetPlatformIdIndex(); } return check; } - public async Task ReplaceCheck() + public void ReplaceCheck() { - var replaces = this.getSubstitutes(); - if (replaces != null) { - foreach(var replacement in replaces) { + var replaces = this.GetSubstitutes(); + + if (replaces != null) + { + foreach (var replacement in replaces) + { string identifier = $"{replacement.author}.{replacement.shortname}"; - Core c = new Core(){identifier = identifier, platform_id = replacement.platform_id}; - if (c.isInstalled()) { + Core c = new Core { identifier = identifier, platform_id = replacement.platform_id }; + + if (c.IsInstalled()) + { Replace(c); c.Uninstall(); - _writeMessage($"Uninstalled {identifier}. It was replaced by this core."); + WriteMessage($"Uninstalled {identifier}. It was replaced by this core."); } } } @@ -581,72 +696,79 @@ public async Task ReplaceCheck() private void Replace(Core core) { - string root = Factory.GetGlobals().UpdateDirectory; + string root = GlobalHelper.UpdateDirectory; string path = Path.Combine(root, "Assets", core.platform_id, core.identifier); - if(Directory.Exists(path)) { + + if (Directory.Exists(path)) + { Directory.Move(path, Path.Combine(root, "Assets", core.platform_id, this.identifier)); } - + path = Path.Combine(root, "Saves", core.platform_id, core.identifier); - if(Directory.Exists(path)) { + if (Directory.Exists(path)) + { Directory.Move(path, Path.Combine(root, "Saves", core.platform_id, this.identifier)); } path = Path.Combine(root, "Settings", core.identifier); - if(Directory.Exists(path)) { + if (Directory.Exists(path)) + { Directory.Move(path, Path.Combine(root, "Settings", this.identifier)); } } - public async Task GetVideoConfig() + public Video GetVideoConfig() { - checkUpdateDirectory(); - string file = Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores", this.identifier, "video.json"); + CheckUpdateDirectory(); + + string file = Path.Combine(GlobalHelper.UpdateDirectory, "Cores", this.identifier, "video.json"); + if (!File.Exists(file)) { return null; } + string json = File.ReadAllText(file); - var options = new JsonSerializerOptions() - { - AllowTrailingCommas = true - }; - Analogue.Cores.Video.Video? config = JsonSerializer.Deserialize>(json, options)["video"]; + var options = new JsonSerializerOptions { AllowTrailingCommas = true }; + Video config = JsonSerializer.Deserialize>(json, options)["video"]; return config; } - public async Task AddDisplayModes() + public void AddDisplayModes() { - var info = this.getConfig(); - var video = await GetVideoConfig(); - List all = new List(); - foreach(string id in allModes) { - all.Add(new Analogue.Cores.Video.DisplayMode{id = id}); - } - if(info.metadata.platform_ids.Contains("gb")) { - foreach(string id in gbModes) { - all.Add(new Analogue.Cores.Video.DisplayMode{id = id}); + var info = this.GetConfig(); + var video = GetVideoConfig(); + List all = new List(); + + foreach (string id in ALL_MODES) + { + all.Add(new DisplayMode { id = id }); + } + + if (info.metadata.platform_ids.Contains("gb")) + { + foreach (string id in GB_MODES) + { + all.Add(new DisplayMode { id = id }); } } + video.display_modes = all; - Dictionary output = new Dictionary(); - output.Add("video", video); - var options = new JsonSerializerOptions() - { - WriteIndented = true - }; + Dictionary output = new Dictionary { { "video", video } }; + var options = new JsonSerializerOptions { WriteIndented = true }; string json = JsonSerializer.Serialize(output, options); - - File.WriteAllText(Path.Combine(Factory.GetGlobals().UpdateDirectory, "Cores", this.identifier, "video.json"), json); + + File.WriteAllText(Path.Combine(GlobalHelper.UpdateDirectory, "Cores", this.identifier, "video.json"), json); } } -public class myReverserClass : IComparer { - - // Calls CaseInsensitiveComparer.Compare with the parameters reversed. - int IComparer.Compare( Object x, Object y ) { - return( (new CaseInsensitiveComparer()).Compare( y, x ) ); - } - } +public class ReverseComparer : IComparer +{ + // Calls CaseInsensitiveComparer.Compare with the parameters reversed. + int IComparer.Compare(object x, object y) + { + return new CaseInsensitiveComparer().Compare(y, x); + } +} diff --git a/src/models/Github/GithubAsset.cs b/src/models/Github/GithubAsset.cs index bc882c3f..2c2deb9f 100644 --- a/src/models/Github/GithubAsset.cs +++ b/src/models/Github/GithubAsset.cs @@ -1,9 +1,9 @@ -namespace Github; +namespace Pannella.Models.Github; public class Asset { - public string? url {get; set;} - public string? name {get; set;} - public string? content_type {get; set;} - public string? browser_download_url {get; set;} -} \ No newline at end of file + public string url {get; set;} + public string name {get; set;} + public string content_type {get; set;} + public string browser_download_url {get; set;} +} diff --git a/src/models/Github/GithubFile.cs b/src/models/Github/GithubFile.cs index 7dde4b7c..a225ac54 100644 --- a/src/models/Github/GithubFile.cs +++ b/src/models/Github/GithubFile.cs @@ -1,12 +1,12 @@ -namespace Github; +namespace Pannella.Models.Github; public class File { - public string? name {get; set;} - public string? path {get; set;} - public string? sha {get; set;} + public string name {get; set;} + public string path {get; set;} + public string sha {get; set;} public int? size {get; set;} - public string? url {get; set;} - public string? html_url {get; set;} - public string? download_url {get; set;} -} \ No newline at end of file + public string url {get; set;} + public string html_url {get; set;} + public string download_url {get; set;} +} diff --git a/src/models/Github/GithubRelease.cs b/src/models/Github/GithubRelease.cs index c21e1d79..8bf4be1c 100644 --- a/src/models/Github/GithubRelease.cs +++ b/src/models/Github/GithubRelease.cs @@ -1,12 +1,12 @@ -namespace Github; +namespace Pannella.Models.Github; public class Release { - public string? tag_name { get; set; } - public string? name { get; set; } + public string tag_name { get; set; } + public string name { get; set; } public bool prerelease { get; set; } - public string? url { get; set; } - public List? assets { get; set; } + public string url { get; set; } + public List assets { get; set; } public bool draft { get; set; } - public string? html_url { get; set; } -} \ No newline at end of file + public string html_url { get; set; } +} diff --git a/src/models/ImagePack.cs b/src/models/ImagePack.cs index 7197d157..6c7e0032 100644 --- a/src/models/ImagePack.cs +++ b/src/models/ImagePack.cs @@ -1,74 +1,98 @@ -namespace pannella.analoguepocket; -using System.IO; using System.IO.Compression; +using Pannella.Helpers; +using Pannella.Models.Github; +using Pannella.Services; +using File = System.IO.File; + +namespace Pannella.Models; public class ImagePack { public string owner { get; set; } public string repository { get; set; } - public string? variant { get; set; } - + public string variant { get; set; } - public async Task Install(string path) + public async Task Install(string path) { - string filepath = await fetchImagePack(path); - return await installImagePack(path, filepath); + string filepath = await this.FetchImagePack(path); + + InstallImagePack(path, filepath); } - private async Task fetchImagePack(string path) + private async Task FetchImagePack(string path) { - Github.Release release = await GithubApi.GetLatestRelease(this.owner, this.repository); + Release release = await GithubApiService.GetLatestRelease(this.owner, this.repository); string localFile = Path.Combine(path, "imagepack.zip"); - string downloadUrl = ""; - if(release.assets == null) { + string downloadUrl = string.Empty; + + if (release.assets == null) + { throw new Exception("Github Release contains no assets"); } - if(this.variant == null) { + + if (this.variant == null) + { downloadUrl = release.assets[0].browser_download_url; - } else { - foreach(Github.Asset asset in release.assets) { - if(asset.name.Contains(this.variant)) { - downloadUrl = asset.browser_download_url; - } + } + else + { + foreach (var asset in release.assets.Where(asset => asset.name.Contains(this.variant))) + { + downloadUrl = asset.browser_download_url; } } - if(downloadUrl != "") { + + if (downloadUrl != string.Empty) + { Console.WriteLine("Downloading image pack..."); - await Factory.GetHttpHelper().DownloadFileAsync(downloadUrl, localFile); + + await HttpHelper.Instance.DownloadFileAsync(downloadUrl, localFile); + Console.WriteLine("Download complete."); + return localFile; } - return ""; + + return string.Empty; } - private async Task installImagePack(string path, string filepath) + private static void InstallImagePack(string path, string filepath) { Console.WriteLine("Installing..."); + string extractPath = Path.Combine(path, "temp"); + ZipFile.ExtractToDirectory(filepath, extractPath, true); + string imagePack = FindImagePack(extractPath); string target = Path.Combine(path, "Platforms", "_images"); + Util.CopyDirectory(imagePack, target, false, true); Directory.Delete(extractPath, true); File.Delete(filepath); - Console.WriteLine("All Done"); - return true; + Console.WriteLine("All Done"); } - private string FindImagePack(string temp) + private static string FindImagePack(string temp) { string path = Path.Combine(temp, "Platforms", "_images"); - if(Directory.Exists(path)) { + + if (Directory.Exists(path)) + { return path; } - foreach(string d in Directory.EnumerateDirectories(temp)) { + foreach (string d in Directory.EnumerateDirectories(temp)) + { path = Path.Combine(d, "Platforms", "_images"); - if(Directory.Exists(path)) { + + if (Directory.Exists(path)) + { return path; } } + throw new Exception("Can't find image pack"); } } diff --git a/src/models/InstancePackager/DataSlot.cs b/src/models/InstancePackager/DataSlot.cs index 5bf0da94..f427cde9 100644 --- a/src/models/InstancePackager/DataSlot.cs +++ b/src/models/InstancePackager/DataSlot.cs @@ -1,4 +1,4 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models.InstancePackager; public class DataSlot { @@ -7,4 +7,4 @@ public class DataSlot public bool required { get; set; } public string sort { get; set; } = "ascending"; public bool as_filename { get; set; } -} \ No newline at end of file +} diff --git a/src/models/InstancePackager/InstancePackager.cs b/src/models/InstancePackager/InstanceJsonPackager.cs similarity index 71% rename from src/models/InstancePackager/InstancePackager.cs rename to src/models/InstancePackager/InstanceJsonPackager.cs index 2c30fd28..97b6cc27 100644 --- a/src/models/InstancePackager/InstancePackager.cs +++ b/src/models/InstancePackager/InstanceJsonPackager.cs @@ -1,9 +1,9 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models.InstancePackager; -public class InstancePackager +public class InstanceJsonPackager { public List data_slots { get; set; } public string output { get; set; } public string platform_id { get; set; } public Dictionary slot_limit { get; set; } -} \ No newline at end of file +} diff --git a/src/models/Platform.cs b/src/models/Platform.cs index 6e237c59..df94935f 100644 --- a/src/models/Platform.cs +++ b/src/models/Platform.cs @@ -1,4 +1,4 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models; public class Platform { @@ -6,4 +6,4 @@ public class Platform public string name { get; set; } public int year { get; set; } public string manufacturer { get; set; } -} \ No newline at end of file +} diff --git a/src/models/Repo.cs b/src/models/Repo.cs index aee7ab37..75240eb0 100644 --- a/src/models/Repo.cs +++ b/src/models/Repo.cs @@ -1,8 +1,8 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models; public class Repo { - public string? name { get; set; } - public string? owner { get; set; } - public string? platform { get; set; } -} \ No newline at end of file + public string name { get; set; } + public string owner { get; set; } + public string platform { get; set; } +} diff --git a/src/models/Settings/Config.cs b/src/models/Settings/Config.cs index 74787984..0772406c 100644 --- a/src/models/Settings/Config.cs +++ b/src/models/Settings/Config.cs @@ -1,43 +1,26 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models.Settings; public class Config { - public bool download_assets { get; set; } - public string archive_name { get; set; } - public string? github_token { get; set; } - public bool download_firmware { get; set; } - public bool core_selector { get; set; } - public bool preserve_platforms_folder { get; set; } - public bool delete_skipped_cores { get; set; } - public string? download_new_cores { get; set; } - public bool build_instance_jsons { get; set; } - public bool crc_check { get; set; } - public bool fix_jt_names { get; set; } - public bool skip_alternative_assets { get; set; } + public bool download_assets { get; set; } = true; + public string archive_name { get; set; } = "openFPGA-Files"; + public string github_token { get; set; } = string.Empty; + public bool download_firmware { get; set; } = true; + public bool core_selector { get; set; } = true; + public bool preserve_platforms_folder { get; set; } = false; + public bool delete_skipped_cores { get; set; } = true; + public string download_new_cores { get; set; } + public bool build_instance_jsons { get; set; } = true; + public bool crc_check { get; set; } = true; + public bool fix_jt_names { get; set; } = true; + public bool skip_alternative_assets { get; set; } = true; public bool backup_saves { get; set; } - public string? backup_saves_location { get; set; } - public bool use_custom_archive { get; set; } - public Dictionary custom_archive { get; set; } + public string backup_saves_location { get; set; } = "Backups"; + public bool use_custom_archive { get; set; } = false; - public Config() + public Dictionary custom_archive { get; set; } = new() { - download_assets = true; - download_firmware = true; - archive_name = "openFPGA-Files"; - core_selector = true; - preserve_platforms_folder = false; - delete_skipped_cores = true; - download_new_cores = null; - build_instance_jsons = true; - crc_check = true; - fix_jt_names = true; - skip_alternative_assets = true; - backup_saves = false; - backup_saves_location = "Backups"; - use_custom_archive = false; - custom_archive = new Dictionary() { - {"url", "https://updater.retrodriven.com"}, - {"index", "updater.php"} - }; - } -} \ No newline at end of file + { "url", "https://updater.retrodriven.com" }, + { "index", "updater.php" } + }; +} diff --git a/src/models/Settings/CoreSettings.cs b/src/models/Settings/CoreSettings.cs index 493e717c..fefb23c9 100644 --- a/src/models/Settings/CoreSettings.cs +++ b/src/models/Settings/CoreSettings.cs @@ -1,15 +1,8 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models.Settings; public class CoreSettings { public bool skip { get; set; } - public bool download_assets { get; set; } - public bool platform_rename { get; set; } - - public CoreSettings() - { - skip = false; - download_assets = true; - platform_rename = true; - } -} \ No newline at end of file + public bool download_assets { get; set; } = true; + public bool platform_rename { get; set; } = true; +} diff --git a/src/models/Settings/Firmware.cs b/src/models/Settings/Firmware.cs index 3e8c37ec..971529fa 100644 --- a/src/models/Settings/Firmware.cs +++ b/src/models/Settings/Firmware.cs @@ -1,11 +1,6 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models.Settings; public class Firmware { - public string version { get; set; } - - public Firmware() - { - version = ""; - } -} \ No newline at end of file + public string version { get; set; } = string.Empty; +} diff --git a/src/models/Settings/Settings.cs b/src/models/Settings/Settings.cs index 9bdf5f34..6367f472 100644 --- a/src/models/Settings/Settings.cs +++ b/src/models/Settings/Settings.cs @@ -1,15 +1,8 @@ -namespace pannella.analoguepocket; +namespace Pannella.Models.Settings; public class Settings { - public Firmware firmware { get; set; } - public Config config { get; set; } - public Dictionary coreSettings { get; set; } - - public Settings() - { - firmware = new Firmware(); - config = new Config(); - coreSettings = new Dictionary(); - } -} \ No newline at end of file + public Firmware firmware { get; set; } = new(); + public Config config { get; set; } = new(); + public Dictionary coreSettings { get; set; } = new(); +} diff --git a/src/models/Sponsor.cs b/src/models/Sponsor.cs index ccff8403..215d4d15 100644 --- a/src/models/Sponsor.cs +++ b/src/models/Sponsor.cs @@ -1,15 +1,45 @@ -namespace pannella.analoguepocket; +using System.Text; + +namespace Pannella.Models; public class Sponsor { - public string? community_bridge { get; set; } - public List? github { get; set; } - public string? issuehunt { get; set; } - public string? ko_fi { get; set; } - public string? liberapay { get; set; } - public string? open_collective { get; set; } - public string? otechie { get; set; } - public string? patreon { get; set; } - public string? tidelift { get; set; } - public List? custom { get; set; } -} \ No newline at end of file + public string community_bridge { get; set; } + public List github { get; set; } + public string issuehunt { get; set; } + public string ko_fi { get; set; } + public string liberapay { get; set; } + public string open_collective { get; set; } + public string otechie { get; set; } + public string patreon { get; set; } + public string tidelift { get; set; } + public List custom { get; set; } + + public override string ToString() + { + var links = new StringBuilder(); + var properties = typeof(Sponsor).GetProperties(); + + foreach (var prop in properties) + { + object value = prop.GetValue(this, null); + + if (value == null) continue; + + links.AppendLine(); + + if (value.GetType() == typeof(List)) + { + var stringArray = (List)value; + + links.Append(string.Join(Environment.NewLine, stringArray)); + } + else if (value is string) + { + links.Append(value); + } + } + + return links.ToString(); + } +} diff --git a/src/models/Updater/Substitute.cs b/src/models/Updater/Substitute.cs index ddea0457..01c94d24 100644 --- a/src/models/Updater/Substitute.cs +++ b/src/models/Updater/Substitute.cs @@ -1,8 +1,8 @@ -namespace pannella.analoguepocket.Updater; +namespace Pannella.Models.Updater; public class Substitute { public string shortname { get; set; } public string author { get; set; } public string platform_id { get; set; } -} \ No newline at end of file +} diff --git a/src/models/Updater/Updaters.cs b/src/models/Updater/Updaters.cs index 98447eeb..c30c891a 100644 --- a/src/models/Updater/Updaters.cs +++ b/src/models/Updater/Updaters.cs @@ -1,6 +1,6 @@ -namespace pannella.analoguepocket.Updater; +namespace Pannella.Models.Updater; public class Updaters { public Substitute[] previous { get; set; } -} \ No newline at end of file +} diff --git a/src/options/AssetsOptions.cs b/src/options/AssetsOptions.cs new file mode 100644 index 00000000..57700bea --- /dev/null +++ b/src/options/AssetsOptions.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("assets", HelpText = "Run the asset downloader")] +public class AssetsOptions +{ + [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] + public string InstallPath { get; set; } + + [Option ('c', "core", Required = false, HelpText = "The core you want to download assets for.")] + public string CoreName { get; set; } +} diff --git a/src/options/BackupSavesOptions.cs b/src/options/BackupSavesOptions.cs new file mode 100644 index 00000000..2510ebfc --- /dev/null +++ b/src/options/BackupSavesOptions.cs @@ -0,0 +1,16 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("backup-saves", HelpText = "Create a compressed zip file of the Saves directory.")] +public class BackupSavesOptions +{ + [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] + public string InstallPath { get; set; } + + [Option('l', "location", HelpText = "Absolute path to backup location", Required = true)] + public string BackupPath { get; set; } = null!; + + [Option('s', "save", HelpText = "Save settings to the config file", Required = false)] + public bool Save { get; set; } +} diff --git a/src/options/FirmwareOptions.cs b/src/options/FirmwareOptions.cs new file mode 100644 index 00000000..8c1ce1c1 --- /dev/null +++ b/src/options/FirmwareOptions.cs @@ -0,0 +1,10 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("firmware", HelpText = "Check for Pocket firmware updates")] +public class FirmwareOptions +{ + [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] + public string InstallPath { get; set; } +} diff --git a/src/options/FundOptions.cs b/src/options/FundOptions.cs new file mode 100644 index 00000000..606e260a --- /dev/null +++ b/src/options/FundOptions.cs @@ -0,0 +1,10 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("fund", HelpText = "List sponsor links")] +public class FundOptions +{ + [Option('c', "core", HelpText = "The core to check funding links for", Required = false)] + public string Core { get; set; } +} diff --git a/src/options/ImagesOptions.cs b/src/options/ImagesOptions.cs new file mode 100644 index 00000000..5b3e1d1c --- /dev/null +++ b/src/options/ImagesOptions.cs @@ -0,0 +1,19 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("images", HelpText = "Download image packs")] +public class ImagesOptions +{ + [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] + public string InstallPath { get; set; } + + [Option('o', "owner", Required = true, HelpText = "Image pack repo username")] + public string ImagePackOwner { get; set; } + + [Option('i', "imagepack", Required = true, HelpText = "Github repo name for image pack")] + public string ImagePackRepo { get; set; } + + [Option('v', "variant", Required = false, HelpText = "The optional variant")] + public string ImagePackVariant { get; set; } +} diff --git a/src/options/InstanceGeneratorOptions.cs b/src/options/InstanceGeneratorOptions.cs new file mode 100644 index 00000000..8f6862c3 --- /dev/null +++ b/src/options/InstanceGeneratorOptions.cs @@ -0,0 +1,10 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("instancegenerator", HelpText = "Run the instance JSON generator")] +public class InstanceGeneratorOptions +{ + [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] + public string InstallPath { get; set; } +} diff --git a/src/options/MenuOptions.cs b/src/options/MenuOptions.cs new file mode 100644 index 00000000..899d23f6 --- /dev/null +++ b/src/options/MenuOptions.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("menu", isDefault: true, HelpText = "Interactive Main Menu")] +public class MenuOptions +{ + [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] + public string InstallPath { get; set; } + + [Option('s', "skip-update", HelpText = "Skip the self update check", Required = false)] + public bool SkipUpdate { get; set; } +} diff --git a/src/options/UninstallOptions.cs b/src/options/UninstallOptions.cs new file mode 100644 index 00000000..86357ba3 --- /dev/null +++ b/src/options/UninstallOptions.cs @@ -0,0 +1,16 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("uninstall", HelpText = "Delete a core")] +public class UninstallOptions +{ + [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] + public string InstallPath { get; set; } + + [Option ('c', "core", Required = true, HelpText = "The core you want to delete.")] + public string CoreName { get; set; } + + [Option('a', "assets", Required = false, HelpText = "Delete the core specific Assets folder")] + public bool DeleteAssets { get; set; } +} diff --git a/src/options/UpdateOptions.cs b/src/options/UpdateOptions.cs new file mode 100644 index 00000000..fd198618 --- /dev/null +++ b/src/options/UpdateOptions.cs @@ -0,0 +1,20 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("update", HelpText = "Run update all. (You can configure via the settings menu)")] +public class UpdateOptions +{ + [Option('p', "path", HelpText = "Absolute path to install location", Required = false)] + public string InstallPath { get; set; } + + [Option ('c', "core", Required = false, HelpText = "The core you want to update.")] + public string CoreName { get; set; } + + [Option('f', "platformsfolder", Required = false, HelpText = "Preserve the Platforms folder, so customizations aren't overwritten by updates.")] + public bool PreservePlatformsFolder { get; set; } + + [Option('r', "clean", Required = false, HelpText = "Clean install. Remove all existing core files, before updating")] + public bool CleanInstall { get; set; } + +} diff --git a/src/options/UpdateSelfOptions.cs b/src/options/UpdateSelfOptions.cs new file mode 100644 index 00000000..dc2e7b31 --- /dev/null +++ b/src/options/UpdateSelfOptions.cs @@ -0,0 +1,6 @@ +using CommandLine; + +namespace Pannella.Options; + +[Verb("update-self", HelpText = "Update this utility")] +public class UpdateSelfOptions { } diff --git a/src/partials/Program.Constants.cs b/src/partials/Program.Constants.cs new file mode 100644 index 00000000..bc082179 --- /dev/null +++ b/src/partials/Program.Constants.cs @@ -0,0 +1,94 @@ +using System.Reflection; + +namespace Pannella; + +internal partial class Program +{ + private static readonly string VERSION = Assembly.GetExecutingAssembly().GetName().Version.ToString(3); + + private static readonly string SYSTEM_OS_PLATFORM = GetSystemPlatform(); + + private const string USER = "mattpannella"; + + private const string REPOSITORY = "pocket-updater-utility"; + + private const string RELEASE_URL = "https://github.com/mattpannella/pocket-updater-utility/releases/download/{0}/pupdate_{1}.zip"; + + private static readonly string[] WELCOME_MESSAGES = + { + @" + _____ _ _ ___ _____ _ +| __ | |___ _____ ___ _ _ ___ _ _ ___ ___ ___| | _| ___ ___ | __|___ _| | +| __ -| | .'| | -_| | | | . | | | _|_ -| -_| | _| | . | _| | | | . | . | +|_____|_|__,|_|_|_|___| |_ |___|___|_| |___|___|_|_| |___|_| |_____|___|___| + |___| ", + @" + _ _ _ _ _ _____ _ _ +| | | |___| |___ ___ _____ ___ | |_ ___ | __| |___ _ _ ___ ___| |_ ___ _ _ _ ___ +| | | | -_| | _| . | | -_| | _| . | | __| | .'| | | . | _| _| . | | | | | +|_____|___|_|___|___|_|_|_|___| |_| |___| |__| |_|__,|\_/|___|_| |_| |___|_____|_|_| + ", + @" + _ ___ _ _ _ _ + ___| |_ ___ | _|_|___ _| |___ _ _ ___ _ _ ___ ___ _ _ ___| |_ _ _ _| |___ _ _ ___ +|_ -| | -_| | _| | | . |_ -| | | | . | | | | _| _| | |_ -| _| | | | . | .'| | | -_| +|___|_|_|___| |_| |_|_|_|___|___| |_ |___|___| |___|_| |___|___|_| |_ | |___|__,|\_/|___| + |___| |___| ", + @" + _____ _ _ _ ___ _ _ +|_ _| |_|_|___ |_|___ ___ | _|___ ___ ___ _ _ ___ ___ ___| |_ ___ _ _ ___ ___ ___| |_ + | | | | |_ -| | |_ -| | .'| | _| .'| | _| | | | _| -_|_ -| _| .'| | | _| .'| | _| + |_| |_|_|_|___| |_|___| |__,| |_| |__,|_|_|___|_ | |_| |___|___|_| |__,|___|_| |__,|_|_|_| + |___| ", + @" __ + _ _ _ _ _ _ _ _____ _ _ _____ _ _ | | +| | | |___| |___ ___ _____ ___ | |_ ___ | |_| |_ ___ | __ | |___ ___| |_ | |___ ___| |_ ___| |_| | +| | | | -_| | _| . | | -_| | _| . | | _| | -_| | __ -| | .'| _| '_| | | | | .'| _| '_| -_| _|__| +|_____|___|_|___|___|_|_|_|___| |_| |___| |_| |_|_|___| |_____|_|__,|___|_,_| |_|_|_|__,|_| |_,_|___|_| |__| + ", + @" + _____ _ _ _____ _ +| | |_ ___| |_ _ _ |_ _|___ ___| |_ _ _ +|- -| _| _| | | |_ | | | .'|_ -| _| | |_ +|_____|_| |___|_|_|_ |_| |_| |__,|___|_| |_ |_| + |___| |___| ", + @" _ _____ + _ _ _ _ _ | | _ _ |___ | +| | | | |_ ___| |_|_|___ ___ _ _ ___ | |_ _ _ _ _|_|___| _| +| | | | | .'| _| | _| -_| | | | .'| | . | | | | | | |_| +|_____|_|_|__,|_| |_| |___| |_ |__,| |___|___|_ |_|_|_|_| + |___| |___| ", + @" _____ + _ _ _ _ _ _ |___ | +| | | | |_ ___| |_ |_|___ ___ _____ ___ ___| _| +| | | | | .'| _| | |_ -| | .'| | | .'| |_| +|_____|_|_|__,|_| |_|___| |__,| |_|_|_|__,|_|_|_| + ", + @" + _____ _ _ _____ _ _ _ +| __|_|___ ___|_|___ ___ | |___|_| |___ _| | +| __| |_ -|_ -| | . | | | | | | .'| | | -_| . | +|__| |_|___|___|_|___|_|_| |_|_|_|__,|_|_|___|___| + ", + @" + _____ _ +| | |_____| |_ ___ ___ ___ +| | | | . | .'|_ -| .'| +|_____|_|_|_|___|__,|___|__,| + ", + @" _ + __ _ | | +| | ___| |_|_|___ _____ ___ ___ ___ _ _ +| |__| -_| _| |_ -| | | . |_ -| -_| | | +|_____|___|_| |___| |_|_|_|___|___|___|_ | + |___|", + @" _=,_ + o_/6 /#\ + \__ |##/ + ='|--\ + / #'-. + \#|_ _'-. / + |/ \_( # |'' + C/ ,--___/" + }; +} diff --git a/src/partials/Program.GameAndWatch.cs b/src/partials/Program.GameAndWatch.cs new file mode 100644 index 00000000..c8ae0c97 --- /dev/null +++ b/src/partials/Program.GameAndWatch.cs @@ -0,0 +1,103 @@ +using System.Diagnostics; +using System.IO.Compression; +using Pannella.Helpers; +using Pannella.Models.Github; +using Pannella.Services; +using File = System.IO.File; +using GithubAsset = Pannella.Models.Github.Asset; + +namespace Pannella; + +internal partial class Program +{ + private static async Task BuildGameAndWatchRoms(string directory) + { + Release release = await GithubApiService.GetLatestRelease("agg23", "fpga-gameandwatch"); + + foreach (GithubAsset asset in release.assets) + { + if (asset.name.EndsWith("Tools.zip")) + { + string downloadPath = Path.Combine(directory, "tools", "gameandwatch"); + string filename = Path.Combine(downloadPath, asset.name); + + if (!File.Exists(filename)) + { + Directory.CreateDirectory(downloadPath); + await HttpHelper.Instance.DownloadFileAsync(asset.browser_download_url, filename); + ZipFile.ExtractToDirectory(filename, downloadPath, true); + } + + break; + } + } + + string execName = "fpga-gnw-romgenerator"; + string execLocation = Path.Combine(directory, "tools", "gameandwatch"); + string manifestPath = Path.Combine(directory, "tools", "gameandwatch"); + + switch (SYSTEM_OS_PLATFORM) + { + case "win": + execName += ".exe"; + execLocation = Path.Combine(execLocation, "windows", execName); + manifestPath = Path.Combine(manifestPath, "windows", "manifest.json"); + break; + + case "mac": + execLocation = Path.Combine(execLocation, "mac", execName); + manifestPath = Path.Combine(manifestPath, "mac", "manifest.json"); + Exec($"chmod +x {execLocation}"); + break; + + default: + execLocation = Path.Combine(execLocation, "linux", execName); + manifestPath = Path.Combine(manifestPath, "linux", "manifest.json"); + Exec($"chmod +x {execLocation}"); + break; + } + + string romLocation = Path.Combine(directory, "Assets", "gameandwatch", "agg23.GameAndWatch"); + string outputLocation = Path.Combine(directory, "Assets", "gameandwatch", "common"); + + try + { + // Execute + Console.WriteLine($"Executing {execLocation}"); + + ProcessStartInfo pInfo = new ProcessStartInfo(execLocation) + { + Arguments = $"--mame-path \"{romLocation}\" --output-path \"{outputLocation}\" --manifest-path \"{manifestPath}\" supported", + UseShellExecute = false + }; + + Process p = Process.Start(pInfo); + + p.WaitForExit(); + } + catch (Exception e) + { + Console.Error.WriteLine($"An error occurred: {e.GetType().Name} : {e}"); + } + } + + private static void Exec(string cmd) + { + var escapedArgs = cmd.Replace("\"", "\\\""); + + using var process = new Process(); + + process.StartInfo = new ProcessStartInfo + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"" + }; + + process.Start(); + process.WaitForExit(); + } +} diff --git a/src/partials/Program.Helpers.cs b/src/partials/Program.Helpers.cs new file mode 100644 index 00000000..b9567623 --- /dev/null +++ b/src/partials/Program.Helpers.cs @@ -0,0 +1,115 @@ +using System.Runtime.InteropServices; +using Pannella.Helpers; +using Pannella.Services; +using GithubRelease = Pannella.Models.Github.Release; + +namespace Pannella; + +internal partial class Program +{ + // return true if newer version is available + private static async Task CheckVersion(string path) + { + try + { + List releases = await GithubApiService.GetReleases(USER, REPOSITORY); + + string tag_name = releases[0].tag_name; + string v = SemverUtil.FindSemver(tag_name); + + if (v != null) + { + bool check = SemverUtil.SemverCompare(v, VERSION); + + if (check) + { + Console.WriteLine("A new version is available. Downloading now..."); + + string url = string.Format(RELEASE_URL, tag_name, SYSTEM_OS_PLATFORM); + string saveLocation = Path.Combine(path, "pupdate.zip"); + + await HttpHelper.Instance.DownloadFileAsync(url, saveLocation); + + Console.WriteLine("Download complete."); + Console.WriteLine(saveLocation); + Console.WriteLine("Go to " + releases[0].html_url + " for a change log"); + } + else + { + Console.WriteLine("Up to date."); + } + + return check; + } + + return false; + } + catch (HttpRequestException e) + { + Console.WriteLine(e); + return false; + } + } + + private static void FunFacts() + { + if (GlobalHelper.InstalledCores.Count == 0) + { + return; + } + + string[] sleepSupported = GlobalHelper.InstalledCores + .Where(c => c.GetConfig().framework.sleep_supported) + .Select(c => c.identifier) + .ToArray(); + string list = string.Join(", ", sleepSupported); + + Console.WriteLine(); + Console.WriteLine("Fun fact! The ONLY cores that support save states and sleep are the following:"); + Console.WriteLine(list); + Console.WriteLine("Please don't bother the developers of the other cores about this feature. It's a lot of work and most likely will not be coming."); + } + + private static string GetSystemPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "win"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "mac"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Architecture arch = RuntimeInformation.ProcessArchitecture; + + return arch switch + { + Architecture.Arm64 => "linux_arm64", + Architecture.Arm => "linux_arm32", + _ => "linux" + }; + } + + return string.Empty; + } + + private static void Pause() + { + if (!CLI_MODE) + { + Console.WriteLine("Press any key to continue."); + Console.ReadKey(true); + } + } + + private static void PauseExit(int exitCode = 0) + { + Console.WriteLine("Press any key to exit."); + Console.ReadKey(true); // wait for input so the console doesn't auto close in windows + Environment.Exit(exitCode); + } +} diff --git a/src/partials/Program.ImagePack.cs b/src/partials/Program.ImagePack.cs new file mode 100644 index 00000000..ecd6877d --- /dev/null +++ b/src/partials/Program.ImagePack.cs @@ -0,0 +1,67 @@ +using ConsoleTools; +using Pannella.Models; +using Pannella.Services; + +namespace Pannella; + +internal partial class Program +{ + private static async Task ImagePackSelector(string path) + { + Console.Clear(); + Console.WriteLine("Checking for image packs...\n"); + + ImagePack[] packs = await ImagePacksService.GetImagePacks(); + + if (packs.Length > 0) + { + int choice = 0; + var menu = new ConsoleMenu() + .Configure(config => + { + config.Selector = "=>"; + config.EnableWriteTitle = false; + //config.EnableBreadcrumb = true; + config.WriteHeaderAction = () => Console.WriteLine("So, what'll it be?:"); + config.SelectedItemBackgroundColor = Console.ForegroundColor; + config.SelectedItemForegroundColor = Console.BackgroundColor; + }); + + foreach (var pack in packs) + { + menu.Add($"{pack.owner}: {pack.repository} {pack.variant}", thisMenu => + { + choice = thisMenu.CurrentItem.Index; + thisMenu.CloseMenu(); + }); + } + + menu.Add("Go Back", thisMenu => + { + choice = packs.Length; + thisMenu.CloseMenu(); + }); + + menu.Show(); + + if (choice < packs.Length && choice >= 0) + { + await packs[choice].Install(path); + Pause(); + } + else if (choice == packs.Length) + { + } + else + { + Console.WriteLine("You fucked up."); + Pause(); + } + } + else + { + Console.WriteLine("None found. Have a nice day."); + Pause(); + } + } +} diff --git a/src/partials/Program.InstanceGenerator.cs b/src/partials/Program.InstanceGenerator.cs new file mode 100644 index 00000000..b702d6a9 --- /dev/null +++ b/src/partials/Program.InstanceGenerator.cs @@ -0,0 +1,22 @@ +namespace Pannella; + +internal partial class Program +{ + private static void RunInstanceGenerator(PocketCoreUpdater pocketCoreUpdater, bool force = false) + { + if (!force) + { + Console.Write("Do you want to overwrite existing json files? [Y/N] "); + Console.WriteLine(""); + + var response = Console.ReadKey(false).Key; + + if (response == ConsoleKey.Y) + { + force = true; + } + } + + pocketCoreUpdater.BuildInstanceJson(force); + } +} diff --git a/src/partials/Program.Menus.cs b/src/partials/Program.Menus.cs new file mode 100644 index 00000000..12d3b07c --- /dev/null +++ b/src/partials/Program.Menus.cs @@ -0,0 +1,234 @@ +using ConsoleTools; +using Pannella.Helpers; +using Pannella.Models; +using Pannella.Models.Settings; + +namespace Pannella; + +internal partial class Program +{ + private static int DisplayMenuNew() + { + Console.Clear(); + + string[] menuItems = + { + "Update All", + "Update Firmware", + "Download Required Assets", + "Select Cores", + "Download Platform Image Packs", + "Generate Instance JSON Files", + "Generate Game and Watch ROMS", + "Enable All Display Modes", + "Backup Saves Directory", + "Settings", + "Exit" + }; + + Random random = new Random(); + int i = random.Next(0, WELCOME_MESSAGES.Length); + string welcome = WELCOME_MESSAGES[i]; + int choice = 0; + + var menu = new ConsoleMenu() + .Configure(config => + { + config.Selector = "=>"; + //config.EnableFilter = true; + config.Title = $"{welcome}\r\n{GetRandomSponsorLinks()}\r\n"; + config.EnableWriteTitle = true; + //config.EnableBreadcrumb = true; + config.WriteHeaderAction = () => Console.WriteLine("Choose your destiny:"); + config.SelectedItemBackgroundColor = Console.ForegroundColor; + config.SelectedItemForegroundColor = Console.BackgroundColor; + }); + + foreach (var item in menuItems) + { + menu.Add(item, thisMenu => + { + choice = thisMenu.CurrentItem.Index; + thisMenu.CloseMenu(); + }); + } + + menu.Show(); + + return choice; + } + + private static void AskAboutNewCores(bool force = false) + { + while (GlobalHelper.SettingsManager.GetConfig().download_new_cores == null || force) + { + force = false; + + Console.WriteLine("Would you like to, by default, install new cores? [Y]es, [N]o, [A]sk for each:"); + + ConsoleKey response = Console.ReadKey(false).Key; + + GlobalHelper.SettingsManager.GetConfig().download_new_cores = response switch + { + ConsoleKey.Y => "yes", + ConsoleKey.N => "no", + ConsoleKey.A => "ask", + _ => null + }; + } + } + + private static void RunCoreSelector(List cores, string message = "Select your cores.") + { + if (GlobalHelper.SettingsManager.GetConfig().download_new_cores?.ToLowerInvariant() == "yes") + { + foreach (Core core in cores) + { + GlobalHelper.SettingsManager.EnableCore(core.identifier); + } + } + else + { + const int pageSize = 15; + var offset = 0; + bool more = true; + + while (more) + { + var menu = new ConsoleMenu() + .Configure(config => + { + config.Selector = "=>"; + config.EnableWriteTitle = false; + config.WriteHeaderAction = () => Console.WriteLine($"{message} Use enter to check/uncheck your choices."); + config.SelectedItemBackgroundColor = Console.ForegroundColor; + config.SelectedItemForegroundColor = Console.BackgroundColor; + //config.WriteItemAction = item => Console.Write("{1}", item.Index, item.Name); + config.WriteItemAction = item => Console.Write("{0}", item.Name); + }); + var current = -1; + + if ((offset + pageSize) <= cores.Count) + { + menu.Add("Next Page", thisMenu => + { + offset += pageSize; + thisMenu.CloseMenu(); + }); + } + + foreach (Core core in cores) + { + current++; + + if ((current <= (offset + pageSize)) && (current >= offset)) + { + var coreSettings = GlobalHelper.SettingsManager.GetCoreSettings(core.identifier); + var selected = !coreSettings.skip; + var name = core.identifier; + + if (core.requires_license) + { + name += " (Requires beta access)"; + } + + var title = MenuItemName(name, selected); + + menu.Add(title, thisMenu => + { + selected = !selected; + + if (!selected) + { + GlobalHelper.SettingsManager.DisableCore(core.identifier); + } + else + { + GlobalHelper.SettingsManager.EnableCore(core.identifier); + } + + thisMenu.CurrentItem.Name = MenuItemName(core.identifier, selected); + }); + } + } + + if ((offset + pageSize) <= cores.Count) + { + menu.Add("Next Page", thisMenu => + { + offset += pageSize; + thisMenu.CloseMenu(); + }); + } + + menu.Add("Save Choices", thisMenu => + { + thisMenu.CloseMenu(); + more = false; + }); + + menu.Show(); + } + } + + GlobalHelper.SettingsManager.GetConfig().core_selector = false; + GlobalHelper.SettingsManager.SaveSettings(); + } + + private static void SettingsMenu() + { + Console.Clear(); + + var menuItems = new Dictionary + { + { "download_firmware", "Download Firmware Updates during 'Update All'" }, + { "download_assets", "Download Missing Assets (ROMs and BIOS Files) during 'Update All'" }, + { "build_instance_jsons", "Build game JSON files for supported cores during 'Update All'" }, + { "delete_skipped_cores", "Delete untracked cores during 'Update All'" }, + { "fix_jt_names", "Automatically rename Jotego cores during 'Update All" }, + { "crc_check", "Use CRC check when checking ROMs and BIOS files" }, + { "preserve_platforms_folder", "Preserve 'Platforms' folder during 'Update All'" }, + { "skip_alternative_assets", "Skip alternative roms when downloading assets" }, + { "backup_saves", "Compress and backup Saves directory during 'Update All'" }, + { "use_custom_archive", "Use custom asset archive" } + }; + + var type = typeof(Config); + var menu = new ConsoleMenu() + .Configure(config => + { + config.Selector = "=>"; + config.EnableWriteTitle = false; + config.WriteHeaderAction = () => Console.WriteLine("Settings. Use enter to check/uncheck your choices."); + config.SelectedItemBackgroundColor = Console.ForegroundColor; + config.SelectedItemForegroundColor = Console.BackgroundColor; + //config.WriteItemAction = item => Console.Write("{1}", item.Index, item.Name); + config.WriteItemAction = item => Console.Write("{0}", item.Name); + }); + + foreach (var (name, text) in menuItems) + { + var property = type.GetProperty(name); + var value = (bool)property.GetValue(GlobalHelper.SettingsManager.GetConfig()); + var title = MenuItemName(text, value); + + menu.Add(title, thisMenu => + { + value = !value; + property.SetValue(GlobalHelper.SettingsManager.GetConfig(), value); + thisMenu.CurrentItem.Name = MenuItemName(text, value); + }); + } + + menu.Add("Save", thisMenu => { thisMenu.CloseMenu(); }); + + menu.Show(); + + GlobalHelper.SettingsManager.SaveSettings(); + } + + private static string MenuItemName(string title, bool value) + { + return $"[{ (value ? "x" : " ") }] {title}"; + } +} diff --git a/src/partials/Program.Sponsors.cs b/src/partials/Program.Sponsors.cs new file mode 100644 index 00000000..8e7c9344 --- /dev/null +++ b/src/partials/Program.Sponsors.cs @@ -0,0 +1,65 @@ +using System.Text; +using Pannella.Helpers; +using Pannella.Models; + +namespace Pannella; + +internal partial class Program +{ + private static string GetRandomSponsorLinks() + { + var output = new StringBuilder(); + + if (GlobalHelper.InstalledCores.Count > 0) + { + var random = new Random(); + var index = random.Next(GlobalHelper.InstalledCores.Count); + var randomItem = GlobalHelper.InstalledCores[index]; + + if (randomItem.sponsor != null) + { + var author = randomItem.GetConfig().metadata.author; + + output.AppendLine(); + output.AppendLine($"Please consider supporting {author} for their work on the {randomItem} core:"); + output.Append(randomItem.sponsor); + } + } + + return output.ToString(); + } + + private static void Funding(string identifier) + { + if (GlobalHelper.InstalledCores.Count == 0) + { + return; + } + + List cores = new List(); + + if (identifier == null) + { + cores = GlobalHelper.InstalledCores; + } + else + { + var c = GlobalHelper.GetCore(identifier); + + if (c != null && c.IsInstalled()) + { + cores.Add(c); + } + } + + foreach (Core core in cores) + { + if (core.sponsor != null) + { + Console.WriteLine(); + Console.WriteLine($"{core.identifier}:"); + Console.WriteLine(core.sponsor); + } + } + } +} diff --git a/src/partials/Program.UpdateSelfAndRun.cs b/src/partials/Program.UpdateSelfAndRun.cs new file mode 100644 index 00000000..fd912fb2 --- /dev/null +++ b/src/partials/Program.UpdateSelfAndRun.cs @@ -0,0 +1,66 @@ +using System.Diagnostics; +using System.IO.Compression; +using System.Reflection; + +namespace Pannella; + +internal partial class Program +{ + private static int UpdateSelfAndRun(string directory, string[] updaterArgs) + { + string execName = "pupdate"; + + if (SYSTEM_OS_PLATFORM == "win") + { + execName += ".exe"; + } + + string execLocation = Path.Combine(directory, execName); + string backupName = $"{execName}.backup"; + string backupLocation = Path.Combine(directory, backupName); + const string updateName = "pupdate.zip"; + string updateLocation = Path.Combine(directory, updateName); + + int exitCode = int.MinValue; + + try + { + // Load System.IO.Compression now + Assembly.Load("System.IO.Compression"); + + if (SYSTEM_OS_PLATFORM != "win") + { + Assembly.Load("System.IO.Pipes"); + } + + // Move current process file + Console.WriteLine($"Renaming {execLocation} to {backupLocation}"); + File.Move(execLocation, backupLocation, true); + + // Extract update + Console.WriteLine($"Extracting {updateLocation} to {directory}"); + ZipFile.ExtractToDirectory(updateLocation, directory, true); + + // Execute + Console.WriteLine($"Executing {execLocation}"); + + ProcessStartInfo pInfo = new ProcessStartInfo(execLocation) + { + Arguments = string.Join(' ', updaterArgs), + UseShellExecute = false + }; + + Process p = Process.Start(pInfo); + + p.WaitForExit(); + + exitCode = p.ExitCode; + } + catch (Exception e) + { + Console.Error.WriteLine($"An error occurred: {e.GetType().Name}:{e}"); + } + + return exitCode; + } +} diff --git a/src/services/AnalogueFirmware.cs b/src/services/AnalogueFirmware.cs deleted file mode 100644 index 3fdab280..00000000 --- a/src/services/AnalogueFirmware.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Text.Json; - -namespace pannella.analoguepocket; -using Analogue; - -public static class AnalogueFirmware -{ - private const string BASE_URL = "https://www.analogue.co/"; - private const string VERSION = "support/pocket/firmware/latest"; - private const string DOWNLOAD = "support/pocket/firmware/{0}/download"; - private const string DETAILS = "support/pocket/firmware/{0}/details"; - - private static ReleaseDetails? latest = null; - - public static async Task GetLatestVersion() - { - ReleaseDetails? details = await GetDetails(); - return details.version; - } - - public static async Task GetDetails(string version = "latest") - { - if(latest != null) { - return latest; - } - string url = String.Format(BASE_URL + DETAILS, version); - string response = await Factory.GetHttpHelper().GetHTML(url); - ReleaseDetails? details = JsonSerializer.Deserialize(response); - - if (version == "latest") { - latest = details; - } - - return details; - } - - public static async Task GetFirmwareUrl(string version = "latest") - { - ReleaseDetails? details = await GetDetails(version); - return details.download_url; - } -} \ No newline at end of file diff --git a/src/services/AnalogueFirmwareService.cs b/src/services/AnalogueFirmwareService.cs new file mode 100644 index 00000000..16475867 --- /dev/null +++ b/src/services/AnalogueFirmwareService.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Pannella.Helpers; +using Pannella.Models.Analogue; + +namespace Pannella.Services; + +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] +public static class AnalogueFirmwareService +{ + private const string BASE_URL = "https://www.analogue.co/"; + private const string DETAILS = "support/pocket/firmware/{0}/details"; + + private static ReleaseDetails latest; + + public static async Task GetDetails(string version = "latest") + { + if (latest != null) + { + return latest; + } + + string url = string.Format(BASE_URL + DETAILS, version); + string response = await HttpHelper.Instance.GetHTML(url); + ReleaseDetails details = JsonSerializer.Deserialize(response); + + if (version == "latest") + { + latest = details; + } + + return details; + } +} diff --git a/src/services/ArchiveService.cs b/src/services/ArchiveService.cs index 3822988c..4bb2345a 100644 --- a/src/services/ArchiveService.cs +++ b/src/services/ArchiveService.cs @@ -1,31 +1,36 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using Pannella.Helpers; +using Pannella.Models.Archive; -namespace pannella.analoguepocket; -using archiveorg; +namespace Pannella.Services; +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public static class ArchiveService { private const string END_POINT = "https://archive.org/metadata/{0}"; - public static async Task GetFiles(string archive) { - string url = String.Format(END_POINT, archive); - string json = await Factory.GetHttpHelper().GetHTML(url); + string url = string.Format(END_POINT, archive); + string json = await HttpHelper.Instance.GetHTML(url); Archive result = JsonSerializer.Deserialize(json); return result; } - public static async Task GetFilesCustom(string url) + public static async Task GetFilesCustom(string url) { - try { - string json = await Factory.GetHttpHelper().GetHTML(url); + try + { + string json = await HttpHelper.Instance.GetHTML(url); Archive result = JsonSerializer.Deserialize(json); return result; - } catch(Exception e) { + } + catch (Exception) + { return null; } } -} \ No newline at end of file +} diff --git a/src/services/AssetsService.cs b/src/services/AssetsService.cs index a80f3608..05ff4270 100644 --- a/src/services/AssetsService.cs +++ b/src/services/AssetsService.cs @@ -1,21 +1,27 @@ +using System.Diagnostics.CodeAnalysis; using System.IO.Compression; using System.Text.Json; +using Pannella.Helpers; -namespace pannella.analoguepocket; +namespace Pannella.Services; +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public static class AssetsService { - private const string IMAGE_PACKS = "https://raw.githubusercontent.com/mattpannella/pupdate/main/image_packs.json"; private const string BLACKLIST = "https://raw.githubusercontent.com/mattpannella/pupdate/main/blacklist.json"; public static void BackupSaves(string directory, string backupLocation) { if (string.IsNullOrEmpty(directory)) + { throw new ArgumentNullException(nameof(directory)); + } if (string.IsNullOrEmpty(backupLocation)) + { throw new ArgumentNullException(nameof(backupLocation)); - + } + Console.WriteLine("Compressing and backing up Saves directory..."); string savesPath = Path.Combine(directory, "Saves"); string fileName = $"Saves_Backup_{DateTime.Now:yyyy-MM-dd_HH.mm.ss}.zip"; @@ -24,8 +30,10 @@ public static void BackupSaves(string directory, string backupLocation) if (Directory.Exists(savesPath)) { if (!Directory.Exists(backupLocation)) + { Directory.CreateDirectory(backupLocation); - + } + ZipFile.CreateFromDirectory(savesPath, archiveName); Console.WriteLine("Complete."); } @@ -34,28 +42,12 @@ public static void BackupSaves(string directory, string backupLocation) Console.WriteLine("No Saves directory found, skipping backup..."); } } - - public static async Task GetImagePacks() - { - string json = await Factory.GetHttpHelper().GetHTML(IMAGE_PACKS); - ImagePack[] packs = JsonSerializer.Deserialize(json); - - if(packs != null) { - return packs; - } - - return new ImagePack[0]; - } public static async Task GetBlacklist() { - string json = await Factory.GetHttpHelper().GetHTML(BLACKLIST); - string[] files = JsonSerializer.Deserialize(json); - - if(files != null) { - return files; - } + string json = await HttpHelper.Instance.GetHTML(BLACKLIST); + string[] files = JsonSerializer.Deserialize(json); - return new string[0]; + return files ?? Array.Empty(); } -} \ No newline at end of file +} diff --git a/src/services/CoresService.cs b/src/services/CoresService.cs index 548d21ab..10d96317 100644 --- a/src/services/CoresService.cs +++ b/src/services/CoresService.cs @@ -1,21 +1,33 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using Pannella.Helpers; +using Pannella.Models; -namespace pannella.analoguepocket; +namespace Pannella.Services; +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public static class CoresService { private const string END_POINT = "https://openfpga-cores-inventory.github.io/analogue-pocket/api/v2/cores.json"; public static async Task> GetCores() { - string json = await Factory.GetHttpHelper().GetHTML(END_POINT); - Dictionary> parsed = JsonSerializer.Deserialize>>(json); + string json = await HttpHelper.Instance.GetHTML(END_POINT); - if(parsed.ContainsKey("data")) { - var cores = parsed["data"]; - return cores; - } else { - throw new Exception("Error communicating with openFPGA Cores API"); + try + { + Dictionary> parsed = JsonSerializer.Deserialize>>(json); + + if (parsed.TryGetValue("data", out var cores)) + { + return cores; + } + } + catch (Exception e) + { + Console.WriteLine(e); } + + throw new Exception("Error communicating with openFPGA Cores API"); } -} \ No newline at end of file +} diff --git a/src/services/GithubApiService.cs b/src/services/GithubApiService.cs index b479acdf..6d397bb4 100644 --- a/src/services/GithubApiService.cs +++ b/src/services/GithubApiService.cs @@ -1,83 +1,86 @@ -using System.Text.Json; +using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; -using System.Net.Http; +using System.Text.Json; +using Pannella.Helpers; +using Pannella.Models.Github; +using GithubFile = Pannella.Models.Github.File; -namespace pannella.analoguepocket; +namespace Pannella.Services; -public static class GithubApi +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] +public static class GithubApiService { private const string RELEASES = "https://api.github.com/repos/{0}/{1}/releases"; private const string CONTENTS = "https://api.github.com/repos/{0}/{1}/contents/{2}"; - public static async Task> GetReleases(string user, string repository, string? token = "") + public static async Task> GetReleases(string user, string repository) { - string url = String.Format(RELEASES, user, repository); - var responseBody = await CallAPI(url, token); - - List? releases = JsonSerializer.Deserialize>(responseBody); - - if(releases == null) { - releases = new List(); - } + string url = string.Format(RELEASES, user, repository); + var responseBody = await CallAPI(url, GlobalHelper.SettingsManager.GetConfig().github_token); + List releases = JsonSerializer.Deserialize>(responseBody) ?? new List(); return releases; } - public static async Task GetRelease(string user, string repository, string tag_name, string? token = "") + public static async Task GetRelease(string user, string repository, string tagName) { - string url = String.Format(RELEASES, user, repository) + "/tags/" + tag_name; - - var responseBody = await CallAPI(url, token); - Github.Release? release = JsonSerializer.Deserialize(responseBody); - + string url = string.Format(RELEASES, user, repository) + "/tags/" + tagName; + var responseBody = await CallAPI(url, GlobalHelper.SettingsManager.GetConfig().github_token); + Release release = JsonSerializer.Deserialize(responseBody); + return release; } - public static async Task GetLatestRelease(string user, string repository, string? token = "") + public static async Task GetLatestRelease(string user, string repository) { - string url = String.Format(RELEASES, user, repository) + "/latest"; - - var responseBody = await CallAPI(url, token); - Github.Release? release = JsonSerializer.Deserialize(responseBody); - + string url = string.Format(RELEASES, user, repository) + "/latest"; + var responseBody = await CallAPI(url, GlobalHelper.SettingsManager.GetConfig().github_token); + Release release = JsonSerializer.Deserialize(responseBody); + return release; } - public static async Task GetFile(string user, string repository, string path, string? token = "") + public static async Task GetFile(string user, string repository, string path) { - string url = String.Format(CONTENTS, user, repository, path); - - var responseBody = await CallAPI(url, token); - Github.File? file = JsonSerializer.Deserialize(responseBody); - + string url = string.Format(CONTENTS, user, repository, path); + var responseBody = await CallAPI(url, GlobalHelper.SettingsManager.GetConfig().github_token); + GithubFile file = JsonSerializer.Deserialize(responseBody); + return file; } - public static async Task?> GetFiles(string user, string repository, string path, string? token = "") + public static async Task> GetFiles(string user, string repository, string path) { - string url = String.Format(CONTENTS, user, repository, path); - - var responseBody = await CallAPI(url, token); - List? files = JsonSerializer.Deserialize>(responseBody); - + string url = string.Format(CONTENTS, user, repository, path); + var responseBody = await CallAPI(url, GlobalHelper.SettingsManager.GetConfig().github_token); + List files = JsonSerializer.Deserialize>(responseBody); + return files; } - private static async Task CallAPI(string url, string? token = "") + private static async Task CallAPI(string url, string token) { var client = new HttpClient(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var request = new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri(url) }; + var agent = new ProductInfoHeaderValue("Analogue-Pocket-Updater-Utility", "1.0"); + request.Headers.UserAgent.Add(agent); - if(token != null && token != "") { + + if (!string.IsNullOrEmpty(token)) + { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", token); } + var response = await client.SendAsync(request).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); diff --git a/src/services/ImagePacksService.cs b/src/services/ImagePacksService.cs index 07b96962..327b8387 100644 --- a/src/services/ImagePacksService.cs +++ b/src/services/ImagePacksService.cs @@ -1,20 +1,20 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using Pannella.Helpers; +using Pannella.Models; -namespace pannella.analoguepocket; +namespace Pannella.Services; +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public static class ImagePacksService { private const string END_POINT = "https://raw.githubusercontent.com/mattpannella/pupdate/main/image_packs.json"; public static async Task GetImagePacks() { - string json = await Factory.GetHttpHelper().GetHTML(END_POINT); - ImagePack[] packs = JsonSerializer.Deserialize(json); + string json = await HttpHelper.Instance.GetHTML(END_POINT); + ImagePack[] packs = JsonSerializer.Deserialize(json); - if(packs != null) { - return packs; - } - - return new ImagePack[0]; + return packs ?? Array.Empty(); } -} \ No newline at end of file +}