2026-04-17 22:24:47 -05:00
using LANCommander.Launcher.Data.Models ;
2024-08-04 17:53:19 -05:00
using LANCommander.Launcher.Models ;
2024-08-07 01:14:13 -05:00
using LANCommander.SDK.Enums ;
2024-05-25 15:40:31 -05:00
using LANCommander.SDK.Exceptions ;
2024-09-16 20:49:03 -05:00
using LANCommander.SDK.Extensions ;
2024-09-11 00:24:34 -05:00
using Microsoft.Extensions.Logging ;
2024-05-25 15:40:31 -05:00
using System.Collections.ObjectModel ;
using System.Diagnostics ;
2026-04-17 22:24:47 -05:00
using LANCommander.SDK.Models ;
2025-01-29 22:52:27 -06:00
using LANCommander.SDK.Services ;
2026-04-17 22:24:47 -05:00
using Game = LANCommander . Launcher . Data . Models . Game ;
using Tool = LANCommander . Launcher . Data . Models . Tool ;
2024-05-25 15:40:31 -05:00
2024-08-04 17:53:19 -05:00
namespace LANCommander.Launcher.Services
2024-05-25 15:40:31 -05:00
{
2024-10-26 15:48:47 -05:00
public class InstallService : BaseService
2024-05-25 15:40:31 -05:00
{
2026-01-18 04:40:18 -06:00
private readonly GameService _gameService ;
2026-02-10 18:04:49 -06:00
private readonly ToolService _toolService ;
private readonly ImportService _importService ;
2026-01-18 04:40:18 -06:00
private readonly GameClient _gameClient ;
private readonly RedistributableClient _redistributableClient ;
2026-02-10 18:04:49 -06:00
private readonly ToolClient _toolClient ;
2026-01-18 04:40:18 -06:00
private readonly MediaClient _mediaClient ;
2024-05-25 15:40:31 -05:00
private Stopwatch Stopwatch { get ; set ; }
2024-10-26 15:48:47 -05:00
public ObservableCollection < IInstallQueueItem > Queue { get ; set ; }
2026-04-17 22:24:47 -05:00
2025-01-31 00:34:37 -06:00
public delegate Task OnProgressHandler ( InstallProgress progress ) ;
2025-01-29 22:52:27 -06:00
public event OnProgressHandler OnProgress ;
2024-05-25 15:40:31 -05:00
2026-04-17 22:24:47 -05:00
public delegate Task OnTaskProgressUpdateHandler ( InstallTaskProgress progress ) ;
public event OnTaskProgressUpdateHandler OnTaskProgressUpdate ;
2024-06-02 17:23:36 -05:00
public delegate Task OnQueueChangedHandler ( ) ;
2024-05-25 15:40:31 -05:00
public event OnQueueChangedHandler OnQueueChanged ;
2024-06-24 19:30:43 -05:00
public delegate Task OnInstallCompleteHandler ( Game game ) ;
2024-05-29 02:02:40 -05:00
public event OnInstallCompleteHandler OnInstallComplete ;
2024-06-24 19:30:43 -05:00
public delegate Task OnInstallFailHandler ( Game game ) ;
public event OnInstallFailHandler OnInstallFail ;
2024-10-26 15:48:47 -05:00
public InstallService (
ILogger < InstallService > logger ,
2026-01-18 04:40:18 -06:00
GameService gameService ,
2026-02-10 18:04:49 -06:00
ToolService toolService ,
ImportService importService ,
2026-01-18 04:40:18 -06:00
GameClient gameClient ,
RedistributableClient redistributableClient ,
2026-02-10 18:04:49 -06:00
ToolClient toolClient ,
2026-01-18 04:40:18 -06:00
MediaClient mediaClient ) : base ( logger )
2024-05-25 15:40:31 -05:00
{
2026-01-18 04:40:18 -06:00
_gameService = gameService ;
2026-02-10 18:04:49 -06:00
_toolService = toolService ;
_importService = importService ;
2026-01-18 04:40:18 -06:00
_gameClient = gameClient ;
_redistributableClient = redistributableClient ;
2026-02-10 18:04:49 -06:00
_toolClient = toolClient ;
2026-01-18 04:40:18 -06:00
_mediaClient = mediaClient ;
2026-04-17 22:24:47 -05:00
2024-05-25 15:40:31 -05:00
Stopwatch = new Stopwatch ( ) ;
2024-10-26 15:48:47 -05:00
Queue = new ObservableCollection < IInstallQueueItem > ( ) ;
2024-05-25 15:40:31 -05:00
Queue . CollectionChanged + = ( sender , e ) = >
{
OnQueueChanged ? . Invoke ( ) ;
2026-04-17 22:24:47 -05:00
} ;
2024-05-29 20:01:46 -05:00
2026-04-18 18:55:28 -05:00
// Legacy progress forwarding — also update the service queue item
// so that RefreshQueueAsync reads current values
2026-01-18 04:40:18 -06:00
_gameClient . OnInstallProgressUpdate + = ( e ) = >
2024-08-07 01:14:13 -05:00
{
2026-04-18 18:55:28 -05:00
UpdateQueueItemFromProgress ( e ) ;
2025-01-29 22:52:27 -06:00
OnProgress ? . Invoke ( e ) ;
2024-08-07 01:14:13 -05:00
} ;
2026-01-18 04:40:18 -06:00
_redistributableClient . OnInstallProgressUpdate + = ( e ) = >
2025-02-04 02:34:31 -06:00
{
2026-04-18 18:55:28 -05:00
UpdateQueueItemFromProgress ( e ) ;
2025-02-04 02:34:31 -06:00
OnProgress ? . Invoke ( e ) ;
} ;
2026-04-17 22:24:47 -05:00
// New task-level progress forwarding
_gameClient . OnTaskProgress + = OnSdkTaskProgress ;
_toolClient . OnTaskProgress + = OnSdkTaskProgress ;
2024-05-29 20:01:46 -05:00
}
2026-04-18 18:55:28 -05:00
private void UpdateQueueItemFromProgress ( InstallProgress progress )
{
if ( progress . Game = = null )
return ;
var queueItem = Queue . FirstOrDefault ( i = > i . Id = = progress . Game . Id ) ;
if ( queueItem ! = null )
{
queueItem . BytesDownloaded = progress . BytesTransferred ;
queueItem . TotalBytes = progress . TotalBytes ;
queueItem . TransferSpeed = progress . TransferSpeed ;
}
}
2026-04-17 22:24:47 -05:00
private void OnSdkTaskProgress ( InstallTaskProgress taskProgress )
2024-05-29 20:01:46 -05:00
{
2026-04-17 22:24:47 -05:00
// Update the matching queue item's current task and progress
var queueItem = Queue . FirstOrDefault ( i = > i . Id = = taskProgress . QueueItemId ) ;
2024-05-29 20:01:46 -05:00
2026-04-17 22:24:47 -05:00
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 ) ;
2024-05-29 20:01:46 -05:00
OnQueueChanged ? . Invoke ( ) ;
2024-05-25 15:40:31 -05:00
}
2025-05-15 03:04:48 +02:00
[Obsolete("Use Add(Game, string, Game[] ) instead . ")]
public async Task AddObsolete ( Game game , string installDirectory = "" , Guid [ ] ? addonIds = null )
{
2026-01-18 04:40:18 -06:00
var addons = addonIds ! = null ? await _gameClient . GetAddonsAsync ( game . Id ) : [ ] ;
2025-05-15 03:04:48 +02:00
var selectedAddons = addons ? . Where ( x = > addons . Contains ( x ) ) . ToArray ( ) ;
await Add ( game , installDirectory , selectedAddons ) ;
}
public async Task Add ( Game game , string installDirectory = "" , SDK . Models . Game [ ] ? addons = null )
2024-05-25 15:40:31 -05:00
{
2026-01-18 04:40:18 -06:00
var gameInfo = await _gameClient . GetAsync ( game . Id ) ;
2026-04-17 22:24:47 -05:00
2026-01-18 04:40:18 -06:00
// TODO: Throw exception (and gracefully handle) when gameInfo == null
// Game probably couldn't be found or deserialized from server
2024-05-25 15:40:31 -05:00
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Add: Adding game {GameTitle} ({GameId}) to the queue, installDirectory={InstallDirectory}, addonCount={AddonCount}" ,
gameInfo . Title , gameInfo . Id , installDirectory , addons ? . Length ? ? 0 ) ;
2024-09-16 20:49:03 -05:00
2024-05-25 15:40:31 -05:00
// Check to see if we need to install the base game (this game is probably a mod or expansion)
2024-11-12 02:17:26 -06:00
if ( gameInfo . BaseGameId ! = Guid . Empty )
2024-05-25 15:40:31 -05:00
{
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Add: Game {GameTitle} has BaseGameId={BaseGameId}, checking if base game needs install" , gameInfo . Title , gameInfo . BaseGameId ) ;
2026-01-18 04:40:18 -06:00
var baseGame = await _gameService . GetAsync ( gameInfo . BaseGameId ) ;
2024-05-25 15:40:31 -05:00
if ( baseGame ! = null & & ! baseGame . Installed )
{
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Add: Base game {BaseGameTitle} ({BaseGameId}) is not installed, adding it first" , baseGame . Title , baseGame . Id ) ;
2024-09-12 19:09:13 -05:00
await Add ( baseGame , installDirectory ) ;
2024-05-25 15:40:31 -05:00
}
2026-04-17 23:52:22 -05:00
else
{
Logger ? . LogInformation ( "[InstallQueue] Add: Base game is {Status}" , baseGame = = null ? "not found in local DB" : "already installed" ) ;
}
2024-05-25 15:40:31 -05:00
}
2026-04-17 22:24:47 -05:00
// Clear completed items for this game
2024-10-26 15:48:47 -05:00
try
{
2025-01-31 00:34:37 -06:00
var gameCompletedQueueItems = Queue . Where ( i = > i . Status = = InstallStatus . Complete & & i . Id = = game . Id ) . ToList ( ) ;
2024-10-26 15:48:47 -05:00
foreach ( var queueItem in gameCompletedQueueItems )
{
Queue . Remove ( queueItem ) ;
}
OnQueueChanged ? . Invoke ( ) ;
}
catch ( Exception ex )
{
2026-04-17 23:52:22 -05:00
Logger ? . LogWarning ( ex , "[InstallQueue] Add: Error clearing completed queue items for {GameId}" , game . Id ) ;
2024-10-26 15:48:47 -05:00
}
2026-04-17 22:24:47 -05:00
if ( Queue . Any ( i = > i . Id = = game . Id & & i . Status = = InstallStatus . Queued ) )
2026-04-17 23:52:22 -05:00
{
Logger ? . LogInformation ( "[InstallQueue] Add: Game {GameTitle} ({GameId}) already queued, skipping" , gameInfo . Title , game . Id ) ;
2026-04-17 22:24:47 -05:00
return ;
2026-04-17 23:52:22 -05:00
}
2024-05-25 15:40:31 -05:00
2026-04-17 22:24:47 -05:00
// Generate install plan from SDK
var addonIds = addons ? . Select ( x = > x . Id ) . ToArray ( ) ;
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Add: Generating install plan for {GameTitle} ({GameId}) with {AddonCount} addons" ,
gameInfo . Title , game . Id , addonIds ? . Length ? ? 0 ) ;
2026-04-17 22:24:47 -05:00
var plan = await _gameClient . GenerateInstallPlanAsync ( game . Id , installDirectory , addonIds ) ;
2024-09-12 19:09:13 -05:00
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Add: Plan generated with {ItemCount} items: {Items}" ,
plan . Items . Count ,
string . Join ( ", " , plan . Items . OrderBy ( i = > i . Order ) . Select ( i = > $"[{i.Order}] {i.Type}:{i.Title} (id={i.EntityId}, depends={i.DependsOnId})" ) ) ) ;
2026-04-17 22:24:47 -05:00
// Add each plan item to the queue
foreach ( var planItem in plan . Items . OrderBy ( i = > i . Order ) )
{
// Skip if already queued
if ( Queue . Any ( i = > i . Id = = planItem . EntityId & & i . Status . ValueIsIn ( InstallStatus . Queued , InstallStatus . Starting , InstallStatus . Downloading ) ) )
2026-04-17 23:52:22 -05:00
{
Logger ? . LogInformation ( "[InstallQueue] Add: Skipping plan item {Title} ({EntityId}), already in queue" , planItem . Title , planItem . EntityId ) ;
2026-04-17 22:24:47 -05:00
continue ;
2026-04-17 23:52:22 -05:00
}
2025-05-15 03:04:48 +02:00
2026-04-17 22:24:47 -05:00
IInstallQueueItem queueItem ;
2024-10-09 19:23:42 -05:00
2026-04-17 22:24:47 -05:00
switch ( planItem . Type )
2024-05-25 15:40:31 -05:00
{
2026-04-17 22:24:47 -05:00
case InstallPlanItemType . Game :
case InstallPlanItemType . Addon :
var addonGame = planItem . Type = = InstallPlanItemType . Addon
? await _gameClient . GetAsync ( planItem . EntityId )
: gameInfo ;
queueItem = new InstallQueueGame ( planItem , addonGame ) ;
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 )
2026-04-17 23:52:22 -05:00
{
Logger ? . LogInformation ( "[InstallQueue] Add: Redistributable {EntityId} not found in game redistributables, skipping" , planItem . EntityId ) ;
2026-04-17 22:24:47 -05:00
continue ;
2026-04-17 23:52:22 -05:00
}
2026-04-17 22:24:47 -05:00
queueItem = new DownloadQueueRedistributable ( planItem , redist ) ;
break ;
case InstallPlanItemType . Tool :
var tool = await _toolClient . GetAsync ( planItem . EntityId ) ;
queueItem = new InstallQueueTool ( planItem , tool ) ;
break ;
default :
continue ;
}
2024-05-25 15:40:31 -05:00
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Add: Enqueuing {Type} {Title} ({Id}), dependsOn={DependsOn}, taskCount={TaskCount}" ,
queueItem . ItemType , queueItem . Title , queueItem . Id , queueItem . DependsOnId , queueItem . Tasks ? . Count ? ? 0 ) ;
2026-04-17 22:24:47 -05:00
Queue . Add ( queueItem ) ;
}
2024-05-25 15:40:31 -05:00
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Add: Queue now has {Count} items: {Items}" ,
Queue . Count ,
string . Join ( ", " , Queue . Select ( i = > $"{i.Title}({i.Status}, depends={i.DependsOnId})" ) ) ) ;
2026-04-17 22:24:47 -05:00
// Start processing if nothing active
if ( ! Queue . Any ( i = > i . State ) )
{
var firstItem = Queue . FirstOrDefault ( i = > i . Status = = InstallStatus . Queued ) ;
if ( firstItem ! = null )
{
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Add: No active items, starting first queued item: {Title} ({Id})" , firstItem . Title , firstItem . Id ) ;
2026-04-17 22:24:47 -05:00
firstItem . Status = InstallStatus . Starting ;
2024-10-26 15:48:47 -05:00
await Next ( ) ;
2024-05-25 15:40:31 -05:00
}
2026-04-17 23:52:22 -05:00
else
{
Logger ? . LogInformation ( "[InstallQueue] Add: No active items and no queued items to start" ) ;
}
}
else
{
Logger ? . LogInformation ( "[InstallQueue] Add: Queue already has active items, not auto-starting" ) ;
2024-05-25 15:40:31 -05:00
}
2026-04-17 22:24:47 -05:00
OnQueueChanged ? . Invoke ( ) ;
2024-05-25 15:40:31 -05:00
}
2026-04-17 22:24:47 -05:00
2026-02-10 18:04:49 -06:00
public async Task Add ( SDK . Models . Tool tool , string installDirectory = "" )
{
var toolInfo = await _toolClient . GetAsync ( tool . Id ) ;
2026-04-17 22:24:47 -05:00
Logger ? . LogTrace ( "Adding tool {ToolName} to the queue" , toolInfo . Name ) ;
2026-02-10 18:04:49 -06:00
try
{
var toolCompletedQueueItems = Queue . Where ( i = > i . Status = = InstallStatus . Complete & & i . Id = = tool . Id ) . ToList ( ) ;
foreach ( var queueItem in toolCompletedQueueItems )
{
Queue . Remove ( queueItem ) ;
}
OnQueueChanged ? . Invoke ( ) ;
}
catch ( Exception ex )
{
}
2026-04-17 22:24:47 -05:00
if ( Queue . Any ( i = > i . Id = = tool . Id & & i . Status = = InstallStatus . Queued ) )
return ;
2026-02-10 18:04:49 -06:00
2026-04-17 22:24:47 -05:00
// Generate install plan from SDK
var plan = await _toolClient . GenerateInstallPlanAsync ( toolInfo , installDirectory ) ;
2026-02-10 18:04:49 -06:00
2026-04-17 22:24:47 -05:00
foreach ( var planItem in plan . Items . OrderBy ( i = > i . Order ) )
{
if ( Queue . Any ( i = > i . Id = = planItem . EntityId & & i . Status . ValueIsIn ( InstallStatus . Queued , InstallStatus . Starting , InstallStatus . Downloading ) ) )
continue ;
2026-02-10 18:04:49 -06:00
2026-04-17 22:24:47 -05:00
var queueItem = new InstallQueueTool ( planItem , toolInfo ) ;
Queue . Add ( queueItem ) ;
}
2026-02-10 18:04:49 -06:00
2026-04-17 22:24:47 -05:00
if ( ! Queue . Any ( i = > i . State ) )
{
var firstItem = Queue . FirstOrDefault ( i = > i . Status = = InstallStatus . Queued ) ;
if ( firstItem ! = null )
{
firstItem . Status = InstallStatus . Starting ;
2026-02-10 18:04:49 -06:00
await Next ( ) ;
}
}
2026-04-17 22:24:47 -05:00
OnQueueChanged ? . Invoke ( ) ;
2026-02-10 18:04:49 -06:00
}
2024-05-25 15:40:31 -05:00
2024-06-09 01:03:29 -05:00
public void Remove ( Guid id )
{
var queueItem = Queue . FirstOrDefault ( i = > i . Id = = id ) ;
2024-09-16 20:49:03 -05:00
if ( queueItem ! = null )
{
2026-04-17 22:24:47 -05:00
Logger ? . LogTrace ( "Removing the item {Title} from the queue" , queueItem . Title ) ;
2024-09-16 20:49:03 -05:00
Remove ( queueItem ) ;
}
2024-06-09 01:03:29 -05:00
}
2024-10-26 15:48:47 -05:00
public void Remove ( IInstallQueueItem queueItem )
2024-06-09 01:03:29 -05:00
{
if ( queueItem ! = null )
2024-09-16 20:49:03 -05:00
{
2026-04-17 22:24:47 -05:00
Logger ? . LogTrace ( "Removing the item {Title} from the queue" , queueItem . Title ) ;
2024-09-16 20:49:03 -05:00
2024-06-09 01:03:29 -05:00
Queue . Remove ( queueItem ) ;
2024-09-16 20:49:03 -05:00
}
2024-06-09 01:03:29 -05:00
}
2025-10-15 21:26:40 -05:00
public async Task CancelInstallAsync ( Guid queueItemId )
2024-05-25 15:40:31 -05:00
{
2025-10-15 21:26:40 -05:00
var queueItem = Queue . FirstOrDefault ( i = > i . Id = = queueItemId ) ;
2026-04-17 22:24:47 -05:00
if ( queueItem = = null )
return ;
2025-10-15 21:26:40 -05:00
await queueItem . CancellationToken . CancelAsync ( ) ;
2026-04-17 22:24:47 -05:00
2025-10-15 21:26:40 -05:00
queueItem . Status = InstallStatus . Canceled ;
2026-04-17 22:24:47 -05:00
2025-10-15 21:26:40 -05:00
OnQueueChanged ? . Invoke ( ) ;
2026-04-17 22:24:47 -05:00
2025-10-15 21:26:40 -05:00
Logger ? . LogTrace ( "Canceling install queue item {QueueItem}" , queueItem . Title ) ;
2024-05-25 15:40:31 -05:00
}
2024-10-26 15:48:47 -05:00
public async Task Next ( )
2024-05-25 15:40:31 -05:00
{
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Next: Evaluating queue. Total items: {Count}, statuses: {Statuses}" ,
Queue . Count ,
string . Join ( ", " , Queue . Select ( i = > $"{i.Title}({i.Status}, type={i.ItemType}, depends={i.DependsOnId})" ) ) ) ;
var pendingItems = Queue . Where ( i = > i . Status . ValueIsIn ( InstallStatus . Queued , InstallStatus . Starting ) ) . ToList ( ) ;
Logger ? . LogInformation ( "[InstallQueue] Next: Found {Count} pending items" , pendingItems . Count ) ;
2024-05-25 15:40:31 -05:00
2026-04-17 22:24:47 -05:00
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 ( dependency ! = null & & dependency . Status ! = InstallStatus . Complete )
2026-04-17 23:52:22 -05:00
{
Logger ? . LogInformation ( "[InstallQueue] Next: Skipping {Title} ({Id}) — dependency {DepTitle} ({DepId}) is {DepStatus}" ,
candidate . Title , candidate . Id , dependency . Title , dependency . Id , dependency . Status ) ;
2026-04-17 22:24:47 -05:00
continue ;
2026-04-17 23:52:22 -05:00
}
if ( dependency = = null )
{
Logger ? . LogInformation ( "[InstallQueue] Next: {Title} ({Id}) depends on {DependsOnId} but dependency not in queue (assumed complete)" ,
candidate . Title , candidate . Id , candidate . DependsOnId . Value ) ;
}
2026-04-17 22:24:47 -05:00
}
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Next: Processing eligible item: {Title} ({Id}), type={Type}, clrType={ClrType}" ,
candidate . Title , candidate . Id , candidate . ItemType , candidate . GetType ( ) . Name ) ;
2026-04-17 22:24:47 -05:00
// Found an eligible item — process it
switch ( candidate )
{
case InstallQueueGame gameQueueItem :
await Next ( gameQueueItem ) ;
return ;
2024-05-25 15:40:31 -05:00
2026-04-17 22:24:47 -05:00
case InstallQueueTool toolQueueItem :
await Next ( toolQueueItem ) ;
return ;
2026-02-10 18:04:49 -06:00
2026-04-17 22:24:47 -05:00
case DownloadQueueRedistributable redistQueueItem :
await Next ( redistQueueItem ) ;
return ;
}
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Next: Item {Title} ({Id}) did not match any known type: {ClrType}" , candidate . Title , candidate . Id , candidate . GetType ( ) . Name ) ;
2026-04-17 22:24:47 -05:00
}
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Next: No eligible items found to process" ) ;
2026-02-10 18:04:49 -06:00
}
private async Task Next ( InstallQueueGame queueItem )
{
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Next(Game): Processing game queue item {Title} ({Id}), itemType={ItemType}, installDir={InstallDir}, taskCount={TaskCount}" ,
queueItem . Title , queueItem . Id , queueItem . ItemType , queueItem . InstallDirectory , queueItem . Tasks ? . Count ? ? 0 ) ;
2024-10-26 15:48:47 -05:00
Game localGame = null ;
SDK . Models . Game remoteGame = null ;
2024-05-25 15:40:31 -05:00
try
{
2026-02-10 18:04:49 -06:00
localGame = await _gameService . GetAsync ( queueItem . Id ) ;
remoteGame = await _gameClient . GetAsync ( queueItem . Id ) ;
2024-09-16 20:49:03 -05:00
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Next(Game): localGame={LocalFound}, remoteGame={RemoteFound}, localInstalled={Installed}" ,
localGame ! = null , remoteGame ! = null , localGame ? . Installed ) ;
2024-10-26 15:48:47 -05:00
if ( localGame = = null )
2024-09-16 20:49:03 -05:00
{
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Next(Game): Game {Id} does not exist in local database, skipping" , queueItem . Id ) ;
2026-02-10 18:04:49 -06:00
Remove ( queueItem ) ;
2024-10-26 15:48:47 -05:00
OnQueueChanged ? . Invoke ( ) ;
2024-09-16 20:49:03 -05:00
return ;
}
2024-10-26 15:48:47 -05:00
if ( remoteGame = = null )
2024-09-16 20:49:03 -05:00
{
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Next(Game): Game {Id} info could not be retrieved from the server" , queueItem . Id ) ;
2024-10-26 15:48:47 -05:00
2026-02-10 18:04:49 -06:00
queueItem . Status = InstallStatus . Failed ;
2024-10-26 15:48:47 -05:00
OnQueueChanged ? . Invoke ( ) ;
2024-09-16 20:49:03 -05:00
return ;
}
2024-10-26 15:48:47 -05:00
if ( localGame . Installed )
{
2025-05-17 02:02:57 +02:00
// update current local installed game first, might be moved afterwards
2026-01-18 04:40:18 -06:00
await _gameClient . UpdateGameInstallationAsync ( localGame . InstallDirectory , remoteGame ) ;
2025-05-17 02:02:57 +02:00
2024-10-26 15:48:47 -05:00
// Probably doing a modification of some sort
2026-02-10 18:04:49 -06:00
if ( localGame . InstallDirectory . StartsWith ( queueItem . InstallDirectory ) )
2025-05-15 03:04:48 +02:00
{
2025-05-16 01:04:03 +02:00
var allAddons = remoteGame . DependentGames . ToArray ( ) ;
2026-02-10 18:04:49 -06:00
var removeAddons = allAddons . Except ( queueItem . AddonIds ? ? [ ] ) . ToArray ( ) ;
var addAddons = allAddons . Intersect ( queueItem . AddonIds ? ? [ ] ) . ToArray ( ) ;
2025-05-16 01:04:03 +02:00
2026-01-18 04:40:18 -06:00
var uninstallResult = await _gameClient . UninstallAddonsAsync ( localGame . InstallDirectory , localGame . Id , removeAddons ) ;
var installResult = await _gameClient . InstallAddonsAsync ( localGame . InstallDirectory , localGame . Id , addAddons ) ;
await _gameClient . RestoreFilesAsync ( localGame . InstallDirectory , localGame . Id , uninstallResult . FileList , installResult . FileList ) ;
2025-05-18 08:31:56 +02:00
2026-02-10 18:04:49 -06:00
UpdateGameState ( queueItem , localGame , localGame . InstallDirectory ) ;
2026-01-18 04:40:18 -06:00
await _gameService . UpdateAsync ( localGame ) ;
2026-04-08 18:07:32 -05:00
2026-02-10 18:04:49 -06:00
queueItem . Status = InstallStatus . Complete ;
2025-05-15 03:04:48 +02:00
OnQueueChanged ? . Invoke ( ) ;
OnInstallComplete ? . Invoke ( localGame ) ;
}
2024-10-26 15:48:47 -05:00
else
{
2026-02-10 18:04:49 -06:00
await Move ( queueItem , localGame , remoteGame ) ;
2024-10-26 15:48:47 -05:00
}
2026-04-08 18:07:32 -05:00
await Next ( ) ;
2024-10-26 15:48:47 -05:00
}
else
{
2026-02-10 18:04:49 -06:00
await Install ( queueItem , localGame , remoteGame ) ;
2024-10-26 15:48:47 -05:00
}
2024-05-25 15:40:31 -05:00
}
catch ( Exception ex )
{
2024-10-26 15:48:47 -05:00
Logger ? . LogError ( ex , "An unknown error occured while trying to retrieve game info from the server" ) ;
2024-05-25 15:40:31 -05:00
}
2024-10-26 15:48:47 -05:00
}
2024-05-25 15:40:31 -05:00
2026-02-10 18:04:49 -06:00
private async Task Next ( InstallQueueTool queueItem )
{
Tool localTool = null ;
SDK . Models . Tool remoteTool = null ;
try
{
localTool = await _toolService . GetAsync ( queueItem . Id ) ;
remoteTool = await _toolClient . GetAsync ( queueItem . Id ) ;
2026-04-17 22:24:47 -05:00
2026-02-10 18:04:49 -06:00
if ( remoteTool = = null )
{
Logger ? . LogError ( "Tool info could not be retrieved from the server" ) ;
queueItem . Status = InstallStatus . Failed ;
OnQueueChanged ? . Invoke ( ) ;
return ;
}
if ( localTool = = null )
{
Logger ? . LogError ( "Tool does not exist in local database, importing" ) ;
2026-04-17 22:24:47 -05:00
2026-02-10 18:04:49 -06:00
await _importService . ImportToolAsync ( queueItem . Id ) ;
await Next ( queueItem ) ;
return ;
}
if ( localTool . Installed )
{
2026-04-17 22:24:47 -05:00
// Modify — currently no-op
2026-02-10 18:04:49 -06:00
}
else
{
await Install ( queueItem , localTool , remoteTool ) ;
}
}
catch
{
}
}
2026-04-17 22:24:47 -05:00
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 ) ;
}
}
2026-02-10 18:04:49 -06:00
public async Task Install ( InstallQueueGame currentItem , Game localGame , SDK . Models . Game remoteGame )
2024-10-26 15:48:47 -05:00
{
using ( var operation = Logger . BeginOperation ( "Installing game {GameTitle} ({GameId})" , localGame . Title , localGame . Id ) )
2024-06-20 17:14:59 -05:00
{
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Install(Game): Starting install of {Title} ({Id}), itemType={ItemType}, installDir={InstallDir}, taskCount={TaskCount}, tasks={Tasks}" ,
currentItem . Title , currentItem . Id , currentItem . ItemType , currentItem . InstallDirectory ,
currentItem . Tasks ? . Count ? ? 0 ,
string . Join ( ", " , ( currentItem . Tasks ? ? [ ] ) . Select ( t = > $"{t.Type}:{t.Title}" ) ) ) ;
2025-01-31 00:34:37 -06:00
currentItem . Status = InstallStatus . Downloading ;
2024-10-26 15:48:47 -05:00
OnQueueChanged ? . Invoke ( ) ;
2024-09-16 20:49:03 -05:00
try
2024-06-20 17:14:59 -05:00
{
2026-04-17 22:24:47 -05:00
// 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 ,
} ;
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Install(Game): Executing plan item with {TaskCount} tasks, type={Type}" , planItem . Tasks ? . Count ? ? 0 , planItem . Type ) ;
2026-04-17 22:24:47 -05:00
var result = await _gameClient . ExecuteInstallPlanItemAsync ( planItem , currentItem . CancellationToken . Token ) ;
2026-04-17 23:52:22 -05:00
Logger ? . LogInformation ( "[InstallQueue] Install(Game): ExecuteInstallPlanItemAsync completed, installDir={InstallDir}" , result . InstallDirectory ) ;
2026-04-17 22:24:47 -05:00
UpdateGameState ( currentItem , localGame , result . InstallDirectory ) ;
2024-09-16 20:49:03 -05:00
}
catch ( InstallCanceledException ex )
{
Logger ? . LogError ( "Install canceled, removing from queue" ) ;
Queue . Remove ( currentItem ) ;
return ;
}
catch ( InstallException ex )
{
Logger ? . LogError ( ex , "An error occurred during install, removing from queue" ) ;
2026-04-17 22:24:47 -05:00
currentItem . Status = InstallStatus . Failed ;
OnQueueChanged ? . Invoke ( ) ;
OnInstallFail ? . Invoke ( localGame ) ;
await Next ( ) ;
2024-09-16 20:49:03 -05:00
return ;
}
catch ( Exception ex )
{
Logger ? . LogError ( ex , "An unknown error occurred during install, removing from queue" ) ;
2026-04-17 22:24:47 -05:00
currentItem . Status = InstallStatus . Failed ;
OnQueueChanged ? . Invoke ( ) ;
OnInstallFail ? . Invoke ( localGame ) ;
await Next ( ) ;
2024-09-16 20:49:03 -05:00
return ;
}
2024-06-20 17:14:59 -05:00
2024-09-16 20:49:03 -05:00
#region Download Manuals
try
{
2024-10-26 15:48:47 -05:00
foreach ( var manual in remoteGame . Media . Where ( m = > m . Type = = SDK . Enums . MediaType . Manual ) )
2024-06-20 17:14:59 -05:00
{
2026-01-18 04:40:18 -06:00
var localPath = Path . Combine ( _mediaClient . GetLocalPath ( manual ) , $"{manual.FileId}-{manual.Crc32}" ) ;
2024-09-16 20:49:03 -05:00
if ( ! File . Exists ( localPath ) )
{
2026-01-18 04:40:18 -06:00
foreach ( var staleFile in _mediaClient . GetStaleLocalPaths ( manual ) )
2024-09-16 20:49:03 -05:00
File . Delete ( staleFile ) ;
2026-01-18 04:40:18 -06:00
await _mediaClient . DownloadAsync ( new SDK . Models . Media
2024-09-16 20:49:03 -05:00
{
Id = manual . Id ,
FileId = manual . FileId
} , localPath ) ;
}
}
2024-06-20 17:14:59 -05:00
}
2024-09-16 20:49:03 -05:00
catch ( Exception ex )
{
2024-10-26 15:48:47 -05:00
Logger ? . LogError ( ex , "An unknown error occurred while trying to download game manuals for game {GameTitle} ({GameId})" , localGame . Title , localGame . Id ) ;
2024-09-16 20:49:03 -05:00
}
#endregion
2024-06-20 17:14:59 -05:00
2026-04-17 22:24:47 -05:00
currentItem . CompletedOn = DateTime . Now ;
currentItem . Status = InstallStatus . Complete ;
currentItem . Progress = 1 ;
currentItem . BytesDownloaded = currentItem . TotalBytes ;
2024-05-29 02:02:40 -05:00
2026-04-17 22:24:47 -05:00
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 ) ;
}
2024-08-11 12:31:30 -05:00
2026-04-17 22:24:47 -05:00
OnQueueChanged ? . Invoke ( ) ;
2024-06-12 23:20:31 -05:00
2026-04-17 22:24:47 -05:00
Logger ? . LogTrace ( "Install of game {GameTitle} ({GameId}) complete!" , localGame . Title , localGame . Id ) ;
2024-06-01 22:47:22 -05:00
2026-04-17 22:24:47 -05:00
OnInstallComplete ? . Invoke ( localGame ) ;
2024-09-16 20:52:30 -05:00
2026-04-17 22:24:47 -05:00
operation . Complete ( ) ;
2024-05-25 15:40:31 -05:00
}
2024-05-29 19:25:41 -05:00
2024-10-26 15:48:47 -05:00
await Next ( ) ;
}
2025-05-15 03:04:48 +02:00
2026-02-10 18:04:49 -06:00
public async Task Install ( InstallQueueTool currentItem , Tool localTool , SDK . Models . Tool remoteTool )
{
using ( var operation = Logger . BeginOperation ( "Installing tool {ToolName} ({ToolId})" , localTool . Name , localTool . Id ) )
{
currentItem . Status = InstallStatus . Downloading ;
OnQueueChanged ? . Invoke ( ) ;
try
{
2026-04-17 22:24:47 -05:00
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 ) ;
2026-02-10 18:04:49 -06:00
UpdateToolState ( currentItem , localTool , result . InstallDirectory ) ;
}
catch ( InstallCanceledException ex )
{
Logger ? . LogError ( "Install canceled, removing from queue" ) ;
Queue . Remove ( currentItem ) ;
return ;
}
catch ( InstallException ex )
{
Logger ? . LogError ( ex , "An error occurred during install, removing from queue" ) ;
Queue . Remove ( currentItem ) ;
return ;
}
catch ( Exception ex )
{
Logger . LogError ( ex , "An unknown error occurred during install, removing from queue" ) ;
Queue . Remove ( currentItem ) ;
return ;
}
currentItem . CompletedOn = DateTime . Now ;
currentItem . Status = InstallStatus . Complete ;
currentItem . Progress = 1 ;
currentItem . BytesDownloaded = currentItem . TotalBytes ;
try
{
await _toolService . UpdateAsync ( localTool ) ;
}
catch ( Exception ex )
{
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 ) ;
}
2026-04-17 22:24:47 -05:00
2026-02-10 18:04:49 -06:00
OnQueueChanged ? . Invoke ( ) ;
2026-04-17 22:24:47 -05:00
Logger ? . LogTrace ( "Install of tool {ToolName} ({ToolId}) complete!" , localTool . Name , localTool . Id ) ;
2026-02-10 18:04:49 -06:00
operation . Complete ( ) ;
}
await Next ( ) ;
}
2026-04-17 22:24:47 -05:00
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 ( ) ;
}
2026-02-10 18:04:49 -06:00
private static void UpdateGameState ( InstallQueueGame currentItem , Game localGame , string installDirectory )
2025-05-15 03:04:48 +02:00
{
localGame . InstallDirectory = installDirectory ;
localGame . Installed = true ;
localGame . InstalledVersion = currentItem . Version ;
localGame . InstalledOn ? ? = DateTime . Now ;
2025-05-16 01:04:03 +02:00
foreach ( var localAddon in ( localGame . DependentGames ? ? [ ] ) )
2025-05-15 03:04:48 +02:00
{
2025-05-16 01:04:03 +02:00
bool isInstalled = currentItem . AddonIds ? . Contains ( localAddon . Id ) ? ? false ;
2025-05-15 03:04:48 +02:00
2025-05-16 01:04:03 +02:00
if ( isInstalled )
{
localAddon . InstallDirectory = installDirectory ;
localAddon . Installed = true ;
localAddon . InstalledVersion = ( currentItem . AddonVersions ? ? [ ] ) . TryGetValue ( localAddon . Id , out var addonVersion ) ? addonVersion : null ;
localAddon . InstalledOn ? ? = DateTime . Now ;
}
else
{
localAddon . InstallDirectory = null ;
localAddon . Installed = false ;
localAddon . InstalledVersion = null ;
localAddon . InstalledOn = null ;
}
2025-05-15 03:04:48 +02:00
}
}
2026-04-17 22:24:47 -05:00
2026-02-10 18:04:49 -06:00
private static void UpdateToolState ( InstallQueueTool currentItem , Tool localTool , string installDirectory )
{
localTool . InstallDirectory = installDirectory ;
localTool . Installed = true ;
localTool . InstalledVersion = currentItem . Version ;
localTool . InstalledOn ? ? = DateTime . Now ;
}
2024-10-26 15:48:47 -05:00
public async Task Move ( IInstallQueueItem currentItem , Game localGame , SDK . Models . Game remoteGame )
{
using ( var operation = Logger . BeginOperation ( "Moving game {GameTitle} ({GameId}) to {Destination}" , localGame . Title , localGame . Id , currentItem . InstallDirectory ) )
{
2025-02-16 15:42:00 -06:00
currentItem . Status = InstallStatus . Moving ;
OnQueueChanged ? . Invoke ( ) ;
2026-04-17 22:24:47 -05:00
2026-01-18 04:40:18 -06:00
var newInstallDirectory = await _gameClient . GetInstallDirectory ( remoteGame , currentItem . InstallDirectory ) ;
2024-10-26 15:48:47 -05:00
2026-01-18 04:40:18 -06:00
newInstallDirectory = await _gameClient . MoveAsync ( remoteGame , localGame . InstallDirectory , newInstallDirectory ) ;
2024-10-26 15:48:47 -05:00
localGame . InstallDirectory = newInstallDirectory ;
2026-01-18 04:40:18 -06:00
await _gameService . UpdateAsync ( localGame ) ;
2024-10-26 15:48:47 -05:00
2025-01-31 00:34:37 -06:00
currentItem . Status = InstallStatus . Complete ;
2024-10-26 15:48:47 -05:00
OnQueueChanged ? . Invoke ( ) ;
OnInstallComplete ? . Invoke ( localGame ) ;
operation . Complete ( ) ;
}
2024-05-29 19:25:41 -05:00
}
2024-05-25 15:40:31 -05:00
}
}