- Converted steam store page parsing into actual steam store API calls
- Substantially increased the speed of gathering app information, as DLC gathering will now attempt to use the steam store API system to gather information, only reverting back to SteamCMD if it was unsuccessful (if the DLC isn't on the steam store)
- Program will now try to utilize cached Epic Store info if the web request fails
- Reverted the maximum SteamCMD processes to 20
- Fixed a rare concurrency issue with the new SteamCMD method
- Improved the used images and icons for the context menu
- Added the "Open in SteamDB" context menu option for Steam DLC
This commit is contained in:
pointfeev 2022-03-06 20:00:45 -05:00
parent 5b40924734
commit 7f3a6a6aa0
10 changed files with 280 additions and 65 deletions

View file

@ -5,7 +5,7 @@
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Resources\ini.ico</ApplicationIcon>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<Version>3.1.0.1</Version>
<Version>3.2.0.0</Version>
<PackageIcon>Resources\ini.ico</PackageIcon>
<PackageIconUrl />
<Description />

View file

@ -14,6 +14,8 @@ internal partial class DialogForm : CustomForm
{
if (customFormIcon is not null)
Icon = customFormIcon;
if (descriptionIcon is null)
descriptionIcon = Icon;
icon.Image = descriptionIcon.ToBitmap();
descriptionLabel.Text = descriptionText;
acceptButton.Text = acceptButtonText;

View file

@ -24,8 +24,20 @@ internal static class EpicStore
{
List<(string id, string name, string product, string icon, string developer)> dlcIds = new();
Response response = await QueryGraphQL(categoryNamespace);
string cacheFile = ProgramData.AppInfoPath + @$"\{categoryNamespace}.json";
if (response is null)
if (Directory.Exists(Directory.GetDirectoryRoot(cacheFile)) && File.Exists(cacheFile))
try
{
response = JsonConvert.DeserializeObject<Response>(File.ReadAllText(cacheFile));
}
catch { }
if (response is null) return dlcIds;
try { File.WriteAllText(ProgramData.AppInfoPath + @$"\{categoryNamespace}.json", JsonConvert.SerializeObject(response, Formatting.Indented)); } catch { }
try
{
File.WriteAllText(cacheFile, JsonConvert.SerializeObject(response, Formatting.Indented));
}
catch { }
List<Element> searchStore = new(response.Data.Catalog.SearchStore.Elements);
foreach (Element element in searchStore)
{

View file

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Gameloop.Vdf.Linq;
namespace CreamInstaller;
internal enum DlcType
@ -24,7 +22,7 @@ internal class ProgramSelection
internal string ProductUrl = null;
internal string IconUrl = null;
internal string ClientIconUrl = null;
internal string SubIconUrl = null;
internal string Publisher = null;
@ -32,7 +30,6 @@ internal class ProgramSelection
internal List<string> DllDirectories = null;
internal bool IsSteam = false;
internal VProperty AppInfo = null;
internal readonly SortedList<string, (DlcType type, string name, string icon)> AllDlc = new();
internal readonly SortedList<string, (DlcType type, string name, string icon)> SelectedDlc = new();

View file

@ -142,8 +142,9 @@ internal partial class SelectForm : CustomForm
RemoveFromRemainingGames(name);
return;
}
AppData appData = await SteamStore.QueryStoreAPI(appId);
VProperty appInfo = appInfo = await SteamCMD.GetAppInfo(appId, branch, buildId);
if (appInfo is null)
if (appInfo is null || appData is null)
{
RemoveFromRemainingGames(name);
return;
@ -151,8 +152,8 @@ internal partial class SelectForm : CustomForm
if (Program.Canceled) return;
ConcurrentDictionary<string, (DlcType type, string name, string icon)> dlc = new();
List<Task> dlcTasks = new();
List<string> dlcIds = await SteamCMD.ParseDlcAppIds(appInfo);
await SteamStore.ParseDlcAppIds(appId, dlcIds);
List<string> dlcIds = await SteamStore.ParseDlcAppIds(appData);
dlcIds.AddRange(await SteamCMD.ParseDlcAppIds(appInfo));
if (dlcIds.Count > 0)
{
foreach (string dlcAppId in dlcIds)
@ -163,20 +164,30 @@ internal partial class SelectForm : CustomForm
{
if (Program.Canceled) return;
string dlcName = null;
string dlcIconStaticId = null;
VProperty dlcAppInfo = await SteamCMD.GetAppInfo(dlcAppId);
if (dlcAppInfo is not null)
string dlcIcon = null;
AppData dlcAppData = await SteamStore.QueryStoreAPI(dlcAppId);
if (dlcAppData is not null)
{
dlcName = dlcAppInfo.Value?.GetChild("common")?.GetChild("name")?.ToString();
dlcIconStaticId = dlcAppInfo.Value?.GetChild("common")?.GetChild("icon")?.ToString();
dlcIconStaticId ??= dlcAppInfo.Value?.GetChild("common")?.GetChild("logo_small")?.ToString();
dlcIconStaticId ??= dlcAppInfo.Value?.GetChild("common")?.GetChild("logo")?.ToString();
if (dlcIconStaticId is not null)
dlcIconStaticId = IconGrabber.SteamAppImagesPath + @$"\{dlcAppId}\{dlcIconStaticId}.jpg";
dlcName = dlcAppData.name;
dlcIcon = dlcAppData.header_image;
}
else
{
VProperty dlcAppInfo = await SteamCMD.GetAppInfo(dlcAppId);
if (dlcAppInfo is not null)
{
dlcName = dlcAppInfo.Value?.GetChild("common")?.GetChild("name")?.ToString();
string dlcIconStaticId = dlcAppInfo.Value?.GetChild("common")?.GetChild("icon")?.ToString();
dlcIconStaticId ??= dlcAppInfo.Value?.GetChild("common")?.GetChild("logo_small")?.ToString();
dlcIconStaticId ??= dlcAppInfo.Value?.GetChild("common")?.GetChild("logo")?.ToString();
if (dlcIconStaticId is not null)
dlcIcon = IconGrabber.SteamAppImagesPath + @$"\{dlcAppId}\{dlcIconStaticId}.jpg";
}
}
if (Program.Canceled) return;
if (!string.IsNullOrWhiteSpace(dlcName))
dlc[dlcAppId] = (DlcType.Default, dlcName, dlcIconStaticId);
if (string.IsNullOrWhiteSpace(dlcName))
return; //dlcName = "Unknown DLC";
dlc[dlcAppId] = (DlcType.Default, dlcName, dlcIcon);
RemoveFromRemainingDLCs(dlcAppId);
});
dlcTasks.Add(task);
@ -195,6 +206,7 @@ internal partial class SelectForm : CustomForm
await task;
}
name = appData.name ?? name;
selection ??= new();
selection.Enabled = allCheckBox.Checked || selection.SelectedDlc.Any() || selection.ExtraDlc.Any();
selection.Usable = true;
@ -203,11 +215,10 @@ internal partial class SelectForm : CustomForm
selection.RootDirectory = directory;
selection.DllDirectories = dllDirectories;
selection.IsSteam = true;
selection.AppInfo = appInfo;
selection.ProductUrl = "https://store.steampowered.com/app/" + appId;
selection.IconUrl = IconGrabber.SteamAppImagesPath + @$"\{appId}\{appInfo?.Value?.GetChild("common")?.GetChild("icon")?.ToString()}.jpg";
selection.ClientIconUrl = IconGrabber.SteamAppImagesPath + @$"\{appId}\{appInfo?.Value?.GetChild("common")?.GetChild("clienticon")?.ToString()}.ico";
selection.Publisher = appInfo?.Value?.GetChild("extended")?.GetChild("publisher")?.ToString();
selection.IconUrl = IconGrabber.SteamAppImagesPath + @$"\{appId}\{appInfo?.Value?.GetChild("common")?.GetChild("clienticon")?.ToString()}.ico";
selection.SubIconUrl = appData.header_image ?? IconGrabber.SteamAppImagesPath + @$"\{appId}\{appInfo?.Value?.GetChild("common")?.GetChild("icon")?.ToString()}.jpg";
selection.Publisher = appData.publishers[0] ?? appInfo?.Value?.GetChild("extended")?.GetChild("publisher")?.ToString();
if (Program.Canceled) return;
Program.Invoke(selectionTreeView, delegate
@ -600,30 +611,30 @@ internal partial class SelectForm : CustomForm
}
}
nodeContextMenu.Items.Add(header);
string appInfo = $@"{SteamCMD.AppInfoPath}\{id}.vdf";
string appInfoEpic = $@"{SteamCMD.AppInfoPath}\{id}.json";
if (Directory.Exists(Directory.GetDirectoryRoot(appInfo)) && (File.Exists(appInfo) || File.Exists(appInfoEpic)))
string appInfoVDF = $@"{SteamCMD.AppInfoPath}\{id}.vdf";
string appInfoJSON = $@"{SteamCMD.AppInfoPath}\{id}.json";
if (Directory.Exists(Directory.GetDirectoryRoot(appInfoVDF)) && (File.Exists(appInfoVDF) || File.Exists(appInfoJSON)))
{
nodeContextMenu.Items.Add(new ToolStripSeparator());
nodeContextMenu.Items.Add(new ToolStripMenuItem("Open AppInfo", Image("Notepad"),
new EventHandler((sender, e) =>
{
if (File.Exists(appInfo))
Diagnostics.OpenFileInNotepad(appInfo);
else
Diagnostics.OpenFileInNotepad(appInfoEpic);
if (File.Exists(appInfoVDF))
Diagnostics.OpenFileInNotepad(appInfoVDF);
else if (File.Exists(appInfoJSON))
Diagnostics.OpenFileInNotepad(appInfoJSON);
})));
nodeContextMenu.Items.Add(new ToolStripMenuItem("Refresh AppInfo", Image("Command Prompt"),
new EventHandler((sender, e) =>
{
try
{
File.Delete(appInfo);
File.Delete(appInfoVDF);
}
catch { }
try
{
File.Delete(appInfoEpic);
File.Delete(appInfoJSON);
}
catch { }
OnLoad();
@ -713,7 +724,7 @@ internal partial class SelectForm : CustomForm
}
else
new DialogForm(this).Show(SystemIcons.Error, "Paradox Launcher repair failed!"
+ "\n\nAn original Steamworks API or Epic Online Services SDK file could not be found."
+ "\n\nAn original Steamworks/Epic Online Services SDK file could not be found."
+ "\nYou must reinstall Paradox Launcher to fix this issue.", "OK");
})));
}
@ -723,17 +734,21 @@ internal partial class SelectForm : CustomForm
for (int i = 0; i < selection.DllDirectories.Count; i++)
{
string directory = selection.DllDirectories[i];
nodeContextMenu.Items.Add(new ToolStripMenuItem($"Open {(selection.IsSteam ? "Steamworks API" : "Epic Online Services SDK")} Directory ({i + 1})", Image("File Explorer"),
nodeContextMenu.Items.Add(new ToolStripMenuItem($"Open {(selection.IsSteam ? "Steamworks" : "Epic Online Services")} SDK Directory ({i + 1})", Image("File Explorer"),
new EventHandler((sender, e) => Diagnostics.OpenDirectoryInFileExplorer(directory))));
}
}
ProgramSelection dlcParentSelection = dlc.HasValue ? ProgramSelection.FromId(dlc.Value.gameAppId) : null;
if (selection is not null || dlcParentSelection is not null && dlcParentSelection.IsSteam)
{
nodeContextMenu.Items.Add(new ToolStripSeparator());
nodeContextMenu.Items.Add(new ToolStripMenuItem("Open SteamDB", Image("SteamDB"),
new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://steamdb.info/app/" + id))));
}
if (id != "ParadoxLauncher" && selection is not null)
{
if (selection.IsSteam)
{
nodeContextMenu.Items.Add(new ToolStripSeparator());
nodeContextMenu.Items.Add(new ToolStripMenuItem("Open SteamDB", Image("SteamDB"),
new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser("https://steamdb.info/app/" + id))));
nodeContextMenu.Items.Add(new ToolStripMenuItem("Open Steam Store", Image("Steam Store"),
new EventHandler((sender, e) => Diagnostics.OpenUrlInInternetBrowser(selection.ProductUrl))));
ToolStripMenuItem steamCommunity = new("Open Steam Community", Image("ClientIcon_" + id),
@ -742,7 +757,7 @@ internal partial class SelectForm : CustomForm
if (steamCommunity.Image is null)
{
steamCommunity.Image = Image("Steam Community");
TrySetImageAsync(steamCommunity, id, selection.ClientIconUrl, true);
TrySetImageAsync(steamCommunity, id, selection.SubIconUrl, true);
}
}
else

View file

@ -0,0 +1,147 @@
using System.Collections.Generic;
namespace CreamInstaller.Steam;
#pragma warning disable IDE1006 // Naming Styles
public class PriceOverview
{
public string currency { get; set; }
public int initial { get; set; }
public int final { get; set; }
public int discount_percent { get; set; }
public string initial_formatted { get; set; }
public string final_formatted { get; set; }
}
public class Sub
{
public int packageid { get; set; }
public string percent_savings_text { get; set; }
public int percent_savings { get; set; }
public string option_text { get; set; }
public string option_description { get; set; }
public string can_get_free_license { get; set; }
public bool is_free_license { get; set; }
public int price_in_cents_with_discount { get; set; }
}
public class PackageGroup
{
public string name { get; set; }
public string title { get; set; }
public string description { get; set; }
public string selection_text { get; set; }
public string save_text { get; set; }
public object display_type { get; set; }
public string is_recurring_subscription { get; set; }
public List<Sub> subs { get; set; }
}
public class Platforms
{
public bool windows { get; set; }
public bool mac { get; set; }
public bool linux { get; set; }
}
public class Metacritic
{
public int score { get; set; }
public string url { get; set; }
}
public class Category
{
public int id { get; set; }
public string description { get; set; }
}
public class Genre
{
public string id { get; set; }
public string description { get; set; }
}
public class Screenshot
{
public int id { get; set; }
public string path_thumbnail { get; set; }
public string path_full { get; set; }
}
public class Recommendations
{
public int total { get; set; }
}
public class Highlighted
{
public string name { get; set; }
public string path { get; set; }
}
public class Achievements
{
public int total { get; set; }
public List<Highlighted> highlighted { get; set; }
}
public class ReleaseDate
{
public bool coming_soon { get; set; }
public string date { get; set; }
}
public class SupportInfo
{
public string url { get; set; }
public string email { get; set; }
}
public class ContentDescriptors
{
public List<object> ids { get; set; }
public object notes { get; set; }
}
public class AppData
{
public string type { get; set; }
public string name { get; set; }
public int steam_appid { get; set; }
public int required_age { get; set; }
public bool is_free { get; set; }
public List<int> dlc { get; set; }
public string detailed_description { get; set; }
public string about_the_game { get; set; }
public string short_description { get; set; }
public string supported_languages { get; set; }
public string reviews { get; set; }
public string header_image { get; set; }
public string website { get; set; }
public string legal_notice { get; set; }
public List<string> developers { get; set; }
public List<string> publishers { get; set; }
public PriceOverview price_overview { get; set; }
public List<int> packages { get; set; }
public List<PackageGroup> package_groups { get; set; }
public Platforms platforms { get; set; }
public Metacritic metacritic { get; set; }
public List<Category> categories { get; set; }
public List<Genre> genres { get; set; }
public List<Screenshot> screenshots { get; set; }
public Recommendations recommendations { get; set; }
public Achievements achievements { get; set; }
public ReleaseDate release_date { get; set; }
public SupportInfo support_info { get; set; }
public string background { get; set; }
public string background_raw { get; set; }
public ContentDescriptors content_descriptors { get; set; }
}
public class AppDetails
{
public bool success { get; set; }
public AppData data { get; set; }
}
#pragma warning restore IDE1006 // Naming Styles

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -18,15 +19,17 @@ namespace CreamInstaller.Steam;
internal static class SteamCMD
{
internal static readonly int ProcessLimit = 30;
internal static readonly int ProcessLimit = 20;
internal static string DirectoryPath => ProgramData.DirectoryPath;
internal static string AppInfoPath => ProgramData.AppInfoPath;
internal static readonly string FilePath = DirectoryPath + @"\steamcmd.exe";
private static readonly Dictionary<string, int> AttemptCount = new(); // the more app_updates, the longer SteamCMD should wait for app_info_print
private static string GetArguments(string appId) => $@"@ShutdownOnFailedCommand 0 +force_install_dir {DirectoryPath} +login anonymous +app_info_print {appId} " + string.Concat(Enumerable.Repeat("+app_update 4 ", AttemptCount[appId])) + "+quit";
private static readonly ConcurrentDictionary<string, int> AttemptCount = new(); // the more app_updates, the longer SteamCMD should wait for app_info_print
private static string GetArguments(string appId) => AttemptCount.TryGetValue(appId, out int attempts)
? $@"@ShutdownOnFailedCommand 0 +force_install_dir {DirectoryPath} +login anonymous +app_info_print {appId} " + string.Concat(Enumerable.Repeat("+app_update 4 ", attempts)) + "+quit"
: $"+login anonymous +app_info_print {appId} +quit";
private static readonly int[] locks = new int[ProcessLimit];
internal static async Task<string> Run(string appId) => await Task.Run(() =>

View file

@ -1,24 +1,61 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using CreamInstaller.Utility;
using HtmlAgilityPack;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CreamInstaller.Steam;
internal static class SteamStore
{
internal static async Task ParseDlcAppIds(string appId, List<string> dlcIds)
internal static async Task<List<string>> ParseDlcAppIds(AppData appData) => await Task.Run(() =>
{
// currently this is only really needed to get DLC that release without changing game buildid (very rare)
// it also finds things which aren't really connected to the game itself, and thus not needed (usually soundtracks, collections, packs, etc.)
HtmlNodeCollection nodes = await HttpClientManager.GetDocumentNodes(
$"https://store.steampowered.com/dlc/{appId}",
"//div[@class='recommendation']/div/a");
if (nodes is not null)
foreach (HtmlNode node in nodes)
if (int.TryParse(node.Attributes?["data-ds-appid"]?.Value, out int dlcAppId) && dlcAppId > 0 && !dlcIds.Contains("" + dlcAppId))
dlcIds.Add("" + dlcAppId);
List<string> dlcIds = new();
if (appData.dlc is null) return dlcIds;
foreach (int appId in appData.dlc)
dlcIds.Add(appId.ToString());
return dlcIds;
});
internal static async Task<AppData> QueryStoreAPI(string appId)
{
if (Program.Canceled) return null;
string response = await HttpClientManager.EnsureGet($"https://store.steampowered.com/api/appdetails?appids={appId}");
string cacheFile = ProgramData.AppInfoPath + @$"\{appId}.json";
if (response is not null)
{
IDictionary<string, JToken> apps = (dynamic)JsonConvert.DeserializeObject(response);
foreach (KeyValuePair<string, JToken> app in apps)
{
try
{
AppData data = JsonConvert.DeserializeObject<AppDetails>(app.Value.ToString()).data;
try
{
File.WriteAllText(cacheFile, JsonConvert.SerializeObject(data, Formatting.Indented));
}
catch { }
return data;
}
catch (Exception e)
{
new DialogForm(null).Show(SystemIcons.Error, "Unsuccessful deserialization of query for appid " + appId + ":\n\n" + e.ToString(), "FUCK");
}
}
}
if (Directory.Exists(Directory.GetDirectoryRoot(cacheFile)) && File.Exists(cacheFile))
{
try
{
return JsonConvert.DeserializeObject<AppData>(File.ReadAllText(cacheFile));
}
catch { }
}
return null;
}
}

View file

@ -1,9 +1,8 @@
using System;
using System.Drawing;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using HtmlAgilityPack;
@ -15,21 +14,17 @@ internal static class HttpClientManager
internal static void Setup()
{
HttpClient = new();
HttpClient.DefaultRequestHeaders.Add("User-Agent", $"CreamInstaller-{Environment.MachineName}_{Environment.UserDomainName}_{Environment.UserName}");
HttpClient.DefaultRequestHeaders.Add("User-Agent", $"{Environment.MachineName}CI{Application.ProductVersion.Replace(".", "")}{Environment.UserName}");
}
internal static async Task<HtmlDocument> Get(string url)
internal static async Task<string> EnsureGet(string url)
{
try
{
using HttpRequestMessage request = new(HttpMethod.Get, url);
using HttpResponseMessage response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
using StreamReader reader = new(stream, Encoding.UTF8);
HtmlDocument document = new();
document.LoadHtml(reader.ReadToEnd());
return document;
return await response.Content.ReadAsStringAsync();
}
catch
{
@ -37,7 +32,14 @@ internal static class HttpClientManager
}
}
internal static async Task<HtmlNodeCollection> GetDocumentNodes(string url, string xpath) => (await Get(url))?.DocumentNode?.SelectNodes(xpath);
internal static HtmlAgilityPack.HtmlDocument ToHtmlDocument(this string html)
{
HtmlAgilityPack.HtmlDocument document = new();
document.LoadHtml(html);
return document;
}
internal static async Task<HtmlNodeCollection> GetDocumentNodes(string url, string xpath) => (await EnsureGet(url))?.ToHtmlDocument()?.DocumentNode?.SelectNodes(xpath);
internal static async Task<Image> GetImageFromUrl(string url)
{

View file

@ -14,7 +14,7 @@ internal static class ProgramData
internal static readonly string AppInfoPath = DirectoryPath + @"\appinfo";
internal static readonly string AppInfoVersionPath = AppInfoPath + @"\version.txt";
internal static readonly Version MinimumAppInfoVersion = Version.Parse("2.4.0.0");
internal static readonly Version MinimumAppInfoVersion = Version.Parse("3.2.0.0");
internal static async Task Setup() => await Task.Run(() =>
{