2023-11-10 06:29:16 +00:00
|
|
|
|
using LANCommander.SDK.Enums;
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
namespace LANCommander.SDK
|
|
|
|
|
{
|
|
|
|
|
public class GameManager
|
|
|
|
|
{
|
2023-11-11 02:53:48 +00:00
|
|
|
|
private readonly ILogger Logger;
|
2023-11-10 06:29:16 +00:00
|
|
|
|
private Client Client { get; set; }
|
|
|
|
|
private string DefaultInstallDirectory { get; set; }
|
|
|
|
|
|
|
|
|
|
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
|
|
|
|
|
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
|
|
|
|
|
|
|
|
|
|
public delegate void OnArchiveExtractionProgressHandler(long position, long length);
|
|
|
|
|
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress;
|
|
|
|
|
|
2023-11-12 07:04:05 +00:00
|
|
|
|
private TrackableStream Stream;
|
|
|
|
|
private IReader Reader;
|
|
|
|
|
|
2023-11-11 03:36:35 +00:00
|
|
|
|
public GameManager(Client client, string defaultInstallDirectory)
|
2023-11-10 06:29:16 +00:00
|
|
|
|
{
|
|
|
|
|
Client = client;
|
2023-11-11 03:36:35 +00:00
|
|
|
|
DefaultInstallDirectory = defaultInstallDirectory;
|
2023-11-10 06:29:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-11 03:36:35 +00:00
|
|
|
|
public GameManager(Client client, string defaultInstallDirectory, ILogger logger)
|
2023-11-11 02:53:48 +00:00
|
|
|
|
{
|
|
|
|
|
Client = client;
|
2023-11-12 08:10:04 +00:00
|
|
|
|
DefaultInstallDirectory = defaultInstallDirectory;
|
2023-11-11 02:53:48 +00:00
|
|
|
|
Logger = logger;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-10 06:29:16 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Downloads, extracts, and runs post-install scripts for the specified game
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="game">Game to install</param>
|
|
|
|
|
/// <param name="maxAttempts">Maximum attempts in case of transmission error</param>
|
|
|
|
|
/// <returns>Final install path</returns>
|
|
|
|
|
/// <exception cref="Exception"></exception>
|
|
|
|
|
public string Install(Guid gameId, int maxAttempts = 10)
|
|
|
|
|
{
|
2023-11-17 17:48:45 +00:00
|
|
|
|
GameManifest manifest = null;
|
|
|
|
|
|
2023-11-10 06:29:16 +00:00
|
|
|
|
var game = Client.GetGame(gameId);
|
|
|
|
|
|
2023-11-17 17:48:45 +00:00
|
|
|
|
var destination = Path.Combine(DefaultInstallDirectory, game.Title.SanitizeFilename());
|
|
|
|
|
|
2023-11-21 00:20:34 +00:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (ManifestHelper.Exists(destination))
|
|
|
|
|
manifest = ManifestHelper.Read(destination);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger?.LogTrace(ex, "Error reading manifest before install");
|
|
|
|
|
}
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-17 17:48:45 +00:00
|
|
|
|
if (manifest == null || manifest.Id != gameId)
|
2023-11-10 06:29:16 +00:00
|
|
|
|
{
|
2023-11-17 17:48:45 +00:00
|
|
|
|
Logger?.LogTrace("Installing game {GameTitle} ({GameId})", game.Title, game.Id);
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-17 17:48:45 +00:00
|
|
|
|
var result = RetryHelper.RetryOnException<ExtractionResult>(maxAttempts, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () =>
|
|
|
|
|
{
|
|
|
|
|
Logger?.LogTrace("Attempting to download and extract game");
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-17 17:48:45 +00:00
|
|
|
|
return DownloadAndExtract(game, destination);
|
|
|
|
|
});
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-17 17:48:45 +00:00
|
|
|
|
if (!result.Success && !result.Canceled)
|
|
|
|
|
throw new Exception("Could not extract the installer. Retry the install or check your connection");
|
|
|
|
|
else if (result.Canceled)
|
|
|
|
|
return "";
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-17 17:48:45 +00:00
|
|
|
|
game.InstallDirectory = result.Directory;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Logger?.LogTrace("Game {GameTitle} ({GameId}) is already installed to {InstallDirectory}", game.Title, game.Id, destination);
|
|
|
|
|
|
|
|
|
|
game.InstallDirectory = destination;
|
|
|
|
|
}
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
|
|
|
|
var writeManifestSuccess = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromSeconds(1), false, () =>
|
|
|
|
|
{
|
2023-11-11 02:53:28 +00:00
|
|
|
|
Logger?.LogTrace("Attempting to get game manifest");
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
|
|
|
|
manifest = Client.GetGameManifest(game.Id);
|
|
|
|
|
|
|
|
|
|
ManifestHelper.Write(manifest, game.InstallDirectory);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!writeManifestSuccess)
|
|
|
|
|
throw new Exception("Could not grab the manifest file. Retry the install or check your connection");
|
|
|
|
|
|
2023-11-11 02:53:28 +00:00
|
|
|
|
Logger?.LogTrace("Saving scripts");
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
|
|
|
|
ScriptHelper.SaveScript(game, ScriptType.Install);
|
|
|
|
|
ScriptHelper.SaveScript(game, ScriptType.Uninstall);
|
|
|
|
|
ScriptHelper.SaveScript(game, ScriptType.NameChange);
|
|
|
|
|
ScriptHelper.SaveScript(game, ScriptType.KeyChange);
|
|
|
|
|
|
2023-11-17 17:48:45 +00:00
|
|
|
|
return game.InstallDirectory;
|
2023-11-10 06:29:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Uninstall(string installDirectory)
|
|
|
|
|
{
|
|
|
|
|
|
2023-11-11 02:53:28 +00:00
|
|
|
|
Logger?.LogTrace("Attempting to delete the install directory");
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
|
|
|
|
if (Directory.Exists(installDirectory))
|
|
|
|
|
Directory.Delete(installDirectory, true);
|
|
|
|
|
|
2023-11-11 02:53:28 +00:00
|
|
|
|
Logger?.LogTrace("Deleted install directory {InstallDirectory}", installDirectory);
|
2023-11-10 06:29:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-17 17:48:45 +00:00
|
|
|
|
private ExtractionResult DownloadAndExtract(Game game, string destination)
|
2023-11-10 06:29:16 +00:00
|
|
|
|
{
|
|
|
|
|
if (game == null)
|
|
|
|
|
{
|
2023-11-11 02:53:28 +00:00
|
|
|
|
Logger?.LogTrace("Game failed to download, no game was specified");
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
|
|
|
|
throw new ArgumentNullException("No game was specified");
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-11 02:53:28 +00:00
|
|
|
|
Logger?.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination);
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-12 07:04:05 +00:00
|
|
|
|
var extractionResult = new ExtractionResult
|
|
|
|
|
{
|
|
|
|
|
Canceled = false,
|
|
|
|
|
};
|
|
|
|
|
|
2023-11-10 06:29:16 +00:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(destination);
|
|
|
|
|
|
2023-11-12 07:04:05 +00:00
|
|
|
|
Stream = Client.StreamGame(game.Id);
|
|
|
|
|
Reader = ReaderFactory.Open(Stream);
|
|
|
|
|
|
|
|
|
|
Stream.OnProgress += (pos, len) =>
|
2023-11-10 06:29:16 +00:00
|
|
|
|
{
|
2023-11-12 07:04:05 +00:00
|
|
|
|
OnArchiveExtractionProgress?.Invoke(pos, len);
|
|
|
|
|
};
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-12 07:04:05 +00:00
|
|
|
|
Reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
|
|
|
|
|
{
|
|
|
|
|
OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs
|
2023-11-10 06:29:16 +00:00
|
|
|
|
{
|
2023-11-12 07:04:05 +00:00
|
|
|
|
Entry = e.Item,
|
|
|
|
|
Progress = e.ReaderProgress,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
while (Reader.MoveToNextEntry())
|
|
|
|
|
{
|
|
|
|
|
if (Reader.Cancelled)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
Reader.WriteEntryToDirectory(destination, new ExtractionOptions()
|
2023-11-10 06:29:16 +00:00
|
|
|
|
{
|
|
|
|
|
ExtractFullPath = true,
|
2023-11-12 07:04:05 +00:00
|
|
|
|
Overwrite = true,
|
|
|
|
|
PreserveFileTime = true,
|
2023-11-10 06:29:16 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
2023-11-12 07:04:05 +00:00
|
|
|
|
|
|
|
|
|
Reader.Dispose();
|
|
|
|
|
Stream.Dispose();
|
2023-11-10 06:29:16 +00:00
|
|
|
|
}
|
2023-11-29 03:20:07 +00:00
|
|
|
|
catch (ReaderCancelledException ex)
|
2023-11-10 06:29:16 +00:00
|
|
|
|
{
|
2023-11-29 03:20:07 +00:00
|
|
|
|
Logger?.LogTrace("User cancelled the download");
|
2023-11-12 07:04:05 +00:00
|
|
|
|
|
2023-11-29 03:20:07 +00:00
|
|
|
|
extractionResult.Canceled = true;
|
2023-11-12 07:04:05 +00:00
|
|
|
|
|
2023-11-29 03:20:07 +00:00
|
|
|
|
if (Directory.Exists(destination))
|
2023-11-10 06:29:16 +00:00
|
|
|
|
{
|
2023-11-29 03:20:07 +00:00
|
|
|
|
Logger?.LogTrace("Cleaning up orphaned files after cancelled install");
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-29 03:20:07 +00:00
|
|
|
|
Directory.Delete(destination, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger?.LogError(ex, "Could not extract to path {Destination}", destination);
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-29 03:20:07 +00:00
|
|
|
|
if (Directory.Exists(destination))
|
|
|
|
|
{
|
|
|
|
|
Logger?.LogTrace("Cleaning up orphaned install files after bad install");
|
2023-11-10 06:29:16 +00:00
|
|
|
|
|
2023-11-29 03:20:07 +00:00
|
|
|
|
Directory.Delete(destination, true);
|
2023-11-10 06:29:16 +00:00
|
|
|
|
}
|
2023-11-29 03:20:07 +00:00
|
|
|
|
|
|
|
|
|
throw new Exception("The game archive could not be extracted, is it corrupted? Please try again");
|
2023-11-10 06:29:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!extractionResult.Canceled)
|
|
|
|
|
{
|
|
|
|
|
extractionResult.Success = true;
|
|
|
|
|
extractionResult.Directory = destination;
|
|
|
|
|
|
2023-11-11 02:53:28 +00:00
|
|
|
|
Logger?.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination);
|
2023-11-10 06:29:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return extractionResult;
|
|
|
|
|
}
|
2023-11-12 07:04:05 +00:00
|
|
|
|
|
|
|
|
|
public void CancelInstall()
|
|
|
|
|
{
|
|
|
|
|
Reader?.Cancel();
|
|
|
|
|
// Reader?.Dispose();
|
|
|
|
|
// Stream?.Dispose();
|
|
|
|
|
}
|
2023-11-10 06:29:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|