Convert SteamCMD VDF to JSON to prepare for compatibility with web API

This commit is contained in:
pointfeev 2024-07-21 21:35:25 -04:00
parent a198b7f9e5
commit eb8c75d249
8 changed files with 172 additions and 118 deletions

View file

@ -154,6 +154,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Gameloop.Vdf" Version="0.6.2" />
<PackageReference Include="Gameloop.Vdf.JsonConverter" Version="0.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ServiceModel.Primitives" Version="8.0.0" />
</ItemGroup>

View file

@ -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<SelectionDLC, byte> dlc = new();
List<Task> dlcTasks = new();
HashSet<string> dlcIds = new();
if (appData is not null)
foreach (string dlcId in await SteamStore.ParseDlcAppIds(appData))
List<Task> dlcTasks = [];
HashSet<string> 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

View file

@ -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<string, dynamic> 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<string, CmdAppData> Data { get; set; }
}

View file

@ -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<VProperty> GetAppInfo(string appId, string branch = "public", int buildId = 0)
internal static async Task<CmdAppData> 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<CmdAppData>() 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<string> dlcAppIds = await ParseDlcAppIds(appInfo);
return appData;
HashSet<string> 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<HashSet<string>> ParseDlcAppIds(VProperty appInfo)
internal static async Task<HashSet<string>> ParseDlcAppIds(CmdAppData appData)
=> await Task.Run(() =>
{
HashSet<string> 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(","))
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);
}
VToken depots = appInfo.Value.GetChild("depots");
Dictionary<string, dynamic> 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);
}

View file

@ -18,20 +18,20 @@ internal static class SteamStore
private const int CooldownGame = 600;
private const int CooldownDlc = 1200;
internal static async Task<HashSet<string>> ParseDlcAppIds(AppData appData)
internal static async Task<HashSet<string>> ParseDlcAppIds(StoreAppData storeAppData)
=> await Task.Run(() =>
{
HashSet<string> 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<AppData> QueryStoreAPI(string appId, bool isDlc = false, int attempts = 0)
internal static async Task<StoreAppData> QueryStoreAPI(string appId, bool isDlc = false, int attempts = 0)
{
while (!Program.Canceled)
{
@ -50,11 +50,12 @@ internal static class SteamStore
foreach (KeyValuePair<string, JToken> app in apps)
try
{
AppDetails appDetails = JsonConvert.DeserializeObject<AppDetails>(app.Value.ToString());
if (appDetails is not null)
StoreAppDetails storeAppDetails =
JsonConvert.DeserializeObject<StoreAppDetails>(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<AppData>(cacheFile.ReadFile());
return JsonConvert.DeserializeObject<StoreAppData>(cacheFile.ReadFile());
}
catch
{

View file

@ -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<int> DLC { get; set; }
@ -40,10 +40,10 @@ public class AppData
public List<int> 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; }
}

View file

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

View file

@ -75,7 +75,7 @@ internal static class SafeIO
Form form = null)
{
if (!directoryPath.DirectoryExists())
return Enumerable.Empty<string>();
return [];
while (!Program.Canceled)
try
{
@ -92,7 +92,7 @@ internal static class SafeIO
break;
}
return Enumerable.Empty<string>();
return [];
}
internal static IEnumerable<string> 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<string>();
return [];
while (!Program.Canceled)
try
{
@ -117,7 +117,7 @@ internal static class SafeIO
break;
}
return Enumerable.Empty<string>();
return [];
}
internal static bool FileExists(this string filePath) => File.Exists(filePath);