LANCommander/LANCommander.Launcher.Services/InstallService.cs

428 lines
16 KiB
C#
Raw Permalink Normal View History

using LANCommander.Launcher.Data.Models;
using LANCommander.Launcher.Models;
2024-08-07 01:14:13 -05:00
using LANCommander.SDK.Enums;
using LANCommander.SDK.Exceptions;
using LANCommander.SDK.Extensions;
2024-09-11 00:24:34 -05:00
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
using System.Diagnostics;
using LANCommander.SDK.Services;
namespace LANCommander.Launcher.Services
{
public class InstallService : BaseService
{
private readonly GameService GameService;
private readonly SDK.Client Client;
private Stopwatch Stopwatch { get; set; }
public ObservableCollection<IInstallQueueItem> Queue { get; set; }
public delegate Task OnProgressHandler(InstallProgress progress);
public event OnProgressHandler OnProgress;
public delegate Task OnQueueChangedHandler();
public event OnQueueChangedHandler OnQueueChanged;
public delegate Task OnInstallCompleteHandler(Game game);
2024-05-29 02:02:40 -05:00
public event OnInstallCompleteHandler OnInstallComplete;
public delegate Task OnInstallFailHandler(Game game);
public event OnInstallFailHandler OnInstallFail;
public InstallService(
2024-09-11 00:24:34 -05:00
SDK.Client client,
ILogger<InstallService> logger,
GameService gameService) : base(logger)
{
Client = client;
GameService = gameService;
Stopwatch = new Stopwatch();
Queue = new ObservableCollection<IInstallQueueItem>();
Queue.CollectionChanged += (sender, e) =>
{
OnQueueChanged?.Invoke();
};
2024-05-29 20:01:46 -05:00
Client.Games.OnInstallProgressUpdate += (e) =>
2024-08-07 01:14:13 -05:00
{
OnProgress?.Invoke(e);
2024-08-07 01:14:13 -05: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();
}
[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)
{
var gameInfo = await Client.Games.GetAsync(game.Id);
Logger?.LogTrace("Adding game {GameTitle} to the queue", gameInfo.Title);
// Check to see if we need to install the base game (this game is probably a mod or expansion)
if (gameInfo.BaseGameId != Guid.Empty)
{
var baseGame = await GameService.GetAsync(gameInfo.BaseGameId);
if (baseGame != null && !baseGame.Installed)
{
await Add(baseGame, installDirectory);
}
}
try
{
var gameCompletedQueueItems = Queue.Where(i => i.Status == InstallStatus.Complete && i.Id == game.Id).ToList();
foreach (var queueItem in gameCompletedQueueItems)
{
Queue.Remove(queueItem);
}
OnQueueChanged?.Invoke();
}
catch (Exception ex)
{
}
if (!Queue.Any(i => i.Id == game.Id && i.Status == InstallStatus.Queued))
{
var queueItem = new InstallQueueGame(gameInfo);
queueItem.InstallDirectory = installDirectory;
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() ?? [];
queueItem.AddonIds = addonIds;
queueItem.AddonVersions = addonIds.ToDictionary(x => x, y => versions[y]?.FirstOrDefault());
}
if (Queue.Any(i => i.State))
Queue.Add(queueItem);
else
{
Logger?.LogTrace("Download queue is empty, starting the game download immediately");
queueItem.Status = InstallStatus.Starting;
Queue.Add(queueItem);
await Next();
}
OnQueueChanged?.Invoke();
}
}
public void Remove(Guid id)
{
var queueItem = Queue.FirstOrDefault(i => i.Id == id);
if (queueItem != null)
{
Logger?.LogTrace("Removing the game {GameTitle} from the queue", queueItem.Title);
Remove(queueItem);
}
}
public void Remove(IInstallQueueItem queueItem)
{
if (queueItem != null)
{
Logger?.LogTrace("Removing the game {GameTitle} from the queue", queueItem.Title);
Queue.Remove(queueItem);
}
}
public async Task CancelInstallAsync(Guid queueItemId)
{
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);
}
public async Task Next()
{
var currentItem = Queue.FirstOrDefault(i => i.Status.ValueIsIn(InstallStatus.Queued, InstallStatus.Starting));
if (currentItem == null)
return;
Game localGame = null;
SDK.Models.Game remoteGame = null;
try
{
localGame = await GameService.GetAsync(currentItem.Id);
remoteGame = await Client.Games.GetAsync(currentItem.Id);
if (localGame == null)
{
Logger?.LogError("Game does not exist in local database, skipping");
Remove(currentItem);
OnQueueChanged?.Invoke();
return;
}
if (remoteGame == null)
{
Logger?.LogError("Game info could not be retrieved from the server");
currentItem.Status = InstallStatus.Failed;
OnQueueChanged?.Invoke();
return;
}
if (localGame.Installed)
{
// update current local installed game first, might be moved afterwards
await Client.Games.UpdateGameInstallationAsync(localGame.InstallDirectory, remoteGame);
// Probably doing a modification of some sort
if (localGame.InstallDirectory.StartsWith(currentItem.InstallDirectory))
{
var allAddons = remoteGame.DependentGames.ToArray();
var removeAddons = allAddons.Except(currentItem.AddonIds ?? []).ToArray();
var addAddons = allAddons.Intersect(currentItem.AddonIds ?? []).ToArray();
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);
UpdateGameState(currentItem, localGame, localGame.InstallDirectory);
await GameService.UpdateAsync(localGame);
currentItem.Status = InstallStatus.Complete;
OnQueueChanged?.Invoke();
OnInstallComplete?.Invoke(localGame);
}
else
{
await Move(currentItem, localGame, remoteGame);
}
}
else
{
await Install(currentItem, localGame, remoteGame);
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "An unknown error occured while trying to retrieve game info from the server");
}
}
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))
{
string installDirectory;
currentItem.Status = InstallStatus.Downloading;
OnQueueChanged?.Invoke();
try
{
var gameFileList = await Client.Games.InstallAsync(remoteGame.Id, currentItem.InstallDirectory, currentItem.AddonIds, cancellationToken: currentItem.CancellationToken.Token);
installDirectory = gameFileList.InstallDirectory;
UpdateGameState(currentItem, localGame, 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;
}
#region Download Manuals
try
{
foreach (var manual in remoteGame.Media.Where(m => m.Type == SDK.Enums.MediaType.Manual))
{
var localPath = Path.Combine(Client.Media.GetLocalPath(manual), $"{manual.FileId}-{manual.Crc32}");
if (!File.Exists(localPath))
{
foreach (var staleFile in Client.Media.GetStaleLocalPaths(manual))
File.Delete(staleFile);
await Client.Media.DownloadAsync(new SDK.Models.Media
{
Id = manual.Id,
FileId = manual.FileId
}, localPath);
}
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "An unknown error occurred while trying to download game manuals for game {GameTitle} ({GameId})", localGame.Title, localGame.Id);
}
#endregion
if (currentItem is InstallQueueGame)
{
currentItem.CompletedOn = DateTime.Now;
currentItem.Status = InstallStatus.Complete;
currentItem.Progress = 1;
currentItem.BytesDownloaded = currentItem.TotalBytes;
try
{
await GameService.UpdateAsync(localGame);
}
catch (Exception ex)
{
Logger?.LogError(ex, "An unknown error occurred while trying to write changes to the database after install of game {GameTitle} ({GameId})", localGame.Title, localGame.Id);
}
2024-05-29 02:02:40 -05:00
OnQueueChanged?.Invoke();
2024-08-11 12:31:30 -05:00
Logger?.LogTrace("Install of game {GameTitle} ({GameId}) complete!", localGame.Title, localGame.Id);
2024-06-12 23:20:31 -05:00
// ShowCompletedNotification(currentItem);
OnInstallComplete?.Invoke(localGame);
2024-09-16 20:52:30 -05:00
operation.Complete();
}
}
await Next();
}
private static void UpdateGameState(IInstallQueueItem currentItem, Game localGame, string installDirectory)
{
localGame.InstallDirectory = installDirectory;
localGame.Installed = true;
localGame.InstalledVersion = currentItem.Version;
localGame.InstalledOn ??= DateTime.Now;
foreach (var localAddon in (localGame.DependentGames ?? []))
{
bool isInstalled = currentItem.AddonIds?.Contains(localAddon.Id) ?? false;
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;
}
}
}
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();
var newInstallDirectory = await Client.Games.GetInstallDirectory(remoteGame, currentItem.InstallDirectory);
newInstallDirectory = await Client.Games.MoveAsync(remoteGame, localGame.InstallDirectory, newInstallDirectory);
localGame.InstallDirectory = newInstallDirectory;
await GameService.UpdateAsync(localGame);
currentItem.Status = InstallStatus.Complete;
OnQueueChanged?.Invoke();
OnInstallComplete?.Invoke(localGame);
operation.Complete();
}
}
/*private void ShowCompletedNotification(IDownloadQueueItem queueItem)
{
var builder = new ToastContentBuilder();
if (queueItem.IsUpdate)
builder.AddText("Game Updated")
.AddText($"{queueItem.Title} has finished updating!");
else
builder.AddText("Game Installed")
.AddText($"{queueItem.Title} has finished installing!");
builder.AddArgument("gameId", queueItem.Id.ToString())
.AddButton(
new ToastButton()
.SetContent("Play")
.AddArgument("action", "play")
)
.AddButton(
new ToastButton()
.SetContent("View in Library")
.AddArgument("action", "viewInLibrary")
);
//.Show
// .AddAppLogoOverride()
//.Show();
}*/
}
}