Skip to content

Instantly share code, notes, and snippets.

@AndrewSav
Last active January 14, 2025 05:15
Show Gist options
  • Save AndrewSav/872b1469e11d9a4b802c to your computer and use it in GitHub Desktop.
Save AndrewSav/872b1469e11d9a4b802c to your computer and use it in GitHub Desktop.
Creates shortcuts of your whole installed steam library in a folder on your destop
// Creates shortcuts of your whole installed steam library in a folder on your desktop
// Note 1: icons for some games do not get set correctly, but than they do not get set correctly by steam itself either
// Note 2: code heavily depends on the steam internal file formats so this can stop working without notice any time
// Note 3: this code does not have any command line arguments, it tries to detect path to your steam folder
// if it can't feel free to modify it to hardcode the path. Similarly, you can change where the shortcuts
// are written to in the code. Sorry, not a user-friendly tool at all - you are assumed to be a developer
// Some links useful for fixing this, when format changes:
// https://github.com/solsticegamestudios/vdf
// https://github.com/Matoking/protontricks/blob/master/src/protontricks/steam.py
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Win32;
internal static class Program
{
private static class CacheToXml
{
public static XDocument Dump(string inputFile)
{
using FileStream inputStream = new(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read);
if (inputStream.Length < sizeof(int))
{
throw new ApplicationException("Cache file is too small");
}
if (inputStream.Length >= (50 * 1024 * 1024))
{
throw new ApplicationException("Cache file is too large");
}
BinaryReader reader = new(inputStream);
uint magic = reader.ReadUInt32();
if (magic != 0x07564429)
{
throw new ApplicationException("Cache file is of unsupported format");
}
MemoryStream ms = new();
XmlWriterSettings writerSettings = new() { Indent = true, IndentChars = "\t" };
using (XmlWriter writer = XmlWriter.Create(ms, writerSettings))
{
DumpAppDataCache(reader, writer);
}
ms.Seek(0, SeekOrigin.Begin);
return XDocument.Load(ms);
}
private static void DumpAppDataCache(BinaryReader reader, XmlWriter writer)
{
int universe = reader.ReadInt32();
writer.WriteStartElement("AppDataCache");
writer.WriteAttributeString("universe", universe.ToString(NumberFormatInfo.InvariantInfo));
List<string> keyTable = ReadKeyTable(reader);
for (int appId = reader.ReadInt32(); appId != 0; appId = reader.ReadInt32())
{
int dataSize = reader.ReadInt32();
byte[] data = reader.ReadBytes(dataSize);
if (data.Length != dataSize)
throw new EndOfStreamException();
writer.WriteStartElement("Application");
writer.WriteAttributeString("id", appId.ToString(NumberFormatInfo.InvariantInfo));
writer.WriteAttributeString("dataSize", dataSize.ToString(NumberFormatInfo.InvariantInfo));
using (BinaryReader dataReader = new(new MemoryStream(data, false)))
{
int state = dataReader.ReadInt32();
DateTime lastChange = new((dataReader.ReadUInt32() + 62135596800)*TimeSpan.TicksPerSecond,
DateTimeKind.Utc);
long accessToken = dataReader.ReadInt64();
byte[] sha1Text = dataReader.ReadBytes(20);
int changeNumber = dataReader.ReadInt32();
byte[] sha1 = dataReader.ReadBytes(20);
writer.WriteAttributeString("state", GetAppInfoState(state).ToString(NumberFormatInfo.InvariantInfo));
writer.WriteAttributeString("lastChange", lastChange.ToString("o", DateTimeFormatInfo.InvariantInfo));
writer.WriteAttributeString("accessToken", accessToken.ToString(NumberFormatInfo.InvariantInfo));
writer.WriteAttributeString("sha1", BitConverter.ToString(sha1).Replace("-", ""));
writer.WriteAttributeString("changeNumber", changeNumber.ToString(NumberFormatInfo.InvariantInfo));
writer.WriteAttributeString("sha1_text", BitConverter.ToString(sha1Text).Replace("-", ""));
DumpAppDataCacheSections(dataReader, writer, keyTable);
}
writer.WriteEndElement();
}
writer.WriteEndElement();
}
private static List<string> ReadKeyTable(BinaryReader reader)
{
List<string> result = [];
long keyTableOffset = reader.ReadInt64();
long save = reader.BaseStream.Position;
reader.BaseStream.Seek(keyTableOffset, SeekOrigin.Begin);
int keyCount = reader.ReadInt32();
for (int i = 0; i < keyCount; i++)
{
using MemoryStream ms = new();
while (true)
{
byte b = reader.ReadByte();
if (b == 0) break;
ms.WriteByte(b);
}
result.Add(Encoding.UTF8.GetString(ms.ToArray()));
}
reader.BaseStream.Position = save;
return result;
}
private static void DumpAppDataCacheSections(BinaryReader reader, XmlWriter writer, List<string> keyTable)
{
writer.WriteStartElement("Section");
writer.WriteAttributeString("type", "section");
DumpKeyValues(reader, writer, keyTable);
writer.WriteEndElement();
}
private static string GetAppInfoState(int value)
{
switch (value)
{
case 1:
return "Unavailable";
case 2:
return "Available";
default:
Trace.Fail("Unknown application info state: " + value.ToString(NumberFormatInfo.InvariantInfo));
return value.ToString(NumberFormatInfo.InvariantInfo);
}
}
private static void DumpKeyValues(BinaryReader reader, XmlWriter writer, List<string> keyTable)
{
for (byte valueType = reader.ReadByte(); valueType != 8; valueType = reader.ReadByte())
{
string name = keyTable[reader.ReadInt32()];
if (valueType == 0)
{
writer.WriteStartElement("Key");
if (!string.IsNullOrEmpty(name))
{
writer.WriteAttributeString("name", name);
}
DumpKeyValues(reader, writer, keyTable);
writer.WriteEndElement();
}
else
{
writer.WriteStartElement("Value");
writer.WriteAttributeString("name", name);
switch (valueType)
{
case 1:
string valueString = ReadString(reader);
writer.WriteAttributeString("type", "string");
writer.WriteString(valueString);
break;
case 2:
int valueInt32 = reader.ReadInt32();
writer.WriteAttributeString("type", "int32");
writer.WriteString(valueInt32.ToString(NumberFormatInfo.InvariantInfo));
break;
case 3:
float valueSingle = reader.ReadSingle();
writer.WriteAttributeString("type", "single");
writer.WriteString(valueSingle.ToString(NumberFormatInfo.InvariantInfo));
break;
case 4:
throw new NotSupportedException("Pointers cannot be encoded in the binary format.");
case 5:
string valueWString = ReadWideString(reader);
writer.WriteAttributeString("type", "wstring");
writer.WriteString(valueWString);
break;
case 6:
byte valueColorR = reader.ReadByte();
byte valueColorG = reader.ReadByte();
byte valueColorB = reader.ReadByte();
writer.WriteAttributeString("type", "color");
writer.WriteString(valueColorR.ToString(NumberFormatInfo.InvariantInfo) + " " +
valueColorG.ToString(NumberFormatInfo.InvariantInfo) + " " +
valueColorB.ToString(NumberFormatInfo.InvariantInfo));
break;
case 7:
ulong valueUInt64 = reader.ReadUInt64();
writer.WriteAttributeString("type", "uint64");
writer.WriteString(valueUInt64.ToString(NumberFormatInfo.InvariantInfo));
break;
default:
throw new NotImplementedException("The value type " + valueType +
" has not been implemented.");
}
writer.WriteEndElement();
}
}
}
private static string ReadString(BinaryReader reader)
{
byte[] buffer;
int bufferLength;
using (MemoryStream ms = new())
{
byte b;
while ((b = reader.ReadByte()) != 0)
ms.WriteByte(b);
buffer = ms.GetBuffer();
bufferLength = (int) ms.Length;
}
string s = Encoding.UTF8.GetString(buffer, 0, bufferLength);
s = s.Replace("\v", "\\v");
return s;
}
private static string ReadWideString(BinaryReader reader)
{
StringBuilder sb = new();
for (char value = (char) reader.ReadUInt16(); value != 0; value = (char) reader.ReadUInt16())
{
if (value == '\v')
sb.Append("\\v");
else
sb.Append(value);
}
return sb.ToString();
}
}
private static class SteamShortcuts
{
private static IEnumerable<string> GetInstalledAppIds(string steamFolder)
{
return Directory.GetFiles(Path.Combine(steamFolder, "steamapps"), "appmanifest_*.acf")
.Select(Path.GetFileName)
.Select(x => x[12..^4]);
}
private static string GetCacheValue(IEnumerable<XElement> l, string name)
{
XElement data =
l.FirstOrDefault(
x => x.Name == "Value" && x.Attribute("name") != null && x.Attribute("name")!.Value == name
&& x.Parent != null && x.Parent.Attribute("name") != null &&
x.Parent.Attribute("name")!.Value == "common");
return data?.Value;
}
public static void CreateShortcuts(string steamFolder, string shortcutsFolder)
{
XDocument data = CacheToXml.Dump(Path.Combine(steamFolder, @"appcache\appinfo.vdf"));
if (!Directory.Exists(shortcutsFolder))
{
Directory.CreateDirectory(shortcutsFolder!);
}
IEnumerable<string> ids = GetInstalledAppIds(steamFolder);
foreach (string id in ids)
{
List<XElement> list = data.Descendants()
.Single(x => x.Name == "Application" && x.Attribute("id")!.Value == id)
.Descendants().ToList();
string name = GetCacheValue(list, "name");
string icon = GetCacheValue(list, "clienticon");
name = name.Replace("/", "-")
.Replace("\\", "-")
.Replace("<", "-")
.Replace(">", "-")
.Replace(":", "-")
.Replace("\"", "-")
.Replace("?", "-")
.Replace("*", "-");
icon ??= name;
name = Path.Combine(shortcutsFolder, Path.ChangeExtension(name, "url"));
icon = Path.Combine(Path.Combine(steamFolder, "steam\\games"), Path.ChangeExtension(icon, "ico"));
StringBuilder shortcut = new();
shortcut.AppendLine("[{000214A0-0000-0000-C000-000000000046}]");
shortcut.AppendLine("Prop3=19,0");
shortcut.AppendLine("[InternetShortcut]");
shortcut.AppendLine("IDList=");
shortcut.AppendLine("IconIndex=0");
shortcut.AppendLine($"URL=steam://rungameid/{id}");
shortcut.AppendLine($"IconFile={icon}");
File.WriteAllText(name, shortcut.ToString());
}
}
}
private static string GetSteamFolder()
{
if (Registry.GetValue(@"HKEY_CLASSES_ROOT\steam\Shell\Open\Command", null, null) is not string steam)
{
return null;
}
int i = steam.IndexOf('"', 1);
return i <= 0 ? null : Path.GetDirectoryName(steam[1..i]);
}
private static string GetShortcutsFolder()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "SteamShortcuts");
}
private static void Main()
{
SteamShortcuts.CreateShortcuts(GetSteamFolder(), GetShortcutsFolder());
}
}
@Kreegola
Copy link

how do I lunch it?

@coololly
Copy link

This wont compile in visual studio in command prompt.

throw new ApplicationException("Cache file is of unsupported format"); breaks the execute

@AndrewSav
Copy link
Author

AndrewSav commented Sep 4, 2019

@coololly, it looks like they changed metadata format. I updated the code to reflect it.

@Kreegola, you launch it as ususal, you need to compile it first with any method you prefer, e.g. Visual Studio or dotnet command line or even with Roslyn compiler (not for the faint of heart).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment