From 2795ccf92ed91b0c92d5a94e6133c28ea04d19ef Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 8 Sep 2024 01:23:35 +0100 Subject: [PATCH] Bulk restoration of deleted mod files --- Bloxstrap/Bootstrapper.cs | 283 ++++++++++++++------------- Bloxstrap/Models/Manifest/Package.cs | 5 + 2 files changed, 152 insertions(+), 136 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 8b2c3320..4ad89c3b 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -48,7 +48,7 @@ public class Bootstrapper private long _totalDownloadedBytes = 0; private bool _mustUpgrade => File.Exists(AppData.LockFilePath) || !File.Exists(AppData.ExecutablePath); - private bool _skipUpgrade = false; + private bool _noConnection = false; public IBootstrapperDialog? Dialog = null; @@ -165,10 +165,11 @@ public async Task Run() await GetLatestVersionInfo(); // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (!_skipUpgrade && (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)) + if (!_noConnection && (AppData.State.VersionGuid != _latestVersionGuid || _mustUpgrade)) await UpgradeRoblox(); - //await ApplyModifications(); + if (!_noConnection) + await ApplyModifications(); // check if launch uri is set to our bootstrapper // this doesn't go under register, so we check every launch @@ -729,144 +730,164 @@ private async Task UpgradeRoblox() _isInstalling = false; } - //private async Task ApplyModifications() - //{ - // const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - - // if (Process.GetProcessesByName(AppData.ExecutableName[..^4]).Any()) - // { - // App.Logger.WriteLine(LOG_IDENT, "Roblox is running, aborting mod check"); - // return; - // } + private async Task ApplyModifications() + { + const string LOG_IDENT = "Bootstrapper::ApplyModifications"; - // SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); + SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); - // // handle file mods - // App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); + // handle file mods + App.Logger.WriteLine(LOG_IDENT, "Checking file mods..."); - // // manifest has been moved to State.json - // File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); + // manifest has been moved to State.json + File.Delete(Path.Combine(Paths.Base, "ModManifest.txt")); - // List modFolderFiles = new(); + List modFolderFiles = new(); - // if (!Directory.Exists(Paths.Modifications)) - // Directory.CreateDirectory(Paths.Modifications); + if (!Directory.Exists(Paths.Modifications)) + Directory.CreateDirectory(Paths.Modifications); - // // check custom font mod - // // instead of replacing the fonts themselves, we'll just alter the font family manifests + // check custom font mod + // instead of replacing the fonts themselves, we'll just alter the font family manifests - // string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); + string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); - // if (File.Exists(Paths.CustomFont)) - // { - // App.Logger.WriteLine(LOG_IDENT, "Begin font check"); + if (File.Exists(Paths.CustomFont)) + { + App.Logger.WriteLine(LOG_IDENT, "Begin font check"); - // Directory.CreateDirectory(modFontFamiliesFolder); + Directory.CreateDirectory(modFontFamiliesFolder); - // foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(_versionFolder, "content\\fonts\\families"))) - // { - // string jsonFilename = Path.GetFileName(jsonFilePath); - // string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); + const string path = "rbxasset://fonts/CustomFont.ttf"; - // if (File.Exists(modFilepath)) - // continue; + foreach (string jsonFilePath in Directory.GetFiles(Path.Combine(AppData.Directory, "content\\fonts\\families"))) + { + string jsonFilename = Path.GetFileName(jsonFilePath); + string modFilepath = Path.Combine(modFontFamiliesFolder, jsonFilename); - // App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); + if (File.Exists(modFilepath)) + continue; - // FontFamily? fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); + App.Logger.WriteLine(LOG_IDENT, $"Setting font for {jsonFilename}"); - // if (fontFamilyData is null) - // continue; + var fontFamilyData = JsonSerializer.Deserialize(File.ReadAllText(jsonFilePath)); - // foreach (FontFace fontFace in fontFamilyData.Faces) - // fontFace.AssetId = "rbxasset://fonts/CustomFont.ttf"; + if (fontFamilyData is null) + continue; - // // TODO: writing on every launch is not necessary - // File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); - // } + bool shouldWrite = false; + + foreach (var fontFace in fontFamilyData.Faces) + { + if (fontFace.AssetId != path) + { + fontFace.AssetId = path; + shouldWrite = true; + } + } - // App.Logger.WriteLine(LOG_IDENT, "End font check"); - // } - // else if (Directory.Exists(modFontFamiliesFolder)) - // { - // Directory.Delete(modFontFamiliesFolder, true); - // } + if (shouldWrite) + File.WriteAllText(modFilepath, JsonSerializer.Serialize(fontFamilyData, new JsonSerializerOptions { WriteIndented = true })); + } - // foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) - // { - // // get relative directory path - // string relativeFile = file.Substring(Paths.Modifications.Length + 1); + App.Logger.WriteLine(LOG_IDENT, "End font check"); + } + else if (Directory.Exists(modFontFamiliesFolder)) + { + Directory.Delete(modFontFamiliesFolder, true); + } - // // v1.7.0 - README has been moved to the preferences menu now - // if (relativeFile == "README.txt") - // { - // File.Delete(file); - // continue; - // } + foreach (string file in Directory.GetFiles(Paths.Modifications, "*.*", SearchOption.AllDirectories)) + { + // get relative directory path + string relativeFile = file.Substring(Paths.Modifications.Length + 1); - // if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) - // continue; + // v1.7.0 - README has been moved to the preferences menu now + if (relativeFile == "README.txt") + { + File.Delete(file); + continue; + } - // if (relativeFile.EndsWith(".lock")) - // continue; + if (!App.Settings.Prop.UseFastFlagManager && String.Equals(relativeFile, "ClientSettings\\ClientAppSettings.json", StringComparison.OrdinalIgnoreCase)) + continue; - // modFolderFiles.Add(relativeFile); + if (relativeFile.EndsWith(".lock")) + continue; - // string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); - // string fileVersionFolder = Path.Combine(_versionFolder, relativeFile); + modFolderFiles.Add(relativeFile); - // if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) - // { - // App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); - // continue; - // } + string fileModFolder = Path.Combine(Paths.Modifications, relativeFile); + string fileVersionFolder = Path.Combine(AppData.Directory, relativeFile); - // Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); + if (File.Exists(fileVersionFolder) && MD5Hash.FromFile(fileModFolder) == MD5Hash.FromFile(fileVersionFolder)) + { + App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} already exists in the version folder, and is a match"); + continue; + } - // Filesystem.AssertReadOnly(fileVersionFolder); - // File.Copy(fileModFolder, fileVersionFolder, true); - // Filesystem.AssertReadOnly(fileVersionFolder); + Directory.CreateDirectory(Path.GetDirectoryName(fileVersionFolder)!); - // App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); - // } + Filesystem.AssertReadOnly(fileVersionFolder); + File.Copy(fileModFolder, fileVersionFolder, true); + Filesystem.AssertReadOnly(fileVersionFolder); - // // the manifest is primarily here to keep track of what files have been - // // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages - // // now check for files that have been deleted from the mod folder according to the manifest + App.Logger.WriteLine(LOG_IDENT, $"{relativeFile} has been copied to the version folder"); + } - // // TODO: this needs to extract the files from packages in bulk, this is way too slow - // foreach (string fileLocation in App.State.Prop.ModManifest) - // { - // if (modFolderFiles.Contains(fileLocation)) - // continue; + // the manifest is primarily here to keep track of what files have been + // deleted from the modifications folder, so that we know when to restore the original files from the downloaded packages + // now check for files that have been deleted from the mod folder according to the manifest - // var package = AppData.PackageDirectoryMap.SingleOrDefault(x => x.Value != "" && fileLocation.StartsWith(x.Value)); + var fileRestoreMap = new Dictionary>(); - // // package doesn't exist, likely mistakenly placed file - // if (String.IsNullOrEmpty(package.Key)) - // { - // App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); + foreach (string fileLocation in App.State.Prop.ModManifest) + { + if (modFolderFiles.Contains(fileLocation)) + continue; - // string versionFileLocation = Path.Combine(_versionFolder, fileLocation); + var packageMapEntry = AppData.PackageDirectoryMap.SingleOrDefault(x => !String.IsNullOrEmpty(x.Value) && fileLocation.StartsWith(x.Value)); + string packageName = packageMapEntry.Key; - // if (File.Exists(versionFileLocation)) - // File.Delete(versionFileLocation); + // package doesn't exist, likely mistakenly placed file + if (String.IsNullOrEmpty(packageName)) + { + App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod but does not belong to a package"); - // continue; - // } + string versionFileLocation = Path.Combine(AppData.Directory, fileLocation); - // // restore original file - // string fileName = fileLocation.Substring(package.Value.Length); - // await ExtractFileFromPackage(package.Key, fileName); + if (File.Exists(versionFileLocation)) + File.Delete(versionFileLocation); - // App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restored from {package.Key}"); - // } + continue; + } - // App.State.Prop.ModManifest = modFolderFiles; - // App.State.Save(); + string fileName = fileLocation.Substring(packageMapEntry.Value.Length); - // App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); - //} + if (!fileRestoreMap.ContainsKey(packageName)) + fileRestoreMap[packageName] = new(); + + fileRestoreMap[packageName].Add(fileName); + + App.Logger.WriteLine(LOG_IDENT, $"{fileLocation} was removed as a mod, restoring from {packageName}"); + } + + foreach (var entry in fileRestoreMap) + { + var package = _versionPackageManifest.Find(x => x.Name == entry.Key); + + if (package is not null) + { + await DownloadPackage(package); + ExtractPackage(package, entry.Value); + } + } + + App.State.Prop.ModManifest = modFolderFiles; + App.State.Save(); + + App.Logger.WriteLine(LOG_IDENT, $"Finished checking file mods"); + } private async Task DownloadPackage(Package package) { @@ -876,14 +897,13 @@ private async Task DownloadPackage(Package package) return; string packageUrl = RobloxDeployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); - string packageLocation = Path.Combine(Paths.Downloads, package.Signature); string robloxPackageLocation = Path.Combine(Paths.LocalAppData, "Roblox", "Downloads", package.Signature); - if (File.Exists(packageLocation)) + if (File.Exists(package.DownloadPath)) { - var file = new FileInfo(packageLocation); + var file = new FileInfo(package.DownloadPath); - string calculatedMD5 = MD5Hash.FromFile(packageLocation); + string calculatedMD5 = MD5Hash.FromFile(package.DownloadPath); if (calculatedMD5 != package.Signature) { @@ -906,7 +926,7 @@ private async Task DownloadPackage(Package package) // then we can just copy the one from there App.Logger.WriteLine(LOG_IDENT, $"Found existing copy at '{robloxPackageLocation}'! Copying to Downloads folder..."); - File.Copy(robloxPackageLocation, packageLocation); + File.Copy(robloxPackageLocation, package.DownloadPath); _totalDownloadedBytes += package.PackedSize; UpdateProgressBar(); @@ -914,7 +934,7 @@ private async Task DownloadPackage(Package package) return; } - if (File.Exists(packageLocation)) + if (File.Exists(package.DownloadPath)) return; // TODO: telemetry for this. chances are that this is completely unnecessary and that it can be removed. @@ -937,7 +957,7 @@ private async Task DownloadPackage(Package package) { var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead, _cancelTokenSource.Token); await using var stream = await response.Content.ReadAsStreamAsync(_cancelTokenSource.Token); - await using var fileStream = new FileStream(packageLocation, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Delete); + await using var fileStream = new FileStream(package.DownloadPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Delete); while (true) { @@ -987,8 +1007,8 @@ private async Task DownloadPackage(Package package) else if (i >= maxTries) throw; - if (File.Exists(packageLocation)) - File.Delete(packageLocation); + if (File.Exists(package.DownloadPath)) + File.Delete(package.DownloadPath); _totalDownloadedBytes -= totalBytesRead; UpdateProgressBar(); @@ -1005,40 +1025,31 @@ private async Task DownloadPackage(Package package) } } - private void ExtractPackage(Package package) + private void ExtractPackage(Package package, List? files = null) { const string LOG_IDENT = "Bootstrapper::ExtractPackage"; - string packageLocation = Path.Combine(Paths.Downloads, package.Signature); string packageFolder = Path.Combine(AppData.Directory, AppData.PackageDirectoryMap[package.Name]); + string? fileFilter = null; + + // for sharpziplib, each file in the filter + if (files is not null) + { + var regexList = new List(); + + foreach (string file in files) + regexList.Add("^" + file.Replace("\\", "\\\\") + "$"); + + fileFilter = String.Join(';', regexList); + } App.Logger.WriteLine(LOG_IDENT, $"Extracting {package.Name}..."); var fastZip = new ICSharpCode.SharpZipLib.Zip.FastZip(); - fastZip.ExtractZip(packageLocation, packageFolder, null); + fastZip.ExtractZip(package.DownloadPath, packageFolder, fileFilter); App.Logger.WriteLine(LOG_IDENT, $"Finished extracting {package.Name}"); } - - //private async Task ExtractFileFromPackage(string packageName, string fileName) - //{ - // Package? package = _versionPackageManifest.Find(x => x.Name == packageName); - - // if (package is null) - // return; - - // await DownloadPackage(package); - - // using ZipArchive archive = ZipFile.OpenRead(Path.Combine(Paths.Downloads, package.Signature)); - - // ZipArchiveEntry? entry = archive.Entries.FirstOrDefault(x => x.FullName == fileName); - - // if (entry is null) - // return; - - // string extractionPath = Path.Combine(_versionFolder, AppData.PackageDirectoryMap[package.Name], entry.FullName); - // entry.ExtractToFile(extractionPath, true); - //} -#endregion + #endregion } } diff --git a/Bloxstrap/Models/Manifest/Package.cs b/Bloxstrap/Models/Manifest/Package.cs index 6feb162e..fe66d81b 100644 --- a/Bloxstrap/Models/Manifest/Package.cs +++ b/Bloxstrap/Models/Manifest/Package.cs @@ -9,10 +9,15 @@ namespace Bloxstrap.Models.Manifest public class Package { public string Name { get; set; } = ""; + public string Signature { get; set; } = ""; + public int PackedSize { get; set; } + public int Size { get; set; } + public string DownloadPath => Path.Combine(Paths.Downloads, Signature); + public override string ToString() { return $"[{Signature}] {Name}";