Last active
August 17, 2024 11:41
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Creates shortcuts of your whole installed steam library in a folder on your destop | |
// 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 interal 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. Similarily 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 | |
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 FileStream(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 BinaryReader(inputStream); | |
uint magic = reader.ReadUInt32(); | |
if (magic != 0x07564428) | |
{ | |
throw new ApplicationException("Cache file is of unsupported format"); | |
} | |
MemoryStream ms = new MemoryStream(); | |
XmlWriterSettings writerSettings = new XmlWriterSettings {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)); | |
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 BinaryReader(new MemoryStream(data, false))) | |
{ | |
int state = dataReader.ReadInt32(); | |
DateTime lastChange = new DateTime((dataReader.ReadUInt32() + 62135596800)*TimeSpan.TicksPerSecond, | |
DateTimeKind.Utc); | |
long accessToken = dataReader.ReadInt64(); | |
byte[] sha1_text = 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(sha1_text).Replace("-", "")); | |
DumpAppDataCacheSections(dataReader, writer); | |
} | |
writer.WriteEndElement(); | |
} | |
writer.WriteEndElement(); | |
} | |
private static void DumpAppDataCacheSections(BinaryReader reader, XmlWriter writer) | |
{ | |
writer.WriteStartElement("Section"); | |
writer.WriteAttributeString("type", "section"); | |
DumpKeyValues(reader, writer); | |
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) | |
{ | |
for (byte valueType = reader.ReadByte(); valueType != 8; valueType = reader.ReadByte()) | |
{ | |
string name = ReadString(reader); | |
if (valueType == 0) | |
{ | |
writer.WriteStartElement("Key"); | |
if (!string.IsNullOrEmpty(name)) | |
{ | |
writer.WriteAttributeString("name", name); | |
} | |
DumpKeyValues(reader, writer); | |
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 MemoryStream()) | |
{ | |
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 StringBuilder(); | |
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.Substring(12, x.Length - 16)); | |
} | |
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 == null ? null : 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> bla = data.Descendants() | |
.Single(x => x.Name == "Application" && x.Attribute("id").Value == id) | |
.Descendants().ToList(); | |
string name = GetCacheValue(bla, "name"); | |
string icon = GetCacheValue(bla, "clienticon"); | |
name = name.Replace("/", "-") | |
.Replace("\\", "-") | |
.Replace("<", "-") | |
.Replace(">", "-") | |
.Replace(":", "-") | |
.Replace("\"", "-") | |
.Replace("?", "-") | |
.Replace("*", "-"); | |
icon = 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 StringBuilder(); | |
shortcut.AppendLine("[{000214A0-0000-0000-C000-000000000046}]"); | |
shortcut.AppendLine("Prop3=19,0"); | |
shortcut.AppendLine("[InternetShortcut]"); | |
shortcut.AppendLine("IDList="); | |
shortcut.AppendLine("IconIndex=0"); | |
shortcut.AppendLine(string.Format("URL=steam://rungameid/{0}", id)); | |
shortcut.AppendLine(string.Format("IconFile={0}", icon)); | |
File.WriteAllText(name, shortcut.ToString()); | |
} | |
} | |
} | |
private static string GetSteamFolder() | |
{ | |
string steam = Registry.GetValue(@"HKEY_CLASSES_ROOT\steam\Shell\Open\Command", null, null) as string; | |
if (steam == null) | |
{ | |
return null; | |
} | |
int i = steam.IndexOf('"', 1); | |
return i <= 0 ? null : Path.GetDirectoryName(steam.Substring(1, i - 1)); | |
} | |
private static string GetShortcutsFolder() | |
{ | |
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "SteamShortcuts"); | |
} | |
//static void Main(string[] args) | |
private static void Main() | |
{ | |
SteamShortcuts.CreateShortcuts(GetSteamFolder(), GetShortcutsFolder()); | |
} | |
} |
@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
This wont compile in visual studio in command prompt.
throw new ApplicationException("Cache file is of unsupported format");
breaks the execute