Steam Library Shortcuts

You can help by

January 2024
Source mods under /sourcemods/, when configured correctly, show up on Steam Library after a restart. Source Mods placed in |all_source_engine_paths|, GoldSrc mods or even Source 2 mods don't show up by default.
Steam shortcuts file
Any game/mod could by added to the Steam Library by creating a shortcut. The shortcuts file for a given user are located under the directory steam_install_dir\\userdata\\, and the path ends with config. The shortcuts are stored inside this directory, in a file named shortcuts.vdf
.
Shortcut format
- string AppName
- string Exe
- string StartDir
- string Icon
- string ShortcutPath
- string LaunchOptions
- bool Hidden
- array<string> Tags
Reading the shortcuts.vdf
// 1. Read all bytes from the file at 'filepath'.
// 2. Convert the bytes to a string using the appropriate encoding.
// 3. Assign the string to 'shortcutsString'.
// 4. Return the list with the data
//
// Unicodes:
// \u0000 - NUL
// \u0001 - SOH
// \u0002 - STX
public class Shortcut
{
// adding '?' to declare as a nullable property, for example "Icon" would be null if the icon wasn't set
// public string? AppId { get; set; }
public byte[]? AppName { get; set; }
public string? Exe { get; set; }
public string? StartDir { get; set; }
public string? Icon { get; set; }
public string? ShortcutPath { get; set; }
public string? LaunchOptions { get; set; }
public bool IsHidden { get; set; }
public bool AllowDesktopConfig { get; set; }
public bool AllowOverlay { get; set; }
public bool OpenVR { get; set; }
public bool Devkit { get; set; }
public string? DevkitGameID { get; set; }
public string? DevkitOverrideAppID { get; set; }
// public string? LastPlayTime { get; set; }
public string? FlatpakAppID { get; set; }
public List<string> Tags { get; set; } = new List<string>();
}
public static List<Shortcut> ReadAndReturn(string filepath)
{
string shortcutsString = File.ReadAllText(filepath, Encoding.GetEncoding("ISO-8859-1")); // Read the file with the correct encoding
List<Shortcut> result = new List<Shortcut>();
int start = shortcutsString.IndexOf("\u0000shortcuts\u0000") + "\u0000shortcuts\u0000".Length;
int end = shortcutsString.LastIndexOf("\u0008\u0008");
shortcutsString = shortcutsString.Substring(start, end - start);
Shortcut shortcut = null;
string word = "";
string key = "";
bool readingTags = false;
int tagId = -1;
foreach(char c in shortcutsString.ToCharArray())
{
if (c == '\u0000') {
if (word.EndsWith("\u0001AppName")) {
if (shortcut != null)
result.Add(shortcut);
// New shortcut
shortcut = new Shortcut();
key = "\u0001AppName";
}
else if (
//word == "\u0002appid" || // NOTE: there is no NUL terminated at the end of it, so it is not read correctly
word == "\u0001Exe" ||
word == "\u0001StartDir" ||
word == "\u0001icon" ||
word == "\u0001ShortcutPath" ||
word == "\u0001LaunchOptions" ||
word == "\u0002IsHidden" ||
word == "\u0002AllowDesktopConfig" ||
word == "\u0002AllowOverlay" ||
word == "\u0002OpenVR" ||
word == "\u0002Devkit" ||
word == "\u0002DevkitGameID" ||
word == "\u0002DevkitOverrideAppID" ||
//word == "\u0002LastPlayTime" || // NOTE: there is no NUL terminated at the end of it, so it is not read correctly
// Which means that this bottom one is getting picked up by the LastPlayTime
word == "\u0001FlatpakAppID"
) {
key = word;
}
else if (word == "tags") {
readingTags = true;
}
else if (key != "") {
switch (key) {
//case "\u0002appid": shortcut.AppId = word; break;
case "\u0001AppName": shortcut.AppName = word; break;
case "\u0001Exe": shortcut.Exe = word.Trim('"'); break;
case "\u0001StartDir": shortcut.StartDir = word; break;
case "\u0001icon": shortcut.Icon = word; break;
case "\u0001ShortcutPath": shortcut.ShortcutPath = word; break;
case "\u0001LaunchOptions": shortcut.LaunchOptions = word; break;
case "\u0002IsHidden": shortcut.IsHidden = (word == "\u0001"); break;
case "\u0002AllowDesktopConfig": shortcut.AllowDesktopConfig = (word == "\u0001"); break;
case "\u0002AllowOverlay": shortcut.AllowOverlay = (word == "\u0001"); break;
case "\u0002OpenVR": shortcut.OpenVR = (word == "\u0001"); break;
case "\u0002Devkit": shortcut.Devkit = (word == "\u0001"); break;
case "\u0002DevkitGameID": shortcut.DevkitGameID = word; break;
case "\u0002DevkitOverrideAppID": shortcut.DevkitOverrideAppID = word; break;
//case "\u0002LastPlayTime": shortcut.LastPlayTime = word; break;
case "\u0001FlatpakAppID": shortcut.FlatpakAppID = word; break;
default:
break;
}
key = "";
}
else if (readingTags) {
if (word.StartsWith("\u0001")) {
tagId = int.Parse(word.Substring("\u0001".Length));
}
else if (tagId >= 0) {
shortcut.Tags.Add(word);
tagId = -1;
}
else {
readingTags = false;
}
}
word = "";
}
else {
word += c;
}
}
if (shortcut != null)
result.Add(shortcut);
return result;
}
// to read and return data, use this somewhere:
List<Shortcut> shortcuts = (YourClassName.)ReadAndReturn(filePath);
Writing the shortcuts.vdf
public static string BuildShortcuts(List<Shortcut> shortcuts)
{
string shortcutsString = "\0shortcuts\0";
for (int i = 0; i < shortcuts.Count; i++)
{
shortcutsString += "\0" + i + "\0";
shortcutsString += BuildShortcut(shortcuts[i]);
shortcutsString += "\u0008";
}
shortcutsString += "\u0008\u0008";
return shortcutsString;
}
private static string BuildShortcut(Shortcut shortcut)
{
string shortcutString = "";
string appIdBytes = shortcut.AppId != null ? Encoding.Latin1.GetString(shortcut.AppId) : "\0\0\0\0";
// somewhere in the code i fucked it up and now i have to use 3 instead of a 4... Too bad!
// it should still work on adding a shortcut
string appIdStr = appIdBytes.Length >= 4 ? appIdBytes.Substring(0, 3) : appIdBytes;
shortcutString += "\u0002appid\0" + appIdStr; // AppID is a 4-byte integer, so it doesn't have a NUL terminator
shortcutString += "\u0001AppName\0" + shortcut.AppName + "\0"; // AppName
shortcutString += "\u0001Exe\0" + shortcut.Exe + "\0"; // Exe
shortcutString += "\u0001StartDir\0" + (shortcut.StartDir ?? "") + "\0"; // StartDir
shortcutString += "\u0001icon\0" + shortcut.Icon + "\0"; // Icon
shortcutString += "\u0001ShortcutPath\0" + shortcut.ShortcutPath + "\0"; // ShortcutPath
shortcutString += "\u0001LaunchOptions\0" + shortcut.LaunchOptions + "\0"; // LaunchOptions
shortcutString += "\u0002IsHidden\0" + GetBooleanChar(shortcut.IsHidden) + "\0\0\0"; // IsHidden
shortcutString += "\u0002AllowDesktopConfig\0" + GetBooleanChar(shortcut.AllowDesktopConfig) + "\0\0\0"; // AllowDesktopConfig
shortcutString += "\u0002AllowOverlay\0" + GetBooleanChar(shortcut.AllowOverlay) + "\0\0\0"; // AllowOverlay
shortcutString += "\u0002OpenVR\0" + GetBooleanChar(shortcut.OpenVR) + "\0\0\0"; // OpenVR
shortcutString += "\u0002Devkit\0" + GetBooleanChar(shortcut.Devkit) + "\0\0\0"; // Devkit
shortcutString += "\u0001DevkitGameID\0" + (shortcut.DevkitGameID ?? "") + "\0"; // DevkitGameID
shortcutString += "\u0002DevkitOverrideAppID\0" + (shortcut.DevkitOverrideAppID ?? "\0") + "\0\0\0"; // DevkitOverrideAppID
shortcutString += "\u0002LastPlayTime\0" + (shortcut.LastPlayTime ?? "") + "\0"; // LastPlayTime
shortcutString += "\u0001FlatpakAppID\0" + (shortcut.FlatpakAppID ?? "") + "\0"; // FlatpakAppID
shortcutString += buildTags(shortcut.Tags); // Tags
return shortcutString;
}
private static string buildTags(List<string> tags)
{
var tagString = "\0tags\0";
for (var i = 0; i < tags.Count; ++i)
{
tagString += "\u0001" + i + "\0" + tags[i] + "\0";
}
tagString += "\u0008";
return tagString;
}
private static char GetBooleanChar(bool value)
{
return value ? '\u0001' : '\0';
}
Get AppId for a shortcut
To display an image in the library, the app id of the shortcut must be known. What has been documented so far is that the app id is given by the following (untested) method:
var longValue = new Long(crcValue, crcValue, true);
longValue = longValue.or(0x80000000);
longValue = longValue.shl(32);
longValue = longValue.or(0x02000000);
return result.ToString();
private string GetSHA1()
{
var data = Encoding.ASCII.GetBytes(Exe);
var hashData = new SHA1Managed().ComputeHash(data);
var hash = string.Empty;
foreach (var b in hashData)
{
hash += b.ToString("X2");
}
return hash;
}
This code works, it adds an app on the steam library, Steam must be shut downed in order to append it in the library
private static uint ComputeCRC32(byte[] bytes)
{
// Your CRC32 implementation here (or use the one you have)
// Simple example:
const uint Polynomial = 0xEDB88320;
uint[] table = new uint[256];
for (uint i = 0; i < 256; i++)
{
uint temp = i;
for (int j = 0; j < 8; j++)
temp = (temp & 1) == 1 ? (Polynomial ^ (temp >> 1)) : (temp >> 1);
table[i] = temp;
}
uint crc = 0xFFFFFFFF;
foreach (byte b in bytes)
{
byte index = (byte)((crc & 0xFF) ^ b);
crc = (crc >> 8) ^ table[index];
}
return ~crc;
}
public static string GenerateEncrypedAppID(Shortcut shortcut)
{
// Combine exe + launch options
string combined = shortcut.Exe + shortcut.LaunchOptions;
// Encode in Windows-1252 (Steam uses ANSI encoding)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
byte[] data = Encoding.GetEncoding("Windows-1252").GetBytes(combined);
// Compute CRC32 of combined string
uint crc = ComputeCRC32(data);
// Set the shortcut flag bit
uint encodedValue = crc | 0x80000000;
string hexString = encodedValue.ToString("X8");
byte[] bytes = new byte[4];
for (int i = 0; i < 4; i++)
bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
// Convert byte array to a string where each char represents one byte
string binaryString = Encoding.Latin1.GetString(bytes);
return binaryString;
}
Main program for writing
class Program
{
static void Main()
{
string exe = @"""A:\Path\to\your.exe""";
Shortcut shortcut = new Shortcut
{
Exe = exe,
LaunchOptions = "",
AppName = "App name",
StartDir = @"A:\Path\to\startDir\"
};
string encrypedValue = GenerateEncrypedAppID(shortcut);
byte[] appIdRawBytes = Encoding.Latin1.GetBytes(encrypedValue);
List<Shortcut> shortcuts = new List<Shortcut> {
new Shortcut
{
AppId = appIdRawBytes,
AppName = "App name",
Exe = exe,
StartDir = @"A:\Path\to\startDir\",
Icon = "",
ShortcutPath = "",
LaunchOptions = "",
IsHidden = false,
AllowDesktopConfig = true,
AllowOverlay = true,
OpenVR = false,
Devkit = false,
DevkitGameID = null,
DevkitOverrideAppID = null,
FlatpakAppID = null,
}
};
string getStructureData = BuildShortcuts(shortcuts);
File.WriteAllText(@"STEAMROOT\userdata\<user id>\config\shortcuts.vdf", getStructureData);
Console.WriteLine("Proccess done, you can safely close the window.");
}
}