diff --git a/Patcher/_port/Patcher/PatchClient/App.axaml b/Patcher/_port/Patcher/PatchClient/App.axaml index d7696bb..fed9ac5 100644 --- a/Patcher/_port/Patcher/PatchClient/App.axaml +++ b/Patcher/_port/Patcher/PatchClient/App.axaml @@ -9,4 +9,20 @@ + + + + #121212 + #FFC107 + #FFFFFF + #282828 + #323947 + + + + + + + + diff --git a/Patcher/_port/Patcher/PatchClient/Assets/Styles.axaml b/Patcher/_port/Patcher/PatchClient/Assets/Styles.axaml index 7a7c3d2..309755a 100644 --- a/Patcher/_port/Patcher/PatchClient/Assets/Styles.axaml +++ b/Patcher/_port/Patcher/PatchClient/Assets/Styles.axaml @@ -1,12 +1,19 @@  - - - - - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:cc="using:PatchClient.CustomControls"> + + + + + + + + + - @@ -19,7 +26,7 @@ - + + + + + + + diff --git a/Patcher/_port/Patcher/PatchClient/CustomControls/TitleBar.axaml.cs b/Patcher/_port/Patcher/PatchClient/CustomControls/TitleBar.axaml.cs new file mode 100644 index 0000000..9fb5ffa --- /dev/null +++ b/Patcher/_port/Patcher/PatchClient/CustomControls/TitleBar.axaml.cs @@ -0,0 +1,58 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using System.Windows.Input; + +namespace PatchClient.CustomControls +{ + public partial class TitleBar : UserControl + { + public TitleBar() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public static readonly StyledProperty TitleProperty = + AvaloniaProperty.Register(nameof(Title)); + + public string Title + { + get => GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public static new readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); + + public new IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static new readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background)); + + public new IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + //Close Button Command (X Button) Property + public static readonly StyledProperty XButtonCommandProperty = + AvaloniaProperty.Register(nameof(XButtonCommand)); + + public ICommand XButtonCommand + { + get => GetValue(XButtonCommandProperty); + set => SetValue(XButtonCommandProperty, value); + } + } +} diff --git a/Patcher/_port/Patcher/PatchClient/Models/LineItemProgress.cs b/Patcher/_port/Patcher/PatchClient/Models/LineItemProgress.cs index 36264b9..fef4514 100644 --- a/Patcher/_port/Patcher/PatchClient/Models/LineItemProgress.cs +++ b/Patcher/_port/Patcher/PatchClient/Models/LineItemProgress.cs @@ -13,7 +13,7 @@ namespace PatchClient.Models set => this.RaiseAndSetIfChanged(ref _Completed, value); } - private int total = 0; + public int Total { get; private set; } = 0; private string _Info = ""; public string Info @@ -29,11 +29,22 @@ namespace PatchClient.Models set => this.RaiseAndSetIfChanged(ref _Progress, value); } + private string _ProgressInfo = ""; + public string ProgressInfo + { + get => _ProgressInfo; + set => this.RaiseAndSetIfChanged(ref _ProgressInfo, value); + } + public void UpdateProgress(int RemainingCount) { - if (Completed) return; //this doesn't work right ... need to look at it. + if (Completed) return; - Progress = (int)Math.Floor((double)RemainingCount / total * 100); + int processed = Total - RemainingCount; + + Progress = (int)Math.Floor((double)processed / Total * 100); + + ProgressInfo = $"{processed} / {Total}"; if (Progress == 100) Completed = true; } @@ -42,9 +53,9 @@ namespace PatchClient.Models { Info = Item.ItemText; - total = Item.ItemValue; + Total = Item.ItemValue; - Progress = (int)Math.Floor((double)Item.ItemValue / total * 100); + Progress = (int)Math.Floor((double)Item.ItemValue / Total * 100); } } } diff --git a/Patcher/_port/Patcher/PatchClient/PatchClient.csproj b/Patcher/_port/Patcher/PatchClient/PatchClient.csproj index 5c1c2da..feb71ac 100644 --- a/Patcher/_port/Patcher/PatchClient/PatchClient.csproj +++ b/Patcher/_port/Patcher/PatchClient/PatchClient.csproj @@ -8,6 +8,10 @@ + + + + diff --git a/Patcher/_port/Patcher/PatchClient/Properties/launchSettings.json b/Patcher/_port/Patcher/PatchClient/Properties/launchSettings.json new file mode 100644 index 0000000..97bb0df --- /dev/null +++ b/Patcher/_port/Patcher/PatchClient/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "PatchClient": { + "commandName": "Executable", + "executablePath": "Z:\\SPTarkov\\Patcher\\Patcher\\_port\\Patcher\\PatchClient\\bin\\Debug\\net5.0\\PatchClient.exe", + "workingDirectory": "C:\\Users\\JohnO\\Desktop\\12.12.2.16165" + } + } +} \ No newline at end of file diff --git a/Patcher/_port/Patcher/PatchClient/Resources/xdelta3.exe b/Patcher/_port/Patcher/PatchClient/Resources/xdelta3.exe new file mode 100644 index 0000000..1cce3c5 Binary files /dev/null and b/Patcher/_port/Patcher/PatchClient/Resources/xdelta3.exe differ diff --git a/Patcher/_port/Patcher/PatchClient/ViewModels/MainWindowViewModel.cs b/Patcher/_port/Patcher/PatchClient/ViewModels/MainWindowViewModel.cs index 4a3431a..206c971 100644 --- a/Patcher/_port/Patcher/PatchClient/ViewModels/MainWindowViewModel.cs +++ b/Patcher/_port/Patcher/PatchClient/ViewModels/MainWindowViewModel.cs @@ -1,10 +1,21 @@ +using Avalonia; using PatchClient.Models; +using ReactiveUI; using Splat; +using System.Windows.Input; namespace PatchClient.ViewModels { public class MainWindowViewModel : ViewModelBase { + public ICommand CloseCommand => ReactiveCommand.Create(() => + { + if(Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp) + { + desktopApp.MainWindow.Close(); + } + }); + public ViewNavigator navigator { get; set; } = new ViewNavigator(); public MainWindowViewModel() { diff --git a/Patcher/_port/Patcher/PatchClient/ViewModels/PatcherViewModel.cs b/Patcher/_port/Patcher/PatchClient/ViewModels/PatcherViewModel.cs index 75a910a..34b9bca 100644 --- a/Patcher/_port/Patcher/PatchClient/ViewModels/PatcherViewModel.cs +++ b/Patcher/_port/Patcher/PatchClient/ViewModels/PatcherViewModel.cs @@ -49,9 +49,9 @@ namespace PatchClient.ViewModels { Task.Run(() => { - LineItem x = new LineItem("test 1", 100); + LineItem x = new LineItem("test 1", 30); LineItem xx = new LineItem("test 2", 100); - LineItem xxx = new LineItem("test 3", 100); + LineItem xxx = new LineItem("test 3", 70); LineItems.Add(new LineItemProgress(x)); LineItems.Add(new LineItemProgress(xx)); @@ -65,11 +65,11 @@ namespace PatchClient.ViewModels foreach(var item in LineItems) { - item.UpdateProgress(i); + item.UpdateProgress(item.Total - i); } } - //navigator.SelectedViewModel = new MessageViewModel("Patch completed without issues"); + navigator.SelectedViewModel = new MessageViewModel("Test Run Complete").WithDelay(400); }); } @@ -77,40 +77,24 @@ namespace PatchClient.ViewModels { Task.Run(() => { - FilePatcher bp = new FilePatcher() - { - TargetBase = Environment.CurrentDirectory, - PatchBase = LazyOperations.PatchFolder.FromCwd() - }; + PatchHelper patcher = new PatchHelper(Environment.CurrentDirectory, null, LazyOperations.PatchFolder); - bp.ProgressChanged += Bp_ProgressChanged; + patcher.ProgressChanged += patcher_ProgressChanged; - try - { - if (bp.Run()) - { - //navigator.SelectedViewModel = new MessageViewModel("Patch completed without issues"); - } - else - { - navigator.SelectedViewModel = new MessageViewModel("Failed to patch client"); - } - } - catch (Exception ex) - { - navigator.SelectedViewModel = new MessageViewModel(ex.Message); - } + string message = patcher.ApplyPatches(); + + navigator.SelectedViewModel = new MessageViewModel(message).WithDelay(400); }); } - private void Bp_ProgressChanged(object Sender, int Progress, int Total, int Percent, string Message = "", params LineItem[] AdditionalLineItems) + private void patcher_ProgressChanged(object Sender, int Progress, int Total, int Percent, string Message = "", params LineItem[] AdditionalLineItems) { foreach (LineItem item in AdditionalLineItems) { - if (item.ItemValue <= 0) continue; if(initLineItemProgress) { + if (item.ItemValue <= 0) continue; LineItems.Add(new LineItemProgress(item)); } diff --git a/Patcher/_port/Patcher/PatchClient/ViewModels/ViewModelBase.cs b/Patcher/_port/Patcher/PatchClient/ViewModels/ViewModelBase.cs index a2896a8..9e4534b 100644 --- a/Patcher/_port/Patcher/PatchClient/ViewModels/ViewModelBase.cs +++ b/Patcher/_port/Patcher/PatchClient/ViewModels/ViewModelBase.cs @@ -2,10 +2,25 @@ using ReactiveUI; using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; +using PatchClient.Models; namespace PatchClient.ViewModels { public class ViewModelBase : ReactiveObject { + /// + /// Delay the return of the viewmodel + /// + /// The amount of time in milliseconds to delay + /// The viewmodel after the delay time + /// Useful to delay the navigation to another view via the . For instance, to allow an animation to complete. + public ViewModelBase WithDelay(int Milliseconds) + { + System.Threading.Thread.Sleep(Milliseconds); + + return this; + } } + } diff --git a/Patcher/_port/Patcher/PatchClient/Views/MainWindow.axaml b/Patcher/_port/Patcher/PatchClient/Views/MainWindow.axaml index 1e185d3..e1c50c3 100644 --- a/Patcher/_port/Patcher/PatchClient/Views/MainWindow.axaml +++ b/Patcher/_port/Patcher/PatchClient/Views/MainWindow.axaml @@ -3,19 +3,33 @@ xmlns:vm="using:PatchClient.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:cc="using:PatchClient.CustomControls" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="PatchClient.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="Patch Client" Height="300" Width="600" WindowStartupLocation="CenterScreen" + ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaChromeHints="NoChrome" + ExtendClientAreaTitleBarHeightHint="-1" > + + + - - - + + + + + + + + + diff --git a/Patcher/_port/Patcher/PatchClient/Views/PatcherView.axaml b/Patcher/_port/Patcher/PatchClient/Views/PatcherView.axaml index 128539f..530e304 100644 --- a/Patcher/_port/Patcher/PatchClient/Views/PatcherView.axaml +++ b/Patcher/_port/Patcher/PatchClient/Views/PatcherView.axaml @@ -36,6 +36,7 @@ - - References\Aki.ByteBanger.dll - References\Aki.Common.dll - - References\ComponentAce.Compression.Libs.zlib.dll - diff --git a/Patcher/_port/Patcher/PatcherUtils/FileCompare.cs b/Patcher/_port/Patcher/PatcherUtils/FileCompare.cs deleted file mode 100644 index 8c69866..0000000 --- a/Patcher/_port/Patcher/PatcherUtils/FileCompare.cs +++ /dev/null @@ -1,185 +0,0 @@ -// NOTES: -// - redo search pattern; -// - compare both directories against eachother, not just one to the other -// - add ability to handle missing directories - -using System.IO; -using Aki.Common.Utils; -using Aki.ByteBanger; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace PatcherUtils -{ - public class FileCompare - { - public string PatchBase; - public string TargetBase; - public string CompareBase; - private int fileCount; - private int fileIt; - - private int diffCount = 0; - private int newCount = 0; - private int delCount = 0; - private int matchCount = 0; - - private List TargetPaths; - private List ComparePaths; - private List AdditionalInfo = new List(); - - /// - /// Provides patch generation progress changes - /// - public event ProgressChangedHandler ProgressChanged; - protected virtual void RaiseProgressChanged(int progress, int total, string Message = "", params LineItem[] AdditionalLineItems) - { - int percent = (int)Math.Floor((double)progress / total * 100); - - ProgressChanged?.Invoke(this, progress, total, percent, Message, AdditionalLineItems); - } - - /// - /// Compare a target file to an assumed compareable file. - /// - /// The known target path - /// The assumed comparable file path - /// True if a comparison was made | False if a comparison could not be made - private bool Compare(string targetFile, string assumedCompareFile) - { - string patchFilePath = targetFile.Replace(TargetBase, PatchBase); - //we know our target file exists - byte[] targetData = VFS.ReadFile(targetFile); - - if(!File.Exists(assumedCompareFile)) - { - //save the data we won't have in our target as new - VFS.WriteFile($"{patchFilePath}.new", Zlib.Compress(targetData, ZlibCompression.Maximum)); - newCount++; - return true; - } - - //now our compare file is known to exist - byte[] compareData = VFS.ReadFile(assumedCompareFile); - - // get diffs - DiffResult result = PatchUtil.Diff(compareData, targetData); - - switch (result.Result) - { - case DiffResultType.Success: - VFS.WriteFile($"{patchFilePath}.bpf", result.PatchInfo.ToBytes()); - diffCount++; - return true; - - case DiffResultType.FilesMatch: - matchCount++; - return true; - - default: - return false; - } - } - - /// - /// Compares the base folders and generates patch files. - /// - /// True if patches were generated successfully | False if patch generation failed - public bool CompareAll() - { - DirectoryInfo targetDirInfo = new DirectoryInfo(TargetBase); - DirectoryInfo compareDirInfo = new DirectoryInfo(CompareBase); - - AdditionalInfo.Add(new LineItem("Diff Patch", 0)); - AdditionalInfo.Add(new LineItem("New Patch", 0)); - AdditionalInfo.Add(new LineItem("Del Patch", 0)); - AdditionalInfo.Add(new LineItem("Files Match", 0)); - - if (!targetDirInfo.Exists || !compareDirInfo.Exists) - { - Console.WriteLine("Target or Compare folder does not exist"); - return false; - } - - //Get all the files recursively - TargetPaths = new List(targetDirInfo.GetFiles("*.*", SearchOption.AllDirectories)); - ComparePaths = new List(compareDirInfo.GetFiles("*.*", SearchOption.AllDirectories)); - - RaiseProgressChanged(0, fileCount, "Generating diffs..."); - - /* Comparing Target files -> Compare files - * - Exists = Diff (.bfd file) - * - Doesn't Exist = New (.new file) - * - * Once everything has been compared from one side, any remaining paths in our ComparePaths - * are things that don't exist in our target and can be deleted (.del file) - */ - - for (int x = 0; x < TargetPaths.Count; x++) - { - FileInfo file = TargetPaths[x]; - - string assumedComparePath = file.DirectoryName.Replace(TargetBase, CompareBase); - - if (!Compare(file.FullName, VFS.Combine(assumedComparePath, file.Name))) - { - return false; - } - - //remove any existing files from our ComparePaths - FileInfo assumedFile = new FileInfo(VFS.Combine(assumedComparePath, file.Name)); - if (assumedFile.Exists && ComparePaths.Exists(x => x.FullName == assumedFile.FullName)) - { - ComparePaths.Remove(ComparePaths.Where(x => x.FullName == assumedFile.FullName).FirstOrDefault()); - } - - - AdditionalInfo[0].ItemValue = diffCount; - AdditionalInfo[1].ItemValue = newCount; - AdditionalInfo[3].ItemValue = matchCount; - - fileIt++; - RaiseProgressChanged(fileIt, fileCount, file.Name, AdditionalInfo.ToArray()); - } - - - if (ComparePaths.Count == 0) - { - //if there are no files to delete, just return true - return true; - } - - //progress reset for files that need to be deleted - RaiseProgressChanged(0, ComparePaths.Count, "Processing .del files..."); - fileIt = 0; - fileCount = ComparePaths.Count; - - //the paths remaining in ComparePaths don't exist in our target and need to be removed during patching. - foreach (FileInfo file in ComparePaths) - { - //add del files replace root dir with patch base - string patchFilePath = file.FullName.Replace(CompareBase, PatchBase); - VFS.WriteFile($"{patchFilePath}.del", new byte[0]); - - delCount++; - AdditionalInfo[2].ItemValue = delCount; - - fileIt++; - RaiseProgressChanged(fileIt, fileCount, "", AdditionalInfo.ToArray()); - } - - return true; - } - - public FileCompare(string TargetBase, string CompareBase, string PatchBase) - { - this.TargetBase = TargetBase; - this.CompareBase = CompareBase; - this.PatchBase = PatchBase; - - fileCount = VFS.GetFilesCount(TargetBase); - fileIt = 0; - } - } -} diff --git a/Patcher/_port/Patcher/PatcherUtils/FilePatcher.cs b/Patcher/_port/Patcher/PatcherUtils/FilePatcher.cs deleted file mode 100644 index a82486f..0000000 --- a/Patcher/_port/Patcher/PatcherUtils/FilePatcher.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.IO; -using Aki.Common.Utils; -using Aki.ByteBanger; -using System.Collections.Generic; -using System.Linq; - -namespace PatcherUtils -{ - public class FilePatcher - { - public string TargetBase; - public string PatchBase; - private int fileCount; - private int fileIt; - - private int diffCount; - private int newCount; - private int delCount; - - private List AdditionalInfo; - - - public event ProgressChangedHandler ProgressChanged; - - protected virtual void RaiseProgressChanged(int progress, int total, string Message = "", params LineItem[] AdditionalLineItems) - { - int percent = (int)Math.Floor((double)progress / total * 100); - - ProgressChanged?.Invoke(this, progress, total, percent, Message, AdditionalLineItems); - } - - public bool Patch(string targetfile, string patchfile) - { - byte[] target = VFS.ReadFile(targetfile); - byte[] patch = VFS.ReadFile(patchfile); - - PatchResult result = PatchUtil.Patch(target, PatchInfo.FromBytes(patch)); - - switch (result.Result) - { - case PatchResultType.Success: - VFS.WriteFile(targetfile, result.PatchedData); - return true; - - case PatchResultType.AlreadyPatched: - case PatchResultType.InputChecksumMismatch: - case PatchResultType.InputLengthMismatch: - return true; - - case PatchResultType.OutputChecksumMismatch: - default: - return false; - } - } - - private bool PatchAll(string targetpath, string patchpath) - { - DirectoryInfo di = new DirectoryInfo(patchpath); - - foreach (FileInfo file in di.GetFiles()) - { - FileInfo target = null; - - switch (file.Extension) - { - // patch - case ".bpf": - { - target = new FileInfo(VFS.Combine(targetpath, file.Name.Replace(".bpf", ""))); - - if (!Patch(target.FullName, file.FullName)) - { - // patch failed - return false; - } - - diffCount--; - } - break; - - // add new files - case ".new": - { - target = new FileInfo(VFS.Combine(targetpath, file.Name.Replace(".new", ""))); - VFS.WriteFile(target.FullName, Zlib.Decompress(VFS.ReadFile(file.FullName))); - newCount--; - } - break; - - // delete old files - case ".del": - { - target = new FileInfo(VFS.Combine(targetpath, file.Name.Replace(".del", ""))); - target.IsReadOnly = false; - target.Delete(); - delCount--; - } - break; - } - - AdditionalInfo[0].ItemValue = diffCount; - AdditionalInfo[1].ItemValue = newCount; - AdditionalInfo[2].ItemValue = delCount; - - ++fileIt; - RaiseProgressChanged(fileIt, fileCount, target.Name, AdditionalInfo.ToArray()); - } - - foreach (DirectoryInfo directory in di.GetDirectories()) - { - PatchAll(VFS.Combine(targetpath, directory.Name), directory.FullName); - } - - di.Refresh(); - - if (di.GetFiles().Length == 0 && di.GetDirectories().Length == 0) - { - // remove empty folders - di.Delete(); - } - - return true; - } - - public bool Run() - { - fileCount = VFS.GetFilesCount(PatchBase); - - FileInfo[] files = new DirectoryInfo(PatchBase).GetFiles("*.*", SearchOption.AllDirectories); - - diffCount = files.Where(x => x.Extension == ".bpf").Count(); - newCount = files.Where(x => x.Extension == ".new").Count(); - delCount = files.Where(x => x.Extension == ".del").Count(); - - AdditionalInfo = new List() - { - new LineItem("Patches Remaining", diffCount), - new LineItem("New Files to Inflate", newCount), - new LineItem("Files to Delete", delCount) - }; - - fileIt = 0; - return PatchAll(TargetBase, PatchBase); - } - } -} diff --git a/Patcher/_port/Patcher/PatcherUtils/LazyOperations.cs b/Patcher/_port/Patcher/PatcherUtils/LazyOperations.cs index d38d6aa..06d4098 100644 --- a/Patcher/_port/Patcher/PatcherUtils/LazyOperations.cs +++ b/Patcher/_port/Patcher/PatcherUtils/LazyOperations.cs @@ -12,12 +12,13 @@ namespace PatcherUtils /// public static string TempDir = "PATCHER_TEMP".FromCwd(); - private static string SevenZExe = "7za.exe"; /// /// The folder that the patches will be stored in /// - public static string PatchFolder = "Aki_Data\\Patcher"; + public static string PatchFolder = "Aki_Patches"; + + private static string SevenZExe = "7za.exe"; /// /// The path to the 7za.exe file in the @@ -30,6 +31,13 @@ namespace PatcherUtils /// public static string PatcherClientPath = $"{TempDir}\\{PatcherClient}"; + private static string XDelta3EXE = "xdelta3.exe"; + + /// + /// The path to the xdelta3.exe flie in the + /// + public static string XDelta3Path = $"{TempDir}\\{XDelta3EXE}"; + /// /// Streams embedded resources out of the assembly /// @@ -73,6 +81,11 @@ namespace PatcherUtils StreamResourceOut(resource, PatcherClientPath); break; } + case string a when a.EndsWith(XDelta3EXE): + { + StreamResourceOut(resource, XDelta3Path); + break; + } } } } diff --git a/Patcher/_port/Patcher/PatcherUtils/PatchHelper.cs b/Patcher/_port/Patcher/PatcherUtils/PatchHelper.cs new file mode 100644 index 0000000..a72db81 --- /dev/null +++ b/Patcher/_port/Patcher/PatcherUtils/PatchHelper.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; + +namespace PatcherUtils +{ + public class PatchHelper + { + private string SourceFolder = ""; + private string TargetFolder = ""; + private string DeltaFolder = ""; + + private int fileCountTotal; + private int filesProcessed; + + private int deltaCount; + private int newCount; + private int delCount; + + private List AdditionalInfo = new List(); + + public event ProgressChangedHandler ProgressChanged; + + protected virtual void RaiseProgressChanged(int progress, int total, string Message = "", params LineItem[] AdditionalLineItems) + { + int percent = (int)Math.Floor((double)progress / total * 100); + + ProgressChanged?.Invoke(this, progress, total, percent, Message, AdditionalLineItems); + } + + public PatchHelper(string SourceFolder, string TargetFolder, string DeltaFolder) + { + this.SourceFolder = SourceFolder; + this.TargetFolder = TargetFolder; + this.DeltaFolder = DeltaFolder; + } + + private string GetDeltaPath(string SourceFilePath, string SourceFolderPath, string FileExtension) + { + return Path.Join(DeltaFolder, $"{SourceFilePath.Replace(SourceFolderPath, "")}.{FileExtension}"); + } + + private bool CompareFileHashes(string SourceFilePath, string TargetFilePath) + { + using (MD5 md5Service = MD5.Create()) + using (var sourceStream = File.OpenRead(SourceFilePath)) + using (var targetStream = File.OpenRead(TargetFilePath)) + { + byte[] sourceHash = md5Service.ComputeHash(sourceStream); + byte[] targetHash = md5Service.ComputeHash(targetStream); + + return Enumerable.SequenceEqual(sourceHash, targetHash); + } + } + + private void ApplyDelta(string SourceFilePath, string DeltaFilePath) + { + string decodedPath = SourceFilePath + ".decoded"; + + Process.Start(new ProcessStartInfo + { + FileName = LazyOperations.XDelta3Path, + Arguments = $"-d -f -s \"{SourceFilePath}\" \"{DeltaFilePath}\" \"{decodedPath}\"", + CreateNoWindow = true + }) + .WaitForExit(); + + if(File.Exists(decodedPath)) + { + File.Delete(SourceFilePath); + File.Move(decodedPath, SourceFilePath); + } + } + + private void CreateDelta(string SourceFilePath, string TargetFilePath) + { + FileInfo sourceFileInfo = new FileInfo(SourceFilePath); + + string deltaPath = GetDeltaPath(SourceFilePath, SourceFolder, "delta"); + + Directory.CreateDirectory(deltaPath.Replace(sourceFileInfo.Name+".delta", "")); + + //TODO - don't hardcode FileName + + Process.Start(new ProcessStartInfo + { + FileName = LazyOperations.XDelta3Path, + Arguments = $"-0 -e -f -s \"{SourceFilePath}\" \"{TargetFilePath}\" \"{deltaPath}\"", + CreateNoWindow = true + }) + .WaitForExit(); + } + + private void CreateDelFile(string SourceFile) + { + FileInfo sourceFileInfo = new FileInfo(SourceFile); + + string deltaPath = GetDeltaPath(SourceFile, SourceFolder, "del"); + + Directory.CreateDirectory(deltaPath.Replace(sourceFileInfo.Name+".del", "")); + + File.Create(deltaPath); + } + + private void CreateNewFile(string TargetFile) + { + FileInfo targetSourceInfo = new FileInfo(TargetFile); + + string deltaPath = GetDeltaPath(TargetFile, TargetFolder, "new"); + + Directory.CreateDirectory(deltaPath.Replace(targetSourceInfo.Name+".new", "")); + + targetSourceInfo.CopyTo(deltaPath, true); + } + + public bool GeneratePatches() + { + //get all directory information needed + DirectoryInfo sourceDir = new DirectoryInfo(SourceFolder); + DirectoryInfo targetDir = new DirectoryInfo(TargetFolder); + DirectoryInfo deltaDir = Directory.CreateDirectory(DeltaFolder); + + //make sure all directories exist + if (!sourceDir.Exists || !targetDir.Exists || !deltaDir.Exists) + { + //One of the directories doesn't exist + return false; + } + + List SourceFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories).ToList(); + + fileCountTotal = SourceFiles.Count; + + AdditionalInfo.Clear(); + AdditionalInfo.Add(new LineItem("Delta Patch", 0)); + AdditionalInfo.Add(new LineItem("New Patch", 0)); + AdditionalInfo.Add(new LineItem("Del Patch", 0)); + + filesProcessed = 0; + + RaiseProgressChanged(0, fileCountTotal, "Generating deltas..."); + + foreach (FileInfo targetFile in targetDir.GetFiles("*", SearchOption.AllDirectories)) + { + //find a matching source file based on the relative path of the file + FileInfo sourceFile = SourceFiles.Find(f => f.FullName.Replace(sourceDir.FullName, "") == targetFile.FullName.Replace(targetDir.FullName, "")); + + //if the target file doesn't exist in the source files, the target file needs to be added. + if (sourceFile == null) + { + CreateNewFile(targetFile.FullName); + + newCount++; + filesProcessed++; + + RaiseProgressChanged(filesProcessed, fileCountTotal, targetFile.Name, AdditionalInfo.ToArray()); + + continue; + } + + //if a matching source file was found, check the file hashes and get the delta. + if(CompareFileHashes(sourceFile.FullName, targetFile.FullName)) + { + CreateDelta(sourceFile.FullName, targetFile.FullName); + deltaCount++; + } + + SourceFiles.Remove(sourceFile); + + filesProcessed++; + + AdditionalInfo[0].ItemValue = deltaCount; + AdditionalInfo[1].ItemValue = newCount; + + RaiseProgressChanged(filesProcessed, fileCountTotal, targetFile.Name, AdditionalInfo.ToArray()); + } + + //Any remaining source files do not exist in the target folder and can be removed. + //reset progress info + RaiseProgressChanged(0, SourceFiles.Count, "Processing .del files..."); + filesProcessed = 0; + fileCountTotal = SourceFiles.Count; + + foreach (FileInfo delFile in SourceFiles) + { + CreateDelFile(delFile.FullName); + + delCount++; + + AdditionalInfo[2].ItemValue = delCount; + + filesProcessed++; + RaiseProgressChanged(filesProcessed, fileCountTotal, "", AdditionalInfo.ToArray()); + } + + return true; + } + + public string ApplyPatches() + { + //get needed directory information + DirectoryInfo sourceDir = new DirectoryInfo(SourceFolder); + DirectoryInfo deltaDir = new DirectoryInfo(DeltaFolder); + + //check directories exist + if (!sourceDir.Exists || !deltaDir.Exists) + { + return "One of the supplied directories doesn't exist"; + } + + LazyOperations.CleanupTempDir(); + LazyOperations.PrepTempDir(); + + List SourceFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories).ToList(); + + List deltaFiles = deltaDir.GetFiles("*", SearchOption.AllDirectories).ToList(); + + deltaCount = deltaFiles.Where(x => x.Extension == ".delta").Count(); + newCount = deltaFiles.Where(x => x.Extension == ".new").Count(); + delCount = deltaFiles.Where(x => x.Extension == ".del").Count(); + + + AdditionalInfo = new List() + { + new LineItem("Patches Remaining", deltaCount), + new LineItem("New Files to Add", newCount), + new LineItem("Files to Delete", delCount) + }; + + filesProcessed = 0; + + fileCountTotal = deltaFiles.Count; + + foreach (FileInfo deltaFile in deltaDir.GetFiles("*", SearchOption.AllDirectories)) + { + switch(deltaFile.Extension) + { + case ".delta": + { + //apply delta + FileInfo sourceFile = SourceFiles.Find(f => f.FullName.Replace(sourceDir.FullName, "") == deltaFile.FullName.Replace(deltaDir.FullName, "").Replace(".delta", "")); + + if(sourceFile == null) + { + return $"Failed to find matching source file for '{deltaFile.FullName}'"; + } + + ApplyDelta(sourceFile.FullName, deltaFile.FullName); + + deltaCount--; + + break; + } + case ".new": + { + if(newCount == 2 || newCount == 1 || newCount == 0) + { + + } + + //copy new file + string destination = Path.Join(sourceDir.FullName, deltaFile.FullName.Replace(deltaDir.FullName, "").Replace(".new", "")); + + File.Copy(deltaFile.FullName, destination); + + newCount--; + + break; + } + case ".del": + { + //remove unneeded file + string delFilePath = Path.Join(sourceDir.FullName, deltaFile.FullName.Replace(deltaDir.FullName, "").Replace(".del", "")); + + File.Delete(delFilePath); + + delCount--; + + break; + } + } + + AdditionalInfo[0].ItemValue = deltaCount; + AdditionalInfo[1].ItemValue = newCount; + AdditionalInfo[2].ItemValue = delCount; + + ++filesProcessed; + RaiseProgressChanged(filesProcessed, fileCountTotal, deltaFile.Name, AdditionalInfo.ToArray()); + } + + LazyOperations.CleanupTempDir(); + + Directory.Delete(LazyOperations.PatchFolder, true); + + return $"Patching Complete. You can delete the patcher.exe file."; + } + } +} diff --git a/Patcher/_port/Patcher/PatcherUtils/PatcherUtils.csproj b/Patcher/_port/Patcher/PatcherUtils/PatcherUtils.csproj index 2e72d93..3943a8d 100644 --- a/Patcher/_port/Patcher/PatcherUtils/PatcherUtils.csproj +++ b/Patcher/_port/Patcher/PatcherUtils/PatcherUtils.csproj @@ -7,23 +7,19 @@ + + - - ..\PatchGenerator\References\Aki.ByteBanger.dll - ..\PatchGenerator\References\Aki.Common.dll - - ..\PatchGenerator\References\ComponentAce.Compression.Libs.zlib.dll - diff --git a/Patcher/_port/Patcher/PatcherUtils/Resources/xdelta3.exe b/Patcher/_port/Patcher/PatcherUtils/Resources/xdelta3.exe new file mode 100644 index 0000000..1cce3c5 Binary files /dev/null and b/Patcher/_port/Patcher/PatcherUtils/Resources/xdelta3.exe differ