diff --git a/WheelWizard/Features/CustomDistributions/CustomDistributionSingletonService.cs b/WheelWizard/Features/CustomDistributions/CustomDistributionSingletonService.cs new file mode 100644 index 00000000..6232ff7f --- /dev/null +++ b/WheelWizard/Features/CustomDistributions/CustomDistributionSingletonService.cs @@ -0,0 +1,28 @@ +using System.IO.Abstractions; +using WheelWizard.CustomDistributions.Domain; +using WheelWizard.Shared.Services; + +namespace WheelWizard.CustomDistributions; + +public interface ICustomDistributionSingletonService +{ + List GetAllDistributions(); + RetroRewind RetroRewind { get; } +} + +public class CustomDistributionSingletonService : ICustomDistributionSingletonService +{ + public IFileSystem FileSystem { get; } + public RetroRewind RetroRewind { get; } + + public CustomDistributionSingletonService(IFileSystem fileSystem, IApiCaller api) + { + FileSystem = fileSystem; + RetroRewind = new RetroRewind(fileSystem, api); + } + + public List GetAllDistributions() + { + return [RetroRewind]; + } +} diff --git a/WheelWizard/Features/CustomDistributions/CustomDistributionsExtentions.cs b/WheelWizard/Features/CustomDistributions/CustomDistributionsExtentions.cs new file mode 100644 index 00000000..3174beff --- /dev/null +++ b/WheelWizard/Features/CustomDistributions/CustomDistributionsExtentions.cs @@ -0,0 +1,16 @@ +using Refit; +using WheelWizard.CustomDistributions.Domain; +using WheelWizard.Services; + +namespace WheelWizard.CustomDistributions; + +public static class CustomDistributionsExtentions +{ + public static IServiceCollection AddCustomDistributionService(this IServiceCollection services) + { + services.AddWhWzRefitApi(Endpoints.RRUrl); + + services.AddSingleton(); + return services; + } +} diff --git a/WheelWizard/Features/CustomDistributions/Domain/RetroRewindApi.cs b/WheelWizard/Features/CustomDistributions/Domain/RetroRewindApi.cs new file mode 100644 index 00000000..5711db77 --- /dev/null +++ b/WheelWizard/Features/CustomDistributions/Domain/RetroRewindApi.cs @@ -0,0 +1,18 @@ +using Refit; + +namespace WheelWizard.CustomDistributions.Domain; + +public interface IRetroRewindApi +{ + [Get("/RetroRewind/RetroRewind.zip")] + Task DownloadRetroRewindZip(); + + [Get("/RetroRewind/RetroRewindVersion.txt")] + Task GetVersionFile(); + + [Get("/RetroRewind/RetroRewindDelete.txt")] + Task GetDeletionFile(); + + [Get("/")] + Task Ping(); // use to test server reachability +} diff --git a/WheelWizard/Features/CustomDistributions/IDistribution.cs b/WheelWizard/Features/CustomDistributions/IDistribution.cs new file mode 100644 index 00000000..51f6e2fd --- /dev/null +++ b/WheelWizard/Features/CustomDistributions/IDistribution.cs @@ -0,0 +1,38 @@ +using Semver; +using WheelWizard.Models.Enums; +using WheelWizard.Views.Popups.Generic; + +namespace WheelWizard.CustomDistributions; + +//todo: we cannot make more distributions before we also write a mystuff service and a service to download using UI + +public interface IDistribution +{ + /// + /// The title of the given distribution. + /// + public string Title { get; } + + /// + /// The name of the primary folder where the distribution is installed within the wheelwizard folder. + /// + string FolderName { get; } + + /// + /// Install the distribution. + /// + Task InstallAsync(ProgressWindow progressWindow); + + /// + /// Update the distribution. + /// + Task UpdateAsync(ProgressWindow progressWindow); + + Task RemoveAsync(ProgressWindow progressWindow); + + Task ReinstallAsync(ProgressWindow progressWindow); + + Task> GetCurrentStatusAsync(); + + SemVersion? GetCurrentVersion(); +} diff --git a/WheelWizard/Features/CustomDistributions/RetroRewind.cs b/WheelWizard/Features/CustomDistributions/RetroRewind.cs new file mode 100644 index 00000000..1850a1b3 --- /dev/null +++ b/WheelWizard/Features/CustomDistributions/RetroRewind.cs @@ -0,0 +1,559 @@ +using System.IO.Abstractions; +using System.IO.Compression; +using System.Text.RegularExpressions; +using Avalonia.Threading; +using Semver; +using WheelWizard.CustomDistributions.Domain; +using WheelWizard.Helpers; +using WheelWizard.Models.Enums; +using WheelWizard.Resources.Languages; +using WheelWizard.Services; +using WheelWizard.Services.Settings; +using WheelWizard.Shared.Services; +using WheelWizard.Views.Popups.Generic; + +namespace WheelWizard.CustomDistributions; + +public class RetroRewind : IDistribution +{ + private readonly IFileSystem _fileSystem; + private readonly IApiCaller _api; + + public RetroRewind(IFileSystem fileSystem, IApiCaller api) + { + _api = api; + _fileSystem = fileSystem; + } + + public string Title => "Retro Rewind"; + + // Keep in mind, whenever we download update files from the server, they are actually 1 folder higher, so it contains this folder. + public string FolderName => "RetroRewind6"; + + public async Task InstallAsync(ProgressWindow progressWindow) + { + if (GetCurrentVersion() is not null) + { + var removeResult = await RemoveAsync(progressWindow); + if (removeResult.IsFailure) + return removeResult; + } + + if (HasOldRksys()) + { + var rksysQuestion = new YesNoWindow() + .SetMainText(Phrases.PopupText_OldRksysFound) + .SetExtraText(Phrases.PopupText_OldRksysFoundExplained); + if (await rksysQuestion.AwaitAnswer()) + await BackupOldrksys(); + } + var serverResponse = await _api.CallApiAsync(api => api.Ping()); // actual response doesnt matter + if (serverResponse.IsFailure) + { + return "Could not connect to the server"; + } + var downloadResult = await DownloadAndExtractRetroRewind(progressWindow); + if (downloadResult.IsFailure) + return downloadResult; + var updateResult = await UpdateAsync(progressWindow); + if (updateResult.IsFailure) + return updateResult; + return Ok(); + } + + private async Task DownloadAndExtractRetroRewind(ProgressWindow progressWindow) + { + progressWindow.SetExtraText(Phrases.PopupText_InstallingRRFirstTime); + // path to the downloaded .zip + var tempZipPath = PathManager.RetroRewindTempFile; + // where we'll do the extraction + var tempExtractionPath = PathManager.TempModsFolderPath; + // where the final RR folder should live + var finalDestination = _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, FolderName); + + try + { + // 1) Download + if (_fileSystem.Directory.Exists(tempExtractionPath)) + _fileSystem.Directory.Delete(tempExtractionPath, recursive: true); + _fileSystem.Directory.CreateDirectory(tempExtractionPath); + + //todo, service + await DownloadHelper.DownloadToLocationAsync(Endpoints.RRZipUrl, tempZipPath, progressWindow); + + // 2) Extract + progressWindow.SetExtraText(Common.State_Extracting); + + var extractResult = await Task.Run(() => ExtractZipFile(tempZipPath, tempExtractionPath, progressWindow)); + + if (extractResult.IsFailure) + return extractResult; + + // 3) Locate the extracted sub-folder + var sourceFolder = _fileSystem.Path.Combine(tempExtractionPath, FolderName); + if (!_fileSystem.Directory.Exists(sourceFolder)) + { + var directories = _fileSystem.Directory.GetDirectories(tempExtractionPath); + if (directories.Length == 1) + sourceFolder = directories[0]; + else + return new DirectoryNotFoundException($"Could not find a '{FolderName}' folder inside {tempExtractionPath}"); + } + + // 4) Replace existing install, if any + if (_fileSystem.Directory.Exists(finalDestination)) + _fileSystem.Directory.Delete(finalDestination, recursive: true); + + // 5) Move into place + _fileSystem.Directory.Move(sourceFolder, finalDestination); + } + finally + { + if (_fileSystem.File.Exists(tempZipPath)) + _fileSystem.File.Delete(tempZipPath); + + if (_fileSystem.Directory.Exists(tempExtractionPath)) + _fileSystem.Directory.Delete(tempExtractionPath, recursive: true); + } + return Ok(); + } + + private async Task BackupOldrksys() + { + var rrWfc = GetOldRksys(); + if (!_fileSystem.Directory.Exists(rrWfc)) + return; + var rksysFiles = _fileSystem.Directory.GetFiles(rrWfc, "rksys.dat", SearchOption.AllDirectories); + if (rksysFiles.Length == 0) + return; + var sourceFile = rksysFiles[0]; + var regionFolder = _fileSystem.Path.GetDirectoryName(sourceFile); + var regionFolderName = _fileSystem.Path.GetFileName(regionFolder); + var datFileData = await _fileSystem.File.ReadAllBytesAsync(sourceFile); + if (regionFolderName == null) + return; + var destinationFolder = _fileSystem.Path.Combine(PathManager.SaveFolderPath, regionFolderName); + _fileSystem.Directory.CreateDirectory(destinationFolder); + var destinationFile = _fileSystem.Path.Combine(destinationFolder, "rksys.dat"); + await _fileSystem.File.WriteAllBytesAsync(destinationFile, datFileData); + } + + private bool HasOldRksys() + { + return !string.IsNullOrWhiteSpace(GetOldRksys()); + } + + private string GetOldRksys() + { + // todo, maybe we should check for the existence of the file instead of the folder? and also find the oldest one? + var rrWfcPaths = new[] + { + PathManager.SaveFolderPath, + // Also consider the folder with upper-case `Save` + _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, "riivolution", "Save", "RetroWFC"), + _fileSystem.Path.Combine(PathManager.LoadFolderPath, "Riivolution", "save", "RetroWFC"), + _fileSystem.Path.Combine(PathManager.LoadFolderPath, "Riivolution", "Save", "RetroWFC"), + _fileSystem.Path.Combine(PathManager.LoadFolderPath, "riivolution", "save", "RetroWFC"), + _fileSystem.Path.Combine(PathManager.LoadFolderPath, "riivolution", "Save", "RetroWFC"), + }; + + foreach (var rrWfc in rrWfcPaths) + { + if (!_fileSystem.Directory.Exists(rrWfc)) + continue; + var rksysFiles = _fileSystem.Directory.GetFiles(rrWfc, "rksys.dat", SearchOption.AllDirectories); + if (rksysFiles.Length > 0) + return rrWfc; + } + + return string.Empty; + } + + private async Task> IsRRUpToDate(SemVersion currentVersion) + { + var latestVersionResult = await LatestServerVersion(); + if (latestVersionResult.IsFailure) + return "Failed to check for updates"; + var latestVersion = latestVersionResult.Value; + var isUpToDate = currentVersion.ComparePrecedenceTo(latestVersion) >= 0; + return isUpToDate; + } + + private async Task> LatestServerVersion() + { + var response = await _api.CallApiAsync(api => api.GetVersionFile()); + if (!response.IsSuccess || String.IsNullOrWhiteSpace(response.Value)) + return "Failed to check for updates"; + + var result = response.Value.Split('\n', StringSplitOptions.RemoveEmptyEntries).Last().Split(' ')[0]; + return SemVersion.Parse(result); + } + + public async Task UpdateAsync(ProgressWindow progressWindow) + { + try + { + var currentVersion = GetCurrentVersion(); + if (currentVersion == null) + return await InstallAsync(progressWindow); + + var isRRUpToDate = await IsRRUpToDate(currentVersion); + if (isRRUpToDate.IsFailure) + return isRRUpToDate; + + if (isRRUpToDate.Value) + { + return Ok(); + } + + //if current version is below 3.2.6 we need to do a full reinstall + if (currentVersion.ComparePrecedenceTo(new SemVersion(3, 2, 6)) < 0) + { + var result = await ReinstallAsync(progressWindow); + return result.IsSuccess ? Ok() : result; + } + return await ApplyUpdates(currentVersion, progressWindow); + } + catch (Exception e) + { + return e; + } + } + + private async Task ApplyUpdates(SemVersion currentVersion, ProgressWindow progressWindow) + { + var allVersions = await GetAllVersionData(); + var updatesToApply = GetUpdatesToApply(currentVersion, allVersions); + // Step 1: Get the version we are updating to + var targetVersion = updatesToApply.Any() ? updatesToApply.Last().Version : currentVersion; + + // Step 2: Apply file deletions for versions between current and targetVersion + var deleteSuccess = await ApplyFileDeletionsBetweenVersions(currentVersion, targetVersion); + if (deleteSuccess.IsFailure) + { + return (Phrases.PopupText_FailedUpdateDelete); + } + + // Step 3: Download and apply the updates (if any) + for (var i = 0; i < updatesToApply.Count; i++) + { + var update = updatesToApply[i]; + + var success = await DownloadAndApplyUpdate(update, updatesToApply.Count, i + 1, progressWindow); + if (success.IsFailure) + { + return (Phrases.PopupText_FailedUpdateApply); + } + + // Update the version file after each successful update + UpdateVersionFile(update.Version); + } + return Ok(); + } + + private void UpdateVersionFile(SemVersion newVersion) + { + var versionFilePath = _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, FolderName, "version.txt"); + _fileSystem.File.WriteAllText(versionFilePath, newVersion.ToString()); + } + + private async Task DownloadAndApplyUpdate( + UpdateData update, + int totalUpdates, + int currentUpdateIndex, + ProgressWindow popupWindow + ) + { + var tempZipPath = _fileSystem.Path.Combine(_fileSystem.Path.GetTempPath(), _fileSystem.Path.GetRandomFileName()); + try + { + popupWindow.SetExtraText($"{Common.Action_Update} {currentUpdateIndex}/{totalUpdates}: {update.Description}"); + var finalFile = await DownloadHelper.DownloadToLocationAsync(update.Url, tempZipPath, popupWindow); + + popupWindow.UpdateProgress(100); + popupWindow.SetExtraText(Common.State_Extracting); + var destinationDirectoryPath = PathManager.RiivolutionWhWzFolderPath; + _fileSystem.Directory.CreateDirectory(destinationDirectoryPath); + + if (finalFile == null) + return "Failed to download update file"; + var extractResult = ExtractZipFile(finalFile, destinationDirectoryPath, popupWindow); + if (extractResult.IsFailure) + return extractResult; + + if (_fileSystem.File.Exists(finalFile)) + _fileSystem.File.Delete(finalFile); + } + finally + { + if (_fileSystem.File.Exists(tempZipPath)) + _fileSystem.File.Delete(tempZipPath); + } + + return Ok(); + } + + private OperationResult ExtractZipFile(string path, string destinationDirectory, ProgressWindow progressWindow) + { + using var archive = ZipFile.OpenRead(path); + + // 1) Compute total work units (we’ll treat each entry as one “unit”) + var entries = archive.Entries.Where(e => !e.FullName.EndsWith("desktop.ini", StringComparison.OrdinalIgnoreCase)).ToList(); + var total = entries.Count; + if (total == 0) + return Ok(); + + // Tell the UI what we’re doing, and set a “goal” so it can estimate MB or items + + Dispatcher.UIThread.Post(() => + { + progressWindow.SetExtraText(Common.State_Extracting).SetGoal($"Extracting {total} files"); + }); + + // Absolute path of the destination directory + var absoluteDestinationPath = _fileSystem.Path.GetFullPath(destinationDirectory + Path.AltDirectorySeparatorChar); + + for (var i = 0; i < total; i++) + { + var entry = entries[i]; + var destinationPath = _fileSystem.Path.GetFullPath(Path.Combine(destinationDirectory, entry.FullName)); + + // Directory traversal check + if (!destinationPath.StartsWith(absoluteDestinationPath, StringComparison.Ordinal)) + return ("The file path is outside the destination directory. Please contact the developers."); + + // If it’s a directory, create it + if (entry.FullName.EndsWith(Path.AltDirectorySeparatorChar)) + { + _fileSystem.Directory.CreateDirectory(destinationPath); + } + else + { + // Ensure folder exists + var dir = _fileSystem.Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(dir)) + _fileSystem.Directory.CreateDirectory(dir); + + // Extract the file + entry.ExtractToFile(destinationPath, overwrite: true); + } + + // Report incremental progress (0–100) + var percent = (int)(((i + 1) / (double)total) * 100); + Dispatcher.UIThread.Post(() => + { + progressWindow.UpdateProgress(percent); + }); + } + + return Ok(); + } + + private async Task ApplyFileDeletionsBetweenVersions(SemVersion currentVersion, SemVersion targetVersion) + { + try + { + var deleteListResult = await GetFileDeletionList(); + if (deleteListResult.IsFailure) + { + return "Failed to get file deletion list"; + } + var deleteList = deleteListResult.Value; + var deletionsToApply = GetDeletionsToApply(currentVersion, targetVersion, deleteList); + + foreach (var file in deletionsToApply) + { + var absoluteDestinationPath = _fileSystem.Path.GetFullPath( + PathManager.RiivolutionWhWzFolderPath + _fileSystem.Path.AltDirectorySeparatorChar + ); + var filePath = _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine(absoluteDestinationPath, file.Path.TrimStart('/'))); + //because we are actually getting the path from the server, + //we need to make sure we are not getting hacked, so we check if the path is in the riivolution folder + var resolvedPath = _fileSystem.Path.GetFullPath(new FileInfo(filePath).FullName); + if ( + !resolvedPath.StartsWith(absoluteDestinationPath, StringComparison.Ordinal) + || !filePath.StartsWith(absoluteDestinationPath, StringComparison.Ordinal) + || file.Path.Contains("..") + ) + { + return "Invalid file path detected. Please contact the developers.\n Server error: " + resolvedPath; + } + + if (_fileSystem.File.Exists(filePath)) + _fileSystem.File.Delete(filePath); + else if (_fileSystem.Directory.Exists(filePath)) + _fileSystem.Directory.Delete(filePath, recursive: true); + } + + return Ok(); + } + catch (Exception e) + { + return ($"Failed to delete files: {e.Message}"); + } + } + + private struct DeletionData + { + public SemVersion Version; + public string Path; + } + + private async Task>> GetFileDeletionList() + { + var deleteList = new List(); + + var deleteListOperation = await _api.CallApiAsync(api => api.GetDeletionFile()); + if (deleteListOperation.IsFailure) + return "Failed to get file deletion list"; + var lines = deleteListOperation.Value.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + foreach (var line in lines) + { + var parts = line.Split(' ', 2); + if (parts.Length < 2) + continue; + var deletionVersion = parts[0].Trim(); + var path = parts[1].Trim(); + if (string.IsNullOrWhiteSpace(deletionVersion) || string.IsNullOrWhiteSpace(path)) + continue; + if (!SemVersion.TryParse(deletionVersion, out var parsedVersion)) + return "Failed to parse version"; + var deletionData = new DeletionData { Version = parsedVersion, Path = path }; + deleteList.Add(deletionData); + } + + return deleteList; + } + + private struct UpdateData + { + public SemVersion Version; + public string Url; + public string Description; + } + + private async Task> GetAllVersionData() + { + var versions = new List(); + + var allVersionsResult = await _api.CallApiAsync(api => api.GetVersionFile()); + if (allVersionsResult.IsFailure) + return new(); + var lines = allVersionsResult.Value.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + foreach (var line in lines) + { + var parts = line.Split(' ', 4); + if (parts.Length < 4) + continue; + var version = parts[0].Trim(); + var url = parts[1].Trim(); + var path = parts[2].Trim(); // Path unused in our program since on pc we manually decide where to extract + var description = parts[3].Trim(); + if (string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(path)) + continue; + if (!SemVersion.TryParse(version, out var _)) + continue; + var parsedVersion = SemVersion.Parse(version); + var updateData = new UpdateData + { + Version = parsedVersion, + Url = url, + Description = description, + }; + versions.Add(updateData); + } + return versions; + } + + //todo: see if we can make this generic to the point we dont have to split up deletions and updates + private static List GetUpdatesToApply(SemVersion currentVersion, List allVersions) + { + var updatesToApply = new List(); + foreach (var update in allVersions) + { + if (update.Version.ComparePrecedenceTo(currentVersion) > 0) + { + updatesToApply.Add(update); + } + } + return updatesToApply; + } + + private static List GetDeletionsToApply( + SemVersion currentVersion, + SemVersion targetVersion, + List allDeletions + ) + { + var deletionsToApply = new List(); + allDeletions = allDeletions + .OrderByDescending(d => d.Version, Comparer.Create((a, b) => a.ComparePrecedenceTo(b))) + .ToList(); + foreach (var deletion in allDeletions) + { + if (deletion.Version.ComparePrecedenceTo(currentVersion) > 0 && deletion.Version.ComparePrecedenceTo(targetVersion) <= 0) + { + deletionsToApply.Add(deletion); + } + } + + deletionsToApply.Reverse(); + return deletionsToApply; + } + + public Task RemoveAsync(ProgressWindow progressWindow) + { + var retroRewindPath = _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, FolderName); + if (_fileSystem.Directory.Exists(retroRewindPath)) + _fileSystem.Directory.Delete(retroRewindPath, true); + return Task.FromResult(Ok()); + } + + public async Task ReinstallAsync(ProgressWindow progressWindow) + { + //Remove and install + var removeResult = await RemoveAsync(progressWindow); + if (removeResult.IsFailure) + return removeResult; + return await InstallAsync(progressWindow); + } + + public async Task> GetCurrentStatusAsync() + { + if (!SettingsHelper.PathsSetupCorrectly()) + return WheelWizardStatus.ConfigNotFinished; + + var serverEnabled = await _api.CallApiAsync(api => api.Ping()); + var rrInstalled = GetCurrentVersion() != null; + + if (serverEnabled.IsFailure) + return rrInstalled ? WheelWizardStatus.NoServerButInstalled : WheelWizardStatus.NoServer; + + if (!rrInstalled) + return WheelWizardStatus.NotInstalled; + var currentVersion = GetCurrentVersion(); + if (currentVersion == null) + return WheelWizardStatus.NotInstalled; + var retroRewindUpToDateResult = await IsRRUpToDate(currentVersion); + if (retroRewindUpToDateResult.IsFailure) + return "Failed to check for updates"; + var retroRewindUpToDate = retroRewindUpToDateResult.Value; + return !retroRewindUpToDate ? WheelWizardStatus.OutOfDate : WheelWizardStatus.Ready; + } + + public SemVersion? GetCurrentVersion() + { + var versionFilePath = _fileSystem.Path.Combine(PathManager.RiivolutionWhWzFolderPath, FolderName, "version.txt"); + if (!_fileSystem.File.Exists(versionFilePath)) + return null; + + var versionText = _fileSystem.File.ReadAllText(versionFilePath).Trim(); + var versionPattern = @"^\d+\.\d+\.\d+$"; + if (!Regex.IsMatch(versionText, versionPattern)) + return null; + + return SemVersion.Parse(versionText); + } +} diff --git a/WheelWizard/Helpers/DownloadHelper.cs b/WheelWizard/Helpers/DownloadHelper.cs index 34c003d7..ef1c9864 100644 --- a/WheelWizard/Helpers/DownloadHelper.cs +++ b/WheelWizard/Helpers/DownloadHelper.cs @@ -2,6 +2,7 @@ namespace WheelWizard.Helpers; +// todo: Delete this static class and write a service for it. public static class DownloadHelper { private const int MaxRetries = 5; @@ -22,7 +23,7 @@ public static async Task DownloadToLocationAsync( return toLocationAsync; } - public static async Task DownloadToLocationAsync( + public static async Task DownloadToLocationAsync( string url, string tempFile, ProgressWindow progressPopupWindow, @@ -47,8 +48,6 @@ public static async Task DownloadToLocationAsync( response.EnsureSuccessStatusCode(); if (response.RequestMessage == null || response.RequestMessage.RequestUri == null) { - // Do we want this error? - // new MessageBoxWindow().SetTitleText("Failed to resolve final URL.").Show(); return null; } diff --git a/WheelWizard/Services/Installation/RetroRewindInstaller.cs b/WheelWizard/Services/Installation/RetroRewindInstaller.cs deleted file mode 100644 index 60bd6740..00000000 --- a/WheelWizard/Services/Installation/RetroRewindInstaller.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.IO.Compression; -using System.Text.RegularExpressions; -using WheelWizard.Helpers; -using WheelWizard.Resources.Languages; -using WheelWizard.Views.Popups.Generic; - -namespace WheelWizard.Services.Installation; - -public static class RetroRewindInstaller -{ - private static readonly string NotInstalledVersion = "Not Installed"; - - public static bool IsRetroRewindInstalled() => CurrentRRVersion() != NotInstalledVersion; - - public static string CurrentRRVersion() - { - var versionFilePath = PathManager.RetroRewindVersionFile; - if (!File.Exists(versionFilePath)) - return NotInstalledVersion; - - var versionText = File.ReadAllText(versionFilePath).Trim(); - var versionPattern = @"^\d+\.\d+\.\d+$"; - if (!Regex.IsMatch(versionText, versionPattern)) - return NotInstalledVersion; - - return versionText; - } - - public static async Task HandleNotInstalled() - { - var result = await new YesNoWindow() - .SetMainText(Phrases.PopupText_RRNotDeterment) - .SetExtraText(Phrases.PopupText_DownloadRR) - .AwaitAnswer(); - - if (!result) - return false; - - await InstallRetroRewind(); - return true; - } - - public static async Task HandleOldVersion() - { - var result = await new YesNoWindow() - .SetMainText(Phrases.PopupText_RRToOld) - .SetExtraText(Phrases.PopupText_ReinstallRR) - .AwaitAnswer(); - - if (!result) - return false; - - await InstallRetroRewind(); - return true; - } - - public static async Task InstallRetroRewind() - { - if (IsRetroRewindInstalled()) - DeleteExistingRetroRewind(); - - if (HasOldRksys()) - { - var rksysQuestion = new YesNoWindow() - .SetMainText(Phrases.PopupText_OldRksysFound) - .SetExtraText(Phrases.PopupText_OldRksysFoundExplained); - if (await rksysQuestion.AwaitAnswer()) - await BackupOldrksys(); - } - var serverResponse = await HttpClientHelper.GetAsync(Endpoints.RRUrl); - if (!serverResponse.Succeeded) - { - await new MessageBoxWindow() - .SetMessageType(MessageBoxWindow.MessageType.Warning) - .SetTitleText("Could not connect to the server") - .SetInfoText(Phrases.PopupText_CouldNotConnectServer) - .ShowDialog(); - return; - } - await DownloadAndExtractRetroRewind(PathManager.RetroRewindTempFile); - await RetroRewindUpdater.UpdateRR(); - } - - public static async Task ReinstallRR() - { - var result = await new YesNoWindow() - .SetMainText(Phrases.PopupText_ReinstallRR) - .SetExtraText(Phrases.PopupText_ReinstallQuestion) - .AwaitAnswer(); - - if (!result) - return; - - DeleteExistingRetroRewind(); - await InstallRetroRewind(); - } - - private static async Task DownloadAndExtractRetroRewind(string tempZipPath) - { - var progressWindow = new ProgressWindow(Phrases.PopupText_InstallingRR); - progressWindow.SetExtraText(Phrases.PopupText_InstallingRRFirstTime); - progressWindow.Show(); - - try - { - await DownloadHelper.DownloadToLocationAsync(Endpoints.RRZipUrl, tempZipPath, progressWindow); - progressWindow.SetExtraText(Common.State_Extracting); - var extractionPath = PathManager.RiivolutionWhWzFolderPath; - ZipFile.ExtractToDirectory(tempZipPath, extractionPath, true); - } - finally - { - progressWindow.Close(); - if (File.Exists(tempZipPath)) - File.Delete(tempZipPath); - } - } - - private static bool HasOldRksys() - { - return !string.IsNullOrWhiteSpace(GetOldRksys()); - } - - private static string GetOldRksys() - { - var rrWfcPaths = new[] - { - Path.Combine(PathManager.SaveFolderPath), - // Also consider the folder with upper-case `Save` - Path.Combine(PathManager.RiivolutionWhWzFolderPath, "riivolution", "Save", "RetroWFC"), - Path.Combine(PathManager.LoadFolderPath, "Riivolution", "save", "RetroWFC"), - Path.Combine(PathManager.LoadFolderPath, "Riivolution", "Save", "RetroWFC"), - Path.Combine(PathManager.LoadFolderPath, "riivolution", "save", "RetroWFC"), - Path.Combine(PathManager.LoadFolderPath, "riivolution", "Save", "RetroWFC"), - }; - - foreach (var rrWfc in rrWfcPaths) - { - if (!Directory.Exists(rrWfc)) - continue; - var rksysFiles = Directory.GetFiles(rrWfc, "rksys.dat", SearchOption.AllDirectories); - if (rksysFiles.Length > 0) - return rrWfc; - } - - return string.Empty; - } - - private static async Task BackupOldrksys() - { - var rrWfc = Path.Combine(GetOldRksys()); - if (!Directory.Exists(rrWfc)) - return; - var rksysFiles = Directory.GetFiles(rrWfc, "rksys.dat", SearchOption.AllDirectories); - if (rksysFiles.Length == 0) - return; - var sourceFile = rksysFiles[0]; - var regionFolder = Path.GetDirectoryName(sourceFile); - var regionFolderName = Path.GetFileName(regionFolder); - var datFileData = await File.ReadAllBytesAsync(sourceFile); - if (regionFolderName == null) - return; - var destinationFolder = Path.Combine(PathManager.SaveFolderPath, regionFolderName); - Directory.CreateDirectory(destinationFolder); - var destinationFile = Path.Combine(destinationFolder, "rksys.dat"); - await File.WriteAllBytesAsync(destinationFile, datFileData); - } - - private static void DeleteExistingRetroRewind() - { - var retroRewindPath = PathManager.RetroRewind6FolderPath; - if (Directory.Exists(retroRewindPath)) - Directory.Delete(retroRewindPath, true); - } -} diff --git a/WheelWizard/Services/Installation/RetroRewindUpdater.cs b/WheelWizard/Services/Installation/RetroRewindUpdater.cs deleted file mode 100644 index 14dcba41..00000000 --- a/WheelWizard/Services/Installation/RetroRewindUpdater.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System.IO.Compression; -using WheelWizard.Helpers; -using WheelWizard.Resources.Languages; -using WheelWizard.Views.Popups.Generic; - -namespace WheelWizard.Services.Installation; - -public static class RetroRewindUpdater -{ - public static async Task IsRRUpToDate(string currentVersion) - { - var latestVersion = await GetLatestVersionString(); - return currentVersion.Trim() == latestVersion.Trim(); - } - - private static async Task GetLatestVersionString() - { - var response = await HttpClientHelper.GetAsync(Endpoints.RRVersionUrl); - if (response.Succeeded && response.Content != null) - return response.Content.Split('\n').Last().Split(' ')[0]; - new YesNoWindow().SetMainText(Phrases.PopupText_FailCheckUpdates).AwaitAnswer(); - return "Failed to check for updates"; - } - - public static async Task UpdateRR() - { - try - { - if (!RetroRewindInstaller.IsRetroRewindInstalled()) - return await RetroRewindInstaller.HandleNotInstalled(); - - var currentVersion = RetroRewindInstaller.CurrentRRVersion(); - if (await IsRRUpToDate(currentVersion)) - { - await new MessageBoxWindow() - .SetMessageType(MessageBoxWindow.MessageType.Message) - .SetTitleText(Phrases.PopupText_RRUpToDate) - .SetInfoText(Phrases.PopupText_RRUpToDate) - .ShowDialog(); - return true; - } - - //if current version is below 3.2.6 we need to do a full reinstall - if (CompareVersions(currentVersion, "3.2.6") < 0) - return await RetroRewindInstaller.HandleOldVersion(); - return await ApplyUpdates(currentVersion); - } - catch (Exception e) - { - AbortingUpdate($"Reason: {e.Message}"); - return false; - } - } - - private static async Task ApplyUpdates(string currentVersion) - { - var allVersions = await GetAllVersionData(); - var updatesToApply = GetUpdatesToApply(currentVersion, allVersions); - - var progressWindow = new ProgressWindow(Phrases.PopupText_UpdateRR); - progressWindow.Show(); - - // Step 1: Get the version we are updating to - var targetVersion = updatesToApply.Any() ? updatesToApply.Last().Version : currentVersion; - - // Step 2: Apply file deletions for versions between current and targetVersion - var deleteSuccess = await ApplyFileDeletionsBetweenVersions(currentVersion, targetVersion); - if (!deleteSuccess) - { - AbortingUpdate(Phrases.PopupText_FailedUpdateDelete); - progressWindow.Close(); - return false; - } - - // Step 3: Download and apply the updates (if any) - for (var i = 0; i < updatesToApply.Count; i++) - { - var update = updatesToApply[i]; - - var success = await DownloadAndApplyUpdate(update, updatesToApply.Count, i + 1, progressWindow); - if (!success) - { - progressWindow.Close(); - AbortingUpdate(Phrases.PopupText_FailedUpdateApply); - return false; - } - - // Update the version file after each successful update - UpdateVersionFile(update.Version); - } - - progressWindow.Close(); - return true; - } - - private static async Task ApplyFileDeletionsBetweenVersions(string currentVersion, string targetVersion) - { - try - { - var deleteList = await GetFileDeletionList(); - var deletionsToApply = GetDeletionsToApply(currentVersion, targetVersion, deleteList); - - foreach (var file in deletionsToApply) - { - var absoluteDestinationPath = Path.GetFullPath(PathManager.RiivolutionWhWzFolderPath + Path.AltDirectorySeparatorChar); - var filePath = Path.GetFullPath(Path.Combine(absoluteDestinationPath, file.Path.TrimStart('/'))); - //because we are actually getting the path from the server, - //we need to make sure we are not getting hacked, so we check if the path is in the riivolution folder - var resolvedPath = Path.GetFullPath(new FileInfo(filePath).FullName); - if ( - !resolvedPath.StartsWith(absoluteDestinationPath, StringComparison.Ordinal) - || !filePath.StartsWith(absoluteDestinationPath, StringComparison.Ordinal) - || file.Path.Contains("..") - ) - { - AbortingUpdate("Invalid file path detected. Please contact the developers.\n Server error: " + resolvedPath); - return false; - } - - if (File.Exists(filePath)) - File.Delete(filePath); - else if (Directory.Exists(filePath)) - Directory.Delete(filePath, recursive: true); - } - - return true; - } - catch (Exception e) - { - AbortingUpdate($"Failed to delete files: {e.Message}"); - return false; - } - } - - private static List<(string Version, string Path)> GetDeletionsToApply( - string currentVersion, - string targetVersion, - List<(string Version, string Path)> allDeletions - ) - { - var deletionsToApply = new List<(string Version, string Path)>(); - allDeletions.Sort((a, b) => CompareVersions(b.Version, a.Version)); // Sort in descending order - foreach (var deletion in allDeletions) - { - if (CompareVersions(deletion.Version, currentVersion) > 0 && CompareVersions(deletion.Version, targetVersion) <= 0) - { - deletionsToApply.Add(deletion); - } - } - - deletionsToApply.Reverse(); - return deletionsToApply; - } - - private static async Task> GetFileDeletionList() - { - var deleteList = new List<(string Version, string Path)>(); - - using var httpClient = new HttpClient(); - var deleteListText = await httpClient.GetStringAsync(Endpoints.RRVersionDeleteUrl); - var lines = deleteListText.Split('\n', StringSplitOptions.RemoveEmptyEntries); - - foreach (var line in lines) - { - var parts = line.Split(' ', 2); - if (parts.Length < 2) - continue; - deleteList.Add((parts[0].Trim(), parts[1].Trim())); - } - - return deleteList; - } - - private static void UpdateVersionFile(string newVersion) - { - var versionFilePath = Path.Combine(PathManager.RetroRewind6FolderPath, "version.txt"); - File.WriteAllText(versionFilePath, newVersion); - } - - private static async Task> GetAllVersionData() - { - var versions = new List<(string Version, string Url, string Path, string Description)>(); - - using var httpClient = new HttpClient(); - var allVersionsText = await httpClient.GetStringAsync(Endpoints.RRVersionUrl); - var lines = allVersionsText.Split('\n', StringSplitOptions.RemoveEmptyEntries); - - foreach (var line in lines) - { - var parts = line.Split(' ', 4); - if (parts.Length < 4) - continue; - versions.Add((parts[0].Trim(), parts[1].Trim(), parts[2].Trim(), parts[3].Trim())); - } - - return versions; - } - - private static List<(string Version, string Url, string Path, string Description)> GetUpdatesToApply( - string currentVersion, - List<(string Version, string Url, string Path, string Description)> allVersions - ) - { - var updatesToApply = new List<(string Version, string Url, string Path, string Description)>(); - allVersions.Sort((a, b) => CompareVersions(b.Version, a.Version)); // Sort in descending order - foreach (var version in allVersions) - { - if (CompareVersions(version.Version, currentVersion) > 0) - updatesToApply.Add(version); - else - break; - } - - updatesToApply.Reverse(); - return updatesToApply; - } - - private static int CompareVersions(string v1, string v2) - { - var parts1 = v1.Split('.').Select(int.Parse).ToArray(); - var parts2 = v2.Split('.').Select(int.Parse).ToArray(); - for (var i = 0; i < Math.Max(parts1.Length, parts2.Length); i++) - { - var p1 = i < parts1.Length ? parts1[i] : 0; - var p2 = i < parts2.Length ? parts2[i] : 0; - if (p1 != p2) - return p1.CompareTo(p2); - } - - return 0; - } - - private static async Task DownloadAndApplyUpdate( - (string Version, string Url, string Path, string Description) update, - int totalUpdates, - int currentUpdateIndex, - ProgressWindow popupWindow - ) - { - var tempZipPath = Path.GetTempFileName(); - try - { - popupWindow.SetExtraText($"{Common.Action_Update} {currentUpdateIndex}/{totalUpdates}: {update.Description}"); - var finalFile = await DownloadHelper.DownloadToLocationAsync(update.Url, tempZipPath, popupWindow); - - popupWindow.UpdateProgress(100); - popupWindow.SetExtraText(Common.State_Extracting); - var destinationDirectoryPath = PathManager.RiivolutionWhWzFolderPath; - Directory.CreateDirectory(destinationDirectoryPath); - ExtractZipFile(finalFile, destinationDirectoryPath); - if (File.Exists(finalFile)) - File.Delete(finalFile); - } - finally - { - if (File.Exists(tempZipPath)) - File.Delete(tempZipPath); - } - - return true; - } - - private static void ExtractZipFile(string path, string destinationDirectory) - { - using var archive = ZipFile.OpenRead(path); - - // Absolute path of the destination directory - var absoluteDestinationPath = Path.GetFullPath(destinationDirectory + Path.AltDirectorySeparatorChar); - - foreach (var entry in archive.Entries) - { - if (entry.FullName.EndsWith("desktop.ini", StringComparison.OrdinalIgnoreCase)) - continue; // Skip the desktop.ini file - - // Get the full path of the file - var destinationPath = Path.GetFullPath(Path.Combine(destinationDirectory, entry.FullName)); - - // Check for directory traversal attacks - if (!destinationPath.StartsWith(absoluteDestinationPath, StringComparison.Ordinal)) - { - AbortingUpdate("The file path is outside the destination directory. Please contact the developers."); - return; - } - - // If the entry is a directory, create it - if (entry.FullName.EndsWith(Path.AltDirectorySeparatorChar)) - { - Directory.CreateDirectory(destinationPath); - continue; - } - - // Create directory if it doesn't exist - var directoryName = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrEmpty(directoryName)) - Directory.CreateDirectory(directoryName); - - // Extract the file - entry.ExtractToFile(destinationPath, overwrite: true); - } - } - - public static void AbortingUpdate(string reason) - { - new MessageBoxWindow() - .SetMessageType(MessageBoxWindow.MessageType.Error) - .SetTitleText("Aborting RR Update") - .SetInfoText(reason) - .Show(); - } -} diff --git a/WheelWizard/Services/Launcher/RrLauncher.cs b/WheelWizard/Services/Launcher/RrLauncher.cs index 26ceb75e..25a6c9e3 100644 --- a/WheelWizard/Services/Launcher/RrLauncher.cs +++ b/WheelWizard/Services/Launcher/RrLauncher.cs @@ -1,4 +1,5 @@ using Avalonia.Threading; +using WheelWizard.CustomDistributions; using WheelWizard.Helpers; using WheelWizard.Models.Enums; using WheelWizard.Resources.Languages; @@ -6,6 +7,8 @@ using WheelWizard.Services.Launcher.Helpers; using WheelWizard.Services.Settings; using WheelWizard.Services.WiiManagement; +using WheelWizard.Shared.DependencyInjection; +using WheelWizard.Views; using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Services.Launcher; @@ -15,6 +18,10 @@ public class RrLauncher : ILauncher public string GameTitle { get; } = "Retro Rewind"; private static string RrLaunchJsonFilePath => PathManager.RrLaunchJsonFilePath; + [Inject] + private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = + App.Services.GetRequiredService(); + public async Task Launch() { try @@ -55,25 +62,31 @@ public async Task Launch() } } - public Task Install() => RetroRewindInstaller.InstallRetroRewind(); + public async Task Install() + { + var progressWindow = new ProgressWindow(); + progressWindow.Show(); + await CustomDistributionSingletonService.RetroRewind.InstallAsync(progressWindow); + progressWindow.Close(); + } - public Task Update() => RetroRewindUpdater.UpdateRR(); + public async Task Update() + { + var progressWindow = new ProgressWindow(); + progressWindow.Show(); + await CustomDistributionSingletonService.RetroRewind.UpdateAsync(progressWindow); + progressWindow.Close(); + } public async Task GetCurrentStatus() { - if (!SettingsHelper.PathsSetupCorrectly()) - return WheelWizardStatus.ConfigNotFinished; - - var serverEnabled = await HttpClientHelper.GetAsync(Endpoints.RRUrl); - var rrInstalled = RetroRewindInstaller.IsRetroRewindInstalled(); - - if (!serverEnabled.Succeeded) - return rrInstalled ? WheelWizardStatus.NoServerButInstalled : WheelWizardStatus.NoServer; - - if (!rrInstalled) + if (CustomDistributionSingletonService == null) + { return WheelWizardStatus.NotInstalled; - - var retroRewindUpToDate = await RetroRewindUpdater.IsRRUpToDate(RetroRewindInstaller.CurrentRRVersion()); - return !retroRewindUpToDate ? WheelWizardStatus.OutOfDate : WheelWizardStatus.Ready; + } + var statusResult = await CustomDistributionSingletonService.RetroRewind.GetCurrentStatusAsync(); + if (statusResult.IsFailure) + return WheelWizardStatus.NotInstalled; + return statusResult.Value; } } diff --git a/WheelWizard/Services/PathManager.cs b/WheelWizard/Services/PathManager.cs index fa791018..251952c9 100644 --- a/WheelWizard/Services/PathManager.cs +++ b/WheelWizard/Services/PathManager.cs @@ -33,7 +33,6 @@ public static class PathManager public static readonly string ModConfigFilePath = Path.Combine(ModsFolderPath, "modconfig.json"); public static readonly string TempModsFolderPath = Path.Combine(ModsFolderPath, "Temp"); public static readonly string RetroRewindTempFile = Path.Combine(TempModsFolderPath, "RetroRewind.zip"); - public static string RetroRewindVersionFile => Path.Combine(RetroRewind6FolderPath, "version.txt"); public static string WiiDbFolder => Path.Combine(WiiFolderPath, "shared2", "menu", "FaceLib"); public static string MiiDbFile => Path.Combine(WiiDbFolder, "RFL_DB.dat"); @@ -42,12 +41,15 @@ public static class PathManager //Also remember that mods may not be in a subfolder, all mod files must be located in /MyStuff directly // Helper paths for folders used across multiple files - public static string MyStuffFolderPath => Path.Combine(RetroRewind6FolderPath, "MyStuff"); + + //todo: before we can actually add more distributions, we will have to rewrite the MyStuff as a service aswell + public static string MyStuffFolderPath => Path.Combine(RiivolutionWhWzFolderPath, "RetroRewind6", "MyStuff"); public static string GetModDirectoryPath(string modName) => Path.Combine(ModsFolderPath, modName); public static string RiivolutionWhWzFolderPath => Path.Combine(LoadFolderPath, "Riivolution", "WheelWizard"); - public static string RetroRewind6FolderPath => Path.Combine(RiivolutionWhWzFolderPath, "RetroRewind6"); + + // public static string RetroRewind6FolderPath => Path.Combine(RiivolutionWhWzFolderPath, "RetroRewind6"); // This is not the folder your save file is located in, but its the folder where every Region folder is, so the save file is in SaveFolderPath/Region public static string SaveFolderPath => Path.Combine(RiivolutionWhWzFolderPath, "riivolution", "save", "RetroWFC"); diff --git a/WheelWizard/SetupExtensions.cs b/WheelWizard/SetupExtensions.cs index 8f27afca..d76d1b55 100644 --- a/WheelWizard/SetupExtensions.cs +++ b/WheelWizard/SetupExtensions.cs @@ -5,6 +5,7 @@ using WheelWizard.AutoUpdating; using WheelWizard.Branding; using WheelWizard.CustomCharacters; +using WheelWizard.CustomDistributions; using WheelWizard.GameBanana; using WheelWizard.GitHub; using WheelWizard.MiiImages; @@ -32,6 +33,7 @@ public static void AddWheelWizardServices(this IServiceCollection services) services.AddWiiManagement(); services.AddGameBanana(); services.AddMiiImages(); + services.AddCustomDistributionService(); // IO Abstractions services.AddSingleton(); diff --git a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs index 2f2297ee..4efb88ba 100644 --- a/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/OtherSettings.axaml.cs @@ -1,19 +1,24 @@ using Avalonia.Controls; using Avalonia.Interactivity; +using WheelWizard.CustomDistributions; using WheelWizard.Helpers; using WheelWizard.Models.Settings; using WheelWizard.Resources.Languages; using WheelWizard.Services; using WheelWizard.Services.Installation; using WheelWizard.Services.Settings; +using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Popups.Generic; namespace WheelWizard.Views.Pages.Settings; -public partial class OtherSettings : UserControl +public partial class OtherSettings : UserControlBase { private readonly bool _settingsAreDisabled; + [Inject] + private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = null!; + public OtherSettings() { InitializeComponent(); @@ -126,7 +131,13 @@ private async void WhWzLanguageDropdown_OnSelectionChanged(object? sender, Selec ViewUtils.RefreshWindow(); } - private async void Reinstall_RetroRewind(object sender, RoutedEventArgs e) => await RetroRewindInstaller.ReinstallRR(); + private async void Reinstall_RetroRewind(object sender, RoutedEventArgs e) + { + var progressWindow = new ProgressWindow(); + progressWindow.Show(); + await CustomDistributionSingletonService.RetroRewind.ReinstallAsync(progressWindow); + progressWindow.Close(); + } private void OpenSaveFolder_OnClick(object? sender, RoutedEventArgs e) { diff --git a/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs b/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs index 41196466..f856327f 100644 --- a/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs +++ b/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs @@ -1,7 +1,9 @@ using Avalonia.Controls; using Avalonia.Interactivity; using WheelWizard.Branding; +using WheelWizard.CustomDistributions; using WheelWizard.Services.Installation; +using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Popups; namespace WheelWizard.Views.Pages.Settings; @@ -11,11 +13,14 @@ public partial class SettingsPage : UserControlBase public SettingsPage() : this(new WhWzSettings()) { } + [Inject] + private ICustomDistributionSingletonService CustomDistributionSingletonService { get; set; } = null!; + public SettingsPage(UserControl initialSettingsPage) { InitializeComponent(); - RrVersionText.Text = "RR: " + RetroRewindInstaller.CurrentRRVersion(); + RrVersionText.Text = "RR: " + CustomDistributionSingletonService.RetroRewind.GetCurrentVersion(); var part1 = "Release"; var part2 = "Unknown OS";