From 593f396c548ebdc07fec1e91a7626293febf9692 Mon Sep 17 00:00:00 2001 From: Frog Date: Wed, 27 May 2026 01:20:03 -0700 Subject: [PATCH] Add Test Game Generator for Steam and Epic - Added Test Game Generator to the Debug window for creating fake installed game entries for testing - Updated ThemeManager to properly theme ListBox controls in dark mode - Labels now use transparent backgrounds for better appearance --- CreamInstaller/Forms/DebugForm.Designer.cs | 23 +- CreamInstaller/Forms/DebugForm.cs | 6 + CreamInstaller/Forms/TestGameForm.Designer.cs | 215 +++++++++++ CreamInstaller/Forms/TestGameForm.cs | 362 ++++++++++++++++++ CreamInstaller/Forms/TestGameForm.resx | 120 ++++++ CreamInstaller/Platforms/Epic/EpicLibrary.cs | 7 + CreamInstaller/Platforms/Epic/EpicStore.cs | 47 +++ .../Platforms/Steam/SteamLibrary.cs | 8 + CreamInstaller/Utility/ThemeManager.cs | 16 +- 9 files changed, 798 insertions(+), 6 deletions(-) create mode 100644 CreamInstaller/Forms/TestGameForm.Designer.cs create mode 100644 CreamInstaller/Forms/TestGameForm.cs create mode 100644 CreamInstaller/Forms/TestGameForm.resx diff --git a/CreamInstaller/Forms/DebugForm.Designer.cs b/CreamInstaller/Forms/DebugForm.Designer.cs index 871537c..23c6e30 100644 --- a/CreamInstaller/Forms/DebugForm.Designer.cs +++ b/CreamInstaller/Forms/DebugForm.Designer.cs @@ -32,16 +32,31 @@ partial class DebugForm private void InitializeComponent() { debugTextBox = new RichTextBox(); + testGameButton = new Button(); SuspendLayout(); // + // testGameButton + // + testGameButton.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + testGameButton.AutoSize = true; + testGameButton.AutoSizeMode = AutoSizeMode.GrowAndShrink; + testGameButton.Location = new System.Drawing.Point(10, 10); + testGameButton.Name = "testGameButton"; + testGameButton.Padding = new Padding(3, 0, 3, 0); + testGameButton.Size = new System.Drawing.Size(540, 25); + testGameButton.TabIndex = 1; + testGameButton.Text = "Test Game"; + testGameButton.UseVisualStyleBackColor = true; + testGameButton.Click += OnTestGame; + // // debugTextBox // - debugTextBox.Dock = DockStyle.Fill; - debugTextBox.Location = new System.Drawing.Point(10, 10); + debugTextBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + debugTextBox.Location = new System.Drawing.Point(10, 41); debugTextBox.Name = "debugTextBox"; debugTextBox.ReadOnly = true; debugTextBox.ScrollBars = RichTextBoxScrollBars.ForcedBoth; - debugTextBox.Size = new System.Drawing.Size(540, 317); + debugTextBox.Size = new System.Drawing.Size(540, 286); debugTextBox.TabIndex = 0; debugTextBox.TabStop = false; debugTextBox.Text = ""; @@ -52,6 +67,7 @@ partial class DebugForm AutoScaleMode = AutoScaleMode.Font; ClientSize = new System.Drawing.Size(560, 337); ControlBox = false; + Controls.Add(testGameButton); Controls.Add(debugTextBox); FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; @@ -68,4 +84,5 @@ partial class DebugForm #endregion private RichTextBox debugTextBox; + private Button testGameButton; } \ No newline at end of file diff --git a/CreamInstaller/Forms/DebugForm.cs b/CreamInstaller/Forms/DebugForm.cs index 864c2dc..b21feb0 100644 --- a/CreamInstaller/Forms/DebugForm.cs +++ b/CreamInstaller/Forms/DebugForm.cs @@ -81,4 +81,10 @@ internal sealed partial class DebugForm : CustomForm debugTextBox.AppendText(text, color, true); }); } + + private void OnTestGame(object sender, EventArgs e) + { + using TestGameForm form = new(this); + _ = form.ShowDialog(this); + } } \ No newline at end of file diff --git a/CreamInstaller/Forms/TestGameForm.Designer.cs b/CreamInstaller/Forms/TestGameForm.Designer.cs new file mode 100644 index 0000000..5b9e6fa --- /dev/null +++ b/CreamInstaller/Forms/TestGameForm.Designer.cs @@ -0,0 +1,215 @@ +using System.ComponentModel; +using System.Windows.Forms; + +namespace CreamInstaller.Forms; + +partial class TestGameForm +{ + private IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + components.Dispose(); + base.Dispose(disposing); + } + + // All coordinates are based on ClientSize = 560 x 330 + // Left margin = 12, right edge of usable area = 548 (560 - 12) + // Usable width = 536 + + private void InitializeComponent() + { + platformGroupBox = new GroupBox(); + steamRadioButton = new RadioButton(); + epicRadioButton = new RadioButton(); + appIdLabel = new Label(); + appIdTextBox = new TextBox(); + gameNameLabel = new Label(); + gameNameTextBox = new TextBox(); + epicSearchButton = new Button(); + epicResultsListBox = new ListBox(); + dlcGroupBox = new GroupBox(); + dlcListBox = new ListBox(); + dlcIdLabel = new Label(); + dlcIdTextBox = new TextBox(); + dlcNameLabel = new Label(); + dlcNameTextBox = new TextBox(); + addDlcButton = new Button(); + removeDlcButton = new Button(); + generateButton = new Button(); + clearButton = new Button(); + closeButton = new Button(); + statusLabel = new Label(); + platformGroupBox.SuspendLayout(); + dlcGroupBox.SuspendLayout(); + SuspendLayout(); + + // ── Platform group box ── y=8, h=44 + platformGroupBox.Location = new System.Drawing.Point(12, 8); + platformGroupBox.Size = new System.Drawing.Size(536, 44); + platformGroupBox.TabStop = false; + platformGroupBox.Text = "Platform"; + platformGroupBox.Controls.Add(steamRadioButton); + platformGroupBox.Controls.Add(epicRadioButton); + + steamRadioButton.AutoSize = true; + steamRadioButton.Checked = true; + steamRadioButton.Location = new System.Drawing.Point(10, 17); + steamRadioButton.TabStop = true; + steamRadioButton.Text = "Steam"; + steamRadioButton.CheckedChanged += OnPlatformChanged; + + epicRadioButton.AutoSize = true; + epicRadioButton.Location = new System.Drawing.Point(80, 17); + epicRadioButton.Text = "Epic"; + epicRadioButton.CheckedChanged += OnPlatformChanged; + + // ── App ID row ── y=62 + appIdLabel.AutoSize = true; + appIdLabel.Location = new System.Drawing.Point(12, 66); + appIdLabel.Text = "App ID:"; + + appIdTextBox.Location = new System.Drawing.Point(105, 63); + appIdTextBox.Size = new System.Drawing.Size(443, 23); + appIdTextBox.PlaceholderText = "e.g. 480"; + + // ── Game Name row ── y=96 + gameNameLabel.AutoSize = true; + gameNameLabel.Location = new System.Drawing.Point(12, 100); + gameNameLabel.Text = "Game Name:"; + + // Steam: full width; Epic: leaves room for Search button (75px + 4px gap) + gameNameTextBox.Location = new System.Drawing.Point(105, 97); + gameNameTextBox.Size = new System.Drawing.Size(443, 23); + + epicSearchButton.Location = new System.Drawing.Point(468, 97); + epicSearchButton.Size = new System.Drawing.Size(80, 23); + epicSearchButton.Text = "Search"; + epicSearchButton.Visible = false; + epicSearchButton.Click += OnEpicSearch; + + // ── Epic results list ── y=130, same slot as DLC group + epicResultsListBox.Location = new System.Drawing.Point(12, 130); + epicResultsListBox.Size = new System.Drawing.Size(536, 80); + epicResultsListBox.Visible = false; + epicResultsListBox.SelectedIndexChanged += OnEpicResultSelected; + + // ── DLC group box ── y=130, h=130 + dlcGroupBox.Location = new System.Drawing.Point(12, 130); + dlcGroupBox.Size = new System.Drawing.Size(536, 130); + dlcGroupBox.TabStop = false; + dlcGroupBox.Text = "DLC Entries (Steam only)"; + dlcGroupBox.Controls.Add(dlcListBox); + dlcGroupBox.Controls.Add(dlcIdLabel); + dlcGroupBox.Controls.Add(dlcIdTextBox); + dlcGroupBox.Controls.Add(dlcNameLabel); + dlcGroupBox.Controls.Add(dlcNameTextBox); + dlcGroupBox.Controls.Add(addDlcButton); + dlcGroupBox.Controls.Add(removeDlcButton); + + dlcListBox.Location = new System.Drawing.Point(6, 20); + dlcListBox.Size = new System.Drawing.Size(524, 60); + + // DLC row inside group box — left-to-right: + // "DLC ID:" label + 70px box + "DLC Name:" label + 160px box + "Add"(60) + "Remove"(70) + // Total: ~48 + 70 + ~72 + 160 + 60 + 70 = 480 (fits in 524) + dlcIdLabel.AutoSize = true; + dlcIdLabel.Location = new System.Drawing.Point(6, 92); + dlcIdLabel.Text = "DLC ID:"; + + dlcIdTextBox.Location = new System.Drawing.Point(62, 89); + dlcIdTextBox.Size = new System.Drawing.Size(70, 23); + dlcIdTextBox.PlaceholderText = "e.g. 12345"; + + dlcNameLabel.AutoSize = true; + dlcNameLabel.Location = new System.Drawing.Point(140, 92); + dlcNameLabel.Text = "DLC Name:"; + + dlcNameTextBox.Location = new System.Drawing.Point(216, 89); + dlcNameTextBox.Size = new System.Drawing.Size(184, 23); + dlcNameTextBox.PlaceholderText = "e.g. Test DLC"; + + addDlcButton.Location = new System.Drawing.Point(406, 89); + addDlcButton.Size = new System.Drawing.Size(52, 23); + addDlcButton.Text = "Add"; + addDlcButton.Click += OnAddDlc; + + removeDlcButton.Location = new System.Drawing.Point(462, 89); + removeDlcButton.Size = new System.Drawing.Size(62, 23); + removeDlcButton.Text = "Remove"; + removeDlcButton.Click += OnRemoveDlc; + + // ── Action buttons ── y=270 + generateButton.Location = new System.Drawing.Point(12, 270); + generateButton.Size = new System.Drawing.Size(150, 26); + generateButton.Text = "Generate Test Game"; + generateButton.Click += OnGenerate; + + clearButton.Location = new System.Drawing.Point(168, 270); + clearButton.Size = new System.Drawing.Size(110, 26); + clearButton.Text = "Clear All Tests"; + clearButton.Click += OnClearAll; + + closeButton.Location = new System.Drawing.Point(284, 270); + closeButton.Size = new System.Drawing.Size(70, 26); + closeButton.Text = "Close"; + closeButton.Click += OnClose; + + // ── Status label ── y=302 + statusLabel.Location = new System.Drawing.Point(12, 302); + statusLabel.Size = new System.Drawing.Size(536, 20); + statusLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F); + + // ── Form ── + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(560, 328); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + StartPosition = FormStartPosition.CenterParent; + Text = "Test Game Generator"; + Controls.Add(platformGroupBox); + Controls.Add(appIdLabel); + Controls.Add(appIdTextBox); + Controls.Add(gameNameLabel); + Controls.Add(gameNameTextBox); + Controls.Add(epicSearchButton); + Controls.Add(epicResultsListBox); + Controls.Add(dlcGroupBox); + Controls.Add(generateButton); + Controls.Add(clearButton); + Controls.Add(closeButton); + Controls.Add(statusLabel); + + platformGroupBox.ResumeLayout(false); + platformGroupBox.PerformLayout(); + dlcGroupBox.ResumeLayout(false); + dlcGroupBox.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + private GroupBox platformGroupBox; + private RadioButton steamRadioButton; + private RadioButton epicRadioButton; + private Label appIdLabel; + private TextBox appIdTextBox; + private Label gameNameLabel; + private TextBox gameNameTextBox; + private Button epicSearchButton; + private ListBox epicResultsListBox; + private GroupBox dlcGroupBox; + private ListBox dlcListBox; + private Label dlcIdLabel; + private TextBox dlcIdTextBox; + private Label dlcNameLabel; + private TextBox dlcNameTextBox; + private Button addDlcButton; + private Button removeDlcButton; + private Button generateButton; + private Button clearButton; + private Button closeButton; + private Label statusLabel; +} diff --git a/CreamInstaller/Forms/TestGameForm.cs b/CreamInstaller/Forms/TestGameForm.cs new file mode 100644 index 0000000..27e86a6 --- /dev/null +++ b/CreamInstaller/Forms/TestGameForm.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using CreamInstaller.Components; +using CreamInstaller.Platforms.Epic; +using CreamInstaller.Platforms.Steam; +using CreamInstaller.Utility; + +namespace CreamInstaller.Forms; + +internal sealed partial class TestGameForm : CustomForm +{ + private static readonly string TestGamesRoot = + Path.Combine(ProgramData.DirectoryPath, "TestGames"); + + private static readonly List CreatedDirectories = []; + + // Steam DLC entries per-form: (dlcId, dlcName) + private readonly List<(string id, string name)> dlcEntries = []; + + // Cached Epic search results from the last search: (namespace, name) + private readonly List<(string ns, string name)> epicSearchResults = []; + + private bool IsEpicMode => epicRadioButton.Checked; + + internal TestGameForm(IWin32Window owner) : base(owner) + { + InitializeComponent(); + appIdTextBox.Leave += OnAppIdLeave; + RefreshDlcList(); + UpdatePlatformMode(); + } + + private void UpdatePlatformMode() + { + bool epic = IsEpicMode; + + // App ID row: Steam only + appIdLabel.Visible = !epic; + appIdTextBox.Visible = !epic; + + // Search button: Epic only — shrink the game name box to make room + epicSearchButton.Visible = epic; + gameNameTextBox.Size = new System.Drawing.Size(epic ? 354 : 443, 23); + + // Placeholder text — call RefreshCueBanner to flush the Win32 cue so only one text shows + gameNameTextBox.PlaceholderText = epic ? "Enter game name and click Search" : "e.g. Spacewar"; + NativeMethods.RefreshCueBanner(gameNameTextBox); + + // DLC group and Epic results share the same vertical slot + dlcGroupBox.Visible = !epic; + epicResultsListBox.Visible = false; // hidden until search runs + + if (!epic) + epicSearchResults.Clear(); + + SetStatus(epic + ? "Enter a game name and click Search to find it on the Epic store." + : "Enter the App ID, then tab out to auto-detect the game name."); + } + + private void OnPlatformChanged(object sender, EventArgs e) => UpdatePlatformMode(); + + // ── Steam: auto-detect name from AppID ────────────────────────────────── + + private async void OnAppIdLeave(object sender, EventArgs e) + { + if (IsEpicMode) + return; + string appId = appIdTextBox.Text.Trim(); + if (string.IsNullOrWhiteSpace(appId) || !int.TryParse(appId, out _)) + return; + if (!string.IsNullOrWhiteSpace(gameNameTextBox.Text)) + return; + + SetStatus("Looking up game name . . ."); + generateButton.Enabled = false; + + string name = await Task.Run(async () => + { + // Use a dedicated client with a neutral UA so Steam's store API doesn't reject the request. + using System.Net.Http.HttpClient client = new(); + client.DefaultRequestHeaders.UserAgent.ParseAdd($"{Program.Name}/{Program.Version}"); + string url = $"https://store.steampowered.com/api/appdetails?appids={appId}&filters=basic"; + try + { + string json = await client.GetStringAsync(url); + Newtonsoft.Json.Linq.JObject root = Newtonsoft.Json.Linq.JObject.Parse(json); + string title = root[appId]?["data"]?["name"]?.ToString(); + if (!string.IsNullOrWhiteSpace(title)) + return title; + } + catch { /* fall through to SteamCMD */ } + + CmdAppData cmdData = await SteamCMD.GetAppInfo(appId); + return cmdData?.Common?.Name; + }); + + generateButton.Enabled = true; + + if (name is not null) + { + gameNameTextBox.Text = name; + SetStatus($"✓ Game name detected: {name}"); + } + else + { + SetStatus("Could not auto-detect name — enter it manually."); + } + } + + // ── Epic: search by name ───────────────────────────────────────────────── + + private async void OnEpicSearch(object sender, EventArgs e) + { + string keyword = gameNameTextBox.Text.Trim(); + if (string.IsNullOrWhiteSpace(keyword)) + { + SetStatus("Enter a game name to search."); + return; + } + + SetStatus("Searching Epic store . . ."); + epicSearchButton.Enabled = false; + generateButton.Enabled = false; + epicResultsListBox.Items.Clear(); + epicResultsListBox.Visible = false; + epicSearchResults.Clear(); + + List<(string ns, string name)> results = await EpicStore.QuerySearch(keyword); + + epicSearchButton.Enabled = true; + generateButton.Enabled = true; + + if (results.Count == 0) + { + SetStatus("No results found. Try a different name."); + return; + } + + epicSearchResults.AddRange(results); + foreach ((string _, string name) in results) + epicResultsListBox.Items.Add(name); + + epicResultsListBox.Visible = true; + SetStatus($"Found {results.Count} result(s). Select one to use it."); + } + + private void OnEpicResultSelected(object sender, EventArgs e) + { + int idx = epicResultsListBox.SelectedIndex; + if (idx < 0 || idx >= epicSearchResults.Count) + return; + gameNameTextBox.Text = epicSearchResults[idx].name; + SetStatus($"✓ Selected: {epicSearchResults[idx].name}"); + } + + // ── DLC (Steam) ────────────────────────────────────────────────────────── + + private void OnAddDlc(object sender, EventArgs e) + { + string dlcId = dlcIdTextBox.Text.Trim(); + string dlcName = dlcNameTextBox.Text.Trim(); + if (string.IsNullOrWhiteSpace(dlcId) || !int.TryParse(dlcId, out _)) + { + SetStatus("DLC ID must be a valid integer."); + return; + } + + if (string.IsNullOrWhiteSpace(dlcName)) + { + SetStatus("DLC Name cannot be empty."); + return; + } + + if (dlcEntries.Any(d => d.id == dlcId)) + { + SetStatus($"DLC ID {dlcId} is already in the list."); + return; + } + + dlcEntries.Add((dlcId, dlcName)); + RefreshDlcList(); + dlcIdTextBox.Clear(); + dlcNameTextBox.Clear(); + SetStatus($"Added DLC: {dlcId} = {dlcName}"); + } + + private void OnRemoveDlc(object sender, EventArgs e) + { + if (dlcListBox.SelectedIndex < 0) + return; + dlcEntries.RemoveAt(dlcListBox.SelectedIndex); + RefreshDlcList(); + SetStatus("Removed selected DLC entry."); + } + + private void OnDlcListBoxSelectionChanged(object sender, EventArgs e) { } + + private void RefreshDlcList() + { + dlcListBox.Items.Clear(); + foreach ((string id, string name) in dlcEntries) + dlcListBox.Items.Add($"{id} = {name}"); + } + + // ── Generate ──────────────────────────────────────────────────────────── + + private void OnGenerate(object sender, EventArgs e) + { + if (IsEpicMode) + GenerateEpic(); + else + GenerateSteam(); + } + + private void GenerateSteam() + { + string appId = appIdTextBox.Text.Trim(); + string gameName = gameNameTextBox.Text.Trim(); + + if (string.IsNullOrWhiteSpace(appId) || !int.TryParse(appId, out _)) + { + SetStatus("App ID must be a valid integer."); + return; + } + + if (string.IsNullOrWhiteSpace(gameName)) + { + SetStatus("Game Name cannot be empty."); + return; + } + + if (SteamLibrary.TestGames.Any(g => g.appId == appId)) + { + SetStatus($"A test game with App ID {appId} already exists."); + return; + } + + try + { + string gameDir = Path.Combine(TestGamesRoot, $"steam_{appId}_{SanitizeName(gameName)}"); + Directory.CreateDirectory(gameDir); + + string dllPath = Path.Combine(gameDir, "steam_api64.dll"); + WriteSteamApiStub(dllPath); + + CreatedDirectories.Add(gameDir); + SteamLibrary.TestGames.Add((appId, gameName, "public", 1, gameDir)); + ProgramData.Log($"[TestGame] Steam: {gameName} ({appId}) at {gameDir}"); + SetStatus($"✓ Steam test game '{gameName}' ({appId}) generated. Press Rescan."); + } + catch (Exception ex) + { + SetStatus($"Error: {ex.Message}"); + } + } + + private void GenerateEpic() + { + string gameName = gameNameTextBox.Text.Trim(); + if (string.IsNullOrWhiteSpace(gameName)) + { + SetStatus("Game Name cannot be empty. Search for a game first."); + return; + } + + // Use the selected search result namespace if available, otherwise derive a stub + string catalogNamespace; + int idx = epicResultsListBox.SelectedIndex; + if (idx >= 0 && idx < epicSearchResults.Count) + { + catalogNamespace = epicSearchResults[idx].ns; + gameName = epicSearchResults[idx].name; + } + else + { + catalogNamespace = $"test_{SanitizeName(gameName).ToLowerInvariant()}"; + } + + if (EpicLibrary.TestManifests.Any(m => m.CatalogNamespace == catalogNamespace)) + { + SetStatus("An Epic test game with that namespace already exists."); + return; + } + + try + { + string gameDir = Path.Combine(TestGamesRoot, $"epic_{SanitizeName(gameName)}"); + Directory.CreateDirectory(gameDir); + + // Stub DLL so Epic DLL-directory scanning finds the game + string dllPath = Path.Combine(gameDir, "EOSSDK-Win64-Shipping.dll"); + WriteSteamApiStub(dllPath); + + CreatedDirectories.Add(gameDir); + + EpicLibrary.TestManifests.Add(new Manifest + { + DisplayName = gameName, + CatalogNamespace = catalogNamespace, + InstallLocation = gameDir + }); + + ProgramData.Log($"[TestGame] Epic: {gameName} ({catalogNamespace}) at {gameDir}"); + SetStatus($"✓ Epic test game '{gameName}' generated. Press Rescan."); + } + catch (Exception ex) + { + SetStatus($"Error: {ex.Message}"); + } + } + + // ── Clear / Close ──────────────────────────────────────────────────────── + + private void OnClearAll(object sender, EventArgs e) + { + SteamLibrary.TestGames.Clear(); + EpicLibrary.TestManifests.Clear(); + foreach (string dir in CreatedDirectories) + try { Directory.Delete(dir, true); } catch { /* best-effort */ } + CreatedDirectories.Clear(); + dlcEntries.Clear(); + RefreshDlcList(); + epicSearchResults.Clear(); + epicResultsListBox.Items.Clear(); + epicResultsListBox.Visible = false; + SetStatus("All test games cleared. Press Rescan in the main window."); + } + + private void OnClose(object sender, EventArgs e) => Close(); + + // ── Helpers ────────────────────────────────────────────────────────────── + + private void SetStatus(string message) + { + statusLabel.Text = message; + statusLabel.ForeColor = message.StartsWith("✓", StringComparison.Ordinal) + ? System.Drawing.Color.Green + : System.Drawing.Color.FromArgb(212, 212, 212); + } + + private static string SanitizeName(string name) + { + char[] invalid = Path.GetInvalidFileNameChars(); + return new string(name.Select(c => invalid.Contains(c) ? '_' : c).ToArray()); + } + + private static void WriteSteamApiStub(string path) + { + byte[] mzStub = + [ + 0x4D, 0x5A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]; + File.WriteAllBytes(path, mzStub); + } +} diff --git a/CreamInstaller/Forms/TestGameForm.resx b/CreamInstaller/Forms/TestGameForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/CreamInstaller/Forms/TestGameForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/CreamInstaller/Platforms/Epic/EpicLibrary.cs b/CreamInstaller/Platforms/Epic/EpicLibrary.cs index 539364c..1d28fb0 100644 --- a/CreamInstaller/Platforms/Epic/EpicLibrary.cs +++ b/CreamInstaller/Platforms/Epic/EpicLibrary.cs @@ -28,10 +28,17 @@ internal static class EpicLibrary } } + internal static readonly List TestManifests = []; + internal static async Task> GetGames() => await Task.Run(async () => { List games = new(); + + foreach (Manifest test in TestManifests) + if (games.All(g => g.CatalogNamespace != test.CatalogNamespace)) + games.Add(test); + string manifests = EpicManifestsPath; if (manifests.DirectoryExists()) foreach (string item in manifests.EnumerateDirectory("*.item")) diff --git a/CreamInstaller/Platforms/Epic/EpicStore.cs b/CreamInstaller/Platforms/Epic/EpicStore.cs index 0655fea..2fa51d7 100644 --- a/CreamInstaller/Platforms/Epic/EpicStore.cs +++ b/CreamInstaller/Platforms/Epic/EpicStore.cs @@ -132,6 +132,53 @@ internal static class EpicStore public static bool EpicBool = true; + internal static async Task> QuerySearch(string keyword) + { + List<(string, string)> results = []; + try + { + string query = """ + query searchByKeyword($keywords: String!) { + Catalog { + searchStore(keywords: $keywords, category: "games/edition/base", count: 10, country: "US", locale: "en-US", allowCountries: "US") { + elements { + title + namespace + } + } + } + } + """; + var payload = new { query, variables = new { keywords = keyword } }; + string payloadJson = JsonConvert.SerializeObject(payload); + using HttpContent content = new StringContent(payloadJson, System.Text.Encoding.UTF8, "application/json"); + HttpClient client = HttpClientManager.HttpClient; + if (client is null) + return results; + HttpResponseMessage httpResponse = + await client.PostAsync(new Uri("https://launcher.store.epicgames.com/graphql"), content); + _ = httpResponse.EnsureSuccessStatusCode(); + string response = await httpResponse.Content.ReadAsStringAsync(); + Newtonsoft.Json.Linq.JObject root = Newtonsoft.Json.Linq.JObject.Parse(response); + Newtonsoft.Json.Linq.JToken elements = root["data"]?["Catalog"]?["searchStore"]?["elements"]; + if (elements is null) + return results; + foreach (Newtonsoft.Json.Linq.JToken el in elements) + { + string name = el["title"]?.ToString(); + string ns = el["namespace"]?.ToString(); + if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(ns) + && results.All(r => r.Item1 != ns)) + results.Add((ns, name)); + } + } + catch + { + // ignored + } + return results; + } + private static async Task QueryGraphQL(string categoryNamespace) { try diff --git a/CreamInstaller/Platforms/Steam/SteamLibrary.cs b/CreamInstaller/Platforms/Steam/SteamLibrary.cs index 7d20395..f4b67db 100644 --- a/CreamInstaller/Platforms/Steam/SteamLibrary.cs +++ b/CreamInstaller/Platforms/Steam/SteamLibrary.cs @@ -10,6 +10,9 @@ namespace CreamInstaller.Platforms.Steam; internal static class SteamLibrary { + internal static readonly List<(string appId, string name, string branch, int buildId, string gameDirectory)> + TestGames = []; + private static string installPath; internal static string InstallPath @@ -41,6 +44,11 @@ internal static class SteamLibrary games.Add(game); } + foreach ((string appId, string name, string branch, int buildId, string gameDirectory) testGame in + TestGames.Where(t => games.All(g => g.appId != t.appId))) + games.Add(testGame); + if (TestGames.Count > 0) + ProgramData.Log($"[Steam] Injected {TestGames.Count} test game(s)."); ProgramData.Log($"[Steam] Total games detected: {games.Count}"); return games; }); diff --git a/CreamInstaller/Utility/ThemeManager.cs b/CreamInstaller/Utility/ThemeManager.cs index efe08cd..a640a71 100644 --- a/CreamInstaller/Utility/ThemeManager.cs +++ b/CreamInstaller/Utility/ThemeManager.cs @@ -180,9 +180,9 @@ internal static class ThemeManager ll.VisitedLinkColor = DarkLink; break; - // Labels: dark background, light foreground + // Labels: transparent so they blend with whatever container they sit in case Label lbl: - lbl.BackColor = DarkBack; + lbl.BackColor = Color.Transparent; lbl.ForeColor = DarkFore; break; @@ -206,6 +206,12 @@ internal static class ThemeManager rtb.ForeColor = DarkFore; break; + // ListBox follows alternate dark background + case ListBox lb: + lb.BackColor = DarkBackAlt; + lb.ForeColor = DarkFore; + break; + // TextBox follows alternate dark background case TextBox tb: tb.BackColor = DarkBackAlt; @@ -249,7 +255,7 @@ internal static class ThemeManager ll.VisitedLinkColor = SystemColors.HotTrack; break; case Label lbl: - lbl.BackColor = LightBack; + lbl.BackColor = Color.Transparent; lbl.ForeColor = LightFore; break; case ProgressBar pb: @@ -266,6 +272,10 @@ internal static class ThemeManager rtb.BackColor = LightBack; rtb.ForeColor = LightFore; break; + case ListBox lb: + lb.BackColor = LightBackAlt; + lb.ForeColor = LightFore; + break; case TextBox tb: tb.BackColor = LightBackAlt; tb.ForeColor = LightFore;