Refactor C# code to imperative, top-level statements style

Updated the existing C# code into a more modern, imperative and top-level statements style. This involves shortening the code by removing unnecessary parts like additional brackets and explicit namespace declarations. It's done to improve clarity and readability.
This commit is contained in:
Philipp Heenemann 2023-07-12 09:19:33 +02:00
parent d6aaeda28c
commit a8b91f4ee6
44 changed files with 1905 additions and 2005 deletions

View File

@ -4,26 +4,25 @@ using Avalonia.Markup.Xaml;
using SPTInstaller.ViewModels;
using SPTInstaller.Views;
namespace SPTInstaller
namespace SPTInstaller;
public partial class App : Application
{
public partial class App : Application
public override void Initialize()
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainWindow
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
}

View File

@ -1,20 +1,19 @@
using Avalonia;
using Avalonia.Interactivity;
namespace SPTInstaller.Behaviors
namespace SPTInstaller.Behaviors;
public class SpanBehavior : AvaloniaObject
{
public class SpanBehavior : AvaloniaObject
public static readonly AttachedProperty<bool> SpanProperty = AvaloniaProperty.RegisterAttached<SpanBehavior, Interactive, bool>("Span");
public static void SetSpan(AvaloniaObject element, bool value)
{
public static readonly AttachedProperty<bool> SpanProperty = AvaloniaProperty.RegisterAttached<SpanBehavior, Interactive, bool>("Span");
element.SetValue(SpanProperty, value);
}
public static void SetSpan(AvaloniaObject element, bool value)
{
element.SetValue(SpanProperty, value);
}
public static bool GetSpan(AvaloniaObject element)
{
return element.GetValue(SpanProperty);
}
public static bool GetSpan(AvaloniaObject element)
{
return element.GetValue(SpanProperty);
}
}

View File

@ -2,67 +2,65 @@
using SharpCompress;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SPTInstaller.Controllers
namespace SPTInstaller.Controllers;
public class InstallController
{
public class InstallController
public event EventHandler<IProgressableTask> TaskChanged = delegate { };
private IPreCheck[] _preChecks { get; set; }
private IProgressableTask[] _tasks { get; set; }
public InstallController(IProgressableTask[] tasks, IPreCheck[] preChecks = null)
{
public event EventHandler<IProgressableTask> TaskChanged = delegate { };
_tasks = tasks;
_preChecks = preChecks;
}
private IPreCheck[] _preChecks { get; set; }
private IProgressableTask[] _tasks { get; set; }
public async Task<IResult> RunPreChecks()
{
Log.Information("-<>--<>- Running PreChecks -<>--<>-");
var requiredResults = new List<IResult>();
public InstallController(IProgressableTask[] tasks, IPreCheck[] preChecks = null)
_preChecks.ForEach(x => x.IsPending = true);
foreach (var check in _preChecks)
{
_tasks = tasks;
_preChecks = preChecks;
var result = await check.RunCheck();
Log.Information($"PreCheck: {check.Name} ({(check.IsRequired ? "Required" : "Optional")}) -> {(result.Succeeded ? "Passed" : "Failed")}");
if (check.IsRequired)
{
requiredResults.Add(result);
}
}
public async Task<IResult> RunPreChecks()
if (requiredResults.Any(result => !result.Succeeded))
{
Log.Information("-<>--<>- Running PreChecks -<>--<>-");
var requiredResults = new List<IResult>();
_preChecks.ForEach(x => x.IsPending = true);
foreach (var check in _preChecks)
{
var result = await check.RunCheck();
Log.Information($"PreCheck: {check.Name} ({(check.IsRequired ? "Required" : "Optional")}) -> {(result.Succeeded ? "Passed" : "Failed")}");
if (check.IsRequired)
{
requiredResults.Add(result);
}
}
foreach(var result in requiredResults)
{
if (!result.Succeeded)
return Result.FromError("Some required checks have failed");
}
return Result.FromSuccess();
return Result.FromError("Some required checks have failed");
}
public async Task<IResult> RunTasks()
return Result.FromSuccess();
}
public async Task<IResult> RunTasks()
{
Log.Information("-<>--<>- Running Installer Tasks -<>--<>-");
foreach (var task in _tasks)
{
Log.Information("-<>--<>- Running Installer Tasks -<>--<>-");
TaskChanged?.Invoke(null, task);
foreach (var task in _tasks)
{
TaskChanged?.Invoke(null, task);
var result = await task.RunAsync();
var result = await task.RunAsync();
if (!result.Succeeded) return result;
}
return Result.FromSuccess("Install Complete. Happy Playing!");
if (!result.Succeeded) return result;
}
return Result.FromSuccess("Install Complete. Happy Playing!");
}
}

View File

@ -1,29 +1,27 @@
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace SPTInstaller.Converters
namespace SPTInstaller.Converters;
public class InvertedProgressConverter : IValueConverter
{
public class InvertedProgressConverter : IValueConverter
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
if( value is int progress)
{
if( value is int progress)
{
return 100 - progress;
}
return value;
return 100 - progress;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if ( value is int invertedProgress)
{
return 100 - invertedProgress;
}
return value;
}
return value;
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if ( value is int invertedProgress)
{
return 100 - invertedProgress;
}
return value;
}
}

View File

@ -1,77 +1,75 @@
using Avalonia;
using Avalonia.Controls;
using SPTInstaller.Behaviors;
using System;
using System.Linq;
namespace SPTInstaller.CustomControls
namespace SPTInstaller.CustomControls;
public class DistributedSpacePanel : Panel
{
public class DistributedSpacePanel : Panel
protected override Size MeasureOverride(Size availableSize)
{
protected override Size MeasureOverride(Size availableSize)
var children = Children;
for (int i = 0; i < children.Count; i++)
{
var children = Children;
for (int i = 0; i < children.Count; i++)
{
// measure child objects so we can use their desired size in the arrange override
var child = children[i];
child.Measure(availableSize);
}
// we want to use all available space
return availableSize;
// measure child objects so we can use their desired size in the arrange override
var child = children[i];
child.Measure(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
// we want to use all available space
return availableSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var children = Children;
Rect rcChild = new Rect(finalSize);
double previousChildSize = 0.0;
// get child objects that don't want to span the entire control
var nonSpanningChildren = children.Where(x => x.GetValue(SpanBehavior.SpanProperty) == false).ToList();
// get the total height off all non-spanning child objects
var totalChildHeight = nonSpanningChildren.Select(x => x.DesiredSize.Height).Sum();
// remove the total child height from our controls final size and divide it by the total non-spanning child objects
// except the last one, since it needs no space after it
var spacing = (finalSize.Height - totalChildHeight) / (nonSpanningChildren.Count - 1);
for (int i = 0; i < children.Count; i++)
{
var children = Children;
Rect rcChild = new Rect(finalSize);
double previousChildSize = 0.0;
var child = children[i];
// get child objects that don't want to span the entire control
var nonSpanningChildren = children.Where(x => x.GetValue(SpanBehavior.SpanProperty) == false).ToList();
var spanChild = child.GetValue(SpanBehavior.SpanProperty);
// get the total height off all non-spanning child objects
var totalChildHeight = nonSpanningChildren.Select(x => x.DesiredSize.Height).Sum();
// remove the total child height from our controls final size and divide it by the total non-spanning child objects
// except the last one, since it needs no space after it
var spacing = (finalSize.Height - totalChildHeight) / (nonSpanningChildren.Count - 1);
for (int i = 0; i < children.Count; i++)
if (spanChild)
{
var child = children[i];
// move any spanning children to the top of the array to push them behind the other controls (visually)
children.Move(i, 0);
var spanChild = child.GetValue(SpanBehavior.SpanProperty);
if (spanChild)
{
// move any spanning children to the top of the array to push them behind the other controls (visually)
children.Move(i, 0);
rcChild = rcChild.WithY(0)
.WithX(0)
.WithHeight(finalSize.Height)
.WithWidth(finalSize.Width);
rcChild = rcChild.WithY(0)
.WithX(0)
.WithHeight(finalSize.Height)
.WithWidth(finalSize.Width);
child.Arrange(rcChild);
continue;
};
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
previousChildSize = child.DesiredSize.Height;
rcChild = rcChild.WithHeight(previousChildSize)
.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
previousChildSize += spacing;
child.Arrange(rcChild);
}
continue;
};
return finalSize;
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
previousChildSize = child.DesiredSize.Height;
rcChild = rcChild.WithHeight(previousChildSize)
.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
previousChildSize += spacing;
child.Arrange(rcChild);
}
return finalSize;
}
}

View File

@ -1,62 +1,57 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Threading;
using ReactiveUI;
using System.Windows.Input;
namespace SPTInstaller.CustomControls
namespace SPTInstaller.CustomControls;
public partial class PreCheckItem : UserControl
{
public partial class PreCheckItem : UserControl
public PreCheckItem()
{
public PreCheckItem()
{
InitializeComponent();
}
public string PreCheckName
{
get => GetValue(PreCheckNameProperty);
set => SetValue(PreCheckNameProperty, value);
}
public static readonly StyledProperty<string> PreCheckNameProperty =
AvaloniaProperty.Register<PreCheckItem, string>(nameof(PreCheckName));
public bool IsRunning
{
get => GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
public static readonly StyledProperty<bool> IsRunningProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRunning));
public bool IsPending
{
get => GetValue(IsPendingProperty);
set => SetValue(IsPendingProperty, value);
}
public static readonly StyledProperty<bool> IsPendingProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsPending));
public bool Passed
{
get => GetValue(PassedProperty);
set => SetValue(PassedProperty, value);
}
public static readonly StyledProperty<bool> PassedProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(Passed));
public bool IsRequired
{
get => GetValue(IsRequiredProperty);
set => SetValue(IsRequiredProperty, value);
}
public static readonly StyledProperty<bool> IsRequiredProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRequired));
InitializeComponent();
}
public string PreCheckName
{
get => GetValue(PreCheckNameProperty);
set => SetValue(PreCheckNameProperty, value);
}
public static readonly StyledProperty<string> PreCheckNameProperty =
AvaloniaProperty.Register<PreCheckItem, string>(nameof(PreCheckName));
public bool IsRunning
{
get => GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
public static readonly StyledProperty<bool> IsRunningProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRunning));
public bool IsPending
{
get => GetValue(IsPendingProperty);
set => SetValue(IsPendingProperty, value);
}
public static readonly StyledProperty<bool> IsPendingProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsPending));
public bool Passed
{
get => GetValue(PassedProperty);
set => SetValue(PassedProperty, value);
}
public static readonly StyledProperty<bool> PassedProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(Passed));
public bool IsRequired
{
get => GetValue(IsRequiredProperty);
set => SetValue(IsRequiredProperty, value);
}
public static readonly StyledProperty<bool> IsRequiredProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRequired));
}

View File

@ -2,76 +2,75 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace SPTInstaller.CustomControls
namespace SPTInstaller.CustomControls;
public partial class ProgressableTaskItem : UserControl
{
public partial class ProgressableTaskItem : UserControl
public ProgressableTaskItem()
{
public ProgressableTaskItem()
{
InitializeComponent();
}
public string TaskId
{
get => GetValue(TaskIdProperty);
set => SetValue(TaskIdProperty, value);
}
public static readonly StyledProperty<string> TaskIdProperty =
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskId));
public string TaskName
{
get => GetValue(TaskNameProperty);
set => SetValue(TaskNameProperty, value);
}
public static readonly StyledProperty<string> TaskNameProperty =
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskName));
public bool IsCompleted
{
get => GetValue(IsCompletedProperty);
set => SetValue(IsCompletedProperty, value);
}
public static readonly StyledProperty<bool> IsCompletedProperty =
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsCompleted));
public bool IsRunning
{
get => GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
public static readonly StyledProperty<bool> IsRunningProperty =
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsRunning));
public IBrush PendingColor
{
get => GetValue(PendingColorProperty);
set => SetValue(PendingColorProperty, value);
}
public static readonly StyledProperty<IBrush> PendingColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
public IBrush RunningColor
{
get => GetValue(RunningColorProperty);
set => SetValue(RunningColorProperty, value);
}
public static readonly StyledProperty<IBrush> RunningColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
public IBrush CompletedColor
{
get => GetValue(CompletedColorProperty);
set => SetValue(CompletedColorProperty, value);
}
public static readonly StyledProperty<IBrush> CompletedColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
InitializeComponent();
}
public string TaskId
{
get => GetValue(TaskIdProperty);
set => SetValue(TaskIdProperty, value);
}
public static readonly StyledProperty<string> TaskIdProperty =
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskId));
public string TaskName
{
get => GetValue(TaskNameProperty);
set => SetValue(TaskNameProperty, value);
}
public static readonly StyledProperty<string> TaskNameProperty =
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskName));
public bool IsCompleted
{
get => GetValue(IsCompletedProperty);
set => SetValue(IsCompletedProperty, value);
}
public static readonly StyledProperty<bool> IsCompletedProperty =
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsCompleted));
public bool IsRunning
{
get => GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
public static readonly StyledProperty<bool> IsRunningProperty =
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsRunning));
public IBrush PendingColor
{
get => GetValue(PendingColorProperty);
set => SetValue(PendingColorProperty, value);
}
public static readonly StyledProperty<IBrush> PendingColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
public IBrush RunningColor
{
get => GetValue(RunningColorProperty);
set => SetValue(RunningColorProperty, value);
}
public static readonly StyledProperty<IBrush> RunningColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
public IBrush CompletedColor
{
get => GetValue(CompletedColorProperty);
set => SetValue(CompletedColorProperty, value);
}
public static readonly StyledProperty<IBrush> CompletedColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
}

View File

@ -2,96 +2,93 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using DynamicData;
using DynamicData.Binding;
using SPTInstaller.Models;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace SPTInstaller.CustomControls
namespace SPTInstaller.CustomControls;
public partial class ProgressableTaskList : UserControl
{
public partial class ProgressableTaskList : UserControl
public ProgressableTaskList()
{
public ProgressableTaskList()
InitializeComponent();
this.AttachedToVisualTree += ProgressableTaskList_AttachedToVisualTree;
}
private int _taskProgress;
public int TaskProgress
{
get => _taskProgress;
set => SetAndRaise(ProgressableTaskList.TaskProgressProperty, ref _taskProgress, value);
}
public static readonly DirectProperty<ProgressableTaskList, int> TaskProgressProperty =
AvaloniaProperty.RegisterDirect<ProgressableTaskList, int>(nameof(TaskProgress), o => o.TaskProgress);
public ObservableCollection<InstallerTaskBase> Tasks
{
get => GetValue(TasksProperty);
set => SetValue(TasksProperty, value);
}
public static readonly StyledProperty<ObservableCollection<InstallerTaskBase>> TasksProperty =
AvaloniaProperty.Register<ProgressableTaskList, ObservableCollection<InstallerTaskBase>>(nameof(Tasks));
public IBrush PendingColor
{
get => GetValue(PendingColorProperty);
set => SetValue(PendingColorProperty, value);
}
public static readonly StyledProperty<IBrush> PendingColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
public IBrush RunningColor
{
get => GetValue(RunningColorProperty);
set => SetValue(RunningColorProperty, value);
}
public static readonly StyledProperty<IBrush> RunningColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
public IBrush CompletedColor
{
get => GetValue(CompletedColorProperty);
set => SetValue(CompletedColorProperty, value);
}
public static readonly StyledProperty<IBrush> CompletedColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
private void UpdateTaskProgress()
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
InitializeComponent();
var completedTasks = Tasks.Where(x => x.IsCompleted == true).Count();
this.AttachedToVisualTree += ProgressableTaskList_AttachedToVisualTree;
}
var progress = (int)Math.Floor((double)completedTasks / (Tasks.Count - 1) * 100);
private int _taskProgress;
public int TaskProgress
{
get => _taskProgress;
set => SetAndRaise(ProgressableTaskList.TaskProgressProperty, ref _taskProgress, value);
}
public static readonly DirectProperty<ProgressableTaskList, int> TaskProgressProperty =
AvaloniaProperty.RegisterDirect<ProgressableTaskList, int>(nameof(TaskProgress), o => o.TaskProgress);
public ObservableCollection<InstallerTaskBase> Tasks
{
get => GetValue(TasksProperty);
set => SetValue(TasksProperty, value);
}
public static readonly StyledProperty<ObservableCollection<InstallerTaskBase>> TasksProperty =
AvaloniaProperty.Register<ProgressableTaskList, ObservableCollection<InstallerTaskBase>>(nameof(Tasks));
public IBrush PendingColor
{
get => GetValue(PendingColorProperty);
set => SetValue(PendingColorProperty, value);
}
public static readonly StyledProperty<IBrush> PendingColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
public IBrush RunningColor
{
get => GetValue(RunningColorProperty);
set => SetValue(RunningColorProperty, value);
}
public static readonly StyledProperty<IBrush> RunningColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
public IBrush CompletedColor
{
get => GetValue(CompletedColorProperty);
set => SetValue(CompletedColorProperty, value);
}
public static readonly StyledProperty<IBrush> CompletedColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
private void UpdateTaskProgress()
{
Dispatcher.UIThread.InvokeAsync(async () =>
for(; TaskProgress < progress;)
{
var completedTasks = Tasks.Where(x => x.IsCompleted == true).Count();
var progress = (int)Math.Floor((double)completedTasks / (Tasks.Count - 1) * 100);
for(; TaskProgress < progress;)
{
TaskProgress += 1;
await Task.Delay(1);
}
});
}
private void ProgressableTaskList_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (Tasks == null) return;
foreach (var task in Tasks)
{
task.WhenPropertyChanged(x => x.IsCompleted)
.Subscribe(x => UpdateTaskProgress());
TaskProgress += 1;
await Task.Delay(1);
}
});
}
private void ProgressableTaskList_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (Tasks == null) return;
foreach (var task in Tasks)
{
task.WhenPropertyChanged(x => x.IsCompleted)
.Subscribe(x => UpdateTaskProgress());
}
}
}

View File

@ -1,58 +1,57 @@
using Avalonia;
using Avalonia.Controls;
namespace SPTInstaller.CustomControls
namespace SPTInstaller.CustomControls;
public partial class TaskDetails : UserControl
{
public partial class TaskDetails : UserControl
public TaskDetails()
{
public TaskDetails()
{
InitializeComponent();
}
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<TaskDetails, string>(nameof(Message));
public string Details
{
get => GetValue(DetailsProperty);
set => SetValue(DetailsProperty, value);
}
public static readonly StyledProperty<string> DetailsProperty =
AvaloniaProperty.Register<TaskDetails, string>(nameof(Details));
public int Progress
{
get => GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
public static readonly StyledProperty<int> ProgressProperty =
AvaloniaProperty.Register<TaskDetails, int>(nameof(Progress));
public bool ShowProgress
{
get => GetValue(ShowProgressProperty);
set => SetValue(ShowProgressProperty, value);
}
public static readonly StyledProperty<bool> ShowProgressProperty =
AvaloniaProperty.Register<TaskDetails, bool>(nameof(ShowProgress));
public bool IndeterminateProgress
{
get => GetValue(IndeterminateProgressProperty);
set => SetValue(IndeterminateProgressProperty, value);
}
public static readonly StyledProperty<bool> IndeterminateProgressProperty =
AvaloniaProperty.Register<TaskDetails, bool>(nameof(IndeterminateProgress));
InitializeComponent();
}
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<TaskDetails, string>(nameof(Message));
public string Details
{
get => GetValue(DetailsProperty);
set => SetValue(DetailsProperty, value);
}
public static readonly StyledProperty<string> DetailsProperty =
AvaloniaProperty.Register<TaskDetails, string>(nameof(Details));
public int Progress
{
get => GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
public static readonly StyledProperty<int> ProgressProperty =
AvaloniaProperty.Register<TaskDetails, int>(nameof(Progress));
public bool ShowProgress
{
get => GetValue(ShowProgressProperty);
set => SetValue(ShowProgressProperty, value);
}
public static readonly StyledProperty<bool> ShowProgressProperty =
AvaloniaProperty.Register<TaskDetails, bool>(nameof(ShowProgress));
public bool IndeterminateProgress
{
get => GetValue(IndeterminateProgressProperty);
set => SetValue(IndeterminateProgressProperty, value);
}
public static readonly StyledProperty<bool> IndeterminateProgressProperty =
AvaloniaProperty.Register<TaskDetails, bool>(nameof(IndeterminateProgress));
}

View File

@ -4,74 +4,73 @@ using Avalonia.Markup.Xaml;
using Avalonia.Media;
using System.Windows.Input;
namespace SPTInstaller.CustomControls
namespace SPTInstaller.CustomControls;
public partial class TitleBar : UserControl
{
public partial class TitleBar : UserControl
public TitleBar()
{
public TitleBar()
{
InitializeComponent();
}
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public static readonly StyledProperty<string> TitleProperty =
AvaloniaProperty.Register<TitleBar, string>(nameof(Title));
public static readonly StyledProperty<string> TitleProperty =
AvaloniaProperty.Register<TitleBar, string>(nameof(Title));
public string Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public string Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly StyledProperty<IBrush> ButtonForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(ButtonForeground));
public static readonly StyledProperty<IBrush> ButtonForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(ButtonForeground));
public IBrush ButtonForeground
{
get => GetValue(ButtonForegroundProperty);
set => SetValue(ButtonForegroundProperty, value);
}
public IBrush ButtonForeground
{
get => GetValue(ButtonForegroundProperty);
set => SetValue(ButtonForegroundProperty, value);
}
public static new readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Foreground));
public static new readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Foreground));
public new IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public new IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static new readonly StyledProperty<IBrush> BackgroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Background));
public static new readonly StyledProperty<IBrush> BackgroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Background));
public new IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public new IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
//Close Button Command (X Button) Property
public static readonly StyledProperty<ICommand> XButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(XButtonCommand));
//Close Button Command (X Button) Property
public static readonly StyledProperty<ICommand> XButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(XButtonCommand));
public ICommand XButtonCommand
{
get => GetValue(XButtonCommandProperty);
set => SetValue(XButtonCommandProperty, value);
}
public ICommand XButtonCommand
{
get => GetValue(XButtonCommandProperty);
set => SetValue(XButtonCommandProperty, value);
}
//Minimize Button Command (- Button) Property
public static readonly StyledProperty<ICommand> MinButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(MinButtonCommand));
//Minimize Button Command (- Button) Property
public static readonly StyledProperty<ICommand> MinButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(MinButtonCommand));
public ICommand MinButtonCommand
{
get => GetValue(MinButtonCommandProperty);
set => SetValue(MinButtonCommandProperty, value);
}
public ICommand MinButtonCommand
{
get => GetValue(MinButtonCommandProperty);
set => SetValue(MinButtonCommandProperty, value);
}
}

View File

@ -1,146 +1,142 @@
using HttpClientProgress;
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace SPTInstaller.Aki.Helper
namespace SPTInstaller.Helpers;
public static class DownloadCacheHelper
{
public static class DownloadCacheHelper
private static HttpClient _httpClient = new() { Timeout = TimeSpan.FromHours(1) };
private static string _cachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "spt-installer/cache");
private static bool CheckCache(FileInfo cacheFile, string expectedHash = null)
{
private static HttpClient _httpClient = new HttpClient() { Timeout = TimeSpan.FromHours(1) };
private static string _cachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "spt-installer/cache");
private static bool CheckCache(FileInfo cacheFile, string expectedHash = null)
try
{
try
cacheFile.Refresh();
Directory.CreateDirectory(_cachePath);
if (cacheFile.Exists)
{
if (expectedHash != null && FileHashHelper.CheckHash(cacheFile, expectedHash))
{
Log.Information($"Using cached file: {cacheFile.Name} - Hash: {expectedHash}");
return true;
}
cacheFile.Delete();
cacheFile.Refresh();
Directory.CreateDirectory(_cachePath);
if (cacheFile.Exists)
{
if (expectedHash != null && FileHashHelper.CheckHash(cacheFile, expectedHash))
{
Log.Information($"Using cached file: {cacheFile.Name} - Hash: {expectedHash}");
return true;
}
cacheFile.Delete();
cacheFile.Refresh();
}
return false;
}
catch
{
return false;
}
return false;
}
private static async Task<Result> DownloadFile(FileInfo outputFile, string targetLink, IProgress<double> progress, string expectedHash = null)
catch
{
try
{
// Use the provided extension method
using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
await _httpClient.DownloadDataAsync(targetLink, file, progress);
outputFile.Refresh();
if (!outputFile.Exists)
{
return Result.FromError($"Failed to download {outputFile.Name}");
}
if (expectedHash != null && !FileHashHelper.CheckHash(outputFile, expectedHash))
{
return Result.FromError("Hash mismatch");
}
return Result.FromSuccess();
}
catch (Exception ex)
{
return Result.FromError(ex.Message);
}
return false;
}
}
private static async Task<Result> ProcessInboundStreamAsync(FileInfo cacheFile, Stream downloadStream, string expectedHash = null)
private static async Task<Result> DownloadFile(FileInfo outputFile, string targetLink, IProgress<double> progress, string expectedHash = null)
{
try
{
try
// Use the provided extension method
using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
await _httpClient.DownloadDataAsync(targetLink, file, progress);
outputFile.Refresh();
if (!outputFile.Exists)
{
if (CheckCache(cacheFile, expectedHash)) return Result.FromSuccess();
using var patcherFileStream = cacheFile.Open(FileMode.Create);
{
await downloadStream.CopyToAsync(patcherFileStream);
}
patcherFileStream.Close();
if (expectedHash != null && !FileHashHelper.CheckHash(cacheFile, expectedHash))
{
return Result.FromError("Hash mismatch");
}
return Result.FromSuccess();
return Result.FromError($"Failed to download {outputFile.Name}");
}
catch(Exception ex)
if (expectedHash != null && !FileHashHelper.CheckHash(outputFile, expectedHash))
{
return Result.FromError(ex.Message);
return Result.FromError("Hash mismatch");
}
return Result.FromSuccess();
}
private static async Task<Result> ProcessInboundFileAsync(FileInfo cacheFile, string targetLink, IProgress<double> progress, string expectedHash = null)
catch (Exception ex)
{
try
{
if (CheckCache(cacheFile, expectedHash)) return Result.FromSuccess();
return await DownloadFile(cacheFile, targetLink, progress, expectedHash);
}
catch(Exception ex)
{
return Result.FromError(ex.Message);
}
return Result.FromError(ex.Message);
}
}
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink, IProgress<double> progress, string expectedHash = null)
private static async Task<Result> ProcessInboundStreamAsync(FileInfo cacheFile, Stream downloadStream, string expectedHash = null)
{
try
{
FileInfo cacheFile = new FileInfo(Path.Join(_cachePath, fileName));
if (CheckCache(cacheFile, expectedHash)) return Result.FromSuccess();
try
using var patcherFileStream = cacheFile.Open(FileMode.Create);
{
var result = await ProcessInboundFileAsync(cacheFile, targetLink, progress, expectedHash);
await downloadStream.CopyToAsync(patcherFileStream);
}
return result.Succeeded ? cacheFile : null;
}
catch(Exception ex)
patcherFileStream.Close();
if (expectedHash != null && !FileHashHelper.CheckHash(cacheFile, expectedHash))
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
return Result.FromError("Hash mismatch");
}
return Result.FromSuccess();
}
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash = null)
catch(Exception ex)
{
FileInfo cacheFile = new FileInfo(Path.Join(_cachePath, fileName));
return Result.FromError(ex.Message);
}
}
try
{
var result = await ProcessInboundStreamAsync(cacheFile, fileDownloadStream, expectedHash);
private static async Task<Result> ProcessInboundFileAsync(FileInfo cacheFile, string targetLink, IProgress<double> progress, string expectedHash = null)
{
try
{
if (CheckCache(cacheFile, expectedHash)) return Result.FromSuccess();
return result.Succeeded ? cacheFile : null;
}
catch(Exception ex)
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
}
return await DownloadFile(cacheFile, targetLink, progress, expectedHash);
}
catch(Exception ex)
{
return Result.FromError(ex.Message);
}
}
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink, IProgress<double> progress, string expectedHash = null)
{
var cacheFile = new FileInfo(Path.Join(_cachePath, fileName));
try
{
var result = await ProcessInboundFileAsync(cacheFile, targetLink, progress, expectedHash);
return result.Succeeded ? cacheFile : null;
}
catch(Exception ex)
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
}
}
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash = null)
{
var cacheFile = new FileInfo(Path.Join(_cachePath, fileName));
try
{
var result = await ProcessInboundStreamAsync(cacheFile, fileDownloadStream, expectedHash);
return result.Succeeded ? cacheFile : null;
}
catch(Exception ex)
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
}
}
}

View File

@ -1,38 +1,34 @@
using Gitea.Model;
using System;
using System.IO;
using System.Linq;
using System.Linq;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using Gitea.Model;
namespace SPTInstaller.Aki.Helper
namespace SPTInstaller.Helpers;
public static class FileHashHelper
{
public static class FileHashHelper
public static string? GetGiteaReleaseHash(Release release)
{
public static string GetGiteaReleaseHash(Release release)
var regex = Regex.Match(release.Body, @"Release Hash: (?<hash>\S+)");
if (regex.Success)
{
var regex = Regex.Match(release.Body, @"Release Hash: (?<hash>\S+)");
if (regex.Success)
{
return regex.Groups["hash"].Value;
}
return null;
return regex.Groups["hash"].Value;
}
public static bool CheckHash(FileInfo file, string expectedHash)
{
using (MD5 md5Service = MD5.Create())
using (var sourceStream = file.OpenRead())
{
byte[] sourceHash = md5Service.ComputeHash(sourceStream);
byte[] expectedHashBytes = Convert.FromBase64String(expectedHash);
return null;
}
bool matched = Enumerable.SequenceEqual(sourceHash, expectedHashBytes);
public static bool CheckHash(FileInfo file, string expectedHash)
{
using var md5Service = MD5.Create();
using var sourceStream = file.OpenRead();
return matched;
}
}
var sourceHash = md5Service.ComputeHash(sourceStream);
var expectedHashBytes = Convert.FromBase64String(expectedHash);
var matched = Enumerable.SequenceEqual(sourceHash, expectedHashBytes);
return matched;
}
}

View File

@ -1,90 +1,86 @@
using ReactiveUI;
using System.Text.RegularExpressions;
using Serilog;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace SPTInstaller.Aki.Helper
namespace SPTInstaller.Helpers;
public static class FileHelper
{
public static class FileHelper
private static Result IterateDirectories(DirectoryInfo sourceDir, DirectoryInfo targetDir)
{
private static Result IterateDirectories(DirectoryInfo sourceDir, DirectoryInfo targetDir)
try
{
try
foreach (var dir in sourceDir.GetDirectories("*", SearchOption.AllDirectories))
{
foreach (var dir in sourceDir.GetDirectories("*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(dir.FullName.Replace(sourceDir.FullName, targetDir.FullName));
}
return Result.FromSuccess();
Directory.CreateDirectory(dir.FullName.Replace(sourceDir.FullName, targetDir.FullName));
}
catch (Exception ex)
return Result.FromSuccess();
}
catch (Exception ex)
{
Log.Error(ex, "Error while creating directories");
return Result.FromError(ex.Message);
}
}
private static Result IterateFiles(DirectoryInfo sourceDir, DirectoryInfo targetDir, Action<string, int> updateCallback = null)
{
try
{
int totalFiles = sourceDir.GetFiles("*.*", SearchOption.AllDirectories).Length;
int processedFiles = 0;
foreach (var file in sourceDir.GetFiles("*.*", SearchOption.AllDirectories))
{
Log.Error(ex, "Error while creating directories");
return Result.FromError(ex.Message);
updateCallback?.Invoke(file.Name, (int)Math.Floor(((double)processedFiles / totalFiles) * 100));
File.Copy(file.FullName, file.FullName.Replace(sourceDir.FullName, targetDir.FullName), true);
processedFiles++;
}
return Result.FromSuccess();
}
catch (Exception ex)
{
Log.Error(ex, "Error while copying files");
return Result.FromError(ex.Message);
}
}
public static string GetRedactedPath(string path)
{
var nameMatched = Regex.Match(path, @".:\\[uU]sers\\(?<NAME>[^\\]+)");
if (nameMatched.Success)
{
var name = nameMatched.Groups["NAME"].Value;
return path.Replace(name, "-REDACTED-");
}
private static Result IterateFiles(DirectoryInfo sourceDir, DirectoryInfo targetDir, Action<string, int> updateCallback = null)
return path;
}
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, IProgress<double> progress = null) =>
CopyDirectoryWithProgress(sourceDir, targetDir, (msg, prog) => progress?.Report(prog));
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, Action<string, int> updateCallback = null)
{
try
{
try
{
int totalFiles = sourceDir.GetFiles("*.*", SearchOption.AllDirectories).Length;
int processedFiles = 0;
var iterateDirectoriesResult = IterateDirectories(sourceDir, targetDir);
foreach (var file in sourceDir.GetFiles("*.*", SearchOption.AllDirectories))
{
updateCallback?.Invoke(file.Name, (int)Math.Floor(((double)processedFiles / totalFiles) * 100));
if(!iterateDirectoriesResult.Succeeded) return iterateDirectoriesResult;
File.Copy(file.FullName, file.FullName.Replace(sourceDir.FullName, targetDir.FullName), true);
processedFiles++;
}
var iterateFilesResult = IterateFiles(sourceDir, targetDir, updateCallback);
return Result.FromSuccess();
}
catch (Exception ex)
{
Log.Error(ex, "Error while copying files");
return Result.FromError(ex.Message);
}
if (!iterateFilesResult.Succeeded) return iterateDirectoriesResult;
return Result.FromSuccess();
}
public static string GetRedactedPath(string path)
catch (Exception ex)
{
var nameMatched = Regex.Match(path, @".:\\[uU]sers\\(?<NAME>[^\\]+)");
if (nameMatched.Success)
{
var name = nameMatched.Groups["NAME"].Value;
return path.Replace(name, "-REDACTED-");
}
return path;
}
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, IProgress<double> progress = null) =>
CopyDirectoryWithProgress(sourceDir, targetDir, (msg, prog) => progress?.Report(prog));
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, Action<string, int> updateCallback = null)
{
try
{
var iterateDirectoriesResult = IterateDirectories(sourceDir, targetDir);
if(!iterateDirectoriesResult.Succeeded) return iterateDirectoriesResult;
var iterateFilesResult = IterateFiles(sourceDir, targetDir, updateCallback);
if (!iterateFilesResult.Succeeded) return iterateDirectoriesResult;
return Result.FromSuccess();
}
catch (Exception ex)
{
Log.Error(ex, "Error during directory copy");
return Result.FromError(ex.Message);
}
Log.Error(ex, "Error during directory copy");
return Result.FromError(ex.Message);
}
}
}

View File

@ -1,57 +1,54 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace HttpClientProgress
{
public static class HttpClientProgressExtensions
{
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<double> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
using (var response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead))
{
var contentLength = response.Content.Headers.ContentLength;
using (var download = await response.Content.ReadAsStreamAsync())
{
// no progress... no contentLength... very sad
if (progress is null || !contentLength.HasValue)
{
await download.CopyToAsync(destination);
return;
}
// Such progress and contentLength much reporting Wow!
var progressWrapper = new Progress<long>(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
}
}
namespace SPTInstaller.Helpers;
float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f;
public static class HttpClientProgressExtensions
{
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<double> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
using (var response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead))
{
var contentLength = response.Content.Headers.ContentLength;
using (var download = await response.Content.ReadAsStreamAsync())
{
// no progress... no contentLength... very sad
if (progress is null || !contentLength.HasValue)
{
await download.CopyToAsync(destination);
return;
}
// Such progress and contentLength much reporting Wow!
var progressWrapper = new Progress<long>(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
}
}
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
if (source is null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new InvalidOperationException($"'{nameof(source)}' is not readable.");
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f;
}
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
if (source is null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new InvalidOperationException($"'{nameof(source)}' is not readable.");
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
}

View File

@ -1,40 +1,37 @@
using Microsoft.Win32;
using SPTInstaller.Models;
using System;
using System.Diagnostics;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using SPTInstaller.Models;
namespace SPTInstaller.Aki.Helper
namespace SPTInstaller.Helpers;
public static class PreCheckHelper
{
public static class PreCheckHelper
private const string registryInstall = @"Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\EscapeFromTarkov";
public static string DetectOriginalGamePath()
{
private const string registryInstall = @"Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\EscapeFromTarkov";
// We can't detect the installed path on non-Windows
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return null;
public static string DetectOriginalGamePath()
var uninstallStringValue = Registry.LocalMachine.OpenSubKey(registryInstall, false)
?.GetValue("UninstallString");
var info = (uninstallStringValue is string key) ? new FileInfo(key) : null;
return info?.DirectoryName;
}
public static Result DetectOriginalGameVersion(string gamePath)
{
try
{
// We can't detect the installed path on non-Windows
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return null;
var uninstallStringValue = Registry.LocalMachine.OpenSubKey(registryInstall, false)
?.GetValue("UninstallString");
var info = (uninstallStringValue is string key) ? new FileInfo(key) : null;
return info?.DirectoryName;
string version = FileVersionInfo.GetVersionInfo(Path.Join(gamePath + "/EscapeFromTarkov.exe")).ProductVersion.Replace('-', '.').Split('.')[^2];
return Result.FromSuccess(version);
}
public static Result DetectOriginalGameVersion(string gamePath)
catch (Exception ex)
{
try
{
string version = FileVersionInfo.GetVersionInfo(Path.Join(gamePath + "/EscapeFromTarkov.exe")).ProductVersion.Replace('-', '.').Split('.')[^2];
return Result.FromSuccess(version);
}
catch (Exception ex)
{
return Result.FromError($"File not found: {ex.Message}");
}
return Result.FromError($"File not found: {ex.Message}");
}
}
}

View File

@ -1,61 +1,60 @@
using SPTInstaller.Models;
using System.Diagnostics;
using System.IO;
using System.Diagnostics;
using SPTInstaller.Models;
namespace SPTInstaller.Aki.Helper
namespace SPTInstaller.Helpers;
public enum PatcherExitCode
{
public enum PatcherExitCode
{
ProgramClosed = 0,
Success = 10,
EftExeNotFound = 11,
NoPatchFolder = 12,
MissingFile = 13,
MissingDir = 14,
PatchFailed = 15
}
ProgramClosed = 0,
Success = 10,
EftExeNotFound = 11,
NoPatchFolder = 12,
MissingFile = 13,
MissingDir = 14,
PatchFailed = 15
}
public static class ProcessHelper
public static class ProcessHelper
{
public static Result PatchClientFiles(FileInfo executable, DirectoryInfo workingDir)
{
public static Result PatchClientFiles(FileInfo executable, DirectoryInfo workingDir)
if (!executable.Exists || !workingDir.Exists)
{
if (!executable.Exists || !workingDir.Exists)
{
return Result.FromError($"Could not find executable ({executable.Name}) or working directory ({workingDir.Name})");
}
return Result.FromError(
$"Could not find executable ({executable.Name}) or working directory ({workingDir.Name})");
}
var process = new Process();
process.StartInfo.FileName = executable.FullName;
process.StartInfo.WorkingDirectory = workingDir.FullName;
process.EnableRaisingEvents = true;
process.StartInfo.Arguments = "autoclose";
process.Start();
var process = new Process();
process.StartInfo.FileName = executable.FullName;
process.StartInfo.WorkingDirectory = workingDir.FullName;
process.EnableRaisingEvents = true;
process.StartInfo.Arguments = "autoclose";
process.Start();
process.WaitForExit();
process.WaitForExit();
switch ((PatcherExitCode)process.ExitCode)
{
case PatcherExitCode.Success:
return Result.FromSuccess("Patcher Finished Successfully, extracting Aki");
switch ((PatcherExitCode)process.ExitCode)
{
case PatcherExitCode.Success:
return Result.FromSuccess("Patcher Finished Successfully, extracting Aki");
case PatcherExitCode.ProgramClosed:
return Result.FromError("Patcher was closed before completing!");
case PatcherExitCode.ProgramClosed:
return Result.FromError("Patcher was closed before completing!");
case PatcherExitCode.EftExeNotFound:
return Result.FromError("EscapeFromTarkov.exe is missing from the install Path");
case PatcherExitCode.EftExeNotFound:
return Result.FromError("EscapeFromTarkov.exe is missing from the install Path");
case PatcherExitCode.NoPatchFolder:
return Result.FromError("Patchers Folder called 'Aki_Patches' is missing");
case PatcherExitCode.NoPatchFolder:
return Result.FromError("Patchers Folder called 'Aki_Patches' is missing");
case PatcherExitCode.MissingFile:
return Result.FromError("EFT files was missing a Vital file to continue");
case PatcherExitCode.MissingFile:
return Result.FromError("EFT files was missing a Vital file to continue");
case PatcherExitCode.PatchFailed:
return Result.FromError("A patch failed to apply");
case PatcherExitCode.PatchFailed:
return Result.FromError("A patch failed to apply");
default:
return Result.FromError("an unknown error occurred in the patcher");
}
default:
return Result.FromError("an unknown error occurred in the patcher");
}
}
}

View File

@ -1,129 +1,127 @@
using Serilog;
using Splat;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SPTInstaller.Helpers
namespace SPTInstaller.Helpers;
/// <summary>
/// A helper class to handle simple service registration to Splat with constructor parameter injection
/// </summary>
/// <remarks>Splat only recognizes the registered types and doesn't account for interfaces :(</remarks>
internal static class ServiceHelper
{
/// <summary>
/// A helper class to handle simple service registration to Splat with constructor parameter injection
/// </summary>
/// <remarks>Splat only recognizes the registered types and doesn't account for interfaces :(</remarks>
internal static class ServiceHelper
private static bool TryRegisterInstance<T, T2>(object[] parameters = null)
{
private static bool TryRegisterInstance<T, T2>(object[] parameters = null)
var instance = Activator.CreateInstance(typeof(T2), parameters);
if (instance != null)
{
var instance = Activator.CreateInstance(typeof(T2), parameters);
if (instance != null)
{
Locator.CurrentMutable.RegisterConstant<T>((T)instance);
return true;
}
return false;
Locator.CurrentMutable.RegisterConstant<T>((T)instance);
return true;
}
/// <summary>
/// Register a class as a service
/// </summary>
/// <typeparam name="T">class to register</typeparam>
public static void Register<T>() where T : class => Register<T, T>();
return false;
}
/// <summary>
/// Register a class as a service by another type
/// </summary>
/// <typeparam name="T">type to register as</typeparam>
/// <typeparam name="T2">class to register</typeparam>
public static void Register<T, T2>() where T : class
/// <summary>
/// Register a class as a service
/// </summary>
/// <typeparam name="T">class to register</typeparam>
public static void Register<T>() where T : class => Register<T, T>();
/// <summary>
/// Register a class as a service by another type
/// </summary>
/// <typeparam name="T">type to register as</typeparam>
/// <typeparam name="T2">class to register</typeparam>
public static void Register<T, T2>() where T : class
{
var constructors = typeof(T2).GetConstructors();
foreach(var constructor in constructors)
{
var constructors = typeof(T2).GetConstructors();
var parmesan = constructor.GetParameters();
foreach(var constructor in constructors)
if(parmesan.Length == 0)
{
var parmesan = constructor.GetParameters();
if (TryRegisterInstance<T, T2>()) return;
if(parmesan.Length == 0)
{
if (TryRegisterInstance<T, T2>()) return;
continue;
}
List<object> parameters = new List<object>();
for(int i = 0; i < parmesan.Length; i++)
{
var parm = parmesan[i];
var parmValue = Get(parm.ParameterType);
if (parmValue != null) parameters.Add(parmValue);
}
if (TryRegisterInstance<T, T2>(parameters.ToArray())) return;
}
}
/// <summary>
/// Get a service from splat
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown if the service isn't found</exception>
public static object Get(Type type)
{
var service = Locator.Current.GetService(type);
if (service == null)
{
var message = $"Could not locate service of type '{type.Name}'";
Log.Error(message);
throw new InvalidOperationException(message);
continue;
}
return service;
}
List<object> parameters = new List<object>();
/// <summary>
/// Get a service from splat
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown if the service isn't found</exception>
public static T Get<T>()
{
var service = Locator.Current.GetService<T>();
if (service == null)
for(int i = 0; i < parmesan.Length; i++)
{
var message = $"Could not locate service of type '{nameof(T)}'";
Log.Error(message);
throw new InvalidOperationException(message);
var parm = parmesan[i];
var parmValue = Get(parm.ParameterType);
if (parmValue != null) parameters.Add(parmValue);
}
return service;
}
/// <summary>
/// Get all services of a type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="InvalidOperationException">thrown if no services are found</exception>
public static T[] GetAll<T>()
{
var services = Locator.Current.GetServices<T>().ToArray();
if (services == null || services.Count() == 0)
{
var message = $"Could not locate service of type '{nameof(T)}'";
Log.Error(message);
throw new InvalidOperationException(message);
}
return services;
if (TryRegisterInstance<T, T2>(parameters.ToArray())) return;
}
}
/// <summary>
/// Get a service from splat
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown if the service isn't found</exception>
public static object Get(Type type)
{
var service = Locator.Current.GetService(type);
if (service == null)
{
var message = $"Could not locate service of type '{type.Name}'";
Log.Error(message);
throw new InvalidOperationException(message);
}
return service;
}
/// <summary>
/// Get a service from splat
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown if the service isn't found</exception>
public static T Get<T>()
{
var service = Locator.Current.GetService<T>();
if (service == null)
{
var message = $"Could not locate service of type '{nameof(T)}'";
Log.Error(message);
throw new InvalidOperationException(message);
}
return service;
}
/// <summary>
/// Get all services of a type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="InvalidOperationException">thrown if no services are found</exception>
public static T[] GetAll<T>()
{
var services = Locator.Current.GetServices<T>().ToArray();
if (services == null || services.Count() == 0)
{
var message = $"Could not locate service of type '{nameof(T)}'";
Log.Error(message);
throw new InvalidOperationException(message);
}
return services;
}
}

View File

@ -1,56 +1,53 @@
using SharpCompress.Archives;
using System.Linq;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Linq;
namespace SPTInstaller.Aki.Helper
namespace SPTInstaller.Helpers;
public static class ZipHelper
{
public static class ZipHelper
public static Result Decompress(FileInfo ArchivePath, DirectoryInfo OutputFolderPath, IProgress<double> progress = null)
{
public static Result Decompress(FileInfo ArchivePath, DirectoryInfo OutputFolderPath, IProgress<double> progress = null)
try
{
try
OutputFolderPath.Refresh();
if (!OutputFolderPath.Exists) OutputFolderPath.Create();
using var archive = ZipArchive.Open(ArchivePath);
var totalEntries = archive.Entries.Where(entry => !entry.IsDirectory);
var processedEntries = 0;
foreach (var entry in totalEntries)
{
OutputFolderPath.Refresh();
if (!OutputFolderPath.Exists) OutputFolderPath.Create();
using var archive = ZipArchive.Open(ArchivePath);
var totalEntries = archive.Entries.Where(entry => !entry.IsDirectory);
int processedEntries = 0;
foreach (var entry in totalEntries)
entry.WriteToDirectory(OutputFolderPath.FullName, new ExtractionOptions()
{
entry.WriteToDirectory(OutputFolderPath.FullName, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
ExtractFullPath = true,
Overwrite = true
});
processedEntries++;
processedEntries++;
if (progress != null)
{
progress.Report(Math.Floor(((double)processedEntries / totalEntries.Count()) * 100));
}
}
OutputFolderPath.Refresh();
if (!OutputFolderPath.Exists)
if (progress != null)
{
return Result.FromError($"Failed to extract files: {ArchivePath.Name}");
progress.Report(Math.Floor(((double)processedEntries / totalEntries.Count()) * 100));
}
return Result.FromSuccess();
}
catch (Exception ex)
OutputFolderPath.Refresh();
if (!OutputFolderPath.Exists)
{
return Result.FromError(ex.Message);
return Result.FromError($"Failed to extract files: {ArchivePath.Name}");
}
return Result.FromSuccess();
}
catch (Exception ex)
{
return Result.FromError(ex.Message);
}
}
}

View File

@ -1,30 +1,26 @@
using Serilog;
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Threading.Tasks;
using SPTInstaller.Helpers;
namespace SPTInstaller.Installer_Tasks
namespace SPTInstaller.Installer_Tasks;
public class CopyClientTask : InstallerTaskBase
{
public class CopyClientTask : InstallerTaskBase
private InternalData _data;
public CopyClientTask(InternalData data) : base("Copy Client Files")
{
private InternalData _data;
_data = data;
}
public CopyClientTask(InternalData data) : base("Copy Client Files")
{
_data = data;
}
public override async Task<IResult> TaskOperation()
{
SetStatus("Copying Client Files", "", 0);
public override async Task<IResult> TaskOperation()
{
SetStatus("Copying Client Files", "", 0);
var originalGameDirInfo = new DirectoryInfo(_data.OriginalGamePath);
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
var originalGameDirInfo = new DirectoryInfo(_data.OriginalGamePath);
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
return FileHelper.CopyDirectoryWithProgress(originalGameDirInfo, targetInstallDirInfo, (message, progress) => { SetStatus(null, message, progress, null, true); });
}
return FileHelper.CopyDirectoryWithProgress(originalGameDirInfo, targetInstallDirInfo, (message, progress) => { SetStatus(null, message, progress, null, true); });
}
}

View File

@ -1,126 +1,123 @@
using CG.Web.MegaApiClient;
using Newtonsoft.Json;
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using SPTInstaller.Helpers;
namespace SPTInstaller.Installer_Tasks
namespace SPTInstaller.Installer_Tasks;
public class DownloadTask : InstallerTaskBase
{
public class DownloadTask : InstallerTaskBase
private InternalData _data;
public DownloadTask(InternalData data) : base("Download Files")
{
private InternalData _data;
_data = data;
}
public DownloadTask(InternalData data) : base("Download Files")
private async Task<IResult> BuildMirrorList()
{
var progress = new Progress<double>((d) => { SetStatus("Downloading Mirror List", "", (int)Math.Floor(d));});
var file = await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", _data.PatcherMirrorsLink, progress);
if (file == null)
{
_data = data;
return Result.FromError("Failed to download mirror list");
}
private async Task<IResult> BuildMirrorList()
var mirrorsList = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(file.FullName));
if (mirrorsList is List<DownloadMirror> mirrors)
{
var progress = new Progress<double>((d) => { SetStatus("Downloading Mirror List", "", (int)Math.Floor(d));});
var file = await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", _data.PatcherMirrorsLink, progress);
if (file == null)
{
return Result.FromError("Failed to download mirror list");
}
var mirrorsList = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(file.FullName));
if (mirrorsList is List<DownloadMirror> mirrors)
{
_data.PatcherReleaseMirrors = mirrors;
return Result.FromSuccess();
}
return Result.FromError("Failed to deserialize mirrors list");
}
private async Task<IResult> DownloadPatcherFromMirrors(IProgress<double> progress)
{
foreach (var mirror in _data.PatcherReleaseMirrors)
{
SetStatus($"Downloading Patcher", mirror.Link);
// mega is a little weird since they use encryption, but thankfully there is a great library for their api :)
if (mirror.Link.StartsWith("https://mega"))
{
var megaClient = new MegaApiClient();
await megaClient.LoginAnonymousAsync();
// if mega fails to connect, try the next mirror
if (!megaClient.IsLoggedIn) continue;
try
{
using var megaDownloadStream = await megaClient.DownloadAsync(new Uri(mirror.Link), progress);
_data.PatcherZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("patcher.zip", megaDownloadStream, mirror.Hash);
if(_data.PatcherZipInfo == null)
{
continue;
}
return Result.FromSuccess();
}
catch
{
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
continue;
}
}
_data.PatcherZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("patcher.zip", mirror.Link, progress, mirror.Hash);
if (_data.PatcherZipInfo != null)
{
return Result.FromSuccess();
}
}
return Result.FromError("Failed to download Patcher");
}
public override async Task<IResult> TaskOperation()
{
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
if (_data.PatchNeeded)
{
var buildResult = await BuildMirrorList();
if (!buildResult.Succeeded)
{
return buildResult;
}
SetStatus(null, null, 0);
var patcherDownloadRresult = await DownloadPatcherFromMirrors(progress);
if (!patcherDownloadRresult.Succeeded)
{
return patcherDownloadRresult;
}
}
SetStatus("Downloading SPT-AKI", _data.AkiReleaseDownloadLink, 0);
_data.AkiZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki.zip", _data.AkiReleaseDownloadLink, progress, _data.AkiReleaseHash);
if (_data.AkiZipInfo == null)
{
return Result.FromError("Failed to download spt-aki");
}
_data.PatcherReleaseMirrors = mirrors;
return Result.FromSuccess();
}
return Result.FromError("Failed to deserialize mirrors list");
}
private async Task<IResult> DownloadPatcherFromMirrors(IProgress<double> progress)
{
foreach (var mirror in _data.PatcherReleaseMirrors)
{
SetStatus($"Downloading Patcher", mirror.Link);
// mega is a little weird since they use encryption, but thankfully there is a great library for their api :)
if (mirror.Link.StartsWith("https://mega"))
{
var megaClient = new MegaApiClient();
await megaClient.LoginAnonymousAsync();
// if mega fails to connect, try the next mirror
if (!megaClient.IsLoggedIn) continue;
try
{
using var megaDownloadStream = await megaClient.DownloadAsync(new Uri(mirror.Link), progress);
_data.PatcherZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("patcher.zip", megaDownloadStream, mirror.Hash);
if(_data.PatcherZipInfo == null)
{
continue;
}
return Result.FromSuccess();
}
catch
{
//most likely a 509 (Bandwidth limit exceeded) due to mega's user quotas.
continue;
}
}
_data.PatcherZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("patcher.zip", mirror.Link, progress, mirror.Hash);
if (_data.PatcherZipInfo != null)
{
return Result.FromSuccess();
}
}
return Result.FromError("Failed to download Patcher");
}
public override async Task<IResult> TaskOperation()
{
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
if (_data.PatchNeeded)
{
var buildResult = await BuildMirrorList();
if (!buildResult.Succeeded)
{
return buildResult;
}
SetStatus(null, null, 0);
var patcherDownloadRresult = await DownloadPatcherFromMirrors(progress);
if (!patcherDownloadRresult.Succeeded)
{
return patcherDownloadRresult;
}
}
SetStatus("Downloading SPT-AKI", _data.AkiReleaseDownloadLink, 0);
_data.AkiZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki.zip", _data.AkiReleaseDownloadLink, progress, _data.AkiReleaseHash);
if (_data.AkiZipInfo == null)
{
return Result.FromError("Failed to download spt-aki");
}
return Result.FromSuccess();
}
}

View File

@ -1,59 +1,58 @@
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System.Threading.Tasks;
using SPTInstaller.Helpers;
namespace SPTInstaller.Installer_Tasks
namespace SPTInstaller.Installer_Tasks;
public class InitializationTask : InstallerTaskBase
{
public class InitializationTask : InstallerTaskBase
private InternalData _data;
public InitializationTask(InternalData data) : base("Startup")
{
private InternalData _data;
_data = data;
}
public InitializationTask(InternalData data) : base("Startup")
public override async Task<IResult> TaskOperation()
{
SetStatus("Initializing", $"Target Install Path: {FileHelper.GetRedactedPath(_data.TargetInstallPath)}");
_data.OriginalGamePath = PreCheckHelper.DetectOriginalGamePath();
if (_data.OriginalGamePath == null)
{
_data = data;
return Result.FromError("EFT IS NOT INSTALLED!");
}
public override async Task<IResult> TaskOperation()
SetStatus(null, $"Installed EFT Game Path: {FileHelper.GetRedactedPath(_data.OriginalGamePath)}");
var result = PreCheckHelper.DetectOriginalGameVersion(_data.OriginalGamePath);
if (!result.Succeeded)
{
SetStatus("Initializing", $"Target Install Path: {FileHelper.GetRedactedPath(_data.TargetInstallPath)}");
_data.OriginalGamePath = PreCheckHelper.DetectOriginalGamePath();
if (_data.OriginalGamePath == null)
{
return Result.FromError("EFT IS NOT INSTALLED!");
}
SetStatus(null, $"Installed EFT Game Path: {FileHelper.GetRedactedPath(_data.OriginalGamePath)}");
var result = PreCheckHelper.DetectOriginalGameVersion(_data.OriginalGamePath);
if (!result.Succeeded)
{
return result;
}
_data.OriginalGameVersion = result.Message;
SetStatus(null, $"Installed EFT Game Version: {_data.OriginalGameVersion}");
if (_data.OriginalGamePath == null)
{
return Result.FromError("Unable to find original EFT directory, please make sure EFT is installed. Please also run EFT once");
}
if (_data.OriginalGamePath == _data.TargetInstallPath)
{
return Result.FromError("Installer is located in EFT's original directory. Please move the installer to a seperate folder as per the guide");
}
if (File.Exists(Path.Join(_data.TargetInstallPath, "EscapeFromTarkov.exe")))
{
return Result.FromError("Installer is located in a folder that has existing game files. Please make sure the installer is in a fresh folder as per the guide");
}
return Result.FromSuccess($"Current Game Version: {_data.OriginalGameVersion}");
return result;
}
_data.OriginalGameVersion = result.Message;
SetStatus(null, $"Installed EFT Game Version: {_data.OriginalGameVersion}");
if (_data.OriginalGamePath == null)
{
return Result.FromError("Unable to find original EFT directory, please make sure EFT is installed. Please also run EFT once");
}
if (_data.OriginalGamePath == _data.TargetInstallPath)
{
return Result.FromError("Installer is located in EFT's original directory. Please move the installer to a seperate folder as per the guide");
}
if (File.Exists(Path.Join(_data.TargetInstallPath, "EscapeFromTarkov.exe")))
{
return Result.FromError("Installer is located in a folder that has existing game files. Please make sure the installer is in a fresh folder as per the guide");
}
return Result.FromSuccess($"Current Game Version: {_data.OriginalGameVersion}");
}
}

View File

@ -1,58 +1,56 @@
using SPTInstaller.Models;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace SPTInstaller.Installer_Tasks.PreChecks
namespace SPTInstaller.Installer_Tasks.PreChecks;
public class NetCore6PreCheck : PreCheckBase
{
public class NetCore6PreCheck : PreCheckBase
public NetCore6PreCheck() : base(".Net Core 6 Desktop Runtime", false)
{
public NetCore6PreCheck() : base(".Net Core 6 Desktop Runtime", false)
}
public override async Task<bool> CheckOperation()
{
var minRequiredVersion = new Version("6.0.0");
string[] output;
try
{
var proc = Process.Start(new ProcessStartInfo()
{
FileName = "dotnet",
Arguments = "--list-runtimes",
RedirectStandardOutput = true,
CreateNoWindow = true
});
proc.WaitForExit();
output = proc.StandardOutput.ReadToEnd().Split("\r\n");
}
public override async Task<bool> CheckOperation()
catch (Exception ex)
{
var minRequiredVersion = new Version("6.0.0");
string[] output;
try
{
var proc = Process.Start(new ProcessStartInfo()
{
FileName = "dotnet",
Arguments = "--list-runtimes",
RedirectStandardOutput = true,
CreateNoWindow = true
});
proc.WaitForExit();
output = proc.StandardOutput.ReadToEnd().Split("\r\n");
}
catch (Exception ex)
{
// TODO: logging
return false;
}
foreach (var lineVersion in output)
{
if (lineVersion.StartsWith("Microsoft.WindowsDesktop.App") && lineVersion.Split(" ").Length > 1)
{
string stringVerion = lineVersion.Split(" ")[1];
var foundVersion = new Version(stringVerion);
// not fully sure if we should only check for 6.x.x versions or if higher major versions are ok -waffle
if (foundVersion >= minRequiredVersion)
{
return true;
}
}
}
// TODO: logging
return false;
}
foreach (var lineVersion in output)
{
if (lineVersion.StartsWith("Microsoft.WindowsDesktop.App") && lineVersion.Split(" ").Length > 1)
{
string stringVerion = lineVersion.Split(" ")[1];
var foundVersion = new Version(stringVerion);
// not fully sure if we should only check for 6.x.x versions or if higher major versions are ok -waffle
if (foundVersion >= minRequiredVersion)
{
return true;
}
}
}
return false;
}
}

View File

@ -1,46 +1,44 @@
using Microsoft.Win32;
using SPTInstaller.Models;
using System;
using System.Threading.Tasks;
namespace SPTInstaller.Installer_Tasks.PreChecks
namespace SPTInstaller.Installer_Tasks.PreChecks;
public class NetFramework472PreCheck : PreCheckBase
{
public class NetFramework472PreCheck : PreCheckBase
public NetFramework472PreCheck() : base(".Net Framework 4.7.2", false)
{
public NetFramework472PreCheck() : base(".Net Framework 4.7.2", false)
}
public override async Task<bool> CheckOperation()
{
try
{
var minRequiredVersion = new Version("4.7.2");
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full");
if (key == null)
{
return false;
}
var value = key.GetValue("Version");
if (value != null && value is string versionString)
{
var installedVersion = new Version(versionString);
return installedVersion > minRequiredVersion;
}
return false;
}
public override async Task<bool> CheckOperation()
catch (Exception ex)
{
try
{
var minRequiredVersion = new Version("4.7.2");
// TODO: log exceptions
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full");
if (key == null)
{
return false;
}
var value = key.GetValue("Version");
if (value != null && value is string versionString)
{
var installedVersion = new Version(versionString);
return installedVersion > minRequiredVersion;
}
return false;
}
catch (Exception ex)
{
// TODO: log exceptions
return false;
}
return false;
}
}
}

View File

@ -1,89 +1,87 @@
using Gitea.Api;
using Gitea.Client;
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Threading.Tasks;
using SPTInstaller.Helpers;
namespace SPTInstaller.Installer_Tasks
namespace SPTInstaller.Installer_Tasks;
public class ReleaseCheckTask : InstallerTaskBase
{
public class ReleaseCheckTask : InstallerTaskBase
private InternalData _data;
public ReleaseCheckTask(InternalData data) : base("Release Checks")
{
private InternalData _data;
_data = data;
}
public ReleaseCheckTask(InternalData data) : base("Release Checks")
public override async Task<IResult> TaskOperation()
{
try
{
_data = data;
Configuration.Default.BasePath = "https://dev.sp-tarkov.com/api/v1";
var repo = new RepositoryApi(Configuration.Default);
SetStatus("Checking SPT Releases", "", null, ProgressStyle.Indeterminate);
var akiRepoReleases = await repo.RepoListReleasesAsync("SPT-AKI", "Stable-releases");
SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate);
var patchRepoReleases = await repo.RepoListReleasesAsync("SPT-AKI", "Downgrade-Patches");
var latestAkiRelease = akiRepoReleases.FindAll(x => !x.Prerelease)[0];
var latestAkiVersion = latestAkiRelease.Name.Replace('(', ' ').Replace(')', ' ').Split(' ')[3];
var comparePatchToAki = patchRepoReleases?.Find(x => x.Name.Contains(_data.OriginalGameVersion) && x.Name.Contains(latestAkiVersion));
_data.PatcherMirrorsLink = comparePatchToAki?.Assets[0].BrowserDownloadUrl;
_data.AkiReleaseDownloadLink = latestAkiRelease.Assets[0].BrowserDownloadUrl;
_data.AkiReleaseHash = FileHashHelper.GetGiteaReleaseHash(latestAkiRelease);
int IntAkiVersion = int.Parse(latestAkiVersion);
int IntGameVersion = int.Parse(_data.OriginalGameVersion);
bool patchNeedCheck = false;
if (IntGameVersion > IntAkiVersion)
{
patchNeedCheck = true;
}
if (IntGameVersion < IntAkiVersion)
{
return Result.FromError("Your client is outdated. Please update EFT");
}
if (IntGameVersion == IntAkiVersion)
{
patchNeedCheck = false;
}
if (comparePatchToAki == null && patchNeedCheck)
{
return Result.FromError("No patcher available for your version");
}
_data.PatchNeeded = patchNeedCheck;
string status = $"Current Release: {latestAkiVersion}";
if (_data.PatchNeeded)
{
status += " - Patch Available";
}
SetStatus(null, status);
return Result.FromSuccess(status);
}
public override async Task<IResult> TaskOperation()
catch (Exception ex)
{
try
{
Configuration.Default.BasePath = "https://dev.sp-tarkov.com/api/v1";
var repo = new RepositoryApi(Configuration.Default);
SetStatus("Checking SPT Releases", "", null, ProgressStyle.Indeterminate);
var akiRepoReleases = await repo.RepoListReleasesAsync("SPT-AKI", "Stable-releases");
SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate);
var patchRepoReleases = await repo.RepoListReleasesAsync("SPT-AKI", "Downgrade-Patches");
var latestAkiRelease = akiRepoReleases.FindAll(x => !x.Prerelease)[0];
var latestAkiVersion = latestAkiRelease.Name.Replace('(', ' ').Replace(')', ' ').Split(' ')[3];
var comparePatchToAki = patchRepoReleases?.Find(x => x.Name.Contains(_data.OriginalGameVersion) && x.Name.Contains(latestAkiVersion));
_data.PatcherMirrorsLink = comparePatchToAki?.Assets[0].BrowserDownloadUrl;
_data.AkiReleaseDownloadLink = latestAkiRelease.Assets[0].BrowserDownloadUrl;
_data.AkiReleaseHash = FileHashHelper.GetGiteaReleaseHash(latestAkiRelease);
int IntAkiVersion = int.Parse(latestAkiVersion);
int IntGameVersion = int.Parse(_data.OriginalGameVersion);
bool patchNeedCheck = false;
if (IntGameVersion > IntAkiVersion)
{
patchNeedCheck = true;
}
if (IntGameVersion < IntAkiVersion)
{
return Result.FromError("Your client is outdated. Please update EFT");
}
if (IntGameVersion == IntAkiVersion)
{
patchNeedCheck = false;
}
if (comparePatchToAki == null && patchNeedCheck)
{
return Result.FromError("No patcher available for your version");
}
_data.PatchNeeded = patchNeedCheck;
string status = $"Current Release: {latestAkiVersion}";
if (_data.PatchNeeded)
{
status += " - Patch Available";
}
SetStatus(null, status);
return Result.FromSuccess(status);
}
catch (Exception ex)
{
//request failed
return Result.FromError($"Request Failed:\n{ex.Message}");
}
//request failed
return Result.FromError($"Request Failed:\n{ex.Message}");
}
}
}

View File

@ -1,88 +1,85 @@
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SPTInstaller.Helpers;
namespace SPTInstaller.Installer_Tasks
namespace SPTInstaller.Installer_Tasks;
public class SetupClientTask : InstallerTaskBase
{
public class SetupClientTask : InstallerTaskBase
private InternalData _data;
public SetupClientTask(InternalData data) : base("Setup Client")
{
private InternalData _data;
_data = data;
}
public SetupClientTask(InternalData data) : base("Setup Client")
public override async Task<IResult> TaskOperation()
{
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
var patcherOutputDir = new DirectoryInfo(Path.Join(_data.TargetInstallPath, "patcher"));
var patcherEXE = new FileInfo(Path.Join(_data.TargetInstallPath, "patcher.exe"));
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
if (_data.PatchNeeded)
{
_data = data;
// extract patcher files
SetStatus("Extrating Patcher", "", 0);
var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, progress);
if (!extractPatcherResult.Succeeded)
{
return extractPatcherResult;
}
// copy patcher files to install directory
SetStatus("Copying Patcher", "", 0);
var patcherDirInfo = patcherOutputDir.GetDirectories("Patcher*", SearchOption.TopDirectoryOnly).First();
var copyPatcherResult = FileHelper.CopyDirectoryWithProgress(patcherDirInfo, targetInstallDirInfo, progress);
if (!copyPatcherResult.Succeeded)
{
return copyPatcherResult;
}
// run patcher
SetStatus("Running Patcher", "", null, ProgressStyle.Indeterminate);
var patchingResult = ProcessHelper.PatchClientFiles(patcherEXE, targetInstallDirInfo);
if (!patchingResult.Succeeded)
{
return patchingResult;
}
}
public override async Task<IResult> TaskOperation()
// extract release files
SetStatus("Extracting Release", "", 0);
var extractReleaseResult = ZipHelper.Decompress(_data.AkiZipInfo, targetInstallDirInfo, progress);
if (!extractReleaseResult.Succeeded)
{
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
var patcherOutputDir = new DirectoryInfo(Path.Join(_data.TargetInstallPath, "patcher"));
var patcherEXE = new FileInfo(Path.Join(_data.TargetInstallPath, "patcher.exe"));
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
if (_data.PatchNeeded)
{
// extract patcher files
SetStatus("Extrating Patcher", "", 0);
var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, progress);
if (!extractPatcherResult.Succeeded)
{
return extractPatcherResult;
}
// copy patcher files to install directory
SetStatus("Copying Patcher", "", 0);
var patcherDirInfo = patcherOutputDir.GetDirectories("Patcher*", SearchOption.TopDirectoryOnly).First();
var copyPatcherResult = FileHelper.CopyDirectoryWithProgress(patcherDirInfo, targetInstallDirInfo, progress);
if (!copyPatcherResult.Succeeded)
{
return copyPatcherResult;
}
// run patcher
SetStatus("Running Patcher", "", null, ProgressStyle.Indeterminate);
var patchingResult = ProcessHelper.PatchClientFiles(patcherEXE, targetInstallDirInfo);
if (!patchingResult.Succeeded)
{
return patchingResult;
}
}
// extract release files
SetStatus("Extracting Release", "", 0);
var extractReleaseResult = ZipHelper.Decompress(_data.AkiZipInfo, targetInstallDirInfo, progress);
if (!extractReleaseResult.Succeeded)
{
return extractReleaseResult;
}
// cleanup temp files
SetStatus("Cleanup", "almost done :)", null, ProgressStyle.Indeterminate);
if(_data.PatchNeeded)
{
patcherOutputDir.Delete(true);
patcherEXE.Delete();
}
return Result.FromSuccess("SPT is Setup. Happy Playing!");
return extractReleaseResult;
}
// cleanup temp files
SetStatus("Cleanup", "almost done :)", null, ProgressStyle.Indeterminate);
if(_data.PatchNeeded)
{
patcherOutputDir.Delete(true);
patcherEXE.Delete();
}
return Result.FromSuccess("SPT is Setup. Happy Playing!");
}
}

View File

@ -1,35 +1,33 @@
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Threading.Tasks;
namespace SPTInstaller.Installer_Tasks
namespace SPTInstaller.Installer_Tasks;
internal class TestTask : InstallerTaskBase
{
internal class TestTask : InstallerTaskBase
public static TestTask FromRandomName() => new TestTask($"Test Task #{new Random().Next(0, 9999)}");
public TestTask(string name) : base(name)
{
public static TestTask FromRandomName() => new TestTask($"Test Task #{new Random().Next(0, 9999)}");
}
public TestTask(string name) : base(name)
public async override Task<IResult> TaskOperation()
{
var total = 4;
var interval = TimeSpan.FromSeconds(1);
for(var i = 0; i < total; i++)
{
var count = i + 1;
var progressMessage = $"Running Task: {Name}";
var progress = (int)Math.Floor((double)count / total * 100);
SetStatus(progressMessage, $"Details: ({count}/{total})", progress);
await Task.Delay(interval);
}
public async override Task<IResult> TaskOperation()
{
int total = 4;
TimeSpan interval = TimeSpan.FromSeconds(1);
for(int i = 0; i < total; i++)
{
var count = i + 1;
string progressMessage = $"Running Task: {Name}";
int progress = (int)Math.Floor((double)count / total * 100);
SetStatus(progressMessage, $"Details: ({count}/{total})", progress);
await Task.Delay(interval);
}
return Result.FromSuccess();
}
return Result.FromSuccess();
}
}

View File

@ -1,17 +1,16 @@
using System.Threading.Tasks;
namespace SPTInstaller.Interfaces
namespace SPTInstaller.Interfaces;
public interface IPreCheck
{
public interface IPreCheck
{
public string Id { get; }
public string Name { get; }
public bool IsRequired { get; }
public string Id { get; }
public string Name { get; }
public bool IsRequired { get; }
public bool IsPending { get; set; }
public bool IsPending { get; set; }
public bool Passed { get; }
public bool Passed { get; }
public Task<IResult> RunCheck();
}
public Task<IResult> RunCheck();
}

View File

@ -1,24 +1,23 @@
using System.Threading.Tasks;
namespace SPTInstaller.Interfaces
namespace SPTInstaller.Interfaces;
public interface IProgressableTask
{
public interface IProgressableTask
{
public string Id { get; }
public string Name { get; }
public string Id { get; }
public string Name { get; }
public bool IsCompleted { get; }
public bool IsCompleted { get; }
public bool HasErrors { get; }
public bool HasErrors { get; }
public bool IsRunning { get; }
public bool IsRunning { get; }
public string StatusMessage { get; }
public string StatusMessage { get; }
public int Progress { get; }
public int Progress { get; }
public bool ShowProgress { get; }
public bool ShowProgress { get; }
public Task<IResult> RunAsync();
}
public Task<IResult> RunAsync();
}

View File

@ -1,14 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SPTInstaller.Interfaces;
namespace SPTInstaller.Interfaces
public interface IResult
{
public interface IResult
{
public bool Succeeded { get; }
public string Message { get; }
}
public bool Succeeded { get; }
public string Message { get; }
}

View File

@ -1,8 +1,7 @@
namespace SPTInstaller.Models
namespace SPTInstaller.Models;
public class DownloadMirror
{
public class DownloadMirror
{
public string Link { get; set; }
public string Hash { get; set; }
}
public string Link { get; set; }
public string Hash { get; set; }
}

View File

@ -1,183 +1,178 @@
using Avalonia.Threading;
using ReactiveUI;
using ReactiveUI;
using Serilog;
using Splat;
using SPTInstaller.Interfaces;
using System;
using System.Security;
using System.Threading.Tasks;
namespace SPTInstaller.Models
namespace SPTInstaller.Models;
public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
{
public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
private string _id;
public string Id
{
private string _id;
public string Id
{
get => _id;
private set => this.RaiseAndSetIfChanged(ref _id, value);
}
get => _id;
private set => this.RaiseAndSetIfChanged(ref _id, value);
}
private string _name;
public string Name
{
get => _name;
private set => this.RaiseAndSetIfChanged(ref _name, value);
}
private string _name;
public string Name
{
get => _name;
private set => this.RaiseAndSetIfChanged(ref _name, value);
}
private bool _isComleted;
public bool IsCompleted
{
get => _isComleted;
private set => this.RaiseAndSetIfChanged(ref _isComleted, value);
}
private bool _isComleted;
public bool IsCompleted
{
get => _isComleted;
private set => this.RaiseAndSetIfChanged(ref _isComleted, value);
}
private bool _hasErrors;
public bool HasErrors
{
get => _hasErrors;
private set => this.RaiseAndSetIfChanged(ref _hasErrors, value);
}
private bool _hasErrors;
public bool HasErrors
{
get => _hasErrors;
private set => this.RaiseAndSetIfChanged(ref _hasErrors, value);
}
private bool _isRunning;
public bool IsRunning
{
get => _isRunning;
private set => this.RaiseAndSetIfChanged(ref _isRunning, value);
}
private bool _isRunning;
public bool IsRunning
{
get => _isRunning;
private set => this.RaiseAndSetIfChanged(ref _isRunning, value);
}
private int _progress;
public int Progress
{
get => _progress;
private set => this.RaiseAndSetIfChanged(ref _progress, value);
}
private int _progress;
public int Progress
{
get => _progress;
private set => this.RaiseAndSetIfChanged(ref _progress, value);
}
private bool _showProgress;
public bool ShowProgress
{
get => _showProgress;
private set => this.RaiseAndSetIfChanged(ref _showProgress, value);
}
private bool _showProgress;
public bool ShowProgress
{
get => _showProgress;
private set => this.RaiseAndSetIfChanged(ref _showProgress, value);
}
private bool _indeterminateProgress;
public bool IndeterminateProgress
{
get => _indeterminateProgress;
private set => this.RaiseAndSetIfChanged(ref _indeterminateProgress, value);
}
private bool _indeterminateProgress;
public bool IndeterminateProgress
{
get => _indeterminateProgress;
private set => this.RaiseAndSetIfChanged(ref _indeterminateProgress, value);
}
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
private set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
private set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
private string _statusDetails;
public string StatusDetails
{
get => _statusDetails;
private set => this.RaiseAndSetIfChanged(ref _statusDetails, value);
}
private string _statusDetails;
public string StatusDetails
{
get => _statusDetails;
private set => this.RaiseAndSetIfChanged(ref _statusDetails, value);
}
public enum ProgressStyle
{
Hidden = 0,
Shown,
Indeterminate,
}
public enum ProgressStyle
{
Hidden = 0,
Shown,
Indeterminate,
}
/// <summary>
/// Update the status details of the task
/// </summary>
/// <param name="message">The main message to display. Not updated if null</param>
/// <param name="details">The details of the task. Not updated if null</param>
/// <param name="progress">Progress of the task. Overrides progressStyle if a non-null value is supplied</param>
/// <param name="progressStyle">The style of the progress bar</param>
public void SetStatus(string? message, string? details, int? progress = null, ProgressStyle? progressStyle = null, bool noLog = false)
/// <summary>
/// Update the status details of the task
/// </summary>
/// <param name="message">The main message to display. Not updated if null</param>
/// <param name="details">The details of the task. Not updated if null</param>
/// <param name="progress">Progress of the task. Overrides progressStyle if a non-null value is supplied</param>
/// <param name="progressStyle">The style of the progress bar</param>
public void SetStatus(string? message, string? details, int? progress = null, ProgressStyle? progressStyle = null, bool noLog = false)
{
if(message != null && message != StatusMessage)
{
if(message != null && message != StatusMessage)
if (!noLog && !string.IsNullOrWhiteSpace(message))
{
if (!noLog && !string.IsNullOrWhiteSpace(message))
{
Log.Information($" <===> {message} <===>");
}
StatusMessage = message;
Log.Information($" <===> {message} <===>");
}
if(details != null && details != StatusDetails)
{
if (!noLog && !string.IsNullOrWhiteSpace(details))
{
Log.Information(details);
}
StatusMessage = message;
}
StatusDetails = details;
if(details != null && details != StatusDetails)
{
if (!noLog && !string.IsNullOrWhiteSpace(details))
{
Log.Information(details);
}
if (progressStyle != null)
{
switch (progressStyle)
{
case ProgressStyle.Hidden:
ShowProgress = false;
IndeterminateProgress = false;
break;
case ProgressStyle.Shown:
ShowProgress = true;
IndeterminateProgress = false;
break;
case ProgressStyle.Indeterminate:
ShowProgress = true;
IndeterminateProgress = true;
break;
}
}
StatusDetails = details;
}
if (progress != null)
if (progressStyle != null)
{
switch (progressStyle)
{
ShowProgress = true;
IndeterminateProgress = false;
Progress = progress.Value;
case ProgressStyle.Hidden:
ShowProgress = false;
IndeterminateProgress = false;
break;
case ProgressStyle.Shown:
ShowProgress = true;
IndeterminateProgress = false;
break;
case ProgressStyle.Indeterminate:
ShowProgress = true;
IndeterminateProgress = true;
break;
}
}
public InstallerTaskBase(string name)
if (progress != null)
{
Name = name;
Id = Guid.NewGuid().ToString();
ShowProgress = true;
IndeterminateProgress = false;
Progress = progress.Value;
}
}
/// <summary>
/// A method for the install controller to call. Do not use this within your task
/// </summary>
/// <returns></returns>
public async Task<IResult> RunAsync()
public InstallerTaskBase(string name)
{
Name = name;
Id = Guid.NewGuid().ToString();
}
/// <summary>
/// A method for the install controller to call. Do not use this within your task
/// </summary>
/// <returns></returns>
public async Task<IResult> RunAsync()
{
IsRunning = true;
var result = await TaskOperation();
IsRunning = false;
if (!result.Succeeded)
{
IsRunning = true;
var result = await TaskOperation();
IsRunning = false;
if (!result.Succeeded)
{
HasErrors = true;
return result;
}
IsCompleted = true;
HasErrors = true;
return result;
}
/// <summary>
/// The task you want to run
/// </summary>
/// <returns></returns>
public abstract Task<IResult> TaskOperation();
IsCompleted = true;
return result;
}
/// <summary>
/// The task you want to run
/// </summary>
/// <returns></returns>
public abstract Task<IResult> TaskOperation();
}

View File

@ -1,58 +1,56 @@
using System.Collections.Generic;
using System.IO;
namespace SPTInstaller.Models
namespace SPTInstaller.Models;
public class InternalData
{
public class InternalData
{
/// <summary>
/// The folder to install SPT into
/// </summary>
public string? TargetInstallPath { get; set; }
/// <summary>
/// The folder to install SPT into
/// </summary>
public string? TargetInstallPath { get; set; }
/// <summary>
/// The orginal EFT game path
/// </summary>
public string? OriginalGamePath { get; set; }
/// <summary>
/// The orginal EFT game path
/// </summary>
public string? OriginalGamePath { get; set; }
/// <summary>
/// The original EFT game version
/// </summary>
public string OriginalGameVersion { get; set; }
/// <summary>
/// The original EFT game version
/// </summary>
public string OriginalGameVersion { get; set; }
/// <summary>
/// Patcher zip file info
/// </summary>
public FileInfo PatcherZipInfo { get; set; }
/// <summary>
/// Patcher zip file info
/// </summary>
public FileInfo PatcherZipInfo { get; set; }
/// <summary>
/// SPT-AKI zip file info
/// </summary>
public FileInfo AkiZipInfo { get; set; }
/// <summary>
/// SPT-AKI zip file info
/// </summary>
public FileInfo AkiZipInfo { get; set; }
/// <summary>
/// The release download link for SPT-AKI
/// </summary>
public string AkiReleaseDownloadLink { get; set; }
/// <summary>
/// The release download link for SPT-AKI
/// </summary>
public string AkiReleaseDownloadLink { get; set; }
/// <summary>
/// The release zip hash
/// </summary>
public string AkiReleaseHash { get; set; } = null;
/// <summary>
/// The release zip hash
/// </summary>
public string AkiReleaseHash { get; set; } = null;
/// <summary>
/// The release download link for the patcher mirror list
/// </summary>
public string PatcherMirrorsLink { get; set; }
/// <summary>
/// The release download link for the patcher mirror list
/// </summary>
public string PatcherMirrorsLink { get; set; }
/// <summary>
/// The release download mirrors for the patcher
/// </summary>
public List<DownloadMirror> PatcherReleaseMirrors { get; set; } = null;
/// <summary>
/// The release download mirrors for the patcher
/// </summary>
public List<DownloadMirror> PatcherReleaseMirrors { get; set; } = null;
/// <summary>
/// Whether or not a patch is needed to downgrade the client files
/// </summary>
public bool PatchNeeded { get; set; }
}
/// <summary>
/// Whether or not a patch is needed to downgrade the client files
/// </summary>
public bool PatchNeeded { get; set; }
}

View File

@ -1,76 +1,74 @@
using ReactiveUI;
using SPTInstaller.Interfaces;
using System;
using System.Threading.Tasks;
namespace SPTInstaller.Models
namespace SPTInstaller.Models;
public abstract class PreCheckBase : ReactiveObject, IPreCheck
{
public abstract class PreCheckBase : ReactiveObject, IPreCheck
private string _id;
public string Id
{
private string _id;
public string Id
{
get => _id;
set => this.RaiseAndSetIfChanged(ref _id, value);
}
private string _name;
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
private bool _required;
public bool IsRequired
{
get => _required;
set => this.RaiseAndSetIfChanged(ref _required, value);
}
private bool _passed;
public bool Passed
{
get => _passed;
set => this.RaiseAndSetIfChanged(ref _passed, value);
}
private bool _isPending;
public bool IsPending
{
get => _isPending;
set => this.RaiseAndSetIfChanged(ref _isPending, value);
}
private bool _isRunning;
public bool IsRunning
{
get => _isRunning;
set => this.RaiseAndSetIfChanged(ref _isRunning, value);
}
/// <summary>
/// Base class for pre-checks to run before installation
/// </summary>
/// <param name="name">The display name of the pre-check</param>
/// <param name="required">If installation should stop on failing this pre-check</param>
public PreCheckBase(string name, bool required)
{
Name = name;
IsRequired = required;
Id = Guid.NewGuid().ToString();
}
public async Task<IResult> RunCheck()
{
IsRunning = true;
Passed = await CheckOperation();
IsRunning = false;
IsPending = false;
return Passed ? Result.FromSuccess() : Result.FromError($"PreCheck Failed: {Name}");
}
public abstract Task<bool> CheckOperation();
get => _id;
set => this.RaiseAndSetIfChanged(ref _id, value);
}
private string _name;
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
private bool _required;
public bool IsRequired
{
get => _required;
set => this.RaiseAndSetIfChanged(ref _required, value);
}
private bool _passed;
public bool Passed
{
get => _passed;
set => this.RaiseAndSetIfChanged(ref _passed, value);
}
private bool _isPending;
public bool IsPending
{
get => _isPending;
set => this.RaiseAndSetIfChanged(ref _isPending, value);
}
private bool _isRunning;
public bool IsRunning
{
get => _isRunning;
set => this.RaiseAndSetIfChanged(ref _isRunning, value);
}
/// <summary>
/// Base class for pre-checks to run before installation
/// </summary>
/// <param name="name">The display name of the pre-check</param>
/// <param name="required">If installation should stop on failing this pre-check</param>
public PreCheckBase(string name, bool required)
{
Name = name;
IsRequired = required;
Id = Guid.NewGuid().ToString();
}
public async Task<IResult> RunCheck()
{
IsRunning = true;
Passed = await CheckOperation();
IsRunning = false;
IsPending = false;
return Passed ? Result.FromSuccess() : Result.FromError($"PreCheck Failed: {Name}");
}
public abstract Task<bool> CheckOperation();
}

View File

@ -1,20 +1,19 @@
using SPTInstaller.Interfaces;
namespace SPTInstaller.Models
namespace SPTInstaller.Models;
public class Result : IResult
{
public class Result : IResult
public bool Succeeded { get; private set; }
public string Message { get; private set; }
protected Result(string message, bool succeeded)
{
public bool Succeeded { get; private set; }
public string Message { get; private set; }
protected Result(string message, bool succeeded)
{
Message = message;
Succeeded = succeeded;
}
public static Result FromSuccess(string message = "") => new Result(message, true);
public static Result FromError(string message) => new Result(message, false);
Message = message;
Succeeded = succeeded;
}
public static Result FromSuccess(string message = "") => new(message, true);
public static Result FromError(string message) => new(message, false);
}

View File

@ -12,44 +12,44 @@ using SPTInstaller.Models;
using System.Linq;
using System.Reflection;
namespace SPTInstaller
namespace SPTInstaller;
internal class Program
{
internal class Program
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
// Register all the things
// Regestering as base classes so ReactiveUI works correctly. Doesn't seem to like the interfaces :(
ServiceHelper.Register<InternalData>();
ServiceHelper.Register<PreCheckBase, NetFramework472PreCheck>();
ServiceHelper.Register<PreCheckBase, NetCore6PreCheck>();
ServiceHelper.Register<PreCheckBase, FreeSpacePreCheck>();
// Register all the things
// Regestering as base classes so ReactiveUI works correctly. Doesn't seem to like the interfaces :(
ServiceHelper.Register<InternalData>();
ServiceHelper.Register<PreCheckBase, NetFramework472PreCheck>();
ServiceHelper.Register<PreCheckBase, NetCore6PreCheck>();
ServiceHelper.Register<PreCheckBase, FreeSpacePreCheck>();
#if !TEST
var logPath = Path.Join(Environment.CurrentDirectory, "spt-aki-installer_.log");
var logPath = Path.Join(Environment.CurrentDirectory, "spt-aki-installer_.log");
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo
.File(path: logPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug,
rollingInterval: RollingInterval.Day)
.CreateLogger();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo
.File(path: logPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug,
rollingInterval: RollingInterval.Day)
.CreateLogger();
ServiceHelper.Register<InstallerTaskBase, InitializationTask>();
ServiceHelper.Register<InstallerTaskBase, ReleaseCheckTask>();
ServiceHelper.Register<InstallerTaskBase, DownloadTask>();
ServiceHelper.Register<InstallerTaskBase, CopyClientTask>();
ServiceHelper.Register<InstallerTaskBase, SetupClientTask>();
ServiceHelper.Register<InstallerTaskBase, InitializationTask>();
ServiceHelper.Register<InstallerTaskBase, ReleaseCheckTask>();
ServiceHelper.Register<InstallerTaskBase, DownloadTask>();
ServiceHelper.Register<InstallerTaskBase, CopyClientTask>();
ServiceHelper.Register<InstallerTaskBase, SetupClientTask>();
#else
for (int i = 0; i < 5; i++)
{
@ -57,19 +57,18 @@ namespace SPTInstaller
}
#endif
// need the interfaces for the controller and splat won't resolve them since we need to base classes in avalonia (what a mess), so doing it manually here
var tasks = Locator.Current.GetServices<InstallerTaskBase>().ToArray() as IProgressableTask[];
var preChecks = Locator.Current.GetServices<PreCheckBase>().ToArray() as IPreCheck[];
// need the interfaces for the controller and splat won't resolve them since we need to base classes in avalonia (what a mess), so doing it manually here
var tasks = Locator.Current.GetServices<InstallerTaskBase>().ToArray() as IProgressableTask[];
var preChecks = Locator.Current.GetServices<PreCheckBase>().ToArray() as IPreCheck[];
var installer = new InstallController(tasks, preChecks);
var installer = new InstallController(tasks, preChecks);
// manually register install controller
Locator.CurrentMutable.RegisterConstant(installer);
// manually register install controller
Locator.CurrentMutable.RegisterConstant(installer);
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
}
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
}
}

View File

@ -1,28 +1,26 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using SPTInstaller.ViewModels;
using System;
namespace SPTInstaller
namespace SPTInstaller;
public class ViewLocator : IDataTemplate
{
public class ViewLocator : IDataTemplate
public IControl Build(object data)
{
public IControl Build(object data)
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
return (Control)Activator.CreateInstance(type)!;
}
public bool Match(object data)
{
return data is ViewModelBase;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}

View File

@ -4,35 +4,32 @@ using SPTInstaller.Helpers;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace SPTInstaller.ViewModels
namespace SPTInstaller.ViewModels;
public class InstallViewModel : ViewModelBase
{
public class InstallViewModel : ViewModelBase
private IProgressableTask _currentTask;
public IProgressableTask CurrentTask
{
private IProgressableTask _currentTask;
public IProgressableTask CurrentTask
get => _currentTask;
set => this.RaiseAndSetIfChanged(ref _currentTask, value);
}
public ObservableCollection<InstallerTaskBase> MyTasks { get; set; } = new(ServiceHelper.GetAll<InstallerTaskBase>());
public InstallViewModel(IScreen host) : base(host)
{
var installer = ServiceHelper.Get<InstallController>();
installer.TaskChanged += (sender, task) => CurrentTask = task;
Task.Run(async () =>
{
get => _currentTask;
set => this.RaiseAndSetIfChanged(ref _currentTask, value);
}
var result = await installer.RunTasks();
public ObservableCollection<InstallerTaskBase> MyTasks { get; set; }
= new ObservableCollection<InstallerTaskBase>(ServiceHelper.GetAll<InstallerTaskBase>());
public InstallViewModel(IScreen host) : base(host)
{
var installer = ServiceHelper.Get<InstallController>();
installer.TaskChanged += (sender, task) => CurrentTask = task;
Task.Run(async () =>
{
var result = await installer.RunTasks();
NavigateTo(new MessageViewModel(HostScreen, result));
});
}
NavigateTo(new MessageViewModel(HostScreen, result));
});
}
}

View File

@ -1,50 +1,48 @@
using Avalonia;
using ReactiveUI;
using Serilog;
using System;
using System.Reflection;
namespace SPTInstaller.ViewModels
namespace SPTInstaller.ViewModels;
public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScreen
{
public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScreen
public RoutingState Router { get; } = new();
public ViewModelActivator Activator { get; } = new();
private string _title;
public string Title
{
public RoutingState Router { get; } = new RoutingState();
public ViewModelActivator Activator { get; } = new ViewModelActivator();
private string _title;
public string Title
{
get => _title;
set => this.RaiseAndSetIfChanged(ref _title, value);
}
public MainWindowViewModel()
{
string? version = Assembly.GetExecutingAssembly().GetName()?.Version?.ToString();
Title = $"SPT Installer {"v" + version ?? "--unknown version--"}";
Log.Information($"========= {Title} Started =========");
Log.Information(Environment.OSVersion.VersionString);
Router.Navigate.Execute(new PreChecksViewModel(this));
}
public void CloseCommand()
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.Close();
}
}
public void MinimizeCommand()
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.WindowState = Avalonia.Controls.WindowState.Minimized;
}
}
get => _title;
set => this.RaiseAndSetIfChanged(ref _title, value);
}
public MainWindowViewModel()
{
string? version = Assembly.GetExecutingAssembly().GetName()?.Version?.ToString();
Title = $"SPT Installer {"v" + version ?? "--unknown version--"}";
Log.Information($"========= {Title} Started =========");
Log.Information(Environment.OSVersion.VersionString);
Router.Navigate.Execute(new PreChecksViewModel(this));
}
public void CloseCommand()
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.Close();
}
}
public void MinimizeCommand()
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.WindowState = Avalonia.Controls.WindowState.Minimized;
}
}
}

View File

@ -4,44 +4,43 @@ using Serilog;
using SPTInstaller.Interfaces;
using System.Windows.Input;
namespace SPTInstaller.ViewModels
namespace SPTInstaller.ViewModels;
public class MessageViewModel : ViewModelBase
{
public class MessageViewModel : ViewModelBase
private bool _HasErrors;
public bool HasErrors
{
private bool _HasErrors;
public bool HasErrors
get => _HasErrors;
set => this.RaiseAndSetIfChanged(ref _HasErrors, value);
}
private string _Message;
public string Message
{
get => _Message;
set => this.RaiseAndSetIfChanged(ref _Message, value);
}
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
get => _HasErrors;
set => this.RaiseAndSetIfChanged(ref _HasErrors, value);
desktopApp.MainWindow.Close();
}
});
public MessageViewModel(IScreen Host, IResult result) : base(Host)
{
Message = result.Message;
if(result.Succeeded)
{
Log.Information(Message);
return;
}
private string _Message;
public string Message
{
get => _Message;
set => this.RaiseAndSetIfChanged(ref _Message, value);
}
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.Close();
}
});
public MessageViewModel(IScreen Host, IResult result) : base(Host)
{
Message = result.Message;
if(result.Succeeded)
{
Log.Information(Message);
return;
}
HasErrors = true;
Log.Error(Message);
}
HasErrors = true;
Log.Error(Message);
}
}

View File

@ -1,61 +1,58 @@
using Avalonia.Threading;
using ReactiveUI;
using System.Threading.Tasks;
using System;
namespace SPTInstaller.ViewModels
namespace SPTInstaller.ViewModels;
public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableViewModel
{
public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableViewModel
public ViewModelActivator Activator { get; } = new();
public string? UrlPathSegment => Guid.NewGuid().ToString().Substring(0, 7);
public IScreen HostScreen { get; }
/// <summary>
/// Delay the return of the viewmodel
/// </summary>
/// <param name="Milliseconds">The amount of time in milliseconds to delay</param>
/// <returns>The viewmodel after the delay time</returns>
/// <remarks>Useful to delay the navigation to another view. For instance, to allow an animation to complete.</remarks>
private async Task<ViewModelBase> WithDelay(int Milliseconds)
{
public ViewModelActivator Activator { get; } = new ViewModelActivator();
await Task.Delay(Milliseconds);
public string? UrlPathSegment => Guid.NewGuid().ToString().Substring(0, 7);
public IScreen HostScreen { get; }
/// <summary>
/// Delay the return of the viewmodel
/// </summary>
/// <param name="Milliseconds">The amount of time in milliseconds to delay</param>
/// <returns>The viewmodel after the delay time</returns>
/// <remarks>Useful to delay the navigation to another view. For instance, to allow an animation to complete.</remarks>
private async Task<ViewModelBase> WithDelay(int Milliseconds)
{
await Task.Delay(Milliseconds);
return this;
}
/// <summary>
/// Navigate to another viewmodel after a delay
/// </summary>
/// <param name="ViewModel"></param>
/// <param name="Milliseconds"></param>
/// <returns></returns>
public async Task NavigateToWithDelay(ViewModelBase ViewModel, int Milliseconds)
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
HostScreen.Router.Navigate.Execute(await ViewModel.WithDelay(Milliseconds));
});
}
/// <summary>
/// Navigate to another viewmodel
/// </summary>
/// <param name="ViewModel"></param>
public void NavigateTo(ViewModelBase ViewModel)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
HostScreen.Router.Navigate.Execute(ViewModel);
});
}
public ViewModelBase(IScreen Host)
{
HostScreen = Host;
}
return this;
}
/// <summary>
/// Navigate to another viewmodel after a delay
/// </summary>
/// <param name="ViewModel"></param>
/// <param name="Milliseconds"></param>
/// <returns></returns>
public async Task NavigateToWithDelay(ViewModelBase ViewModel, int Milliseconds)
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
HostScreen.Router.Navigate.Execute(await ViewModel.WithDelay(Milliseconds));
});
}
/// <summary>
/// Navigate to another viewmodel
/// </summary>
/// <param name="ViewModel"></param>
public void NavigateTo(ViewModelBase ViewModel)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
HostScreen.Router.Navigate.Execute(ViewModel);
});
}
public ViewModelBase(IScreen Host)
{
HostScreen = Host;
}
}

View File

@ -1,14 +1,12 @@
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using SPTInstaller.ViewModels;
namespace SPTInstaller.Views
namespace SPTInstaller.Views;
public partial class InstallView : ReactiveUserControl<InstallViewModel>
{
public partial class InstallView : ReactiveUserControl<InstallViewModel>
public InstallView()
{
public InstallView()
{
InitializeComponent();
}
InitializeComponent();
}
}

View File

@ -1,12 +1,11 @@
using Avalonia.Controls;
namespace SPTInstaller.Views
namespace SPTInstaller.Views;
public partial class MainWindow : Window
{
public partial class MainWindow : Window
public MainWindow()
{
public MainWindow()
{
InitializeComponent();
}
InitializeComponent();
}
}

View File

@ -1,14 +1,12 @@
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using SPTInstaller.ViewModels;
namespace SPTInstaller.Views
namespace SPTInstaller.Views;
public partial class MessageView : ReactiveUserControl<MessageViewModel>
{
public partial class MessageView : ReactiveUserControl<MessageViewModel>
public MessageView()
{
public MessageView()
{
InitializeComponent();
}
InitializeComponent();
}
}

View File

@ -1,13 +1,12 @@
using Avalonia.ReactiveUI;
using SPTInstaller.ViewModels;
namespace SPTInstaller.Views
namespace SPTInstaller.Views;
public partial class PreChecksView : ReactiveUserControl<PreChecksViewModel>
{
public partial class PreChecksView : ReactiveUserControl<PreChecksViewModel>
public PreChecksView()
{
public PreChecksView()
{
InitializeComponent();
}
InitializeComponent();
}
}