diff --git a/CreamInstaller/CreamInstaller.csproj b/CreamInstaller/CreamInstaller.csproj index 6275e9f..6bdcedc 100644 --- a/CreamInstaller/CreamInstaller.csproj +++ b/CreamInstaller/CreamInstaller.csproj @@ -154,6 +154,7 @@ + diff --git a/CreamInstaller/Forms/SelectForm.cs b/CreamInstaller/Forms/SelectForm.cs index a0a03c6..a4b370e 100644 --- a/CreamInstaller/Forms/SelectForm.cs +++ b/CreamInstaller/Forms/SelectForm.cs @@ -201,10 +201,10 @@ internal sealed partial class SelectForm : CustomForm if (Program.Canceled) return; - AppData appData = await SteamStore.QueryStoreAPI(appId); + StoreAppData storeAppData = await SteamStore.QueryStoreAPI(appId); _ = Interlocked.Decrement(ref steamGamesToCheck); - VProperty appInfo = await SteamCMD.GetAppInfo(appId, branch, buildId); - if (appData is null && appInfo is null) + CmdAppData cmdAppData = await SteamCMD.GetAppInfo(appId, branch, buildId); + if (storeAppData is null && cmdAppData is null) { RemoveFromRemainingGames(name); return; @@ -213,13 +213,13 @@ internal sealed partial class SelectForm : CustomForm if (Program.Canceled) return; ConcurrentDictionary dlc = new(); - List dlcTasks = new(); - HashSet dlcIds = new(); - if (appData is not null) - foreach (string dlcId in await SteamStore.ParseDlcAppIds(appData)) + List dlcTasks = []; + HashSet dlcIds = []; + if (storeAppData is not null) + foreach (string dlcId in await SteamStore.ParseDlcAppIds(storeAppData)) _ = dlcIds.Add(dlcId); - if (appInfo is not null) - foreach (string dlcId in await SteamCMD.ParseDlcAppIds(appInfo)) + if (cmdAppData is not null) + foreach (string dlcId in await SteamCMD.ParseDlcAppIds(cmdAppData)) _ = dlcIds.Add(dlcId); if (dlcIds.Count > 0) foreach (string dlcAppId in dlcIds) @@ -240,31 +240,27 @@ internal sealed partial class SelectForm : CustomForm string dlcName = null; string dlcIcon = null; bool onSteamStore = false; - AppData dlcAppData = await SteamStore.QueryStoreAPI(dlcAppId, true); - if (dlcAppData is not null) + StoreAppData dlcStoreAppData = await SteamStore.QueryStoreAPI(dlcAppId, true); + if (dlcStoreAppData is not null) { - dlcName = dlcAppData.Name; - dlcIcon = dlcAppData.HeaderImage; + dlcName = dlcStoreAppData.Name; + dlcIcon = dlcStoreAppData.HeaderImage; onSteamStore = true; - fullGameAppId = dlcAppData.FullGame?.AppId; + fullGameAppId = dlcStoreAppData.FullGame?.AppId; } else { - VProperty dlcAppInfo = await SteamCMD.GetAppInfo(dlcAppId); - if (dlcAppInfo is not null) + CmdAppData dlcCmdAppData = await SteamCMD.GetAppInfo(dlcAppId); + if (dlcCmdAppData 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(); + dlcName = dlcCmdAppData.Common?.Name; + string dlcIconStaticId = dlcCmdAppData.Common?.Icon; + dlcIconStaticId ??= dlcCmdAppData.Common?.LogoSmall; + dlcIconStaticId ??= dlcCmdAppData.Common?.Logo; if (dlcIconStaticId is not null) dlcIcon = IconGrabber.SteamAppImagesPath + @$"\{dlcAppId}\{dlcIconStaticId}.jpg"; - fullGameAppId = dlcAppInfo.Value.GetChild("common")?.GetChild("parent") - ?.ToString(); + fullGameAppId = dlcCmdAppData.Common?.Parent; } } @@ -273,26 +269,23 @@ internal sealed partial class SelectForm : CustomForm string fullGameName = null; string fullGameIcon = null; bool fullGameOnSteamStore = false; - AppData fullGameAppData = await SteamStore.QueryStoreAPI(fullGameAppId, true); - if (fullGameAppData is not null) + StoreAppData fullGameStoreAppData = + await SteamStore.QueryStoreAPI(fullGameAppId, true); + if (fullGameStoreAppData is not null) { - fullGameName = fullGameAppData.Name; - fullGameIcon = fullGameAppData.HeaderImage; + fullGameName = fullGameStoreAppData.Name; + fullGameIcon = fullGameStoreAppData.HeaderImage; fullGameOnSteamStore = true; } else { - VProperty fullGameAppInfo = await SteamCMD.GetAppInfo(fullGameAppId); + CmdAppData fullGameAppInfo = await SteamCMD.GetAppInfo(fullGameAppId); if (fullGameAppInfo is not null) { - fullGameName = fullGameAppInfo.Value.GetChild("common")?.GetChild("name") - ?.ToString(); - string fullGameIconStaticId = fullGameAppInfo.Value.GetChild("common") - ?.GetChild("icon")?.ToString(); - fullGameIconStaticId ??= fullGameAppInfo.Value.GetChild("common") - ?.GetChild("logo_small")?.ToString(); - fullGameIconStaticId ??= fullGameAppInfo.Value.GetChild("common") - ?.GetChild("logo")?.ToString(); + fullGameName = fullGameAppInfo.Common?.Name; + string fullGameIconStaticId = fullGameAppInfo.Common?.Icon; + fullGameIconStaticId ??= fullGameAppInfo.Common?.LogoSmall; + fullGameIconStaticId ??= fullGameAppInfo.Common?.Logo; if (fullGameIconStaticId is not null) dlcIcon = IconGrabber.SteamAppImagesPath + @$"\{fullGameAppId}\{fullGameIconStaticId}.jpg"; @@ -345,17 +338,15 @@ internal sealed partial class SelectForm : CustomForm return; } - Selection selection = Selection.GetOrCreate(Platform.Steam, appId, appData?.Name ?? name, + Selection selection = Selection.GetOrCreate(Platform.Steam, appId, storeAppData?.Name ?? name, gameDirectory, dllDirectories, await gameDirectory.GetExecutableDirectories(true)); selection.Product = "https://store.steampowered.com/app/" + appId; - selection.Icon = IconGrabber.SteamAppImagesPath + - @$"\{appId}\{appInfo?.Value.GetChild("common")?.GetChild("icon")}.jpg"; - selection.SubIcon = appData?.HeaderImage ?? IconGrabber.SteamAppImagesPath - + @$"\{appId}\{appInfo?.Value.GetChild("common")?.GetChild("clienticon")}.ico"; - selection.Publisher = appData?.Publishers[0] ?? - appInfo?.Value.GetChild("extended")?.GetChild("publisher")?.ToString(); - selection.Website = appData?.Website; + selection.Icon = IconGrabber.SteamAppImagesPath + @$"\{appId}\{cmdAppData?.Common?.Icon}.jpg"; + selection.SubIcon = storeAppData?.HeaderImage ?? IconGrabber.SteamAppImagesPath + + @$"\{appId}\{cmdAppData?.Common?.ClientIcon}.ico"; + selection.Publisher = storeAppData?.Publishers[0] ?? cmdAppData?.Extended?.Publisher; + selection.Website = storeAppData?.Website; if (Program.Canceled) return; Invoke(delegate diff --git a/CreamInstaller/Platforms/Steam/CmdAppDetails.cs b/CreamInstaller/Platforms/Steam/CmdAppDetails.cs new file mode 100644 index 0000000..a9b9464 --- /dev/null +++ b/CreamInstaller/Platforms/Steam/CmdAppDetails.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CreamInstaller.Platforms.Steam; + +public class CmdAppCommon +{ + [JsonProperty(PropertyName = "type")] public string Type { get; set; } + + [JsonProperty(PropertyName = "name")] public string Name { get; set; } + + [JsonProperty(PropertyName = "icon")] public string Icon { get; set; } + + [JsonProperty(PropertyName = "clienticon")] + public string ClientIcon { get; set; } + + [JsonProperty(PropertyName = "logo_small")] + public string LogoSmall { get; set; } + + [JsonProperty(PropertyName = "logo")] public string Logo { set; get; } + + [JsonProperty(PropertyName = "parent")] + public string Parent { set; get; } +} + +public class CmdAppExtended +{ + [JsonProperty(PropertyName = "listofdlc")] + public string Dlc { get; set; } + + [JsonProperty(PropertyName = "publisher")] + public string Publisher { get; set; } +} + +public class CmdAppData +{ + [JsonProperty(PropertyName = "common")] + public CmdAppCommon Common { get; set; } + + [JsonProperty(PropertyName = "depots")] + public Dictionary Depots { get; set; } + + [JsonProperty(PropertyName = "extended")] + public CmdAppExtended Extended { get; set; } +} + +public class CmdAppDetails +{ + [JsonProperty(PropertyName = "status")] + public string Status { get; set; } + + [JsonProperty(PropertyName = "data")] public Dictionary Data { get; set; } +} \ No newline at end of file diff --git a/CreamInstaller/Platforms/Steam/SteamCMD.cs b/CreamInstaller/Platforms/Steam/SteamCMD.cs index c5559b2..7290d6c 100644 --- a/CreamInstaller/Platforms/Steam/SteamCMD.cs +++ b/CreamInstaller/Platforms/Steam/SteamCMD.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using CreamInstaller.Resources; using CreamInstaller.Utility; +using Gameloop.Vdf.JsonConverter; using Gameloop.Vdf.Linq; #if DEBUG using CreamInstaller.Forms; @@ -17,7 +18,7 @@ using CreamInstaller.Forms; namespace CreamInstaller.Platforms.Steam; -internal static class SteamCMD +internal static partial class SteamCMD { private const int ProcessLimit = 20; @@ -32,10 +33,6 @@ internal static class SteamCMD private static readonly string DllPath = DirectoryPath + @"\steamclient.dll"; private static readonly string AppCachePath = DirectoryPath + @"\appcache"; - private static readonly string ConfigPath = DirectoryPath + @"\config"; - private static readonly string DumpsPath = DirectoryPath + @"\dumps"; - private static readonly string LogsPath = DirectoryPath + @"\logs"; - private static readonly string SteamAppsPath = DirectoryPath + @"\steamapps"; private static string DirectoryPath => ProgramData.DirectoryPath; internal static string AppInfoPath => ProgramData.AppInfoPath; @@ -179,22 +176,7 @@ internal static class SteamCMD await Kill(); try { - if (ConfigPath.DirectoryExists()) - foreach (string file in ConfigPath.EnumerateDirectory("*.tmp")) - file.DeleteFile(); - foreach (string file in DirectoryPath.EnumerateDirectory("*.old")) - file.DeleteFile(); - foreach (string file in DirectoryPath.EnumerateDirectory("*.delete")) - file.DeleteFile(); - foreach (string file in DirectoryPath.EnumerateDirectory("*.crash")) - file.DeleteFile(); - foreach (string file in DirectoryPath.EnumerateDirectory("*.ntfs_transaction_failed")) - file.DeleteFile(); - AppCachePath - .DeleteDirectory(); // this is definitely needed, so SteamCMD gets the latest information for us - DumpsPath.DeleteDirectory(); - LogsPath.DeleteDirectory(); - SteamAppsPath.DeleteDirectory(); // this is just a useless folder created from +app_update 4 + AppCachePath.DeleteDirectory(); } catch { @@ -202,7 +184,7 @@ internal static class SteamCMD } }); - internal static async Task GetAppInfo(string appId, string branch = "public", int buildId = 0) + internal static async Task GetAppInfo(string appId, string branch = "public", int buildId = 0) { int attempts = 0; while (!Program.Canceled) @@ -254,18 +236,49 @@ internal static class SteamCMD continue; } - if (!appInfo.Value.Children().Any()) - return appInfo; - VToken type = appInfo.Value.GetChild("common")?.GetChild("type"); - if (type is not null && type.ToString() != "Game") - return appInfo; - string buildid = appInfo.Value.GetChild("depots")?.GetChild("branches")?.GetChild(branch) - ?.GetChild("buildid")?.ToString(); + CmdAppData appData; + try + { + if (appInfo.ToJson().Value.ToObject() is not { } cmdAppData) + { + appUpdateFile.DeleteFile(); +#if DEBUG + DebugForm.Current.Log( + "SteamCMD query failed on attempt #" + attempts + " for " + appId + " (" + branch + + "): VDF-JSON conversion failed", + LogTextBox.Warning); +#endif + continue; + } + + appData = cmdAppData; + } + catch +#if DEBUG + (Exception e) +#endif + { + appUpdateFile.DeleteFile(); +#if DEBUG + DebugForm.Current.Log( + "SteamCMD query failed on attempt #" + attempts + " for " + appId + " (" + branch + + "): VDF-JSON conversion failed (" + e.Message + ")", + LogTextBox.Warning); +#endif + continue; + } + + string type = appData.Common?.Type; + if (type is not null && type != "Game") + return appData; + if (appData.Depots is null || !appData.Depots.TryGetValue("branches", out dynamic appBranch)) + return appData; + string buildid = appBranch?[branch]?.buildid; if (buildid is null && type is not null) - return appInfo; + return appData; if (type is not null && (!int.TryParse(buildid, out int gamebuildId) || gamebuildId >= buildId)) - return appInfo; - HashSet dlcAppIds = await ParseDlcAppIds(appInfo); + return appData; + HashSet dlcAppIds = await ParseDlcAppIds(appData); foreach (string dlcAppUpdateFile in dlcAppIds.Select(id => $@"{AppInfoPath}\{id}.vdf")) dlcAppUpdateFile.DeleteFile(); appUpdateFile.DeleteFile(); @@ -279,30 +292,27 @@ internal static class SteamCMD return null; } - internal static async Task> ParseDlcAppIds(VProperty appInfo) + internal static async Task> ParseDlcAppIds(CmdAppData appData) => await Task.Run(() => { HashSet dlcIds = []; - if (Program.Canceled || appInfo is null) + if (Program.Canceled || appData is null) return dlcIds; - VToken extended = appInfo.Value.GetChild("extended"); - if (extended is not null) - foreach (VToken vToken in extended.Where(p => p is VProperty { Key: "listofdlc" })) - { - VProperty property = (VProperty)vToken; - foreach (string id in property.Value.ToString().Split(",")) - if (int.TryParse(id, out int appId) && appId > 0) - _ = dlcIds.Add("" + appId); - } - VToken depots = appInfo.Value.GetChild("depots"); + CmdAppExtended extended = appData.Extended; + if (extended?.Dlc != null) + foreach (string id in extended.Dlc.Split(",")) + if (int.TryParse(id, out int appId) && appId > 0) + _ = dlcIds.Add("" + appId); + + Dictionary depots = appData.Depots; if (depots is null) return dlcIds; - foreach (VToken vToken in depots.Where( - p => p is VProperty property && int.TryParse(property.Key, out int _))) + + foreach ((_, dynamic depot) in depots.Where(p => int.TryParse(p.Key, out _))) { - VProperty property = (VProperty)vToken; - if (int.TryParse(property.Value.GetChild("dlcappid")?.ToString(), out int appId) && appId > 0) + string dlcAppId = depot.dlcappid; + if (dlcAppId is not null && int.TryParse(dlcAppId, out int appId) && appId > 0) _ = dlcIds.Add("" + appId); } diff --git a/CreamInstaller/Platforms/Steam/SteamStore.cs b/CreamInstaller/Platforms/Steam/SteamStore.cs index 2f00978..34914b5 100644 --- a/CreamInstaller/Platforms/Steam/SteamStore.cs +++ b/CreamInstaller/Platforms/Steam/SteamStore.cs @@ -18,20 +18,20 @@ internal static class SteamStore private const int CooldownGame = 600; private const int CooldownDlc = 1200; - internal static async Task> ParseDlcAppIds(AppData appData) + internal static async Task> ParseDlcAppIds(StoreAppData storeAppData) => await Task.Run(() => { HashSet dlcIds = new(); - if (appData.DLC is null) + if (storeAppData.DLC is null) return dlcIds; - foreach (string dlcId in from appId in appData.DLC + foreach (string dlcId in from appId in storeAppData.DLC where appId > 0 select appId.ToString(CultureInfo.InvariantCulture)) _ = dlcIds.Add(dlcId); return dlcIds; }); - internal static async Task QueryStoreAPI(string appId, bool isDlc = false, int attempts = 0) + internal static async Task QueryStoreAPI(string appId, bool isDlc = false, int attempts = 0) { while (!Program.Canceled) { @@ -50,11 +50,12 @@ internal static class SteamStore foreach (KeyValuePair app in apps) try { - AppDetails appDetails = JsonConvert.DeserializeObject(app.Value.ToString()); - if (appDetails is not null) + StoreAppDetails storeAppDetails = + JsonConvert.DeserializeObject(app.Value.ToString()); + if (storeAppDetails is not null) { - AppData data = appDetails.Data; - if (!appDetails.Success) + StoreAppData data = storeAppDetails.Data; + if (!storeAppDetails.Success) { #if DEBUG DebugForm.Current.Log( @@ -123,21 +124,19 @@ internal static class SteamStore + ": Response deserialization null"); #endif } - else - { #if DEBUG + else DebugForm.Current.Log( "Steam store query failed on attempt #" + attempts + " for " + appId + (isDlc ? " (DLC)" : "") + ": Response null", LogTextBox.Warning); #endif - } } if (cachedExists) try { - return JsonConvert.DeserializeObject(cacheFile.ReadFile()); + return JsonConvert.DeserializeObject(cacheFile.ReadFile()); } catch { diff --git a/CreamInstaller/Platforms/Steam/AppDetails.cs b/CreamInstaller/Platforms/Steam/StoreAppDetails.cs similarity index 84% rename from CreamInstaller/Platforms/Steam/AppDetails.cs rename to CreamInstaller/Platforms/Steam/StoreAppDetails.cs index 114482f..903965a 100644 --- a/CreamInstaller/Platforms/Steam/AppDetails.cs +++ b/CreamInstaller/Platforms/Steam/StoreAppDetails.cs @@ -3,14 +3,14 @@ using Newtonsoft.Json; namespace CreamInstaller.Platforms.Steam; -public class AppFullGame +public class StoreAppFullGame { [JsonProperty(PropertyName = "appid")] public string AppId { get; set; } [JsonProperty(PropertyName = "name")] public string Name { get; set; } } -public class AppData +public class StoreAppData { [JsonProperty(PropertyName = "type")] public string Type { get; set; } @@ -20,7 +20,7 @@ public class AppData public int SteamAppId { get; set; } [JsonProperty(PropertyName = "fullgame")] - public AppFullGame FullGame { get; set; } + public StoreAppFullGame FullGame { get; set; } [JsonProperty(PropertyName = "dlc")] public List DLC { get; set; } @@ -40,10 +40,10 @@ public class AppData public List Packages { get; set; } } -public class AppDetails +public class StoreAppDetails { [JsonProperty(PropertyName = "success")] public bool Success { get; set; } - [JsonProperty(PropertyName = "data")] public AppData Data { get; set; } + [JsonProperty(PropertyName = "data")] public StoreAppData Data { get; set; } } \ No newline at end of file diff --git a/CreamInstaller/Utility/ProgramData.cs b/CreamInstaller/Utility/ProgramData.cs index 496bddc..1e80c9a 100644 --- a/CreamInstaller/Utility/ProgramData.cs +++ b/CreamInstaller/Utility/ProgramData.cs @@ -111,7 +111,7 @@ internal static class ProgramData // ignored } - return Enumerable.Empty<(Platform platform, string id)>(); + return []; } internal static void WriteProgramChoices(IEnumerable<(Platform platform, string id)> choices) @@ -144,7 +144,7 @@ internal static class ProgramData // ignored } - return Enumerable.Empty<(Platform platform, string gameId, string dlcId)>(); + return []; } internal static void WriteDlcChoices(List<(Platform platform, string gameId, string dlcId)> choices) @@ -177,7 +177,7 @@ internal static class ProgramData // ignored } - return Enumerable.Empty<(Platform platform, string id, string proxy, bool enabled)>(); + return []; } internal static void WriteProxyChoices( diff --git a/CreamInstaller/Utility/SafeIO.cs b/CreamInstaller/Utility/SafeIO.cs index 2334b80..9b71845 100644 --- a/CreamInstaller/Utility/SafeIO.cs +++ b/CreamInstaller/Utility/SafeIO.cs @@ -75,7 +75,7 @@ internal static class SafeIO Form form = null) { if (!directoryPath.DirectoryExists()) - return Enumerable.Empty(); + return []; while (!Program.Canceled) try { @@ -92,7 +92,7 @@ internal static class SafeIO break; } - return Enumerable.Empty(); + return []; } internal static IEnumerable EnumerateSubdirectories(this string directoryPath, string directoryPattern, @@ -100,7 +100,7 @@ internal static class SafeIO bool crucial = false, Form form = null) { if (!directoryPath.DirectoryExists()) - return Enumerable.Empty(); + return []; while (!Program.Canceled) try { @@ -117,7 +117,7 @@ internal static class SafeIO break; } - return Enumerable.Empty(); + return []; } internal static bool FileExists(this string filePath) => File.Exists(filePath);