using Force.Crc32; using LANCommander.SDK.Enums; using LANCommander.SDK.Exceptions; 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; using System.Threading; namespace LANCommander.SDK { public class GameService { private readonly ILogger Logger; 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, Game game); public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress; public const string PlayerAliasFilename = "PlayerAlias"; public const string KeyFilename = "Key"; private TrackableStream DownloadStream; private IReader Reader; public GameService(Client client, string defaultInstallDirectory) { Client = client; DefaultInstallDirectory = defaultInstallDirectory; } public GameService(Client client, string defaultInstallDirectory, ILogger logger) { Client = client; DefaultInstallDirectory = defaultInstallDirectory; Logger = logger; } public IEnumerable Get() { return Client.GetRequest>("/api/Games"); } public Game Get(Guid id) { return Client.GetRequest($"/api/Games/{id}"); } public GameManifest GetManifest(Guid id) { return Client.GetRequest($"/api/Games/{id}/Manifest"); } private TrackableStream Stream(Guid id) { return Client.StreamRequest($"/api/Games/{id}/Download"); } public void StartPlaySession(Guid id) { Logger?.LogTrace("Starting a game session..."); Client.PostRequest($"/api/PlaySessions/Start/{id}"); } public void EndPlaySession(Guid id) { Logger?.LogTrace("Ending a game session..."); Client.PostRequest($"/api/PlaySessions/End/{id}"); } public string GetAllocatedKey(Guid id) { Logger?.LogTrace("Requesting allocated key..."); var macAddress = Client.GetMacAddress(); var request = new KeyRequest() { GameId = id, MacAddress = macAddress, ComputerName = Environment.MachineName, IpAddress = Client.GetIpAddress(), }; var response = Client.PostRequest($"/api/Keys/GetAllocated/{id}", request); if (response == null) return String.Empty; return response.Value; } public string GetNewKey(Guid id) { Logger?.LogTrace("Requesting new key allocation..."); var macAddress = Client.GetMacAddress(); var request = new KeyRequest() { GameId = id, MacAddress = macAddress, ComputerName = Environment.MachineName, IpAddress = Client.GetIpAddress(), }; var response = Client.PostRequest($"/api/Keys/Allocate/{id}", request); if (response == null) return String.Empty; return response.Value; } /// /// Downloads, extracts, and runs post-install scripts for the specified game /// /// Game to install /// Maximum attempts in case of transmission error /// Final install path /// public string Install(Guid gameId, int maxAttempts = 10) { GameManifest manifest = null; var game = Get(gameId); var destination = GetInstallDirectory(game); if ((game.Type == GameType.StandaloneExpansion || game.Type == GameType.StandaloneMod) && game.BaseGame != null) { destination = GetInstallDirectory(game.BaseGame); if (!Directory.Exists(destination)) destination = Install(game.BaseGame.Id, maxAttempts); } try { if (ManifestHelper.Exists(destination, game.Id)) manifest = ManifestHelper.Read(destination, game.Id); } catch (Exception ex) { Logger?.LogTrace(ex, "Error reading manifest before install"); } Logger?.LogTrace("Installing game {GameTitle} ({GameId})", game.Title, game.Id); var result = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () => { Logger?.LogTrace("Attempting to download and extract game"); return DownloadAndExtract(game, destination); }); if (!result.Success && !result.Canceled) throw new InstallException("Could not extract the installer. Retry the install or check your connection"); else if (result.Canceled) throw new InstallCanceledException("Game install was canceled"); game.InstallDirectory = result.Directory; var writeManifestSuccess = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromSeconds(1), false, () => { Logger?.LogTrace("Attempting to get game manifest"); manifest = GetManifest(game.Id); ManifestHelper.Write(manifest, game.InstallDirectory); return true; }); if (!writeManifestSuccess) throw new InstallException("Could not grab the manifest file. Retry the install or check your connection"); Logger?.LogTrace("Saving scripts"); ScriptHelper.SaveScript(game, ScriptType.Install); ScriptHelper.SaveScript(game, ScriptType.Uninstall); ScriptHelper.SaveScript(game, ScriptType.NameChange); ScriptHelper.SaveScript(game, ScriptType.KeyChange); return game.InstallDirectory; } public void Uninstall(string installDirectory, Guid gameId) { var fileListPath = GameService.GetMetadataFilePath(installDirectory, gameId, "FileList.txt"); if (File.Exists(fileListPath)) { var fileList = File.ReadAllLines(fileListPath); var files = fileList.Select(l => l.Split('|').FirstOrDefault().Trim()); Logger?.LogTrace("Attempting to delete the install files"); foreach (var file in files.Where(f => !f.EndsWith("/"))) { var localPath = Path.Combine(installDirectory, file); try { if (File.Exists(localPath)) File.Delete(localPath); } catch (Exception ex) { Logger?.LogWarning(ex, "Could not remove file {LocalPath}", localPath); } } Logger?.LogTrace("Attempting to delete any empty directories"); DirectoryHelper.DeleteEmptyDirectories(installDirectory); if (!Directory.Exists(installDirectory)) Logger?.LogTrace("Deleted install directory {InstallDirectory}", installDirectory); else Logger?.LogTrace("Removed game files for {GameId}", gameId); } else { Directory.Delete(installDirectory, true); } } private ExtractionResult DownloadAndExtract(Game game, string destination) { if (game == null) { Logger?.LogTrace("Game failed to download, no game was specified"); throw new ArgumentNullException("No game was specified"); } Logger?.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination); var extractionResult = new ExtractionResult { Canceled = false, }; var fileManifest = new StringBuilder(); try { Directory.CreateDirectory(destination); DownloadStream = Stream(game.Id); Reader = ReaderFactory.Open(DownloadStream); DownloadStream.OnProgress += (pos, len) => { OnArchiveExtractionProgress?.Invoke(pos, len, game); }; Reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => { OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs { Entry = e.Item, Progress = e.ReaderProgress, Game = game, }); }; while (Reader.MoveToNextEntry()) { if (Reader.Cancelled) break; var localFile = Path.Combine(destination, Reader.Entry.Key); 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); } } } fileManifest.AppendLine($"{Reader.Entry.Key} | {Reader.Entry.Crc.ToString("X")}"); if (crc == 0 || crc != Reader.Entry.Crc) Reader.WriteEntryToDirectory(destination, new ExtractionOptions() { ExtractFullPath = true, Overwrite = true, PreserveFileTime = true }); else Reader.OpenEntryStream().Dispose(); } Reader.Dispose(); DownloadStream.Dispose(); } catch (ReaderCancelledException ex) { Logger?.LogTrace("User cancelled the download"); extractionResult.Canceled = true; if (Directory.Exists(destination)) { Logger?.LogTrace("Cleaning up orphaned files after cancelled install"); Directory.Delete(destination, true); } } catch (Exception ex) { Logger?.LogError(ex, "Could not extract to path {Destination}", destination); if (Directory.Exists(destination)) { Logger?.LogTrace("Cleaning up orphaned install files after bad install"); Directory.Delete(destination, true); } throw new Exception("The game archive could not be extracted, is it corrupted? Please try again"); } if (!extractionResult.Canceled) { extractionResult.Success = true; extractionResult.Directory = destination; 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()); Logger?.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination); } return extractionResult; } public string GetInstallDirectory(Game game) { if ((game.Type == GameType.Expansion || game.Type == GameType.StandaloneExpansion || game.Type == GameType.Mod || game.Type == GameType.StandaloneMod) && game.BaseGame != null) return GetInstallDirectory(game.BaseGame); else return Path.Combine(DefaultInstallDirectory, game.Title.SanitizeFilename()); } public void CancelInstall() { Reader?.Cancel(); } public static string GetMetadataDirectoryPath(string installDirectory, Guid gameId) { return Path.Combine(installDirectory, ".lancommander", gameId.ToString()); } public static string GetMetadataFilePath(string installDirectory, Guid gameId, string fileName) { return Path.Combine(GetMetadataDirectoryPath(installDirectory, gameId), fileName); } public static string GetPlayerAlias(string installDirectory, Guid gameId) { var aliasFilePath = GameService.GetMetadataFilePath(installDirectory, gameId, GameService.PlayerAliasFilename); if (File.Exists(aliasFilePath)) return File.ReadAllText(aliasFilePath); else return String.Empty; } public static void UpdatePlayerAlias(string installDirectory, Guid gameId, string newName) { File.WriteAllText(GameService.GetMetadataFilePath(installDirectory, gameId, GameService.PlayerAliasFilename), newName); } public static string GetCurrentKey(string installDirectory, Guid gameId) { var keyFilePath = GameService.GetMetadataFilePath(installDirectory, gameId, GameService.KeyFilename); if (File.Exists(keyFilePath)) return File.ReadAllText(keyFilePath); else return String.Empty; } public static void UpdateCurrentKey(string installDirectory, Guid gameId, string newKey) { File.WriteAllText(GameService.GetMetadataFilePath(installDirectory, gameId, GameService.KeyFilename), newKey); } } }