From bdccad6960ad5ebd801ba646cddf645f26c004eb Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Sun, 19 May 2024 11:49:05 +0200 Subject: [PATCH] Remove debug force launch --- Launcher.sln | 6 + Launcher/Program.cs | 1 - LauncherGamePlugin/Utils.cs | 9 + RemoteDownloaderPlugin/Game/GameDownload.cs | 165 +++++++++++++++++ RemoteDownloaderPlugin/Game/InstalledGame.cs | 171 ++++++++++++++++++ RemoteDownloaderPlugin/Game/OnlineGame.cs | 112 ++++++++++++ .../Game/PcLaunchDetails.cs | 16 ++ .../Gui/AddOrEditEmuProfileGui.cs | 118 ++++++++++++ .../Gui/SettingsRemoteIndexGui.cs | 60 ++++++ RemoteDownloaderPlugin/Plugin.cs | 145 +++++++++++++++ RemoteDownloaderPlugin/Remote.cs | 66 +++++++ .../RemoteDownloaderPlugin.csproj | 17 ++ RemoteDownloaderPlugin/Store.cs | 54 ++++++ .../Utils/HttpClientExtensions.cs | 53 ++++++ 14 files changed, 992 insertions(+), 1 deletion(-) create mode 100644 RemoteDownloaderPlugin/Game/GameDownload.cs create mode 100644 RemoteDownloaderPlugin/Game/InstalledGame.cs create mode 100644 RemoteDownloaderPlugin/Game/OnlineGame.cs create mode 100644 RemoteDownloaderPlugin/Game/PcLaunchDetails.cs create mode 100644 RemoteDownloaderPlugin/Gui/AddOrEditEmuProfileGui.cs create mode 100644 RemoteDownloaderPlugin/Gui/SettingsRemoteIndexGui.cs create mode 100644 RemoteDownloaderPlugin/Plugin.cs create mode 100644 RemoteDownloaderPlugin/Remote.cs create mode 100644 RemoteDownloaderPlugin/RemoteDownloaderPlugin.csproj create mode 100644 RemoteDownloaderPlugin/Store.cs create mode 100644 RemoteDownloaderPlugin/Utils/HttpClientExtensions.cs diff --git a/Launcher.sln b/Launcher.sln index f988434..b8b87a4 100644 --- a/Launcher.sln +++ b/Launcher.sln @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamGridDbMiddleware", "St EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HideGamesMiddleware", "HideGamesMiddleware\HideGamesMiddleware.csproj", "{35AB17CA-C0D8-4112-B78D-0DCC9DB70435}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteDownloaderPlugin", "RemoteDownloaderPlugin\RemoteDownloaderPlugin.csproj", "{068D7641-F9EA-40D3-B313-3FBCD09E71F5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,6 +77,10 @@ Global {35AB17CA-C0D8-4112-B78D-0DCC9DB70435}.Debug|Any CPU.Build.0 = Debug|Any CPU {35AB17CA-C0D8-4112-B78D-0DCC9DB70435}.Release|Any CPU.ActiveCfg = Release|Any CPU {35AB17CA-C0D8-4112-B78D-0DCC9DB70435}.Release|Any CPU.Build.0 = Release|Any CPU + {068D7641-F9EA-40D3-B313-3FBCD09E71F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {068D7641-F9EA-40D3-B313-3FBCD09E71F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {068D7641-F9EA-40D3-B313-3FBCD09E71F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {068D7641-F9EA-40D3-B313-3FBCD09E71F5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Launcher/Program.cs b/Launcher/Program.cs index b298577..08ad818 100644 --- a/Launcher/Program.cs +++ b/Launcher/Program.cs @@ -21,7 +21,6 @@ internal class Program [STAThread] public static void Main(string[] args) { - args = new[] { "epic-games", "Salt", "Launch" }; AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; if (args.Length <= 2) diff --git a/LauncherGamePlugin/Utils.cs b/LauncherGamePlugin/Utils.cs index 30d0707..f33027c 100644 --- a/LauncherGamePlugin/Utils.cs +++ b/LauncherGamePlugin/Utils.cs @@ -191,4 +191,13 @@ public static async Task HasNetworkAsync() return combinations.FirstOrDefault(File.Exists); } } + + public static Platform GuessPlatformBasedOnString(string? path) + { + if (path == null) + return Platform.Unknown; + + List windowsFileExt = [".exe", ".bat", ".msi", ".cmd"]; + return windowsFileExt.Any(path.EndsWith) ? Platform.Windows : Platform.Linux; + } } \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Game/GameDownload.cs b/RemoteDownloaderPlugin/Game/GameDownload.cs new file mode 100644 index 0000000..4b3132b --- /dev/null +++ b/RemoteDownloaderPlugin/Game/GameDownload.cs @@ -0,0 +1,165 @@ +using System.IO.Compression; +using LauncherGamePlugin; +using LauncherGamePlugin.Interfaces; +using RemoteDownloaderPlugin.Utils; + +namespace RemoteDownloaderPlugin.Game; + +public class GameDownload : ProgressStatus +{ + private IEntry _entry; + private int _lastSecond = 0; + private readonly CancellationTokenSource _cts = new(); + private bool _doneDownloading = false; + public long TotalSize { get; private set; } + public string Version { get; private set; } + public GameType Type { get; private set; } + public string BaseFileName { get; private set; } + + public GameDownload(IEntry entry) + { + _entry = entry; + } + + private void OnProgressUpdate(object? obj, float progress) + { + if (_doneDownloading || _lastSecond == DateTime.Now.Second) // Only make the UI respond once a second + return; + + _lastSecond = DateTime.Now.Second; + + progress *= 100; + Line1 = $"Downloading: {progress:0}%"; + Percentage = progress; + InvokeOnUpdate(); + } + + public async Task Download(IApp app) + { + _doneDownloading = false; + + if (_entry is EmuEntry emuEntry) + await DownloadEmu(app, emuEntry); + else if (_entry is PcEntry pcEntry) + await DownloadPc(app, pcEntry); + else + throw new Exception("Download failed: Unknown type"); + } + + private async Task DownloadEmu(IApp app, EmuEntry entry) + { + Type = GameType.Emu; + if (entry.Files.Count(x => x.Type == "base") != 1) + { + throw new Exception("Multiple base images, impossible download"); + } + + var basePath = Path.Join(app.GameDir, "Remote", entry.Emu); + string baseGamePath = null; + var extraFilesPath = Path.Join(app.GameDir, "Remote", entry.Emu, entry.GameId); + Directory.CreateDirectory(basePath); + Directory.CreateDirectory(extraFilesPath); + + using HttpClient client = new(); + + for (int i = 0; i < entry.Files.Count; i++) + { + Progress localProcess = new(); + localProcess.ProgressChanged += (sender, f) => + { + var part = (float)1 / entry.Files.Count; + var add = (float)i / entry.Files.Count; + OnProgressUpdate(null, part * f + add); + }; + + var fileEntry = entry.Files[i]; + var destPath = Path.Join(fileEntry.Type == "base" ? basePath : extraFilesPath, fileEntry.Name); + + if (fileEntry.Type == "base") + { + baseGamePath = destPath; + BaseFileName = fileEntry.Name; + } + + var fs = new FileStream(destPath, FileMode.Create); + + try + { + await client.DownloadAsync(fileEntry.Url, fs, localProcess, _cts.Token); + } + catch (TaskCanceledException e) + { + await Task.Run(() => fs.Dispose()); + + if (baseGamePath != null && File.Exists(baseGamePath)) + { + File.Delete(baseGamePath); + } + + Directory.Delete(extraFilesPath, true); + + throw; + } + + Line1 = "Saving file..."; + InvokeOnUpdate(); + await Task.Run(() => fs.Dispose()); + } + + TotalSize = (await Task.Run(() => LauncherGamePlugin.Utils.DirSize(new(extraFilesPath)))) + (new FileInfo(baseGamePath!)).Length; + Version = entry.Files.Last(x => x.Type is "base" or "update").Version; + } + + private async Task DownloadPc(IApp app, PcEntry entry) + { + Type = GameType.Pc; + var basePath = Path.Join(app.GameDir, "Remote", "Pc", entry.GameId); + Directory.CreateDirectory(basePath); + var zipFilePath = Path.Join(basePath, "__game__.zip"); + + using HttpClient client = new(); + var fs = new FileStream(zipFilePath, FileMode.Create); + + Progress progress = new(); + progress.ProgressChanged += OnProgressUpdate; + + try + { + await client.DownloadAsync(entry.Url, fs, progress, _cts.Token); + } + catch (TaskCanceledException e) + { + await Task.Run(() => fs.Dispose()); + + Directory.Delete(basePath); + + throw; + } + + + Percentage = 100; + Line1 = "Saving..."; + InvokeOnUpdate(); + await Task.Run(() => fs.Dispose()); + Line1 = "Unzipping..."; + InvokeOnUpdate(); + await Task.Run(() => ZipFile.ExtractToDirectory(zipFilePath, basePath)); + File.Delete(zipFilePath); + + if (_cts.IsCancellationRequested) + { + Directory.Delete(basePath); + } + + TotalSize = await Task.Run(() => LauncherGamePlugin.Utils.DirSize(new(basePath))); + Version = entry.Version; + } + + public void Stop() + { + if (_doneDownloading) + return; + + _cts.Cancel(); + } +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Game/InstalledGame.cs b/RemoteDownloaderPlugin/Game/InstalledGame.cs new file mode 100644 index 0000000..4fe6a02 --- /dev/null +++ b/RemoteDownloaderPlugin/Game/InstalledGame.cs @@ -0,0 +1,171 @@ +using LauncherGamePlugin; +using LauncherGamePlugin.Enums; +using LauncherGamePlugin.Forms; +using LauncherGamePlugin.Interfaces; +using LauncherGamePlugin.Launcher; + +namespace RemoteDownloaderPlugin.Game; + +public class InstalledGame : IGame +{ + public string InternalName => Game.Id; + public string Name => Game.Name; + public bool IsRunning { get; set; } = false; + public IGameSource Source => _plugin; + public long? Size => Game.GameSize; + public bool HasImage(ImageType type) + => ImageTypeToUri(type) != null; + + public Task GetImage(ImageType type) + { + Uri? url = ImageTypeToUri(type); + + if (url == null) + return Task.FromResult(null); + + return Storage.Cache($"{Game.Id}_{type}", () => Storage.ImageDownload(url)); + } + + public InstalledStatus InstalledStatus => InstalledStatus.Installed; + + public Platform EstimatedGamePlatform => (_type == GameType.Emu) + ? LauncherGamePlugin.Utils.GuessPlatformBasedOnString(_plugin.Storage.Data.EmuProfiles.FirstOrDefault(x => x.Platform == _emuGame!.Emu)?.ExecPath) + : LauncherGamePlugin.Utils.GuessPlatformBasedOnString(_pcLaunchDetails!.LaunchExec); + + public ProgressStatus? ProgressStatus => null; + public event Action? OnUpdate; + + public void InvokeOnUpdate() + => OnUpdate?.Invoke(); + + public IInstalledGame Game { get; } + private Plugin _plugin; + private PcLaunchDetails? _pcLaunchDetails; + private InstalledEmuGame? _emuGame; + private GameType _type; + + public InstalledGame(IInstalledGame game, Plugin plugin) + { + Game = game; + _plugin = plugin; + _type = game is InstalledEmuGame ? GameType.Emu : GameType.Pc; + _pcLaunchDetails = null; + + if (_type == GameType.Pc) + { + var fullPath = Path.Join(plugin.App.GameDir, "Remote", "Pc", Game.Id, "game.json"); + _pcLaunchDetails = PcLaunchDetails.GetFromPath(fullPath); + } + else + { + _emuGame = (game as InstalledEmuGame)!; + } + } + + public void Play() + { + try + { + if (_type == GameType.Emu) + { + var emuProfile = _plugin.Storage.Data.EmuProfiles.FirstOrDefault(x => x.Platform == _emuGame!.Emu); + + if (emuProfile == null) + { + throw new Exception($"No '{_emuGame!.Emu}' emulation profile exists"); + } + + var baseGamePath = Path.Join(_plugin.App.GameDir, "Remote", _emuGame!.Emu, _emuGame.BaseFilename); + + LaunchParams args = new(emuProfile.ExecPath, + emuProfile.CliArgs.Replace("{EXEC}", $"\"{baseGamePath}\""), emuProfile.WorkingDirectory, this, + EstimatedGamePlatform); + _plugin.App.Launch(args); + } + else + { + var execPath = Path.Join(_plugin.App.GameDir, "Remote", "Pc", Game.Id, _pcLaunchDetails!.LaunchExec); + var workingDir = Path.Join(_plugin.App.GameDir, "Remote", "Pc", Game.Id, _pcLaunchDetails!.WorkingDir); + LaunchParams args = new(execPath, _pcLaunchDetails.LaunchArgs, Path.GetDirectoryName(workingDir)!, this, + EstimatedGamePlatform); + _plugin.App.Launch(args); + } + } + catch (Exception e) + { + _plugin.App.ShowDismissibleTextPrompt($"Game Failed to launch: {e.Message}"); + } + } + + public void Delete() + { + if (_type == GameType.Emu) + { + var baseGamePath = Path.Join(_plugin.App.GameDir, "Remote", _emuGame!.Emu, _emuGame.BaseFilename); + var extraDir = Path.Join(_plugin.App.GameDir, "Remote", _emuGame!.Emu, Game.Id); + var success = false; + + try + { + File.Delete(baseGamePath); + Directory.Delete(extraDir, true); + success = true; + } + catch {} + + var game = _plugin.Storage.Data.EmuGames.Find(x => x.Id == Game.Id); + _plugin.Storage.Data.EmuGames.Remove(game!); + + if (!success) + { + _plugin.App.ShowTextPrompt("Failed to delete files. Game has been unlinked"); + } + } + else + { + var path = Path.Join(_plugin.App.GameDir, "Remote", "Pc", Game.Id); + var success = false; + + try + { + Directory.Delete(path, true); + success = true; + } + catch {} + + var game = _plugin.Storage.Data.PcGames.Find(x => x.Id == Game.Id); + _plugin.Storage.Data.PcGames.Remove(game!); + + if (!success) + { + _plugin.App.ShowTextPrompt("Failed to delete files. Game has been unlinked"); + } + } + + _plugin.App.ReloadGames(); + _plugin.Storage.Save(); + } + + public void OpenInFileManager() + { + if (_type == GameType.Emu) + { + LauncherGamePlugin.Utils.OpenFolderWithHighlightedFile(Path.Join(_plugin.App.GameDir, "Remote", _emuGame!.Emu, _emuGame.BaseFilename)); + } + else + { + LauncherGamePlugin.Utils.OpenFolder(Path.Join(_plugin.App.GameDir, "Remote", "Pc", Game.Id)); + } + } + + private Uri? ImageTypeToUri(ImageType type) + => type switch + { + ImageType.Background => Game.Images.Background, + ImageType.VerticalCover => Game.Images.VerticalCover, + ImageType.HorizontalCover => Game.Images.HorizontalCover, + ImageType.Logo => Game.Images.Logo, + ImageType.Icon => Game.Images.Icon, + _ => null + }; +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Game/OnlineGame.cs b/RemoteDownloaderPlugin/Game/OnlineGame.cs new file mode 100644 index 0000000..a7d082d --- /dev/null +++ b/RemoteDownloaderPlugin/Game/OnlineGame.cs @@ -0,0 +1,112 @@ +using LauncherGamePlugin; +using LauncherGamePlugin.Enums; +using LauncherGamePlugin.Interfaces; + +namespace RemoteDownloaderPlugin.Game; + +public class OnlineGame : IGame +{ + public string InternalName => Entry.GameId; + public string Name => $"{Entry.GameName} ({Platform})"; + public bool IsRunning { get; set; } = false; + public IGameSource Source => _plugin; + public long? Size => Entry.GameSize; + public string Platform { get; private set; } + + public bool HasImage(ImageType type) + => ImageTypeToUri(type) != null; + + public Task GetImage(ImageType type) + { + Uri? url = ImageTypeToUri(type); + + if (url == null) + return Task.FromResult(null); + + return Storage.Cache($"{Entry.GameId}_{type}", () => Storage.ImageDownload(url)); + } + + public InstalledStatus InstalledStatus => InstalledStatus.NotInstalled; + public Platform EstimatedGamePlatform => LauncherGamePlugin.Enums.Platform.Unknown; + public ProgressStatus? ProgressStatus => _download; + public event Action? OnUpdate; + public void InvokeOnUpdate() => OnUpdate?.Invoke(); + + public IEntry Entry { get; } + private Plugin _plugin; + private GameDownload? _download = null; + + public OnlineGame(IEntry entry, Plugin plugin) + { + Entry = entry; + _plugin = plugin; + Platform = (entry is EmuEntry emuEntry) + ? emuEntry.Emu + : "Pc"; + } + + public async Task Download() + { + _download = new GameDownload(Entry); + OnUpdate?.Invoke(); + + try + { + await _download.Download(_plugin.App); + } + catch + { + _download = null; + OnUpdate?.Invoke(); + return; + } + + if (_download.Type == GameType.Emu) + { + var entry = Entry as EmuEntry; + _plugin.Storage.Data.EmuGames.Add(new() + { + Id = Entry.GameId, + Name = Entry.GameName, + Emu = entry!.Emu, + GameSize = _download.TotalSize, + Version = _download.Version, + BaseFilename = _download.BaseFileName, + Images = Entry.Img + }); + } + else + { + _plugin.Storage.Data.PcGames.Add(new() + { + Id = Entry.GameId, + Name = Entry.GameName, + GameSize = _download.TotalSize, + Version = _download.Version, + Images = Entry.Img + }); + } + + _plugin.Storage.Save(); + _plugin.App.ReloadGames(); + _download = null; + OnUpdate?.Invoke(); + } + + public void Stop() + { + _download?.Stop(); + _download = null; + } + + private Uri? ImageTypeToUri(ImageType type) + => type switch + { + ImageType.Background => Entry.Img.Background, + ImageType.VerticalCover => Entry.Img.VerticalCover, + ImageType.HorizontalCover => Entry.Img.HorizontalCover, + ImageType.Logo => Entry.Img.Logo, + ImageType.Icon => Entry.Img.Icon, + _ => null + }; +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Game/PcLaunchDetails.cs b/RemoteDownloaderPlugin/Game/PcLaunchDetails.cs new file mode 100644 index 0000000..0d482f8 --- /dev/null +++ b/RemoteDownloaderPlugin/Game/PcLaunchDetails.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace RemoteDownloaderPlugin.Game; + +public class PcLaunchDetails +{ + [JsonProperty("launch_exec")] + public string LaunchExec { get; set; } + [JsonProperty("working_dir")] + public string WorkingDir { get; set; } + [JsonProperty("launch_args")] + public List LaunchArgs { get; set; } + + public static PcLaunchDetails GetFromPath(string path) + => JsonConvert.DeserializeObject(File.ReadAllText(path))!; +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Gui/AddOrEditEmuProfileGui.cs b/RemoteDownloaderPlugin/Gui/AddOrEditEmuProfileGui.cs new file mode 100644 index 0000000..6ef8893 --- /dev/null +++ b/RemoteDownloaderPlugin/Gui/AddOrEditEmuProfileGui.cs @@ -0,0 +1,118 @@ +using LauncherGamePlugin.Forms; +using LauncherGamePlugin.Interfaces; + +namespace RemoteDownloaderPlugin.Gui; + +public class AddOrEditEmuProfileGui +{ + private IApp _app; + private Plugin _instance; + private string _platform; + private string _path; + private string _args; + private string _workDir; + private readonly bool _addOrUpdate; + private string _error; + + public AddOrEditEmuProfileGui(IApp app, Plugin instance) + { + _app = app; + _instance = instance; + + _platform = ""; + _path = ""; + _args = ""; + _workDir = ""; + _addOrUpdate = false; + _error = ""; + } + + public AddOrEditEmuProfileGui(IApp app, Plugin instance, EmuProfile profile) + : this(app, instance) + { + _platform = profile.Platform; + _path = profile.ExecPath; + _args = profile.CliArgs; + _workDir = profile.WorkingDirectory; + _addOrUpdate = true; + } + + public void ShowGui() + { + List formEntries = new() + { + Form.TextBox(_addOrUpdate ? "Add new emulation platform" : "Edit emulation platform", FormAlignment.Left, "Bold"), + Form.TextInput("Platform:", _platform), + Form.FilePicker("Executable Path:", _path), + Form.TextInput("CLI Args:", _args), + Form.FolderPicker("Working Directory:", _workDir), + Form.Button("Cancel", _ => _app.HideForm(), "Save", Process) + }; + + if (!string.IsNullOrWhiteSpace(_error)) + formEntries.Add(Form.TextBox(_error, fontWeight: "Bold")); + + _app.ShowForm(formEntries); + } + + public void Process(Form form) + { + _platform = form.GetValue("Platform:"); + _path = form.GetValue("Executable Path:"); + _args = form.GetValue("CLI Args:"); + _workDir = form.GetValue("Working Directory:"); + + if (string.IsNullOrWhiteSpace(_platform) || + string.IsNullOrWhiteSpace(_path) || + string.IsNullOrWhiteSpace(_args) || + string.IsNullOrWhiteSpace(_workDir)) + { + _error = "Not all fields are filled"; + ShowGui(); + return; + } + + if (!File.Exists(_path)) + { + _error = "Executable path does not exist"; + ShowGui(); + return; + } + + if (!Directory.Exists(_workDir)) + { + _error = "Working directory does not exist"; + ShowGui(); + return; + } + + if (!_args.Contains("{EXEC}")) + { + _error = "Args does not specify an {EXEC} param"; + ShowGui(); + return; + } + + EmuProfile? existingProfile = _instance.Storage.Data.EmuProfiles.FirstOrDefault(x => x.Platform == _platform); + if (existingProfile == null) + { + _instance.Storage.Data.EmuProfiles.Add(new() + { + Platform = _platform, + ExecPath = _path, + CliArgs = _args, + WorkingDirectory = _workDir + }); + } + else + { + existingProfile.CliArgs = _args; + existingProfile.ExecPath = _path; + existingProfile.WorkingDirectory = _workDir; + } + + _instance.Storage.Save(); + _app.HideForm(); + _app.ReloadGlobalCommands(); + } +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Gui/SettingsRemoteIndexGui.cs b/RemoteDownloaderPlugin/Gui/SettingsRemoteIndexGui.cs new file mode 100644 index 0000000..2b682bb --- /dev/null +++ b/RemoteDownloaderPlugin/Gui/SettingsRemoteIndexGui.cs @@ -0,0 +1,60 @@ +using LauncherGamePlugin.Forms; +using LauncherGamePlugin.Interfaces; + +namespace RemoteDownloaderPlugin.Gui; + +public class SettingsRemoteIndexGui +{ + private IApp _app; + private Plugin _instance; + + public SettingsRemoteIndexGui(IApp app, Plugin instance) + { + _app = app; + _instance = instance; + } + + public void ShowGui(string possibleError = "") + { + List formEntries = new() + { + Form.TextBox("Enter remote index URL", FormAlignment.Left, "Bold"), + Form.TextInput("Index URL:", _instance.Storage.Data.IndexUrl), + Form.Button("Cancel", _ => _app.HideForm(), "Save", Process) + }; + + if (!string.IsNullOrWhiteSpace(possibleError)) + formEntries.Add(Form.TextBox(possibleError, fontWeight: "Bold")); + + _app.ShowForm(formEntries); + } + + private async void Process(Form form) + { + _app.ShowTextPrompt("Loading"); + var newUrl = form.GetValue("Index URL:"); + var origIndexUrl = _instance.Storage.Data.IndexUrl; + + if (string.IsNullOrWhiteSpace(newUrl)) + { + ShowGui("Index URL is empty"); + return; + } + + if (newUrl != origIndexUrl) + { + _instance.Storage.Data.IndexUrl = newUrl; + if (!await _instance.FetchRemote()) + { + _instance.Storage.Data.IndexUrl = origIndexUrl; + ShowGui("Failed to fetch data from given URL"); + return; + } + + _instance.Storage.Save(); + } + + _app.HideForm(); + _app.ReloadGames(); + } +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Plugin.cs b/RemoteDownloaderPlugin/Plugin.cs new file mode 100644 index 0000000..b48bb53 --- /dev/null +++ b/RemoteDownloaderPlugin/Plugin.cs @@ -0,0 +1,145 @@ +using LauncherGamePlugin; +using LauncherGamePlugin.Commands; +using LauncherGamePlugin.Enums; +using LauncherGamePlugin.Forms; +using LauncherGamePlugin.Interfaces; +using Newtonsoft.Json; +using RemoteDownloaderPlugin.Game; +using RemoteDownloaderPlugin.Gui; + +namespace RemoteDownloaderPlugin; + +public class Plugin : IGameSource +{ + public string ServiceName => "Remote Games Integration"; + public string Version => "v1.0.0"; + public string SlugServiceName => "remote-games"; + public string ShortServiceName => "Remote"; + public PluginType Type => PluginType.GameSource; + + public Storage Storage { get; private set; } + public IApp App { get; private set; } + + private Remote _cachedRemote = new(); + private List _onlineGames = new(); + + public async Task Initialize(IApp app) + { + App = app; + Storage = new(app, "remote_games.json"); + await FetchRemote(); + + return null; + } + + public List GetGameCommands(IGame game) + { + if (game is OnlineGame onlineGame) + { + if (onlineGame.ProgressStatus != null) + { + return new() + { + new Command("Stop", () => onlineGame.Stop()) + }; + } + else + { + return new() + { + new Command("Install", () => onlineGame.Download()) + }; + } + } + + if (game is InstalledGame installedGame) + { + return new() + { + new Command(game.IsRunning ? "Running" : "Launch", installedGame.Play), + new Command("Open in File Manager", installedGame.OpenInFileManager), + new Command($"Version: {installedGame.Game.Version}"), + new Command("Uninstall", () => + App.Show2ButtonTextPrompt($"Do you want to uninstall {game.Name}?", "Yes", "No", _ => + { + installedGame.Delete(); + App.HideForm(); + }, _ => App.HideForm(), game)) + }; + } + + throw new NotImplementedException(); + } + + public async Task> GetGames() + { + List installedGames = Storage.Data.EmuGames.Select(x => new InstalledGame(x, this)) + .Concat(Storage.Data.PcGames.Select(x => new InstalledGame(x, this))).ToList(); + + List installedIds = installedGames.Select(x => x.Game.Id).ToList(); + + return _onlineGames.Where(x => !installedIds.Contains(x.Entry.GameId)) + .Select(x => (IGame)x) + .Concat(installedGames.Select(x => (IGame)x)) + .ToList(); + } + + public async Task FetchRemote() + { + if (string.IsNullOrEmpty(Storage.Data.IndexUrl)) + return false; + + try + { + using HttpClient client = new(); + var data = await client.GetStringAsync(Storage.Data.IndexUrl); + _cachedRemote = JsonConvert.DeserializeObject(data)!; + + _onlineGames = _cachedRemote.Emu.Select(x => new OnlineGame(x, this)) + .Concat(_cachedRemote.Pc.Select(x => new OnlineGame(x, this))).ToList(); + + return true; + } + catch + { + return false; + } + } + + public List GetGlobalCommands() + { + List commands = new() + { + new Command("Reload", Reload), + new Command(), + new Command("Edit Index URL", () => new SettingsRemoteIndexGui(App, this).ShowGui()), + new Command("Add Emulation Profile", () => new AddOrEditEmuProfileGui(App, this).ShowGui()), + new Command("Edit Emulation Profile", + Storage.Data.EmuProfiles.Select( + x => new Command($"Edit {x.Platform}", new AddOrEditEmuProfileGui(App, this, x).ShowGui)).ToList()), + new Command("Delete Emulation Profile", + Storage.Data.EmuProfiles.Select( + x => new Command($"Delete {x.Platform}", () => + { + App.Show2ButtonTextPrompt($"Do you want to remove platform {x.Platform}?", "Yes", "No", + _ => + { + Storage.Data.EmuProfiles.Remove(x); + Storage.Save(); + App.ReloadGlobalCommands(); + App.HideForm(); + }, _ => App.HideForm()); + })).ToList()) + }; + + return commands; + } + + private async void Reload() + { + App.ShowTextPrompt("Reloading Remote Games..."); + await FetchRemote(); + App.ReloadGames(); + App.HideForm(); + } +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Remote.cs b/RemoteDownloaderPlugin/Remote.cs new file mode 100644 index 0000000..98934c7 --- /dev/null +++ b/RemoteDownloaderPlugin/Remote.cs @@ -0,0 +1,66 @@ +using Newtonsoft.Json; + +namespace RemoteDownloaderPlugin; + +public class Images +{ + public Uri Background { get; set; } + public Uri HorizontalCover { get; set; } + public Uri Icon { get; set; } + public Uri Logo { get; set; } + public Uri VerticalCover { get; set; } +} + +public class EmuFileEntry +{ + [JsonProperty("download_size")] + public long DownloadSize { get; set; } + public string Ext { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public Uri Url { get; set; } + public string Version { get; set; } +} + +public interface IEntry +{ + public string GameId { get; } + public string GameName { get; } + public Images Img { get; } + public long GameSize { get; } +} + +public class EmuEntry : IEntry +{ + [JsonProperty("game_id")] + public string GameId { get; set; } + [JsonProperty("game_name")] + public string GameName { get; set; } + public List Files { get; set; } + public Images Img { get; set; } + public string Emu { get; set; } + + [System.Text.Json.Serialization.JsonIgnore] + public long GameSize => Files.FirstOrDefault(x => x.Type == "base")?.DownloadSize ?? 0; +} + +public class PcEntry : IEntry +{ + [JsonProperty("game_id")] + public string GameId { get; set; } + [JsonProperty("game_name")] + public string GameName { get; set; } + [JsonProperty("download_size")] + public long DownloadSize { get; set; } + [JsonProperty("game_size")] + public long GameSize { get; set; } + public Uri Url { get; set; } + public string Version { get; set; } + public Images Img { get; set; } +} + +public class Remote +{ + public List Pc { get; set; } = new(); + public List Emu { get; set; } = new(); +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/RemoteDownloaderPlugin.csproj b/RemoteDownloaderPlugin/RemoteDownloaderPlugin.csproj new file mode 100644 index 0000000..cc146bd --- /dev/null +++ b/RemoteDownloaderPlugin/RemoteDownloaderPlugin.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + true + + + + + false + runtime + + + + diff --git a/RemoteDownloaderPlugin/Store.cs b/RemoteDownloaderPlugin/Store.cs new file mode 100644 index 0000000..8c1df66 --- /dev/null +++ b/RemoteDownloaderPlugin/Store.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace RemoteDownloaderPlugin; + +public enum GameType +{ + Pc, + Emu, +} + +public interface IInstalledGame +{ + public string Id { get; } + public string Name { get; } + public string Version { get; } + public long GameSize { get; } + public Images Images { get; } +} + +public class InstalledEmuGame : IInstalledGame +{ + public string Id { get; set; } + public string Name { get; set; } + public string Emu { get; set; } + public long GameSize { get; set; } + public string Version { get; set; } + public string BaseFilename { get; set; } + public Images Images { get; set; } +} + +public class InstalledPcGame : IInstalledGame +{ + public string Id { get; set; } + public string Name { get; set; } + public long GameSize { get; set; } + public string Version { get; set; } + public Images Images { get; set; } +} + +public class EmuProfile +{ + public string Platform { get; set; } + public string ExecPath { get; set; } + public string WorkingDirectory { get; set; } = ""; + public string CliArgs { get; set; } = ""; +} + +public class Store +{ + public List EmuGames { get; set; } = new(); + public List PcGames { get; set; } = new(); + public List EmuProfiles { get; set; } = new(); + public string IndexUrl { get; set; } = ""; +} \ No newline at end of file diff --git a/RemoteDownloaderPlugin/Utils/HttpClientExtensions.cs b/RemoteDownloaderPlugin/Utils/HttpClientExtensions.cs new file mode 100644 index 0000000..de1775b --- /dev/null +++ b/RemoteDownloaderPlugin/Utils/HttpClientExtensions.cs @@ -0,0 +1,53 @@ +namespace RemoteDownloaderPlugin.Utils; + +// Thanks stackoverflow https://stackoverflow.com/questions/20661652/progress-bar-with-httpclient +public static class HttpClientExtensions +{ + public static async Task DownloadAsync(this HttpClient client, Uri requestUri, Stream destination, IProgress progress = null, CancellationToken cancellationToken = default) { + // Get the http headers first to examine the content length + using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead)) { + var contentLength = response.Content.Headers.ContentLength; + + using (var download = await response.Content.ReadAsStreamAsync()) { + + // Ignore progress reporting when no progress reporter was + // passed or when the content length is unknown + if (progress == null || !contentLength.HasValue) { + await download.CopyToAsync(destination); + return; + } + + // Convert absolute progress (bytes downloaded) into relative progress (0% - 100%) + var relativeProgress = new Progress(totalBytes => progress.Report((float)totalBytes / contentLength.Value)); + // Use extension method to report progress while downloading + await download.CopyToAsync(destination, 0x800000, relativeProgress, cancellationToken); + progress.Report(1); + } + } + } +} + +public static class StreamExtensions +{ + public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress progress = null, CancellationToken cancellationToken = default) { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + throw new ArgumentException("Has to be readable", nameof(source)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new ArgumentException("Has to be writable", nameof(destination)); + if (bufferSize < 0) + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + + var buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report(totalBytesRead); + } + } +} \ No newline at end of file