Refactor install process to use major/minor task queue
Tasks are broken down into major and minor tasks. Major tasks are like install game, redistributable, or tool. These are reported as individual tasks in the download queue. Minor tasks are running of a script, downloading saves, extracting/downloading, etc.
This commit is contained in:
parent
31dc9e4d3a
commit
eaaaf39b8d
18 changed files with 1211 additions and 176 deletions
|
|
@ -11,6 +11,7 @@ using LANCommander.Launcher.Avalonia.Services;
|
|||
using LANCommander.Launcher.Models;
|
||||
using LANCommander.Launcher.Services;
|
||||
using LANCommander.SDK.Enums;
|
||||
using LANCommander.SDK.Models;
|
||||
using LANCommander.SDK.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -92,6 +93,7 @@ public partial class DownloadQueueViewModel : ViewModelBase
|
|||
|
||||
_installService.OnQueueChanged += OnQueueChanged;
|
||||
_installService.OnProgress += OnProgress;
|
||||
_installService.OnTaskProgressUpdate += OnTaskProgressUpdate;
|
||||
_installService.OnInstallComplete += OnInstallComplete;
|
||||
_installService.OnInstallFail += OnInstallFail;
|
||||
|
||||
|
|
@ -104,6 +106,37 @@ public partial class DownloadQueueViewModel : ViewModelBase
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnTaskProgressUpdate(InstallTaskProgress taskProgress)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var item = QueueItems.FirstOrDefault(i => i.Id == taskProgress.QueueItemId);
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
var task = item.Tasks.FirstOrDefault(t => t.Id == taskProgress.TaskId);
|
||||
if (task == null)
|
||||
return;
|
||||
|
||||
task.Status = taskProgress.TaskStatus;
|
||||
task.Progress = taskProgress.Progress;
|
||||
task.BytesTransferred = taskProgress.BytesTransferred;
|
||||
task.TotalBytes = taskProgress.TotalBytes;
|
||||
task.TransferSpeed = taskProgress.TransferSpeed;
|
||||
task.ErrorMessage = taskProgress.ErrorMessage;
|
||||
|
||||
item.CurrentTask = task;
|
||||
|
||||
// Update current status from task
|
||||
if (item == CurrentItem)
|
||||
{
|
||||
CurrentStatus = taskProgress.TaskTitle;
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnProgress(InstallProgress progress)
|
||||
{
|
||||
_taskbarProgressService.SetProgress(progress.Progress);
|
||||
|
|
@ -364,9 +397,18 @@ public partial class InstallQueueItemViewModel : ViewModelBase
|
|||
[ObservableProperty]
|
||||
private bool _isUpdate;
|
||||
|
||||
public bool IsActive => Status != InstallStatus.Queued &&
|
||||
Status != InstallStatus.Complete &&
|
||||
Status != InstallStatus.Failed &&
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<InstallTaskItemViewModel> _tasks = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private InstallTaskItemViewModel? _currentTask;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hasTasks;
|
||||
|
||||
public bool IsActive => Status != InstallStatus.Queued &&
|
||||
Status != InstallStatus.Complete &&
|
||||
Status != InstallStatus.Failed &&
|
||||
Status != InstallStatus.Canceled;
|
||||
|
||||
public bool IsQueued => Status == InstallStatus.Queued;
|
||||
|
|
@ -390,6 +432,15 @@ public partial class InstallQueueItemViewModel : ViewModelBase
|
|||
CoverId = item.CoverId;
|
||||
IconId = item.IconId;
|
||||
IsUpdate = item.IsUpdate;
|
||||
|
||||
if (item.Tasks != null && item.Tasks.Count > 0)
|
||||
{
|
||||
foreach (var taskDef in item.Tasks.OrderBy(t => t.Order))
|
||||
{
|
||||
Tasks.Add(new InstallTaskItemViewModel(taskDef));
|
||||
}
|
||||
HasTasks = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatBytes(long bytes)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using LANCommander.SDK.Enums;
|
||||
using LANCommander.SDK.Models;
|
||||
|
||||
namespace LANCommander.Launcher.Avalonia.ViewModels;
|
||||
|
||||
public partial class InstallTaskItemViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
private Guid _id;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _title = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private InstallTaskType _type;
|
||||
|
||||
[ObservableProperty]
|
||||
private InstallTaskStatus _status = InstallTaskStatus.Queued;
|
||||
|
||||
[ObservableProperty]
|
||||
private float _progress;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _reportsProgress;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isCritical;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _errorMessage;
|
||||
|
||||
[ObservableProperty]
|
||||
private long _bytesTransferred;
|
||||
|
||||
[ObservableProperty]
|
||||
private long _totalBytes;
|
||||
|
||||
[ObservableProperty]
|
||||
private long _transferSpeed;
|
||||
|
||||
public bool IsCompleted => Status == InstallTaskStatus.Completed;
|
||||
public bool IsRunning => Status == InstallTaskStatus.Running;
|
||||
public bool IsFailed => Status == InstallTaskStatus.Failed;
|
||||
public bool IsQueued => Status == InstallTaskStatus.Queued;
|
||||
public bool IsSkipped => Status == InstallTaskStatus.Skipped;
|
||||
|
||||
public InstallTaskItemViewModel() { }
|
||||
|
||||
public InstallTaskItemViewModel(InstallTaskDefinition taskDef)
|
||||
{
|
||||
Id = taskDef.Id;
|
||||
Title = taskDef.Title;
|
||||
Type = taskDef.Type;
|
||||
ReportsProgress = taskDef.ReportsProgress;
|
||||
IsCritical = taskDef.IsCritical;
|
||||
}
|
||||
}
|
||||
|
|
@ -114,6 +114,51 @@
|
|||
IsVisible="{Binding TimeRemainingText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Task list -->
|
||||
<ItemsControl Grid.Row="3"
|
||||
ItemsSource="{Binding CurrentItem.Tasks}"
|
||||
IsVisible="{Binding CurrentItem.HasTasks}"
|
||||
Margin="0,12,0,0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:InstallTaskItemViewModel">
|
||||
<Grid ColumnDefinitions="20,*,Auto"
|
||||
Margin="0,2">
|
||||
<!-- Status icon -->
|
||||
<Panel Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Text="✓" Foreground="#4CAF50" FontSize="11"
|
||||
IsVisible="{Binding IsCompleted}" />
|
||||
<TextBlock Text="●" Foreground="#2196F3" FontSize="8"
|
||||
IsVisible="{Binding IsRunning}"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||
<TextBlock Text="✕" Foreground="IndianRed" FontSize="11"
|
||||
IsVisible="{Binding IsFailed}" />
|
||||
<TextBlock Text="―" Opacity="0.3" FontSize="11"
|
||||
IsVisible="{Binding IsQueued}" />
|
||||
</Panel>
|
||||
|
||||
<!-- Task title -->
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Title}"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<!-- Error message for failed tasks -->
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding ErrorMessage}"
|
||||
FontSize="10"
|
||||
Foreground="IndianRed"
|
||||
Opacity="0.8"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsFailed}"
|
||||
MaxWidth="200"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
<Border Padding="8" CornerRadius="6"
|
||||
Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||
IsVisible="{Binding CurrentItem, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto">
|
||||
<!-- Title and Cancel -->
|
||||
<DockPanel Grid.Row="0">
|
||||
<Button DockPanel.Dock="Right"
|
||||
|
|
@ -96,11 +96,41 @@
|
|||
</StackPanel>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<ProgressBar Grid.Row="2"
|
||||
Value="{Binding CurrentProgress}"
|
||||
<ProgressBar Grid.Row="2"
|
||||
Value="{Binding CurrentProgress}"
|
||||
Minimum="0" Maximum="1"
|
||||
Height="4"
|
||||
Margin="0,4,0,0" />
|
||||
|
||||
<!-- Task list (compact) -->
|
||||
<ItemsControl Grid.Row="3"
|
||||
ItemsSource="{Binding CurrentItem.Tasks}"
|
||||
IsVisible="{Binding CurrentItem.HasTasks}"
|
||||
Margin="0,6,0,0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:InstallTaskItemViewModel">
|
||||
<Grid ColumnDefinitions="14,*" Margin="0,1">
|
||||
<Panel Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Text="✓" Foreground="#4CAF50" FontSize="9"
|
||||
IsVisible="{Binding IsCompleted}" />
|
||||
<TextBlock Text="●" Foreground="#2196F3" FontSize="7"
|
||||
IsVisible="{Binding IsRunning}"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||
<TextBlock Text="✕" Foreground="IndianRed" FontSize="9"
|
||||
IsVisible="{Binding IsFailed}" />
|
||||
<TextBlock Text="―" Opacity="0.3" FontSize="9"
|
||||
IsVisible="{Binding IsQueued}" />
|
||||
</Panel>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Title}"
|
||||
FontSize="10"
|
||||
Opacity="0.7"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
using LANCommander.SDK.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using LANCommander.SDK.Models;
|
||||
|
||||
namespace LANCommander.Launcher.Models
|
||||
{
|
||||
public class DownloadQueueRedistributable : IInstallQueueItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid[] AddonIds { get; set; }
|
||||
public Dictionary<Guid, string?> AddonVersions { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string InstallDirectory { get; set; }
|
||||
|
|
@ -26,12 +20,10 @@ namespace LANCommander.Launcher.Models
|
|||
{
|
||||
switch (Status)
|
||||
{
|
||||
case InstallStatus.Starting:
|
||||
case InstallStatus.Downloading:
|
||||
case InstallStatus.InstallingRedistributables:
|
||||
case InstallStatus.InstallingMods:
|
||||
case InstallStatus.InstallingExpansions:
|
||||
case InstallStatus.RunningScripts:
|
||||
case InstallStatus.DownloadingSaves:
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
|
@ -41,10 +33,18 @@ namespace LANCommander.Launcher.Models
|
|||
}
|
||||
public InstallStatus Status { get; set; }
|
||||
public SDK.Models.Redistributable Redistributable { get; set; }
|
||||
public float Progress { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public double TransferSpeed { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public long BytesDownloaded { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public long TotalBytes { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public InstallPlanItemType ItemType => InstallPlanItemType.Redistributable;
|
||||
public Guid? DependsOnId { get; set; }
|
||||
public List<InstallTaskDefinition> Tasks { get; set; } = new();
|
||||
public Guid? CurrentTaskId { get; set; }
|
||||
public float Progress
|
||||
{
|
||||
get => BytesDownloaded / (float)Math.Max(TotalBytes, 1);
|
||||
set { }
|
||||
}
|
||||
public double TransferSpeed { get; set; }
|
||||
public long BytesDownloaded { get; set; }
|
||||
public long TotalBytes { get; set; }
|
||||
public CancellationTokenSource CancellationToken { get; set; } = new();
|
||||
|
||||
public DownloadQueueRedistributable(SDK.Models.Redistributable redistributable)
|
||||
|
|
@ -52,7 +52,16 @@ namespace LANCommander.Launcher.Models
|
|||
Redistributable = redistributable;
|
||||
Id = redistributable.Id;
|
||||
Title = redistributable.Name;
|
||||
Version = redistributable.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version;
|
||||
Version = redistributable.Archives?.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version;
|
||||
QueuedOn = DateTime.Now;
|
||||
Status = InstallStatus.Queued;
|
||||
}
|
||||
|
||||
public DownloadQueueRedistributable(InstallPlanItem planItem, SDK.Models.Redistributable redistributable) : this(redistributable)
|
||||
{
|
||||
InstallDirectory = planItem.InstallDirectory;
|
||||
DependsOnId = planItem.DependsOnId;
|
||||
Tasks = planItem.Tasks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
using LANCommander.SDK.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using LANCommander.SDK.Models;
|
||||
|
||||
namespace LANCommander.Launcher.Models
|
||||
{
|
||||
|
|
@ -25,5 +21,9 @@ namespace LANCommander.Launcher.Models
|
|||
long BytesDownloaded { get; set; }
|
||||
long TotalBytes { get; set; }
|
||||
CancellationTokenSource CancellationToken { get; set; }
|
||||
InstallPlanItemType ItemType { get; }
|
||||
Guid? DependsOnId { get; set; }
|
||||
List<InstallTaskDefinition> Tasks { get; set; }
|
||||
Guid? CurrentTaskId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
using LANCommander.SDK.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using LANCommander.SDK.Models;
|
||||
|
||||
namespace LANCommander.Launcher.Models
|
||||
{
|
||||
|
|
@ -44,10 +40,17 @@ namespace LANCommander.Launcher.Models
|
|||
}
|
||||
public InstallStatus Status { get; set; }
|
||||
public SDK.Models.Game Game { get; set; }
|
||||
public InstallPlanItemType ItemType => InstallPlanItemType.Game;
|
||||
public Guid? DependsOnId { get; set; }
|
||||
public List<InstallTaskDefinition> Tasks { get; set; } = new();
|
||||
public Guid? CurrentTaskId { get; set; }
|
||||
public float Progress {
|
||||
get
|
||||
{
|
||||
return BytesDownloaded / (float)TotalBytes;
|
||||
if (Tasks != null && Tasks.Count > 0)
|
||||
return BytesDownloaded / (float)Math.Max(TotalBytes, 1);
|
||||
|
||||
return BytesDownloaded / (float)Math.Max(TotalBytes, 1);
|
||||
}
|
||||
set { }
|
||||
}
|
||||
|
|
@ -61,19 +64,26 @@ namespace LANCommander.Launcher.Models
|
|||
Game = game;
|
||||
Id = game.Id;
|
||||
Title = game.Title;
|
||||
Version = game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version;
|
||||
Version = game.Archives?.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version;
|
||||
QueuedOn = DateTime.Now;
|
||||
Status = InstallStatus.Queued;
|
||||
|
||||
var cover = game.Media.FirstOrDefault(m => m.Type == SDK.Enums.MediaType.Cover);
|
||||
var cover = game.Media?.FirstOrDefault(m => m.Type == SDK.Enums.MediaType.Cover);
|
||||
|
||||
if (cover != null)
|
||||
CoverId = cover.Id;
|
||||
|
||||
var icon = game.Media.FirstOrDefault(m => m.Type == SDK.Enums.MediaType.Icon);
|
||||
var icon = game.Media?.FirstOrDefault(m => m.Type == SDK.Enums.MediaType.Icon);
|
||||
|
||||
if (icon != null)
|
||||
IconId = icon.Id;
|
||||
}
|
||||
|
||||
public InstallQueueGame(InstallPlanItem planItem, SDK.Models.Game game) : this(game)
|
||||
{
|
||||
InstallDirectory = planItem.InstallDirectory;
|
||||
DependsOnId = planItem.DependsOnId;
|
||||
Tasks = planItem.Tasks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
using LANCommander.SDK.Enums;
|
||||
using LANCommander.SDK.Models;
|
||||
|
||||
namespace LANCommander.Launcher.Models;
|
||||
|
||||
public class InstallQueueTool : IInstallQueueItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid[] AddonIds { get; set; }
|
||||
public Dictionary<Guid, string?> AddonVersions { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string InstallDirectory { get; set; }
|
||||
|
|
@ -38,14 +37,18 @@ public class InstallQueueTool : IInstallQueueItem
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public InstallStatus Status { get; set; }
|
||||
public SDK.Models.Tool Tool { get; set; }
|
||||
|
||||
public InstallPlanItemType ItemType => InstallPlanItemType.Tool;
|
||||
public Guid? DependsOnId { get; set; }
|
||||
public List<InstallTaskDefinition> Tasks { get; set; } = new();
|
||||
public Guid? CurrentTaskId { get; set; }
|
||||
|
||||
public float Progress {
|
||||
get
|
||||
{
|
||||
return BytesDownloaded / (float)TotalBytes;
|
||||
return BytesDownloaded / (float)Math.Max(TotalBytes, 1);
|
||||
}
|
||||
set { }
|
||||
}
|
||||
|
|
@ -59,8 +62,15 @@ public class InstallQueueTool : IInstallQueueItem
|
|||
Tool = tool;
|
||||
Id = tool.Id;
|
||||
Title = tool.Name;
|
||||
Version = tool.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version ?? "";
|
||||
Version = tool.Archives?.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version ?? "";
|
||||
QueuedOn = DateTime.Now;
|
||||
Status = InstallStatus.Queued;
|
||||
}
|
||||
|
||||
public InstallQueueTool(InstallPlanItem planItem, SDK.Models.Tool tool) : this(tool)
|
||||
{
|
||||
InstallDirectory = planItem.InstallDirectory;
|
||||
DependsOnId = planItem.DependsOnId;
|
||||
Tasks = planItem.Tasks;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using LANCommander.Launcher.Data.Models;
|
||||
using LANCommander.Launcher.Data.Models;
|
||||
using LANCommander.Launcher.Models;
|
||||
using LANCommander.SDK.Enums;
|
||||
using LANCommander.SDK.Exceptions;
|
||||
|
|
@ -6,7 +6,10 @@ using LANCommander.SDK.Extensions;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using LANCommander.SDK.Models;
|
||||
using LANCommander.SDK.Services;
|
||||
using Game = LANCommander.Launcher.Data.Models.Game;
|
||||
using Tool = LANCommander.Launcher.Data.Models.Tool;
|
||||
|
||||
namespace LANCommander.Launcher.Services
|
||||
{
|
||||
|
|
@ -23,11 +26,14 @@ namespace LANCommander.Launcher.Services
|
|||
private Stopwatch Stopwatch { get; set; }
|
||||
|
||||
public ObservableCollection<IInstallQueueItem> Queue { get; set; }
|
||||
|
||||
|
||||
public delegate Task OnProgressHandler(InstallProgress progress);
|
||||
|
||||
public event OnProgressHandler OnProgress;
|
||||
|
||||
public delegate Task OnTaskProgressUpdateHandler(InstallTaskProgress progress);
|
||||
public event OnTaskProgressUpdateHandler OnTaskProgressUpdate;
|
||||
|
||||
public delegate Task OnQueueChangedHandler();
|
||||
public event OnQueueChangedHandler OnQueueChanged;
|
||||
|
||||
|
|
@ -54,7 +60,7 @@ namespace LANCommander.Launcher.Services
|
|||
_redistributableClient = redistributableClient;
|
||||
_toolClient = toolClient;
|
||||
_mediaClient = mediaClient;
|
||||
|
||||
|
||||
Stopwatch = new Stopwatch();
|
||||
|
||||
Queue = new ObservableCollection<IInstallQueueItem>();
|
||||
|
|
@ -62,8 +68,9 @@ namespace LANCommander.Launcher.Services
|
|||
Queue.CollectionChanged += (sender, e) =>
|
||||
{
|
||||
OnQueueChanged?.Invoke();
|
||||
};
|
||||
};
|
||||
|
||||
// Legacy progress forwarding for backward compatibility
|
||||
_gameClient.OnInstallProgressUpdate += (e) =>
|
||||
{
|
||||
OnProgress?.Invoke(e);
|
||||
|
|
@ -74,17 +81,29 @@ namespace LANCommander.Launcher.Services
|
|||
OnProgress?.Invoke(e);
|
||||
};
|
||||
|
||||
// _gameClient.OnArchiveExtractionProgress += Games_OnArchiveExtractionProgress;
|
||||
// _gameClient.OnArchiveEntryExtractionProgress += Games_OnArchiveEntryExtractionProgress;
|
||||
// New task-level progress forwarding
|
||||
_gameClient.OnTaskProgress += OnSdkTaskProgress;
|
||||
_toolClient.OnTaskProgress += OnSdkTaskProgress;
|
||||
}
|
||||
|
||||
private void Games_OnArchiveExtractionProgress(long position, long length, SDK.Models.Game game)
|
||||
private void OnSdkTaskProgress(InstallTaskProgress taskProgress)
|
||||
{
|
||||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
// Update the matching queue item's current task and progress
|
||||
var queueItem = Queue.FirstOrDefault(i => i.Id == taskProgress.QueueItemId);
|
||||
|
||||
private void Games_OnArchiveEntryExtractionProgress(object sender, SDK.ArchiveEntryExtractionProgressArgs e)
|
||||
{
|
||||
if (queueItem != null)
|
||||
{
|
||||
queueItem.CurrentTaskId = taskProgress.TaskId;
|
||||
|
||||
if (taskProgress.TaskStatus == InstallTaskStatus.Running && taskProgress.TotalBytes > 0)
|
||||
{
|
||||
queueItem.BytesDownloaded = taskProgress.BytesTransferred;
|
||||
queueItem.TotalBytes = taskProgress.TotalBytes;
|
||||
queueItem.TransferSpeed = taskProgress.TransferSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
OnTaskProgressUpdate?.Invoke(taskProgress);
|
||||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +118,7 @@ namespace LANCommander.Launcher.Services
|
|||
public async Task Add(Game game, string installDirectory = "", SDK.Models.Game[]? addons = null)
|
||||
{
|
||||
var gameInfo = await _gameClient.GetAsync(game.Id);
|
||||
|
||||
|
||||
// TODO: Throw exception (and gracefully handle) when gameInfo == null
|
||||
// Game probably couldn't be found or deserialized from server
|
||||
|
||||
|
|
@ -116,6 +135,7 @@ namespace LANCommander.Launcher.Services
|
|||
}
|
||||
}
|
||||
|
||||
// Clear completed items for this game
|
||||
try
|
||||
{
|
||||
var gameCompletedQueueItems = Queue.Where(i => i.Status == InstallStatus.Complete && i.Id == game.Id).ToList();
|
||||
|
|
@ -129,49 +149,81 @@ namespace LANCommander.Launcher.Services
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
if (!Queue.Any(i => i.Id == game.Id && i.Status == InstallStatus.Queued))
|
||||
if (Queue.Any(i => i.Id == game.Id && i.Status == InstallStatus.Queued))
|
||||
return;
|
||||
|
||||
// Generate install plan from SDK
|
||||
var addonIds = addons?.Select(x => x.Id).ToArray();
|
||||
var plan = await _gameClient.GenerateInstallPlanAsync(game.Id, installDirectory, addonIds);
|
||||
|
||||
// Add each plan item to the queue
|
||||
foreach (var planItem in plan.Items.OrderBy(i => i.Order))
|
||||
{
|
||||
var queueItem = new InstallQueueGame(gameInfo);
|
||||
// Skip if already queued
|
||||
if (Queue.Any(i => i.Id == planItem.EntityId && i.Status.ValueIsIn(InstallStatus.Queued, InstallStatus.Starting, InstallStatus.Downloading)))
|
||||
continue;
|
||||
|
||||
queueItem.InstallDirectory = installDirectory;
|
||||
IInstallQueueItem queueItem;
|
||||
|
||||
if (addons != null && addons.Length > 0)
|
||||
switch (planItem.Type)
|
||||
{
|
||||
var versions = addons.ToLookup(addon => addon.Id, x => x.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version);
|
||||
var addonIds = addons.Select(x => x.Id).ToArray() ?? [];
|
||||
case InstallPlanItemType.Game:
|
||||
case InstallPlanItemType.Addon:
|
||||
var addonGame = planItem.Type == InstallPlanItemType.Addon
|
||||
? await _gameClient.GetAsync(planItem.EntityId)
|
||||
: gameInfo;
|
||||
queueItem = new InstallQueueGame(planItem, addonGame);
|
||||
|
||||
queueItem.AddonIds = addonIds;
|
||||
queueItem.AddonVersions = addonIds.ToDictionary(x => x, y => versions[y]?.FirstOrDefault());
|
||||
if (addons != null && planItem.Type == InstallPlanItemType.Game)
|
||||
{
|
||||
var gameQueueItem = (InstallQueueGame)queueItem;
|
||||
gameQueueItem.AddonIds = addonIds;
|
||||
gameQueueItem.AddonVersions = addons.ToDictionary(
|
||||
a => a.Id,
|
||||
a => a.Archives?.OrderByDescending(ar => ar.CreatedOn).FirstOrDefault()?.Version);
|
||||
}
|
||||
break;
|
||||
|
||||
case InstallPlanItemType.Redistributable:
|
||||
var redist = gameInfo.Redistributables?.FirstOrDefault(r => r.Id == planItem.EntityId);
|
||||
if (redist == null)
|
||||
continue;
|
||||
queueItem = new DownloadQueueRedistributable(planItem, redist);
|
||||
break;
|
||||
|
||||
case InstallPlanItemType.Tool:
|
||||
var tool = await _toolClient.GetAsync(planItem.EntityId);
|
||||
queueItem = new InstallQueueTool(planItem, tool);
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Queue.Any(i => i.State))
|
||||
Queue.Add(queueItem);
|
||||
else
|
||||
Queue.Add(queueItem);
|
||||
}
|
||||
|
||||
// Start processing if nothing active
|
||||
if (!Queue.Any(i => i.State))
|
||||
{
|
||||
var firstItem = Queue.FirstOrDefault(i => i.Status == InstallStatus.Queued);
|
||||
if (firstItem != null)
|
||||
{
|
||||
Logger?.LogTrace("Download queue is empty, starting the game download immediately");
|
||||
|
||||
queueItem.Status = InstallStatus.Starting;
|
||||
|
||||
Queue.Add(queueItem);
|
||||
|
||||
firstItem.Status = InstallStatus.Starting;
|
||||
await Next();
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
public async Task Add(SDK.Models.Tool tool, string installDirectory = "")
|
||||
{
|
||||
var toolInfo = await _toolClient.GetAsync(tool.Id);
|
||||
|
||||
// TODO: Throw exception (and gracefully handle) when gameInfo == null
|
||||
// Game probably couldn't be found or deserialized from server
|
||||
|
||||
Logger?.LogTrace("Adding game {ToolName} to the queue", toolInfo.Name);
|
||||
Logger?.LogTrace("Adding tool {ToolName} to the queue", toolInfo.Name);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -186,30 +238,34 @@ namespace LANCommander.Launcher.Services
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
if (!Queue.Any(i => i.Id == tool.Id && i.Status == InstallStatus.Queued))
|
||||
if (Queue.Any(i => i.Id == tool.Id && i.Status == InstallStatus.Queued))
|
||||
return;
|
||||
|
||||
// Generate install plan from SDK
|
||||
var plan = await _toolClient.GenerateInstallPlanAsync(toolInfo, installDirectory);
|
||||
|
||||
foreach (var planItem in plan.Items.OrderBy(i => i.Order))
|
||||
{
|
||||
var queueItem = new InstallQueueTool(toolInfo);
|
||||
if (Queue.Any(i => i.Id == planItem.EntityId && i.Status.ValueIsIn(InstallStatus.Queued, InstallStatus.Starting, InstallStatus.Downloading)))
|
||||
continue;
|
||||
|
||||
queueItem.InstallDirectory = installDirectory;
|
||||
var queueItem = new InstallQueueTool(planItem, toolInfo);
|
||||
Queue.Add(queueItem);
|
||||
}
|
||||
|
||||
if (Queue.Any(i => i.State))
|
||||
Queue.Add(queueItem);
|
||||
else
|
||||
if (!Queue.Any(i => i.State))
|
||||
{
|
||||
var firstItem = Queue.FirstOrDefault(i => i.Status == InstallStatus.Queued);
|
||||
if (firstItem != null)
|
||||
{
|
||||
Logger?.LogTrace("Download queue is empty, starting the tool download immediately");
|
||||
|
||||
queueItem.Status = InstallStatus.Starting;
|
||||
|
||||
Queue.Add(queueItem);
|
||||
|
||||
firstItem.Status = InstallStatus.Starting;
|
||||
await Next();
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Remove(Guid id)
|
||||
|
|
@ -218,7 +274,7 @@ namespace LANCommander.Launcher.Services
|
|||
|
||||
if (queueItem != null)
|
||||
{
|
||||
Logger?.LogTrace("Removing the game {GameTitle} from the queue", queueItem.Title);
|
||||
Logger?.LogTrace("Removing the item {Title} from the queue", queueItem.Title);
|
||||
|
||||
Remove(queueItem);
|
||||
}
|
||||
|
|
@ -228,7 +284,7 @@ namespace LANCommander.Launcher.Services
|
|||
{
|
||||
if (queueItem != null)
|
||||
{
|
||||
Logger?.LogTrace("Removing the game {GameTitle} from the queue", queueItem.Title);
|
||||
Logger?.LogTrace("Removing the item {Title} from the queue", queueItem.Title);
|
||||
|
||||
Queue.Remove(queueItem);
|
||||
}
|
||||
|
|
@ -237,28 +293,50 @@ namespace LANCommander.Launcher.Services
|
|||
public async Task CancelInstallAsync(Guid queueItemId)
|
||||
{
|
||||
var queueItem = Queue.FirstOrDefault(i => i.Id == queueItemId);
|
||||
|
||||
|
||||
if (queueItem == null)
|
||||
return;
|
||||
|
||||
await queueItem.CancellationToken.CancelAsync();
|
||||
|
||||
|
||||
queueItem.Status = InstallStatus.Canceled;
|
||||
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
|
||||
Logger?.LogTrace("Canceling install queue item {QueueItem}", queueItem.Title);
|
||||
}
|
||||
|
||||
public async Task Next()
|
||||
{
|
||||
var currentItem = Queue.FirstOrDefault(i => i.Status.ValueIsIn(InstallStatus.Queued, InstallStatus.Starting));
|
||||
var pendingItems = Queue.Where(i => i.Status.ValueIsIn(InstallStatus.Queued, InstallStatus.Starting));
|
||||
|
||||
if (currentItem == null)
|
||||
return;
|
||||
foreach (var candidate in pendingItems)
|
||||
{
|
||||
// Check dependency — skip items whose dependency hasn't completed
|
||||
if (candidate.DependsOnId.HasValue)
|
||||
{
|
||||
var dependency = Queue.FirstOrDefault(i => i.Id == candidate.DependsOnId.Value);
|
||||
|
||||
if (currentItem is InstallQueueGame gameQueueItem)
|
||||
await Next(gameQueueItem);
|
||||
if (dependency != null && dependency.Status != InstallStatus.Complete)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentItem is InstallQueueTool toolQueueItem)
|
||||
await Next(toolQueueItem);
|
||||
// Found an eligible item — process it
|
||||
switch (candidate)
|
||||
{
|
||||
case InstallQueueGame gameQueueItem:
|
||||
await Next(gameQueueItem);
|
||||
return;
|
||||
|
||||
case InstallQueueTool toolQueueItem:
|
||||
await Next(toolQueueItem);
|
||||
return;
|
||||
|
||||
case DownloadQueueRedistributable redistQueueItem:
|
||||
await Next(redistQueueItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Next(InstallQueueGame queueItem)
|
||||
|
|
@ -338,7 +416,7 @@ namespace LANCommander.Launcher.Services
|
|||
{
|
||||
localTool = await _toolService.GetAsync(queueItem.Id);
|
||||
remoteTool = await _toolClient.GetAsync(queueItem.Id);
|
||||
|
||||
|
||||
if (remoteTool == null)
|
||||
{
|
||||
Logger?.LogError("Tool info could not be retrieved from the server");
|
||||
|
|
@ -351,7 +429,7 @@ namespace LANCommander.Launcher.Services
|
|||
if (localTool == null)
|
||||
{
|
||||
Logger?.LogError("Tool does not exist in local database, importing");
|
||||
|
||||
|
||||
await _importService.ImportToolAsync(queueItem.Id);
|
||||
|
||||
await Next(queueItem);
|
||||
|
|
@ -361,7 +439,7 @@ namespace LANCommander.Launcher.Services
|
|||
|
||||
if (localTool.Installed)
|
||||
{
|
||||
// Modify
|
||||
// Modify — currently no-op
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -373,20 +451,40 @@ namespace LANCommander.Launcher.Services
|
|||
}
|
||||
}
|
||||
|
||||
private async Task Next(DownloadQueueRedistributable queueItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
await InstallRedistributable(queueItem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "An error occurred while installing redistributable {Title}", queueItem.Title);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Install(InstallQueueGame currentItem, Game localGame, SDK.Models.Game remoteGame)
|
||||
{
|
||||
using (var operation = Logger.BeginOperation("Installing game {GameTitle} ({GameId})", localGame.Title, localGame.Id))
|
||||
{
|
||||
string installDirectory;
|
||||
|
||||
currentItem.Status = InstallStatus.Downloading;
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
try
|
||||
{
|
||||
var gameFileList = await _gameClient.InstallAsync(remoteGame.Id, currentItem.InstallDirectory, currentItem.AddonIds, cancellationToken: currentItem.CancellationToken.Token);
|
||||
installDirectory = gameFileList.InstallDirectory;
|
||||
UpdateGameState(currentItem, localGame, installDirectory);
|
||||
// Build a plan item from the queue item's tasks
|
||||
var planItem = new InstallPlanItem
|
||||
{
|
||||
EntityId = currentItem.Id,
|
||||
Title = currentItem.Title,
|
||||
Type = currentItem.ItemType,
|
||||
InstallDirectory = currentItem.InstallDirectory,
|
||||
Tasks = currentItem.Tasks,
|
||||
};
|
||||
|
||||
var result = await _gameClient.ExecuteInstallPlanItemAsync(planItem, currentItem.CancellationToken.Token);
|
||||
|
||||
UpdateGameState(currentItem, localGame, result.InstallDirectory);
|
||||
}
|
||||
catch (InstallCanceledException ex)
|
||||
{
|
||||
|
|
@ -397,13 +495,19 @@ namespace LANCommander.Launcher.Services
|
|||
catch (InstallException ex)
|
||||
{
|
||||
Logger?.LogError(ex, "An error occurred during install, removing from queue");
|
||||
Queue.Remove(currentItem);
|
||||
currentItem.Status = InstallStatus.Failed;
|
||||
OnQueueChanged?.Invoke();
|
||||
OnInstallFail?.Invoke(localGame);
|
||||
await Next();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "An unknown error occurred during install, removing from queue");
|
||||
Queue.Remove(currentItem);
|
||||
currentItem.Status = InstallStatus.Failed;
|
||||
OnQueueChanged?.Invoke();
|
||||
OnInstallFail?.Invoke(localGame);
|
||||
await Next();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -433,32 +537,27 @@ namespace LANCommander.Launcher.Services
|
|||
}
|
||||
#endregion
|
||||
|
||||
if (currentItem is InstallQueueGame)
|
||||
currentItem.CompletedOn = DateTime.Now;
|
||||
currentItem.Status = InstallStatus.Complete;
|
||||
currentItem.Progress = 1;
|
||||
currentItem.BytesDownloaded = currentItem.TotalBytes;
|
||||
|
||||
try
|
||||
{
|
||||
currentItem.CompletedOn = DateTime.Now;
|
||||
currentItem.Status = InstallStatus.Complete;
|
||||
currentItem.Progress = 1;
|
||||
currentItem.BytesDownloaded = currentItem.TotalBytes;
|
||||
|
||||
try
|
||||
{
|
||||
await _gameService.UpdateAsync(localGame);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "An unknown error occurred while trying to write changes to the database after install of game {GameTitle} ({GameId})", localGame.Title, localGame.Id);
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
Logger?.LogTrace("Install of game {GameTitle} ({GameId}) complete!", localGame.Title, localGame.Id);
|
||||
|
||||
// ShowCompletedNotification(currentItem);
|
||||
|
||||
OnInstallComplete?.Invoke(localGame);
|
||||
|
||||
operation.Complete();
|
||||
await _gameService.UpdateAsync(localGame);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "An unknown error occurred while trying to write changes to the database after install of game {GameTitle} ({GameId})", localGame.Title, localGame.Id);
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
Logger?.LogTrace("Install of game {GameTitle} ({GameId}) complete!", localGame.Title, localGame.Id);
|
||||
|
||||
OnInstallComplete?.Invoke(localGame);
|
||||
|
||||
operation.Complete();
|
||||
}
|
||||
|
||||
await Next();
|
||||
|
|
@ -473,7 +572,16 @@ namespace LANCommander.Launcher.Services
|
|||
|
||||
try
|
||||
{
|
||||
var result = await _toolClient.InstallAsync(remoteTool, currentItem.InstallDirectory);
|
||||
var planItem = new InstallPlanItem
|
||||
{
|
||||
EntityId = currentItem.Id,
|
||||
Title = currentItem.Title,
|
||||
Type = InstallPlanItemType.Tool,
|
||||
InstallDirectory = currentItem.InstallDirectory,
|
||||
Tasks = currentItem.Tasks,
|
||||
};
|
||||
|
||||
var result = await _toolClient.ExecuteInstallPlanItemAsync(planItem, currentItem.CancellationToken.Token);
|
||||
|
||||
UpdateToolState(currentItem, localTool, result.InstallDirectory);
|
||||
}
|
||||
|
|
@ -509,12 +617,10 @@ namespace LANCommander.Launcher.Services
|
|||
{
|
||||
Logger?.LogError(ex, "An unknown error occurred while trying to write changes to the database after install of tool {ToolName} ({ToolId})", localTool.Name, localTool.Id);
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
Logger?.LogTrace("Install of tool {ToolName} ({ToolId}) complete!", localTool.Name, localTool.Id);
|
||||
|
||||
// OnInstallComplete?.Invoke(localTool);
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
Logger?.LogTrace("Install of tool {ToolName} ({ToolId}) complete!", localTool.Name, localTool.Id);
|
||||
|
||||
operation.Complete();
|
||||
}
|
||||
|
|
@ -522,6 +628,53 @@ namespace LANCommander.Launcher.Services
|
|||
await Next();
|
||||
}
|
||||
|
||||
private async Task InstallRedistributable(DownloadQueueRedistributable currentItem)
|
||||
{
|
||||
currentItem.Status = InstallStatus.Downloading;
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
try
|
||||
{
|
||||
var planItem = new InstallPlanItem
|
||||
{
|
||||
EntityId = currentItem.Id,
|
||||
Title = currentItem.Title,
|
||||
Type = InstallPlanItemType.Redistributable,
|
||||
InstallDirectory = currentItem.InstallDirectory,
|
||||
Tasks = currentItem.Tasks,
|
||||
DependsOnId = currentItem.DependsOnId,
|
||||
};
|
||||
|
||||
await _gameClient.ExecuteInstallPlanItemAsync(planItem, currentItem.CancellationToken.Token);
|
||||
}
|
||||
catch (InstallCanceledException)
|
||||
{
|
||||
Logger?.LogError("Redistributable install canceled");
|
||||
currentItem.Status = InstallStatus.Canceled;
|
||||
OnQueueChanged?.Invoke();
|
||||
await Next();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "Redistributable {Title} failed to install", currentItem.Title);
|
||||
currentItem.Status = InstallStatus.Failed;
|
||||
OnQueueChanged?.Invoke();
|
||||
await Next();
|
||||
return;
|
||||
}
|
||||
|
||||
currentItem.CompletedOn = DateTime.Now;
|
||||
currentItem.Status = InstallStatus.Complete;
|
||||
currentItem.Progress = 1;
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
Logger?.LogTrace("Install of redistributable {Title} complete!", currentItem.Title);
|
||||
|
||||
await Next();
|
||||
}
|
||||
|
||||
private static void UpdateGameState(InstallQueueGame currentItem, Game localGame, string installDirectory)
|
||||
{
|
||||
localGame.InstallDirectory = installDirectory;
|
||||
|
|
@ -550,7 +703,7 @@ namespace LANCommander.Launcher.Services
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void UpdateToolState(InstallQueueTool currentItem, Tool localTool, string installDirectory)
|
||||
{
|
||||
localTool.InstallDirectory = installDirectory;
|
||||
|
|
@ -566,7 +719,7 @@ namespace LANCommander.Launcher.Services
|
|||
currentItem.Status = InstallStatus.Moving;
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
|
||||
var newInstallDirectory = await _gameClient.GetInstallDirectory(remoteGame, currentItem.InstallDirectory);
|
||||
|
||||
newInstallDirectory = await _gameClient.MoveAsync(remoteGame, localGame.InstallDirectory, newInstallDirectory);
|
||||
|
|
@ -582,34 +735,6 @@ namespace LANCommander.Launcher.Services
|
|||
|
||||
operation.Complete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*private void ShowCompletedNotification(IDownloadQueueItem queueItem)
|
||||
{
|
||||
var builder = new ToastContentBuilder();
|
||||
|
||||
if (queueItem.IsUpdate)
|
||||
builder.AddText("Game Updated")
|
||||
.AddText($"{queueItem.Title} has finished updating!");
|
||||
else
|
||||
builder.AddText("Game Installed")
|
||||
.AddText($"{queueItem.Title} has finished installing!");
|
||||
|
||||
builder.AddArgument("gameId", queueItem.Id.ToString())
|
||||
.AddButton(
|
||||
new ToastButton()
|
||||
.SetContent("Play")
|
||||
.AddArgument("action", "play")
|
||||
)
|
||||
.AddButton(
|
||||
new ToastButton()
|
||||
.SetContent("View in Library")
|
||||
.AddArgument("action", "viewInLibrary")
|
||||
);
|
||||
//.Show
|
||||
// .AddAppLogoOverride()
|
||||
//.Show();
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ namespace LANCommander.SDK.Services
|
|||
public delegate void OnInstallProgressUpdateHandler(InstallProgress e);
|
||||
public event OnInstallProgressUpdateHandler OnInstallProgressUpdate;
|
||||
|
||||
public delegate void OnTaskProgressHandler(InstallTaskProgress progress);
|
||||
public event OnTaskProgressHandler OnTaskProgress;
|
||||
|
||||
private const string PlayerAliasFilename = "PlayerAlias";
|
||||
private const string KeyFilename = "Key";
|
||||
|
||||
|
|
@ -669,6 +672,482 @@ namespace LANCommander.SDK.Services
|
|||
return installResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an install plan for a game, producing a list of queue items and their tasks
|
||||
/// without executing anything.
|
||||
/// </summary>
|
||||
public async Task<InstallPlan> GenerateInstallPlanAsync(Guid gameId, string installDirectory, Guid[] addonIds = null)
|
||||
{
|
||||
var plan = new InstallPlan();
|
||||
var game = await GetAsync(gameId);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(installDirectory))
|
||||
installDirectory = settingsProvider.CurrentValue.Games.InstallDirectories.First();
|
||||
|
||||
var destination = await GetInstallDirectory(game, installDirectory);
|
||||
|
||||
// Handle standalone mods — need base game first
|
||||
if (game.Type == GameType.StandaloneMod && game.BaseGameId != Guid.Empty)
|
||||
{
|
||||
var baseGame = await GetAsync(game.BaseGameId);
|
||||
var baseDestination = await GetInstallDirectory(baseGame, installDirectory);
|
||||
|
||||
if (!Directory.Exists(baseDestination))
|
||||
{
|
||||
var basePlan = await GenerateInstallPlanAsync(game.BaseGameId, installDirectory);
|
||||
plan.Items.AddRange(basePlan.Items);
|
||||
}
|
||||
|
||||
destination = baseDestination;
|
||||
}
|
||||
|
||||
// Base game item
|
||||
var gameItem = new InstallPlanItem
|
||||
{
|
||||
EntityId = game.Id,
|
||||
Title = game.Title,
|
||||
Type = InstallPlanItemType.Game,
|
||||
InstallDirectory = destination,
|
||||
Order = plan.Items.Count,
|
||||
};
|
||||
|
||||
int taskOrder = 0;
|
||||
|
||||
gameItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.DownloadAndExtract,
|
||||
Title = $"Download {game.Title}",
|
||||
Order = taskOrder++,
|
||||
TargetId = game.Id,
|
||||
TargetName = game.Title,
|
||||
IsCritical = true,
|
||||
ReportsProgress = true,
|
||||
});
|
||||
|
||||
gameItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.WriteManifest,
|
||||
Title = "Write manifest",
|
||||
Order = taskOrder++,
|
||||
TargetId = game.Id,
|
||||
TargetName = game.Title,
|
||||
IsCritical = true,
|
||||
});
|
||||
|
||||
gameItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.WriteScripts,
|
||||
Title = "Save scripts",
|
||||
Order = taskOrder++,
|
||||
TargetId = game.Id,
|
||||
TargetName = game.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
|
||||
gameItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.DownloadSaves,
|
||||
Title = "Download saves",
|
||||
Order = taskOrder++,
|
||||
TargetId = game.Id,
|
||||
TargetName = game.Title,
|
||||
IsCritical = false,
|
||||
ReportsProgress = true,
|
||||
});
|
||||
|
||||
if (game.Scripts != null && game.Scripts.Any())
|
||||
{
|
||||
gameItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.RunInstallScript,
|
||||
Title = "Run install script",
|
||||
Order = taskOrder++,
|
||||
TargetId = game.Id,
|
||||
TargetName = game.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
|
||||
gameItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.RunKeyChangeScript,
|
||||
Title = "Apply key",
|
||||
Order = taskOrder++,
|
||||
TargetId = game.Id,
|
||||
TargetName = game.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
|
||||
gameItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.RunNameChangeScript,
|
||||
Title = "Apply player name",
|
||||
Order = taskOrder++,
|
||||
TargetId = game.Id,
|
||||
TargetName = game.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
}
|
||||
|
||||
if (game.Media != null)
|
||||
{
|
||||
foreach (var manual in game.Media.Where(m => m.Type == MediaType.Manual))
|
||||
{
|
||||
gameItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.DownloadManual,
|
||||
Title = $"Download manual",
|
||||
Order = taskOrder++,
|
||||
TargetId = manual.Id,
|
||||
TargetName = game.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
plan.Items.Add(gameItem);
|
||||
|
||||
// Redistributable items
|
||||
if (game.Redistributables != null)
|
||||
{
|
||||
foreach (var redist in game.Redistributables)
|
||||
{
|
||||
var redistItem = new InstallPlanItem
|
||||
{
|
||||
EntityId = redist.Id,
|
||||
Title = redist.Name,
|
||||
Type = InstallPlanItemType.Redistributable,
|
||||
InstallDirectory = destination,
|
||||
Order = plan.Items.Count,
|
||||
DependsOnId = game.Id,
|
||||
};
|
||||
|
||||
redistItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.DownloadAndExtract,
|
||||
Title = $"Download {redist.Name}",
|
||||
Order = 0,
|
||||
TargetId = redist.Id,
|
||||
TargetName = redist.Name,
|
||||
IsCritical = true,
|
||||
ReportsProgress = true,
|
||||
Parameters = new Dictionary<string, string>
|
||||
{
|
||||
["ParentGameId"] = game.Id.ToString(),
|
||||
},
|
||||
});
|
||||
|
||||
redistItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.RunRedistributableInstallScript,
|
||||
Title = $"Install {redist.Name}",
|
||||
Order = 1,
|
||||
TargetId = redist.Id,
|
||||
TargetName = redist.Name,
|
||||
IsCritical = false,
|
||||
Parameters = new Dictionary<string, string>
|
||||
{
|
||||
["ParentGameId"] = game.Id.ToString(),
|
||||
},
|
||||
});
|
||||
|
||||
plan.Items.Add(redistItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Addon items
|
||||
if (addonIds != null)
|
||||
{
|
||||
foreach (var addonId in addonIds)
|
||||
{
|
||||
var addon = await GetAsync(addonId);
|
||||
|
||||
var addonItem = new InstallPlanItem
|
||||
{
|
||||
EntityId = addon.Id,
|
||||
Title = addon.Title,
|
||||
Type = InstallPlanItemType.Addon,
|
||||
InstallDirectory = destination,
|
||||
Order = plan.Items.Count,
|
||||
DependsOnId = game.Id,
|
||||
};
|
||||
|
||||
int addonTaskOrder = 0;
|
||||
|
||||
addonItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.DownloadAndExtract,
|
||||
Title = $"Download {addon.Title}",
|
||||
Order = addonTaskOrder++,
|
||||
TargetId = addon.Id,
|
||||
TargetName = addon.Title,
|
||||
IsCritical = true,
|
||||
ReportsProgress = true,
|
||||
});
|
||||
|
||||
addonItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.WriteManifest,
|
||||
Title = "Write manifest",
|
||||
Order = addonTaskOrder++,
|
||||
TargetId = addon.Id,
|
||||
TargetName = addon.Title,
|
||||
IsCritical = true,
|
||||
});
|
||||
|
||||
addonItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.WriteScripts,
|
||||
Title = "Save scripts",
|
||||
Order = addonTaskOrder++,
|
||||
TargetId = addon.Id,
|
||||
TargetName = addon.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
|
||||
if (addon.Scripts != null && addon.Scripts.Any())
|
||||
{
|
||||
addonItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.RunInstallScript,
|
||||
Title = "Run install script",
|
||||
Order = addonTaskOrder++,
|
||||
TargetId = addon.Id,
|
||||
TargetName = addon.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
|
||||
addonItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.RunKeyChangeScript,
|
||||
Title = "Apply key",
|
||||
Order = addonTaskOrder++,
|
||||
TargetId = addon.Id,
|
||||
TargetName = addon.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
|
||||
addonItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.RunNameChangeScript,
|
||||
Title = "Apply player name",
|
||||
Order = addonTaskOrder++,
|
||||
TargetId = addon.Id,
|
||||
TargetName = addon.Title,
|
||||
IsCritical = false,
|
||||
});
|
||||
}
|
||||
|
||||
plan.Items.Add(addonItem);
|
||||
}
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a single install plan item's tasks in order, firing OnTaskProgress events for each.
|
||||
/// </summary>
|
||||
public async Task<InstallResult> ExecuteInstallPlanItemAsync(InstallPlanItem planItem, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var installResult = new InstallResult(planItem.InstallDirectory, planItem.EntityId);
|
||||
|
||||
switch (planItem.Type)
|
||||
{
|
||||
case InstallPlanItemType.Game:
|
||||
case InstallPlanItemType.Addon:
|
||||
await ExecuteGamePlanItemAsync(planItem, installResult, cancellationToken);
|
||||
break;
|
||||
|
||||
case InstallPlanItemType.Redistributable:
|
||||
await ExecuteRedistributablePlanItemAsync(planItem, installResult, cancellationToken);
|
||||
break;
|
||||
|
||||
case InstallPlanItemType.Tool:
|
||||
var toolResult = await toolClient.ExecuteInstallPlanItemAsync(planItem, cancellationToken);
|
||||
installResult.InstallDirectory = toolResult.InstallDirectory;
|
||||
break;
|
||||
}
|
||||
|
||||
return installResult;
|
||||
}
|
||||
|
||||
private async Task ExecuteGamePlanItemAsync(InstallPlanItem planItem, InstallResult installResult, CancellationToken cancellationToken)
|
||||
{
|
||||
var game = await GetAsync(planItem.EntityId);
|
||||
var gameFileList = installResult.FileList;
|
||||
SDK.Models.Manifest.Game manifest = null;
|
||||
|
||||
foreach (var taskDef in planItem.Tasks.OrderBy(t => t.Order))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var taskProgress = new InstallTaskProgress
|
||||
{
|
||||
QueueItemId = planItem.EntityId,
|
||||
TaskId = taskDef.Id,
|
||||
TaskType = taskDef.Type,
|
||||
TaskTitle = taskDef.Title,
|
||||
TaskStatus = InstallTaskStatus.Running,
|
||||
};
|
||||
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
|
||||
try
|
||||
{
|
||||
switch (taskDef.Type)
|
||||
{
|
||||
case InstallTaskType.DownloadAndExtract:
|
||||
var result = await RetryHelper.RetryOnExceptionAsync(10, TimeSpan.FromMilliseconds(500), new ExtractionResult(), async () =>
|
||||
{
|
||||
return await Task.Run(async () => await DownloadAndExtractAsync(game, planItem.InstallDirectory, cancellationToken));
|
||||
});
|
||||
|
||||
if (!result.Success && !result.Canceled)
|
||||
throw new InstallException("Could not extract the installer. Retry the install or check your connection");
|
||||
else if (result.Canceled)
|
||||
throw new InstallCanceledException("Game install was canceled");
|
||||
|
||||
game.InstallDirectory = result.Directory;
|
||||
installResult.InstallDirectory = result.Directory;
|
||||
planItem.InstallDirectory = result.Directory;
|
||||
|
||||
gameFileList.BaseGame.AddFiles(result.Files?
|
||||
.Where(x => !x.EntryPath.EndsWith("/"))
|
||||
.Select(x => new GameInstallationFileListEntry.FileEntry
|
||||
{
|
||||
EntryPath = x.EntryPath,
|
||||
LocalPath = x.LocalPath,
|
||||
}) ?? []);
|
||||
break;
|
||||
|
||||
case InstallTaskType.WriteManifest:
|
||||
manifest = await RetryHelper.RetryOnExceptionAsync(10, TimeSpan.FromSeconds(1), (SDK.Models.Manifest.Game)null, async () =>
|
||||
{
|
||||
return await WriteManifestAsync(planItem.InstallDirectory, game);
|
||||
});
|
||||
|
||||
if (manifest == null)
|
||||
throw new InstallException("Could not grab the manifest file. Retry the install or check your connection");
|
||||
|
||||
gameFileList.BaseGame.Manifest = manifest;
|
||||
break;
|
||||
|
||||
case InstallTaskType.WriteScripts:
|
||||
await WriteScriptsAsync(planItem.InstallDirectory, game);
|
||||
break;
|
||||
|
||||
case InstallTaskType.DownloadSaves:
|
||||
await saveClient.DownloadAsync(planItem.InstallDirectory, game.Id);
|
||||
break;
|
||||
|
||||
case InstallTaskType.RunInstallScript:
|
||||
await scriptClient.Game_RunInstallScriptAsync(planItem.InstallDirectory, game.Id);
|
||||
break;
|
||||
|
||||
case InstallTaskType.RunKeyChangeScript:
|
||||
var allocatedKey = await GetAllocatedKeyAsync(game.Id);
|
||||
await scriptClient.Game_RunKeyChangeScriptAsync(planItem.InstallDirectory, game.Id, allocatedKey);
|
||||
break;
|
||||
|
||||
case InstallTaskType.RunNameChangeScript:
|
||||
var alias = await profileClient.GetAliasAsync();
|
||||
await scriptClient.Game_RunNameChangeScriptAsync(planItem.InstallDirectory, game.Id, alias);
|
||||
break;
|
||||
|
||||
case InstallTaskType.DownloadManual:
|
||||
// Manual download handled by caller (InstallService) since it needs MediaClient
|
||||
break;
|
||||
}
|
||||
|
||||
taskProgress.TaskStatus = InstallTaskStatus.Completed;
|
||||
taskProgress.Progress = 1.0f;
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
}
|
||||
catch (InstallCanceledException)
|
||||
{
|
||||
taskProgress.TaskStatus = InstallTaskStatus.Canceled;
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (!taskDef.IsCritical)
|
||||
{
|
||||
logger?.LogError(ex, "Non-critical task {TaskTitle} failed for {GameTitle} ({GameId})", taskDef.Title, game.Title, game.Id);
|
||||
taskProgress.TaskStatus = InstallTaskStatus.Failed;
|
||||
taskProgress.ErrorMessage = ex.Message;
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteRedistributablePlanItemAsync(InstallPlanItem planItem, InstallResult installResult, CancellationToken cancellationToken)
|
||||
{
|
||||
// RedistributableClient.InstallAsync bundles download + install into one operation.
|
||||
// We fire task progress for both tasks but execute them as one call.
|
||||
var firstTask = planItem.Tasks.OrderBy(t => t.Order).FirstOrDefault();
|
||||
if (firstTask == null)
|
||||
return;
|
||||
|
||||
// Get parent game context from task parameters
|
||||
Guid parentGameId = Guid.Empty;
|
||||
if (firstTask.Parameters.TryGetValue("ParentGameId", out var parentGameIdStr))
|
||||
Guid.TryParse(parentGameIdStr, out parentGameId);
|
||||
|
||||
var taskProgress = new InstallTaskProgress
|
||||
{
|
||||
QueueItemId = planItem.EntityId,
|
||||
TaskId = firstTask.Id,
|
||||
TaskType = firstTask.Type,
|
||||
TaskTitle = firstTask.Title,
|
||||
TaskStatus = InstallTaskStatus.Running,
|
||||
};
|
||||
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var game = parentGameId != Guid.Empty ? await GetAsync(parentGameId) : null;
|
||||
|
||||
if (game != null)
|
||||
{
|
||||
game.InstallDirectory = planItem.InstallDirectory;
|
||||
|
||||
var redist = game.Redistributables?.FirstOrDefault(r => r.Id == planItem.EntityId);
|
||||
|
||||
if (redist != null)
|
||||
await redistributableClient.InstallAsync(redist, game);
|
||||
}
|
||||
|
||||
// Mark all tasks as completed
|
||||
foreach (var taskDef in planItem.Tasks.OrderBy(t => t.Order))
|
||||
{
|
||||
OnTaskProgress?.Invoke(new InstallTaskProgress
|
||||
{
|
||||
QueueItemId = planItem.EntityId,
|
||||
TaskId = taskDef.Id,
|
||||
TaskType = taskDef.Type,
|
||||
TaskTitle = taskDef.Title,
|
||||
TaskStatus = InstallTaskStatus.Completed,
|
||||
Progress = 1.0f,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (InstallCanceledException)
|
||||
{
|
||||
taskProgress.TaskStatus = InstallTaskStatus.Canceled;
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Redistributable {RedistName} failed to install", planItem.Title);
|
||||
taskProgress.TaskStatus = InstallTaskStatus.Failed;
|
||||
taskProgress.ErrorMessage = ex.Message;
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InstallResult> UninstallAsync(string installDirectory, Guid gameId)
|
||||
{
|
||||
var installResult = new InstallResult(installDirectory, gameId);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ using Force.Crc32;
|
|||
using LANCommander.SDK.Abstractions;
|
||||
using LANCommander.SDK.Exceptions;
|
||||
using LANCommander.SDK.Factories;
|
||||
using Action = System.Action;
|
||||
|
||||
namespace LANCommander.SDK.Services
|
||||
{
|
||||
|
|
@ -90,6 +91,117 @@ namespace LANCommander.SDK.Services
|
|||
.StreamAsync();
|
||||
}
|
||||
|
||||
public delegate void OnTaskProgressHandler(InstallTaskProgress progress);
|
||||
public event OnTaskProgressHandler OnTaskProgress;
|
||||
|
||||
public async Task<InstallPlan> GenerateInstallPlanAsync(Tool tool, string installDirectory)
|
||||
{
|
||||
var plan = new InstallPlan();
|
||||
|
||||
var toolItem = new InstallPlanItem
|
||||
{
|
||||
EntityId = tool.Id,
|
||||
Title = tool.Name,
|
||||
Type = InstallPlanItemType.Tool,
|
||||
InstallDirectory = installDirectory,
|
||||
Order = 0,
|
||||
};
|
||||
|
||||
int taskOrder = 0;
|
||||
|
||||
toolItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.DownloadAndExtract,
|
||||
Title = $"Download {tool.Name}",
|
||||
Order = taskOrder++,
|
||||
TargetId = tool.Id,
|
||||
TargetName = tool.Name,
|
||||
IsCritical = true,
|
||||
ReportsProgress = true,
|
||||
});
|
||||
|
||||
if (tool.Scripts != null && tool.Scripts.Any())
|
||||
{
|
||||
toolItem.Tasks.Add(new InstallTaskDefinition
|
||||
{
|
||||
Type = InstallTaskType.RunInstallScript,
|
||||
Title = "Run install script",
|
||||
Order = taskOrder++,
|
||||
TargetId = tool.Id,
|
||||
TargetName = tool.Name,
|
||||
IsCritical = false,
|
||||
});
|
||||
}
|
||||
|
||||
plan.Items.Add(toolItem);
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
public async Task<InstallResult> ExecuteInstallPlanItemAsync(InstallPlanItem planItem, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tool = await GetAsync(planItem.EntityId);
|
||||
var installResult = new InstallResult();
|
||||
|
||||
foreach (var taskDef in planItem.Tasks.OrderBy(t => t.Order))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var taskProgress = new InstallTaskProgress
|
||||
{
|
||||
QueueItemId = planItem.EntityId,
|
||||
TaskId = taskDef.Id,
|
||||
TaskType = taskDef.Type,
|
||||
TaskTitle = taskDef.Title,
|
||||
TaskStatus = InstallTaskStatus.Running,
|
||||
};
|
||||
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
|
||||
try
|
||||
{
|
||||
switch (taskDef.Type)
|
||||
{
|
||||
case InstallTaskType.DownloadAndExtract:
|
||||
var result = await RetryHelper.RetryOnExceptionAsync(10,
|
||||
TimeSpan.FromMilliseconds(500), new ExtractionResult(),
|
||||
async () => await Task.Run(async () => await DownloadAndExtractAsync(tool, planItem.InstallDirectory, cancellationToken)));
|
||||
|
||||
if (!result.Success && !result.Canceled)
|
||||
throw new InstallException("Could not extract the tool. Retry the install or check your connection");
|
||||
else if (result.Canceled)
|
||||
throw new InstallCanceledException("Tool install was canceled");
|
||||
|
||||
installResult.InstallDirectory = result.Directory;
|
||||
break;
|
||||
|
||||
case InstallTaskType.RunInstallScript:
|
||||
await scriptClient.Tool_RunInstallScriptAsync(planItem.InstallDirectory, tool.Id);
|
||||
break;
|
||||
}
|
||||
|
||||
taskProgress.TaskStatus = InstallTaskStatus.Completed;
|
||||
taskProgress.Progress = 1.0f;
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
}
|
||||
catch (InstallCanceledException)
|
||||
{
|
||||
taskProgress.TaskStatus = InstallTaskStatus.Canceled;
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (!taskDef.IsCritical)
|
||||
{
|
||||
logger?.LogError(ex, "Non-critical task {TaskTitle} failed for tool {ToolName}", taskDef.Title, tool.Name);
|
||||
taskProgress.TaskStatus = InstallTaskStatus.Failed;
|
||||
taskProgress.ErrorMessage = ex.Message;
|
||||
OnTaskProgress?.Invoke(taskProgress);
|
||||
}
|
||||
}
|
||||
|
||||
return installResult;
|
||||
}
|
||||
|
||||
public async Task InstallAsync(Game game)
|
||||
{
|
||||
foreach (var tool in game.Tools)
|
||||
|
|
|
|||
10
LANCommander.SDK/Enums/InstallPlanItemType.cs
Normal file
10
LANCommander.SDK/Enums/InstallPlanItemType.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace LANCommander.SDK.Enums
|
||||
{
|
||||
public enum InstallPlanItemType
|
||||
{
|
||||
Game,
|
||||
Addon,
|
||||
Redistributable,
|
||||
Tool
|
||||
}
|
||||
}
|
||||
12
LANCommander.SDK/Enums/InstallTaskStatus.cs
Normal file
12
LANCommander.SDK/Enums/InstallTaskStatus.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace LANCommander.SDK.Enums
|
||||
{
|
||||
public enum InstallTaskStatus
|
||||
{
|
||||
Queued,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
Skipped,
|
||||
Canceled
|
||||
}
|
||||
}
|
||||
17
LANCommander.SDK/Enums/InstallTaskType.cs
Normal file
17
LANCommander.SDK/Enums/InstallTaskType.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace LANCommander.SDK.Enums
|
||||
{
|
||||
public enum InstallTaskType
|
||||
{
|
||||
DownloadAndExtract,
|
||||
WriteManifest,
|
||||
WriteScripts,
|
||||
VerifyFiles,
|
||||
DownloadSaves,
|
||||
RunInstallScript,
|
||||
RunKeyChangeScript,
|
||||
RunNameChangeScript,
|
||||
RunRedistributableInstallScript,
|
||||
DownloadManual,
|
||||
MoveFiles
|
||||
}
|
||||
}
|
||||
9
LANCommander.SDK/Models/InstallPlan.cs
Normal file
9
LANCommander.SDK/Models/InstallPlan.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace LANCommander.SDK.Models
|
||||
{
|
||||
public class InstallPlan
|
||||
{
|
||||
public List<InstallPlanItem> Items { get; set; } = new();
|
||||
}
|
||||
}
|
||||
17
LANCommander.SDK/Models/InstallPlanItem.cs
Normal file
17
LANCommander.SDK/Models/InstallPlanItem.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LANCommander.SDK.Enums;
|
||||
|
||||
namespace LANCommander.SDK.Models
|
||||
{
|
||||
public class InstallPlanItem
|
||||
{
|
||||
public Guid EntityId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public InstallPlanItemType Type { get; set; }
|
||||
public string InstallDirectory { get; set; }
|
||||
public int Order { get; set; }
|
||||
public List<InstallTaskDefinition> Tasks { get; set; } = new();
|
||||
public Guid? DependsOnId { get; set; }
|
||||
}
|
||||
}
|
||||
19
LANCommander.SDK/Models/InstallTaskDefinition.cs
Normal file
19
LANCommander.SDK/Models/InstallTaskDefinition.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LANCommander.SDK.Enums;
|
||||
|
||||
namespace LANCommander.SDK.Models
|
||||
{
|
||||
public class InstallTaskDefinition
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public InstallTaskType Type { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int Order { get; set; }
|
||||
public Guid TargetId { get; set; }
|
||||
public string TargetName { get; set; }
|
||||
public bool IsCritical { get; set; }
|
||||
public bool ReportsProgress { get; set; }
|
||||
public Dictionary<string, string> Parameters { get; set; } = new();
|
||||
}
|
||||
}
|
||||
21
LANCommander.SDK/Models/InstallTaskProgress.cs
Normal file
21
LANCommander.SDK/Models/InstallTaskProgress.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using LANCommander.SDK.Enums;
|
||||
|
||||
namespace LANCommander.SDK.Models
|
||||
{
|
||||
public class InstallTaskProgress
|
||||
{
|
||||
public Guid QueueItemId { get; set; }
|
||||
public Guid TaskId { get; set; }
|
||||
public InstallTaskType TaskType { get; set; }
|
||||
public string TaskTitle { get; set; }
|
||||
public InstallTaskStatus TaskStatus { get; set; }
|
||||
public float Progress { get; set; }
|
||||
public long BytesTransferred { get; set; }
|
||||
public long TotalBytes { get; set; }
|
||||
public long TransferSpeed { get; set; }
|
||||
public TimeSpan TimeRemaining { get; set; }
|
||||
public bool Indeterminate { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue