Compare commits

..

No commits in common. "main" and "Patcher2.9" have entirely different histories.

28 changed files with 220 additions and 703 deletions

View File

@ -1,13 +0,0 @@
# 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

View File

@ -1,12 +0,0 @@
<?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>

View File

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

View File

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

View File

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

View File

@ -3,12 +3,6 @@ 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
{
@ -17,11 +11,6 @@ namespace PatchClient
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
RxApp.DefaultExceptionHandler = Observer.Create<Exception>((exception) =>
{
PatchLogger.LogException(exception);
});
}
public override void OnFrameworkInitializationCompleted()
@ -29,31 +18,13 @@ namespace PatchClient
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
bool autoClose = false;
bool debugOutput = false;
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"}");
if(desktop.Args != null && desktop.Args.Length >= 1 && desktop.Args[0]?.ToLower() == "autoclose")
autoClose = true;
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(autoClose, debugOutput),
DataContext = new MainWindowViewModel(autoClose),
};
}

View File

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

View File

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

View File

@ -4,13 +4,11 @@ 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
{
@ -18,10 +16,6 @@ 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 = "";
@ -45,20 +39,10 @@ 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, bool debugOutput) : base(Host)
public PatcherViewModel(IScreen Host, bool autoClose) : base(Host)
{
_autoClose = autoClose;
_debugOutput = debugOutput;
ElapsedPatchTimeDetails = "Starting ...";
_udpatePatchElapsedTimer.Elapsed += _udpatePatchElapsedTimer_Elapsed;
this.WhenActivated((CompositeDisposable disposables) =>
{
@ -92,32 +76,18 @@ 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, _debugOutput);
PatchHelper patcher = new PatchHelper(Environment.CurrentDirectory, null, LazyOperations.PatchFolder);
patcher.ProgressChanged += patcher_ProgressChanged;
_udpatePatchElapsedTimer.Start();
_patchStopwatch = Stopwatch.StartNew();
var patchMessage = patcher.ApplyPatches();
_patchStopwatch.Stop();
_udpatePatchElapsedTimer.Stop();
LazyOperations.CleanupTempDir();
Directory.Delete(LazyOperations.PatchFolder, true);

View File

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

View File

@ -20,7 +20,6 @@
<!-- 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}"/>

View File

@ -4,11 +4,6 @@ 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
{
@ -17,20 +12,10 @@ 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;

View File

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

Binary file not shown.

View File

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

View File

@ -14,11 +14,7 @@ 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
{
@ -52,28 +48,17 @@ 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)
{
@ -90,14 +75,6 @@ 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(() =>
@ -110,21 +87,17 @@ 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);
}
PatchLogger.LogInfo("Printing summary info ...");
patchGenStopwatch.Stop();
PrintSummary();
StringBuilder sb = new StringBuilder()
@ -136,33 +109,23 @@ namespace PatchGenerator.ViewModels
ProgressMessage = sb.ToString();
File.Copy(LazyOperations.PatcherClientPath, $"{generationInfo.PatchName.FromCwd()}\\patcher.exe", true);
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.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"));
}
if (generationInfo.AutoClose)
{

View File

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

View File

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

View File

@ -64,9 +64,6 @@
<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}"/>

View File

@ -1,161 +0,0 @@
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;
}
}

View File

@ -1,8 +1,7 @@
using System;
using PatcherUtils.Model;
using PatcherUtils.Model;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using SevenZip;
namespace PatcherUtils
{
@ -17,14 +16,14 @@ namespace PatcherUtils
/// <summary>
/// The folder that the patches will be stored in
/// </summary>
public static string PatchFolder = "SPT_Patches";
public static string PatchFolder = "Aki_Patches";
private static string SevenZDll = "7z.dll";
private static string SevenZExe = "7za.exe";
/// <summary>
/// The path to the 7za.exe file in the <see cref="TempDir"/>
/// </summary>
public static string SevenZDllPath = $"{TempDir}\\{SevenZDll}";
public static string SevenZExePath = $"{TempDir}\\{SevenZExe}";
private static string PatcherClient = "PatchClient.exe";
/// <summary>
@ -80,9 +79,9 @@ namespace PatcherUtils
{
switch (resource)
{
case string a when a.EndsWith(SevenZDll):
case string a when a.EndsWith(SevenZExe):
{
StreamResourceOut(assembly, resource, SevenZDllPath);
StreamResourceOut(assembly, resource, SevenZExePath);
break;
}
case string a when a.EndsWith(PatcherClient):
@ -99,47 +98,17 @@ namespace PatcherUtils
}
}
public static void CompressDirectory(string SourceDirectoryPath, string DestinationFilePath, IProgress<int> progress)
public static void StartZipProcess(string SourcePath, string DestinationPath)
{
try
ProcessStartInfo procInfo = new ProcessStartInfo()
{
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
};
FileName = SevenZExePath,
Arguments = $"a -mm=LZMA {DestinationPath} {SourcePath}"
};
compressor.Compressing += (_, args) => { progress.Report(args.PercentDone); };
Process.Start(procInfo);
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);
}
PatchLogger.LogInfo($"Zip process started");
}
/// <summary>
@ -152,7 +121,7 @@ namespace PatcherUtils
if (dir.Exists)
{
dir.Delete(true);
PatchLogger.LogInfo("Temp directory deleted");
PatchLogger.LogInfo("Temp directory delted");
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace PatcherUtils.Model
{
@ -24,25 +23,6 @@ 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}");

View File

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

View File

@ -1,15 +1,11 @@
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
{
@ -27,8 +23,6 @@ namespace PatcherUtils
private int delCount;
private int existCount;
private bool debugOutput;
private List<LineItem> AdditionalInfo = new List<LineItem>();
/// <summary>
@ -37,8 +31,7 @@ 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);
@ -52,12 +45,11 @@ 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, bool debugOutput = false)
public PatchHelper(string SourceFolder, string TargetFolder, string DeltaFolder)
{
this.SourceFolder = SourceFolder;
this.TargetFolder = TargetFolder;
this.DeltaFolder = DeltaFolder;
this.debugOutput = debugOutput;
}
/// <summary>
@ -83,12 +75,6 @@ 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))
@ -98,8 +84,7 @@ 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;
}
@ -110,34 +95,35 @@ namespace PatcherUtils
/// </summary>
/// <param name="SourceFilePath"></param>
/// <param name="DeltaFilePath"></param>
private (bool, string) ApplyDelta(string SourceFilePath, string DeltaFilePath)
private void ApplyDelta(string SourceFilePath, string DeltaFilePath)
{
string decodedPath = SourceFilePath + ".decoded";
try
Process.Start(new ProcessStartInfo
{
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();
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
return (false, ex.Message);
}
FileName = LazyOperations.XDelta3Path,
Arguments = $"-d -f -s \"{SourceFilePath}\" \"{DeltaFilePath}\" \"{decodedPath}\"",
CreateNoWindow = true
})
.WaitForExit();
try
if (File.Exists(decodedPath))
{
File.Move(decodedPath, SourceFilePath, true);
PatchLogger.LogInfo($"Delta applied: {DeltaFilePath}");
return (true, "");
PatchLogger.LogInfo($"File delta decoded: {SourceFilePath}");
try
{
File.Move(decodedPath, SourceFilePath, true);
PatchLogger.LogInfo($"Delta applied: {DeltaFilePath}");
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
}
}
catch (Exception ex)
else
{
PatchLogger.LogException(ex);
return (false, ex.Message);
PatchLogger.LogError($"Failed to decode file delta: {SourceFilePath}");
}
}
@ -157,18 +143,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 none -s \"{SourceFilePath}\" \"{TargetFilePath}\" \"{deltaPath}\"",
CreateNoWindow = true
})
.WaitForExit();
{
FileName = LazyOperations.XDelta3Path,
Arguments = $"-0 -e -f -s \"{SourceFilePath}\" \"{TargetFilePath}\" \"{deltaPath}\"",
CreateNoWindow = true
})
.WaitForExit();
if (File.Exists(deltaPath))
{
@ -195,7 +181,7 @@ namespace PatcherUtils
{
Directory.CreateDirectory(deltaPath.Replace(sourceFileInfo.Name + ".del", ""));
}
catch (Exception ex)
catch(Exception ex)
{
PatchLogger.LogException(ex);
}
@ -205,7 +191,7 @@ namespace PatcherUtils
File.Create(deltaPath);
PatchLogger.LogInfo($"File Created [DEL]: {deltaPath}");
}
catch (Exception ex)
catch(Exception ex)
{
PatchLogger.LogException(ex);
}
@ -226,7 +212,7 @@ namespace PatcherUtils
{
Directory.CreateDirectory(deltaPath.Replace(targetSourceInfo.Name + ".new", ""));
}
catch (Exception ex)
catch(Exception ex)
{
PatchLogger.LogException(ex);
}
@ -236,7 +222,7 @@ namespace PatcherUtils
targetSourceInfo.CopyTo(deltaPath, true);
PatchLogger.LogInfo($"File Created [NEW]: {deltaPath}");
}
catch (Exception ex)
catch(Exception ex)
{
PatchLogger.LogException(ex);
}
@ -279,11 +265,9 @@ namespace PatcherUtils
LazyOperations.ExtractResourcesToTempDir();
List<FileInfo> sourceFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories).ToList();
var targetFiles = targetDir.GetFiles("*", SearchOption.AllDirectories).ToList();
ConcurrentQueue<FileInfo> foundFilesQueue = new ConcurrentQueue<FileInfo>();
List<FileInfo> SourceFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories).ToList();
fileCountTotal = targetFiles.Count;
fileCountTotal = SourceFiles.Count;
PatchLogger.LogInfo($"Total source files: {fileCountTotal}");
@ -297,101 +281,74 @@ namespace PatcherUtils
RaiseProgressChanged(0, fileCountTotal, "Generating deltas...");
try
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, ""));
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)
//if the target file doesn't exist in the source files, the target file needs to be added.
if (sourceFile == null)
{
RaiseProgressChanged(processedQueueCount, queueTotal, $"Queued file removed: {queuedFile.Name}",
AdditionalInfo.ToArray());
sourceFiles.Remove(queuedFile);
PatchLogger.LogInfo("::: Creating .new file :::");
CreateNewFile(targetFile.FullName);
newCount++;
filesProcessed++;
RaiseProgressChanged(filesProcessed, fileCountTotal, $"{targetFile.FullName.Replace(TargetFolder, "...")}.new", AdditionalInfo.ToArray());
continue;
}
}
catch (Exception ex)
{
PatchLogger.LogException(ex);
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());
}
//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);
@ -401,8 +358,7 @@ 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 :::");
@ -418,8 +374,6 @@ namespace PatcherUtils
{
PatchLogger.LogInfo("::: Starting patch application :::");
PatchLogger.LogOSInfo();
//get needed directory information
DirectoryInfo sourceDir = new DirectoryInfo(SourceFolder);
DirectoryInfo deltaDir = new DirectoryInfo(DeltaFolder);
@ -432,7 +386,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);
@ -458,143 +412,83 @@ namespace PatcherUtils
new LineItem("Files to Delete", delCount)
};
ConcurrentQueue<PatchMessage> errorsQueue = new ConcurrentQueue<PatchMessage>();
filesProcessed = 0;
fileCountTotal = deltaFiles.Count;
var patchingTokenSource = new CancellationTokenSource();
try
foreach (FileInfo deltaFile in deltaDir.GetFiles("*", SearchOption.AllDirectories))
{
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++)
switch (deltaFile.Extension)
{
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);
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;
}
}
return error ?? new PatchMessage("Something went wrong :(", PatcherExitCode.PatchFailed);
AdditionalInfo[0].ItemValue = deltaCount;
AdditionalInfo[1].ItemValue = newCount;
AdditionalInfo[2].ItemValue = delCount;
++filesProcessed;
RaiseProgressChanged(filesProcessed, fileCountTotal, deltaFile.Name, AdditionalInfo.ToArray());
}
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);
}
}
}
}

View File

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

View File

@ -40,6 +40,5 @@ 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)
}
```