2024-08-04 17:53:19 -05:00
using LANCommander.Launcher.Data.Models ;
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 ;
2025-01-29 22:52:27 -06:00
using LANCommander.SDK.Services ;
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
{
private readonly GameService GameService ;
2025-09-24 20:58:23 -05:00
private readonly SDK . Client Client ;
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 ; }
2025-01-29 22:52:27 -06: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
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 (
2024-09-11 00:24:34 -05:00
SDK . Client client ,
2024-10-26 15:48:47 -05:00
ILogger < InstallService > logger ,
2025-09-24 20:58:23 -05:00
GameService gameService ) : base ( logger )
2024-05-25 15:40:31 -05:00
{
2025-09-24 20:58:23 -05:00
Client = client ;
2024-05-25 15:40:31 -05:00
GameService = gameService ;
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 ( ) ;
2025-05-15 03:04:48 +02:00
} ;
2024-05-29 20:01:46 -05:00
2025-01-31 00:34:37 -06:00
Client . Games . OnInstallProgressUpdate + = ( e ) = >
2024-08-07 01:14:13 -05:00
{
2025-01-29 22:52:27 -06:00
OnProgress ? . Invoke ( e ) ;
2024-08-07 01:14:13 -05:00
} ;
2025-02-04 02:34:31 -06:00
Client . Redistributables . OnInstallProgressUpdate + = ( e ) = >
{
OnProgress ? . Invoke ( e ) ;
} ;
2024-05-29 20:01:46 -05:00
// Client.Games.OnArchiveExtractionProgress += Games_OnArchiveExtractionProgress;
// Client.Games.OnArchiveEntryExtractionProgress += Games_OnArchiveEntryExtractionProgress;
}
private void Games_OnArchiveExtractionProgress ( long position , long length , SDK . Models . Game game )
{
OnQueueChanged ? . Invoke ( ) ;
}
private void Games_OnArchiveEntryExtractionProgress ( object sender , SDK . ArchiveEntryExtractionProgressArgs e )
{
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 )
{
var addons = addonIds ! = null ? await Client . Games . GetAddonsAsync ( game . Id ) : [ ] ;
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
{
var gameInfo = await Client . Games . GetAsync ( game . Id ) ;
2024-09-16 20:49:03 -05:00
Logger ? . LogTrace ( "Adding game {GameTitle} to the queue" , gameInfo . Title ) ;
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
{
2025-01-19 00:03:21 -06:00
var baseGame = await GameService . GetAsync ( gameInfo . BaseGameId ) ;
2024-05-25 15:40:31 -05:00
if ( baseGame ! = null & & ! baseGame . Installed )
{
2024-09-12 19:09:13 -05:00
await Add ( baseGame , installDirectory ) ;
2024-05-25 15:40:31 -05:00
}
}
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 )
{
}
2025-01-31 00:34:37 -06:00
if ( ! Queue . Any ( i = > i . Id = = game . Id & & i . Status = = InstallStatus . Queued ) )
2024-05-25 15:40:31 -05:00
{
2024-10-26 15:48:47 -05:00
var queueItem = new InstallQueueGame ( gameInfo ) ;
2024-05-25 15:40:31 -05:00
2024-09-12 19:09:13 -05:00
queueItem . InstallDirectory = installDirectory ;
2025-05-15 03:04:48 +02:00
if ( addons ! = null & & addons . Length > 0 )
{
var versions = addons . ToLookup ( addon = > addon . Id , x = > x . Archives . OrderByDescending ( a = > a . CreatedOn ) . FirstOrDefault ( ) ? . Version ) ;
var addonIds = addons . Select ( x = > x . Id ) . ToArray ( ) ? ? [ ] ;
2024-10-09 19:23:42 -05:00
queueItem . AddonIds = addonIds ;
2025-05-15 03:04:48 +02:00
queueItem . AddonVersions = addonIds . ToDictionary ( x = > x , y = > versions [ y ] ? . FirstOrDefault ( ) ) ;
}
2024-10-09 19:23:42 -05:00
2024-05-25 15:40:31 -05:00
if ( Queue . Any ( i = > i . State ) )
Queue . Add ( queueItem ) ;
else
{
2024-09-16 20:49:03 -05:00
Logger ? . LogTrace ( "Download queue is empty, starting the game download immediately" ) ;
2025-01-31 00:34:37 -06:00
queueItem . Status = InstallStatus . Starting ;
2024-05-25 15:40:31 -05:00
Queue . Add ( queueItem ) ;
2024-10-26 15:48:47 -05:00
await Next ( ) ;
2024-05-25 15:40:31 -05:00
}
OnQueueChanged ? . Invoke ( ) ;
}
}
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 )
{
Logger ? . LogTrace ( "Removing the game {GameTitle} from the queue" , queueItem . Title ) ;
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
{
Logger ? . LogTrace ( "Removing the game {GameTitle} from the queue" , queueItem . Title ) ;
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 ) ;
await queueItem . CancellationToken . CancelAsync ( ) ;
queueItem . Status = InstallStatus . Canceled ;
OnQueueChanged ? . Invoke ( ) ;
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
{
2025-01-31 00:34:37 -06:00
var currentItem = Queue . FirstOrDefault ( i = > i . Status . ValueIsIn ( InstallStatus . Queued , InstallStatus . Starting ) ) ;
2024-05-25 15:40:31 -05:00
if ( currentItem = = null )
return ;
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
{
2025-01-19 00:03:21 -06:00
localGame = await GameService . GetAsync ( currentItem . Id ) ;
2024-10-26 15:48:47 -05:00
remoteGame = await Client . Games . GetAsync ( currentItem . Id ) ;
2024-09-16 20:49:03 -05:00
2024-10-26 15:48:47 -05:00
if ( localGame = = null )
2024-09-16 20:49:03 -05:00
{
Logger ? . LogError ( "Game does not exist in local database, skipping" ) ;
2024-10-26 15:48:47 -05:00
Remove ( currentItem ) ;
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
{
Logger ? . LogError ( "Game info could not be retrieved from the server" ) ;
2024-10-26 15:48:47 -05:00
2025-01-31 00:34:37 -06:00
currentItem . 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
await Client . Games . UpdateGameInstallationAsync ( localGame . InstallDirectory , remoteGame ) ;
2024-10-26 15:48:47 -05:00
// Probably doing a modification of some sort
if ( localGame . InstallDirectory . StartsWith ( currentItem . InstallDirectory ) )
2025-05-15 03:04:48 +02:00
{
2025-05-16 01:04:03 +02:00
var allAddons = remoteGame . DependentGames . ToArray ( ) ;
var removeAddons = allAddons . Except ( currentItem . AddonIds ? ? [ ] ) . ToArray ( ) ;
var addAddons = allAddons . Intersect ( currentItem . AddonIds ? ? [ ] ) . ToArray ( ) ;
2025-05-18 08:31:56 +02:00
var uninstallResult = await Client . Games . UninstallAddonsAsync ( localGame . InstallDirectory , localGame . Id , removeAddons ) ;
var installResult = await Client . Games . InstallAddonsAsync ( localGame . InstallDirectory , localGame . Id , addAddons ) ;
await Client . Games . RestoreFilesAsync ( localGame . InstallDirectory , localGame . Id , uninstallResult . FileList , installResult . FileList ) ;
2025-05-15 03:04:48 +02:00
UpdateGameState ( currentItem , localGame , localGame . InstallDirectory ) ;
await GameService . UpdateAsync ( localGame ) ;
currentItem . Status = InstallStatus . Complete ;
OnQueueChanged ? . Invoke ( ) ;
OnInstallComplete ? . Invoke ( localGame ) ;
}
2024-10-26 15:48:47 -05:00
else
{
await Move ( currentItem , localGame , remoteGame ) ;
}
}
else
{
await Install ( currentItem , localGame , remoteGame ) ;
}
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
2024-10-26 15:48:47 -05:00
public async Task Install ( IInstallQueueItem currentItem , Game localGame , SDK . Models . Game remoteGame )
{
using ( var operation = Logger . BeginOperation ( "Installing game {GameTitle} ({GameId})" , localGame . Title , localGame . Id ) )
2024-06-20 17:14:59 -05:00
{
2024-09-16 20:49:03 -05:00
string installDirectory ;
2024-06-20 17:14:59 -05:00
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
{
2025-10-15 21:26:40 -05:00
var gameFileList = await Client . Games . InstallAsync ( remoteGame . Id , currentItem . InstallDirectory , currentItem . AddonIds , cancellationToken : currentItem . CancellationToken . Token ) ;
2025-05-18 08:31:56 +02:00
installDirectory = gameFileList . InstallDirectory ;
2025-05-15 03:04:48 +02:00
UpdateGameState ( currentItem , localGame , 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" ) ;
Queue . Remove ( currentItem ) ;
return ;
}
catch ( Exception ex )
{
Logger ? . LogError ( ex , "An unknown error occurred during install, removing from queue" ) ;
Queue . Remove ( currentItem ) ;
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
{
2025-09-29 19:49:09 -05:00
var localPath = Path . Combine ( Client . Media . GetLocalPath ( manual ) , $"{manual.FileId}-{manual.Crc32}" ) ;
2024-09-16 20:49:03 -05:00
if ( ! File . Exists ( localPath ) )
{
2025-09-29 19:49:09 -05:00
foreach ( var staleFile in Client . Media . GetStaleLocalPaths ( manual ) )
2024-09-16 20:49:03 -05:00
File . Delete ( staleFile ) ;
await Client . Media . DownloadAsync ( new SDK . Models . Media
{
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
2024-10-26 15:48:47 -05:00
if ( currentItem is InstallQueueGame )
2024-09-16 20:49:03 -05:00
{
currentItem . CompletedOn = DateTime . Now ;
2025-01-31 00:34:37 -06:00
currentItem . Status = InstallStatus . Complete ;
2024-09-16 20:49:03 -05:00
currentItem . Progress = 1 ;
currentItem . BytesDownloaded = currentItem . TotalBytes ;
2024-05-25 15:40:31 -05:00
2024-09-16 20:49:03 -05:00
try
{
2025-01-19 00:03:21 -06:00
await GameService . UpdateAsync ( localGame ) ;
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 write changes to the database after install of game {GameTitle} ({GameId})" , localGame . Title , localGame . Id ) ;
2024-09-16 20:49:03 -05:00
}
2024-05-29 02:02:40 -05:00
2024-09-16 20:49:03 -05:00
OnQueueChanged ? . Invoke ( ) ;
2024-08-11 12:31:30 -05:00
2024-10-26 15:48:47 -05:00
Logger ? . LogTrace ( "Install of game {GameTitle} ({GameId}) complete!" , localGame . Title , localGame . Id ) ;
2024-06-12 23:20:31 -05:00
2024-09-16 20:49:03 -05:00
// ShowCompletedNotification(currentItem);
2024-06-01 22:47:22 -05:00
2024-10-26 15:48:47 -05:00
OnInstallComplete ? . Invoke ( localGame ) ;
2024-09-16 20:52:30 -05:00
operation . Complete ( ) ;
2024-09-16 20:49:03 -05:00
}
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
private static void UpdateGameState ( IInstallQueueItem currentItem , Game localGame , string installDirectory )
{
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
}
}
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 ( ) ;
2024-11-12 02:17:26 -06:00
var newInstallDirectory = await Client . Games . GetInstallDirectory ( remoteGame , currentItem . InstallDirectory ) ;
2024-10-26 15:48:47 -05:00
newInstallDirectory = await Client . Games . MoveAsync ( remoteGame , localGame . InstallDirectory , newInstallDirectory ) ;
localGame . InstallDirectory = newInstallDirectory ;
2025-01-19 00:03:21 -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-08-07 18:48:13 -05:00
/ * private void ShowCompletedNotification ( IDownloadQueueItem queueItem )
2024-06-01 22:47:22 -05:00
{
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" )
2024-06-05 01:21:31 -05:00
) ;
//.Show
2024-06-01 22:47:22 -05:00
// .AddAppLogoOverride()
2024-06-05 01:21:31 -05:00
//.Show();
2024-08-07 18:48:13 -05:00
} * /
2024-05-25 15:40:31 -05:00
}
}