LANCommander/LANCommander.Playnite.Extension/DownloadQueueController.cs
2024-01-25 00:46:10 -06:00

462 lines
18 KiB
C#

using LANCommander.PlaynitePlugin.Models;
using LANCommander.SDK;
using LANCommander.SDK.Exceptions;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.PowerShell;
using Microsoft.Extensions.Logging;
using Microsoft.Toolkit.Uwp.Notifications;
using Playnite.SDK;
using Playnite.SDK.Models;
using Playnite.SDK.Plugins;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LANCommander.PlaynitePlugin
{
public class DownloadQueueController
{
public static readonly Playnite.SDK.ILogger Logger = LogManager.GetLogger();
private LANCommanderLibraryPlugin Plugin { get; set; }
public DownloadQueue DownloadQueue { get; set; }
private Stopwatch Stopwatch { get; set; }
public delegate void OnInstallCompleteHandler(Game game, string installDirectory);
public event OnInstallCompleteHandler OnInstallComplete;
public delegate void OnInstallFailHandler(Game game);
public event OnInstallFailHandler OnInstallFail;
public delegate void OnInstallCancelledHandler(Game game);
public event OnInstallCancelledHandler OnInstallCancelled;
public DownloadQueueController(LANCommanderLibraryPlugin plugin)
{
Plugin = plugin;
DownloadQueue = new DownloadQueue();
Stopwatch = new Stopwatch();
Plugin.LANCommanderClient.Games.OnArchiveExtractionProgress += Games_OnArchiveExtractionProgress;
Plugin.LANCommanderClient.Games.OnArchiveEntryExtractionProgress += Games_OnArchiveEntryExtractionProgress;
ToastNotificationManagerCompat.OnActivated += NotificationHandler;
}
private void NotificationHandler(ToastNotificationActivatedEventArgsCompat e)
{
try
{
var args = ToastArguments.Parse(e.Argument);
var gameId = Guid.Parse(args["gameId"]);
switch (args["action"])
{
case "play":
Plugin.PlayniteApi.StartGame(gameId);
break;
case "viewInLibrary":
default:
Plugin.PlayniteApi.MainView.SwitchToLibraryView();
Plugin.PlayniteApi.MainView.SelectGame(gameId);
break;
}
}
catch { }
}
private void Games_OnArchiveEntryExtractionProgress(object sender, SDK.ArchiveEntryExtractionProgressArgs e)
{
if (DownloadQueue.CurrentItem.CancellationToken != null && DownloadQueue.CurrentItem.CancellationToken.IsCancellationRequested)
{
Plugin.LANCommanderClient.Games.CancelInstall();
}
}
private void Games_OnArchiveExtractionProgress(long position, long length, SDK.Models.Game game)
{
if (Stopwatch.ElapsedMilliseconds > 500)
{
Plugin.PlayniteApi.MainView.UIDispatcher.Invoke(() =>
{
DownloadQueue.CurrentItem.Size = length;
DownloadQueue.CurrentItem.Speed = (double)(position - DownloadQueue.CurrentItem.TotalDownloaded) / (Stopwatch.ElapsedMilliseconds / 1000d);
DownloadQueue.CurrentItem.TotalDownloaded = position;
DownloadQueue.CurrentItem.TimeRemaining = $"{GetTimeRemaining(DownloadQueue.CurrentItem)} Remaining";
Plugin.DownloadQueueSidebarItem.ProgressMaximum = length;
Plugin.DownloadQueueSidebarItem.ProgressValue = position;
});
Stopwatch.Restart();
}
}
private void ChangeCurrentItemStatus(DownloadQueueItemStatus status)
{
Plugin.PlayniteApi.MainView.UIDispatcher.Invoke(() => {
switch (status)
{
case DownloadQueueItemStatus.Downloading:
DownloadQueue.CurrentItem.ProgressIndeterminate = false;
DownloadQueue.CurrentItem.Status = status;
break;
default:
DownloadQueue.CurrentItem.ProgressIndeterminate = true;
DownloadQueue.CurrentItem.Status = status;
break;
}
});
}
private string GetTimeRemaining(DownloadQueueItem item)
{
if (item.Speed <= 0)
return "∞";
var timespan = TimeSpan.FromSeconds((item.Size - item.TotalDownloaded) / item.Speed);
if (timespan.Days > 0)
return timespan.ToString(@"d\:hh\:mm\:ss");
if (timespan.Hours > 0)
return timespan.ToString(@"h\:mm\:ss");
else
return timespan.ToString(@"mm\:ss");
}
public void Add(Game game)
{
var gameId = Guid.Parse(game.GameId);
var gameInfo = Plugin.LANCommanderClient.Games.Get(gameId);
if (gameInfo.BaseGame != null)
{
var baseGame = Plugin.PlayniteApi.Database.Games.Where(g => g.GameId == gameInfo.BaseGame.Id.ToString()).FirstOrDefault();
if (baseGame != null && !baseGame.IsInstalled)
Plugin.PlayniteApi.InstallGame(baseGame.Id);
}
if (!DownloadQueue.Items.Any(i => i.Game.Id == game.Id))
{
DownloadQueue.Items.Add(new DownloadQueueItem()
{
CoverPath = Plugin.PlayniteApi.Database.GetFullFilePath(game.CoverImage),
Game = game,
Title = game.Name,
QueuedOn = DateTime.Now,
});
if (DownloadQueue.Items.Count == 1 && DownloadQueue.CurrentItem == null)
ProcessQueue();
game.IsInstalled = false;
game.IsInstalling = true;
Plugin.PlayniteApi.Database.Games.Update(game);
}
}
public void Remove(DownloadQueueItem downloadQueueItem)
{
Logger?.Trace($"Removing {downloadQueueItem.Title} from the queue");
if (DownloadQueue.Items.Contains(downloadQueueItem))
{
Plugin.PlayniteApi.MainView.UIDispatcher.Invoke(() =>
{
DownloadQueue.Items.Remove(downloadQueueItem);
});
}
else if (DownloadQueue.CurrentItem.Equals(downloadQueueItem))
{
DownloadQueue.CurrentItem = null;
}
}
public bool Exists(Game game)
{
return DownloadQueue.Items.Any(i => i.Game.Id == game.Id);
}
public void ProcessQueue()
{
Logger?.Trace("Processing download queue");
Plugin.PlayniteApi.MainView.UIDispatcher.Invoke(() =>
{
DownloadQueue.CurrentItem = DownloadQueue.Items.FirstOrDefault();
if (DownloadQueue.CurrentItem != null)
DownloadQueue.Items.Remove(DownloadQueue.CurrentItem);
});
Task.Run(() => Install());
}
public void CancelInstall()
{
if (DownloadQueue.CurrentItem.Status == DownloadQueueItemStatus.Downloading)
{
ChangeCurrentItemStatus(DownloadQueueItemStatus.Canceled);
Plugin.LANCommanderClient.Games.CancelInstall();
}
}
private void Install()
{
if (DownloadQueue.CurrentItem == null)
return;
var gameId = Guid.Parse(DownloadQueue.CurrentItem.Game.GameId);
var game = Plugin.LANCommanderClient.Games.Get(gameId);
Logger?.Trace($"Initiating install for {game.Title}");
if (game.BaseGame != null)
{
Logger?.Trace("Game is reliant on another game for installation. Installing the base game first...");
var baseGame = Plugin.PlayniteApi.Database.Games.Where(g => g.GameId == game.BaseGame.Id.ToString()).FirstOrDefault();
if (baseGame != null && !baseGame.IsInstalled)
Plugin.PlayniteApi.InstallGame(baseGame.Id);
}
Stopwatch.Restart();
DownloadQueue.CurrentItem.InProgress = true;
DownloadQueue.CurrentItem.Game.IsInstalling = true;
Plugin.PlayniteApi.Database.Games.Update(DownloadQueue.CurrentItem.Game);
ChangeCurrentItemStatus(DownloadQueueItemStatus.Downloading);
string installDirectory;
try
{
installDirectory = Plugin.LANCommanderClient.Games.Install(game.Id);
}
catch (InstallCanceledException ex)
{
OnInstallCancelled?.Invoke(DownloadQueue.CurrentItem.Game);
Remove(DownloadQueue.CurrentItem);
Stopwatch.Stop();
return;
}
catch (InstallException ex)
{
Plugin.PlayniteApi.Notifications.Add($"InstallFail-{DownloadQueue.CurrentItem.Game.Id}", ex.Message, NotificationType.Error);
ShowFailedNotification(DownloadQueue.CurrentItem);
OnInstallFail?.Invoke(DownloadQueue.CurrentItem.Game);
Remove(DownloadQueue.CurrentItem);
Stopwatch.Stop();
return;
}
catch (Exception ex)
{
Logger?.Error(ex, $"An unknown error occurred while trying to install {game.Title}");
Plugin.PlayniteApi.Notifications.Add($"InstallFail-{DownloadQueue.CurrentItem.Game.Id}", $"An unknown error occurred while trying to install {game.Title}", NotificationType.Error);
ShowFailedNotification(DownloadQueue.CurrentItem);
OnInstallFail?.Invoke(DownloadQueue.CurrentItem.Game);
Remove(DownloadQueue.CurrentItem);
Stopwatch.Stop();
return;
}
Stopwatch.Stop();
Logger?.Trace($"Game successfully installed to {installDirectory}");
Plugin.PlayniteApi.MainView.UIDispatcher.Invoke(() =>
{
DownloadQueue.CurrentItem.ProgressIndeterminate = true;
Plugin.DownloadQueueSidebarItem.ProgressValue = 1;
Plugin.DownloadQueueSidebarItem.ProgressMaximum = 1;
});
if (game.Redistributables != null && game.Redistributables.Any())
{
ChangeCurrentItemStatus(DownloadQueueItemStatus.InstallingRedistributables);
Logger?.Trace("Installing redistributables");
Plugin.LANCommanderClient.Redistributables.Install(game);
}
var manifest = ManifestHelper.Read(installDirectory, gameId);
Plugin.UpdateGame(manifest, installDirectory);
Logger?.Trace("Attempting to download the latest save");
ChangeCurrentItemStatus(DownloadQueueItemStatus.DownloadingSaves);
Plugin.SaveController = new LANCommanderSaveController(Plugin, null);
Plugin.SaveController.Download(gameId, installDirectory);
ChangeCurrentItemStatus(DownloadQueueItemStatus.RunningScripts);
RunInstallScript(game);
RunNameChangeScript(game);
RunKeyChangeScript(game);
DownloadQueue.CurrentItem.CompletedOn = DateTime.Now;
DownloadQueue.CurrentItem.TotalDownloaded = DownloadQueue.CurrentItem.Size;
ChangeCurrentItemStatus(DownloadQueueItemStatus.Idle);
Logger.Trace("Installation process completed successfully. Running final cleanup and marking the game as installed");
ShowCompletedNotification(DownloadQueue.CurrentItem);
DownloadQueue.CurrentItem.InProgress = false;
OnInstallComplete?.Invoke(DownloadQueue.CurrentItem.Game, installDirectory);
Plugin.PlayniteApi.MainView.UIDispatcher.Invoke(() =>
{
DownloadQueue.CurrentItem.ProgressIndeterminate = false;
DownloadQueue.Completed.Add(DownloadQueue.CurrentItem);
Plugin.DownloadQueueSidebarItem.ProgressValue = 0;
Plugin.DownloadQueueSidebarItem.ProgressMaximum = 1;
});
DownloadQueue.CurrentItem.Game.IsInstalled = true;
DownloadQueue.CurrentItem.Game.IsInstalling = false;
Plugin.PlayniteApi.Database.Games.Update(DownloadQueue.CurrentItem.Game);
ProcessQueue();
}
private void ShowCompletedNotification(DownloadQueueItem queueItem)
{
new ToastContentBuilder()
.AddText("Game Installed")
.AddText($"{queueItem.Title} has finished installing!")
.AddArgument("gameId", queueItem.Game.Id.ToString())
.AddButton(
new ToastButton()
.SetContent("Play")
.AddArgument("action", "play")
)
.AddButton(
new ToastButton()
.SetContent("View in Library")
.AddArgument("action", "viewInLibrary")
)
.AddAppLogoOverride(new Uri($"file:///{queueItem.CoverPath}", UriKind.Absolute), ToastGenericAppLogoCrop.None)
.Show();
}
private void ShowFailedNotification(DownloadQueueItem queueItem)
{
new ToastContentBuilder()
.AddText("Install Failed")
.AddText($"{queueItem.Title} could not be installed!")
.AddArgument("gameId", queueItem.Game.Id.ToString())
.AddButton(
new ToastButton()
.SetContent("View in Library")
.AddArgument("action", "viewInLibrary")
)
.AddAppLogoOverride(new Uri($"file:///{queueItem.CoverPath}", UriKind.Absolute), ToastGenericAppLogoCrop.None)
.Show();
}
private int RunInstallScript(SDK.Models.Game game)
{
var installDirectory = Plugin.LANCommanderClient.Games.GetInstallDirectory(game);
var manifest = ManifestHelper.Read(installDirectory, game.Id);
var path = ScriptHelper.GetScriptFilePath(installDirectory, game.Id, SDK.Enums.ScriptType.Install);
if (File.Exists(path))
{
Logger?.Trace("Running install script");
var script = new PowerShellScript();
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory);
script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress);
script.UseFile(ScriptHelper.GetScriptFilePath(installDirectory, game.Id, SDK.Enums.ScriptType.Install));
return script.Execute();
}
return 0;
}
private int RunNameChangeScript(SDK.Models.Game game)
{
var installDirectory = Plugin.LANCommanderClient.Games.GetInstallDirectory(game);
var manifest = ManifestHelper.Read(installDirectory, game.Id);
var path = ScriptHelper.GetScriptFilePath(installDirectory, game.Id, SDK.Enums.ScriptType.NameChange);
var oldName = GameService.GetPlayerAlias(installDirectory, game.Id);
var newName = Plugin.Settings.PlayerAlias;
if (File.Exists(path))
{
Logger?.Trace("Running name change script");
var script = new PowerShellScript();
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory);
script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress);
script.AddVariable("OldPlayerAlias", oldName);
script.AddVariable("NewPlayerAlias", newName);
script.UseFile(path);
GameService.UpdatePlayerAlias(installDirectory, game.Id, newName);
return script.Execute();
}
return 0;
}
private int RunKeyChangeScript(SDK.Models.Game game)
{
var installDirectory = Plugin.LANCommanderClient.Games.GetInstallDirectory(game);
var manifest = ManifestHelper.Read(installDirectory, game.Id);
var path = ScriptHelper.GetScriptFilePath(installDirectory, game.Id, SDK.Enums.ScriptType.KeyChange);
if (File.Exists(path))
{
Logger?.Trace("Running key change script");
var script = new PowerShellScript();
var key = Plugin.LANCommanderClient.Games.GetAllocatedKey(manifest.Id);
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory);
script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress);
script.AddVariable("AllocatedKey", key);
script.UseFile(path);
return script.Execute();
}
return 0;
}
}
}