2024-01-10 17:57:00 -06:00
using Force.Crc32 ;
using LANCommander.SDK.Enums ;
2024-01-24 23:36:52 -06:00
using LANCommander.SDK.Exceptions ;
2023-11-10 00:29:16 -06:00
using LANCommander.SDK.Extensions ;
using LANCommander.SDK.Helpers ;
using LANCommander.SDK.Models ;
using Microsoft.Extensions.Logging ;
using SharpCompress.Common ;
using SharpCompress.Readers ;
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text ;
2024-01-10 17:57:00 -06:00
using System.Threading ;
2024-05-20 20:26:46 -05:00
using System.Threading.Tasks ;
2023-11-10 00:29:16 -06:00
2024-10-04 23:37:49 -05:00
namespace LANCommander.SDK.Services
2023-11-10 00:29:16 -06:00
{
2025-01-31 00:34:37 -06:00
public class InstallProgress
2024-08-07 01:14:13 -05:00
{
public Game Game { get ; set ; }
2025-02-04 02:34:31 -06:00
public string Title { get ; set ; }
2025-01-29 22:52:27 -06:00
public Guid IconId { get ; set ; }
2025-01-31 00:34:37 -06:00
public InstallStatus Status { get ; set ; }
2025-02-16 15:47:57 -06:00
public bool Indeterminate { get ; set ; }
2024-08-07 01:14:13 -05:00
public float Progress
{
get
{
2025-02-16 15:48:34 -06:00
return BytesTransferred / ( float ) TotalBytes ;
2024-08-07 01:14:13 -05:00
}
set { }
}
2025-01-29 23:02:59 -06:00
public long TransferSpeed { get ; set ; }
2025-02-16 15:48:34 -06:00
public long BytesTransferred { get ; set ; }
2024-08-07 01:14:13 -05:00
public long TotalBytes { get ; set ; }
2025-01-29 22:52:27 -06:00
public TimeSpan TimeRemaining { get ; set ; }
2024-08-07 01:14:13 -05:00
}
2025-05-18 08:31:56 +02:00
public class InstallResult
{
public InstallResult ( )
{
}
public InstallResult ( string installDirectory , Guid gameId )
{
FileList = new GameInstallationFileList ( installDirectory , gameId ) ;
}
public string InstallDirectory
{
get = > FileList . InstallDirectory ;
internal set = > FileList . InstallDirectory = value ;
}
public GameInstallationFileList FileList { get ; set ; } = GameInstallationFileList . Empty ;
}
2024-01-02 02:34:58 -06:00
public class GameService
2023-11-10 00:29:16 -06:00
{
2025-08-18 02:47:09 -05:00
private readonly ILogger _logger ;
2025-08-18 03:02:49 -05:00
private readonly Client _client ;
2023-11-10 00:29:16 -06:00
private string DefaultInstallDirectory { get ; set ; }
public delegate void OnArchiveEntryExtractionProgressHandler ( object sender , ArchiveEntryExtractionProgressArgs e ) ;
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress ;
2024-01-10 17:54:46 -06:00
public delegate void OnArchiveExtractionProgressHandler ( long position , long length , Game game ) ;
2023-11-10 00:29:16 -06:00
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress ;
2025-01-31 00:34:37 -06:00
public delegate void OnInstallProgressUpdateHandler ( InstallProgress e ) ;
public event OnInstallProgressUpdateHandler OnInstallProgressUpdate ;
2024-08-07 01:14:13 -05:00
2025-08-18 02:47:09 -05:00
private const string PlayerAliasFilename = "PlayerAlias" ;
private const string KeyFilename = "Key" ;
2024-01-19 00:25:38 -06:00
2025-08-18 02:47:09 -05:00
private TrackableStream _transferStream ;
private IReader _reader ;
2023-11-12 01:04:05 -06:00
2025-08-18 02:47:09 -05:00
private readonly InstallProgress _installProgress = new ( ) ;
2024-08-07 01:14:13 -05:00
2025-08-18 02:47:09 -05:00
private readonly Dictionary < Guid , CancellationTokenSource > _running = new ( ) ;
2024-08-26 20:18:54 -05:00
2024-01-02 02:34:58 -06:00
public GameService ( Client client , string defaultInstallDirectory )
2023-11-10 00:29:16 -06:00
{
2025-08-18 03:02:49 -05:00
_client = client ;
2023-11-10 21:36:35 -06:00
DefaultInstallDirectory = defaultInstallDirectory ;
2023-11-10 00:29:16 -06:00
}
2024-01-02 02:34:58 -06:00
public GameService ( Client client , string defaultInstallDirectory , ILogger logger )
2023-11-10 20:53:48 -06:00
{
2025-08-18 03:02:49 -05:00
_client = client ;
2023-11-12 02:10:04 -06:00
DefaultInstallDirectory = defaultInstallDirectory ;
2025-08-18 02:47:09 -05:00
_logger = logger ;
2023-11-10 20:53:48 -06:00
}
2024-05-20 20:26:46 -05:00
public async Task < IEnumerable < Game > > GetAsync ( )
{
2025-08-18 03:02:49 -05:00
return await _client . GetRequestAsync < IEnumerable < Game > > ( "/api/Games" ) ;
2024-05-20 20:26:46 -05:00
}
2024-01-02 02:34:58 -06:00
public Game Get ( Guid id )
{
2025-08-18 03:02:49 -05:00
return _client . GetRequest < Game > ( $"/api/Games/{id}" ) ;
2024-01-02 02:34:58 -06:00
}
2024-05-20 20:26:46 -05:00
public async Task < Game > GetAsync ( Guid id )
{
2025-08-18 03:02:49 -05:00
return await _client . GetRequestAsync < Game > ( $"/api/Games/{id}" ) ;
2024-05-20 20:26:46 -05:00
}
2024-01-02 02:34:58 -06:00
public GameManifest GetManifest ( Guid id )
{
2025-08-18 03:02:49 -05:00
return _client . GetRequest < GameManifest > ( $"/api/Games/{id}/Manifest" ) ;
2024-01-02 02:34:58 -06:00
}
2025-08-18 02:44:55 -05:00
public async Task < ICollection < GameManifest > > GetManifestsAsync ( string installDirectory , Guid id )
2024-09-05 00:48:04 -05:00
{
var manifests = new List < GameManifest > ( ) ;
2025-01-30 23:57:12 -06:00
var mainManifest = await ManifestHelper . ReadAsync < GameManifest > ( installDirectory , id ) ;
2024-09-05 00:48:04 -05:00
if ( mainManifest = = null )
return manifests ;
manifests . Add ( mainManifest ) ;
if ( mainManifest . DependentGames ! = null )
{
foreach ( var dependentGameId in mainManifest . DependentGames )
{
try
{
2025-05-18 04:11:54 +02:00
if ( ManifestHelper . Exists ( installDirectory , dependentGameId ) )
{
2025-05-18 03:09:22 +02:00
var dependentGameManifest = await ManifestHelper . ReadAsync < GameManifest > ( installDirectory , dependentGameId ) ;
2024-09-05 00:48:04 -05:00
2025-05-18 04:11:54 +02:00
if ( dependentGameManifest ? . Type = = GameType . Expansion | | dependentGameManifest ? . Type = = GameType . Mod )
2025-05-18 03:09:22 +02:00
manifests . Add ( dependentGameManifest ) ;
}
2024-09-05 00:48:04 -05:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , $"Could not load manifest from dependent game {dependentGameId}" ) ;
2024-09-05 00:48:04 -05:00
}
}
}
return manifests ;
}
public async Task < IEnumerable < Models . Action > > GetActionsAsync ( string installDirectory , Guid id )
{
var actions = new List < Models . Action > ( ) ;
2025-01-25 01:34:31 -06:00
try
2024-09-05 00:48:04 -05:00
{
2025-08-18 03:02:49 -05:00
if ( _client . IsConnected ( ) )
actions . AddRange ( await _client . GetRequestAsync < IEnumerable < Models . Action > > ( $"/api/Games/{id}/Actions" ) ) ;
2024-09-05 00:48:04 -05:00
}
2025-01-25 01:34:31 -06:00
catch ( Exception ex )
2024-09-05 00:48:04 -05:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not get actions from server" ) ;
2025-01-25 01:34:31 -06:00
}
var manifests = await GetManifestsAsync ( installDirectory , id ) ;
2024-09-05 00:48:04 -05:00
2025-01-25 01:34:31 -06:00
if ( ! actions . Any ( ) )
{
actions = manifests
. Where ( m = > m ! = null & & m . Actions ! = null )
. SelectMany ( m = > m . Actions )
. OrderByDescending ( a = > a . IsPrimaryAction )
. ThenBy ( a = > a . SortOrder )
. ToList ( ) ;
2024-09-05 00:48:04 -05:00
}
2024-10-04 23:37:49 -05:00
if ( manifests . Any ( m = > m . OnlineMultiplayer ! = null & & m . OnlineMultiplayer . NetworkProtocol = = NetworkProtocol . Lobby | | m . LanMultiplayer ! = null & & m . LanMultiplayer . NetworkProtocol = = NetworkProtocol . Lobby ) )
2024-09-05 00:48:04 -05:00
{
2025-01-25 01:34:31 -06:00
var primaryAction = actions . Where ( a = > a . IsPrimaryAction ) . First ( ) ;
2024-09-05 00:48:04 -05:00
2024-11-03 16:19:39 -06:00
try
2024-09-05 00:48:04 -05:00
{
2025-08-18 03:02:49 -05:00
var lobbies = _client . Lobbies . GetSteamLobbies ( installDirectory , id ) ;
2024-11-03 16:19:39 -06:00
foreach ( var lobby in lobbies )
2024-09-05 00:48:04 -05:00
{
2024-11-03 16:19:39 -06:00
var lobbyAction = new Models . Action
{
Arguments = $"{primaryAction.Arguments} +connect_lobby {lobby.Id}" ,
IsPrimaryAction = true ,
Name = $"Join {lobby.ExternalUsername}'s lobby" ,
SortOrder = actions . Count ,
Variables = primaryAction . Variables ,
Path = primaryAction . Path ,
WorkingDirectory = primaryAction . WorkingDirectory
} ;
actions . Add ( lobbyAction ) ;
}
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not get lobbies" ) ;
2024-09-05 00:48:04 -05:00
}
}
return actions ;
}
2025-01-27 01:25:32 -06:00
public async Task < IEnumerable < Game > > GetAddonsAsync ( Guid id )
{
2025-08-18 03:02:49 -05:00
return await _client . GetRequestAsync < IEnumerable < Game > > ( $"/api/Games/{id}/Addons" ) ;
2025-01-27 01:25:32 -06:00
}
2025-01-25 01:48:37 -06:00
public async Task < bool > CheckForUpdateAsync ( Guid id , string currentVersion )
{
2025-08-18 03:02:49 -05:00
return await _client . GetRequestAsync < bool > ( $"/api/Games/{id}/CheckForUpdate?version={currentVersion}" ) ;
2025-01-25 01:48:37 -06:00
}
2024-01-02 02:34:58 -06:00
private TrackableStream Stream ( Guid id )
{
2025-08-18 03:02:49 -05:00
return _client . StreamRequest ( $"/api/Games/{id}/Download" ) ;
2024-01-02 02:34:58 -06:00
}
2025-02-23 15:39:06 -06:00
public async Task StartedAsync ( Guid id )
2024-07-08 19:51:17 -05:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Signaling to the server that we started the game..." ) ;
2024-07-08 19:51:17 -05:00
2025-02-23 15:39:06 -06:00
try
{
2025-08-18 03:02:49 -05:00
await _client . GetRequestAsync < object > ( $"/api/Games/{id}/Started" ) ;
2025-02-23 15:39:06 -06:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Failed sending start request to server" ) ;
2025-02-23 15:39:06 -06:00
}
2024-07-08 19:51:17 -05:00
}
2025-02-23 15:39:06 -06:00
public async Task StoppedAsync ( Guid id )
2024-07-08 19:51:17 -05:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Signaling to the server that we stopped the game..." ) ;
2024-07-08 19:51:17 -05:00
2024-10-02 12:28:46 -05:00
try
2025-02-28 17:38:25 -06:00
{
2025-08-18 03:02:49 -05:00
await _client . GetRequestAsync < object > ( $"/api/Games/{id}/Stopped" ) ;
2024-10-02 12:28:46 -05:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Failed sending stop request to server" ) ;
2024-10-02 12:28:46 -05:00
}
2024-01-02 02:34:58 -06:00
}
public string GetAllocatedKey ( Guid id )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Requesting allocated key..." ) ;
2024-01-02 02:34:58 -06:00
2025-08-18 03:02:49 -05:00
var macAddress = _client . GetMacAddress ( ) ;
2024-01-02 02:34:58 -06:00
var request = new KeyRequest ( )
{
GameId = id ,
MacAddress = macAddress ,
ComputerName = Environment . MachineName ,
2025-08-18 03:02:49 -05:00
IpAddress = _client . GetIpAddress ( ) ,
2024-01-02 02:34:58 -06:00
} ;
2025-08-18 03:02:49 -05:00
var response = _client . PostRequest < Key > ( $"/api/Keys/GetAllocated/{id}" , request ) ;
2024-01-02 02:34:58 -06:00
if ( response = = null )
2024-10-04 23:37:49 -05:00
return string . Empty ;
2024-01-02 02:34:58 -06:00
return response . Value ;
}
2024-07-15 18:17:04 -05:00
public async Task < string > GetAllocatedKeyAsync ( Guid id )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Requesting allocated key..." ) ;
2024-07-15 18:17:04 -05:00
2025-08-18 03:02:49 -05:00
var macAddress = _client . GetMacAddress ( ) ;
2024-07-15 18:17:04 -05:00
var request = new KeyRequest ( )
{
GameId = id ,
MacAddress = macAddress ,
ComputerName = Environment . MachineName ,
2025-08-18 03:02:49 -05:00
IpAddress = _client . GetIpAddress ( ) ,
2024-07-15 18:17:04 -05:00
} ;
2025-08-18 03:02:49 -05:00
var response = await _client . PostRequestAsync < Key > ( $"/api/Keys/GetAllocated/{id}" , request ) ;
2024-07-15 18:17:04 -05:00
if ( response = = null )
2024-10-04 23:37:49 -05:00
return string . Empty ;
2024-07-15 18:17:04 -05:00
return response . Value ;
}
2024-01-02 02:34:58 -06:00
public string GetNewKey ( Guid id )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Requesting new key allocation..." ) ;
2024-01-02 02:34:58 -06:00
2025-08-18 03:02:49 -05:00
var macAddress = _client . GetMacAddress ( ) ;
2024-01-02 02:34:58 -06:00
var request = new KeyRequest ( )
{
GameId = id ,
MacAddress = macAddress ,
ComputerName = Environment . MachineName ,
2025-08-18 03:02:49 -05:00
IpAddress = _client . GetIpAddress ( ) ,
2024-01-02 02:34:58 -06:00
} ;
2025-08-18 03:02:49 -05:00
var response = _client . PostRequest < Key > ( $"/api/Keys/Allocate/{id}" , request ) ;
2024-01-02 02:34:58 -06:00
if ( response = = null )
2024-10-04 23:37:49 -05:00
return string . Empty ;
2024-01-02 02:34:58 -06:00
return response . Value ;
}
2023-11-10 00:29:16 -06:00
/// <summary>
/// Downloads, extracts, and runs post-install scripts for the specified game
/// </summary>
2025-05-18 08:31:56 +02:00
/// <param name="gameId">Unique identifier of the game to install.</param>
/// <param name="installDirectory">Optional custom installation directory.</param>
/// <param name="addonIds">Optional list of add-on identifiers to install alongside the game.</param>
2023-11-10 00:29:16 -06:00
/// <param name="maxAttempts">Maximum attempts in case of transmission error</param>
2025-05-18 08:31:56 +02:00
/// <returns>
/// An <see cref="InstallResult"/> containing details about the installation outcome such as the final install path.
/// </returns>
/// <exception cref="Exception">
/// Thrown if installation fails after the maximum retry attempts.
/// </exception>
public async Task < InstallResult > InstallAsync ( Guid gameId , string installDirectory = "" , Guid [ ] addonIds = null , int maxAttempts = 10 )
2023-11-10 00:29:16 -06:00
{
2025-05-18 08:31:56 +02:00
var installResult = new InstallResult ( installDirectory , gameId ) ;
var gameFileList = installResult . FileList ;
2023-11-17 11:48:45 -06:00
GameManifest manifest = null ;
2024-10-04 23:37:49 -05:00
if ( string . IsNullOrWhiteSpace ( installDirectory ) )
2025-08-18 03:02:49 -05:00
installDirectory = _client . DefaultInstallDirectory ;
2024-09-12 19:09:13 -05:00
2024-01-02 02:34:58 -06:00
var game = Get ( gameId ) ;
2024-11-12 02:17:26 -06:00
var destination = await GetInstallDirectory ( game , installDirectory ) ;
2023-11-10 00:29:16 -06:00
2025-01-31 00:34:37 -06:00
_installProgress . Game = game ;
2025-02-04 02:34:31 -06:00
_installProgress . Title = game . Title ;
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . Downloading ;
_installProgress . Progress = 0 ;
_installProgress . TransferSpeed = 0 ;
2025-02-04 02:34:31 -06:00
_installProgress . TotalBytes = 0 ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = 0 ;
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-08-07 01:14:13 -05:00
2024-05-01 23:58:07 -05:00
// Handle Standalone Mods
2024-11-12 02:17:26 -06:00
if ( game . Type = = GameType . StandaloneMod & & game . BaseGameId ! = Guid . Empty )
2024-01-08 18:52:00 -06:00
{
2025-08-18 03:02:49 -05:00
var baseGame = await _client . Games . GetAsync ( game . BaseGameId ) ;
2024-11-12 02:17:26 -06:00
destination = await GetInstallDirectory ( baseGame , installDirectory ) ;
2024-01-08 18:52:00 -06:00
if ( ! Directory . Exists ( destination ) )
2025-05-18 08:31:56 +02:00
{
var baseGameFileList = await InstallAsync ( game . BaseGameId , installDirectory , null , maxAttempts ) ;
destination = installResult . InstallDirectory ;
}
2024-01-08 18:52:00 -06:00
}
2023-11-17 11:48:45 -06:00
2023-11-20 18:20:34 -06:00
try
{
2024-01-09 01:22:56 -06:00
if ( ManifestHelper . Exists ( destination , game . Id ) )
2025-01-30 23:57:12 -06:00
manifest = await ManifestHelper . ReadAsync < GameManifest > ( destination , game . Id ) ;
2023-11-20 18:20:34 -06:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( ex , "Error reading manifest before install" ) ;
2023-11-20 18:20:34 -06:00
}
2023-11-10 00:29:16 -06:00
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Installing game {GameTitle} ({GameId})" , game . Title , game . Id ) ;
2023-11-10 00:29:16 -06:00
2024-05-01 23:58:07 -05:00
// Download and extract
2024-10-04 23:37:49 -05:00
var result = await RetryHelper . RetryOnExceptionAsync ( maxAttempts , TimeSpan . FromMilliseconds ( 500 ) , new ExtractionResult ( ) , async ( ) = >
2024-01-10 17:57:00 -06:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Attempting to download and extract game" ) ;
2023-11-10 00:29:16 -06:00
2024-08-07 18:00:43 -05:00
return await Task . Run ( ( ) = > DownloadAndExtract ( game , destination ) ) ;
2024-01-10 17:57:00 -06:00
} ) ;
2023-11-10 00:29:16 -06:00
2024-01-10 17:57:00 -06:00
if ( ! result . Success & & ! result . Canceled )
2024-01-24 23:36:52 -06:00
throw new InstallException ( "Could not extract the installer. Retry the install or check your connection" ) ;
2024-01-10 17:57:00 -06:00
else if ( result . Canceled )
2024-01-25 00:46:10 -06:00
throw new InstallCanceledException ( "Game install was canceled" ) ;
2023-11-10 00:29:16 -06:00
2024-01-10 17:57:00 -06:00
game . InstallDirectory = result . Directory ;
2025-05-18 08:31:56 +02:00
installResult . InstallDirectory = result . Directory ;
2023-11-10 00:29:16 -06:00
2024-05-01 23:58:07 -05:00
// Game is extracted, get metadata
2025-01-30 23:57:12 -06:00
var writeManifestSuccess = await RetryHelper . RetryOnExceptionAsync ( maxAttempts , TimeSpan . FromSeconds ( 1 ) , false , async ( ) = >
2023-11-10 00:29:16 -06:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Attempting to get game manifest" ) ;
2025-05-17 02:02:57 +02:00
manifest = await WriteManifestAsync ( game . InstallDirectory , game ) ;
2023-11-10 00:29:16 -06:00
return true ;
} ) ;
if ( ! writeManifestSuccess )
2024-01-24 23:36:52 -06:00
throw new InstallException ( "Could not grab the manifest file. Retry the install or check your connection" ) ;
2023-11-10 00:29:16 -06:00
2025-05-17 02:02:57 +02:00
// store scripts locally
await WriteScriptsAsync ( game . InstallDirectory , game ) ;
2023-11-10 00:29:16 -06:00
2025-05-18 08:31:56 +02:00
// store manifest and files for current game (could be base game, or any dependent game as this point due to recursive call)
gameFileList . BaseGame . Manifest = manifest ;
var gameFiles = result ? . Files ? . Where ( x = > ! x . EntryPath . EndsWith ( "/" ) ) . Select ( x = > new GameInstallationFileListEntry . FileEntry
2024-04-29 18:33:31 -05:00
{
2025-05-18 08:31:56 +02:00
EntryPath = x . EntryPath ,
LocalPath = x . LocalPath ,
} ) ;
gameFileList . BaseGame . AddFiles ( gameFiles ? ? [ ] ) ;
2023-11-10 00:29:16 -06:00
2025-01-31 00:34:37 -06:00
_installProgress . Progress = 1 ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = _installProgress . TotalBytes ;
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . InstallingRedistributables ;
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-08-07 01:14:13 -05:00
#region Install Redistributables
if ( game . Redistributables ! = null & & game . Redistributables . Any ( ) )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Installing redistributables" ) ;
2024-08-07 01:14:13 -05:00
2025-08-18 03:02:49 -05:00
await _client . Redistributables . InstallAsync ( game ) ;
2024-08-07 01:14:13 -05:00
}
#endregion
#region Download Latest Save
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Attempting to download the latest save" ) ;
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . DownloadingSaves ;
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-08-07 01:14:13 -05:00
2025-08-18 03:02:49 -05:00
await _client . Saves . DownloadAsync ( game . InstallDirectory , game . Id ) ;
2024-08-07 01:14:13 -05:00
#endregion
2024-10-26 15:48:47 -05:00
await RunPostInstallScripts ( game ) ;
2024-08-07 01:14:13 -05:00
2024-10-26 15:48:47 -05:00
if ( addonIds ! = null )
2025-05-18 08:31:56 +02:00
{
var addonsResult = await InstallAddonsAsync ( installDirectory , game , addonIds ) ;
gameFileList . MergeDependentGames ( addonsResult . FileList ) ;
}
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . Complete ;
_installProgress . Progress = 1 ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = _installProgress . TotalBytes ;
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-08-07 01:14:13 -05:00
2025-05-18 08:31:56 +02:00
return installResult ;
2024-10-26 15:48:47 -05:00
}
2024-08-07 01:14:13 -05:00
2025-05-18 08:31:56 +02:00
public async Task < InstallResult > InstallAddonsAsync ( string installDirectory , Guid baseGameId , IEnumerable < Guid > addonIds )
2024-10-26 15:48:47 -05:00
{
2025-08-18 03:02:49 -05:00
var game = await _client . Games . GetAsync ( baseGameId ) ;
2024-08-07 01:14:13 -05:00
2025-05-18 08:31:56 +02:00
return await InstallAddonsAsync ( installDirectory , game , addonIds ) ;
2024-10-26 15:48:47 -05:00
}
2024-08-07 01:14:13 -05:00
2025-05-18 08:31:56 +02:00
public async Task < InstallResult > InstallAddonsAsync ( string installDirectory , Game game , IEnumerable < Guid > addonIds )
2024-10-26 15:48:47 -05:00
{
2025-05-18 08:31:56 +02:00
var installResult = new InstallResult ( installDirectory , game . Id ) ;
var gameFileList = installResult . FileList ;
2024-11-15 23:42:17 -06:00
if ( addonIds ! = null )
2024-10-26 15:48:47 -05:00
{
2025-01-29 22:52:27 -06:00
var addons = new List < Game > ( ) ;
2024-11-15 23:42:17 -06:00
foreach ( var addonId in addonIds )
{
2025-01-29 22:52:27 -06:00
try
{
2025-08-18 03:02:49 -05:00
addons . Add ( await _client . Games . GetAsync ( addonId ) ) ;
2025-01-29 22:52:27 -06:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not get information for addon with ID {AddonId}, skipping install" , addonId ) ;
2025-01-29 22:52:27 -06:00
}
}
var expansions = addons . Where ( a = > a ? . Type = = GameType . Expansion ) . ToList ( ) ;
foreach ( var expansion in expansions )
{
try
{
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . Downloading ;
_installProgress . Game = expansion ;
_installProgress . Progress = 0 ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = 0 ;
2025-01-31 00:34:37 -06:00
_installProgress . TotalBytes = 1 ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = 0 ;
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2025-05-18 08:31:56 +02:00
var expansionResult = await InstallAddonAsync ( installDirectory , expansion ) ;
gameFileList . MergeBaseAsDependentGame ( expansion . Id , expansionResult . FileList ) ;
2025-01-29 22:52:27 -06:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not install expansion with ID {AddonId}" , expansion . Id ) ;
2025-01-29 22:52:27 -06:00
}
}
var mods = addons . Where ( a = > a ? . Type = = GameType . Mod ) . ToList ( ) ;
foreach ( var mod in mods )
{
try
{
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . Downloading ;
_installProgress . Game = mod ;
_installProgress . Progress = 0 ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = 0 ;
2025-01-31 00:34:37 -06:00
_installProgress . TotalBytes = 1 ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = 0 ;
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2025-05-18 08:31:56 +02:00
var modResult = await InstallAddonAsync ( installDirectory , mod ) ;
gameFileList . MergeBaseAsDependentGame ( mod . Id , modResult . FileList ) ;
2025-01-29 22:52:27 -06:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not install mod with ID {AddonId}" , mod . Id ) ;
2025-01-29 22:52:27 -06:00
}
2024-11-15 23:42:17 -06:00
}
2024-10-26 15:48:47 -05:00
}
2025-05-18 08:31:56 +02:00
return installResult ;
2024-10-26 15:48:47 -05:00
}
2024-08-07 01:14:13 -05:00
2025-05-18 08:31:56 +02:00
public async Task < InstallResult > InstallAddonAsync ( string installDirectory , Game addon )
2024-10-26 15:48:47 -05:00
{
2025-05-18 08:31:56 +02:00
var installResult = new InstallResult ( installDirectory , addon . Id ) ;
var gameFileList = installResult . FileList ;
2024-11-12 02:17:26 -06:00
if ( ! addon . IsAddon )
2025-05-18 08:31:56 +02:00
return installResult ;
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-08-07 01:14:13 -05:00
2024-10-26 15:48:47 -05:00
try
{
2025-05-18 08:31:56 +02:00
var addonResult = await InstallAsync ( addon . Id , installDirectory ) ;
gameFileList . Merge ( addonResult . FileList ) ;
2024-10-26 15:48:47 -05:00
}
catch ( InstallCanceledException ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogDebug ( "Install canceled" ) ;
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . Canceled ;
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-08-07 01:14:13 -05:00
2024-10-26 15:48:47 -05:00
throw ;
2024-08-07 01:14:13 -05:00
}
2024-10-26 15:48:47 -05:00
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Failed to install addon {AddonTitle} ({AddonId})" , addon . Title , addon . Id ) ;
2024-08-07 01:14:13 -05:00
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . Failed ;
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-08-07 01:14:13 -05:00
2024-10-26 15:48:47 -05:00
throw ;
}
2024-08-07 01:14:13 -05:00
2024-10-26 15:48:47 -05:00
await RunPostInstallScripts ( addon ) ;
2025-05-18 08:31:56 +02:00
return installResult ;
2023-11-10 00:29:16 -06:00
}
2025-05-18 08:31:56 +02:00
public async Task < InstallResult > UninstallAsync ( string installDirectory , Guid gameId )
2023-11-10 00:29:16 -06:00
{
2025-05-18 08:31:56 +02:00
var installResult = new InstallResult ( installDirectory , gameId ) ;
var gameFileList = installResult . FileList ;
2025-01-30 23:57:12 -06:00
var manifest = await ManifestHelper . ReadAsync < GameManifest > ( installDirectory , gameId ) ;
2025-05-18 03:09:22 +02:00
if ( manifest = = null )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogInformation ( "Unable to read or find manifest for game with ID {GameId}. Skip uninstallation!" , gameId ) ;
2025-05-18 08:31:56 +02:00
return installResult ;
2025-05-18 03:09:22 +02:00
}
2024-08-07 01:47:13 -05:00
2025-05-18 08:31:56 +02:00
// store manifest for current game (could be base game, or any dependent game as this point due to recursive call)
gameFileList . BaseGame . Manifest = manifest ;
var baseFileList = gameFileList . BaseGame ;
2024-08-07 01:47:13 -05:00
#region Uninstall Dependent Games
if ( manifest . DependentGames ! = null )
{
foreach ( var dependentGame in manifest . DependentGames )
{
2025-01-27 00:05:09 -06:00
try
{
2025-05-18 03:09:22 +02:00
if ( ManifestHelper . Exists ( installDirectory , dependentGame ) )
{
2025-05-18 08:31:56 +02:00
var dependentResult = await UninstallAsync ( installDirectory , dependentGame ) ;
gameFileList . MergeDependentGames ( dependentResult . FileList ) ;
2025-05-18 03:09:22 +02:00
}
2025-01-27 00:05:09 -06:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogWarning ( "Could not uninstall dependent game with ID {GameId}. Assuming it's already uninstalled or never installed..." , gameId ) ;
2025-01-27 00:05:09 -06:00
}
2024-08-07 01:47:13 -05:00
}
}
#endregion
#region Delete Files
2024-10-04 23:37:49 -05:00
var fileListPath = GetMetadataFilePath ( installDirectory , gameId , "FileList.txt" ) ;
2023-11-10 00:29:16 -06:00
2024-01-15 16:01:15 -06:00
if ( File . Exists ( fileListPath ) )
2024-01-10 01:59:51 -06:00
{
2024-08-07 01:47:13 -05:00
var fileList = await File . ReadAllLinesAsync ( fileListPath ) ;
2025-08-18 02:44:55 -05:00
var files = fileList . Select ( l = > l . Split ( '|' ) . FirstOrDefault ( ) ? . Trim ( ) ) ;
2024-01-10 01:59:51 -06:00
2025-08-18 02:47:09 -05:00
_logger ? . LogDebug ( "Attempting to delete the install files" ) ;
2024-01-15 16:01:15 -06:00
2025-08-18 02:44:55 -05:00
foreach ( var file in files . Where ( f = > f ! = null & & ! f . EndsWith ( "/" ) ) )
2024-01-15 16:01:15 -06:00
{
var localPath = Path . Combine ( installDirectory , file ) ;
2025-05-18 08:31:56 +02:00
baseFileList . AddFile ( new GameInstallationFileListEntry . FileEntry
{
EntryPath = file ,
LocalPath = localPath ,
} ) ;
2024-01-15 16:01:15 -06:00
2024-02-03 13:55:40 -06:00
try
{
if ( File . Exists ( localPath ) )
File . Delete ( localPath ) ;
2024-08-07 01:47:13 -05:00
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Deleted file {LocalPath}" , localPath ) ;
2024-02-03 13:55:40 -06:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogWarning ( ex , "Could not remove file {LocalPath}" , localPath ) ;
2024-02-03 13:55:40 -06:00
}
2024-01-15 16:01:15 -06:00
}
2023-11-10 00:29:16 -06:00
2025-08-18 02:47:09 -05:00
_logger ? . LogDebug ( "Attempting to delete any empty directories" ) ;
2024-01-10 01:59:51 -06:00
2024-01-15 16:01:15 -06:00
DirectoryHelper . DeleteEmptyDirectories ( installDirectory ) ;
2024-01-10 01:59:51 -06:00
2024-01-15 16:01:15 -06:00
if ( ! Directory . Exists ( installDirectory ) )
2025-08-18 02:47:09 -05:00
_logger ? . LogDebug ( "Deleted install directory {InstallDirectory}" , installDirectory ) ;
2024-01-15 16:01:15 -06:00
else
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Removed game files for {GameTitle} ({GameId})" , manifest . Title , gameId ) ;
2024-01-15 16:01:15 -06:00
}
2024-01-10 01:59:51 -06:00
else
2024-01-15 16:01:15 -06:00
{
Directory . Delete ( installDirectory , true ) ;
}
2024-08-07 01:47:13 -05:00
#endregion
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunUninstallScriptAsync ( installDirectory , gameId ) ;
2024-08-07 01:47:13 -05:00
#region Cleanup Install Directory
var metadataPath = GetMetadataDirectoryPath ( installDirectory , gameId ) ;
if ( Directory . Exists ( metadataPath ) )
Directory . Delete ( metadataPath , true ) ;
DirectoryHelper . DeleteEmptyDirectories ( installDirectory ) ;
#endregion
2025-05-18 08:31:56 +02:00
return installResult ;
2023-11-10 00:29:16 -06:00
}
2025-05-18 08:31:56 +02:00
public async Task < InstallResult > UninstallAddonsAsync ( string installDirectory , Guid baseGameId , IEnumerable < Guid > addonIds )
2025-05-16 01:04:03 +02:00
{
2025-05-18 08:31:56 +02:00
var installResult = new InstallResult ( installDirectory , baseGameId ) ;
var gameFileList = installResult . FileList ;
2025-05-16 01:04:03 +02:00
var baseManifest = await ManifestHelper . ReadAsync < GameManifest > ( installDirectory , baseGameId ) ;
2025-05-18 08:31:56 +02:00
if ( baseManifest = = null )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogInformation ( "Unable to read or find manifest for addon game with ID {GameId}. Skip uninstallation!" , baseGameId ) ;
2025-05-18 08:31:56 +02:00
return installResult ;
}
// store manifest for current addon game, skip any files
gameFileList . BaseGame . Manifest = baseManifest ;
gameFileList . InstallDirectory = installDirectory ;
2025-05-16 01:04:03 +02:00
addonIds ? ? = [ ] ;
foreach ( var dependentGame in baseManifest . DependentGames )
{
if ( ! addonIds . Contains ( dependentGame ) )
continue ;
try
{
2025-05-18 08:31:56 +02:00
var dependentResult = await UninstallAddonAsync ( installDirectory , dependentGame ) ;
gameFileList . MergeBaseAsDependentGame ( dependentGame , dependentResult . FileList ) ;
2025-05-16 01:04:03 +02:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogWarning ( ex , $"Could not uninstall dependent game {dependentGame} of base game {baseGameId}. Assuming it's already uninstalled or never installed..." ) ;
2025-05-16 01:04:03 +02:00
}
}
2025-05-18 08:31:56 +02:00
return installResult ;
2025-05-16 01:04:03 +02:00
}
2025-05-18 08:31:56 +02:00
public async Task < InstallResult > UninstallAddonAsync ( string installDirectory , Guid addonGameId )
2025-05-16 01:04:03 +02:00
{
2025-05-18 08:31:56 +02:00
var installResult = new InstallResult ( installDirectory , addonGameId ) ;
var gameFileList = installResult . FileList ;
2025-05-16 01:04:03 +02:00
var manifest = await ManifestHelper . ReadAsync < GameManifest > ( installDirectory , addonGameId ) ;
if ( manifest ! = null )
{
2025-05-18 08:31:56 +02:00
var dependentResult = await UninstallAsync ( installDirectory , manifest . Id ) ;
gameFileList . BaseGame . Manifest = manifest ;
gameFileList . Merge ( dependentResult . FileList ) ;
2025-05-16 01:04:03 +02:00
}
2025-05-18 08:31:56 +02:00
return installResult ;
2023-11-10 00:29:16 -06:00
}
2024-10-26 15:48:47 -05:00
public async Task < string > MoveAsync ( Guid gameId , string oldInstallDirectory , string newInstallDirectory )
{
var game = await GetAsync ( gameId ) ;
return await MoveAsync ( game , oldInstallDirectory , newInstallDirectory ) ;
}
public async Task < string > MoveAsync ( Game game , string oldInstallDirectory , string newInstallDirectory )
{
var gameAndAddons = new List < Game > ( ) ;
2025-01-31 00:34:37 -06:00
_installProgress . Game = game ;
2025-02-16 15:47:57 -06:00
_installProgress . Status = InstallStatus . EnumeratingFiles ;
_installProgress . Indeterminate = true ;
_installProgress . Progress = 0 ;
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-10-26 15:48:47 -05:00
gameAndAddons . Add ( game ) ;
2024-11-12 02:17:26 -06:00
foreach ( var dependentGameId in game . DependentGames )
{
2025-08-18 03:02:49 -05:00
var dependentGame = await _client . Games . GetAsync ( dependentGameId ) ;
2024-11-12 02:17:26 -06:00
if ( dependentGame . IsAddon )
gameAndAddons . Add ( dependentGame ) ;
}
2024-10-26 15:48:47 -05:00
foreach ( var entry in gameAndAddons )
{
2024-11-12 02:17:26 -06:00
if ( await IsInstalled ( oldInstallDirectory , game , entry . Id ) )
2025-08-18 03:02:49 -05:00
await _client . Saves . UploadAsync ( oldInstallDirectory , entry . Id ) ;
2024-10-26 15:48:47 -05:00
}
if ( Directory . Exists ( newInstallDirectory ) )
2025-02-16 15:42:00 -06:00
{
// Trigger notification eventually
_installProgress . Status = InstallStatus . Failed ;
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
return newInstallDirectory ;
}
2024-10-26 15:48:47 -05:00
var directories = Directory . GetDirectories ( oldInstallDirectory , "*" , SearchOption . AllDirectories ) ;
var files = Directory . GetFiles ( oldInstallDirectory , "*.*" , SearchOption . AllDirectories ) ;
var fileInfos = files . Select ( f = > new FileInfo ( f ) ) ;
var totalSize = fileInfos . Sum ( fi = > fi . Length ) ;
2025-02-16 15:42:00 -06:00
long totalPos = 0 ;
2024-10-26 15:48:47 -05:00
2025-02-16 15:47:57 -06:00
_installProgress . Status = InstallStatus . Moving ;
_installProgress . Indeterminate = false ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = totalPos ;
2025-02-16 15:47:57 -06:00
_installProgress . TotalBytes = totalSize ;
2024-10-26 15:48:47 -05:00
foreach ( var directory in directories )
{
Directory . CreateDirectory ( directory . Replace ( oldInstallDirectory , newInstallDirectory ) ) ;
}
2025-02-16 15:42:00 -06:00
using ( var fileTransferMonitor = new FileTransferMonitor ( totalSize ) )
2024-10-26 15:48:47 -05:00
{
2025-02-16 15:42:00 -06:00
foreach ( var fileInfo in fileInfos )
2024-10-26 15:48:47 -05:00
{
2025-02-16 15:42:00 -06:00
using ( FileStream sourceStream = File . Open ( fileInfo . FullName , FileMode . Open ) )
using ( FileStream destinationStream = File . Create ( fileInfo . FullName . Replace ( oldInstallDirectory , newInstallDirectory ) ) )
2024-10-26 15:48:47 -05:00
{
2025-02-16 15:42:00 -06:00
_installProgress . TotalBytes = totalSize ;
var buffer = new byte [ 81920 ] ;
int bytesRead ;
while ( ( bytesRead = await sourceStream . ReadAsync ( buffer , 0 , buffer . Length ) ) > 0 )
2024-10-26 15:48:47 -05:00
{
2025-02-16 15:42:00 -06:00
await destinationStream . WriteAsync ( buffer , 0 , bytesRead ) ;
totalPos + = bytesRead ;
2025-01-29 22:52:27 -06:00
if ( fileTransferMonitor . CanUpdate ( ) )
{
2025-02-16 15:42:00 -06:00
fileTransferMonitor . Update ( totalPos ) ;
2025-01-29 22:52:27 -06:00
2025-01-31 00:34:37 -06:00
_installProgress . TimeRemaining = fileTransferMonitor . GetTimeRemaining ( ) ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = fileTransferMonitor . GetBytesTransferred ( ) ;
2025-01-31 00:34:37 -06:00
_installProgress . TransferSpeed = fileTransferMonitor . GetSpeed ( ) ;
2025-02-16 15:42:00 -06:00
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2025-01-29 22:52:27 -06:00
}
2025-02-16 15:42:00 -06:00
}
2025-01-29 22:52:27 -06:00
}
2024-10-26 15:48:47 -05:00
}
}
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = totalSize ;
2025-01-31 00:34:37 -06:00
_installProgress . Progress = 1 ;
_installProgress . Status = InstallStatus . RunningScripts ;
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-10-26 15:48:47 -05:00
Directory . Delete ( oldInstallDirectory , true ) ;
foreach ( var entry in gameAndAddons )
{
2024-11-12 02:17:26 -06:00
if ( await IsInstalled ( newInstallDirectory , game , entry . Id ) )
2024-10-26 15:48:47 -05:00
{
await RunPostInstallScripts ( entry ) ;
2025-08-18 03:02:49 -05:00
await _client . Saves . DownloadAsync ( newInstallDirectory , entry . Id ) ;
2024-10-26 15:48:47 -05:00
}
}
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . Complete ;
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-10-26 15:48:47 -05:00
return newInstallDirectory ;
}
2024-11-12 02:17:26 -06:00
public async Task < bool > IsInstalled ( string installDirectory , Game game , Guid ? addonId = null )
2024-10-26 15:48:47 -05:00
{
2024-11-12 02:17:26 -06:00
installDirectory = await GetInstallDirectory ( game , installDirectory ) ;
2024-10-26 15:48:47 -05:00
var metadataPath = ManifestHelper . GetPath ( installDirectory , addonId ? ? game . Id ) ;
return File . Exists ( metadataPath ) ;
}
2025-05-17 02:02:57 +02:00
public async Task UpdateGameInstallationAsync ( string installDirectory , Game game )
{
// update game and scripts locally
await WriteManifestAsync ( installDirectory , game ) ;
await WriteScriptsAsync ( installDirectory , game ) ;
}
private async Task < GameManifest > WriteManifestAsync ( string installDirectory , Game game )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( $"Retrieving game manifest for game {game.Title} with id {game.Id}" ) ;
2025-05-17 02:02:57 +02:00
GameManifest manifest = GetManifest ( game . Id ) ;
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( $"Saving Manifest for game {game.Id} into {installDirectory}" ) ;
2025-05-17 02:02:57 +02:00
await ManifestHelper . WriteAsync ( manifest , installDirectory ) ;
return manifest ;
}
private async Task WriteScriptsAsync ( string installDirectory , Game game )
{
2025-08-07 01:02:22 -05:00
if ( game . Scripts ! = null )
2025-05-17 02:02:57 +02:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( $"Saving scripts for game {game.Title} with id {game.Id} into {installDirectory}" ) ;
2025-08-07 01:02:22 -05:00
foreach ( var script in game . Scripts )
{
await ScriptHelper . SaveScriptAsync ( game , script . Type , installDirectory ) ;
}
2025-05-17 02:02:57 +02:00
}
}
2024-10-26 15:48:47 -05:00
private async Task RunPostInstallScripts ( Game game )
{
if ( game . Scripts ! = null & & game . Scripts . Any ( ) )
{
2025-01-31 00:34:37 -06:00
_installProgress . Status = InstallStatus . RunningScripts ;
2024-10-26 15:48:47 -05:00
2025-01-31 00:34:37 -06:00
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
2024-10-26 15:48:47 -05:00
try
{
var allocatedKey = await GetAllocatedKeyAsync ( game . Id ) ;
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunInstallScriptAsync ( game . InstallDirectory , game . Id ) ;
await _client . Scripts . RunKeyChangeScriptAsync ( game . InstallDirectory , game . Id , allocatedKey ) ;
await _client . Scripts . RunNameChangeScriptAsync ( game . InstallDirectory , game . Id , await _client . Profile . GetAliasAsync ( ) ) ;
2024-10-26 15:48:47 -05:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Scripts failed to execute for game/addon {GameTitle} ({GameId})" , game . Title , game . Id ) ;
2024-10-26 15:48:47 -05:00
}
}
}
2023-11-17 11:48:45 -06:00
private ExtractionResult DownloadAndExtract ( Game game , string destination )
2023-11-10 00:29:16 -06:00
{
if ( game = = null )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Game failed to download, no game was specified" ) ;
2023-11-10 00:29:16 -06:00
throw new ArgumentNullException ( "No game was specified" ) ;
}
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Downloading and extracting {Game} to path {Destination}" , game . Title , destination ) ;
2023-11-10 00:29:16 -06:00
2023-11-12 01:04:05 -06:00
var extractionResult = new ExtractionResult
{
Canceled = false ,
} ;
2024-01-09 20:43:54 -06:00
var fileManifest = new StringBuilder ( ) ;
2025-05-18 08:31:56 +02:00
var files = new List < ExtractionResult . FileEntry > ( ) ;
2024-01-09 20:43:54 -06:00
2023-11-10 00:29:16 -06:00
try
{
Directory . CreateDirectory ( destination ) ;
2025-08-18 02:47:09 -05:00
_transferStream = Stream ( game . Id ) ;
_reader = ReaderFactory . Open ( _transferStream ) ;
2023-11-12 01:04:05 -06:00
2025-08-18 02:47:09 -05:00
using ( var monitor = new FileTransferMonitor ( _transferStream . Length ) )
2023-11-10 00:29:16 -06:00
{
2025-08-18 02:47:09 -05:00
_transferStream . OnProgress + = ( pos , len ) = >
2024-08-07 17:39:55 -05:00
{
2025-02-04 02:34:31 -06:00
if ( monitor . CanUpdate ( ) )
{
monitor . Update ( pos ) ;
2025-02-16 15:48:34 -06:00
_installProgress . BytesTransferred = monitor . GetBytesTransferred ( ) ;
2025-02-04 02:34:31 -06:00
_installProgress . TotalBytes = len ;
_installProgress . TransferSpeed = monitor . GetSpeed ( ) ;
_installProgress . TimeRemaining = monitor . GetTimeRemaining ( ) ;
OnInstallProgressUpdate ? . Invoke ( _installProgress ) ;
}
} ;
}
2023-11-10 00:29:16 -06:00
2025-08-18 02:47:09 -05:00
_reader . EntryExtractionProgress + = ( sender , e ) = >
2023-11-12 01:04:05 -06:00
{
2024-08-07 01:14:13 -05:00
// Do we need this granular of control? If so, invocations should be rate limited
2023-11-12 01:04:05 -06:00
OnArchiveEntryExtractionProgress ? . Invoke ( this , new ArchiveEntryExtractionProgressArgs
2023-11-10 00:29:16 -06:00
{
2023-11-12 01:04:05 -06:00
Entry = e . Item ,
Progress = e . ReaderProgress ,
2024-01-20 21:02:41 -06:00
Game = game ,
2023-11-12 01:04:05 -06:00
} ) ;
} ;
2025-08-18 02:47:09 -05:00
while ( _reader . MoveToNextEntry ( ) )
2023-11-12 01:04:05 -06:00
{
2025-08-18 02:47:09 -05:00
if ( _reader . Cancelled )
2023-11-12 01:04:05 -06:00
break ;
2024-02-22 02:12:18 -06:00
try
{
2025-08-18 02:47:09 -05:00
var localFile = Path . Combine ( destination , _reader . Entry . Key ) ;
2024-01-10 17:57:00 -06:00
2024-02-22 02:12:18 -06:00
uint crc = 0 ;
2024-01-09 20:43:54 -06:00
2024-02-22 02:12:18 -06:00
if ( File . Exists ( localFile ) )
2024-01-10 17:57:00 -06:00
{
2024-02-22 02:12:18 -06:00
using ( FileStream fs = File . Open ( localFile , FileMode . Open ) )
2024-01-10 17:57:00 -06:00
{
2024-02-22 02:12:18 -06:00
var buffer = new byte [ 65536 ] ;
while ( true )
{
var count = fs . Read ( buffer , 0 , buffer . Length ) ;
2024-01-10 17:57:00 -06:00
2024-02-22 02:12:18 -06:00
if ( count = = 0 )
break ;
2024-01-10 17:57:00 -06:00
2024-02-22 02:12:18 -06:00
crc = Crc32Algorithm . Append ( crc , buffer , 0 , count ) ;
}
2024-01-10 17:57:00 -06:00
}
}
2024-02-22 02:12:18 -06:00
2025-08-18 02:47:09 -05:00
fileManifest . AppendLine ( $"{_reader.Entry.Key} | {_reader.Entry.Crc.ToString(" X ")}" ) ;
2025-05-18 08:31:56 +02:00
files . Add ( new ExtractionResult . FileEntry
{
2025-08-18 02:47:09 -05:00
EntryPath = _reader . Entry . Key ,
2025-05-18 08:31:56 +02:00
LocalPath = localFile ,
} ) ;
2024-02-22 02:12:18 -06:00
2025-08-18 02:47:09 -05:00
if ( crc = = 0 | | crc ! = _reader . Entry . Crc )
_reader . WriteEntryToDirectory ( destination , new ExtractionOptions ( )
2024-02-22 02:12:18 -06:00
{
ExtractFullPath = true ,
Overwrite = true ,
PreserveFileTime = true
} ) ;
else // Skip to next entry
2024-06-26 18:05:22 -05:00
try
{
2025-08-18 02:47:09 -05:00
_reader . OpenEntryStream ( ) . Dispose ( ) ;
2024-06-26 18:05:22 -05:00
}
2025-08-18 02:44:55 -05:00
catch
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( "Could not skip to next entry in archive" ) ;
2025-08-18 02:44:55 -05:00
}
2024-01-10 17:57:00 -06:00
}
2024-02-22 02:12:18 -06:00
catch ( IOException ex )
{
var errorCode = ex . HResult & 0xFFFF ;
2024-01-10 17:57:00 -06:00
2024-02-22 02:12:18 -06:00
if ( errorCode = = 87 )
throw ex ;
else
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Not replacing existing file/folder on disk: {Message}" , ex . Message ) ;
2024-01-10 17:57:00 -06:00
2024-02-22 02:12:18 -06:00
// Skip to next entry
2025-08-18 02:47:09 -05:00
_reader . OpenEntryStream ( ) . Dispose ( ) ;
2024-02-22 02:12:18 -06:00
}
2023-11-10 00:29:16 -06:00
}
2023-11-12 01:04:05 -06:00
2025-08-18 02:47:09 -05:00
_reader . Dispose ( ) ;
_transferStream . Dispose ( ) ;
2023-11-10 00:29:16 -06:00
}
2023-11-28 21:20:07 -06:00
catch ( ReaderCancelledException ex )
2023-11-10 00:29:16 -06:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( ex , "User cancelled the download" ) ;
2023-11-12 01:04:05 -06:00
2023-11-28 21:20:07 -06:00
extractionResult . Canceled = true ;
2023-11-12 01:04:05 -06:00
2023-11-28 21:20:07 -06:00
if ( Directory . Exists ( destination ) )
2023-11-10 00:29:16 -06:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Cleaning up orphaned files after cancelled install" ) ;
2023-11-10 00:29:16 -06:00
2023-11-28 21:20:07 -06:00
Directory . Delete ( destination , true ) ;
}
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not extract to path {Destination}" , destination ) ;
2023-11-10 00:29:16 -06:00
2023-11-28 21:20:07 -06:00
if ( Directory . Exists ( destination ) )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Cleaning up orphaned install files after bad install" ) ;
2023-11-10 00:29:16 -06:00
2023-11-28 21:20:07 -06:00
Directory . Delete ( destination , true ) ;
2023-11-10 00:29:16 -06:00
}
2023-11-28 21:20:07 -06:00
throw new Exception ( "The game archive could not be extracted, is it corrupted? Please try again" ) ;
2023-11-10 00:29:16 -06:00
}
if ( ! extractionResult . Canceled )
{
extractionResult . Success = true ;
extractionResult . Directory = destination ;
2025-05-18 08:31:56 +02:00
extractionResult . Files = files ;
2023-11-10 00:29:16 -06:00
2024-01-09 20:43:54 -06:00
var fileListDestination = Path . Combine ( destination , ".lancommander" , game . Id . ToString ( ) , "FileList.txt" ) ;
if ( ! Directory . Exists ( Path . GetDirectoryName ( fileListDestination ) ) )
Directory . CreateDirectory ( Path . GetDirectoryName ( fileListDestination ) ) ;
File . WriteAllText ( fileListDestination , fileManifest . ToString ( ) ) ;
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Game {Game} successfully downloaded and extracted to {Destination}" , game . Title , destination ) ;
2023-11-10 00:29:16 -06:00
}
return extractionResult ;
}
2023-11-12 01:04:05 -06:00
2024-11-12 02:17:26 -06:00
public async Task < string > GetInstallDirectory ( Game game , string installDirectory )
2024-01-08 18:52:00 -06:00
{
2024-10-04 23:37:49 -05:00
if ( string . IsNullOrWhiteSpace ( installDirectory ) )
2025-08-18 03:02:49 -05:00
installDirectory = _client . DefaultInstallDirectory ;
2024-09-12 19:09:13 -05:00
2024-11-12 02:17:26 -06:00
if ( ( game . Type = = GameType . Expansion | | game . Type = = GameType . Mod | | game . Type = = GameType . StandaloneMod ) & & game . BaseGameId ! = Guid . Empty )
{
2025-05-15 02:40:34 +02:00
// modify installation passes the original installation of the game including the game folder, use the existing folder,
// otherwise a name change could lead to installing files into differnt folder
if ( Path . Exists ( installDirectory ) & & Path . Exists ( Path . Combine ( installDirectory , ".lancommander" ) ) )
{
return installDirectory ;
}
else
{
2025-08-18 03:02:49 -05:00
var baseGame = await _client . Games . GetAsync ( game . BaseGameId ) ;
2024-11-12 02:17:26 -06:00
2025-05-15 02:40:34 +02:00
return await GetInstallDirectory ( baseGame , installDirectory ) ;
}
2024-11-12 02:17:26 -06:00
}
2024-01-10 01:25:46 -06:00
else
2024-09-12 19:09:13 -05:00
return Path . Combine ( installDirectory , game . Title . SanitizeFilename ( ) ) ;
2024-01-08 18:52:00 -06:00
}
2023-11-12 01:04:05 -06:00
public void CancelInstall ( )
{
2025-08-18 02:47:09 -05:00
_reader ? . Cancel ( ) ;
2023-11-12 01:04:05 -06:00
}
2024-01-10 01:59:51 -06:00
2025-08-18 02:44:55 -05:00
public async Task < ICollection < GameManifest > > ReadManifestsAsync ( string installDirectory , Guid gameId )
2024-08-26 20:18:54 -05:00
{
var manifests = new List < GameManifest > ( ) ;
2025-01-30 23:57:12 -06:00
var mainManifest = await ManifestHelper . ReadAsync < GameManifest > ( installDirectory , gameId ) ;
2024-08-26 20:18:54 -05:00
if ( mainManifest = = null )
return manifests ;
manifests . Add ( mainManifest ) ;
if ( mainManifest . DependentGames ! = null )
{
foreach ( var dependentGameId in mainManifest . DependentGames )
{
try
{
2025-01-30 23:57:12 -06:00
var dependentGameManifest = await ManifestHelper . ReadAsync < GameManifest > ( installDirectory , dependentGameId ) ;
2024-08-26 20:18:54 -05:00
if ( dependentGameManifest . Type = = GameType . Expansion | | dependentGameManifest . Type = = GameType . Mod )
manifests . Add ( dependentGameManifest ) ;
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not load manifest from dependent game {DependentGameId}" , dependentGameId ) ;
2024-08-26 20:18:54 -05:00
}
}
}
return manifests ;
}
2025-05-18 03:09:22 +02:00
/// <summary>
/// Retrieves the archive entries of the current game installation from the server for the specified game
/// </summary>
/// <param name="gameId">The unique identifier of the game.</param>
/// <param name="manifest">The manifest containing metadata of the game's installation.</param>
/// <returns>
/// A collection of <see cref="ArchiveEntry"/> representing the archive entries.
/// Returns an empty list if no entries are found.
/// </returns>
/// <exception cref="Exception">
/// Thrown if the request to retrieve archive entries encounters an error.
/// </exception>
protected async Task < IEnumerable < ArchiveEntry > > GetGameInstallationArchiveEntries ( Guid gameId , GameManifest manifest )
{
2025-08-18 03:02:49 -05:00
var entries = await _client . GetRequestAsync < IEnumerable < ArchiveEntry > > ( $"/api/Archives/Contents/{manifest.Id}/{manifest.Version}" ) ;
2025-05-18 03:09:22 +02:00
return entries ? ? [ ] ;
}
/// <summary>
/// Retrieves the archive entries for a game installation, including its base game and dependencies.
/// </summary>
/// <param name="installDirectory">The directory where the game is installed.</param>
/// <param name="gameId">The unique identifier of the game.</param>
/// <returns>
/// An instance of <see cref="GameInstallationArchiveEntries"/> containing archive entries
/// for the base game and any dependent games.
/// </returns>
protected async Task < GameInstallationArchiveEntries > GetGameInstallationArchivesEntries ( string installDirectory , Guid gameId )
{
var gameArchives = new GameInstallationArchiveEntries ( ) ;
var manifests = await GetManifestsAsync ( installDirectory , gameId ) ;
if ( manifests = = null | | ! manifests . Any ( ) )
return gameArchives ;
// Retrieves and processes the base game manifest and its archive entries.
var baseManifest = gameArchives . BaseGame . Manifest = manifests . FirstOrDefault ( mf = > mf . Type . ValueIsIn ( GameType . MainGame , GameType . StandaloneExpansion , GameType . StandaloneMod ) ) ;
if ( baseManifest ! = null )
{
var entries = await GetGameInstallationArchiveEntries ( gameId , baseManifest ) ;
gameArchives . BaseGame . Entries . AddRange ( entries ) ;
2025-08-18 02:44:55 -05:00
manifests = manifests . Except ( [ baseManifest ] ) . ToList ( ) ;
2025-05-18 03:09:22 +02:00
2025-08-18 03:02:49 -05:00
var savePathEntries = baseManifest . SavePaths ? . SelectMany ( p = > _client . Saves . GetFileSavePathEntries ( p , installDirectory ) ) . ToList ( ) ? ? [ ] ;
2025-05-18 03:09:22 +02:00
gameArchives . BaseGame . SavePaths = savePathEntries ;
}
// Processes dependent game manifests and their corresponding archive entries.
foreach ( var depManifest in manifests ? ? [ ] )
{
var depEntries = await GetGameInstallationArchiveEntries ( gameId , depManifest ) ;
if ( ! gameArchives . DependentGames . TryGetValue ( depManifest . Id , out var depArchiveInfo ) )
{
depArchiveInfo = new ( ) ;
gameArchives . DependentGames . Add ( depManifest . Id , depArchiveInfo ) ;
}
depArchiveInfo . Manifest = depManifest ;
depArchiveInfo . Entries . AddRange ( depEntries ) ;
2025-08-18 03:02:49 -05:00
var savePathEntries = depManifest . SavePaths ? . SelectMany ( p = > _client . Saves . GetFileSavePathEntries ( p , installDirectory ) ) . ToList ( ) ? ? [ ] ;
2025-05-18 03:09:22 +02:00
depArchiveInfo . SavePaths = savePathEntries ;
}
return gameArchives ;
}
2024-09-05 00:48:04 -05:00
public async Task RunAsync ( string installDirectory , Guid gameId , Models . Action action , DateTime ? lastRun , string args = "" )
2024-08-26 20:18:54 -05:00
{
var screen = DisplayHelper . GetScreen ( ) ;
2025-08-18 03:02:49 -05:00
using ( var context = new ProcessExecutionContext ( _client , _logger ) )
2024-08-26 20:18:54 -05:00
{
2025-08-18 03:02:49 -05:00
context . AddVariable ( "ServerAddress" , _client . GetServerAddress ( ) ) ;
2025-01-15 02:28:11 -06:00
2025-08-18 02:37:52 -05:00
try
{
context . AddVariable ( "DisplayWidth" , screen . Width . ToString ( ) ) ;
context . AddVariable ( "DisplayHeight" , screen . Height . ToString ( ) ) ;
context . AddVariable ( "DisplayRefreshRate" , screen . RefreshRate . ToString ( ) ) ;
context . AddVariable ( "DisplayBitDepth" , screen . BitsPerPixel . ToString ( ) ) ;
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not get display information for execution context variables" ) ;
2025-08-18 02:37:52 -05:00
}
try
{
2025-08-18 03:02:49 -05:00
if ( _client . IsConnected ( ) & & ! String . IsNullOrWhiteSpace ( _client . Settings . IPXRelayHost ) )
2025-08-18 02:37:52 -05:00
{
2025-08-18 03:02:49 -05:00
context . AddVariable ( "IPXRelayHost" , await _client . GetIPXRelayHostAsync ( ) ) ;
context . AddVariable ( "IPXRelayPort" , _client . Settings . IPXRelayPort . ToString ( ) ) ;
2025-08-18 02:37:52 -05:00
}
}
catch ( Exception ex )
2025-01-15 02:28:11 -06:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not connect to IPXRelay host" ) ;
2025-01-15 02:28:11 -06:00
}
2024-08-26 20:18:54 -05:00
#region Run Scripts
var manifests = await ReadManifestsAsync ( installDirectory , gameId ) ;
foreach ( var manifest in manifests )
{
//manifest.Actions
var currentGamePlayerAlias = await GetPlayerAliasAsync ( installDirectory , manifest . Id ) ;
var currentGameKey = await GetCurrentKeyAsync ( installDirectory , manifest . Id ) ;
#region Check Game ' s Player Name
2025-08-18 03:02:49 -05:00
if ( _client . IsConnected ( ) )
2025-01-15 02:28:11 -06:00
{
2025-08-18 03:02:49 -05:00
var alias = await _client . Profile . GetAliasAsync ( ) ;
2025-01-30 23:57:12 -06:00
2025-01-15 02:28:11 -06:00
if ( currentGamePlayerAlias ! = alias )
2025-01-30 23:57:12 -06:00
{
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunNameChangeScriptAsync ( installDirectory , gameId , alias ) ;
2025-01-30 23:57:12 -06:00
if ( manifest . Redistributables ! = null )
{
foreach ( var redistributable in manifest . Redistributables . Where ( r = > r . Scripts ! = null ) )
{
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunNameChangeScriptAsync ( installDirectory , gameId , redistributable . Id , alias ) ;
2025-01-30 23:57:12 -06:00
}
}
}
2025-01-15 02:28:11 -06:00
}
2024-08-26 20:18:54 -05:00
#endregion
#region Check Key Allocation
2025-08-18 03:02:49 -05:00
if ( _client . IsConnected ( ) )
2024-08-26 20:18:54 -05:00
{
2025-08-18 03:02:49 -05:00
var newKey = await _client . Games . GetAllocatedKeyAsync ( manifest . Id ) ;
2024-08-26 20:18:54 -05:00
if ( currentGameKey ! = newKey )
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunKeyChangeScriptAsync ( installDirectory , manifest . Id , newKey ) ;
2024-08-26 20:18:54 -05:00
}
#endregion
#region Download Latest Saves
2025-08-18 03:02:49 -05:00
if ( _client . IsConnected ( ) )
2024-08-26 20:18:54 -05:00
{
2024-09-27 18:07:14 -05:00
await RetryHelper . RetryOnExceptionAsync ( 10 , TimeSpan . FromSeconds ( 1 ) , false , async ( ) = >
2024-08-26 20:18:54 -05:00
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Attempting to download save" ) ;
2024-09-27 18:07:14 -05:00
2025-08-18 03:02:49 -05:00
var latestSave = await _client . Saves . GetLatestAsync ( manifest . Id ) ;
2024-08-26 20:18:54 -05:00
if ( latestSave ! = null & & ( latestSave . CreatedOn > lastRun | | lastRun = = null ) )
2025-08-18 03:02:49 -05:00
await _client . Saves . DownloadAsync ( installDirectory , manifest . Id ) ;
2024-09-27 18:07:14 -05:00
return true ;
} ) ;
2024-08-26 20:18:54 -05:00
}
#endregion
#region Run Before Start Script
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunBeforeStartScriptAsync ( installDirectory , manifest . Id ) ;
2025-01-30 23:57:12 -06:00
if ( manifest . Redistributables ! = null )
{
foreach ( var redistributable in manifest . Redistributables . Where ( r = > r . Scripts ! = null ) )
{
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunBeforeStartScriptAsync ( installDirectory , gameId , redistributable . Id ) ;
2025-01-30 23:57:12 -06:00
}
}
2024-08-26 20:18:54 -05:00
#endregion
}
#endregion
try
{
2024-10-29 01:27:51 -05:00
var cancellationTokenSource = new CancellationTokenSource ( ) ;
2025-01-29 20:35:42 -06:00
var task = context . ExecuteGameActionAsync ( installDirectory , gameId , action , "" , cancellationTokenSource . Token ) ;
2024-08-26 20:18:54 -05:00
2025-08-18 02:47:09 -05:00
_running [ gameId ] = cancellationTokenSource ;
2024-08-26 20:18:54 -05:00
await task ;
2024-10-29 18:12:14 -05:00
2025-08-18 02:47:09 -05:00
_running . Remove ( gameId ) ;
2025-08-18 02:38:21 -05:00
await UploadSavesAsync ( manifests , installDirectory ) ;
2024-08-26 20:18:54 -05:00
}
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Game failed to run" ) ;
2024-08-26 20:18:54 -05:00
}
foreach ( var manifest in manifests )
{
2025-01-30 23:57:12 -06:00
#region Run After Stop Script
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunAfterStopScriptAsync ( installDirectory , gameId ) ;
2025-01-30 23:57:12 -06:00
if ( manifest . Redistributables ! = null )
{
foreach ( var redistributable in manifest . Redistributables . Where ( r = > r . Scripts ! = null ) )
{
2025-08-18 03:02:49 -05:00
await _client . Scripts . RunAfterStopScriptAsync ( installDirectory , gameId , redistributable . Id ) ;
2025-01-30 23:57:12 -06:00
}
}
#endregion
2024-08-26 20:18:54 -05:00
}
}
}
2025-08-18 02:38:21 -05:00
private async Task UploadSavesAsync ( ICollection < GameManifest > manifests , string installDirectory )
{
2025-08-18 03:02:49 -05:00
if ( _client . IsConnected ( ) )
2025-08-18 02:38:21 -05:00
{
foreach ( var manifest in manifests )
{
await RetryHelper . RetryOnExceptionAsync ( 10 , TimeSpan . FromSeconds ( 1 ) , false , async ( ) = >
{
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Attempting to upload save" ) ;
2025-08-18 02:38:21 -05:00
2025-08-18 03:02:49 -05:00
await _client . Saves . UploadAsync ( installDirectory , manifest . Id ) ;
2025-08-18 02:38:21 -05:00
return true ;
} ) ;
}
}
}
2024-08-26 20:18:54 -05:00
public async Task Stop ( Guid gameId )
{
2025-08-18 02:47:09 -05:00
if ( _running . ContainsKey ( gameId ) )
2024-08-26 20:18:54 -05:00
{
2025-08-18 02:47:09 -05:00
await _running [ gameId ] . CancelAsync ( ) ;
2024-08-26 20:18:54 -05:00
2025-08-18 02:47:09 -05:00
_running . Remove ( gameId ) ;
2024-10-29 01:27:51 -05:00
}
2024-08-26 20:18:54 -05:00
}
public bool IsRunning ( Guid gameId )
{
2025-08-18 02:47:09 -05:00
if ( ! _running . ContainsKey ( gameId ) )
2024-08-26 20:18:54 -05:00
return false ;
2025-08-18 02:47:09 -05:00
return ! _running [ gameId ] . IsCancellationRequested ;
2024-08-26 20:18:54 -05:00
}
2024-08-12 19:33:26 -05:00
public async Task ImportAsync ( string archivePath )
{
using ( var fs = new FileStream ( archivePath , FileMode . Open , FileAccess . Read ) )
{
2025-08-18 03:02:49 -05:00
var objectKey = await _client . ChunkedUploadRequestAsync ( "" , fs ) ;
2024-08-12 19:33:26 -05:00
if ( objectKey ! = Guid . Empty )
2025-08-18 03:02:49 -05:00
await _client . PostRequestAsync < object > ( $"/api/Games/Import/{objectKey}" ) ;
2024-08-12 19:33:26 -05:00
}
}
2024-08-13 17:42:43 -05:00
public async Task ExportAsync ( string destinationPath , Guid gameId )
{
2025-08-18 03:02:49 -05:00
await _client . DownloadRequestAsync ( $"/Games/{gameId}/Export/Full" , destinationPath ) ;
2024-08-13 17:42:43 -05:00
}
2024-10-01 17:56:58 -05:00
public async Task UploadArchiveAsync ( string archivePath , Guid gameId , string version , string changelog = "" )
{
using ( var fs = new FileStream ( archivePath , FileMode . Open , FileAccess . Read ) )
{
2025-08-18 03:02:49 -05:00
var objectKey = await _client . ChunkedUploadRequestAsync ( "" , fs ) ;
2024-10-01 17:56:58 -05:00
if ( objectKey ! = Guid . Empty )
2025-08-18 03:02:49 -05:00
await _client . PostRequestAsync < object > ( $"/api/Games/UploadArchive" , new UploadArchiveRequest
2024-10-01 17:56:58 -05:00
{
Id = gameId ,
ObjectKey = objectKey ,
Version = version ,
Changelog = changelog ,
} ) ;
}
}
2024-11-04 17:31:37 -06:00
/// <summary>
/// Get the archive associated with the installed version of the game and return any non-matching files in the current install.
/// </summary>
/// <param name="installDirectory">The game's install directory</param>
/// <param name="gameId">The game's ID</param>
/// <returns>List of file conflicts</returns>
2024-11-14 20:53:08 -06:00
public async Task < IEnumerable < ArchiveValidationConflict > > ValidateFilesAsync ( string installDirectory , Guid gameId )
2024-11-04 17:31:37 -06:00
{
2025-05-18 03:09:22 +02:00
var archives = await GetGameInstallationArchivesEntries ( installDirectory , gameId ) ;
var manifest = archives ? . BaseGame ? . Entries ;
var entries = archives ? . BaseGame ? . Entries ? . ToList ( ) ? ? [ ] ;
foreach ( ( var dependentGameId , var dependentGameInfo ) in archives ? . DependentGames ? ? [ ] )
{
foreach ( var depArchive in dependentGameInfo . Entries ? ? [ ] )
{
if ( depArchive . FullName . EndsWith ( '/' ) )
continue ;
var archiveIndex = entries . FindLastIndex ( archive = > string . Equals ( archive . FullName , depArchive . FullName ) ) ;
if ( archiveIndex < 0 )
{
entries . Add ( depArchive ) ;
continue ;
}
entries [ archiveIndex ] = depArchive ;
}
}
// lookup for dependent games
var lookupEntry = archives ? . DependentGames ?
. SelectMany ( dep = > dep . Value ? . Entries ? . Select ( entry = > new { GameId = ( Guid ? ) dep . Key , ArchiveEntry = entry } ) ? ? [ ] )
. ToLookup ( tentry = > tentry . ArchiveEntry , tentry = > tentry . GameId ) ? ? Enumerable . Empty < Guid ? > ( ) . ToLookup ( x = > default ( ArchiveEntry ) ) ;
2024-11-04 17:31:37 -06:00
2024-11-14 20:53:08 -06:00
var conflictedEntries = new List < ArchiveValidationConflict > ( ) ;
2025-05-18 03:09:22 +02:00
var savePathEntries = archives ? . BaseGame ? . SavePaths . ToList ( ) ? ? [ ] ;
var depSavePathEntries = archives ? . DependentGames ? . SelectMany ( dep = > dep . Value ? . SavePaths ? ? [ ] ) . ToList ( ) ? ? [ ] ;
savePathEntries . AddRange ( depSavePathEntries ) ;
2024-11-04 17:31:37 -06:00
foreach ( var entry in entries )
{
2024-11-14 20:53:08 -06:00
if ( savePathEntries . Any ( e = > e . ArchivePath . Equals ( entry . FullName , StringComparison . OrdinalIgnoreCase ) ) )
continue ;
if ( entry . FullName . EndsWith ( '/' ) )
continue ;
2024-11-04 17:31:37 -06:00
var localFile = Path . Combine ( installDirectory , entry . FullName . Replace ( '/' , Path . DirectorySeparatorChar ) ) ;
if ( ! Path . Exists ( localFile ) )
2024-11-14 20:53:08 -06:00
conflictedEntries . Add ( new ArchiveValidationConflict
{
2025-05-18 03:09:22 +02:00
GameId = lookupEntry [ entry ] ? . FirstOrDefault ( ) ? ? gameId ,
2024-11-14 20:53:08 -06:00
Name = entry . Name ,
FullName = entry . FullName ,
Crc32 = entry . Crc32 ,
Length = entry . Length ,
} ) ;
2024-11-04 17:31:37 -06:00
else
{
uint crc = 0 ;
if ( File . Exists ( localFile ) )
{
using ( FileStream fs = File . Open ( localFile , FileMode . Open ) )
{
var buffer = new byte [ 65536 ] ;
while ( true )
{
var count = fs . Read ( buffer , 0 , buffer . Length ) ;
if ( count = = 0 )
break ;
crc = Crc32Algorithm . Append ( crc , buffer , 0 , count ) ;
}
}
}
if ( crc = = 0 | | crc ! = entry . Crc32 )
2024-11-14 20:53:08 -06:00
conflictedEntries . Add ( new ArchiveValidationConflict
{
2025-05-18 03:09:22 +02:00
GameId = lookupEntry [ entry ] ? . FirstOrDefault ( ) ? ? gameId ,
2024-11-14 20:53:08 -06:00
Name = entry . Name ,
FullName = entry . FullName ,
Crc32 = entry . Crc32 ,
LocalFileInfo = new FileInfo ( localFile )
} ) ;
2024-11-04 17:31:37 -06:00
}
}
return conflictedEntries ;
}
2025-05-18 03:09:22 +02:00
/// <summary>
/// Downloads the specified files for multiple games (base game, mods, expansions).
/// </summary>
/// <param name="installDirectory">The directory where the games are installed.</param>
/// <param name="entries">
/// A collection of tuples containing the game ID and the corresponding file path.
/// </param>
public async Task DownloadFilesAsync ( string installDirectory , IEnumerable < ( Guid GameId , string FilePath ) > entries )
{
var groups = entries . GroupBy ( x = > x . GameId ) ;
foreach ( var group in groups )
{
2025-08-18 02:44:55 -05:00
await DownloadFilesAsync ( installDirectory , group . Key , group . Select ( x = > x . FilePath ) . ToList ( ) ) ;
2025-05-18 03:09:22 +02:00
}
}
/// <summary>
/// Downloads the specified files for a single game.
/// </summary>
/// <param name="installDirectory">The directory where the game is installed.</param>
/// <param name="gameId">The unique identifier of the game.</param>
/// <param name="entries">A collection of file paths to download.</param>
2025-08-18 02:44:55 -05:00
public async Task DownloadFilesAsync ( string installDirectory , Guid gameId , ICollection < string > entries )
2024-11-04 17:31:37 -06:00
{
2025-01-30 23:57:12 -06:00
var manifest = await ManifestHelper . ReadAsync < GameManifest > ( installDirectory , gameId ) ;
2025-08-18 03:02:49 -05:00
var archive = await _client . GetRequestAsync < Archive > ( $"/api/Archives/ByVersion/{manifest.Version}" ) ;
2024-11-04 17:31:37 -06:00
await Task . Run ( ( ) = >
{
try
{
2025-08-18 02:47:09 -05:00
_transferStream = Stream ( gameId ) ;
_reader = ReaderFactory . Open ( _transferStream ) ;
2024-11-04 17:31:37 -06:00
2025-08-18 02:47:09 -05:00
while ( _reader . MoveToNextEntry ( ) )
2024-11-04 17:31:37 -06:00
{
2025-08-18 02:47:09 -05:00
if ( _reader . Cancelled )
2024-11-04 17:31:37 -06:00
break ;
try
{
2025-08-18 02:47:09 -05:00
if ( entries . Contains ( _reader . Entry . Key ) )
2024-11-04 17:31:37 -06:00
{
2025-08-18 02:47:09 -05:00
var destination = Path . Combine ( installDirectory , _reader . Entry . Key ? . Replace ( '/' , Path . DirectorySeparatorChar ) ? ? string . Empty ) ;
2024-11-04 17:31:37 -06:00
2025-08-18 02:47:09 -05:00
_reader . WriteEntryToFile ( destination , new ExtractionOptions
2024-11-04 17:31:37 -06:00
{
Overwrite = true ,
PreserveFileTime = true ,
} ) ;
}
else // Skip to next entry
try
{
2025-08-18 02:47:09 -05:00
_reader . OpenEntryStream ( ) . Dispose ( ) ;
2024-11-04 17:31:37 -06:00
}
2025-08-18 02:44:55 -05:00
catch ( Exception ex )
{
2025-08-18 02:47:09 -05:00
_logger ? . LogError ( ex , "Could not skip to the next entry in the archive" ) ;
2025-08-18 02:44:55 -05:00
}
2024-11-04 17:31:37 -06:00
}
catch ( IOException ex )
{
var errorCode = ex . HResult & 0xFFFF ;
if ( errorCode = = 87 )
2025-08-18 02:44:55 -05:00
throw ;
2024-11-04 17:31:37 -06:00
else
2025-08-18 02:47:09 -05:00
_logger ? . LogTrace ( "Not replacing existing file/folder on disk: {Message}" , ex . Message ) ;
2024-11-04 17:31:37 -06:00
// Skip to next entry
2025-08-18 02:47:09 -05:00
_reader . OpenEntryStream ( ) . Dispose ( ) ;
2024-11-04 17:31:37 -06:00
}
}
2025-08-18 02:47:09 -05:00
_reader . Dispose ( ) ;
_transferStream . Dispose ( ) ;
2024-11-04 17:31:37 -06:00
}
catch ( Exception ex )
{
throw new Exception ( "The game archive could not be extracted, is it corrupted? Please try again" ) ;
}
} ) ;
}
2025-05-18 08:31:56 +02:00
public Task RestoreFilesAsync ( string installDirectory , Guid gameId , GameInstallationFileList fileListRemoved , GameInstallationFileList fileListAdded )
{
var listRemoved = fileListRemoved ? . ToFlatDistinctFileEntries ( ) ? ? [ ] ;
var listAdded = fileListAdded ? . ToFlatDistinctFileEntries ( ) ? ? [ ] ;
var uniqueList = listRemoved . ExceptBy ( listAdded . Select ( x = > x . EntryPath ) , x = > x . EntryPath , StringComparer . OrdinalIgnoreCase ) ;
var possibleRestoreEntries = uniqueList . Select ( x = > x . EntryPath ) . ToArray ( ) ;
return RestoreFilesAsync ( installDirectory , gameId , possibleRestoreEntries ) ;
}
/// <summary>
/// Restores invalidated files matching the specified files.
/// </summary>
/// <param name="installDirectory">The directory where the game is installed.</param>
/// <param name="gameId">The unique identifier of the game.</param>
/// <param name="entries">A collection of file paths to check and compare with invalidated files.</param>
public async Task RestoreFilesAsync ( string installDirectory , Guid gameId , IEnumerable < string > entries )
{
// early out if no files were removed which would require checking
if ( entries = = null | | ! entries . Any ( ) )
return ;
// validate files, which takes addons into account
var conflicts = await ValidateFilesAsync ( installDirectory , gameId ) ? ? [ ] ;
// build list of files to download by matching up removed files with conflicting files, split by game/addon
var downloadEntries = conflicts
. IntersectBy ( entries , x = > x . FullName , StringComparer . OrdinalIgnoreCase )
. Select ( x = > ( x . GameId ? ? gameId , x . FullName ) ) . ToArray ( ) ;
await DownloadFilesAsync ( installDirectory , downloadEntries ) ;
}
2024-01-10 01:59:51 -06:00
public static string GetMetadataDirectoryPath ( string installDirectory , Guid gameId )
{
2024-10-04 23:37:49 -05:00
if ( string . IsNullOrWhiteSpace ( installDirectory ) )
2024-08-08 18:28:33 -05:00
return "" ;
2024-01-10 01:59:51 -06:00
return Path . Combine ( installDirectory , ".lancommander" , gameId . ToString ( ) ) ;
}
public static string GetMetadataFilePath ( string installDirectory , Guid gameId , string fileName )
{
return Path . Combine ( GetMetadataDirectoryPath ( installDirectory , gameId ) , fileName ) ;
}
2024-01-19 00:25:38 -06:00
public static string GetPlayerAlias ( string installDirectory , Guid gameId )
{
2024-10-04 23:37:49 -05:00
var aliasFilePath = GetMetadataFilePath ( installDirectory , gameId , PlayerAliasFilename ) ;
2024-01-19 00:25:38 -06:00
if ( File . Exists ( aliasFilePath ) )
return File . ReadAllText ( aliasFilePath ) ;
else
2024-10-04 23:37:49 -05:00
return string . Empty ;
2024-01-19 00:25:38 -06:00
}
2024-08-26 20:18:54 -05:00
public static async Task < string > GetPlayerAliasAsync ( string installDirectory , Guid gameId )
{
2024-10-04 23:37:49 -05:00
var aliasFilePath = GetMetadataFilePath ( installDirectory , gameId , PlayerAliasFilename ) ;
2024-08-26 20:18:54 -05:00
if ( File . Exists ( aliasFilePath ) )
return await File . ReadAllTextAsync ( aliasFilePath ) ;
else
2024-10-04 23:37:49 -05:00
return string . Empty ;
2024-08-26 20:18:54 -05:00
}
2024-01-19 00:25:38 -06:00
public static void UpdatePlayerAlias ( string installDirectory , Guid gameId , string newName )
{
2024-10-04 23:37:49 -05:00
File . WriteAllText ( GetMetadataFilePath ( installDirectory , gameId , PlayerAliasFilename ) , newName ) ;
2024-01-19 00:25:38 -06:00
}
2024-02-03 18:32:42 -06:00
2024-08-26 20:18:54 -05:00
public static async Task UpdatePlayerAliasAsync ( string installDirectory , Guid gameId , string newName )
{
2024-10-04 23:37:49 -05:00
await File . WriteAllTextAsync ( GetMetadataFilePath ( installDirectory , gameId , PlayerAliasFilename ) , newName ) ;
2024-08-26 20:18:54 -05:00
}
2024-02-03 18:32:42 -06:00
public static string GetCurrentKey ( string installDirectory , Guid gameId )
{
2024-10-04 23:37:49 -05:00
var keyFilePath = GetMetadataFilePath ( installDirectory , gameId , KeyFilename ) ;
2024-02-03 18:32:42 -06:00
if ( File . Exists ( keyFilePath ) )
return File . ReadAllText ( keyFilePath ) ;
else
2024-10-04 23:37:49 -05:00
return string . Empty ;
2024-02-03 18:32:42 -06:00
}
2024-08-26 20:18:54 -05:00
public static async Task < string > GetCurrentKeyAsync ( string installDirectory , Guid gameId )
{
2024-10-04 23:37:49 -05:00
var keyFilePath = GetMetadataFilePath ( installDirectory , gameId , KeyFilename ) ;
2024-08-26 20:18:54 -05:00
if ( File . Exists ( keyFilePath ) )
return await File . ReadAllTextAsync ( keyFilePath ) ;
else
2024-10-04 23:37:49 -05:00
return string . Empty ;
2024-08-26 20:18:54 -05:00
}
2024-02-03 18:32:42 -06:00
public static void UpdateCurrentKey ( string installDirectory , Guid gameId , string newKey )
{
2024-10-04 23:37:49 -05:00
File . WriteAllText ( GetMetadataFilePath ( installDirectory , gameId , KeyFilename ) , newKey ) ;
2024-02-03 18:32:42 -06:00
}
2024-08-26 20:18:54 -05:00
public static async Task UpdateCurrentKeyAsync ( string installDirectory , Guid gameId , string newKey )
{
2024-10-04 23:37:49 -05:00
await File . WriteAllTextAsync ( GetMetadataFilePath ( installDirectory , gameId , KeyFilename ) , newKey ) ;
2024-08-26 20:18:54 -05:00
}
2023-11-10 00:29:16 -06:00
}
}