Compare commits

...

39 Commits

Author SHA1 Message Date
fc6b574f47 Merge pull request 'update logging' () from impl/better-logging into main
Reviewed-on: 
2024-07-09 22:27:01 +00:00
067410e3a2 update logging 2024-07-09 18:25:23 -04:00
291ff54101 update patchclient resource 2024-06-25 18:42:33 -04:00
e2c1e9831d Merge pull request 'Switch patch apply to use XDeltaSharp' () from DrakiaXYZ/Patcher:feat-xdeltasharp into main
Reviewed-on: 
2024-06-25 12:54:58 +00:00
e5f6c681de Merge pull request 'Improve patch creation performance' () from DrakiaXYZ/Patcher:perf-nocompression into main
Reviewed-on: 

Slightly larger space should be fine, since the installer adds about 10Gb to the space check for misc files (patcher, release, metadata)

LGTM!
2024-06-25 12:54:20 +00:00
DrakiaXYZ
a22fccd42d Switch patch apply to use XDeltaSharp
- This resolves the persistent `-1073741819` error some users receive during patching
- XDeltaSharp doesn't support creating deltas, so patch creation still depends on xdelta3.exe
2024-06-24 21:36:06 -07:00
DrakiaXYZ
38f2425aeb Improve patch creation performance
- Skip MD5 hash generation if the file size differs, as it's guaranteed to be different
- Disable xdelta compression, LZMA does a better job and quicker
2024-06-24 21:09:40 -07:00
c49c076ea7 Merge pull request 'update patch files dir name' () from slugma into main
Reviewed-on: 
2024-05-24 00:20:36 +00:00
a8e8b1d221 update patch files dir name 2024-05-23 20:07:47 -04:00
21f4a58cc7 raise patching timeout to 10mins 2024-05-20 09:23:05 -04:00
66f1832c21 Merge pull request 'try-catch parallel operations' () from fix/change-ui-on-fail into main
Reviewed-on: 
2024-05-18 16:49:38 +00:00
32d910473b try-catch parallel operations 2024-05-18 12:46:44 -04:00
da36ea3438 Merge pull request 'impr/parallel-patching' () from impr/parallel-patching into main
Reviewed-on: 
2024-05-04 14:57:27 +00:00
24fe16177a finish adding parallel processing 2024-05-04 10:56:49 -04:00
5e8293fcbc stuff blah 2024-05-03 14:57:08 -04:00
e560f85e7e Merge pull request 'net8' () from net8 into main
Reviewed-on: 
2024-03-22 18:38:32 +00:00
ecdef3dcec Merge pull request 'update to create 7z archives' () from imp/7z-compression into net8
Reviewed-on: 
2024-03-22 18:33:59 +00:00
c272c0da54 update to create 7z archives 2024-03-22 13:03:07 -04:00
75194f31a5 update patch gen/client to net 8 2024-03-07 15:56:40 -05:00
854a969e37 Merge pull request 'fix-debug-process-hang' () from fix-debug-process-hang into main
Reviewed-on: 
2023-11-17 17:40:15 +00:00
0a481d1e56 add version output 2023-11-17 12:00:03 -05:00
0e97786f13 slight fixes 2023-11-15 22:03:05 -05:00
2945c5aa0a fix process buffer read, fix wrong path in access check 2023-11-15 19:44:29 -05:00
f13b49e169 Merge pull request 'added a debug parameter and a bunch of logging when it's used' () from debug-param into main
Reviewed-on: 
2023-11-15 19:34:37 +00:00
2e704f30a6 update versions 2023-11-15 11:42:03 -05:00
8cab05567d update patch gen client 2023-11-15 10:39:16 -05:00
b5a8c8ba1c added a debug parameter and a bunch of logging when it's used 2023-11-09 20:49:40 -05:00
b31c61ac4e add reactive UI logging of exceptions 2023-09-04 16:12:31 -04:00
8d0673e6f4 Merge pull request 'add elapsed time to patch views' () from feature/show-elapsed-time into main
Reviewed-on: 
2023-07-21 01:53:08 +00:00
d2cb5b6439 add elapsed time to patch views 2023-07-20 21:50:50 -04:00
ff7f0f09ef Merge branch 'main' of https://dev.sp-tarkov.com/waffle.lord/Patcher 2022-07-04 11:12:58 -04:00
567e79caf6 version bump 2022-07-04 11:12:43 -04:00
bb730aaa0d Update 'README.md'
added PatchFailed to exit codes
2022-07-04 15:07:02 +00:00
7136dcdf3c make patch client exit if an error occurs 2022-07-04 11:03:59 -04:00
164e2f55f9 Merge pull request 'fix for missing directory causing errors' () from CWX/Patcher:main into main
Reviewed-on: 
2022-07-04 12:51:24 +00:00
CWX
eb2d40547a fix for missing directory causing errors 2022-07-03 23:39:10 +01:00
91bbdac891 add os info to logs 2022-06-16 09:13:14 -04:00
b6aa4463f4 add missing logging, version bump 2022-06-15 14:01:47 -04:00
8a2a3422bf bump version 2022-06-14 21:34:26 -04:00
28 changed files with 705 additions and 222 deletions

13
Patcher/.idea/.idea.Patcher/.idea/.gitignore generated vendored Normal file

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/.idea.Patcher.iml
/modules.xml
/projectSettingsUpdater.xml
/contentModel.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AvaloniaProject">
<option name="projectPerEditor">
<map>
<entry key="PatchClient/Views/MainWindow.axaml" value="PatchClient/PatchClient.csproj" />
<entry key="PatchGenerator/Views/MainWindow.axaml" value="PatchGenerator/PatchGenerator.csproj" />
<entry key="PatchGenerator/Views/OptionsView.axaml" value="PatchGenerator/PatchGenerator.csproj" />
</map>
</option>
</component>
</project>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

@ -3,6 +3,12 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using PatchClient.ViewModels;
using PatchClient.Views;
using ReactiveUI;
using System.Reactive;
using System;
using System.Linq;
using System.Reflection;
using PatcherUtils.Model;
namespace PatchClient
{
@ -11,6 +17,11 @@ namespace PatchClient
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
RxApp.DefaultExceptionHandler = Observer.Create<Exception>((exception) =>
{
PatchLogger.LogException(exception);
});
}
public override void OnFrameworkInitializationCompleted()
@ -18,13 +29,31 @@ namespace PatchClient
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
bool autoClose = false;
bool debugOutput = false;
if(desktop.Args != null && desktop.Args.Length >= 1 && desktop.Args[0]?.ToLower() == "autoclose")
autoClose = true;
if (desktop.Args != null && desktop.Args.Length >= 1)
{
autoClose = desktop.Args.Any(x => x.ToLower() == "autoclose");
debugOutput = desktop.Args.Any(x => x.ToLower() == "debug");
}
if (debugOutput)
{
PatchLogger.LogInfo("Running in debug mode");
}
if (autoClose)
{
PatchLogger.LogInfo("Running with autoclose");
}
var version = Assembly.GetExecutingAssembly().GetName().Version;
PatchLogger.LogInfo($"Patch Client v{version?.ToString() ?? "N/A"}");
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(autoClose),
DataContext = new MainWindowViewModel(autoClose, debugOutput),
};
}

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<Nullable>enable</Nullable>
<AssemblyVersion>2.7</AssemblyVersion>
<FileVersion>2.7</FileVersion>
<AssemblyVersion>2.15.4</AssemblyVersion>
<FileVersion>2.15.4</FileVersion>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />

@ -18,11 +18,11 @@ namespace PatchClient.ViewModels
}
});
public MainWindowViewModel(bool autoClose)
public MainWindowViewModel(bool autoClose, bool debugOutput)
{
this.WhenActivated((CompositeDisposable disposable) =>
{
Router.Navigate.Execute(new PatcherViewModel(this, autoClose));
Router.Navigate.Execute(new PatcherViewModel(this, autoClose, debugOutput));
});
}
}

@ -4,11 +4,13 @@ using PatcherUtils;
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Disposables;
using System.Reflection;
using System.Threading.Tasks;
using System.Timers;
namespace PatchClient.ViewModels
{
@ -16,6 +18,10 @@ namespace PatchClient.ViewModels
{
private bool _initLineItemProgress = true;
private bool _autoClose = false;
private bool _debugOutput = false;
private Stopwatch _patchStopwatch;
private Timer _udpatePatchElapsedTimer = new Timer(1000);
public ObservableCollection<LineItemProgress> LineItems { get; set; } = new ObservableCollection<LineItemProgress>();
private string _ProgressMessage = "";
@ -39,10 +45,20 @@ namespace PatchClient.ViewModels
set => this.RaiseAndSetIfChanged(ref _PatchMessage, value);
}
private string _ElapsedPatchTimeDetails;
public string ElapsedPatchTimeDetails
{
get => _ElapsedPatchTimeDetails;
set => this.RaiseAndSetIfChanged(ref _ElapsedPatchTimeDetails, value);
}
public PatcherViewModel(IScreen Host, bool autoClose) : base(Host)
public PatcherViewModel(IScreen Host, bool autoClose, bool debugOutput) : base(Host)
{
_autoClose = autoClose;
_debugOutput = debugOutput;
ElapsedPatchTimeDetails = "Starting ...";
_udpatePatchElapsedTimer.Elapsed += _udpatePatchElapsedTimer_Elapsed;
this.WhenActivated((CompositeDisposable disposables) =>
{
@ -76,18 +92,32 @@ namespace PatchClient.ViewModels
});
}
private void _udpatePatchElapsedTimer_Elapsed(object? sender, ElapsedEventArgs e)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
ElapsedPatchTimeDetails = $"Elapsed Patch Time: {_patchStopwatch.Elapsed.ToString("hh':'mm':'ss")}";
});
}
private void RunPatcher()
{
Task.Run(async() =>
{
LazyOperations.ExtractResourcesToTempDir(Assembly.GetExecutingAssembly());
PatchHelper patcher = new PatchHelper(Environment.CurrentDirectory, null, LazyOperations.PatchFolder);
PatchHelper patcher = new PatchHelper(Environment.CurrentDirectory, null, LazyOperations.PatchFolder, _debugOutput);
patcher.ProgressChanged += patcher_ProgressChanged;
_udpatePatchElapsedTimer.Start();
_patchStopwatch = Stopwatch.StartNew();
var patchMessage = patcher.ApplyPatches();
_patchStopwatch.Stop();
_udpatePatchElapsedTimer.Stop();
LazyOperations.CleanupTempDir();
Directory.Delete(LazyOperations.PatchFolder, true);

@ -11,9 +11,6 @@ namespace PatchClient.Views
public MainWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()

@ -20,6 +20,7 @@
<!-- Current Patch Text -->
<Label Content="{Binding ProgressMessage}"/>
<Label Content="{Binding ElapsedPatchTimeDetails}" HorizontalAlignment="Right"/>
<Label Content="{Binding PatchMessage}" Grid.Row="1"
Classes="dark"/>
<ProgressBar Grid.Row="2" Value="{Binding PatchPercent}"/>

@ -4,6 +4,11 @@ using Avalonia.Markup.Xaml;
using PatchGenerator.Models;
using PatchGenerator.ViewModels;
using PatchGenerator.Views;
using ReactiveUI;
using System.Reactive;
using System;
using System.Reflection;
using PatcherUtils.Model;
namespace PatchGenerator
{
@ -12,10 +17,20 @@ namespace PatchGenerator
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
RxApp.DefaultExceptionHandler = Observer.Create<Exception>((exception) =>
{
PatchLogger.LogException(exception);
});
}
public override void OnFrameworkInitializationCompleted()
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
PatchLogger.LogInfo($"Patch Generator v{version?.ToString() ?? "N/A"}");
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.Startup += Desktop_Startup;

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<Nullable>enable</Nullable>
<AssemblyVersion>2.8</AssemblyVersion>
<FileVersion>2.8</FileVersion>
<AssemblyVersion>2.15.4</AssemblyVersion>
<FileVersion>2.15.4</FileVersion>
</PropertyGroup>
<ItemGroup>
@ -27,8 +27,7 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\7za.exe" />
<EmbeddedResource Include="Resources\PatchClient.exe" />
<None Remove="Resources\7z.dll" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.11" />
@ -42,4 +41,7 @@
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\PatchClient.exe" />
</ItemGroup>
</Project>

Binary file not shown.

@ -23,7 +23,6 @@ namespace PatchGenerator.ViewModels
{
this.WhenActivated((CompositeDisposable disposables) =>
{
if (genArgs != null && genArgs.ReadyToRun)
{
PatchGenInfo genInfo = new PatchGenInfo();
@ -31,7 +30,8 @@ namespace PatchGenerator.ViewModels
genInfo.TargetFolderPath = genArgs.TargetFolderPath;
genInfo.SourceFolderPath = genArgs.SourceFolderPath;
genInfo.PatchName = genArgs.OutputFolderName;
genInfo.AutoZip = genArgs.AutoZip;
// issues with auto zip, but it's not really used anymore so just disabling for now
genInfo.AutoZip = false;
genInfo.AutoClose = genArgs.AutoClose;
Router.Navigate.Execute(new PatchGenerationViewModel(this, genInfo));

@ -14,7 +14,11 @@ using System.IO;
using System.Reactive.Disposables;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using PatcherUtils.Model;
using Timer = System.Timers.Timer;
namespace PatchGenerator.ViewModels
{
@ -48,17 +52,28 @@ namespace PatchGenerator.ViewModels
set => this.RaiseAndSetIfChanged(ref _IndeterminateProgress, value);
}
private string _ElapsedTimeDetails;
public string ElapsedTimeDetails
{
get => _ElapsedTimeDetails;
set => this.RaiseAndSetIfChanged(ref _ElapsedTimeDetails, value);
}
private LineItem[] lineItems;
public ObservableCollection<PatchItem> PatchItemCollection { get; set; } = new ObservableCollection<PatchItem>();
public ObservableCollection<PatchItem> PatchItemLegendCollection { get; set; } = new ObservableCollection<PatchItem>();
private Stopwatch patchGenStopwatch = new Stopwatch();
private Timer updateElapsedTimeTimer = new Timer(1000);
private readonly PatchGenInfo generationInfo;
public PatchGenerationViewModel(IScreen Host, PatchGenInfo GenerationInfo) : base(Host)
{
generationInfo = GenerationInfo;
ElapsedTimeDetails = "Starting ...";
updateElapsedTimeTimer.Elapsed += UpdateElapsedTimeTimer_Elapsed;
foreach (KeyValuePair<string, IBrush> pair in PatchItemDefinitions.Colors)
{
@ -75,6 +90,14 @@ namespace PatchGenerator.ViewModels
});
}
private void UpdateElapsedTimeTimer_Elapsed(object? sender, ElapsedEventArgs e)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
ElapsedTimeDetails = $"Elapsed Patch Time: {patchGenStopwatch.Elapsed.ToString("hh':'mm':'ss")}";
});
}
public void GeneratePatches()
{
Task.Run(() =>
@ -87,17 +110,21 @@ namespace PatchGenerator.ViewModels
patcher.ProgressChanged += Patcher_ProgressChanged;
updateElapsedTimeTimer.Start();
patchGenStopwatch.Start();
var message = patcher.GeneratePatches();
patchGenStopwatch.Stop();
updateElapsedTimeTimer.Stop();
if(message.ExitCode != PatcherExitCode.Success && generationInfo.AutoClose)
{
PatchLogger.LogInfo("Exiting: Auto close on failure");
Environment.Exit((int)message.ExitCode);
}
patchGenStopwatch.Stop();
PatchLogger.LogInfo("Printing summary info ...");
PrintSummary();
StringBuilder sb = new StringBuilder()
@ -109,23 +136,33 @@ namespace PatchGenerator.ViewModels
ProgressMessage = sb.ToString();
File.Copy(LazyOperations.PatcherClientPath, $"{generationInfo.PatchName.FromCwd()}\\patcher.exe", true);
if (generationInfo.AutoZip)
{
IndeterminateProgress = true;
PatchItemCollection.Add(new PatchItem("Allowing Time for files to unlock ..."));
System.Threading.Thread.Sleep(2000);
PatchItemCollection.Add(new PatchItem("Kicking off 7zip ..."));
LazyOperations.StartZipProcess(generationInfo.PatchName.FromCwd(), $"{generationInfo.PatchName}.zip".FromCwd());
IndeterminateProgress = false;
PatchItemCollection.Add(new PatchItem("Done"));
}
PatchLogger.LogInfo("Copied patcher.exe to output folder");
// if (generationInfo.AutoZip)
// {
// PatchLogger.LogInfo("AutoZipping");
// IndeterminateProgress = true;
//
// PatchItemCollection.Add(new PatchItem("Allowing Time for files to unlock ..."));
//
// Thread.Sleep(2000);
//
// PatchItemCollection.Add(new PatchItem("Zipping patcher ..."));
//
// ProgressMessage = "Zipping patcher";
//
// IndeterminateProgress = false;
//
// var progress = new Progress<int>(p =>
// {
// PatchPercent = p;
// });
//
// LazyOperations.CompressDirectory(generationInfo.PatchName.FromCwd(), $"{generationInfo.PatchName}.7z".FromCwd(), progress);
//
// PatchItemCollection.Add(new PatchItem("Done"));
// }
if (generationInfo.AutoClose)
{

@ -11,9 +11,6 @@ namespace PatchGenerator.Views
public MainWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()

@ -15,9 +15,10 @@
Grid.Row="2" Grid.ColumnSpan="3"
Watermark="Output Folder Name"
/>
<CheckBox Content="Zip Generated Files" Grid.Row="4"
IsChecked="{Binding GenerationInfo.AutoZip}"/>
<!-- UNRESOLVED ISSUES: disabling for now -->
<!-- <CheckBox Content="Zip Generated Files" Grid.Row="4" -->
<!-- IsChecked="{Binding GenerationInfo.AutoZip}"/> -->
<Button Content="Generate Patches" Grid.ColumnSpan="3" Grid.Row="4"
HorizontalAlignment="Right"

@ -64,6 +64,9 @@
<Label Content="{Binding ProgressMessage}"/>
<Label Content="{Binding PatchPercent, StringFormat={}{0}%}" Grid.Column="2"/>
</Grid>
<Label Content="{Binding ElapsedTimeDetails}" Grid.Row="4" HorizontalAlignment="Left" Margin="10"
/>
<CheckBox Content="AutoScroll" Grid.Row="4" HorizontalAlignment="Right" Margin="10"
IsChecked="{Binding AutoScroll}"/>

@ -0,0 +1,161 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using PatcherUtils.Model;
namespace PatcherUtils.Helpers;
public class XdeltaProcessHelper
{
private readonly int _timeout = (int)TimeSpan.FromMinutes(10).TotalMilliseconds;
private string _args;
private string _sourcePath;
private string _deltaPath;
private string _decodedPath;
private bool _isDebug;
public XdeltaProcessHelper(string args, string sourcePath, string deltaPath, string decodedPath, bool isDebug)
{
_args = args;
_sourcePath = sourcePath;
_deltaPath = deltaPath;
_decodedPath = decodedPath;
_isDebug = isDebug;
}
public bool Run() => _isDebug ? RunDebug() : RunNormal();
private bool RunNormal()
{
try
{
using var proc = new Process();
proc.StartInfo = new ProcessStartInfo
{
FileName = LazyOperations.XDelta3Path,
Arguments = $"{_args} \"{_sourcePath}\" \"{_deltaPath}\" \"{_decodedPath}\"",
CreateNoWindow = true
};
proc.Start();
if (!proc.WaitForExit(_timeout))
{
PatchLogger.LogError("xdelta3 process timed out");
PatchLogger.LogDebug($"xdelta exit code: {proc.ExitCode}");
return false;
}
PatchLogger.LogDebug($"xdelta exit code: {proc.ExitCode}");
return true;
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
return false;
}
}
private bool DebugPathsCheck()
{
try
{
var stream = File.Open(_sourcePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
stream.Close();
stream.Dispose();
PatchLogger.LogDebug($"File is openable: {_sourcePath}");
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
return false;
}
try
{
var stream = File.Open(_deltaPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
stream.Close();
stream.Dispose();
PatchLogger.LogDebug($"File is openable: {_deltaPath}");
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
return false;
}
return true;
}
private bool RunDebug()
{
if (!DebugPathsCheck())
{
return false;
}
using var proc = new Process();
proc.StartInfo = new ProcessStartInfo
{
FileName = LazyOperations.XDelta3Path,
Arguments = $"{_args} \"{_sourcePath}\" \"{_deltaPath}\" \"{_decodedPath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
using AutoResetEvent outputWaitHandle = new AutoResetEvent(false);
using AutoResetEvent errorWaitHandle = new AutoResetEvent(false);
proc.OutputDataReceived += (s, e) =>
{
if (e.Data == null)
{
outputWaitHandle.Set();
}
else
{
outputBuilder.AppendLine(e.Data);
}
};
proc.ErrorDataReceived += (s, e) =>
{
if (e.Data == null)
{
errorWaitHandle.Set();
}
else
{
errorBuilder.AppendLine(e.Data);
}
};
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
if (!proc.WaitForExit(_timeout) || !outputWaitHandle.WaitOne(_timeout) || !errorWaitHandle.WaitOne(_timeout))
{
PatchLogger.LogError("xdelta3 process timed out");
PatchLogger.LogDebug($"xdelta exit code: {proc.ExitCode}");
return false;
}
PatchLogger.LogDebug("__xdelta stdout__");
PatchLogger.LogDebug(outputBuilder.ToString());
PatchLogger.LogDebug("__xdelta stderr__");
PatchLogger.LogDebug(errorBuilder.ToString());
PatchLogger.LogDebug($"xdelta exit code: {proc.ExitCode}");
return true;
}
}

@ -1,7 +1,8 @@
using PatcherUtils.Model;
using System.Diagnostics;
using System;
using PatcherUtils.Model;
using System.IO;
using System.Reflection;
using SevenZip;
namespace PatcherUtils
{
@ -16,14 +17,14 @@ namespace PatcherUtils
/// <summary>
/// The folder that the patches will be stored in
/// </summary>
public static string PatchFolder = "Aki_Patches";
public static string PatchFolder = "SPT_Patches";
private static string SevenZExe = "7za.exe";
private static string SevenZDll = "7z.dll";
/// <summary>
/// The path to the 7za.exe file in the <see cref="TempDir"/>
/// </summary>
public static string SevenZExePath = $"{TempDir}\\{SevenZExe}";
public static string SevenZDllPath = $"{TempDir}\\{SevenZDll}";
private static string PatcherClient = "PatchClient.exe";
/// <summary>
@ -79,9 +80,9 @@ namespace PatcherUtils
{
switch (resource)
{
case string a when a.EndsWith(SevenZExe):
case string a when a.EndsWith(SevenZDll):
{
StreamResourceOut(assembly, resource, SevenZExePath);
StreamResourceOut(assembly, resource, SevenZDllPath);
break;
}
case string a when a.EndsWith(PatcherClient):
@ -98,17 +99,47 @@ namespace PatcherUtils
}
}
public static void StartZipProcess(string SourcePath, string DestinationPath)
public static void CompressDirectory(string SourceDirectoryPath, string DestinationFilePath, IProgress<int> progress)
{
ProcessStartInfo procInfo = new ProcessStartInfo()
try
{
FileName = SevenZExePath,
Arguments = $"a -mm=LZMA {DestinationPath} {SourcePath}"
};
PatchLogger.LogInfo($"Compressing: {SourceDirectoryPath}");
PatchLogger.LogInfo($"Output file: {DestinationFilePath}");
var outputFile = new FileInfo(DestinationFilePath);
SevenZipBase.SetLibraryPath(SevenZDllPath);
PatchLogger.LogInfo($"7z.dll set: {SevenZDllPath}");
var compressor = new SevenZipCompressor()
{
ArchiveFormat = OutArchiveFormat.SevenZip,
CompressionMethod = CompressionMethod.Lzma2,
CompressionLevel = CompressionLevel.Normal,
PreserveDirectoryRoot = true
};
Process.Start(procInfo);
compressor.Compressing += (_, args) => { progress.Report(args.PercentDone); };
PatchLogger.LogInfo($"Zip process started");
using var outputStream = outputFile.OpenWrite();
PatchLogger.LogInfo("Starting compression");
compressor.CompressDirectory(SourceDirectoryPath, outputStream);
PatchLogger.LogInfo("Compression complete");
outputFile.Refresh();
// failed to compress data
if (!outputFile.Exists || outputFile.Length == 0)
{
PatchLogger.LogError("Failed to compress patcher");
}
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
}
/// <summary>
@ -121,7 +152,7 @@ namespace PatcherUtils
if (dir.Exists)
{
dir.Delete(true);
PatchLogger.LogInfo("Temp directory delted");
PatchLogger.LogInfo("Temp directory deleted");
}
}
}

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace PatcherUtils.Model
{
@ -23,6 +24,25 @@ namespace PatcherUtils.Model
return DateTime.Now.ToString("MM/dd/yyyy - hh:mm:ss tt]");
}
public static void LogOSInfo()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
LogToFile($"{GetTimestamp()}[OS]: Windows");
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
LogToFile($"{GetTimestamp()}[OS]: Linux");
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
LogToFile($"{GetTimestamp()}[OS]: OSX");
if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
LogToFile($"{GetTimestamp()}[OS]: FreeBSD");
LogToFile($"{GetTimestamp()}[OS]: {RuntimeInformation.OSDescription}");
}
public static void LogDebug(string message) => LogToFile($"{GetTimestamp()}[DEBUG]: {message}");
public static void LogInfo(string message) => LogToFile($"{GetTimestamp()}[INFO]: {message}");
public static void LogError(string message) => LogToFile($"{GetTimestamp()}[ERROR]: {message}");
public static void LogException(Exception ex) => LogToFile($"{GetTimestamp()}[EXCEPTION]: {ex.Message}\n\nStackTrace:\n{ex.StackTrace}");

@ -7,6 +7,7 @@
EftExeNotFound = 11,
NoPatchFolder = 12,
MissingFile = 13,
MissingDir = 14
MissingDir = 14,
PatchFailed = 15
}
}

@ -1,11 +1,15 @@
using PatchClient.Models;
using PatcherUtils.Model;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using PleOps.XdeltaSharp.Decoder;
namespace PatcherUtils
{
@ -23,6 +27,8 @@ namespace PatcherUtils
private int delCount;
private int existCount;
private bool debugOutput;
private List<LineItem> AdditionalInfo = new List<LineItem>();
/// <summary>
@ -31,7 +37,8 @@ namespace PatcherUtils
/// <remarks>Includes an array of <see cref="LineItem"/> with details for each type of patch</remarks>
public event ProgressChangedHandler ProgressChanged;
protected virtual void RaiseProgressChanged(int progress, int total, string Message = "", params LineItem[] AdditionalLineItems)
protected virtual void RaiseProgressChanged(int progress, int total, string Message = "",
params LineItem[] AdditionalLineItems)
{
int percent = (int)Math.Floor((double)progress / total * 100);
@ -45,11 +52,12 @@ namespace PatcherUtils
/// <param name="TargetFolder">The directory to compare against during patch creation.</param>
/// <param name="DeltaFolder">The directory where the patches are/will be located.</param>
/// <remarks><paramref name="TargetFolder"/> can be null if you only plan to apply patches.</remarks>
public PatchHelper(string SourceFolder, string TargetFolder, string DeltaFolder)
public PatchHelper(string SourceFolder, string TargetFolder, string DeltaFolder, bool debugOutput = false)
{
this.SourceFolder = SourceFolder;
this.TargetFolder = TargetFolder;
this.DeltaFolder = DeltaFolder;
this.debugOutput = debugOutput;
}
/// <summary>
@ -75,6 +83,12 @@ namespace PatcherUtils
var sourceInfo = new FileInfo(SourceFilePath);
var targetInfo = new FileInfo(TargetFilePath);
// Return false if file size differs
if (sourceInfo.Length != targetInfo.Length)
{
return false;
}
using (MD5 md5Service = MD5.Create())
using (var sourceStream = File.OpenRead(SourceFilePath))
using (var targetStream = File.OpenRead(TargetFilePath))
@ -84,7 +98,8 @@ namespace PatcherUtils
bool matched = Enumerable.SequenceEqual(sourceHash, targetHash);
PatchLogger.LogInfo($"Hash Check: S({sourceInfo.Name}|{Convert.ToBase64String(sourceHash)}) - T({targetInfo.Name}|{Convert.ToBase64String(targetHash)}) - Match:{matched}");
PatchLogger.LogInfo(
$"Hash Check: S({sourceInfo.Name}|{Convert.ToBase64String(sourceHash)}) - T({targetInfo.Name}|{Convert.ToBase64String(targetHash)}) - Match:{matched}");
return matched;
}
@ -95,35 +110,34 @@ namespace PatcherUtils
/// </summary>
/// <param name="SourceFilePath"></param>
/// <param name="DeltaFilePath"></param>
private void ApplyDelta(string SourceFilePath, string DeltaFilePath)
private (bool, string) ApplyDelta(string SourceFilePath, string DeltaFilePath)
{
string decodedPath = SourceFilePath + ".decoded";
Process.Start(new ProcessStartInfo
try
{
FileName = LazyOperations.XDelta3Path,
Arguments = $"-d -f -s \"{SourceFilePath}\" \"{DeltaFilePath}\" \"{decodedPath}\"",
CreateNoWindow = true
})
.WaitForExit();
if (File.Exists(decodedPath))
{
PatchLogger.LogInfo($"File delta decoded: {SourceFilePath}");
try
{
File.Move(decodedPath, SourceFilePath, true);
PatchLogger.LogInfo($"Delta applied: {DeltaFilePath}");
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
using var inputFile = new FileStream(SourceFilePath, FileMode.Open);
using var patchFile = new FileStream(DeltaFilePath, FileMode.Open);
using var decodedFile = new FileStream(decodedPath, FileMode.Create);
using var decoder = new Decoder(inputFile, patchFile, decodedFile);
decoder.Run();
}
else
catch (Exception ex)
{
PatchLogger.LogError($"Failed to decode file delta: {SourceFilePath}");
PatchLogger.LogException(ex);
return (false, ex.Message);
}
try
{
File.Move(decodedPath, SourceFilePath, true);
PatchLogger.LogInfo($"Delta applied: {DeltaFilePath}");
return (true, "");
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
return (false, ex.Message);
}
}
@ -143,18 +157,18 @@ namespace PatcherUtils
{
Directory.CreateDirectory(deltaPath.Replace(sourceFileInfo.Name + ".delta", ""));
}
catch(Exception ex)
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
Process.Start(new ProcessStartInfo
{
FileName = LazyOperations.XDelta3Path,
Arguments = $"-0 -e -f -s \"{SourceFilePath}\" \"{TargetFilePath}\" \"{deltaPath}\"",
CreateNoWindow = true
})
.WaitForExit();
{
FileName = LazyOperations.XDelta3Path,
Arguments = $"-0 -e -f -S none -s \"{SourceFilePath}\" \"{TargetFilePath}\" \"{deltaPath}\"",
CreateNoWindow = true
})
.WaitForExit();
if (File.Exists(deltaPath))
{
@ -181,7 +195,7 @@ namespace PatcherUtils
{
Directory.CreateDirectory(deltaPath.Replace(sourceFileInfo.Name + ".del", ""));
}
catch(Exception ex)
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
@ -191,7 +205,7 @@ namespace PatcherUtils
File.Create(deltaPath);
PatchLogger.LogInfo($"File Created [DEL]: {deltaPath}");
}
catch(Exception ex)
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
@ -212,7 +226,7 @@ namespace PatcherUtils
{
Directory.CreateDirectory(deltaPath.Replace(targetSourceInfo.Name + ".new", ""));
}
catch(Exception ex)
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
@ -222,7 +236,7 @@ namespace PatcherUtils
targetSourceInfo.CopyTo(deltaPath, true);
PatchLogger.LogInfo($"File Created [NEW]: {deltaPath}");
}
catch(Exception ex)
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
@ -265,9 +279,11 @@ namespace PatcherUtils
LazyOperations.ExtractResourcesToTempDir();
List<FileInfo> SourceFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories).ToList();
List<FileInfo> sourceFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories).ToList();
var targetFiles = targetDir.GetFiles("*", SearchOption.AllDirectories).ToList();
ConcurrentQueue<FileInfo> foundFilesQueue = new ConcurrentQueue<FileInfo>();
fileCountTotal = SourceFiles.Count;
fileCountTotal = targetFiles.Count;
PatchLogger.LogInfo($"Total source files: {fileCountTotal}");
@ -281,74 +297,101 @@ namespace PatcherUtils
RaiseProgressChanged(0, fileCountTotal, "Generating deltas...");
foreach (FileInfo targetFile in targetDir.GetFiles("*", SearchOption.AllDirectories))
try
{
//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)
Parallel.ForEach(targetFiles,
new ParallelOptions() { MaxDegreeOfParallelism = 5 }, targetFile =>
{
//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)
{
PatchLogger.LogInfo("::: Creating .new file :::");
CreateNewFile(targetFile.FullName);
newCount++;
filesProcessed++;
RaiseProgressChanged(filesProcessed, fileCountTotal,
$"{targetFile.FullName.Replace(TargetFolder, "...")}.new", AdditionalInfo.ToArray());
return;
}
string extension = "";
// if a matching source file was found, check the file hashes and get the delta.
// add it to the bag for removal later
if (!CompareFileHashes(sourceFile.FullName, targetFile.FullName))
{
foundFilesQueue.Enqueue(sourceFile);
PatchLogger.LogInfo("::: Creating .delta file :::");
CreateDelta(sourceFile.FullName, targetFile.FullName);
extension = ".delta";
deltaCount++;
}
else
{
foundFilesQueue.Enqueue(sourceFile);
PatchLogger.LogInfo("::: File Exists :::");
existCount++;
}
filesProcessed++;
AdditionalInfo[0].ItemValue = deltaCount;
AdditionalInfo[1].ItemValue = newCount;
AdditionalInfo[3].ItemValue = existCount;
RaiseProgressChanged(filesProcessed, fileCountTotal,
$"{targetFile.FullName.Replace(TargetFolder, "...")}{extension}", AdditionalInfo.ToArray());
});
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
// remove all queued files that were found in the source files list
PatchLogger.LogInfo(":: Updating Source List ::");
try
{
int processedQueueCount = 0;
int queueTotal = foundFilesQueue.Count;
foreach (var queuedFile in foundFilesQueue)
{
PatchLogger.LogInfo("::: Creating .new file :::");
CreateNewFile(targetFile.FullName);
newCount++;
filesProcessed++;
RaiseProgressChanged(filesProcessed, fileCountTotal, $"{targetFile.FullName.Replace(TargetFolder, "...")}.new", AdditionalInfo.ToArray());
continue;
RaiseProgressChanged(processedQueueCount, queueTotal, $"Queued file removed: {queuedFile.Name}",
AdditionalInfo.ToArray());
sourceFiles.Remove(queuedFile);
}
string extension = "";
//if a matching source file was found, check the file hashes and get the delta.
if (!CompareFileHashes(sourceFile.FullName, targetFile.FullName))
{
PatchLogger.LogInfo("::: Creating .delta file :::");
CreateDelta(sourceFile.FullName, targetFile.FullName);
extension = ".delta";
deltaCount++;
}
else
{
PatchLogger.LogInfo("::: File Exists :::");
existCount++;
}
try
{
SourceFiles.Remove(sourceFile);
}
catch(Exception ex)
{
PatchLogger.LogException(ex);
}
filesProcessed++;
AdditionalInfo[0].ItemValue = deltaCount;
AdditionalInfo[1].ItemValue = newCount;
AdditionalInfo[3].ItemValue = existCount;
RaiseProgressChanged(filesProcessed, fileCountTotal, $"{targetFile.FullName.Replace(TargetFolder, "...")}{extension}", AdditionalInfo.ToArray());
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
//Any remaining source files do not exist in the target folder and can be removed.
//reset progress info
if (SourceFiles.Count == 0)
if (sourceFiles.Count == 0)
{
PatchLogger.LogInfo("::: Patch Generation Complete :::");
return new PatchMessage("Generation Done", PatcherExitCode.Success);
}
RaiseProgressChanged(0, SourceFiles.Count, "Processing .del files...");
RaiseProgressChanged(0, sourceFiles.Count, "Processing .del files...");
filesProcessed = 0;
fileCountTotal = SourceFiles.Count;
fileCountTotal = sourceFiles.Count;
foreach (FileInfo delFile in SourceFiles)
foreach (FileInfo delFile in sourceFiles)
{
PatchLogger.LogInfo("::: Creating .del file :::");
CreateDelFile(delFile.FullName);
@ -358,7 +401,8 @@ namespace PatcherUtils
AdditionalInfo[2].ItemValue = delCount;
filesProcessed++;
RaiseProgressChanged(filesProcessed, fileCountTotal, $"{delFile.FullName.Replace(SourceFolder, "...")}.del", AdditionalInfo.ToArray());
RaiseProgressChanged(filesProcessed, fileCountTotal,
$"{delFile.FullName.Replace(SourceFolder, "...")}.del", AdditionalInfo.ToArray());
}
PatchLogger.LogInfo("::: Patch Generation Complete :::");
@ -374,6 +418,8 @@ namespace PatcherUtils
{
PatchLogger.LogInfo("::: Starting patch application :::");
PatchLogger.LogOSInfo();
//get needed directory information
DirectoryInfo sourceDir = new DirectoryInfo(SourceFolder);
DirectoryInfo deltaDir = new DirectoryInfo(DeltaFolder);
@ -386,7 +432,7 @@ namespace PatcherUtils
return new PatchMessage(message, PatcherExitCode.MissingDir);
}
if(!deltaDir.Exists)
if (!deltaDir.Exists)
{
string message = $"Could not find delta directory: {deltaDir.FullName}";
PatchLogger.LogError(message);
@ -412,83 +458,143 @@ namespace PatcherUtils
new LineItem("Files to Delete", delCount)
};
ConcurrentQueue<PatchMessage> errorsQueue = new ConcurrentQueue<PatchMessage>();
filesProcessed = 0;
fileCountTotal = deltaFiles.Count;
var patchingTokenSource = new CancellationTokenSource();
foreach (FileInfo deltaFile in deltaDir.GetFiles("*", SearchOption.AllDirectories))
try
{
switch (deltaFile.Extension)
Parallel.ForEach(deltaDir.GetFiles("*", SearchOption.AllDirectories).ToList(),
new ParallelOptions() { MaxDegreeOfParallelism = 5, CancellationToken = patchingTokenSource.Token },
deltaFile =>
{
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)
{
errorsQueue.Enqueue(new PatchMessage(
$"Failed to find matching source file for '{deltaFile.FullName}'",
PatcherExitCode.MissingFile));
patchingTokenSource.Cancel();
return;
}
PatchLogger.LogInfo("::: Applying Delta :::");
var result = ApplyDelta(sourceFile.FullName, deltaFile.FullName);
if (!result.Item1)
{
errorsQueue.Enqueue(new PatchMessage(result.Item2, PatcherExitCode.PatchFailed));
patchingTokenSource.Cancel();
return;
}
deltaCount--;
break;
}
case ".new":
{
//copy new file
string destination = Path.Join(sourceDir.FullName,
deltaFile.FullName.Replace(deltaDir.FullName, "").Replace(".new", ""));
PatchLogger.LogInfo("::: Adding New File :::");
try
{
Directory.CreateDirectory(Path.GetDirectoryName(destination));
File.Copy(deltaFile.FullName, destination, true);
PatchLogger.LogInfo($"File added: {destination}");
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
errorsQueue.Enqueue(new PatchMessage(ex.Message, PatcherExitCode.PatchFailed));
patchingTokenSource.Cancel();
return;
}
newCount--;
break;
}
case ".del":
{
//remove unneeded file
string delFilePath = Path.Join(sourceDir.FullName,
deltaFile.FullName.Replace(deltaDir.FullName, "").Replace(".del", ""));
PatchLogger.LogInfo("::: Removing Uneeded File :::");
try
{
File.Delete(delFilePath);
PatchLogger.LogInfo($"File removed: {delFilePath}");
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
errorsQueue.Enqueue(new PatchMessage(ex.Message, PatcherExitCode.PatchFailed));
patchingTokenSource.Cancel();
return;
}
delCount--;
break;
}
}
AdditionalInfo[0].ItemValue = deltaCount;
AdditionalInfo[1].ItemValue = newCount;
AdditionalInfo[2].ItemValue = delCount;
++filesProcessed;
RaiseProgressChanged(filesProcessed, fileCountTotal, deltaFile.Name, AdditionalInfo.ToArray());
});
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
if (errorsQueue.Count > 0)
{
PatchLogger.LogError($"Error queue entry count: {errorsQueue.Count}");
PatchLogger.LogError("Dequeuing errors");
PatchMessage error = null;
for (int i = 0; i < errorsQueue.Count; i++)
{
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 new PatchMessage($"Failed to find matching source file for '{deltaFile.FullName}'", PatcherExitCode.MissingFile);
}
PatchLogger.LogInfo("::: Applying Delta :::");
ApplyDelta(sourceFile.FullName, deltaFile.FullName);
deltaCount--;
break;
}
case ".new":
{
//copy new file
string destination = Path.Join(sourceDir.FullName, deltaFile.FullName.Replace(deltaDir.FullName, "").Replace(".new", ""));
PatchLogger.LogInfo("::: Adding New File :::");
try
{
File.Copy(deltaFile.FullName, destination, true);
}
catch(Exception ex)
{
PatchLogger.LogException(ex);
}
newCount--;
break;
}
case ".del":
{
//remove unneeded file
string delFilePath = Path.Join(sourceDir.FullName, deltaFile.FullName.Replace(deltaDir.FullName, "").Replace(".del", ""));
PatchLogger.LogInfo("::: Removing Uneeded File :::");
try
{
File.Delete(delFilePath);
}
catch(Exception ex)
{
PatchLogger.LogException(ex);
}
delCount--;
break;
}
if (!errorsQueue.TryDequeue(out error))
{
return new PatchMessage(
"Errors occurred during patching, but we couldn't dequeue them :(\n\nThere may be more information in the log",
PatcherExitCode.PatchFailed);
}
PatchLogger.LogError(error.Message);
}
AdditionalInfo[0].ItemValue = deltaCount;
AdditionalInfo[1].ItemValue = newCount;
AdditionalInfo[2].ItemValue = delCount;
++filesProcessed;
RaiseProgressChanged(filesProcessed, fileCountTotal, deltaFile.Name, AdditionalInfo.ToArray());
return error ?? new PatchMessage("Something went wrong :(", PatcherExitCode.PatchFailed);
}
PatchLogger.LogInfo("::: Patching Complete :::");
return new PatchMessage($"Patching Complete. You can delete the patcher.exe file.", PatcherExitCode.Success);
return new PatchMessage($"Patching Complete. You can delete the patcher.exe file.",
PatcherExitCode.Success);
}
}
}
}

@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AssemblyVersion>2.15.3</AssemblyVersion>
<FileVersion>2.15.3</FileVersion>
</PropertyGroup>
<ItemGroup>
@ -9,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\7z.dll" />
<EmbeddedResource Include="Resources\xdelta3.exe" />
</ItemGroup>
@ -18,4 +21,9 @@
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="PleOps.XdeltaSharp" Version="1.3.0" />
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.2.24" />
</ItemGroup>
</Project>

Binary file not shown.

@ -40,5 +40,6 @@ public enum PatcherExitCode
NoPatchFolder = 12, // no patch folder was found during patching (patch client only)
MissingFile = 13, // a matching file could not be found during patching (patch client only)
MissingDir = 14 // a directory could not be found during patch generation (source/target/output) (patch generator only)
PatchFailed = 15 // a patch file failed (patch client only)
}
```