diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 084b406..c762c86 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -1,17 +1,14 @@ -using LANCommander.PlaynitePlugin.Helpers; -using LANCommander.SDK.Enums; -using LANCommander.SDK.Extensions; +using LANCommander.SDK; +using LANCommander.SDK.Helpers; using LANCommander.SDK.Models; +using LANCommander.SDK.PowerShell; using Playnite.SDK; using Playnite.SDK.Models; using Playnite.SDK.Plugins; -using SharpCompress.Common; -using SharpCompress.Readers; using System; +using System.Diagnostics; using System.IO; using System.Linq; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace LANCommander.PlaynitePlugin { @@ -20,15 +17,11 @@ namespace LANCommander.PlaynitePlugin public static readonly ILogger Logger = LogManager.GetLogger(); private LANCommanderLibraryPlugin Plugin; - private PowerShellRuntime PowerShellRuntime; - private Playnite.SDK.Models.Game PlayniteGame; public LANCommanderInstallController(LANCommanderLibraryPlugin plugin, Playnite.SDK.Models.Game game) : base(game) { Name = "Install using LANCommander"; Plugin = plugin; - PlayniteGame = game; - PowerShellRuntime = new PowerShellRuntime(); } public override void Install(InstallActionArgs args) @@ -43,450 +36,176 @@ namespace LANCommander.PlaynitePlugin } var gameId = Guid.Parse(Game.GameId); - var game = Plugin.LANCommander.GetGame(gameId); - Logger.Trace($"Installing game {game.Title} ({game.Id})..."); + string installDirectory = null; - var result = RetryHelper.RetryOnException(10, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () => + var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => { - Logger.Trace("Attempting to download and extract game..."); - return DownloadAndExtractGame(game); + var gameManager = new GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory); + + Stopwatch stopwatch = new Stopwatch(); + + stopwatch.Start(); + + var lastTotalSize = 0d; + var speed = 0d; + + gameManager.OnArchiveExtractionProgress += (long pos, long len) => + { + if (stopwatch.ElapsedMilliseconds > 500) + { + var percent = Math.Ceiling((pos / (decimal)len) * 100); + + progress.ProgressMaxValue = len; + progress.CurrentProgressValue = pos; + + speed = (double)(progress.CurrentProgressValue - lastTotalSize) / (stopwatch.ElapsedMilliseconds / 1000d); + + progress.Text = $"Downloading {Game.Name} ({percent}%) | {ByteSizeLib.ByteSize.FromBytes(speed).ToString("#.#")}/s"; + + lastTotalSize = pos; + + stopwatch.Restart(); + } + }; + + gameManager.OnArchiveEntryExtractionProgress += (object sender, ArchiveEntryExtractionProgressArgs e) => + { + if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) + { + gameManager.CancelInstall(); + + progress.IsIndeterminate = true; + } + }; + + installDirectory = gameManager.Install(gameId); + + stopwatch.Stop(); + }, + new GlobalProgressOptions($"Preparing to download {Game.Name}") + { + IsIndeterminate = false, + Cancelable = true, }); - if (!result.Success && !result.Canceled) - throw new Exception("Could not extract the install archive. Retry the install or check your connection."); - else if (result.Canceled) - throw new Exception("Install was canceled"); - - var installInfo = new GameInstallationData() - { - InstallDirectory = result.Directory - }; - - PlayniteGame.InstallDirectory = result.Directory; - - SDK.GameManifest manifest = null; - - var writeManifestSuccess = RetryHelper.RetryOnException(10, TimeSpan.FromSeconds(1), false, () => - { - Logger.Trace("Attempting to get game manifest..."); - - manifest = Plugin.LANCommander.GetGameManifest(gameId); - - WriteManifest(manifest, result.Directory); - - return true; - }); - - if (!writeManifestSuccess) - throw new Exception("Could not get or write the manifest file. Retry the install or check your connection."); - - Logger.Trace("Saving scripts..."); - - SaveScript(game, result.Directory, ScriptType.Install); - SaveScript(game, result.Directory, ScriptType.Uninstall); - SaveScript(game, result.Directory, ScriptType.NameChange); - SaveScript(game, result.Directory, ScriptType.KeyChange); + // Install any redistributables + var game = Plugin.LANCommanderClient.GetGame(gameId); if (game.Redistributables != null && game.Redistributables.Count() > 0) - { - Logger.Trace("Installing required redistributables..."); - InstallRedistributables(game); - } - - try - { - PowerShellRuntime.RunScript(PlayniteGame, ScriptType.Install); - PowerShellRuntime.RunScript(PlayniteGame, ScriptType.NameChange, Plugin.Settings.PlayerName); - - var key = Plugin.LANCommander.GetAllocatedKey(game.Id); - - PowerShellRuntime.RunScript(PlayniteGame, ScriptType.KeyChange, $"\"{key}\""); - } - catch { } - - Plugin.UpdateGame(manifest, gameId); - - Plugin.DownloadCache.Remove(gameId); - - InvokeOnInstalled(new GameInstalledEventArgs(installInfo)); - } - - private ExtractionResult DownloadAndExtractGame(LANCommander.SDK.Models.Game game) - { - if (game == null) - { - Logger.Trace("Game failed to download! No game was specified!"); - - throw new Exception("Game failed to download!"); - } - - var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename()); - - Logger.Trace($"Downloading and extracting \"{game.Title}\" to path {destination}"); - var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => - { - try - { - Directory.CreateDirectory(destination); - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; - - using (var gameStream = Plugin.LANCommander.StreamGame(game.Id)) - using (var reader = ReaderFactory.Open(gameStream)) - { - progress.ProgressMaxValue = gameStream.Length; - - gameStream.OnProgress += (pos, len) => - { - progress.CurrentProgressValue = pos; - }; - - reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => - { - if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) - { - reader.Cancel(); - progress.IsIndeterminate = true; - - reader.Dispose(); - gameStream.Dispose(); - } - }; - - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - } - catch (Exception ex) - { - if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) - { - Logger.Trace("User cancelled the download"); - - if (Directory.Exists(destination)) - { - Logger.Trace("Cleaning up orphaned install files after cancelled install..."); - - Directory.Delete(destination, true); - } - } - else - { - Logger.Error(ex, $"Could not extract to path {destination}"); - - if (Directory.Exists(destination)) - { - Logger.Trace("Cleaning up orphaned install files after bad install..."); - - Directory.Delete(destination, true); - } - - throw new Exception("The game archive could not be extracted. Please try again or fix the archive!"); - } - } - }, - new GlobalProgressOptions($"Downloading {game.Title}...") - { - IsIndeterminate = false, - Cancelable = true, - }); - - var extractionResult = new ExtractionResult - { - Canceled = result.Canceled - }; - - if (!result.Canceled) - { - extractionResult.Success = true; - extractionResult.Directory = destination; - Logger.Trace($"Game successfully downloaded and extracted to {destination}"); - } - - return extractionResult; - } - - private void InstallRedistributables(LANCommander.SDK.Models.Game game) - { - foreach (var redistributable in game.Redistributables) - { - string installScriptTempFile = null; - string detectionScriptTempFile = null; - string extractTempPath = null; - - try - { - var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install); - installScriptTempFile = SaveTempScript(installScript); - - var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall); - detectionScriptTempFile = SaveTempScript(detectionScript); - - var detectionResult = PowerShellRuntime.RunScript(detectionScriptTempFile, detectionScript.RequiresAdmin); - - // Redistributable is not installed - if (detectionResult == 0) - { - if (redistributable.Archives.Count() > 0) - { - var extractionResult = DownloadAndExtractRedistributable(redistributable); - - if (extractionResult.Success) - { - extractTempPath = extractionResult.Directory; - - PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath); - } - } - else - { - PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath); - } - } - } - catch (Exception ex) - { - Logger.Error(ex, $"Redistributable {redistributable.Name} failed to install"); - } - finally - { - if (File.Exists(installScriptTempFile)) - File.Delete(installScriptTempFile); - - if (File.Exists(detectionScriptTempFile)) - File.Delete(detectionScriptTempFile); - - if (Directory.Exists(extractTempPath)) - Directory.Delete(extractTempPath); - } - } - } - - private ExtractionResult DownloadAndExtractRedistributable(LANCommander.SDK.Models.Redistributable redistributable) - { - if (redistributable == null) - { - Logger.Trace("Redistributable failed to download! No redistributable was specified!"); - - throw new Exception("Redistributable failed to download!"); - } - - var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename()); - - Logger.Trace($"Downloading and extracting \"{redistributable.Name}\" to path {destination}"); - var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => - { - try - { - Directory.CreateDirectory(destination); - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; - - using (var redistributableStream = Plugin.LANCommander.StreamRedistributable(redistributable.Id)) - using (var reader = ReaderFactory.Open(redistributableStream)) - { - progress.ProgressMaxValue = redistributableStream.Length; - - redistributableStream.OnProgress += (pos, len) => - { - progress.CurrentProgressValue = pos; - }; - - reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => - { - if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) - { - reader.Cancel(); - progress.IsIndeterminate = true; - - reader.Dispose(); - redistributableStream.Dispose(); - } - }; - - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - } - catch (Exception ex) - { - if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) - { - Logger.Trace("User cancelled the download"); - - if (Directory.Exists(destination)) - { - Logger.Trace("Cleaning up orphaned install files after cancelled install..."); - - Directory.Delete(destination, true); - } - } - else - { - Logger.Error(ex, $"Could not extract to path {destination}"); - - if (Directory.Exists(destination)) - { - Logger.Trace("Cleaning up orphaned install files after bad install..."); - - Directory.Delete(destination, true); - } - - throw new Exception("The redistributable archive could not be extracted. Please try again or fix the archive!"); - } - } - }, - new GlobalProgressOptions($"Downloading {redistributable.Name}...") - { - IsIndeterminate = false, - Cancelable = true, - }); - - var extractionResult = new ExtractionResult - { - Canceled = result.Canceled - }; - - if (!result.Canceled) - { - extractionResult.Success = true; - extractionResult.Directory = destination; - Logger.Trace($"Redistributable successfully downloaded and extracted to {destination}"); - } - - return extractionResult; - } - - private string Download(LANCommander.SDK.Models.Game game) - { - string tempFile = String.Empty; - - if (game != null) { Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => { - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; + var redistributableManager = new RedistributableManager(Plugin.LANCommanderClient); - var destination = Plugin.LANCommander.DownloadGame(game.Id, (changed) => - { - progress.CurrentProgressValue = changed.ProgressPercentage; - }, (complete) => - { - progress.CurrentProgressValue = 100; - }); - - // Lock the thread until download is done - while (progress.CurrentProgressValue != 100) - { - - } - - tempFile = destination; + redistributableManager.Install(game); }, - new GlobalProgressOptions($"Downloading {game.Title}...") + new GlobalProgressOptions("Installing redistributables...") { - IsIndeterminate = false, + IsIndeterminate = true, Cancelable = false, }); - - return tempFile; } - else - throw new Exception("Game failed to download!"); - } - private string Extract(LANCommander.SDK.Models.Game game, string archivePath) - { - var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename()); - - Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => + if (!result.Canceled && result.Error == null && !String.IsNullOrWhiteSpace(installDirectory)) { - Directory.CreateDirectory(destination); + var manifest = ManifestHelper.Read(installDirectory); - using (var fs = File.OpenRead(archivePath)) - using (var ts = new TrackableStream(fs)) - using (var reader = ReaderFactory.Open(ts)) + Plugin.UpdateGame(manifest); + + var installInfo = new GameInstallationData { - progress.ProgressMaxValue = ts.Length; - ts.OnProgress += (pos, len) => - { - progress.CurrentProgressValue = pos; - }; + InstallDirectory = installDirectory, + }; - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - }, - new GlobalProgressOptions($"Extracting {game.Title}...") + RunInstallScript(installDirectory); + RunNameChangeScript(installDirectory); + RunKeyChangeScript(installDirectory); + + InvokeOnInstalled(new GameInstalledEventArgs(installInfo)); + } + else if (result.Canceled) { - IsIndeterminate = false, - Cancelable = false, - }); + var dbGame = Plugin.PlayniteApi.Database.Games.Get(Game.Id); - return destination; + dbGame.IsInstalling = false; + dbGame.IsInstalled = false; + + Plugin.PlayniteApi.Database.Games.Update(dbGame); + } + else if (result.Error != null) + throw result.Error; } - private void WriteManifest(SDK.GameManifest manifest, string installDirectory) + private int RunInstallScript(string installDirectory) { - var destination = Path.Combine(installDirectory, "_manifest.yml"); + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install); - Logger.Trace($"Attempting to write manifest to path {destination}"); + if (File.Exists(path)) + { + var script = new PowerShellScript(); - var serializer = new SerializerBuilder() - .WithNamingConvention(new PascalCaseNamingConvention()) - .Build(); + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory); + script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress); - Logger.Trace("Serializing manifest..."); - var yaml = serializer.Serialize(manifest); + script.UseFile(ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install)); - Logger.Trace("Writing manifest file..."); - File.WriteAllText(destination, yaml); + return script.Execute(); + } + + return 0; } - private string SaveTempScript(LANCommander.SDK.Models.Script script) + private int RunNameChangeScript(string installDirectory) { - var tempPath = Path.GetTempFileName(); + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.NameChange); - File.Move(tempPath, tempPath + ".ps1"); + if (File.Exists(path)) + { + var script = new PowerShellScript(); - tempPath = tempPath + ".ps1"; + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory); + script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress); + script.AddVariable("OldPlayerAlias", ""); + script.AddVariable("NewPlayerAlias", Plugin.Settings.PlayerName); - Logger.Trace($"Writing script {script.Name} to {tempPath}"); + script.UseFile(path); - File.WriteAllText(tempPath, script.Contents); + return script.Execute(); + } - return tempPath; + return 0; } - private void SaveScript(LANCommander.SDK.Models.Game game, string installationDirectory, ScriptType type) + private int RunKeyChangeScript(string installDirectory) { - var script = game.Scripts.FirstOrDefault(s => s.Type == type); + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.KeyChange); - if (script == null) - return; + if (File.Exists(path)) + { + var script = new PowerShellScript(); - if (script.RequiresAdmin) - script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents; + var key = Plugin.LANCommanderClient.GetAllocatedKey(manifest.Id); - var filename = PowerShellRuntime.GetScriptFilePath(PlayniteGame, type); + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory); + script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress); + script.AddVariable("AllocatedKey", key); - if (File.Exists(filename)) - File.Delete(filename); + script.UseFile(path); - Logger.Trace($"Writing {type} script to {filename}"); + return script.Execute(); + } - File.WriteAllText(filename, script.Contents); + return 0; } } } diff --git a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj index 20a9688..81642af 100644 --- a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj +++ b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj @@ -12,6 +12,7 @@ v4.6.2 512 true + true true @@ -34,9 +35,15 @@ ..\packages\rix0rrr.BeaconLib.1.0.2\lib\net40\BeaconLib.dll + + ..\packages\ByteSize.2.1.1\lib\net45\ByteSize.dll + ..\packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + ..\packages\Microsoft.Extensions.Logging.Abstractions.7.0.0\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll + ..\packages\PlayniteSDK.6.10.0\lib\net462\Playnite.SDK.dll @@ -45,8 +52,8 @@ ..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll - - ..\packages\SharpCompress.0.34.1\lib\net462\SharpCompress.dll + + ..\packages\SharpCompress.0.34.2\lib\net462\SharpCompress.dll @@ -93,21 +100,18 @@ ..\packages\YamlDotNet.5.4.0\lib\net45\YamlDotNet.dll + True ..\packages\ZstdSharp.Port.0.7.2\lib\net461\ZstdSharp.dll - - - - - + + - @@ -148,6 +152,10 @@ + + {807943bf-0c7d-4ed3-8393-cfee64e3138c} + LANCommander.PowerShell + {4c2a71fd-a30b-4d62-888a-4ef843d8e506} LANCommander.SDK diff --git a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs index 3f80ec0..46d67f9 100644 --- a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs +++ b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs @@ -1,5 +1,6 @@ using LANCommander.PlaynitePlugin.Extensions; -using LANCommander.PlaynitePlugin.Services; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.PowerShell; using Playnite.SDK; using Playnite.SDK.Events; using Playnite.SDK.Models; @@ -21,9 +22,8 @@ namespace LANCommander.PlaynitePlugin { public static readonly ILogger Logger = LogManager.GetLogger(); internal LANCommanderSettingsViewModel Settings { get; set; } - internal LANCommanderClient LANCommander { get; set; } - internal PowerShellRuntime PowerShellRuntime { get; set; } - internal GameSaveService GameSaveService { get; set; } + internal LANCommander.SDK.Client LANCommanderClient { get; set; } + internal LANCommanderSaveController SaveController { get; set; } public override Guid Id { get; } = Guid.Parse("48e1bac7-e0a0-45d7-ba83-36f5e9e959fc"); public override string Name => "LANCommander"; @@ -39,16 +39,14 @@ namespace LANCommander.PlaynitePlugin Settings = new LANCommanderSettingsViewModel(this); - LANCommander = new LANCommanderClient(Settings.ServerAddress); - LANCommander.Token = new SDK.Models.AuthToken() + LANCommanderClient = new SDK.Client(Settings.ServerAddress, new PlayniteLogger(Logger)); + LANCommanderClient.UseToken(new SDK.Models.AuthToken() { AccessToken = Settings.AccessToken, RefreshToken = Settings.RefreshToken, - }; + }); - PowerShellRuntime = new PowerShellRuntime(); - - GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime); + // GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime); api.UriHandler.RegisterSource("lancommander", args => { @@ -91,7 +89,7 @@ namespace LANCommander.PlaynitePlugin public bool ValidateConnection() { - return LANCommander.ValidateToken(LANCommander.Token); + return LANCommanderClient.ValidateToken(); } public override IEnumerable GetGames(LibraryGetGamesArgs args) @@ -111,7 +109,7 @@ namespace LANCommander.PlaynitePlugin } } - var games = LANCommander + var games = LANCommanderClient .GetGames() .Where(g => g != null && g.Archives != null && g.Archives.Count() > 0); @@ -121,7 +119,7 @@ namespace LANCommander.PlaynitePlugin { Logger.Trace($"Importing/updating metadata for game \"{game.Title}\"..."); - var manifest = LANCommander.GetGameManifest(game.Id); + var manifest = LANCommanderClient.GetGameManifest(game.Id); Logger.Trace("Successfully grabbed game manifest"); var existingGame = PlayniteApi.Database.Games.FirstOrDefault(g => g.GameId == game.Id.ToString() && g.PluginId == Id && g.IsInstalled); @@ -130,7 +128,7 @@ namespace LANCommander.PlaynitePlugin { Logger.Trace("Game already exists in library, updating metadata..."); - UpdateGame(manifest, game.Id); + UpdateGame(manifest); continue; } @@ -183,13 +181,13 @@ namespace LANCommander.PlaynitePlugin metadata.Features.Add(new MetadataNameProperty($"Online Multiplayer {manifest.OnlineMultiplayer.GetPlayerCount()}".Trim())); if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Icon)) - metadata.Icon = new MetadataFile(LANCommander.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Icon))); + metadata.Icon = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Icon))); if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Cover)) - metadata.CoverImage = new MetadataFile(LANCommander.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Cover))); + metadata.CoverImage = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Cover))); if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Background)) - metadata.BackgroundImage = new MetadataFile(LANCommander.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Background))); + metadata.BackgroundImage = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Background))); gameMetadata.Add(metadata); } @@ -224,9 +222,9 @@ namespace LANCommander.PlaynitePlugin if (args.Games.Count == 1 && args.Games.First().IsInstalled && !String.IsNullOrWhiteSpace(args.Games.First().InstallDirectory)) { - var nameChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.NameChange); - var keyChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.KeyChange); - var installScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.Install); + var nameChangeScriptPath = ScriptHelper.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.NameChange); + var keyChangeScriptPath = ScriptHelper.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.KeyChange); + var installScriptPath = ScriptHelper.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.Install); if (File.Exists(nameChangeScriptPath)) { @@ -243,8 +241,11 @@ namespace LANCommander.PlaynitePlugin if (result.Result == true) { - PowerShellRuntime.RunScript(nameChangeArgs.Games.First(), SDK.Enums.ScriptType.NameChange, $@"""{result.SelectedString}"" ""{oldName}"""); - LANCommander.ChangeAlias(result.SelectedString); + var game = nameChangeArgs.Games.First(); + + RunNameChangeScript(game.InstallDirectory, oldName, result.SelectedString); + + LANCommanderClient.ChangeAlias(result.SelectedString); } } }; @@ -264,12 +265,12 @@ namespace LANCommander.PlaynitePlugin if (Guid.TryParse(keyChangeArgs.Games.First().GameId, out gameId)) { // NUKIEEEE - var newKey = LANCommander.GetNewKey(gameId); + var newKey = LANCommanderClient.GetNewKey(gameId); if (String.IsNullOrEmpty(newKey)) PlayniteApi.Dialogs.ShowErrorMessage("There are no more keys available on the server.", "No Keys Available"); else - PowerShellRuntime.RunScript(keyChangeArgs.Games.First(), SDK.Enums.ScriptType.KeyChange, $@"""{newKey}"""); + RunKeyChangeScript(keyChangeArgs.Games.First().InstallDirectory, newKey); } else { @@ -291,13 +292,9 @@ namespace LANCommander.PlaynitePlugin Guid gameId; if (Guid.TryParse(installArgs.Games.First().GameId, out gameId)) - { - PowerShellRuntime.RunScript(installArgs.Games.First(), SDK.Enums.ScriptType.Install); - } + RunInstallScript(installArgs.Games.First().InstallDirectory); else - { PlayniteApi.Dialogs.ShowErrorMessage("This game could not be found on the server. Your game may be corrupted."); - } } }; } @@ -334,12 +331,42 @@ namespace LANCommander.PlaynitePlugin public override void OnGameStarting(OnGameStartingEventArgs args) { - GameSaveService.DownloadSave(args.Game); + if (args.Game.PluginId == Id) + { + var gameId = Guid.Parse(args.Game.GameId); + + LANCommanderClient.StartPlaySession(gameId); + + try + { + SaveController = new LANCommanderSaveController(this, args.Game); + SaveController.Download(args.Game); + } + catch (Exception ex) + { + Logger?.Error(ex, "Could not download save"); + } + } } public override void OnGameStopped(OnGameStoppedEventArgs args) { - GameSaveService.UploadSave(args.Game); + if (args.Game.PluginId == Id) + { + var gameId = Guid.Parse(args.Game.GameId); + + LANCommanderClient.EndPlaySession(gameId); + + try + { + SaveController = new LANCommanderSaveController(this, args.Game); + SaveController.Upload(args.Game); + } + catch (Exception ex) + { + Logger?.Error(ex, "Could not upload save"); + } + } } public override IEnumerable GetTopPanelItems() @@ -393,6 +420,8 @@ namespace LANCommander.PlaynitePlugin } else { + var oldName = Settings.PlayerName; + Settings.PlayerName = result.SelectedString; Logger.Trace($"New player name of \"{Settings.PlayerName}\" has been set!"); @@ -402,18 +431,28 @@ namespace LANCommander.PlaynitePlugin var games = PlayniteApi.Database.Games.Where(g => g.IsInstalled).ToList(); - LANCommander.ChangeAlias(result.SelectedString); + LANCommanderClient.ChangeAlias(result.SelectedString); Logger.Trace($"Running name change scripts across {games.Count} installed game(s)"); - PowerShellRuntime.RunScripts(games, SDK.Enums.ScriptType.NameChange, Settings.PlayerName); + foreach (var game in games) + { + var script = new PowerShellScript(); + + script.AddVariable("OldName", oldName); + script.AddVariable("NewName", Settings.PlayerName); + + script.UseFile(ScriptHelper.GetScriptFilePath(game.InstallDirectory, SDK.Enums.ScriptType.NameChange)); + + script.Execute(); + } } } else Logger.Trace("Name change was cancelled"); } - public Window ShowAuthenticationWindow(string serverAddress = null) + public Window ShowAuthenticationWindow(string serverAddress = null, EventHandler onClose = null) { Window window = null; Application.Current.Dispatcher.Invoke((Action)delegate @@ -435,15 +474,19 @@ namespace LANCommander.PlaynitePlugin window.Owner = PlayniteApi.Dialogs.GetCurrentAppWindow(); window.WindowStartupLocation = WindowStartupLocation.CenterOwner; window.ResizeMode = ResizeMode.NoResize; + + if (onClose != null) + window.Closed += onClose; + window.ShowDialog(); }); return window; } - public void UpdateGame(SDK.GameManifest manifest, Guid gameId) + public void UpdateGame(SDK.GameManifest manifest) { - var game = PlayniteApi.Database.Games.First(g => g.GameId == gameId.ToString()); + var game = PlayniteApi.Database.Games.FirstOrDefault(g => g.GameId == manifest?.Id.ToString()); if (game == null) return; @@ -525,5 +568,77 @@ namespace LANCommander.PlaynitePlugin PlayniteApi.Database.Games.Update(game); } + + private int RunInstallScript(string installDirectory) + { + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install); + + if (File.Exists(path)) + { + var script = new PowerShellScript(); + + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", Settings.InstallDirectory); + script.AddVariable("ServerAddress", Settings.ServerAddress); + + script.UseFile(path); + + return script.Execute(); + } + + return 0; + } + + private int RunNameChangeScript(string installDirectory, string oldPlayerAlias, string newPlayerAlias) + { + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.NameChange); + + if (File.Exists(path)) + { + var script = new PowerShellScript(); + + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", Settings.InstallDirectory); + script.AddVariable("ServerAddress", Settings.ServerAddress); + script.AddVariable("OldPlayerAlias", oldPlayerAlias); + script.AddVariable("NewPlayerAlias", newPlayerAlias); + + script.UseFile(path); + + return script.Execute(); + } + + return 0; + } + + private int RunKeyChangeScript(string installDirectory, string key = "") + { + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.KeyChange); + + if (File.Exists(path)) + { + var script = new PowerShellScript(); + + if (String.IsNullOrEmpty(key)) + key = LANCommanderClient.GetAllocatedKey(manifest.Id); + + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", Settings.InstallDirectory); + script.AddVariable("ServerAddress", Settings.ServerAddress); + script.AddVariable("AllocatedKey", key); + + script.UseFile(path); + + return script.Execute(); + } + + return 0; + } } } diff --git a/LANCommander.Playnite.Extension/PlayniteLogger.cs b/LANCommander.Playnite.Extension/PlayniteLogger.cs new file mode 100644 index 0000000..a570720 --- /dev/null +++ b/LANCommander.Playnite.Extension/PlayniteLogger.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LANCommander.PlaynitePlugin +{ + public sealed class PlayniteLogger : ILogger + { + private readonly Playnite.SDK.ILogger Logger; + + public PlayniteLogger(Playnite.SDK.ILogger logger) { + Logger = logger; + } + + public IDisposable BeginScope(TState state) + { + return default; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + switch (logLevel) + { + case LogLevel.Trace: + Logger?.Trace(formatter.Invoke(state, exception)); + break; + + case LogLevel.Debug: + Logger?.Debug(formatter.Invoke(state, exception)); + break; + + case LogLevel.Information: + Logger.Info(formatter.Invoke(state, exception)); + break; + + case LogLevel.Warning: + Logger.Warn(formatter.Invoke(state, exception)); + break; + + case LogLevel.Error: + case LogLevel.Critical: + Logger.Error(formatter.Invoke(state, exception)); + break; + } + } + } +} diff --git a/LANCommander.Playnite.Extension/PowerShellRuntime.cs b/LANCommander.Playnite.Extension/PowerShellRuntime.cs deleted file mode 100644 index 4b17858..0000000 --- a/LANCommander.Playnite.Extension/PowerShellRuntime.cs +++ /dev/null @@ -1,165 +0,0 @@ -using LANCommander.SDK.Enums; -using Playnite.SDK; -using Playnite.SDK.Models; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Runtime.InteropServices; -using System.Security.RightsManagement; -using System.Text; -using System.Threading.Tasks; - -namespace LANCommander.PlaynitePlugin -{ - internal class PowerShellRuntime - { - public static readonly ILogger Logger = LogManager.GetLogger(); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool Wow64RevertWow64FsRedirection(ref IntPtr ptr); - - public void RunCommand(string command, bool asAdmin = false) - { - Logger.Trace($"Executing command `{command}` | Admin: {asAdmin}"); - - var tempScript = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".ps1"); - - Logger.Trace($"Creating temp script at path {tempScript}"); - - File.WriteAllText(tempScript, command); - - RunScript(tempScript, asAdmin); - - File.Delete(tempScript); - } - - public int RunScript(string path, bool asAdmin = false, string arguments = null, string workingDirectory = null) - { - Logger.Trace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}"); - - var wow64Value = IntPtr.Zero; - - // Disable Wow64 redirection so we can hit areas of the registry absolutely - Wow64DisableWow64FsRedirection(ref wow64Value); - - var process = new Process(); - - process.StartInfo.FileName = "powershell.exe"; - process.StartInfo.Arguments = $@"-ExecutionPolicy Unrestricted -File ""{path}"""; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardOutput = false; - - if (arguments != null) - process.StartInfo.Arguments += " " + arguments; - - if (workingDirectory != null) - process.StartInfo.WorkingDirectory = workingDirectory; - - if (asAdmin) - { - process.StartInfo.Verb = "runas"; - process.StartInfo.UseShellExecute = true; - } - - process.Start(); - process.WaitForExit(); - - Wow64RevertWow64FsRedirection(ref wow64Value); - - return process.ExitCode; - } - - public void RunScript(Game game, ScriptType type, string arguments = null) - { - var path = GetScriptFilePath(game, type); - - if (File.Exists(path)) - { - var contents = File.ReadAllText(path); - - if (contents.StartsWith("# Requires Admin")) - RunScript(path, true, arguments); - else - RunScript(path, false, arguments); - } - } - - public void RunScriptsAsAdmin(IEnumerable paths, string arguments = null) - { - // Concatenate scripts - var sb = new StringBuilder(); - - Logger.Trace("Concatenating scripts..."); - - foreach (var path in paths) - { - var contents = File.ReadAllText(path); - - sb.AppendLine(contents); - - Logger.Trace($"Added {path}!"); - } - - Logger.Trace("Done concatenating!"); - - if (sb.Length > 0) - { - var scriptPath = Path.GetTempFileName(); - - Logger.Trace($"Creating temp script at path {scriptPath}"); - - File.WriteAllText(scriptPath, sb.ToString()); - - RunScript(scriptPath, true, arguments); - } - } - - public void RunScripts(IEnumerable games, ScriptType type, string arguments = null) - { - List scripts = new List(); - List adminScripts = new List(); - - foreach (var game in games) - { - var path = GetScriptFilePath(game, type); - - if (!File.Exists(path)) - continue; - - var contents = File.ReadAllText(path); - - if (contents.StartsWith("# Requires Admin")) - adminScripts.Add(path); - else - scripts.Add(path); - } - - RunScriptsAsAdmin(adminScripts, arguments); - - foreach (var script in scripts) - { - RunScript(script, false, arguments); - } - } - - public static string GetScriptFilePath(Game game, ScriptType type) - { - Dictionary filenames = new Dictionary() { - { ScriptType.Install, "_install.ps1" }, - { ScriptType.Uninstall, "_uninstall.ps1" }, - { ScriptType.NameChange, "_changename.ps1" }, - { ScriptType.KeyChange, "_changekey.ps1" } - }; - - var filename = filenames[type]; - - return Path.Combine(game.InstallDirectory, filename); - } - } -} diff --git a/LANCommander.Playnite.Extension/SaveController.cs b/LANCommander.Playnite.Extension/SaveController.cs new file mode 100644 index 0000000..557ee2e --- /dev/null +++ b/LANCommander.Playnite.Extension/SaveController.cs @@ -0,0 +1,61 @@ +using LANCommander.SDK; +using Playnite.SDK; +using Playnite.SDK.Models; +using Playnite.SDK.Plugins; + +namespace LANCommander.PlaynitePlugin +{ + public class LANCommanderSaveController : ControllerBase + { + private static readonly ILogger Logger; + + private LANCommanderLibraryPlugin Plugin; + + public LANCommanderSaveController(LANCommanderLibraryPlugin plugin, Game game) : base(game) + { + Name = "Download save using LANCommander"; + Plugin = plugin; + } + + public void Download(Game game) + { + if (game != null) + { + Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => + { + progress.ProgressMaxValue = 100; + progress.CurrentProgressValue = 0; + + var saveManager = new GameSaveManager(Plugin.LANCommanderClient); + + saveManager.OnDownloadProgress += (downloadProgress) => + { + progress.CurrentProgressValue = downloadProgress.ProgressPercentage; + }; + + saveManager.OnDownloadComplete += (downloadComplete) => + { + progress.CurrentProgressValue = 100; + }; + + saveManager.Download(game.InstallDirectory); + + // Lock the thread until the download is done + while (progress.CurrentProgressValue != 100) { } + }, + new GlobalProgressOptions("Downloading latest save...") + { + IsIndeterminate = false, + Cancelable = false + }); + } + } + + public void Upload(Game game) + { + var saveManager = new GameSaveManager(Plugin.LANCommanderClient); + + saveManager.Upload(game.InstallDirectory); + } + } +} diff --git a/LANCommander.Playnite.Extension/UninstallController.cs b/LANCommander.Playnite.Extension/UninstallController.cs index 3a77ee6..df9788f 100644 --- a/LANCommander.Playnite.Extension/UninstallController.cs +++ b/LANCommander.Playnite.Extension/UninstallController.cs @@ -1,4 +1,6 @@ using LANCommander.SDK.Enums; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.PowerShell; using Playnite.SDK; using Playnite.SDK.Models; using Playnite.SDK.Plugins; @@ -12,29 +14,49 @@ namespace LANCommander.PlaynitePlugin public static readonly ILogger Logger = LogManager.GetLogger(); private LANCommanderLibraryPlugin Plugin; - private PowerShellRuntime PowerShellRuntime; public LANCommanderUninstallController(LANCommanderLibraryPlugin plugin, Game game) : base(game) { Name = "Uninstall LANCommander Game"; Plugin = plugin; - PowerShellRuntime = new PowerShellRuntime(); } public override void Uninstall(UninstallActionArgs args) { try { - PowerShellRuntime.RunScript(Game, ScriptType.Uninstall); + var gameManager = new LANCommander.SDK.GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory); + + try + { + var scriptPath = ScriptHelper.GetScriptFilePath(Game.InstallDirectory, SDK.Enums.ScriptType.Uninstall); + + if (!String.IsNullOrEmpty(scriptPath) && File.Exists(scriptPath)) + { + var manifest = ManifestHelper.Read(Game.InstallDirectory); + var script = new PowerShellScript(); + + script.AddVariable("InstallDirectory", Game.InstallDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory); + script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress); + + script.UseFile(scriptPath); + + script.Execute(); + } + } + catch (Exception ex) + { + Logger.Error(ex, "There was an error running the uninstall script"); + } + + gameManager.Uninstall(Game.InstallDirectory); + } + catch (Exception ex) + { + Logger.Error(ex, "There was an error uninstalling the game"); } - catch { } - - Logger.Trace("Attempting to delete install directory..."); - - if (!String.IsNullOrWhiteSpace(Game.InstallDirectory) && Directory.Exists(Game.InstallDirectory)) - Directory.Delete(Game.InstallDirectory, true); - - Logger.Trace("Deleted!"); InvokeOnUninstalled(new GameUninstalledEventArgs()); } diff --git a/LANCommander.Playnite.Extension/Views/Authentication.xaml.cs b/LANCommander.Playnite.Extension/Views/Authentication.xaml.cs index 6b3db4f..51bab38 100644 --- a/LANCommander.Playnite.Extension/Views/Authentication.xaml.cs +++ b/LANCommander.Playnite.Extension/Views/Authentication.xaml.cs @@ -100,24 +100,18 @@ namespace LANCommander.PlaynitePlugin.Views LoginButton.Content = "Logging in..."; })); - if (Plugin.LANCommander == null || Plugin.LANCommander.Client == null) - Plugin.LANCommander = new LANCommanderClient(Context.ServerAddress); + if (Plugin.LANCommanderClient == null) + Plugin.LANCommanderClient = new LANCommander.SDK.Client(Context.ServerAddress); else - Plugin.LANCommander.Client.BaseUrl = new Uri(Context.ServerAddress); + Plugin.LANCommanderClient.UseServerAddress(Context.ServerAddress); - var response = await Plugin.LANCommander.AuthenticateAsync(Context.UserName, Context.Password); + var response = await Plugin.LANCommanderClient.AuthenticateAsync(Context.UserName, Context.Password); Plugin.Settings.ServerAddress = Context.ServerAddress; Plugin.Settings.AccessToken = response.AccessToken; Plugin.Settings.RefreshToken = response.RefreshToken; - Plugin.LANCommander.Token = new AuthToken() - { - AccessToken = response.AccessToken, - RefreshToken = response.RefreshToken, - }; - - var profile = Plugin.LANCommander.GetProfile(); + var profile = Plugin.LANCommanderClient.GetProfile(); Plugin.Settings.PlayerName = String.IsNullOrWhiteSpace(profile.Alias) ? profile.UserName : profile.Alias; @@ -130,6 +124,8 @@ namespace LANCommander.PlaynitePlugin.Views } catch (Exception ex) { + Logger.Error(ex, ex.Message); + Plugin.PlayniteApi.Dialogs.ShowErrorMessage(ex.Message); LoginButton.Dispatcher.Invoke(new System.Action(() => @@ -148,24 +144,16 @@ namespace LANCommander.PlaynitePlugin.Views RegisterButton.IsEnabled = false; RegisterButton.Content = "Working..."; - if (Plugin.LANCommander == null || Plugin.LANCommander.Client == null) - Plugin.LANCommander = new LANCommanderClient(Context.ServerAddress); - else - Plugin.LANCommander.Client.BaseUrl = new Uri(Context.ServerAddress); + if (Plugin.LANCommanderClient == null) + Plugin.LANCommanderClient = new LANCommander.SDK.Client(Context.ServerAddress); - var response = await Plugin.LANCommander.RegisterAsync(Context.UserName, Context.Password); + var response = await Plugin.LANCommanderClient.RegisterAsync(Context.UserName, Context.Password); Plugin.Settings.ServerAddress = Context.ServerAddress; Plugin.Settings.AccessToken = response.AccessToken; Plugin.Settings.RefreshToken = response.RefreshToken; Plugin.Settings.PlayerName = Context.UserName; - Plugin.LANCommander.Token = new AuthToken() - { - AccessToken = response.AccessToken, - RefreshToken = response.RefreshToken, - }; - Context.Password = String.Empty; Plugin.SavePluginSettings(Plugin.Settings); diff --git a/LANCommander.Playnite.Extension/Views/LANCommanderSettingsView.xaml.cs b/LANCommander.Playnite.Extension/Views/LANCommanderSettingsView.xaml.cs index 2388840..ef2063d 100644 --- a/LANCommander.Playnite.Extension/Views/LANCommanderSettingsView.xaml.cs +++ b/LANCommander.Playnite.Extension/Views/LANCommanderSettingsView.xaml.cs @@ -47,7 +47,7 @@ namespace LANCommander.PlaynitePlugin RefreshToken = Settings.RefreshToken, }; - var task = Task.Run(() => Plugin.LANCommander.ValidateToken(token)) + var task = Task.Run(() => Plugin.LANCommanderClient.ValidateToken(token)) .ContinueWith(antecedent => { try @@ -81,16 +81,14 @@ namespace LANCommander.PlaynitePlugin private void AuthenticateButton_Click(object sender, RoutedEventArgs e) { - var authWindow = Plugin.ShowAuthenticationWindow(); - - authWindow.Closed += AuthWindow_Closed; + var authWindow = Plugin.ShowAuthenticationWindow(Settings.ServerAddress, AuthWindow_Closed); } private void DisconnectButton_Click(object sender, RoutedEventArgs e) { Plugin.Settings.AccessToken = String.Empty; Plugin.Settings.RefreshToken = String.Empty; - Plugin.LANCommander.Token = null; + Plugin.LANCommanderClient.UseToken(null); Plugin.SavePluginSettings(Plugin.Settings); diff --git a/LANCommander.Playnite.Extension/installermanifest.yaml b/LANCommander.Playnite.Extension/installermanifest.yaml index 62d3e15..1928b4d 100644 --- a/LANCommander.Playnite.Extension/installermanifest.yaml +++ b/LANCommander.Playnite.Extension/installermanifest.yaml @@ -28,3 +28,13 @@ Packages: - LANCommander servers can now provide game art to Playnite clients - Added server address to addon settings - Added disconnect button to addon settings + - Version: 0.2.2 + RequiredApiVersion: 6.0.0 + ReleaseDate: 2023-11-20 + PackageUrl: https://github.com/LANCommander/LANCommander/releases/download/v0.2.2/LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc_0_2_2.pext + Changelog: + - _manifest.yml files in the game's install directory now includes the game's ID + - Installation progress dialog now shows the download percentage and transfer speed + - Full game download will be skipped if the game files already exist, see full release notes for more details + - Play sessions are now recorded to the server with user ID, start time, and end time + - Connection status now updates correctly when authenticating through addon settings \ No newline at end of file diff --git a/LANCommander.Playnite.Extension/packages.config b/LANCommander.Playnite.Extension/packages.config index 48c1616..900ade3 100644 --- a/LANCommander.Playnite.Extension/packages.config +++ b/LANCommander.Playnite.Extension/packages.config @@ -1,12 +1,14 @@  + + - + diff --git a/LANCommander.PowerShell.Tests/Cmdlets.cs b/LANCommander.PowerShell.Tests/Cmdlets.cs new file mode 100644 index 0000000..b56e654 --- /dev/null +++ b/LANCommander.PowerShell.Tests/Cmdlets.cs @@ -0,0 +1,64 @@ +using LANCommander.PowerShell.Cmdlets; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; + +namespace LANCommander.PowerShell.Tests +{ + [TestClass] + public class CmdletTests + { + [TestMethod] + public void ConvertToSerializedBase64ShouldBeDeserializable() + { + var testPhrase = "Hello world! This should be deserializable back to its original form."; + + var encodingCmdlet = new ConvertToSerializedBase64Cmdlet() + { + Input = testPhrase + }; + + var encodingResults = encodingCmdlet.Invoke().OfType().ToList(); + + Assert.AreEqual(1, encodingResults.Count); + + var decodingCmdlet = new ConvertFromSerializedBase64Cmdlet() + { + Input = encodingResults.First() + }; + + var decodingResults = decodingCmdlet.Invoke().OfType().ToList(); + + Assert.AreEqual(1, encodingResults.Count); + Assert.AreEqual(testPhrase, decodingResults.First()); + } + + [TestMethod] + [DataRow(640, 480, 640, 360, 16, 9)] + [DataRow(1024, 768, 1024, 576, 16, 9)] + [DataRow(1600, 1200, 1600, 900, 16, 9)] + [DataRow(1920, 1080, 1440, 1080, 4, 3)] + [DataRow(1366, 1024, 1024, 768, 4, 3)] + [DataRow(854, 480, 640, 480, 4, 3)] + public void ConvertAspectRatioShouldReturnCorrectBounds(int x1, int y1, int x2, int y2, int ratioX, int ratioY) + { + var aspectRatio = (double)ratioX / (double)ratioY; + + var cmdlet = new ConvertAspectRatioCmdlet() + { + AspectRatio = aspectRatio, + Width = x1, + Height = y1 + }; + + var output = cmdlet.Invoke().OfType().ToList(); + + Assert.AreEqual(1, output.Count); + + var bounds = output.First(); + + Assert.AreEqual(x2, bounds.Width); + Assert.AreEqual(y2, bounds.Height); + } + } +} diff --git a/LANCommander.PowerShell.Tests/LANCommander.PowerShell.Tests.csproj b/LANCommander.PowerShell.Tests/LANCommander.PowerShell.Tests.csproj new file mode 100644 index 0000000..af5df94 --- /dev/null +++ b/LANCommander.PowerShell.Tests/LANCommander.PowerShell.Tests.csproj @@ -0,0 +1,75 @@ + + + + + + Debug + AnyCPU + {D7069A13-F0AA-4CBF-9013-4276F130A6DD} + Library + Properties + LANCommander.PowerShell.Tests + LANCommander.PowerShell.Tests + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + {807943bf-0c7d-4ed3-8393-cfee64e3138c} + LANCommander.PowerShell + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/LANCommander.PowerShell.Tests/Properties/AssemblyInfo.cs b/LANCommander.PowerShell.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..82c8b70 --- /dev/null +++ b/LANCommander.PowerShell.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("LANCommander.PowerShell.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LANCommander.PowerShell.Tests")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("d7069a13-f0aa-4cbf-9013-4276f130a6dd")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LANCommander.PowerShell.Tests/packages.config b/LANCommander.PowerShell.Tests/packages.config new file mode 100644 index 0000000..e47cc4d --- /dev/null +++ b/LANCommander.PowerShell.Tests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/LANCommander.PowerShell/Cmdlets/Convert-AspectRatio.cs b/LANCommander.PowerShell/Cmdlets/Convert-AspectRatio.cs new file mode 100644 index 0000000..a31949a --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/Convert-AspectRatio.cs @@ -0,0 +1,45 @@ +using System; +using System.Management.Automation; + +namespace LANCommander.PowerShell.Cmdlets +{ + public class DisplayResolution + { + public int Width { get; set; } + public int Height { get; set; } + } + + [Cmdlet(VerbsData.Convert, "AspectRatio")] + [OutputType(typeof(string))] + public class ConvertAspectRatioCmdlet : Cmdlet + { + [Parameter(Mandatory = true, Position = 0)] + public int Width { get; set; } + + [Parameter(Mandatory = true, Position = 1)] + public int Height { get; set; } + + [Parameter(Mandatory = true, Position = 2)] + public double AspectRatio { get; set; } + + protected override void ProcessRecord() + { + var resolution = new DisplayResolution(); + + // Display is wider, pillar box + if ((Width / Height) < AspectRatio) + { + resolution.Width = (int)Math.Ceiling(Height * AspectRatio); + resolution.Height = Height; + } + // Letterbox + else + { + resolution.Width = Width; + resolution.Height = (int)Math.Ceiling(Width * (1 / AspectRatio)); + } + + WriteObject(resolution); + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs b/LANCommander.PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs new file mode 100644 index 0000000..d600b38 --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsData.ConvertFrom, "SerializedBase64")] + [OutputType(typeof(object))] + public class ConvertFromSerializedBase64Cmdlet : Cmdlet + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + public string Input { get; set; } + + protected override void ProcessRecord() + { + var xml = Encoding.UTF8.GetString(Convert.FromBase64String(Input)); + + WriteObject(PSSerializer.Deserialize(xml)); + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs b/LANCommander.PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs new file mode 100644 index 0000000..fe92654 --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsData.ConvertTo, "SerializedBase64")] + [OutputType(typeof(object))] + public class ConvertToSerializedBase64Cmdlet : Cmdlet + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + public object Input { get; set; } + + protected override void ProcessRecord() + { + var output = Convert.ToBase64String(Encoding.UTF8.GetBytes(PSSerializer.Serialize(Input))); + + WriteObject(output); + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/ConvertTo-StringBytes.cs b/LANCommander.PowerShell/Cmdlets/ConvertTo-StringBytes.cs new file mode 100644 index 0000000..dced3b9 --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/ConvertTo-StringBytes.cs @@ -0,0 +1,40 @@ +using LANCommander.SDK; +using LANCommander.SDK.Helpers; +using System.Management.Automation; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsData.ConvertTo, "StringBytes")] + [OutputType(typeof(byte[]))] + public class ConvertToStringBytesCmdlet : Cmdlet + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + public string Input { get; set; } + + [Parameter] + public bool Utf16 { get; set; } = false; + + [Parameter] + public bool BigEndian { get; set; } = false; + + [Parameter] + public int MaxLength { get; set; } = 0; + + protected override void ProcessRecord() + { + byte[] output; + + if (MaxLength > 0 && Input.Length > MaxLength) + Input = Input.Substring(0, MaxLength); + + if (Utf16 && BigEndian) + output = System.Text.Encoding.BigEndianUnicode.GetBytes(Input); + else if (Utf16) + output = System.Text.Encoding.Unicode.GetBytes(Input); + else + output = System.Text.Encoding.ASCII.GetBytes(Input); + + WriteObject(output); + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/Edit-PatchBinary.cs b/LANCommander.PowerShell/Cmdlets/Edit-PatchBinary.cs new file mode 100644 index 0000000..95e2e46 --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/Edit-PatchBinary.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using System.Management.Automation; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsData.Edit, "PatchBinary")] + [OutputType(typeof(string))] + public class EditPatchBinaryCmdlet : Cmdlet + { + [Parameter(Mandatory = true, Position = 0)] + public long Offset { get; set; } + + [Parameter(Mandatory = true, Position = 1)] + public byte[] Data { get; set; } + + [Parameter(Mandatory = true, Position = 2)] + public string FilePath { get; set; } + + protected override void ProcessRecord() + { + using (var writer = File.OpenWrite(FilePath)) + { + writer.Seek(Offset, SeekOrigin.Begin); + + writer.Write(Data, 0, Data.Length); + } + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/Get-GameManifest.cs b/LANCommander.PowerShell/Cmdlets/Get-GameManifest.cs new file mode 100644 index 0000000..d11ea2b --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/Get-GameManifest.cs @@ -0,0 +1,19 @@ +using LANCommander.SDK; +using LANCommander.SDK.Helpers; +using System.Management.Automation; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "GameManifest")] + [OutputType(typeof(GameManifest))] + public class GetGameManifestCmdlet : Cmdlet + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + public string Path { get; set; } + + protected override void ProcessRecord() + { + WriteObject(ManifestHelper.Read(Path)); + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/Get-PrimaryDisplay.cs b/LANCommander.PowerShell/Cmdlets/Get-PrimaryDisplay.cs new file mode 100644 index 0000000..93c838e --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/Get-PrimaryDisplay.cs @@ -0,0 +1,18 @@ +using System.Linq; +using System.Management.Automation; +using System.Windows.Forms; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "PrimaryDisplay")] + [OutputType(typeof(string))] + public class GetPrimaryDisplayCmdlet : Cmdlet + { + protected override void ProcessRecord() + { + var screens = Screen.AllScreens; + + WriteObject(screens.First(s => s.Primary)); + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/Install-Game.cs b/LANCommander.PowerShell/Cmdlets/Install-Game.cs new file mode 100644 index 0000000..c24b5ed --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/Install-Game.cs @@ -0,0 +1,132 @@ +using LANCommander.SDK; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.PowerShell; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Windows.Forms; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsLifecycle.Install, "Game")] + [OutputType(typeof(string))] + public class InstallGameCmdlet : Cmdlet + { + [Parameter(Mandatory = true)] + public Client Client { get; set; } + + [Parameter(Mandatory = true)] + public Guid Id { get; set; } + + [Parameter(Mandatory = false)] + public string InstallDirectory { get; set; } = "C:\\Games"; + + protected override void ProcessRecord() + { + var gameManager = new GameManager(Client, InstallDirectory); + var game = Client.GetGame(Id); + + var progress = new ProgressRecord(1, $"Installing {game.Title}", "Progress:"); + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + gameManager.OnArchiveExtractionProgress += (long position, long length) => + { + // Only update a max of every 500ms + if (stopwatch.ElapsedMilliseconds > 500) + { + progress.PercentComplete = (int)Math.Ceiling((position / (decimal)length) * 100); + + WriteProgress(progress); + + stopwatch.Restart(); + } + }; + + var installDirectory = gameManager.Install(Id); + + stopwatch.Stop(); + + RunInstallScript(installDirectory); + RunNameChangeScript(installDirectory); + RunKeyChangeScript(installDirectory); + + WriteObject(installDirectory); + } + + private int RunInstallScript(string installDirectory) + { + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install); + + if (File.Exists(path)) + { + var script = new PowerShellScript(); + + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", InstallDirectory); + script.AddVariable("ServerAddress", Client.BaseUrl); + + script.UseFile(ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install)); + + return script.Execute(); + } + + return 0; + } + + private int RunNameChangeScript(string installDirectory) + { + var user = Client.GetProfile(); + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.NameChange); + + if (File.Exists(path)) + { + var script = new PowerShellScript(); + + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", InstallDirectory); + script.AddVariable("ServerAddress", Client.BaseUrl); + script.AddVariable("OldPlayerAlias", ""); + script.AddVariable("NewPlayerAlias", user.UserName); + + script.UseFile(path); + + return script.Execute(); + } + + return 0; + } + + private int RunKeyChangeScript(string installDirectory) + { + var manifest = ManifestHelper.Read(installDirectory); + var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.KeyChange); + + if (File.Exists(path)) + { + var script = new PowerShellScript(); + + var key = Client.GetAllocatedKey(manifest.Id); + + script.AddVariable("InstallDirectory", installDirectory); + script.AddVariable("GameManifest", manifest); + script.AddVariable("DefaultInstallDirectory", InstallDirectory); + script.AddVariable("ServerAddress", Client.BaseUrl); + script.AddVariable("AllocatedKey", key); + + script.UseFile(path); + + return script.Execute(); + } + + return 0; + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/Uninstall-Game.cs b/LANCommander.PowerShell/Cmdlets/Uninstall-Game.cs new file mode 100644 index 0000000..27b3dc4 --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/Uninstall-Game.cs @@ -0,0 +1,42 @@ +using LANCommander.SDK; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.PowerShell; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Windows.Forms; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsLifecycle.Uninstall, "Game")] + [OutputType(typeof(string))] + public class UninstallGameCmdlet : Cmdlet + { + [Parameter(Mandatory = true)] + public string InstallDirectory { get; set; } + + protected override void ProcessRecord() + { + var scriptPath = ScriptHelper.GetScriptFilePath(InstallDirectory, SDK.Enums.ScriptType.Uninstall); + + if (!String.IsNullOrEmpty(scriptPath) && File.Exists(scriptPath)) + { + var manifest = ManifestHelper.Read(InstallDirectory); + var script = new PowerShellScript(); + + script.AddVariable("InstallDirectory", InstallDirectory); + script.AddVariable("GameManifest", manifest); + + script.UseFile(scriptPath); + + script.Execute(); + } + + var gameManager = new GameManager(null, InstallDirectory); + + gameManager.Uninstall(InstallDirectory); + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/Write-GameManifest.cs b/LANCommander.PowerShell/Cmdlets/Write-GameManifest.cs new file mode 100644 index 0000000..28ff74d --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/Write-GameManifest.cs @@ -0,0 +1,24 @@ +using LANCommander.SDK; +using LANCommander.SDK.Helpers; +using System.Management.Automation; + +namespace LANCommander.PowerShell.Cmdlets +{ + [Cmdlet(VerbsCommunications.Write, "GameManifest")] + [OutputType(typeof(string))] + public class WriteGameManifestCmdlet : Cmdlet + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + public string Path { get; set; } + + [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + public GameManifest Manifest { get; set; } + + protected override void ProcessRecord() + { + var destination = ManifestHelper.Write(Manifest, Path); + + WriteObject(destination); + } + } +} diff --git a/LANCommander.PowerShell/Cmdlets/Write-ReplaceContentInFile.cs b/LANCommander.PowerShell/Cmdlets/Write-ReplaceContentInFile.cs new file mode 100644 index 0000000..5160e91 --- /dev/null +++ b/LANCommander.PowerShell/Cmdlets/Write-ReplaceContentInFile.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Management.Automation; +using System.Text.RegularExpressions; + +namespace LANCommander.PowerShell.Cmdlets +{ + + [Cmdlet(VerbsCommunications.Write, "ReplaceContentInFile")] + [OutputType(typeof(string))] + public class ReplaceContentInFileCmdlet : Cmdlet + { + [Parameter(Mandatory = true, Position = 0)] + public string Pattern { get; set; } + + [Parameter(Mandatory = true, Position = 1)] + public string Substitution { get; set; } + + [Parameter(Mandatory = true, Position = 2)] + public string FilePath { get; set; } + + protected override void ProcessRecord() + { + if (File.Exists(FilePath)) + { + var contents = File.ReadAllText(FilePath); + var regex = new Regex(Pattern, RegexOptions.Multiline); + + contents = regex.Replace(contents, Substitution); + + File.WriteAllText(FilePath, contents); + + WriteObject(contents); + } + } + } +} diff --git a/LANCommander.PowerShell/LANCommander.PowerShell.csproj b/LANCommander.PowerShell/LANCommander.PowerShell.csproj new file mode 100644 index 0000000..b59a2f6 --- /dev/null +++ b/LANCommander.PowerShell/LANCommander.PowerShell.csproj @@ -0,0 +1,75 @@ + + + + + Debug + AnyCPU + {807943BF-0C7D-4ED3-8393-CFEE64E3138C} + Library + Properties + LANCommander.PowerShell + LANCommander.PowerShell + v4.6.2 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + ..\packages\PowerShellStandard.Library.5.1.1\lib\net452\System.Management.Automation.dll + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + {4c2a71fd-a30b-4d62-888a-4ef843d8e506} + LANCommander.SDK + + + + \ No newline at end of file diff --git a/LANCommander.PowerShell/LANCommander.PowerShell.psd1 b/LANCommander.PowerShell/LANCommander.PowerShell.psd1 new file mode 100644 index 0000000..53c9b8a Binary files /dev/null and b/LANCommander.PowerShell/LANCommander.PowerShell.psd1 differ diff --git a/LANCommander.PowerShell/Properties/AssemblyInfo.cs b/LANCommander.PowerShell/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..87468b6 --- /dev/null +++ b/LANCommander.PowerShell/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LANCommander.PowerShell")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LANCommander.PowerShell")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("807943bf-0c7d-4ed3-8393-cfee64e3138c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LANCommander.PowerShell/packages.config b/LANCommander.PowerShell/packages.config new file mode 100644 index 0000000..411c02f --- /dev/null +++ b/LANCommander.PowerShell/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/LANCommander.Playnite.Extension/LANCommanderClient.cs b/LANCommander.SDK/Client.cs similarity index 65% rename from LANCommander.Playnite.Extension/LANCommanderClient.cs rename to LANCommander.SDK/Client.cs index e2cdfd1..d299672 100644 --- a/LANCommander.Playnite.Extension/LANCommanderClient.cs +++ b/LANCommander.SDK/Client.cs @@ -1,6 +1,6 @@ using LANCommander.SDK; using LANCommander.SDK.Models; -using Playnite.SDK; +using Microsoft.Extensions.Logging; using RestSharp; using System; using System.Collections.Generic; @@ -11,19 +11,33 @@ using System.Net; using System.Net.NetworkInformation; using System.Threading.Tasks; -namespace LANCommander.PlaynitePlugin +namespace LANCommander.SDK { - internal class LANCommanderClient + public class Client { - public static readonly ILogger Logger = LogManager.GetLogger(); + private readonly ILogger Logger; - public readonly RestClient Client; - public AuthToken Token; + private RestClient ApiClient; + private AuthToken Token; - public LANCommanderClient(string baseUrl) + public string BaseUrl; + + public Client(string baseUrl) { - if (!String.IsNullOrWhiteSpace(baseUrl)) - Client = new RestClient(baseUrl); + BaseUrl = baseUrl; + + if (!String.IsNullOrWhiteSpace(BaseUrl)) + ApiClient = new RestClient(BaseUrl); + } + + public Client(string baseUrl, ILogger logger) + { + BaseUrl = baseUrl; + + if (!String.IsNullOrWhiteSpace(BaseUrl)) + ApiClient = new RestClient(BaseUrl); + + Logger = logger; } private T PostRequest(string route, object body) @@ -32,7 +46,17 @@ namespace LANCommander.PlaynitePlugin .AddJsonBody(body) .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); - var response = Client.Post(request); + var response = ApiClient.Post(request); + + return response.Data; + } + + private T PostRequest(string route) + { + var request = new RestRequest(route) + .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); + + var response = ApiClient.Post(request); return response.Data; } @@ -42,7 +66,7 @@ namespace LANCommander.PlaynitePlugin var request = new RestRequest(route) .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); - var response = Client.Get(request); + var response = ApiClient.Get(request); return response.Data; } @@ -58,7 +82,7 @@ namespace LANCommander.PlaynitePlugin client.DownloadProgressChanged += (s, e) => progressHandler(e); client.DownloadFileCompleted += (s, e) => completeHandler(e); - client.DownloadFileAsync(new Uri($"{Client.BaseUrl}{route}"), tempFile); + client.DownloadFileAsync(new Uri($"{ApiClient.BaseUrl}{route}"), tempFile); return tempFile; } @@ -72,14 +96,14 @@ namespace LANCommander.PlaynitePlugin client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); - var ws = client.OpenRead(new Uri($"{Client.BaseUrl}{route}")); + var ws = client.OpenRead(new Uri($"{ApiClient.BaseUrl}{route}")); return new TrackableStream(ws, true, Convert.ToInt64(client.ResponseHeaders["Content-Length"])); } - public async Task AuthenticateAsync(string username, string password) + public async Task AuthenticateAsync(string username, string password) { - var response = await Client.ExecuteAsync(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest() + var response = await ApiClient.ExecuteAsync(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest() { UserName = username, Password = password @@ -88,7 +112,14 @@ namespace LANCommander.PlaynitePlugin switch (response.StatusCode) { case HttpStatusCode.OK: - return response.Data; + Token = new AuthToken + { + AccessToken = response.Data.AccessToken, + RefreshToken = response.Data.RefreshToken, + Expiration = response.Data.Expiration + }; + + return Token; case HttpStatusCode.Forbidden: case HttpStatusCode.BadRequest: @@ -100,9 +131,9 @@ namespace LANCommander.PlaynitePlugin } } - public async Task RegisterAsync(string username, string password) + public async Task RegisterAsync(string username, string password) { - var response = await Client.ExecuteAsync(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest() + var response = await ApiClient.ExecuteAsync(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest() { UserName = username, Password = password @@ -111,7 +142,14 @@ namespace LANCommander.PlaynitePlugin switch (response.StatusCode) { case HttpStatusCode.OK: - return response.Data; + Token = new AuthToken + { + AccessToken = response.Data.AccessToken, + RefreshToken = response.Data.RefreshToken, + Expiration = response.Data.Expiration + }; + + return Token; case HttpStatusCode.BadRequest: case HttpStatusCode.Forbidden: @@ -125,33 +163,45 @@ namespace LANCommander.PlaynitePlugin public async Task PingAsync() { - var response = await Client.ExecuteAsync(new RestRequest("/api/Ping", Method.GET)); + var response = await ApiClient.ExecuteAsync(new RestRequest("/api/Ping", Method.GET)); return response.StatusCode == HttpStatusCode.OK; } - public AuthResponse RefreshToken(AuthToken token) + public AuthToken RefreshToken(AuthToken token) { - Logger.Trace("Refreshing token..."); + Logger?.LogTrace("Refreshing token..."); var request = new RestRequest("/api/Auth/Refresh") .AddJsonBody(token); - var response = Client.Post(request); + var response = ApiClient.Post(request); if (response.StatusCode != HttpStatusCode.OK) throw new WebException(response.ErrorMessage); - return response.Data; + Token = new AuthToken + { + AccessToken = response.Data.AccessToken, + RefreshToken = response.Data.RefreshToken, + Expiration = response.Data.Expiration + }; + + return Token; + } + + public bool ValidateToken() + { + return ValidateToken(Token); } public bool ValidateToken(AuthToken token) { - Logger.Trace("Validating token..."); + Logger?.LogTrace("Validating token..."); if (token == null) { - Logger.Trace("Token is null!"); + Logger?.LogTrace("Token is null!"); return false; } @@ -160,22 +210,33 @@ namespace LANCommander.PlaynitePlugin if (String.IsNullOrEmpty(token.AccessToken) || String.IsNullOrEmpty(token.RefreshToken)) { - Logger.Trace("Token is empty!"); + Logger?.LogTrace("Token is empty!"); return false; } - var response = Client.Post(request); + var response = ApiClient.Post(request); var valid = response.StatusCode == HttpStatusCode.OK; if (valid) - Logger.Trace("Token is valid!"); + Logger?.LogTrace("Token is valid!"); else - Logger.Trace("Token is invalid!"); + Logger?.LogTrace("Token is invalid!"); return response.StatusCode == HttpStatusCode.OK; } + public void UseToken(AuthToken token) + { + Token = token; + } + + public void UseServerAddress(string address) + { + BaseUrl = address; + ApiClient = new RestClient(BaseUrl); + } + public IEnumerable GetGames() { return GetRequest>("/api/Games"); @@ -223,26 +284,26 @@ namespace LANCommander.PlaynitePlugin public GameSave UploadSave(string gameId, byte[] data) { - Logger.Trace("Uploading save..."); + Logger?.LogTrace("Uploading save..."); var request = new RestRequest($"/api/Saves/Upload/{gameId}", Method.POST) .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); request.AddFile(gameId, data, gameId); - var response = Client.Post(request); + var response = ApiClient.Post(request); return response.Data; } public string GetMediaUrl(Media media) { - return (new Uri(Client.BaseUrl, $"/api/Media/{media.Id}/Download?fileId={media.FileId}").ToString()); + return (new Uri(ApiClient.BaseUrl, $"/api/Media/{media.Id}/Download?fileId={media.FileId}").ToString()); } public string GetKey(Guid id) { - Logger.Trace("Requesting key allocation..."); + Logger?.LogTrace("Requesting key allocation..."); var macAddress = GetMacAddress(); @@ -261,7 +322,7 @@ namespace LANCommander.PlaynitePlugin public string GetAllocatedKey(Guid id) { - Logger.Trace("Requesting allocated key..."); + Logger?.LogTrace("Requesting allocated key..."); var macAddress = GetMacAddress(); @@ -283,7 +344,7 @@ namespace LANCommander.PlaynitePlugin public string GetNewKey(Guid id) { - Logger.Trace("Requesting new key allocation..."); + Logger?.LogTrace("Requesting new key allocation..."); var macAddress = GetMacAddress(); @@ -305,20 +366,34 @@ namespace LANCommander.PlaynitePlugin public User GetProfile() { - Logger.Trace("Requesting player's profile..."); + Logger?.LogTrace("Requesting player's profile..."); return GetRequest("/api/Profile"); } public string ChangeAlias(string alias) { - Logger.Trace("Requesting to change player alias..."); + Logger?.LogTrace("Requesting to change player alias..."); var response = PostRequest("/api/Profile/ChangeAlias", alias); return alias; } + public void StartPlaySession(Guid gameId) + { + Logger?.LogTrace("Starting a game session..."); + + PostRequest($"/api/PlaySessions/Start/{gameId}"); + } + + public void EndPlaySession(Guid gameId) + { + Logger?.LogTrace("Ending a game session..."); + + PostRequest($"/api/PlaySessions/End/{gameId}"); + } + private string GetMacAddress() { return NetworkInterface.GetAllNetworkInterfaces() diff --git a/LANCommander.SDK/EventArgs.cs b/LANCommander.SDK/EventArgs.cs new file mode 100644 index 0000000..c0af51a --- /dev/null +++ b/LANCommander.SDK/EventArgs.cs @@ -0,0 +1,20 @@ +using SharpCompress.Common; +using SharpCompress.Readers; +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK +{ + public class ArchiveExtractionProgressArgs : EventArgs + { + public long Position { get; set; } + public long Length { get; set; } + } + + public class ArchiveEntryExtractionProgressArgs : EventArgs + { + public ReaderProgress Progress { get; set; } + public IEntry Entry { get; set; } + } +} diff --git a/LANCommander.Playnite.Extension/ExtractionResult.cs b/LANCommander.SDK/ExtractionResult.cs similarity index 88% rename from LANCommander.Playnite.Extension/ExtractionResult.cs rename to LANCommander.SDK/ExtractionResult.cs index ee4fb2c..ac9e1d8 100644 --- a/LANCommander.Playnite.Extension/ExtractionResult.cs +++ b/LANCommander.SDK/ExtractionResult.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace LANCommander.PlaynitePlugin +namespace LANCommander.SDK { internal class ExtractionResult { diff --git a/LANCommander.SDK/GameManager.cs b/LANCommander.SDK/GameManager.cs new file mode 100644 index 0000000..634cde1 --- /dev/null +++ b/LANCommander.SDK/GameManager.cs @@ -0,0 +1,227 @@ +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 + { + 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); + public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress; + + private TrackableStream Stream; + private IReader Reader; + + public GameManager(Client client, string defaultInstallDirectory) + { + Client = client; + DefaultInstallDirectory = defaultInstallDirectory; + } + + public GameManager(Client client, string defaultInstallDirectory, ILogger logger) + { + Client = client; + DefaultInstallDirectory = defaultInstallDirectory; + Logger = logger; + } + + /// + /// 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 = Client.GetGame(gameId); + + var destination = Path.Combine(DefaultInstallDirectory, game.Title.SanitizeFilename()); + + try + { + if (ManifestHelper.Exists(destination)) + manifest = ManifestHelper.Read(destination); + } + catch (Exception ex) + { + Logger?.LogTrace(ex, "Error reading manifest before install"); + } + + if (manifest == null || manifest.Id != gameId) + { + 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 Exception("Could not extract the installer. Retry the install or check your connection"); + else if (result.Canceled) + return ""; + + game.InstallDirectory = result.Directory; + } + else + { + Logger?.LogTrace("Game {GameTitle} ({GameId}) is already installed to {InstallDirectory}", game.Title, game.Id, destination); + + game.InstallDirectory = destination; + } + + var writeManifestSuccess = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromSeconds(1), false, () => + { + Logger?.LogTrace("Attempting to get game manifest"); + + 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"); + + 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) + { + + Logger?.LogTrace("Attempting to delete the install directory"); + + if (Directory.Exists(installDirectory)) + Directory.Delete(installDirectory, true); + + Logger?.LogTrace("Deleted install directory {InstallDirectory}", installDirectory); + } + + 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, + }; + + try + { + Directory.CreateDirectory(destination); + + Stream = Client.StreamGame(game.Id); + Reader = ReaderFactory.Open(Stream); + + Stream.OnProgress += (pos, len) => + { + OnArchiveExtractionProgress?.Invoke(pos, len); + }; + + Reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => + { + OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs + { + Entry = e.Item, + Progress = e.ReaderProgress, + }); + }; + + while (Reader.MoveToNextEntry()) + { + if (Reader.Cancelled) + break; + + Reader.WriteEntryToDirectory(destination, new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true, + PreserveFileTime = true, + }); + } + + Reader.Dispose(); + Stream.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; + + Logger?.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination); + } + + return extractionResult; + } + + public void CancelInstall() + { + Reader?.Cancel(); + // Reader?.Dispose(); + // Stream?.Dispose(); + } + } +} diff --git a/LANCommander.Playnite.Extension/Services/GameSaveService.cs b/LANCommander.SDK/GameSaveManager.cs similarity index 51% rename from LANCommander.Playnite.Extension/Services/GameSaveService.cs rename to LANCommander.SDK/GameSaveManager.cs index a650e5c..caf53de 100644 --- a/LANCommander.Playnite.Extension/Services/GameSaveService.cs +++ b/LANCommander.SDK/GameSaveManager.cs @@ -1,66 +1,56 @@ using LANCommander.SDK; -using Playnite.SDK; -using Playnite.SDK.Models; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.Models; +using LANCommander.SDK.PowerShell; using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Readers; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; +using System.Net; using System.Text; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace LANCommander.PlaynitePlugin.Services +namespace LANCommander.SDK { - internal class GameSaveService + public class GameSaveManager { - private readonly LANCommanderClient LANCommander; - private readonly IPlayniteAPI PlayniteApi; - private readonly PowerShellRuntime PowerShellRuntime; + private readonly Client Client; - internal GameSaveService(LANCommanderClient lanCommander, IPlayniteAPI playniteApi, PowerShellRuntime powerShellRuntime) + public delegate void OnDownloadProgressHandler(DownloadProgressChangedEventArgs e); + public event OnDownloadProgressHandler OnDownloadProgress; + + public delegate void OnDownloadCompleteHandler(AsyncCompletedEventArgs e); + public event OnDownloadCompleteHandler OnDownloadComplete; + + public GameSaveManager(Client client) { - LANCommander = lanCommander; - PlayniteApi = playniteApi; - PowerShellRuntime = powerShellRuntime; + Client = client; } - internal void DownloadSave(Game game) + public void Download(string installDirectory) { + var manifest = ManifestHelper.Read(installDirectory); + string tempFile = String.Empty; - if (game != null) + if (manifest != null) { - PlayniteApi.Dialogs.ActivateGlobalProgress(progress => + var destination = Client.DownloadLatestSave(manifest.Id, (changed) => { - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; - - var destination = LANCommander.DownloadLatestSave(Guid.Parse(game.GameId), (changed) => - { - progress.CurrentProgressValue = changed.ProgressPercentage; - }, (complete) => - { - progress.CurrentProgressValue = 100; - }); - - // Lock the thread until download is done - while (progress.CurrentProgressValue != 100) - { - - } - - tempFile = destination; - }, - new GlobalProgressOptions("Downloading latest save...") + OnDownloadProgress?.Invoke(changed); + }, (complete) => { - IsIndeterminate = false, - Cancelable = false + OnDownloadComplete?.Invoke(complete); }); + tempFile = destination; + // Go into the archive and extract the files to the correct locations try { @@ -74,10 +64,6 @@ namespace LANCommander.PlaynitePlugin.Services .WithNamingConvention(new PascalCaseNamingConvention()) .Build(); - var manifestContents = File.ReadAllText(Path.Combine(tempLocation, "_manifest.yml")); - - var manifest = deserializer.Deserialize(manifestContents); - #region Move files foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) { @@ -86,7 +72,7 @@ namespace LANCommander.PlaynitePlugin.Services var tempSavePathFile = Path.Combine(tempSavePath, savePath.Path.Replace('/', '\\').Replace("{InstallDir}\\", "")); - var destination = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory)); + destination = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", installDirectory)); if (File.Exists(tempSavePathFile)) { @@ -107,7 +93,7 @@ namespace LANCommander.PlaynitePlugin.Services if (inInstallDir) { // Files are in the game's install directory. Move them there from the save path. - destination = file.Replace(tempSavePath, savePath.Path.Replace('/', '\\').TrimEnd('\\').Replace("{InstallDir}", game.InstallDirectory)); + destination = file.Replace(tempSavePath, savePath.Path.Replace('/', '\\').TrimEnd('\\').Replace("{InstallDir}", installDirectory)); if (File.Exists(destination)) File.Delete(destination); @@ -141,7 +127,14 @@ namespace LANCommander.PlaynitePlugin.Services { var registryImportFileContents = File.ReadAllText(registryImportFilePath); - PowerShellRuntime.RunCommand($"regedit.exe /s \"{registryImportFilePath}\"", registryImportFileContents.Contains("HKEY_LOCAL_MACHINE")); + var script = new PowerShellScript(); + + script.UseInline($"regedit.exe /s \"{registryImportFilePath}\""); + + if (registryImportFileContents.Contains("HKEY_LOCAL_MACHINE")) + script.RunAsAdmin(); + + script.Execute(); } #endregion @@ -155,97 +148,93 @@ namespace LANCommander.PlaynitePlugin.Services } } - internal void UploadSave(Game game) + public void Upload(string installDirectory) { - var manifestPath = Path.Combine(game.InstallDirectory, "_manifest.yml"); + var manifest = ManifestHelper.Read(installDirectory); - if (File.Exists(manifestPath)) + var temp = Path.GetTempFileName(); + + if (manifest.SavePaths != null && manifest.SavePaths.Count() > 0) { - var deserializer = new DeserializerBuilder() - .WithNamingConvention(new PascalCaseNamingConvention()) - .Build(); - - var manifest = deserializer.Deserialize(File.ReadAllText(manifestPath)); - var temp = Path.GetTempFileName(); - - if (manifest.SavePaths != null && manifest.SavePaths.Count() > 0) + using (var archive = ZipArchive.Create()) { - using (var archive = ZipArchive.Create()) + archive.DeflateCompressionLevel = SharpCompress.Compressors.Deflate.CompressionLevel.BestCompression; + + #region Add files from defined paths + foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) { - archive.DeflateCompressionLevel = SharpCompress.Compressors.Deflate.CompressionLevel.BestCompression; + var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", installDirectory)); - #region Add files from defined paths - foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) + if (Directory.Exists(localPath)) { - var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory)); - - if (Directory.Exists(localPath)) - { - AddDirectoryToZip(archive, localPath, localPath, savePath.Id); - } - else if (File.Exists(localPath)) - { - archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath); - } + AddDirectoryToZip(archive, localPath, localPath, savePath.Id); } - #endregion - - #region Add files from defined paths - foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) + else if (File.Exists(localPath)) { - var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory)); - - if (Directory.Exists(localPath)) - { - AddDirectoryToZip(archive, localPath, localPath, savePath.Id); - } - else if (File.Exists(localPath)) - { - archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath); - } + archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath); } - #endregion + } + #endregion - #region Export registry keys - if (manifest.SavePaths.Any(sp => sp.Type == "Registry")) + #region Add files from defined paths + foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) + { + var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", installDirectory)); + + if (Directory.Exists(localPath)) { - List tempRegFiles = new List(); - - var exportCommand = new StringBuilder(); - - foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "Registry")) - { - var tempRegFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".reg"); - - exportCommand.AppendLine($"reg.exe export \"{savePath.Path.Replace(":\\", "\\")}\" \"{tempRegFile}\""); - tempRegFiles.Add(tempRegFile); - } - - PowerShellRuntime.RunCommand(exportCommand.ToString()); - - var exportFile = new StringBuilder(); - - foreach (var tempRegFile in tempRegFiles) - { - exportFile.AppendLine(File.ReadAllText(tempRegFile)); - File.Delete(tempRegFile); - } - - archive.AddEntry("_registry.reg", new MemoryStream(Encoding.UTF8.GetBytes(exportFile.ToString())), true); + AddDirectoryToZip(archive, localPath, localPath, savePath.Id); } - #endregion - - archive.AddEntry("_manifest.yml", manifestPath); - - using (var ms = new MemoryStream()) + else if (File.Exists(localPath)) { - archive.SaveTo(ms); - - ms.Seek(0, SeekOrigin.Begin); - - var save = LANCommander.UploadSave(game.GameId, ms.ToArray()); + archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath); } } + #endregion + + #region Export registry keys + if (manifest.SavePaths.Any(sp => sp.Type == "Registry")) + { + List tempRegFiles = new List(); + + var exportCommand = new StringBuilder(); + + foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "Registry")) + { + var tempRegFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".reg"); + + exportCommand.AppendLine($"reg.exe export \"{savePath.Path.Replace(":\\", "\\")}\" \"{tempRegFile}\""); + tempRegFiles.Add(tempRegFile); + } + + var script = new PowerShellScript(); + + script.UseInline(exportCommand.ToString()); + + script.Execute(); + + var exportFile = new StringBuilder(); + + foreach (var tempRegFile in tempRegFiles) + { + exportFile.AppendLine(File.ReadAllText(tempRegFile)); + File.Delete(tempRegFile); + } + + archive.AddEntry("_registry.reg", new MemoryStream(Encoding.UTF8.GetBytes(exportFile.ToString())), true); + } + #endregion + + archive.AddEntry("_manifest.yml", ManifestHelper.GetPath(installDirectory)); + + using (var ms = new MemoryStream()) + { + archive.SaveTo(ms); + + ms.Seek(0, SeekOrigin.Begin); + + var save = Client.UploadSave(manifest.Id.ToString(), ms.ToArray()); + } } } } diff --git a/LANCommander.SDK/Helpers/ManifestHelper.cs b/LANCommander.SDK/Helpers/ManifestHelper.cs new file mode 100644 index 0000000..34816b4 --- /dev/null +++ b/LANCommander.SDK/Helpers/ManifestHelper.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization; + +namespace LANCommander.SDK.Helpers +{ + public static class ManifestHelper + { + public static readonly ILogger Logger; + + public const string ManifestFilename = "_manifest.yml"; + + public static bool Exists(string installDirectory) + { + var path = GetPath(installDirectory); + + return File.Exists(path); + } + + public static GameManifest Read(string installDirectory) + { + var source = GetPath(installDirectory); + var yaml = File.ReadAllText(source); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(new PascalCaseNamingConvention()) + .Build(); + + Logger?.LogTrace("Deserializing manifest"); + + var manifest = deserializer.Deserialize(yaml); + + return manifest; + } + + public static string Write(GameManifest manifest, string installDirectory) + { + var destination = GetPath(installDirectory); + + Logger?.LogTrace("Attempting to write manifest to path {Destination}", destination); + + var serializer = new SerializerBuilder() + .WithNamingConvention(new PascalCaseNamingConvention()) + .Build(); + + Logger?.LogTrace("Serializing manifest"); + + var yaml = serializer.Serialize(manifest); + + Logger?.LogTrace("Writing manifest file"); + + File.WriteAllText(destination, yaml); + + return destination; + } + + public static string GetPath(string installDirectory) + { + return Path.Combine(installDirectory, ManifestFilename); + } + } +} diff --git a/LANCommander.Playnite.Extension/Helpers/RetryHelper.cs b/LANCommander.SDK/Helpers/RetryHelper.cs similarity index 64% rename from LANCommander.Playnite.Extension/Helpers/RetryHelper.cs rename to LANCommander.SDK/Helpers/RetryHelper.cs index 898a1d8..4a912eb 100644 --- a/LANCommander.Playnite.Extension/Helpers/RetryHelper.cs +++ b/LANCommander.SDK/Helpers/RetryHelper.cs @@ -1,15 +1,12 @@ -using Playnite.SDK; +using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -namespace LANCommander.PlaynitePlugin.Helpers +namespace LANCommander.SDK.Helpers { internal static class RetryHelper { - internal static readonly ILogger Logger = LogManager.GetLogger(); + internal static readonly ILogger Logger; internal static T RetryOnException(int maxAttempts, TimeSpan delay, T @default, Func action) { @@ -19,14 +16,14 @@ namespace LANCommander.PlaynitePlugin.Helpers { try { - Logger.Trace($"Attempt #{attempts + 1}/{maxAttempts}..."); + Logger?.LogTrace($"Attempt #{attempts + 1}/{maxAttempts}..."); attempts++; return action(); } catch (Exception ex) { - Logger.Error(ex, $"Attempt failed!"); + Logger?.LogError(ex, $"Attempt failed!"); if (attempts >= maxAttempts) return @default; diff --git a/LANCommander.SDK/Helpers/ScriptHelper.cs b/LANCommander.SDK/Helpers/ScriptHelper.cs new file mode 100644 index 0000000..3fde3f8 --- /dev/null +++ b/LANCommander.SDK/Helpers/ScriptHelper.cs @@ -0,0 +1,78 @@ +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LANCommander.SDK.Helpers +{ + public static class ScriptHelper + { + public static readonly ILogger Logger; + + public static string SaveTempScript(Script script) + { + var tempPath = SaveTempScript(script.Contents); + + Logger?.LogTrace("Wrote script {Script} to {Destination}", script.Name, tempPath); + + return tempPath; + } + + public static string SaveTempScript(string contents) + { + var tempPath = Path.GetTempFileName(); + + // PowerShell will only run scripts with the .ps1 file extension + File.Move(tempPath, tempPath + ".ps1"); + + tempPath = tempPath + ".ps1"; + + File.WriteAllText(tempPath, contents); + + return tempPath; + } + + public static void SaveScript(Game game, ScriptType type) + { + var script = game.Scripts.FirstOrDefault(s => s.Type == type); + + if (script == null) + return; + + if (script.RequiresAdmin) + script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents; + + var filename = GetScriptFilePath(game, type); + + if (File.Exists(filename)) + File.Delete(filename); + + Logger?.LogTrace("Writing {ScriptType} script to {Destination}", type, filename); + + File.WriteAllText(filename, script.Contents); + } + + public static string GetScriptFilePath(Game game, ScriptType type) + { + return GetScriptFilePath(game.InstallDirectory, type); + } + + public static string GetScriptFilePath(string installDirectory, ScriptType type) + { + Dictionary filenames = new Dictionary() { + { ScriptType.Install, "_install.ps1" }, + { ScriptType.Uninstall, "_uninstall.ps1" }, + { ScriptType.NameChange, "_changename.ps1" }, + { ScriptType.KeyChange, "_changekey.ps1" } + }; + + var filename = filenames[type]; + + return Path.Combine(installDirectory, filename); + } + } +} diff --git a/LANCommander.SDK/LANCommander.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj index 9f5c4f4..ba884b6 100644 --- a/LANCommander.SDK/LANCommander.SDK.csproj +++ b/LANCommander.SDK/LANCommander.SDK.csproj @@ -1,7 +1,15 @@ - + netstandard2.0 + + + + + + + + diff --git a/LANCommander.SDK/Models/AuthToken.cs b/LANCommander.SDK/Models/AuthToken.cs index 4f5f97b..f8728e6 100644 --- a/LANCommander.SDK/Models/AuthToken.cs +++ b/LANCommander.SDK/Models/AuthToken.cs @@ -8,5 +8,6 @@ namespace LANCommander.SDK.Models { public string AccessToken { get; set; } public string RefreshToken { get; set; } + public DateTime Expiration { get; set; } } } diff --git a/LANCommander.SDK/Models/Game.cs b/LANCommander.SDK/Models/Game.cs index d13737b..befabf4 100644 --- a/LANCommander.SDK/Models/Game.cs +++ b/LANCommander.SDK/Models/Game.cs @@ -10,6 +10,7 @@ namespace LANCommander.SDK.Models public string DirectoryName { get; set; } public string Description { get; set; } public DateTime ReleasedOn { get; set; } + public string InstallDirectory { get; set; } public virtual IEnumerable Actions { get; set; } public virtual IEnumerable Tags { get; set; } public virtual Company Publisher { get; set; } diff --git a/LANCommander.SDK/Models/GameManifest.cs b/LANCommander.SDK/Models/GameManifest.cs index af24437..6457504 100644 --- a/LANCommander.SDK/Models/GameManifest.cs +++ b/LANCommander.SDK/Models/GameManifest.cs @@ -6,6 +6,7 @@ namespace LANCommander.SDK { public class GameManifest { + public Guid Id { get; set; } public string Title { get; set; } public string SortTitle { get; set; } public string Description { get; set; } diff --git a/LANCommander.SDK/PowerShell/PowerShellArgument.cs b/LANCommander.SDK/PowerShell/PowerShellArgument.cs new file mode 100644 index 0000000..173e861 --- /dev/null +++ b/LANCommander.SDK/PowerShell/PowerShellArgument.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK.PowerShell +{ + public class PowerShellArgument + { + public string Name { get; set; } + public object Value { get; set; } + public Type Type { get; set; } + + public PowerShellArgument(string name, object value, Type type) + { + Name = name; + Value = value; + Type = type; + } + } +} diff --git a/LANCommander.SDK/PowerShell/PowerShellScript.cs b/LANCommander.SDK/PowerShell/PowerShellScript.cs new file mode 100644 index 0000000..a9ca191 --- /dev/null +++ b/LANCommander.SDK/PowerShell/PowerShellScript.cs @@ -0,0 +1,193 @@ +using LANCommander.SDK.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace LANCommander.SDK.PowerShell +{ + public class PowerShellScript + { + private string Contents { get; set; } = ""; + private string WorkingDirectory { get; set; } = ""; + private bool AsAdmin { get; set; } = false; + private bool ShellExecute { get; set; } = false; + private bool IgnoreWow64 { get; set; } = false; + private ICollection Variables { get; set; } + private Dictionary Arguments { get; set; } + private List Modules { get; set; } + private Process Process { get; set; } + + public PowerShellScript() + { + Variables = new List(); + Arguments = new Dictionary(); + Modules = new List(); + Process = new Process(); + + Process.StartInfo.FileName = "powershell.exe"; + Process.StartInfo.RedirectStandardOutput = false; + + AddArgument("ExecutionPolicy", "Unrestricted"); + + var moduleManifests = Directory.EnumerateFiles(Environment.CurrentDirectory, "LANCommander.PowerShell.psd1", SearchOption.AllDirectories); + + if (moduleManifests.Any()) + AddModule(moduleManifests.First()); + + IgnoreWow64Redirection(); + } + + public PowerShellScript UseFile(string path) + { + Contents = File.ReadAllText(path); + + return this; + } + + public PowerShellScript UseInline(string contents) + { + Contents = contents; + + return this; + } + + public PowerShellScript UseWorkingDirectory(string path) + { + WorkingDirectory = path; + + return this; + } + + public PowerShellScript UseShellExecute() + { + ShellExecute = true; + + return this; + } + + public PowerShellScript AddVariable(string name, T value) + { + Variables.Add(new PowerShellVariable(name, value, typeof(T))); + + return this; + } + + public PowerShellScript AddArgument(string name, T value) + { + Arguments.Add(name, $"\"{value}\""); + + return this; + } + + public PowerShellScript AddArgument(string name, int value) + { + Arguments[name] = value.ToString(); + + return this; + } + + public PowerShellScript AddArgument(string name, long value) + { + Arguments[name] = value.ToString(); + + return this; + } + + public PowerShellScript AddModule(string path) + { + Modules.Add(path); + + return this; + } + + public PowerShellScript RunAsAdmin() + { + AsAdmin = true; + + Process.StartInfo.Verb = "runas"; + Process.StartInfo.UseShellExecute = true; + + return this; + } + + public PowerShellScript IgnoreWow64Redirection() + { + IgnoreWow64 = true; + + return this; + } + + public int Execute() + { + var scriptBuilder = new StringBuilder(); + + var wow64Value = IntPtr.Zero; + + if (Contents.StartsWith("# Requires Admin")) + RunAsAdmin(); + + foreach (var module in Modules) + { + scriptBuilder.AppendLine($"Import-Module \"{module}\""); + } + + foreach (var variable in Variables) + { + scriptBuilder.AppendLine($"${variable.Name} = ConvertFrom-SerializedBase64 \"{Serialize(variable.Value)}\""); + } + + scriptBuilder.AppendLine(Contents); + + var path = ScriptHelper.SaveTempScript(scriptBuilder.ToString()); + + AddArgument("File", path); + + if (IgnoreWow64) + Wow64DisableWow64FsRedirection(ref wow64Value); + + foreach (var argument in Arguments) + { + Process.StartInfo.Arguments += $" -{argument.Key} {argument.Value}"; + } + + if (!String.IsNullOrEmpty(WorkingDirectory)) + Process.StartInfo.WorkingDirectory = WorkingDirectory; + + if (ShellExecute) + Process.StartInfo.UseShellExecute = true; + + if (AsAdmin) + { + Process.StartInfo.Verb = "runas"; + Process.StartInfo.UseShellExecute = true; + } + + Process.Start(); + Process.WaitForExit(); + + if (IgnoreWow64) + Wow64RevertWow64FsRedirection(ref wow64Value); + + if (File.Exists(path)) + File.Delete(path); + + return Process.ExitCode; + } + + public static string Serialize(T input) + { + // Use the PowerShell serializer to generate XML for our input. Then convert to base64 so we can put it on one line. + return Convert.ToBase64String(Encoding.UTF8.GetBytes(System.Management.Automation.PSSerializer.Serialize(input))); + } + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool Wow64RevertWow64FsRedirection(ref IntPtr ptr); + } +} diff --git a/LANCommander.SDK/PowerShell/PowerShellVariable.cs b/LANCommander.SDK/PowerShell/PowerShellVariable.cs new file mode 100644 index 0000000..a7f78b8 --- /dev/null +++ b/LANCommander.SDK/PowerShell/PowerShellVariable.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK.PowerShell +{ + public class PowerShellVariable + { + public string Name { get; set; } + public object Value { get; set; } + public Type Type { get; set; } + + public PowerShellVariable(string name, object value, Type type) + { + Name = name; + Value = value; + Type = type; + } + } +} diff --git a/LANCommander.SDK/RedistributableManager.cs b/LANCommander.SDK/RedistributableManager.cs new file mode 100644 index 0000000..ec242b6 --- /dev/null +++ b/LANCommander.SDK/RedistributableManager.cs @@ -0,0 +1,184 @@ +using LANCommander.SDK.Enums; +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.Models; +using LANCommander.SDK.PowerShell; +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 RedistributableManager + { + private readonly ILogger Logger; + private Client Client { 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; + + public RedistributableManager(Client client) + { + Client = client; + } + + public RedistributableManager(Client client, ILogger logger) + { + Client = client; + Logger = logger; + } + + public void Install(Game game) + { + foreach (var redistributable in game.Redistributables) + { + Install(redistributable); + } + } + + public void Install(Redistributable redistributable) + { + string installScriptTempFile = null; + string detectionScriptTempFile = null; + string extractTempPath = null; + + try + { + var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install); + installScriptTempFile = ScriptHelper.SaveTempScript(installScript); + + var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall); + detectionScriptTempFile = ScriptHelper.SaveTempScript(detectionScript); + + var detectionResult = RunScript(detectionScriptTempFile, redistributable, detectionScript.RequiresAdmin); + + // Redistributable is not installed + if (detectionResult == 0) + { + if (redistributable.Archives.Count() > 0) + { + var extractionResult = DownloadAndExtract(redistributable); + + if (extractionResult.Success) + { + extractTempPath = extractionResult.Directory; + + RunScript(installScriptTempFile, redistributable, installScript.RequiresAdmin, extractTempPath); + } + } + else + { + RunScript(installScriptTempFile, redistributable, installScript.RequiresAdmin, extractTempPath); + } + } + } + catch (Exception ex) + { + Logger?.LogError(ex, "Redistributable {Redistributable} failed to install", redistributable.Name); + } + finally + { + if (File.Exists(installScriptTempFile)) + File.Delete(installScriptTempFile); + + if (File.Exists(detectionScriptTempFile)) + File.Delete(detectionScriptTempFile); + + if (Directory.Exists(extractTempPath)) + Directory.Delete(extractTempPath); + } + } + + private ExtractionResult DownloadAndExtract(Redistributable redistributable) + { + if (redistributable == null) + { + Logger?.LogTrace("Redistributable failed to download! No redistributable was specified"); + throw new ArgumentNullException("No redistributable was specified"); + } + + var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename()); + + Logger?.LogTrace("Downloading and extracting {Redistributable} to path {Destination}", redistributable.Name, destination); + + try + { + Directory.CreateDirectory(destination); + + using (var redistributableStream = Client.StreamRedistributable(redistributable.Id)) + using (var reader = ReaderFactory.Open(redistributableStream)) + { + redistributableStream.OnProgress += (pos, len) => + { + OnArchiveExtractionProgress?.Invoke(pos, len); + }; + + reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => + { + OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs + { + Entry = e.Item, + Progress = e.ReaderProgress, + }); + }; + + reader.WriteAllToDirectory(destination, new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + catch (Exception ex) + { + Logger?.LogError(ex, "Could not extract to path {Destination}", destination); + + if (Directory.Exists(destination)) + { + Logger?.LogTrace("Cleaning up orphaned files after bad install"); + + Directory.Delete(destination, true); + } + + throw new Exception("The redistributable archive could not be extracted, is it corrupted? Please try again"); + } + + var extractionResult = new ExtractionResult + { + Canceled = false + }; + + if (!extractionResult.Canceled) + { + extractionResult.Success = true; + extractionResult.Directory = destination; + Logger?.LogTrace("Redistributable {Redistributable} successfully downloaded and extracted to {Destination}", redistributable.Name, destination); + } + + return extractionResult; + } + + private int RunScript(string path, Redistributable redistributable, bool requiresAdmin = false, string workingDirectory = "") + { + var script = new PowerShellScript(); + + script.AddVariable("Redistributable", redistributable); + + script.UseWorkingDirectory(workingDirectory); + script.UseFile(path); + + if (requiresAdmin) + script.RunAsAdmin(); + + return script.Execute(); + } + } +} diff --git a/LANCommander.Playnite.Extension/TrackableStream.cs b/LANCommander.SDK/TrackableStream.cs similarity index 99% rename from LANCommander.Playnite.Extension/TrackableStream.cs rename to LANCommander.SDK/TrackableStream.cs index cff27f8..38f8835 100644 --- a/LANCommander.Playnite.Extension/TrackableStream.cs +++ b/LANCommander.SDK/TrackableStream.cs @@ -1,9 +1,9 @@ using System; using System.IO; -namespace LANCommander.PlaynitePlugin +namespace LANCommander.SDK { - internal class TrackableStream : MemoryStream, IDisposable + public class TrackableStream : MemoryStream, IDisposable { public delegate void OnProgressDelegate(long Position, long Length); public event OnProgressDelegate OnProgress = delegate { }; diff --git a/LANCommander.sln b/LANCommander.sln index 529f8cc..f77170c 100644 --- a/LANCommander.sln +++ b/LANCommander.sln @@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LANCommander.SDK", "LANComm EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LANCommander.PCGamingWiki", "LANCommander.PCGamingWiki\LANCommander.PCGamingWiki.csproj", "{2436B817-4475-4E70-9BB2-E1E7866DB79F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LANCommander.PowerShell", "LANCommander.PowerShell\LANCommander.PowerShell.csproj", "{807943BF-0C7D-4ED3-8393-CFEE64E3138C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LANCommander.PowerShell.Tests", "LANCommander.PowerShell.Tests\LANCommander.PowerShell.Tests.csproj", "{D7069A13-F0AA-4CBF-9013-4276F130A6DD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +37,14 @@ Global {2436B817-4475-4E70-9BB2-E1E7866DB79F}.Debug|Any CPU.Build.0 = Debug|Any CPU {2436B817-4475-4E70-9BB2-E1E7866DB79F}.Release|Any CPU.ActiveCfg = Release|Any CPU {2436B817-4475-4E70-9BB2-E1E7866DB79F}.Release|Any CPU.Build.0 = Release|Any CPU + {807943BF-0C7D-4ED3-8393-CFEE64E3138C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {807943BF-0C7D-4ED3-8393-CFEE64E3138C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {807943BF-0C7D-4ED3-8393-CFEE64E3138C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {807943BF-0C7D-4ED3-8393-CFEE64E3138C}.Release|Any CPU.Build.0 = Release|Any CPU + {D7069A13-F0AA-4CBF-9013-4276F130A6DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7069A13-F0AA-4CBF-9013-4276F130A6DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7069A13-F0AA-4CBF-9013-4276F130A6DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7069A13-F0AA-4CBF-9013-4276F130A6DD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LANCommander/Components/ArchiveEditor.razor b/LANCommander/Components/ArchiveEditor.razor index ce060e2..a3f7b3e 100644 --- a/LANCommander/Components/ArchiveEditor.razor +++ b/LANCommander/Components/ArchiveEditor.razor @@ -46,15 +46,14 @@ - + @code { [Parameter] public Guid GameId { get; set; } [Parameter] public Guid RedistributableId { get; set; } - [Parameter] public ICollection Archives { get; set; } - [Parameter] public EventCallback> ArchivesChanged { get; set; } - Archive Archive; + ICollection Archives { get; set; } + ArchiveUploader Uploader; protected override async Task OnInitializedAsync() @@ -66,7 +65,10 @@ private async Task LoadData() { - Archives = await ArchiveService.Get(a => a.GameId == GameId).ToListAsync(); + if (GameId != Guid.Empty) + Archives = await ArchiveService.Get(a => a.GameId == GameId).ToListAsync(); + else if (RedistributableId != Guid.Empty) + Archives = await ArchiveService.Get(a => a.RedistributableId == RedistributableId).ToListAsync(); } private async Task Download(Archive archive) @@ -78,27 +80,7 @@ private async Task UploadArchive() { - if (GameId != Guid.Empty) - Archive = new Archive() { GameId = GameId, Id = Guid.NewGuid() }; - - if (RedistributableId != Guid.Empty) - Archive = new Archive() { RedistributableId = RedistributableId, Id = Guid.NewGuid() }; - - await Uploader.Open(Archive); - } - - private async Task AddArchive(Archive archive) - { - var lastArchive = Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault(); - - Archive = await ArchiveService.Add(archive); - - await LoadData(); - - var settings = SettingService.GetSettings(); - - if (lastArchive != null && settings.Archives.EnablePatching) - BackgroundJob.Enqueue(x => x.Execute(lastArchive.Id, Archive.Id)); + await Uploader.Open(); } private async Task Delete(Archive archive) @@ -107,6 +89,8 @@ { await ArchiveService.Delete(archive); + await LoadData(); + await MessageService.Success("Archive deleted!"); } catch (Exception ex) diff --git a/LANCommander/Components/ArchiveUploader.razor b/LANCommander/Components/ArchiveUploader.razor index e7a5961..a690c4d 100644 --- a/LANCommander/Components/ArchiveUploader.razor +++ b/LANCommander/Components/ArchiveUploader.razor @@ -2,6 +2,7 @@ @using System.Diagnostics; @using Hangfire; @using LANCommander.Jobs.Background; +@using Microsoft.EntityFrameworkCore; @inject HttpClient HttpClient @inject NavigationManager Navigator @inject ArchiveService ArchiveService @@ -62,7 +63,9 @@ @code { - [Parameter] public EventCallback OnArchiveUploaded { get; set; } + [Parameter] public Guid GameId { get; set; } + [Parameter] public Guid RedistributableId { get; set; } + [Parameter] public EventCallback OnArchiveUploaded { get; set; } Archive Archive; @@ -113,9 +116,21 @@ File = args.File; } - public async Task Open(Archive archive) + public async Task Open(Guid? archiveId = null) { - Archive = archive; + if (archiveId.HasValue && archiveId != Guid.Empty) + { + Archive = await ArchiveService.Get(archiveId.Value); + } + else + { + Archive = new Archive(); + + if (GameId != Guid.Empty) + Archive.GameId = GameId; + else if (RedistributableId != Guid.Empty) + Archive.RedistributableId = RedistributableId; + } Visible = true; @@ -128,8 +143,8 @@ { if (FileInput != null) { - if (!String.IsNullOrWhiteSpace(archive.ObjectKey) && archive.ObjectKey != Guid.Empty.ToString()) - await JS.InvokeVoidAsync("Uploader.Init", "FileInput", archive.ObjectKey.ToString()); + if (!String.IsNullOrWhiteSpace(Archive.ObjectKey) && Archive.ObjectKey != Guid.Empty.ToString()) + await JS.InvokeVoidAsync("Uploader.Init", "FileInput", Archive.ObjectKey.ToString()); else await JS.InvokeVoidAsync("Uploader.Init", "FileInput", ""); @@ -163,12 +178,32 @@ Archive.ObjectKey = objectKey.ToString(); Archive.CompressedSize = File.Size; + if (Archive.Id != Guid.Empty) + Archive = await ArchiveService.Update(Archive); + else + Archive = await ArchiveService.Add(Archive); + Visible = false; await InvokeAsync(StateHasChanged); + Archive? lastArchive = null; + + var settings = SettingService.GetSettings(); + + if (settings.Archives.EnablePatching) + { + if (Archive.GameId != Guid.Empty) + lastArchive = await ArchiveService.Get(a => a.Id != Archive.Id && a.GameId == Archive.GameId).OrderByDescending(a => a.CreatedOn).FirstOrDefaultAsync(); + else if (Archive.RedistributableId != Guid.Empty) + lastArchive = await ArchiveService.Get(a => a.Id != Archive.Id && a.RedistributableId == Archive.RedistributableId).OrderByDescending(a => a.CreatedOn).FirstOrDefaultAsync(); + + if (lastArchive != null && settings.Archives.EnablePatching) + BackgroundJob.Enqueue(x => x.Execute(lastArchive.Id, Archive.Id)); + } + if (OnArchiveUploaded.HasDelegate) - await OnArchiveUploaded.InvokeAsync(Archive); + await OnArchiveUploaded.InvokeAsync(Archive.Id); await MessageService.Success("Archive uploaded!"); } diff --git a/LANCommander/Components/FileManagerComponents/FileManager.razor b/LANCommander/Components/FileManagerComponents/FileManager.razor index b7d91ae..4f23ba3 100644 --- a/LANCommander/Components/FileManagerComponents/FileManager.razor +++ b/LANCommander/Components/FileManagerComponents/FileManager.razor @@ -239,24 +239,37 @@ async Task> GetArchiveDirectoriesAsync(Guid archiveId) { - var entries = await ArchiveService.GetContents(archiveId); - var directories = new HashSet(); - - var root = new FileManagerDirectory + try { - Name = "Root", - Path = "", - IsExpanded = true - }; + var entries = await ArchiveService.GetContents(archiveId); + var directories = new HashSet(); - root.PopulateChildren(entries); + var root = new FileManagerDirectory + { + Name = "Root", + Path = "", + IsExpanded = true + }; - await ChangeDirectory(root, true); + root.PopulateChildren(entries); - return new HashSet + await ChangeDirectory(root, true); + + return new HashSet + { + root + }; + } + catch (FileNotFoundException ex) { - root - }; + MessageService.Error("Could not open archive! Is it missing?"); + } + catch (Exception ex) + { + MessageService.Error("An unknown error occurred trying to open the archive"); + } + + return new HashSet(); } string GetEntryName(IFileManagerEntry entry) diff --git a/LANCommander/Components/ScriptEditor.razor b/LANCommander/Components/ScriptEditor.razor index a2c2a0c..ab35a6d 100644 --- a/LANCommander/Components/ScriptEditor.razor +++ b/LANCommander/Components/ScriptEditor.razor @@ -4,75 +4,12 @@ @using LANCommander.Models @using LANCommander.Services @using System.IO.Compression; +@using Microsoft.EntityFrameworkCore; @inject ScriptService ScriptService @inject ModalService ModalService @inject IMessageService MessageService -@{ - RenderFragment Footer = - @; -} - -
- - @foreach (var group in Snippets.Select(s => s.Group).Distinct()) - { - - - - @foreach (var snippet in Snippets.Where(s => s.Group == group)) - { - - @snippet.Name - - } - - - - - - - - } - - @if (ArchiveId != Guid.Empty) - { - - } - - - - - - - - - - - - - - - - - - Requires Admin - - - -