Merge branch 'main' into save-path-regex

save-path-regex
Pat Hartl 2023-11-30 17:23:40 -06:00
commit 349001d8f6
117 changed files with 9461 additions and 1239 deletions

View File

@ -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<ExtractionResult>(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<IEntry> 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<IEntry> 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;
}
}
}

View File

@ -12,6 +12,7 @@
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -34,9 +35,15 @@
<Reference Include="BeaconLib, Version=1.0.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\rix0rrr.BeaconLib.1.0.2\lib\net40\BeaconLib.dll</HintPath>
</Reference>
<Reference Include="ByteSize, Version=2.1.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\ByteSize.2.1.1\lib\net45\ByteSize.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Extensions.Logging.Abstractions.7.0.0\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Playnite.SDK, Version=6.10.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\PlayniteSDK.6.10.0\lib\net462\Playnite.SDK.dll</HintPath>
</Reference>
@ -45,8 +52,8 @@
<Reference Include="RestSharp, Version=106.15.0.0, Culture=neutral, PublicKeyToken=598062e77f915f75, processorArchitecture=MSIL">
<HintPath>..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll</HintPath>
</Reference>
<Reference Include="SharpCompress, Version=0.34.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\SharpCompress.0.34.1\lib\net462\SharpCompress.dll</HintPath>
<Reference Include="SharpCompress, Version=0.34.2.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>..\packages\SharpCompress.0.34.2\lib\net462\SharpCompress.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
@ -93,21 +100,18 @@
<Reference Include="WindowsBase" />
<Reference Include="YamlDotNet, Version=5.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\YamlDotNet.5.4.0\lib\net45\YamlDotNet.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="ZstdSharp, Version=0.7.2.0, Culture=neutral, PublicKeyToken=8d151af33a4ad5cf, processorArchitecture=MSIL">
<HintPath>..\packages\ZstdSharp.Port.0.7.2\lib\net461\ZstdSharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="ExtractionResult.cs" />
<Compile Include="TrackableStream.cs" />
<Compile Include="Extensions\MultiplayerInfoExtensions.cs" />
<Compile Include="Helpers\RetryHelper.cs" />
<Compile Include="PowerShellRuntime.cs" />
<Compile Include="Services\GameSaveService.cs" />
<Compile Include="PlayniteLogger.cs" />
<Compile Include="SaveController.cs" />
<Compile Include="UninstallController.cs" />
<Compile Include="InstallController.cs" />
<Compile Include="LANCommanderClient.cs" />
<Compile Include="LANCommanderLibraryPlugin.cs" />
<Compile Include="ViewModels\LANCommanderSettingsViewModel.cs" />
<Compile Include="Views\LANCommanderSettingsView.xaml.cs">
@ -148,6 +152,10 @@
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LANCommander.PowerShell\LANCommander.PowerShell.csproj">
<Project>{807943bf-0c7d-4ed3-8393-cfee64e3138c}</Project>
<Name>LANCommander.PowerShell</Name>
</ProjectReference>
<ProjectReference Include="..\LANCommander.SDK\LANCommander.SDK.csproj">
<Project>{4c2a71fd-a30b-4d62-888a-4ef843d8e506}</Project>
<Name>LANCommander.SDK</Name>

View File

@ -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<GameMetadata> 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<TopPanelItem> 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;
}
}
}

View File

@ -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>(TState state)
{
return default;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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;
}
}
}
}

View File

@ -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<string> 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<Game> games, ScriptType type, string arguments = null)
{
List<string> scripts = new List<string>();
List<string> adminScripts = new List<string>();
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<ScriptType, string> filenames = new Dictionary<ScriptType, string>() {
{ 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="ByteSize" version="2.1.1" targetFramework="net462" />
<package id="Microsoft.Bcl.AsyncInterfaces" version="7.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Logging.Abstractions" version="7.0.0" targetFramework="net462" />
<package id="NuGet.CommandLine" version="6.7.0" targetFramework="net462" developmentDependency="true" />
<package id="PlayniteSDK" version="6.10.0" targetFramework="net462" />
<package id="PowerShellStandard.Library" version="5.1.1" targetFramework="net462" />
<package id="RestSharp" version="106.15.0" targetFramework="net462" />
<package id="rix0rrr.BeaconLib" version="1.0.2" targetFramework="net462" />
<package id="SharpCompress" version="0.34.1" targetFramework="net462" />
<package id="SharpCompress" version="0.34.2" targetFramework="net462" />
<package id="System.Buffers" version="4.5.1" targetFramework="net462" />
<package id="System.Memory" version="4.5.5" targetFramework="net462" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net462" />

View File

@ -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<string>().ToList();
Assert.AreEqual(1, encodingResults.Count);
var decodingCmdlet = new ConvertFromSerializedBase64Cmdlet()
{
Input = encodingResults.First()
};
var decodingResults = decodingCmdlet.Invoke().OfType<string>().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<DisplayResolution>().ToList();
Assert.AreEqual(1, output.Count);
var bounds = output.First();
Assert.AreEqual(x2, bounds.Width);
Assert.AreEqual(y2, bounds.Height);
}
}
}

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.props" Condition="Exists('..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{D7069A13-F0AA-4CBF-9013-4276F130A6DD}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LANCommander.PowerShell.Tests</RootNamespace>
<AssemblyName>LANCommander.PowerShell.Tests</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<IsCodedUITest>False</IsCodedUITest>
<TestProjectType>UnitTest</TestProjectType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</ItemGroup>
<ItemGroup>
<Compile Include="Cmdlets.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LANCommander.PowerShell\LANCommander.PowerShell.csproj">
<Project>{807943bf-0c7d-4ed3-8393-cfee64e3138c}</Project>
<Name>LANCommander.PowerShell</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.props'))" />
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.targets'))" />
</Target>
<Import Project="..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.targets')" />
</Project>

View File

@ -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")]

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="MSTest.TestAdapter" version="2.2.10" targetFramework="net462" />
<package id="MSTest.TestFramework" version="2.2.10" targetFramework="net462" />
</packages>

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{807943BF-0C7D-4ED3-8393-CFEE64E3138C}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LANCommander.PowerShell</RootNamespace>
<AssemblyName>LANCommander.PowerShell</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\PowerShellStandard.Library.5.1.1\lib\net452\System.Management.Automation.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Cmdlets\ConvertTo-SerializedBase64.cs" />
<Compile Include="Cmdlets\ConvertFrom-SerializedBase64.cs" />
<Compile Include="Cmdlets\Edit-PatchBinary.cs" />
<Compile Include="Cmdlets\Uninstall-Game.cs" />
<Compile Include="Cmdlets\Install-Game.cs" />
<Compile Include="Cmdlets\Write-ReplaceContentInFile.cs" />
<Compile Include="Cmdlets\ConvertTo-StringBytes.cs" />
<Compile Include="Cmdlets\Get-PrimaryDisplay.cs" />
<Compile Include="Cmdlets\Convert-AspectRatio.cs" />
<Compile Include="Cmdlets\Get-GameManifest.cs" />
<Compile Include="Cmdlets\Write-GameManifest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="LANCommander.PowerShell.psd1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LANCommander.SDK\LANCommander.SDK.csproj">
<Project>{4c2a71fd-a30b-4d62-888a-4ef843d8e506}</Project>
<Name>LANCommander.SDK</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

Binary file not shown.

View File

@ -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")]

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="PowerShellStandard.Library" version="5.1.1" targetFramework="net472" />
</packages>

View File

@ -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<T>(string route, object body)
@ -32,7 +46,17 @@ namespace LANCommander.PlaynitePlugin
.AddJsonBody(body)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
var response = Client.Post<T>(request);
var response = ApiClient.Post<T>(request);
return response.Data;
}
private T PostRequest<T>(string route)
{
var request = new RestRequest(route)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
var response = ApiClient.Post<T>(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<T>(request);
var response = ApiClient.Get<T>(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<AuthResponse> AuthenticateAsync(string username, string password)
public async Task<AuthToken> AuthenticateAsync(string username, string password)
{
var response = await Client.ExecuteAsync<AuthResponse>(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest()
var response = await ApiClient.ExecuteAsync<AuthResponse>(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<AuthResponse> RegisterAsync(string username, string password)
public async Task<AuthToken> RegisterAsync(string username, string password)
{
var response = await Client.ExecuteAsync<AuthResponse>(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest()
var response = await ApiClient.ExecuteAsync<AuthResponse>(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<bool> 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<AuthResponse>(request);
var response = ApiClient.Post<AuthResponse>(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<Game> GetGames()
{
return GetRequest<IEnumerable<Game>>("/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<GameSave>(request);
var response = ApiClient.Post<GameSave>(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<User>("/api/Profile");
}
public string ChangeAlias(string alias)
{
Logger.Trace("Requesting to change player alias...");
Logger?.LogTrace("Requesting to change player alias...");
var response = PostRequest<object>("/api/Profile/ChangeAlias", alias);
return alias;
}
public void StartPlaySession(Guid gameId)
{
Logger?.LogTrace("Starting a game session...");
PostRequest<object>($"/api/PlaySessions/Start/{gameId}");
}
public void EndPlaySession(Guid gameId)
{
Logger?.LogTrace("Ending a game session...");
PostRequest<object>($"/api/PlaySessions/End/{gameId}");
}
private string GetMacAddress()
{
return NetworkInterface.GetAllNetworkInterfaces()

View File

@ -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; }
}
}

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin
namespace LANCommander.SDK
{
internal class ExtractionResult
{

View File

@ -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;
}
/// <summary>
/// Downloads, extracts, and runs post-install scripts for the specified game
/// </summary>
/// <param name="game">Game to install</param>
/// <param name="maxAttempts">Maximum attempts in case of transmission error</param>
/// <returns>Final install path</returns>
/// <exception cref="Exception"></exception>
public string Install(Guid gameId, int maxAttempts = 10)
{
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<ExtractionResult>(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<IEntry> 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();
}
}
}

View File

@ -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<GameManifest>(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<GameManifest>(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<string> tempRegFiles = new List<string>();
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<string> tempRegFiles = new List<string>();
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());
}
}
}
}

View File

@ -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<GameManifest>(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);
}
}
}

View File

@ -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<T>(int maxAttempts, TimeSpan delay, T @default, Func<T> 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;

View File

@ -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<ScriptType, string> filenames = new Dictionary<ScriptType, string>() {
{ 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);
}
}
}

View File

@ -1,7 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" />
<PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="SharpCompress" Version="0.34.2" />
<PackageReference Include="YamlDotNet" Version="5.4.0" />
</ItemGroup>
</Project>

View File

@ -8,5 +8,6 @@ namespace LANCommander.SDK.Models
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime Expiration { get; set; }
}
}

View File

@ -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<Action> Actions { get; set; }
public virtual IEnumerable<Tag> Tags { get; set; }
public virtual Company Publisher { get; set; }

View File

@ -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; }

View File

@ -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;
}
}
}

View File

@ -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<PowerShellVariable> Variables { get; set; }
private Dictionary<string, string> Arguments { get; set; }
private List<string> Modules { get; set; }
private Process Process { get; set; }
public PowerShellScript()
{
Variables = new List<PowerShellVariable>();
Arguments = new Dictionary<string, string>();
Modules = new List<string>();
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<T>(string name, T value)
{
Variables.Add(new PowerShellVariable(name, value, typeof(T)));
return this;
}
public PowerShellScript AddArgument<T>(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>(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);
}
}

View File

@ -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;
}
}
}

View File

@ -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<IEntry> 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();
}
}
}

View File

@ -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 { };

View File

@ -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

View File

@ -46,15 +46,14 @@
</SpaceItem>
</Space>
<ArchiveUploader @ref="Uploader" OnArchiveUploaded="AddArchive" />
<ArchiveUploader @ref="Uploader" GameId="GameId" RedistributableId="RedistributableId" OnArchiveUploaded="LoadData" />
@code {
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
[Parameter] public ICollection<Archive> Archives { get; set; }
[Parameter] public EventCallback<ICollection<Archive>> ArchivesChanged { get; set; }
Archive Archive;
ICollection<Archive> 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<PatchArchiveBackgroundJob>(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)

View File

@ -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 @@
</Modal>
@code {
[Parameter] public EventCallback<Archive> OnArchiveUploaded { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
[Parameter] public EventCallback<Guid> 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<PatchArchiveBackgroundJob>(x => x.Execute(lastArchive.Id, Archive.Id));
}
if (OnArchiveUploaded.HasDelegate)
await OnArchiveUploaded.InvokeAsync(Archive);
await OnArchiveUploaded.InvokeAsync(Archive.Id);
await MessageService.Success("Archive uploaded!");
}

View File

@ -239,24 +239,37 @@
async Task<HashSet<FileManagerDirectory>> GetArchiveDirectoriesAsync(Guid archiveId)
{
var entries = await ArchiveService.GetContents(archiveId);
var directories = new HashSet<FileManagerDirectory>();
var root = new FileManagerDirectory
try
{
Name = "Root",
Path = "",
IsExpanded = true
};
var entries = await ArchiveService.GetContents(archiveId);
var directories = new HashSet<FileManagerDirectory>();
root.PopulateChildren(entries);
var root = new FileManagerDirectory
{
Name = "Root",
Path = "",
IsExpanded = true
};
await ChangeDirectory(root, true);
root.PopulateChildren(entries);
return new HashSet<FileManagerDirectory>
await ChangeDirectory(root, true);
return new HashSet<FileManagerDirectory>
{
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<FileManagerDirectory>();
}
string GetEntryName(IFileManagerEntry entry)

View File

@ -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 =
@<Template>
<Button OnClick="Save" Disabled="@(String.IsNullOrWhiteSpace(Script.Name))" Type="@ButtonType.Primary">Save</Button>
<Button OnClick="Close">Close</Button>
</Template>;
}
<Modal Visible="ModalVisible" Footer="@Footer" Title="@(Script == null ? "Add Script" : "Edit Script")" OkText="@("Save")" Maximizable="false" DefaultMaximized="true" Closable="true" OnCancel="Close">
<Form Model="@Script" Layout="@FormLayout.Vertical">
<FormItem>
@foreach (var group in Snippets.Select(s => s.Group).Distinct())
{
<Dropdown>
<Overlay>
<Menu>
@foreach (var snippet in Snippets.Where(s => s.Group == group))
{
<MenuItem OnClick="() => InsertSnippet(snippet)">
@snippet.Name
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<Button Type="@ButtonType.Primary">@group</Button>
</ChildContent>
</Dropdown>
}
@if (ArchiveId != Guid.Empty)
{
<Button Icon="@IconType.Outline.FolderOpen" OnClick="BrowseForPath" Type="@ButtonType.Text">Browse</Button>
}
<Button Icon="@IconType.Outline.Build" OnClick="() => RegToPowerShell.Open()" Type="@ButtonType.Text">Import .reg</Button>
</FormItem>
<FormItem>
<StandaloneCodeEditor @ref="Editor" Id="editor" ConstructionOptions="EditorConstructionOptions" />
</FormItem>
<FormItem Label="Name">
<Input @bind-Value="@context.Name" />
</FormItem>
<FormItem Label="Type">
<Select @bind-Value="context.Type" TItem="ScriptType" TItemValue="ScriptType" DataSource="Enum.GetValues<ScriptType>().Where(st => AllowedTypes == null || AllowedTypes.Contains(st))">
<LabelTemplate Context="Value">@Value.GetDisplayName()</LabelTemplate>
<ItemTemplate Context="Value">@Value.GetDisplayName()</ItemTemplate>
</Select>
</FormItem>
<FormItem>
<Checkbox @bind-Checked="context.RequiresAdmin">Requires Admin</Checkbox>
</FormItem>
<FormItem Label="Description">
<TextArea @bind-Value="context.Description" MaxLength=500 ShowCount />
</FormItem>
</Form>
</Modal>
<RegToPowerShell @ref="RegToPowerShell" OnParsed="(text) => InsertText(text)" />
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<SpaceItem>
@ -85,8 +22,9 @@
<ActionColumn Title="">
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => Edit(context)" Icon="@IconType.Outline.Edit" Type="@ButtonType.Text" />
<Button OnClick="() => Edit(context.Id)" Icon="@IconType.Outline.Edit" Type="@ButtonType.Text" />
</SpaceItem>
<SpaceItem>
<Popconfirm OnConfirm="() => Delete(context)" Title="Are you sure you want to delete this script?">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
</Popconfirm>
@ -115,59 +53,44 @@
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public ICollection<Script> Scripts { get; set; }
[Parameter] public EventCallback<ICollection<Script>> ScriptsChanged { get; set; }
[Parameter] public IEnumerable<ScriptType> AllowedTypes { get; set; }
Script Script;
ICollection<Script> Scripts { get; set; } = new List<Script>();
bool ModalVisible = false;
IEnumerable<Snippet> Snippets { get; set; }
StandaloneCodeEditor? Editor;
RegToPowerShell RegToPowerShell;
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
protected override async Task OnParametersSetAsync()
{
return new StandaloneEditorConstructionOptions
if (GameId != Guid.Empty)
Scripts = await ScriptService.Get(s => s.GameId == GameId).ToListAsync();
else if (RedistributableId != Guid.Empty)
Scripts = await ScriptService.Get(s => s.RedistributableId == RedistributableId).ToListAsync();
}
private async void Edit(Guid? scriptId = null)
{
var modalOptions = new ModalOptions()
{
AutomaticLayout = true,
Language = "powershell",
Value = Script.Contents,
Theme = "vs-dark",
Title = scriptId == null ? "Add Script" : "Edit Script",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Save"
};
}
protected override async Task OnInitializedAsync()
{
if (Scripts == null)
Scripts = new List<Script>();
var options = new ScriptEditorOptions()
{
ScriptId = scriptId ?? default,
AllowedTypes = AllowedTypes,
ArchiveId = ArchiveId,
GameId = GameId,
RedistributableId = RedistributableId
};
Snippets = ScriptService.GetSnippets();
var modalRef = await ModalService.CreateModalAsync<ScriptEditorDialog, ScriptEditorOptions, Script>(modalOptions, options);
if (Script == null)
Script = new Script();
}
modalRef.OnOk = async (script) =>
{
private async void Edit(Script script = null)
{
if (script == null) {
if (GameId != Guid.Empty)
Script = new Script() { GameId = GameId };
if (RedistributableId != Guid.Empty)
Script = new Script() { RedistributableId = RedistributableId };
if (Editor != null)
await Editor.SetValue("");
}
else
Script = script;
if (Editor != null && Script != null)
await Editor.SetValue(Script.Contents);
ModalVisible = true;
};
StateHasChanged();
}
@ -179,81 +102,4 @@
await MessageService.Success("Script deleted!");
}
private void Close()
{
ModalVisible = false;
}
private async Task Save()
{
var value = await Editor.GetValue();
Script.Contents = value;
if (Script.Id == Guid.Empty)
Script = await ScriptService.Add(Script);
else
Script = await ScriptService.Update(Script);
Close();
StateHasChanged();
await MessageService.Success("Script saved!");
}
private async Task InsertText(string text)
{
var line = await Editor.GetPosition();
var range = new BlazorMonaco.Range(line.LineNumber, 1, line.LineNumber, 1);
var currentSelections = await Editor.GetSelections();
await Editor.ExecuteEdits("ScriptEditor", new List<IdentifiedSingleEditOperation>()
{
new IdentifiedSingleEditOperation
{
Range = range,
Text = text,
ForceMoveMarkers = true
}
}, currentSelections);
}
private async Task InsertSnippet(Snippet snippet)
{
await InsertText(snippet.Content);
}
private async void BrowseForPath()
{
var modalOptions = new ModalOptions()
{
Title = "Choose Reference",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Insert File Path"
};
var browserOptions = new FilePickerOptions()
{
ArchiveId = ArchiveId,
Select = true,
Multiple = false
};
var modalRef = await ModalService.CreateModalAsync<FilePickerDialog, FilePickerOptions, IEnumerable<IFileManagerEntry>>(modalOptions, browserOptions);
modalRef.OnOk = (results) =>
{
var path = results.FirstOrDefault().Path;
InsertText($"$InstallDir\\{path.Replace('/', '\\')}");
StateHasChanged();
return Task.CompletedTask;
};
}
}

View File

@ -0,0 +1,176 @@
@using LANCommander.Components.FileManagerComponents;
@using LANCommander.Extensions;
@using LANCommander.Data.Enums;
@using LANCommander.Models;
@inherits FeedbackComponent<ScriptEditorOptions, Script>
@inject ScriptService ScriptService
@inject ModalService ModalService
@inject IMessageService MessageService
<Form Model="@Script" Layout="@FormLayout.Vertical">
<FormItem>
@foreach (var group in Snippets.Select(s => s.Group).Distinct())
{
<Dropdown>
<Overlay>
<Menu>
@foreach (var snippet in Snippets.Where(s => s.Group == group))
{
<MenuItem OnClick="() => InsertSnippet(snippet)">
@snippet.Name
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<Button Type="@ButtonType.Primary">@group</Button>
</ChildContent>
</Dropdown>
}
@if (Options.ArchiveId != Guid.Empty)
{
<Button Icon="@IconType.Outline.FolderOpen" OnClick="BrowseForPath" Type="@ButtonType.Text">Browse</Button>
}
<Button Icon="@IconType.Outline.Build" OnClick="() => RegToPowerShell.Open()" Type="@ButtonType.Text">Import .reg</Button>
</FormItem>
<FormItem>
<StandaloneCodeEditor @ref="Editor" Id="@("editor-" + Id.ToString())" ConstructionOptions="EditorConstructionOptions" />
</FormItem>
<FormItem Label="Name">
<Input @bind-Value="@context.Name" />
</FormItem>
<FormItem Label="Type">
<Select @bind-Value="context.Type" TItem="ScriptType" TItemValue="ScriptType" DataSource="Enum.GetValues<ScriptType>().Where(st => Options.AllowedTypes == null || Options.AllowedTypes.Contains(st))">
<LabelTemplate Context="Value">@Value.GetDisplayName()</LabelTemplate>
<ItemTemplate Context="Value">@Value.GetDisplayName()</ItemTemplate>
</Select>
</FormItem>
<FormItem>
<Checkbox @bind-Checked="context.RequiresAdmin">Requires Admin</Checkbox>
</FormItem>
<FormItem Label="Description">
<TextArea @bind-Value="context.Description" MaxLength=500 ShowCount />
</FormItem>
</Form>
<RegToPowerShell @ref="RegToPowerShell" OnParsed="(text) => InsertText(text)" />
@code {
Guid Id = Guid.NewGuid();
Script Script;
StandaloneCodeEditor? Editor;
RegToPowerShell RegToPowerShell;
IEnumerable<Snippet> Snippets { get; set; }
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
AutomaticLayout = true,
Language = "powershell",
Value = Script.Contents,
Theme = "vs-dark",
};
}
protected override async Task OnInitializedAsync()
{
if (Options.ScriptId != Guid.Empty)
Script = await ScriptService.Get(Options.ScriptId);
Snippets = ScriptService.GetSnippets();
}
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
await Save();
Editor.Dispose();
await base.OkCancelRefWithResult!.OnOk(Script);
}
public override async Task CancelAsync(ModalClosingEventArgs args)
{
Editor.Dispose();
await base.CancelAsync(args);
}
private async void BrowseForPath()
{
var modalOptions = new ModalOptions()
{
Title = "Choose Reference",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Insert File Path"
};
var browserOptions = new FilePickerOptions()
{
ArchiveId = Options.ArchiveId,
Select = true,
Multiple = false
};
var modalRef = await ModalService.CreateModalAsync<FilePickerDialog, FilePickerOptions, IEnumerable<IFileManagerEntry>>(modalOptions, browserOptions);
modalRef.OnOk = (results) =>
{
var path = results.FirstOrDefault().Path;
InsertText($"$InstallDir\\{path.Replace('/', '\\')}");
StateHasChanged();
return Task.CompletedTask;
};
}
private async Task InsertText(string text)
{
var line = await Editor.GetPosition();
var range = new BlazorMonaco.Range(line.LineNumber, 1, line.LineNumber, 1);
var currentSelections = await Editor.GetSelections();
await Editor.ExecuteEdits("ScriptEditor", new List<IdentifiedSingleEditOperation>()
{
new IdentifiedSingleEditOperation
{
Range = range,
Text = text,
ForceMoveMarkers = true
}
}, currentSelections);
}
private async Task InsertSnippet(Snippet snippet)
{
await InsertText(snippet.Content);
}
private async Task Save()
{
var value = await Editor.GetValue();
Script.Contents = value;
if (Script.Id == Guid.Empty)
Script = await ScriptService.Add(Script);
else
Script = await ScriptService.Update(Script);
await MessageService.Success("Script saved!");
}
}

View File

@ -0,0 +1,53 @@
using LANCommander.Data.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers.Api
{
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
[ApiController]
public class PlaySessionsController : ControllerBase
{
private readonly PlaySessionService PlaySessionService;
private readonly GameService GameService;
private readonly UserManager<User> UserManager;
public PlaySessionsController(PlaySessionService playSessionService, GameService gameService, UserManager<User> userManager)
{
PlaySessionService = playSessionService;
GameService = gameService;
UserManager = userManager;
}
[HttpPost("Start/{id}")]
public async Task<IActionResult> Start(Guid id)
{
var user = await UserManager.FindByNameAsync(User.Identity.Name);
var game = await GameService.Get(id);
if (game == null || user == null)
return BadRequest();
await PlaySessionService.StartSession(game.Id, user.Id);
return Ok();
}
[HttpPost("End/{id}")]
public async Task<IActionResult> End(Guid id)
{
var user = await UserManager.FindByNameAsync(User.Identity.Name);
var game = await GameService.Get(id);
if (game == null || user == null)
return BadRequest();
await PlaySessionService.EndSession(game.Id, user.Id);
return Ok();
}
}
}

View File

@ -31,7 +31,14 @@ namespace LANCommander.Controllers
if (!System.IO.File.Exists(filename))
return NotFound();
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{archive.Game.Title.SanitizeFilename()}.zip");
string name = "";
if (archive.GameId != null && archive.GameId != Guid.Empty)
name = $"{archive.Game.Title.SanitizeFilename()}.zip";
else if (archive.RedistributableId != null && archive.RedistributableId != Guid.Empty)
name = $"{archive.Redistributable.Name.SanitizeFilename()}.zip";
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", name);
}
}
}

View File

@ -17,6 +17,25 @@ namespace LANCommander.Data
{
base.OnModelCreating(builder);
builder.ConfigureBaseRelationships<Data.Models.Action>();
builder.ConfigureBaseRelationships<Archive>();
builder.ConfigureBaseRelationships<Category>();
builder.ConfigureBaseRelationships<Company>();
builder.ConfigureBaseRelationships<Game>();
builder.ConfigureBaseRelationships<GameSave>();
builder.ConfigureBaseRelationships<Genre>();
builder.ConfigureBaseRelationships<Key>();
builder.ConfigureBaseRelationships<Media>();
builder.ConfigureBaseRelationships<MultiplayerMode>();
builder.ConfigureBaseRelationships<PlaySession>();
builder.ConfigureBaseRelationships<Redistributable>();
builder.ConfigureBaseRelationships<SavePath>();
builder.ConfigureBaseRelationships<Script>();
builder.ConfigureBaseRelationships<Server>();
builder.ConfigureBaseRelationships<ServerConsole>();
builder.ConfigureBaseRelationships<ServerHttpPath>();
builder.ConfigureBaseRelationships<Tag>();
builder.Entity<Genre>()
.HasMany(g => g.Games)
.WithMany(g => g.Genres);
@ -34,6 +53,7 @@ namespace LANCommander.Data
.HasMany(t => t.Games)
.WithMany(g => g.Tags);
#region Game Relationships
builder.Entity<Game>()
.HasMany(g => g.Archives)
.WithOne(g => g.Game)
@ -63,6 +83,28 @@ namespace LANCommander.Data
.IsRequired(true)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Game>()
.HasMany(g => g.Media)
.WithOne(m => m.Game)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Game>()
.HasMany(g => g.SavePaths)
.WithOne(p => p.Game)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Game>()
.HasMany(g => g.PlaySessions)
.WithOne(ps => ps.Game)
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
builder.Entity<Game>()
.HasMany(g => g.GameSaves)
.WithOne(gs => gs.Game)
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
builder.Entity<Game>()
.HasMany(g => g.Developers)
.WithMany(c => c.DevelopedGames)
@ -89,29 +131,28 @@ namespace LANCommander.Data
gr => gr.HasOne<Redistributable>().WithMany().HasForeignKey("RedistributableId"),
gr => gr.HasOne<Game>().WithMany().HasForeignKey("GameId")
);
#endregion
builder.Entity<Game>()
.HasMany(g => g.Media)
.WithOne(m => m.Game)
.OnDelete(DeleteBehavior.Cascade);
#region User Relationships
builder.Entity<User>()
.HasMany(u => u.GameSaves)
.WithOne(gs => gs.User)
.IsRequired(true)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<Game>()
.HasMany(g => g.GameSaves)
.WithOne(gs => gs.Game)
builder.Entity<User>()
.HasMany(u => u.PlaySessions)
.WithOne(ps => ps.User)
.IsRequired(true)
.OnDelete(DeleteBehavior.NoAction);
.OnDelete(DeleteBehavior.Cascade);
#endregion
#region Server Relationships
builder.Entity<Server>()
.HasOne(s => s.Game)
.WithMany(g => g.Servers)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
.OnDelete(DeleteBehavior.SetNull);
builder.Entity<Server>()
.HasMany<ServerConsole>()
@ -124,7 +165,9 @@ namespace LANCommander.Data
.WithOne(s => s.Server)
.IsRequired(true)
.OnDelete(DeleteBehavior.Cascade);
#endregion
#region Redistributable Relationships
builder.Entity<Redistributable>()
.HasMany(r => r.Archives)
.WithOne(a => a.Redistributable)
@ -136,6 +179,7 @@ namespace LANCommander.Data
.WithOne(s => s.Redistributable)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
#endregion
}
public DbSet<Game>? Games { get; set; }
@ -152,6 +196,8 @@ namespace LANCommander.Data
public DbSet<GameSave>? GameSaves { get; set; }
public DbSet<PlaySession>? PlaySessions { get; set; }
public DbSet<Server>? Servers { get; set; }
public DbSet<ServerConsole>? ServerConsoles { get; set; }

View File

@ -0,0 +1,23 @@
using LANCommander.Data.Models;
using Microsoft.EntityFrameworkCore;
namespace LANCommander.Data
{
public static class ModelBuilderExtensions
{
public static void ConfigureBaseRelationships<T>(this ModelBuilder modelBuilder) where T : BaseModel
{
modelBuilder.Entity<T>()
.HasOne(x => x.CreatedBy)
.WithMany()
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<T>()
.HasOne(x => x.UpdatedBy)
.WithMany()
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
}
}
}

View File

@ -10,10 +10,13 @@ namespace LANCommander.Data.Models
[Display(Name = "Created On")]
public DateTime CreatedOn { get; set; }
[Display(Name = "Created By")]
public virtual User? CreatedBy { get; set; }
[Display(Name = "Updated On")]
public DateTime UpdatedOn { get; set; }
[Display(Name = "Updated By")]
public virtual User? UpdatedBy { get; set; }
}

View File

@ -31,6 +31,7 @@ namespace LANCommander.Data.Models
public virtual ICollection<Archive>? Archives { get; set; }
public virtual ICollection<Script>? Scripts { get; set; }
public virtual ICollection<GameSave>? GameSaves { get; set; }
public virtual ICollection<PlaySession>? PlaySessions { get; set; }
public virtual ICollection<SavePath>? SavePaths { get; set; }
public virtual ICollection<Server>? Servers { get; set; }
public virtual ICollection<Redistributable>? Redistributables { get; set; }

View File

@ -7,7 +7,7 @@ namespace LANCommander.Data.Models
[Table("GameSaves")]
public class GameSave : BaseModel
{
public Guid GameId { get; set; }
public Guid? GameId { get; set; }
[JsonIgnore]
[ForeignKey(nameof(GameId))]
[InverseProperty("GameSaves")]

View File

@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace LANCommander.Data.Models
{
[Table("PlaySessions")]
public class PlaySession : BaseModel
{
public Guid? GameId { get; set; }
[JsonIgnore]
[ForeignKey(nameof(GameId))]
[InverseProperty("PlaySessions")]
public virtual Game? Game { get; set; }
public Guid UserId { get; set; }
[ForeignKey(nameof(UserId))]
[InverseProperty("PlaySessions")]
public virtual User? User { get; set; }
[Display(Name = "Start")]
public DateTime? Start { get; set; }
[Display(Name = "End")]
public DateTime? End { get; set; }
}
}

View File

@ -46,6 +46,9 @@ namespace LANCommander.Data.Models
[JsonIgnore]
public virtual ICollection<GameSave>? GameSaves { get; set; }
[JsonIgnore]
public virtual ICollection<PlaySession>? PlaySessions { get; set; }
[JsonIgnore]
public bool Approved { get; set; }

View File

@ -88,6 +88,81 @@
<None Update="favicon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Copy Directory.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Create Directory.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Create Registry Path.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Get User Registry Virtual Store.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Patch Binary.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Remove Directory.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Rename File.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Sanitize Filename.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Set Compatibility Mode.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Set Registry Key.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Trim String.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Examples\Write to File.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Functions\Convert-AspectRatio.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Functions\ConvertTo-StringBytes.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Functions\Edit-PatchBinary.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Functions\Get-GameManifest.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Functions\Get-PrimaryDisplay.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Functions\Write-ReplaceContentInFile.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Variables\Allocated Key.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Variables\Default Install Directory.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Variables\Game Install Directory.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Variables\Game Manifest.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Variables\New Player Alias.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Variables\Old Player Alias.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Snippets\Variables\Server Address.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
using LANCommander.Data.Enums;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class DeleteDeprecatedSnippets : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
string[] snippetsToRemove = new string[]
{
"Examples\\Replace Content In File.ps1",
"Examples\\Separate ASCII Bytes.ps1",
"Examples\\String to ASCII Bytes.ps1",
"Functions\\Get-43Resolution.ps1",
"Functions\\Get-AsciiBytes.ps1",
"Functions\\Patch-Binary.ps1",
"Functions\\Separate-AsciiBytes.ps1",
"Variables\\Display.ps1",
"Variables\\InstallDir.ps1",
"Variables\\NewName.ps1",
"Variables\\OldName.ps1",
};
foreach (var snippet in snippetsToRemove)
{
var path = Path.Combine("Snippets", snippet);
if (File.Exists(path))
File.Delete(path);
}
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, '$NewName = $args[0]' || char(13) || char(10), '')");
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, '$OldName = \"\"' || char(13) || char(10) || 'if ($args[1]) {' || char(13) || char(10) || char(9) || '$OldName = $args[1]' || char(13) || char(10) || '}' || char(13) || char(10), '')");
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, '$InstallDir = $PSScriptRoot' || char(13) || char(10), '')");
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, '$InstallDir', '$InstallDirectory')");
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, '$NewName', '$NewPlayerAlias')");
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, '$OldName', '$OldPlayerAlias')");
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, 'function Write-ReplaceContentInFile', 'function Write-ReplaceContentInFile-Old')");
migrationBuilder.Sql($"UPDATE Scripts SET Contents = REPLACE(Contents, '$args[0]', '$AllocatedKey') WHERE Type = {(int)ScriptType.Install}");
migrationBuilder.Sql($"UPDATE Scripts SET Contents = REPLACE(Contents, '$args[0]', '$AllocatedKey') WHERE Type = {(int)ScriptType.KeyChange}");
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, 'Add-Type -AssemblyName System.Windows.Forms' || char(13) || char(10), '')");
migrationBuilder.Sql("UPDATE Scripts SET Contents = REPLACE(Contents, '$Display = [System.Windows.Forms.Screen]::AllScreens | Where-Object Primary | Select Bounds', '$Display = Get-PrimaryDisplay')");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddPlaySessions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PlaySessions",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
GameId = table.Column<Guid>(type: "TEXT", nullable: false),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
Start = table.Column<DateTime>(type: "TEXT", nullable: true),
End = table.Column<DateTime>(type: "TEXT", nullable: true),
CreatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedById = table.Column<Guid>(type: "TEXT", nullable: true),
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedById = table.Column<Guid>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlaySessions", x => x.Id);
table.ForeignKey(
name: "FK_PlaySessions_AspNetUsers_CreatedById",
column: x => x.CreatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_PlaySessions_AspNetUsers_UpdatedById",
column: x => x.UpdatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_PlaySessions_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PlaySessions_Games_GameId",
column: x => x.GameId,
principalTable: "Games",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_PlaySessions_CreatedById",
table: "PlaySessions",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_PlaySessions_GameId",
table: "PlaySessions",
column: "GameId");
migrationBuilder.CreateIndex(
name: "IX_PlaySessions_UpdatedById",
table: "PlaySessions",
column: "UpdatedById");
migrationBuilder.CreateIndex(
name: "IX_PlaySessions_UserId",
table: "PlaySessions",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PlaySessions");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,975 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class FixDeletionBehaviors : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Actions_AspNetUsers_CreatedById",
table: "Actions");
migrationBuilder.DropForeignKey(
name: "FK_Actions_AspNetUsers_UpdatedById",
table: "Actions");
migrationBuilder.DropForeignKey(
name: "FK_Archive_AspNetUsers_CreatedById",
table: "Archive");
migrationBuilder.DropForeignKey(
name: "FK_Archive_AspNetUsers_UpdatedById",
table: "Archive");
migrationBuilder.DropForeignKey(
name: "FK_Categories_AspNetUsers_CreatedById",
table: "Categories");
migrationBuilder.DropForeignKey(
name: "FK_Categories_AspNetUsers_UpdatedById",
table: "Categories");
migrationBuilder.DropForeignKey(
name: "FK_Companies_AspNetUsers_CreatedById",
table: "Companies");
migrationBuilder.DropForeignKey(
name: "FK_Companies_AspNetUsers_UpdatedById",
table: "Companies");
migrationBuilder.DropForeignKey(
name: "FK_Games_AspNetUsers_CreatedById",
table: "Games");
migrationBuilder.DropForeignKey(
name: "FK_Games_AspNetUsers_UpdatedById",
table: "Games");
migrationBuilder.DropForeignKey(
name: "FK_GameSaves_AspNetUsers_CreatedById",
table: "GameSaves");
migrationBuilder.DropForeignKey(
name: "FK_GameSaves_AspNetUsers_UpdatedById",
table: "GameSaves");
migrationBuilder.DropForeignKey(
name: "FK_GameSaves_Games_GameId",
table: "GameSaves");
migrationBuilder.DropForeignKey(
name: "FK_Genres_AspNetUsers_CreatedById",
table: "Genres");
migrationBuilder.DropForeignKey(
name: "FK_Genres_AspNetUsers_UpdatedById",
table: "Genres");
migrationBuilder.DropForeignKey(
name: "FK_Keys_AspNetUsers_CreatedById",
table: "Keys");
migrationBuilder.DropForeignKey(
name: "FK_Keys_AspNetUsers_UpdatedById",
table: "Keys");
migrationBuilder.DropForeignKey(
name: "FK_Media_AspNetUsers_CreatedById",
table: "Media");
migrationBuilder.DropForeignKey(
name: "FK_Media_AspNetUsers_UpdatedById",
table: "Media");
migrationBuilder.DropForeignKey(
name: "FK_MultiplayerModes_AspNetUsers_CreatedById",
table: "MultiplayerModes");
migrationBuilder.DropForeignKey(
name: "FK_MultiplayerModes_AspNetUsers_UpdatedById",
table: "MultiplayerModes");
migrationBuilder.DropForeignKey(
name: "FK_PlaySessions_AspNetUsers_CreatedById",
table: "PlaySessions");
migrationBuilder.DropForeignKey(
name: "FK_PlaySessions_AspNetUsers_UpdatedById",
table: "PlaySessions");
migrationBuilder.DropForeignKey(
name: "FK_PlaySessions_Games_GameId",
table: "PlaySessions");
migrationBuilder.DropForeignKey(
name: "FK_Redistributables_AspNetUsers_CreatedById",
table: "Redistributables");
migrationBuilder.DropForeignKey(
name: "FK_Redistributables_AspNetUsers_UpdatedById",
table: "Redistributables");
migrationBuilder.DropForeignKey(
name: "FK_SavePaths_AspNetUsers_CreatedById",
table: "SavePaths");
migrationBuilder.DropForeignKey(
name: "FK_SavePaths_AspNetUsers_UpdatedById",
table: "SavePaths");
migrationBuilder.DropForeignKey(
name: "FK_SavePaths_Games_GameId",
table: "SavePaths");
migrationBuilder.DropForeignKey(
name: "FK_Scripts_AspNetUsers_CreatedById",
table: "Scripts");
migrationBuilder.DropForeignKey(
name: "FK_Scripts_AspNetUsers_UpdatedById",
table: "Scripts");
migrationBuilder.DropForeignKey(
name: "FK_ServerConsoles_AspNetUsers_CreatedById",
table: "ServerConsoles");
migrationBuilder.DropForeignKey(
name: "FK_ServerConsoles_AspNetUsers_UpdatedById",
table: "ServerConsoles");
migrationBuilder.DropForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_CreatedById",
table: "ServerHttpPath");
migrationBuilder.DropForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_UpdatedById",
table: "ServerHttpPath");
migrationBuilder.DropForeignKey(
name: "FK_Servers_AspNetUsers_CreatedById",
table: "Servers");
migrationBuilder.DropForeignKey(
name: "FK_Servers_AspNetUsers_UpdatedById",
table: "Servers");
migrationBuilder.DropForeignKey(
name: "FK_Servers_Games_GameId",
table: "Servers");
migrationBuilder.DropForeignKey(
name: "FK_Tags_AspNetUsers_CreatedById",
table: "Tags");
migrationBuilder.DropForeignKey(
name: "FK_Tags_AspNetUsers_UpdatedById",
table: "Tags");
migrationBuilder.AlterColumn<Guid>(
name: "GameId",
table: "PlaySessions",
type: "TEXT",
nullable: true,
oldClrType: typeof(Guid),
oldType: "TEXT");
migrationBuilder.AlterColumn<Guid>(
name: "GameId",
table: "GameSaves",
type: "TEXT",
nullable: true,
oldClrType: typeof(Guid),
oldType: "TEXT");
migrationBuilder.AddForeignKey(
name: "FK_Actions_AspNetUsers_CreatedById",
table: "Actions",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Actions_AspNetUsers_UpdatedById",
table: "Actions",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Archive_AspNetUsers_CreatedById",
table: "Archive",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Archive_AspNetUsers_UpdatedById",
table: "Archive",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Categories_AspNetUsers_CreatedById",
table: "Categories",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Categories_AspNetUsers_UpdatedById",
table: "Categories",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Companies_AspNetUsers_CreatedById",
table: "Companies",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Companies_AspNetUsers_UpdatedById",
table: "Companies",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Games_AspNetUsers_CreatedById",
table: "Games",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Games_AspNetUsers_UpdatedById",
table: "Games",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_GameSaves_AspNetUsers_CreatedById",
table: "GameSaves",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_GameSaves_AspNetUsers_UpdatedById",
table: "GameSaves",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_GameSaves_Games_GameId",
table: "GameSaves",
column: "GameId",
principalTable: "Games",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Genres_AspNetUsers_CreatedById",
table: "Genres",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Genres_AspNetUsers_UpdatedById",
table: "Genres",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Keys_AspNetUsers_CreatedById",
table: "Keys",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Keys_AspNetUsers_UpdatedById",
table: "Keys",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Media_AspNetUsers_CreatedById",
table: "Media",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Media_AspNetUsers_UpdatedById",
table: "Media",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_MultiplayerModes_AspNetUsers_CreatedById",
table: "MultiplayerModes",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_MultiplayerModes_AspNetUsers_UpdatedById",
table: "MultiplayerModes",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_PlaySessions_AspNetUsers_CreatedById",
table: "PlaySessions",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_PlaySessions_AspNetUsers_UpdatedById",
table: "PlaySessions",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_PlaySessions_Games_GameId",
table: "PlaySessions",
column: "GameId",
principalTable: "Games",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Redistributables_AspNetUsers_CreatedById",
table: "Redistributables",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Redistributables_AspNetUsers_UpdatedById",
table: "Redistributables",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_SavePaths_AspNetUsers_CreatedById",
table: "SavePaths",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_SavePaths_AspNetUsers_UpdatedById",
table: "SavePaths",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_SavePaths_Games_GameId",
table: "SavePaths",
column: "GameId",
principalTable: "Games",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Scripts_AspNetUsers_CreatedById",
table: "Scripts",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Scripts_AspNetUsers_UpdatedById",
table: "Scripts",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_ServerConsoles_AspNetUsers_CreatedById",
table: "ServerConsoles",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_ServerConsoles_AspNetUsers_UpdatedById",
table: "ServerConsoles",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_CreatedById",
table: "ServerHttpPath",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_UpdatedById",
table: "ServerHttpPath",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Servers_AspNetUsers_CreatedById",
table: "Servers",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Servers_AspNetUsers_UpdatedById",
table: "Servers",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Servers_Games_GameId",
table: "Servers",
column: "GameId",
principalTable: "Games",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Tags_AspNetUsers_CreatedById",
table: "Tags",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Tags_AspNetUsers_UpdatedById",
table: "Tags",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Actions_AspNetUsers_CreatedById",
table: "Actions");
migrationBuilder.DropForeignKey(
name: "FK_Actions_AspNetUsers_UpdatedById",
table: "Actions");
migrationBuilder.DropForeignKey(
name: "FK_Archive_AspNetUsers_CreatedById",
table: "Archive");
migrationBuilder.DropForeignKey(
name: "FK_Archive_AspNetUsers_UpdatedById",
table: "Archive");
migrationBuilder.DropForeignKey(
name: "FK_Categories_AspNetUsers_CreatedById",
table: "Categories");
migrationBuilder.DropForeignKey(
name: "FK_Categories_AspNetUsers_UpdatedById",
table: "Categories");
migrationBuilder.DropForeignKey(
name: "FK_Companies_AspNetUsers_CreatedById",
table: "Companies");
migrationBuilder.DropForeignKey(
name: "FK_Companies_AspNetUsers_UpdatedById",
table: "Companies");
migrationBuilder.DropForeignKey(
name: "FK_Games_AspNetUsers_CreatedById",
table: "Games");
migrationBuilder.DropForeignKey(
name: "FK_Games_AspNetUsers_UpdatedById",
table: "Games");
migrationBuilder.DropForeignKey(
name: "FK_GameSaves_AspNetUsers_CreatedById",
table: "GameSaves");
migrationBuilder.DropForeignKey(
name: "FK_GameSaves_AspNetUsers_UpdatedById",
table: "GameSaves");
migrationBuilder.DropForeignKey(
name: "FK_GameSaves_Games_GameId",
table: "GameSaves");
migrationBuilder.DropForeignKey(
name: "FK_Genres_AspNetUsers_CreatedById",
table: "Genres");
migrationBuilder.DropForeignKey(
name: "FK_Genres_AspNetUsers_UpdatedById",
table: "Genres");
migrationBuilder.DropForeignKey(
name: "FK_Keys_AspNetUsers_CreatedById",
table: "Keys");
migrationBuilder.DropForeignKey(
name: "FK_Keys_AspNetUsers_UpdatedById",
table: "Keys");
migrationBuilder.DropForeignKey(
name: "FK_Media_AspNetUsers_CreatedById",
table: "Media");
migrationBuilder.DropForeignKey(
name: "FK_Media_AspNetUsers_UpdatedById",
table: "Media");
migrationBuilder.DropForeignKey(
name: "FK_MultiplayerModes_AspNetUsers_CreatedById",
table: "MultiplayerModes");
migrationBuilder.DropForeignKey(
name: "FK_MultiplayerModes_AspNetUsers_UpdatedById",
table: "MultiplayerModes");
migrationBuilder.DropForeignKey(
name: "FK_PlaySessions_AspNetUsers_CreatedById",
table: "PlaySessions");
migrationBuilder.DropForeignKey(
name: "FK_PlaySessions_AspNetUsers_UpdatedById",
table: "PlaySessions");
migrationBuilder.DropForeignKey(
name: "FK_PlaySessions_Games_GameId",
table: "PlaySessions");
migrationBuilder.DropForeignKey(
name: "FK_Redistributables_AspNetUsers_CreatedById",
table: "Redistributables");
migrationBuilder.DropForeignKey(
name: "FK_Redistributables_AspNetUsers_UpdatedById",
table: "Redistributables");
migrationBuilder.DropForeignKey(
name: "FK_SavePaths_AspNetUsers_CreatedById",
table: "SavePaths");
migrationBuilder.DropForeignKey(
name: "FK_SavePaths_AspNetUsers_UpdatedById",
table: "SavePaths");
migrationBuilder.DropForeignKey(
name: "FK_SavePaths_Games_GameId",
table: "SavePaths");
migrationBuilder.DropForeignKey(
name: "FK_Scripts_AspNetUsers_CreatedById",
table: "Scripts");
migrationBuilder.DropForeignKey(
name: "FK_Scripts_AspNetUsers_UpdatedById",
table: "Scripts");
migrationBuilder.DropForeignKey(
name: "FK_ServerConsoles_AspNetUsers_CreatedById",
table: "ServerConsoles");
migrationBuilder.DropForeignKey(
name: "FK_ServerConsoles_AspNetUsers_UpdatedById",
table: "ServerConsoles");
migrationBuilder.DropForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_CreatedById",
table: "ServerHttpPath");
migrationBuilder.DropForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_UpdatedById",
table: "ServerHttpPath");
migrationBuilder.DropForeignKey(
name: "FK_Servers_AspNetUsers_CreatedById",
table: "Servers");
migrationBuilder.DropForeignKey(
name: "FK_Servers_AspNetUsers_UpdatedById",
table: "Servers");
migrationBuilder.DropForeignKey(
name: "FK_Servers_Games_GameId",
table: "Servers");
migrationBuilder.DropForeignKey(
name: "FK_Tags_AspNetUsers_CreatedById",
table: "Tags");
migrationBuilder.DropForeignKey(
name: "FK_Tags_AspNetUsers_UpdatedById",
table: "Tags");
migrationBuilder.AlterColumn<Guid>(
name: "GameId",
table: "PlaySessions",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "GameId",
table: "GameSaves",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_Actions_AspNetUsers_CreatedById",
table: "Actions",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Actions_AspNetUsers_UpdatedById",
table: "Actions",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Archive_AspNetUsers_CreatedById",
table: "Archive",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Archive_AspNetUsers_UpdatedById",
table: "Archive",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Categories_AspNetUsers_CreatedById",
table: "Categories",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Categories_AspNetUsers_UpdatedById",
table: "Categories",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Companies_AspNetUsers_CreatedById",
table: "Companies",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Companies_AspNetUsers_UpdatedById",
table: "Companies",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Games_AspNetUsers_CreatedById",
table: "Games",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Games_AspNetUsers_UpdatedById",
table: "Games",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_GameSaves_AspNetUsers_CreatedById",
table: "GameSaves",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_GameSaves_AspNetUsers_UpdatedById",
table: "GameSaves",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_GameSaves_Games_GameId",
table: "GameSaves",
column: "GameId",
principalTable: "Games",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Genres_AspNetUsers_CreatedById",
table: "Genres",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Genres_AspNetUsers_UpdatedById",
table: "Genres",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Keys_AspNetUsers_CreatedById",
table: "Keys",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Keys_AspNetUsers_UpdatedById",
table: "Keys",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Media_AspNetUsers_CreatedById",
table: "Media",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Media_AspNetUsers_UpdatedById",
table: "Media",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_MultiplayerModes_AspNetUsers_CreatedById",
table: "MultiplayerModes",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_MultiplayerModes_AspNetUsers_UpdatedById",
table: "MultiplayerModes",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_PlaySessions_AspNetUsers_CreatedById",
table: "PlaySessions",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_PlaySessions_AspNetUsers_UpdatedById",
table: "PlaySessions",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_PlaySessions_Games_GameId",
table: "PlaySessions",
column: "GameId",
principalTable: "Games",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Redistributables_AspNetUsers_CreatedById",
table: "Redistributables",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Redistributables_AspNetUsers_UpdatedById",
table: "Redistributables",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_SavePaths_AspNetUsers_CreatedById",
table: "SavePaths",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_SavePaths_AspNetUsers_UpdatedById",
table: "SavePaths",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_SavePaths_Games_GameId",
table: "SavePaths",
column: "GameId",
principalTable: "Games",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Scripts_AspNetUsers_CreatedById",
table: "Scripts",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Scripts_AspNetUsers_UpdatedById",
table: "Scripts",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_ServerConsoles_AspNetUsers_CreatedById",
table: "ServerConsoles",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_ServerConsoles_AspNetUsers_UpdatedById",
table: "ServerConsoles",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_CreatedById",
table: "ServerHttpPath",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_ServerHttpPath_AspNetUsers_UpdatedById",
table: "ServerHttpPath",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Servers_AspNetUsers_CreatedById",
table: "Servers",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Servers_AspNetUsers_UpdatedById",
table: "Servers",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Servers_Games_GameId",
table: "Servers",
column: "GameId",
principalTable: "Games",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Tags_AspNetUsers_CreatedById",
table: "Tags",
column: "CreatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Tags_AspNetUsers_UpdatedById",
table: "Tags",
column: "UpdatedById",
principalTable: "AspNetUsers",
principalColumn: "Id");
}
}
}

View File

@ -355,7 +355,7 @@ namespace LANCommander.Migrations
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<Guid>("GameId")
b.Property<Guid?>("GameId")
.HasColumnType("TEXT");
b.Property<Guid?>("UpdatedById")
@ -570,6 +570,49 @@ namespace LANCommander.Migrations
b.ToTable("MultiplayerModes");
});
modelBuilder.Entity("LANCommander.Data.Models.PlaySession", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid?>("CreatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<DateTime?>("End")
.HasColumnType("TEXT");
b.Property<Guid?>("GameId")
.HasColumnType("TEXT");
b.Property<DateTime?>("Start")
.HasColumnType("TEXT");
b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("GameId");
b.HasIndex("UpdatedById");
b.HasIndex("UserId");
b.ToTable("PlaySessions");
});
modelBuilder.Entity("LANCommander.Data.Models.Redistributable", b =>
{
b.Property<Guid>("Id")
@ -1208,7 +1251,8 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Actions")
@ -1218,7 +1262,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1231,7 +1276,8 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Archives")
@ -1249,7 +1295,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1266,7 +1313,8 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Category", "Parent")
.WithMany("Children")
@ -1274,7 +1322,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1287,11 +1336,13 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1302,11 +1353,13 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1317,17 +1370,18 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("GameSaves")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "User")
.WithMany("GameSaves")
@ -1348,11 +1402,13 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1367,7 +1423,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Keys")
@ -1377,7 +1434,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("ClaimedByUser");
@ -1392,7 +1450,8 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Media")
@ -1402,7 +1461,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1415,7 +1475,8 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("MultiplayerModes")
@ -1425,7 +1486,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1434,15 +1496,49 @@ namespace LANCommander.Migrations
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.PlaySession", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("PlaySessions")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "User")
.WithMany("PlaySessions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreatedBy");
b.Navigation("Game");
b.Navigation("UpdatedBy");
b.Navigation("User");
});
modelBuilder.Entity("LANCommander.Data.Models.Redistributable", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1453,15 +1549,18 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("SavePaths")
.HasForeignKey("GameId");
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1474,7 +1573,8 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Scripts")
@ -1488,7 +1588,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1503,16 +1604,18 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Game", "Game")
.WithMany("Servers")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.NoAction);
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1525,7 +1628,8 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Server", "Server")
.WithMany()
@ -1539,7 +1643,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1552,7 +1657,8 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.Server", "Server")
.WithMany()
@ -1566,7 +1672,8 @@ namespace LANCommander.Migrations
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1579,11 +1686,13 @@ namespace LANCommander.Migrations
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
.HasForeignKey("UpdatedById")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CreatedBy");
@ -1660,6 +1769,8 @@ namespace LANCommander.Migrations
b.Navigation("MultiplayerModes");
b.Navigation("PlaySessions");
b.Navigation("SavePaths");
b.Navigation("Scripts");
@ -1684,6 +1795,8 @@ namespace LANCommander.Migrations
modelBuilder.Entity("LANCommander.Data.Models.User", b =>
{
b.Navigation("GameSaves");
b.Navigation("PlaySessions");
});
#pragma warning restore 612, 618
}

View File

@ -0,0 +1,13 @@
using LANCommander.Data.Enums;
namespace LANCommander.Models
{
public class ScriptEditorOptions
{
public Guid ScriptId { get; set; }
public Guid GameId { get; set; }
public Guid RedistributableId { get; set; }
public Guid ArchiveId { get; set; }
public IEnumerable<ScriptType> AllowedTypes { get; set; }
}
}

View File

@ -16,7 +16,7 @@
</Template>;
}
<Modal Visible="ModalVisible" Title="Game Metadata Lookup" Footer="@Footer">
<Modal Visible="ModalVisible" Title="Game Metadata Lookup" Footer="@Footer" Class="game-metadata-lookup-modal">
<Table
@ref="ResultsTable"
TItem="Game"

View File

@ -128,11 +128,11 @@
</div>
<div data-panel="Scripts">
<ScriptEditor @bind-Scripts="Game.Scripts" GameId="Game.Id" ArchiveId="@LatestArchiveId" AllowedTypes="new ScriptType[] { ScriptType.Install, ScriptType.Uninstall, ScriptType.NameChange, ScriptType.KeyChange }" />
<ScriptEditor GameId="Game.Id" ArchiveId="@LatestArchiveId" AllowedTypes="new ScriptType[] { ScriptType.Install, ScriptType.Uninstall, ScriptType.NameChange, ScriptType.KeyChange }" />
</div>
<div data-panel="Archives">
<ArchiveEditor @bind-Archives="Game.Archives" GameId="Game.Id" />
<ArchiveEditor GameId="Game.Id" />
</div>
}
@ -269,7 +269,7 @@ else
Game.Tags = new List<Data.Models.Tag>();
if (result.IGDBMetadata.GameModes != null && result.IGDBMetadata.GameModes.Values != null)
Game.Singleplayer = result.IGDBMetadata.GameModes.Values.Any(gm => gm.Name == "Singleplayer");
Game.Singleplayer = result.IGDBMetadata.GameModes.Values.Any(gm => gm.Name == "Single player");
if (result.IGDBMetadata.InvolvedCompanies != null && result.IGDBMetadata.InvolvedCompanies.Values != null)
{

View File

@ -7,6 +7,7 @@
@attribute [Authorize]
@inject GameService GameService
@inject NavigationManager NavigationManager
@inject IMessageService MessageService
<PageHeader Title="Games">
<PageHeaderExtra>
@ -174,7 +175,14 @@
Loading = true;
await GameService.Delete(game);
try
{
await GameService.Delete(game);
}
catch (Exception ex)
{
MessageService.Error("Could not delete the game!");
}
Games = await GameService.Get(x => true).OrderBy(g => String.IsNullOrWhiteSpace(g.SortTitle) ? g.Title : g.SortTitle).ToListAsync();

View File

@ -14,7 +14,7 @@
<div style="padding: 0 24px;">
<Table TItem="GameSave" DataSource="@GameSaves">
<PropertyColumn Property="s => s.Game.Title" Sortable />
<PropertyColumn Property="s => (s.Game == null ? String.Empty : s.Game.Title)" Sortable Title="Game" />
<PropertyColumn Property="s => s.CreatedOn" Format="MM/dd/yyyy hh:mm tt" Sortable />
<ActionColumn Title="" Style="text-align: right; white-space: nowrap">
<Space Direction="DirectionVHType.Horizontal">
@ -45,7 +45,7 @@
User = await UserManager.FindByNameAsync(authState.User.Identity.Name);
if (User != null)
GameSaves = User.GameSaves.OrderBy(s => s.Game.Title).ThenBy(s => s.CreatedOn).ToList();
GameSaves = User.GameSaves.OrderBy(s => s.Game?.Title).ThenBy(s => s.CreatedOn).ToList();
Loading = false;
}

View File

@ -56,11 +56,11 @@
@if (Redistributable != null && Redistributable.Id != Guid.Empty)
{
<div data-panel="Scripts">
<ScriptEditor @bind-Scripts="Redistributable.Scripts" RedistributableId="Redistributable.Id" ArchiveId="@LatestArchiveId" AllowedTypes="new ScriptType[] { ScriptType.Install, ScriptType.DetectInstall }" />
<ScriptEditor RedistributableId="Redistributable.Id" ArchiveId="@LatestArchiveId" AllowedTypes="new ScriptType[] { ScriptType.Install, ScriptType.DetectInstall }" />
</div>
<div data-panel="Archives">
<ArchiveEditor @bind-Archives="Redistributable.Archives" RedistributableId="Redistributable.Id" />
<ArchiveEditor RedistributableId="Redistributable.Id" />
</div>
}
</div>

View File

@ -39,7 +39,7 @@
<ChildContent>
<Space Direction="DirectionVHType.Horizontal">
<SpaceItem>
<Button OnClick="() => Edit(context)">Edit</Button>
<a href="/Redistributables/@(context.Id)" class="ant-btn ant-btn-primary">Edit</a>
</SpaceItem>
<SpaceItem>
<Popconfirm OnConfirm="() => Delete(context)" Title="Are you sure you want to delete this redistributable?">
@ -85,11 +85,6 @@
NavigationManager.NavigateTo("/Redistributables/Add");
}
private void Edit(Redistributable redistributable)
{
NavigationManager.NavigateTo($"/Redistributables/{redistributable.Id}/General");
}
private async Task Delete(Redistributable redistributable)
{
Redistributables = new List<Redistributable>();

View File

@ -2,6 +2,7 @@
@page "/Servers/{id:guid}/{panel}"
@page "/Servers/{id:guid}/{panel}/{logId}"
@page "/Servers/Add"
@attribute [Authorize(Roles = "Administrator")]
@using LANCommander.Components.FileManagerComponents;
@using LANCommander.Pages.Servers.Components
@inject GameService GameService

View File

@ -1,7 +1,7 @@
@page "/Servers"
@using LANCommander.Pages.Servers.Components
@using Microsoft.EntityFrameworkCore;
@attribute [Authorize]
@attribute [Authorize(Roles = "Administrator")]
@inject ServerService ServerService
@inject ServerProcessService ServerProcessService
@inject NavigationManager NavigationManager

View File

@ -34,7 +34,7 @@
</ActionColumn>
</Table>
<ArchiveUploader @ref="Uploader" OnArchiveUploaded="OnArchiveUploaded" />
<ArchiveUploader @ref="Uploader" OnArchiveUploaded="LoadData" />
</div>
@ -78,21 +78,7 @@
System.IO.File.Create(archiveFilePath).Close();
await Uploader.Open(archive);
}
async Task OnArchiveUploaded(Archive archive)
{
try
{
await ArchiveService.Update(archive);
await LoadData();
}
catch (Exception ex)
{
await MessageService.Error("Archive could not be updated.");
}
await Uploader.Open(archive.Id);
}
async Task Delete(Archive archive)

View File

@ -11,6 +11,7 @@ using System.Text;
using Hangfire;
using NLog;
using LANCommander.Services.MediaGrabbers;
using Microsoft.Data.Sqlite;
namespace LANCommander
{
@ -142,6 +143,7 @@ namespace LANCommander
builder.Services.AddScoped<ServerService>();
builder.Services.AddScoped<ServerConsoleService>();
builder.Services.AddScoped<GameSaveService>();
builder.Services.AddScoped<PlaySessionService>();
builder.Services.AddScoped<MediaService>();
builder.Services.AddScoped<RedistributableService>();
builder.Services.AddScoped<IMediaGrabberService, SteamGridDBMediaGrabber>();
@ -220,11 +222,23 @@ namespace LANCommander
if (!Directory.Exists("Snippets"))
Directory.CreateDirectory("Snippets");
if (!Directory.Exists("Backups"))
Directory.CreateDirectory("Backups");
// Migrate
Logger.Debug("Migrating database if required");
await using var scope = app.Services.CreateAsyncScope();
using var db = scope.ServiceProvider.GetService<DatabaseContext>();
await db.Database.MigrateAsync();
if ((await db.Database.GetPendingMigrationsAsync()).Any())
{
var dataSource = new SqliteConnectionStringBuilder(settings.DatabaseConnectionString).DataSource;
if (File.Exists(dataSource))
File.Copy(dataSource, Path.Combine("Backups", $"LANCommander.db.{DateTime.Now.ToString("dd-MM-yyyy-HH.mm.ss.bak")}"));
await db.Database.MigrateAsync();
}
// Autostart any server processes
Logger.Debug("Autostarting Servers");

View File

@ -44,6 +44,7 @@ namespace LANCommander.Services
var manifest = new GameManifest()
{
Id = game.Id,
Title = game.Title,
SortTitle = game.SortTitle,
Description = game.Description,

View File

@ -0,0 +1,41 @@
using LANCommander.Data;
using LANCommander.Data.Models;
using LANCommander.Helpers;
using LANCommander.Models;
namespace LANCommander.Services
{
public class PlaySessionService : BaseDatabaseService<PlaySession>
{
public PlaySessionService(DatabaseContext dbContext, IHttpContextAccessor httpContextAccessor) : base(dbContext, httpContextAccessor) { }
public async Task StartSession(Guid gameId, Guid userId)
{
var existingSession = Get(ps => ps.GameId == gameId && ps.UserId == userId && ps.End == null).FirstOrDefault();
if (existingSession != null)
await Delete(existingSession);
var session = new PlaySession()
{
GameId = gameId,
UserId = userId,
Start = DateTime.UtcNow
};
await Add(session);
}
public async Task EndSession(Guid gameId, Guid userId)
{
var existingSession = Get(ps => ps.GameId == gameId && ps.UserId == userId && ps.End == null).FirstOrDefault();
if (existingSession != null)
{
existingSession.End = DateTime.UtcNow;
await Update(existingSession);
}
}
}
}

View File

@ -1 +1 @@
Copy-Item -Path "$InstallDir\<Source Path>" -Destination "$InstallDir\<Destination Path>" -Recurse
Copy-Item -Path "$InstallDirectory\<Source Path>" -Destination "$InstallDirectory\<Destination Path>" -Recurse

View File

@ -1 +1 @@
New-Item -ItemType Directory -Force -Path "$InstallDir\<Path>"
New-Item -ItemType Directory -Force -Path "$InstallDirectory\<Path>"

View File

@ -1,2 +1,2 @@
# Writes byte[] to a file at an offset
Patch-Binary -FilePath "$InstallDir\<File Path>" -Offset 0x00 -Data $bytes
Patch-Binary -FilePath "$InstallDirectory\<File Path>" -Offset 0x00 -Data $bytes

View File

@ -1 +1 @@
Remove-Item "$InstallDir\<DirectoryPath>" -Recurse -ErrorAction Ignore
Remove-Item "$InstallDirectory\<DirectoryPath>" -Recurse -ErrorAction Ignore

View File

@ -1 +1 @@
Rename-Item -Path "$InstallDir\<FilePath>" -NewName "$InstallDir\<NewName>"
Rename-Item -Path "$InstallDirectory\<FilePath>" -NewName "$InstallDirectory\<NewName>"

View File

@ -1,2 +0,0 @@
# Use regex to replace text within a file. Quotes are escaped by double quoting ("")
Write-ReplaceContentInFile -Regex '^game.setPlayerName "(.+)"' -Replacement "game.setPlayerName ""$NewName""" -FilePath "$InstallDir\<File Path>"

View File

@ -1,2 +0,0 @@
# Takes an input byte[] and separates it with 0x00 between each character
$bytes = Separate-AsciiBytes -Data $bytes

View File

@ -9,4 +9,4 @@
# WIN7RTM
# WIN8RTM
# See: https://ss64.com/nt/syntax-compatibility.html
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" -Name "$InstallDir\<Executable>" -Value "~ WINXPSP2 HIGHDPIAWARE" -Force
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" -Name "$InstallDirectory\<Executable>" -Value "~ WINXPSP2 HIGHDPIAWARE" -Force

View File

@ -1,2 +0,0 @@
# Convert an input string to ASCII-encoded byte[]. Shorter strings will pad out to 12 bytes, longer strings will be trimmed.
$bytes = Get-AsciiBytes -InputString "Hello world!" -MaxLength 12

View File

@ -1,4 +1,4 @@
# Trim a string down to a specified amount of characters
if ($NewName.Length -gt 10) {
$NewName = $NewName.Substring(0, 10);
if ($NewPlayerAlias.Length -gt 10) {
$NewPlayerAlias = $NewPlayerAlias.Substring(0, 10);
}

View File

@ -1,2 +1,2 @@
# Write contents of a string to a file
Set-Content "$InstallDir\<File Path>" "Hello world!"
Set-Content "$InstallDirectory\<File Path>" "Hello world!"

View File

@ -0,0 +1,2 @@
# Bounds accessible via $Resolution.Height, $Resolution.Width
$Resolution = Convert-AspectRatio -Width 1280 -Height 800 -AspectRatio (4 / 3)

View File

@ -0,0 +1,2 @@
# Converts a string to a UTF16-encoded byte array. This looks like ASCII characters separated by 0x00 in most cases.
$bytes = ConvertTo-StringBytes -Input "Hello World!" -Utf16 1 -MaxLength 12

View File

@ -0,0 +1 @@
Edit-PatchBinary -FilePath "$InstallDirectory\<File Path>" -Offset 0x00 -Data $bytes

View File

@ -1,14 +0,0 @@
function Get-43Resolution([int]$Width, [int]$Height) {
$ratio = 4 / 3
if (($Width -gt $Height) -or ($Width -eq $Height)) {
return @{ Width = [math]::Round($ratio * $Height); Height = $Height }
}
if ($Width -lt $Height) {
return @{ Width = $Width; Height = [math]::Round($Width / $ratio) }
}
}
# Accessible via $Resolution.Height, $Resolution.Width
$Resolution = Get-43Resolution -Width 1280 -Height 800

View File

@ -1,30 +0,0 @@
function Get-AsciiBytes([string]$InputString, [int]$MaxLength)
{
if ($InputString.Length -gt $MaxLength)
{
$InputString = $InputString.Substring(0, $MaxLength)
}
$bytes = [System.Text.Encoding]::ASCII.GetBytes($InputString)
$array = @()
$count = 0
$extraPadding = $MaxLength - $bytes.Length
foreach ($byte in $bytes)
{
if ($count -lt $MaxLength)
{
$array += $byte
$count++
}
}
# Pad the end with 0x00 to meet our max length
for ($i = $count; $i -lt $MaxLength; $i++)
{
$array += 0x00
}
return $array
}

View File

@ -0,0 +1 @@
$manifest = Get-GameManifest "C:\Games\<Game Directory>"

Some files were not shown because too many files have changed in this diff Show More