Steam Library Shortcuts: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
mNo edit summary
(Fix read shortcuts, old code)
 
(12 intermediate revisions by the same user not shown)
Line 1: Line 1:
{{Multiple issues|
{{LanguageBar}}
{{Dead end|date=January 2024}}
{{Orphan|date=January 2024}}
{{Orphan|date=January 2024}}
}}
{{tocright}}
 
Source mods under {{Code|/sourcemods/}}, when configured correctly, show up on Steam Library after a restart. Source Mods placed in {{Code|<nowiki>|all_source_engine_paths|</nowiki>}}, {{GoldSrc|1}} mods or even {{Source 2|1}} mods don't show up by default.
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 ==
== 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'''.
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 {{Code|steam_install_dir\\userdata\\}}, and the path ends with {{Code|config}}. The shortcuts are stored inside this directory, in a file named {{File|shortcuts|vdf}}.


== Shortcut format ==
== Shortcut format ==
 
This format represents every property used as of 2025.
* string AppName
<syntaxhighlight lang=csharp>
* string Exe
public class Shortcut
* string StartDir
{
* string Icon
public byte[]? AppId { get; set; }
* string ShortcutPath
public string? AppName { get; set; }
* string LaunchOptions
public string? Exe { get; set; }
* bool Hidden
public string? StartDir { get; set; }
* array<string> Tags
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 byte[]? DevkitOverrideAppID { get; set; }
public byte[]? LastPlayTime { get; set; }
public string? FlatpakAppID { get; set; }
public string? SortAs { get; set; }
public List<string> Tags { get; set; } = new List<string>();
}
</syntaxhighlight>


== Reading the shortcuts.vdf ==
== Reading the shortcuts.vdf ==
{{For|the format structure|[[Add_Non-Steam_Game#File_format|Adding Non-Steam Games]]}}
{{For|the format structure|[[Add_Non-Steam_Game#File_format|Adding Non-Steam Games]]}}
{{For|how the script is set up|[[Binary VDF|How to read VDF binary]]}}
<syntaxhighlight lang=csharp>
<syntaxhighlight lang=csharp>
// 1. Read all bytes from the file at 'filepath'.
public static List<Shortcut> ReadShortcuts(string filepath)
// 2. Convert the bytes to a string using the appropriate encoding.
{
// 3. Assign the string to 'shortcutsString'.
if (!File.Exists(filepath) || new FileInfo(filepath).Length == 0)
// 4. Return the list with the data
return [];
//
 
// Unicodes:
using var fs = File.OpenRead(filepath);
// \u0000 - NUL
using var reader = new BinaryReader(fs, Encoding.UTF8);
// \u0001 - SOH
 
// \u0002 - STX
byte rootType = reader.ReadByte();
string rootKey = ReadNullTerminatedString(reader);
 
if (rootType != 0x00 || rootKey != "shortcuts")
throw new Exception("Unexpected root key or type");
 
var shortcuts = new List<Shortcut>();
 
while (true)
{
byte type = reader.ReadByte();
if (type == 0x08) break; // end of shortcuts dictionary
 
if (type != 0x00)
throw new Exception($"Expected dictionary key (0x00), got 0x{type:X2}");
 
Shortcut shortcut = ParseShortcut(reader);
shortcuts.Add(shortcut);
}
 
return shortcuts;
}
 
private static Shortcut ParseShortcut(BinaryReader reader)
{
var sc = new Shortcut();
 
while (true)
{
byte type = reader.ReadByte();
if (type == 0x08) break; // end of this shortcut dict
 
string key = ReadNullTerminatedString(reader);


public class Shortcut
switch (type)
{
case 0x00: // nested dict, e.g. tags
if (key == "tags")
sc.Tags = ParseTags(reader);
else
SkipDictionary(reader);
break;
case 0x01: // string
string val = ReadNullTerminatedString(reader);
AssignString(sc, key, val);
break;
case 0x02: // int32
int intval = reader.ReadInt32();
AssignInt(sc, key, intval);
break;
default:
throw new Exception($"Unknown type 0x{type:X2} for key '{key}'");
}
}
return sc;
}
 
private static List<string> ParseTags(BinaryReader reader)
{
{
// adding '?' to declare as a nullable property, for example "Icon" would be null if the icon wasn't set
var tags = new List<string>();
    // public string? AppId { get; set; }
while (true)
    public string? AppName { get; set; }
{
    public string? Exe { get; set; }
byte type = reader.ReadByte();
    public string? StartDir { get; set; }
if (type == 0x08) break; // end of tags dict
    public string? Icon { get; set; }
 
    public string? ShortcutPath { get; set; }
if (type != 0x01)
    public string? LaunchOptions { get; set; }
throw new Exception($"Unexpected type in tags dict: 0x{type:X2}");
    public bool IsHidden { get; set; }
 
    public bool AllowDesktopConfig { get; set; }
string val = ReadNullTerminatedString(reader);
    public bool AllowOverlay { get; set; }
tags.Add(val);
    public bool OpenVR { get; set; }
}
    public bool Devkit { get; set; }
return tags;
    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)
private static void SkipDictionary(BinaryReader reader)
{
{
    string shortcutsString = File.ReadAllText(filepath, Encoding.GetEncoding("ISO-8859-1")); // Read the file with the correct encoding
while (true)
{
byte type = reader.ReadByte();
if (type == 0x08) break; // end of dictionary
 
switch (type)
{
case 0x00:
SkipDictionary(reader);
break;
case 0x01:
ReadNullTerminatedString(reader);
break;
case 0x02:
reader.ReadInt32();
break;
default:
throw new Exception($"Unknown type 0x{type:X2} while skipping dict");
}
}
}


    List<Shortcut> result = new List<Shortcut>();
private static void AssignString(Shortcut sc, string key, string val)
    int start = shortcutsString.IndexOf("\u0000shortcuts\u0000") + "\u0000shortcuts\u0000".Length;
{
    int end = shortcutsString.LastIndexOf("\u0008\u0008");
switch (key)
    shortcutsString = shortcutsString.Substring(start, end - start);
{
    Shortcut shortcut = null;
case "AppName": sc.AppName = val; break;
    string word = "";
case "Exe": sc.Exe = val; break;
    string key = "";
case "StartDir": sc.StartDir = val; break;
    bool readingTags = false;
case "icon": sc.Icon = val; break;
    int tagId = -1;
case "ShortcutPath": sc.ShortcutPath = val; break;
   
case "LaunchOptions": sc.LaunchOptions = val; break;
    foreach(char c in shortcutsString.ToCharArray())
case "FlatpakAppID": sc.FlatpakAppID = val; break;
    {
case "sortas": sc.SortAs = val; break;
        if (c == '\u0000') {
case "DevkitGameID": sc.DevkitGameID = val; break;
            if (word.EndsWith("\u0001AppName")) {
default: break; // unknown string keys ignored
                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)
private static void AssignInt(Shortcut sc, string key, int val)
        result.Add(shortcut);
{
switch (key)
{
case "appid": sc.AppId = BitConverter.GetBytes(val); break;
case "IsHidden": sc.IsHidden = val != 0; break;
case "AllowDesktopConfig": sc.AllowDesktopConfig = val != 0; break;
case "AllowOverlay": sc.AllowOverlay = val != 0; break;
case "OpenVR": sc.OpenVR = val != 0; break;
case "Devkit": sc.Devkit = val != 0; break;
case "DevkitOverrideAppID": sc.DevkitOverrideAppID = BitConverter.GetBytes(val); break;
case "LastPlayTime": sc.LastPlayTime = BitConverter.GetBytes(val); break;
default: break; // unknown int keys ignored
}
}


    return result;
private static string ReadNullTerminatedString(BinaryReader reader)
{
var bytes = new List<byte>();
while (true)
{
byte b = reader.ReadByte();
if (b == 0) break;
bytes.Add(b);
}
return Encoding.UTF8.GetString(bytes.ToArray());
}
}


// to read and return data, use this somewhere:
// to read and return data, use this somewhere:
List<Shortcut> shortcuts = (YourClassName.)ReadAndReturn(filePath);
List<Shortcut> shortcuts = (YourClassName.)ReadShortcuts(filePath);
</syntaxhighlight>
</syntaxhighlight>


== Writing the shortcuts.vdf ==
== Writing the shortcuts.vdf ==
This should correctly add images with the id.
It must be added with bytes, or Steam will generate a random id for images.
<syntaxhighlight lang=csharp>
<syntaxhighlight lang=csharp>
  private static string BuildShortcuts(List<Shortcut> shortcuts)
public static byte[] BuildShortcuts(List<Shortcut> shortcuts)
  {
{
      string shortcutsString = "\u0000shortcuts\u0000";
var seenAppIds = new HashSet<string>();
      for (int i = 0; i < shortcuts.Count; i++)
 
      {
for (int i = 0; i < shortcuts.Count; i++)
          shortcutsString += "\u0000" + i + "\u0000";
{
          shortcutsString += BuildShortcut(shortcuts[i]);
if (shortcuts[i].AppId == null)
          shortcutsString += "\u0008";
continue;
      }
 
      shortcutsString += "\u0008\u0008";
string appIdString = BitConverter.ToString(shortcuts[i].AppId);
      return shortcutsString;
if (seenAppIds.Contains(appIdString))
  }
{
Console.WriteLine($"Warning: Duplicate AppID found: {appIdString}. Aborting shortcut build.");
return Array.Empty<byte>();
}
seenAppIds.Add(appIdString);
}
 
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms, Encoding.UTF8);
 
writer.Write((byte)0x00); // null terminator
writer.Write(Encoding.ASCII.GetBytes("shortcuts"));
writer.Write((byte)0x00);
 
if (shortcuts.Count > 0)
{
for (int i = 0; i < shortcuts.Count; i++)
{
writer.Write((byte)0x00); // null terminator
writer.Write(Encoding.ASCII.GetBytes(i.ToString()));
writer.Write((byte)0x00);
 
writer.Write(BuildShortcut(shortcuts[i]));
 
writer.Write((byte)0x08); // end of shortcut
}
}
else
{
Console.WriteLine("Warning: Empty list recived, no shortcuts to write.");
}
 
writer.Write((byte)0x08); // end of shortcuts dict
writer.Write((byte)0x08); // end of root dict
 
return ms.ToArray();
}
 
private static byte[] BuildShortcut(Shortcut shortcut)
{
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms, Encoding.UTF8);
 
// Write AppID: type (0x02), key, null terminator, then 4 raw bytes (no terminator)
writer.Write((byte)0x02); // int32 type
writer.Write(Encoding.ASCII.GetBytes("appid"));
writer.Write((byte)0x00);
if (shortcut.AppId != null && shortcut.AppId.Length == 4)
writer.Write(shortcut.AppId);
else
writer.Write(new byte[4] { 0, 0, 0, 0 });
 
WriteStringField(writer, "AppName", shortcut.AppName);
WriteStringField(writer, "Exe", shortcut.Exe);
WriteStringField(writer, "StartDir", shortcut.StartDir ?? "");
WriteStringField(writer, "icon", shortcut.Icon ?? "");
WriteStringField(writer, "ShortcutPath", shortcut.ShortcutPath ?? "");
WriteStringField(writer, "LaunchOptions", shortcut.LaunchOptions ?? "");
WriteBoolField(writer, "IsHidden", shortcut.IsHidden);
WriteBoolField(writer, "AllowDesktopConfig", shortcut.AllowDesktopConfig);
WriteBoolField(writer, "AllowOverlay", shortcut.AllowOverlay);
WriteBoolField(writer, "OpenVR", shortcut.OpenVR);
WriteBoolField(writer, "Devkit", shortcut.Devkit);
WriteStringField(writer, "DevkitGameID", shortcut.DevkitGameID ?? "");
WriteIntField(writer, "DevkitOverrideAppID", GetIntFromBytes(shortcut.DevkitOverrideAppID));
WriteIntField(writer, "LastPlayTime", GetIntFromBytes(shortcut.LastPlayTime));
WriteStringField(writer, "FlatpakAppID", shortcut.FlatpakAppID ?? "");
WriteStringField(writer, "sortas", shortcut.SortAs ?? "");
WriteTags(writer, shortcut.Tags);
 
return ms.ToArray();
}
 
// Helper to write string fields
private static void WriteStringField(BinaryWriter writer, string key, string value)
{
writer.Write((byte)0x01); // string type
writer.Write(Encoding.ASCII.GetBytes(key));
writer.Write((byte)0x00);
writer.Write(Encoding.UTF8.GetBytes(value));
writer.Write((byte)0x00);
}
 
// Helper to write bool fields (int32 with 0 or 1)
private static void WriteBoolField(BinaryWriter writer, string key, bool value)
{
writer.Write((byte)0x02); // int32 type
writer.Write(Encoding.ASCII.GetBytes(key));
writer.Write((byte)0x00);
writer.Write(value ? 1 : 0);
}
 
// Helper to write int32 fields
private static void WriteIntField(BinaryWriter writer, string key, int value)
{
writer.Write((byte)0x02); // int32 type
writer.Write(Encoding.ASCII.GetBytes(key));
writer.Write((byte)0x00);
writer.Write(value);
}
private static int GetIntFromBytes(byte[] bytes)
{
if (bytes == null || bytes.Length < 4) return 0;
return BitConverter.ToInt32(bytes, 0);
}
 
// Helper to write tags dictionary
private static void WriteTags(BinaryWriter writer, List<string> tags)
{
writer.Write((byte)0x00); // begin dict
writer.Write(Encoding.ASCII.GetBytes("tags"));
writer.Write((byte)0x00);
 
for (int i = 0; i < tags.Count; i++)
{
writer.Write((byte)0x01); // string type
writer.Write(Encoding.ASCII.GetBytes(i.ToString()));
writer.Write((byte)0x00);
writer.Write(Encoding.UTF8.GetBytes(tags[i]));
writer.Write((byte)0x00);
}
 
writer.Write((byte)0x08); // end dict
}
public static void WriteShortcuts(string filePath, List<Shortcut> shortcuts)
{
using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var writer = new BinaryWriter(fs, Encoding.UTF8);
 
// Write root dictionary start
writer.Write((byte)0x00);
writer.Write(Encoding.UTF8.GetBytes("shortcuts"));
writer.Write((byte)0x00);
 
for (int i = 0; i < shortcuts.Count; i++)
{
writer.Write((byte)0x00); // dictionary key type (string)
writer.Write(Encoding.UTF8.GetBytes(i.ToString()));
writer.Write((byte)0x00);


  private static string BuildShortcut(Shortcut shortcut)
// Write shortcut binary data
  {
var shortcutBytes = BuildShortcut(shortcuts[i]);
      string shortcutString = "";
writer.Write(shortcutBytes);
      //shortcutString += "\u0002appid\u0000" + shortcut.GetAppID() + "\u0000";
      shortcutString += "\u0001appname\u0000" + shortcut.AppName + "\u0000";
      shortcutString += "\u0001exe\u0000\"" + shortcut.Exe + "\"\u0000";
      shortcutString += "\u0001StartDir\u0000\"" + shortcut.StartDir + "\"\u0000";
      shortcutString += "\u0001icon\u0000" + shortcut.Icon + "\u0000";
      shortcutString += "\u0001ShortcutPath\u0000" + shortcut.ShortcutPath + "\u0000";
      shortcutString += "\u0001LaunchOptions\u0000" + shortcut.LaunchOptions + "\u0000";
      shortcutString += "\u0002hidden\u0000" + (shortcut.Hidden ? "\u0001" : "\u0000") + "\u0000\u0000\u0000";
      shortcutString += buildTags(shortcut.Tags);
      return shortcutString;
  }


  private static string buildTags(List<string> tags)
writer.Write((byte)0x08); // end of shortcut dictionary
  {
}
      var tagString = "\u0000tags\u0000";
 
      for (var i = 0; i < tags.Count; ++i)
// Write final end markers for shortcuts dictionary
      {
writer.Write((byte)0x08);
          tagString += "\u0001" + i + "\u0000" + tags[i] + "\u0000";
writer.Write((byte)0x08);
      }
}
      tagString += "\u0008";
      return tagString;
  }
</syntaxhighlight>
</syntaxhighlight>


== Get AppId for a shortcut ==
== 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:
To display an image in the library, the app id of the shortcut must be known. What has been tested so far is that the app id is given by the following method:
 
In order to add a steam short cut, Steam must be shut downed or it wont add the shortcut.
<syntaxhighlight lang=csharp>
<syntaxhighlight lang=csharp>
  var longValue = new Long(crcValue, crcValue, true);
private static uint ComputeCRC32(byte[] bytes)
  longValue = longValue.or(0x80000000);
{
  longValue = longValue.shl(32);
const uint Polynomial = 0xEDB88320;
  longValue = longValue.or(0x02000000);
uint[] table = new uint[256];
  return result.ToString();
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 uint GenerateAppID(Shortcut shortcut)
{
string combined = shortcut.AppName + shortcut.Exe + "\0";
 
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
byte[] data = Encoding.GetEncoding("Windows-1252").GetBytes(combined);
 
uint crc = ComputeCRC32(data);
return crc | 0x80000000;
}


  private string GetSHA1()
public static string GenerateEncryptedAppID(Shortcut shortcut)
  {
{
      var data = Encoding.ASCII.GetBytes(Exe);
uint appid = GenerateAppID(shortcut);
      var hashData = new SHA1Managed().ComputeHash(data);
 
      var hash = string.Empty;
byte[] bytes = BitConverter.GetBytes(appid);
      foreach (var b in hashData)
if (!BitConverter.IsLittleEndian)
      {
Array.Reverse(bytes);
          hash += b.ToString("X2");
 
      }
return Encoding.Latin1.GetString(bytes);
      return hash;
}
  }
</syntaxhighlight>
</syntaxhighlight>
== Main program for writing shortcuts.vdf ==
{{Expand|1=
<syntaxhighlight lang=csharp>
class Program
{
public static void CreateSteamGrid(uint appid, string sourceFolder, string destinationFolder)
{
if (appid == 0)
{
    Console.WriteLine("Invalid appid provided. Cannot create Steam grid images.");
  return;
}
try
{
// Ensure the destination directory exists
System.IO.Directory.CreateDirectory(destinationFolder);
// Define source files and their target names
var fileMap = new Dictionary<string, string>
{
{ "ID_hero.png", $"{appid}_hero.png" },
{ "ID_600x900.png", $"{appid}.png" },
{ "ID_logo.png", $"{appid}_logo.png" },
{ "ID.json", $"{appid}.json" }
};
foreach (var pair in fileMap)
{
string sourcePath = System.IO.Path.Combine(sourceFolder, pair.Key);
string destPath = System.IO.Path.Combine(destinationFolder, pair.Value);
if (System.IO.File.Exists(sourcePath))
{
System.IO.File.Copy(sourcePath, destPath, true);
}
else
{
Console.WriteLine($"Warning: Source file not found: {sourcePath}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error creating Steam grid images: {ex.Message}");
}
}
static void Main()
{
List<Shortcut> shortcuts = new List<Shortcut> {
new Shortcut
{
AppName = "My Custom Game",
Exe = @"""C:\Path\to\the.exe""",
StartDir = @"C:\Path\to\",
Icon = "",
ShortcutPath = "",
LaunchOptions = "",
IsHidden = false,
AllowDesktopConfig = true,
AllowOverlay = true,
OpenVR = false,
Devkit = false,
DevkitGameID = null,
DevkitOverrideAppID = null,
FlatpakAppID = null
}
};
uint appId = (YourClassNane.)GenerateAppID(shortcuts[0]);
shortcuts[0].AppId = BitConverter.GetBytes(appId);
byte[] shortcutData = (YourClassName.)BuildShortcuts(shortcuts);
File.WriteAllBytes(@"C:\Program Files (x86)\Steam\userdata\<user id>\config\shortcuts.vdf", shortcutData);
(YourClassName.)CreateSteamGrid(appId,
@"C:\Path\to\template images\",
@"C:\Program Files (x86)\Steam\userdata\<user id>\config\grid\");
Console.WriteLine("Proccess done, you can safely close the window.");
}
}
</syntaxhighlight>
}}
== Main program for reading shortcuts.vdf ==
{{Expand|1=
<syntaxhighlight lang=csharp>
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Enter the path to your shortcuts file:");
string? inputPath = Console.ReadLine();
inputPath = inputPath?.Replace('\\', '/'); // Normalize path separators
string filePath = string.IsNullOrWhiteSpace(inputPath)
? "shortcuts.vdf"
: (inputPath.EndsWith(".vdf", StringComparison.OrdinalIgnoreCase) ? inputPath : $"{inputPath.TrimEnd('/', '\\')}/shortcuts.vdf");
Console.WriteLine("Reading from: " + filePath);
// Read shortcuts from file, returns a list
List<Shortcut> shortcuts = (YourClassName.)ReadShortcuts(filePath);
// Display the shortcuts
if (shortcuts.Count != 0)
{
Console.WriteLine("Shortcuts found:");
foreach (var shortcut in shortcuts)
{
Console.WriteLine($"AppId: {BitConverter.ToUInt32(shortcut.AppId, 0)}"); // Decodes the id correctly
Console.WriteLine($"AppName: {shortcut.AppName}");
Console.WriteLine($"Exe: {shortcut.Exe}");
Console.WriteLine($"StartDir: {shortcut.StartDir}");
Console.WriteLine($"Icon: {shortcut.Icon}");
Console.WriteLine($"ShortcutPath: {shortcut.ShortcutPath}");
Console.WriteLine($"LaunchOptions: {shortcut.LaunchOptions}");
Console.WriteLine($"IsHidden: {shortcut.IsHidden}");
Console.WriteLine($"AllowDesktopConfig: {shortcut.AllowDesktopConfig}");
Console.WriteLine($"AllowOverlay: {shortcut.AllowOverlay}");
Console.WriteLine($"OpenVR: {shortcut.OpenVR}");
Console.WriteLine($"Devkit: {shortcut.Devkit}");
Console.WriteLine($"DevkitGameID: {shortcut.DevkitGameID}");
Console.WriteLine($"DevkitOverrideAppID: {BitConverter.ToInt32(shortcut.DevkitOverrideAppID, 0)}");
Console.WriteLine($"FlatpakAppID: {shortcut.FlatpakAppID}");
Console.WriteLine($"LastPlayTime: {BitConverter.ToInt32(shortcut.LastPlayTime, 0)}");
Console.WriteLine("Tags: " + string.Join(", ", shortcut.Tags));
Console.WriteLine(new string('-', 40));
}
}
else
{
Console.WriteLine("No shortcuts found or file is empty.");
return;
}
}
}
</syntaxhighlight>
}}
== See also ==
* [[Add Non-Steam Game|Adding Non-Steam Games]]
* [[Binary VDF]]


{{Uncategorized|date=January 2024}}
[[Category:Tutorials]]
[[Category:Steam]]

Latest revision as of 02:34, 14 October 2025

English (en)Translate (Translate)

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

This format represents every property used as of 2025.

public class Shortcut
{
	public byte[]? AppId { get; set; }
	public string? 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 byte[]? DevkitOverrideAppID { get; set; }
	public byte[]? LastPlayTime { get; set; }
	public string? FlatpakAppID { get; set; }
	public string? SortAs { get; set; }
	public List<string> Tags { get; set; } = new List<string>();
}

Reading the shortcuts.vdf

For the format structure, see Adding Non-Steam Games.
For how the script is set up, see How to read VDF binary.
public static List<Shortcut> ReadShortcuts(string filepath)
{
	if (!File.Exists(filepath) || new FileInfo(filepath).Length == 0)
		return [];

	using var fs = File.OpenRead(filepath);
	using var reader = new BinaryReader(fs, Encoding.UTF8);

	byte rootType = reader.ReadByte();
	string rootKey = ReadNullTerminatedString(reader);

	if (rootType != 0x00 || rootKey != "shortcuts")
		throw new Exception("Unexpected root key or type");

	var shortcuts = new List<Shortcut>();

	while (true)
	{
		byte type = reader.ReadByte();
		if (type == 0x08) break; // end of shortcuts dictionary

		if (type != 0x00)
			throw new Exception($"Expected dictionary key (0x00), got 0x{type:X2}");

		Shortcut shortcut = ParseShortcut(reader);
		shortcuts.Add(shortcut);
	}

	return shortcuts;
}

private static Shortcut ParseShortcut(BinaryReader reader)
{
	var sc = new Shortcut();

	while (true)
	{
		byte type = reader.ReadByte();
		if (type == 0x08) break; // end of this shortcut dict

		string key = ReadNullTerminatedString(reader);

		switch (type)
		{
			case 0x00: // nested dict, e.g. tags
				if (key == "tags")
					sc.Tags = ParseTags(reader);
				else
					SkipDictionary(reader);
				break;
			case 0x01: // string
				string val = ReadNullTerminatedString(reader);
				AssignString(sc, key, val);
				break;
			case 0x02: // int32
				int intval = reader.ReadInt32();
				AssignInt(sc, key, intval);
				break;
			default:
				throw new Exception($"Unknown type 0x{type:X2} for key '{key}'");
		}
	}
	return sc;
}

private static List<string> ParseTags(BinaryReader reader)
{
	var tags = new List<string>();
	while (true)
	{
		byte type = reader.ReadByte();
		if (type == 0x08) break; // end of tags dict

		if (type != 0x01)
			throw new Exception($"Unexpected type in tags dict: 0x{type:X2}");

		string val = ReadNullTerminatedString(reader);
		tags.Add(val);
	}
	return tags;
}

private static void SkipDictionary(BinaryReader reader)
{
	while (true)
	{
		byte type = reader.ReadByte();
		if (type == 0x08) break; // end of dictionary

		switch (type)
		{
			case 0x00:
				SkipDictionary(reader);
				break;
			case 0x01:
				ReadNullTerminatedString(reader);
				break;
			case 0x02:
				reader.ReadInt32();
				break;
			default:
				throw new Exception($"Unknown type 0x{type:X2} while skipping dict");
		}
	}
}

private static void AssignString(Shortcut sc, string key, string val)
{
	switch (key)
	{
		case "AppName": sc.AppName = val; break;
		case "Exe": sc.Exe = val; break;
		case "StartDir": sc.StartDir = val; break;
		case "icon": sc.Icon = val; break;
		case "ShortcutPath": sc.ShortcutPath = val; break;
		case "LaunchOptions": sc.LaunchOptions = val; break;
		case "FlatpakAppID": sc.FlatpakAppID = val; break;
		case "sortas": sc.SortAs = val; break;
		case "DevkitGameID": sc.DevkitGameID = val; break;
		default: break; // unknown string keys ignored
	}
}

private static void AssignInt(Shortcut sc, string key, int val)
{
	switch (key)
	{
		case "appid": sc.AppId = BitConverter.GetBytes(val); break;
		case "IsHidden": sc.IsHidden = val != 0; break;
		case "AllowDesktopConfig": sc.AllowDesktopConfig = val != 0; break;
		case "AllowOverlay": sc.AllowOverlay = val != 0; break;
		case "OpenVR": sc.OpenVR = val != 0; break;
		case "Devkit": sc.Devkit = val != 0; break;
		case "DevkitOverrideAppID": sc.DevkitOverrideAppID = BitConverter.GetBytes(val); break;
		case "LastPlayTime": sc.LastPlayTime = BitConverter.GetBytes(val); break;
		default: break; // unknown int keys ignored
	}
}

private static string ReadNullTerminatedString(BinaryReader reader)
{
	var bytes = new List<byte>();
	while (true)
	{
		byte b = reader.ReadByte();
		if (b == 0) break;
		bytes.Add(b);
	}
	return Encoding.UTF8.GetString(bytes.ToArray());
}

// to read and return data, use this somewhere:
List<Shortcut> shortcuts = (YourClassName.)ReadShortcuts(filePath);

Writing the shortcuts.vdf

This should correctly add images with the id.

It must be added with bytes, or Steam will generate a random id for images.

public static byte[] BuildShortcuts(List<Shortcut> shortcuts)
{
	var seenAppIds = new HashSet<string>();

	for (int i = 0; i < shortcuts.Count; i++)
	{
		if (shortcuts[i].AppId == null)
			continue;

		string appIdString = BitConverter.ToString(shortcuts[i].AppId);
		if (seenAppIds.Contains(appIdString))
		{
			Console.WriteLine($"Warning: Duplicate AppID found: {appIdString}. Aborting shortcut build.");
			return Array.Empty<byte>();
		}
		seenAppIds.Add(appIdString);
	}

	using var ms = new MemoryStream();
	using var writer = new BinaryWriter(ms, Encoding.UTF8);

	writer.Write((byte)0x00); // null terminator
	writer.Write(Encoding.ASCII.GetBytes("shortcuts"));
	writer.Write((byte)0x00);

	if (shortcuts.Count > 0)
	{
		for (int i = 0; i < shortcuts.Count; i++)
		{
			writer.Write((byte)0x00); // null terminator
			writer.Write(Encoding.ASCII.GetBytes(i.ToString()));
			writer.Write((byte)0x00);

			writer.Write(BuildShortcut(shortcuts[i]));

			writer.Write((byte)0x08); // end of shortcut
		}
	}
	else
	{
		Console.WriteLine("Warning: Empty list recived, no shortcuts to write.");
	}

	writer.Write((byte)0x08); // end of shortcuts dict
	writer.Write((byte)0x08); // end of root dict

	return ms.ToArray();
}

private static byte[] BuildShortcut(Shortcut shortcut)
{
	 using var ms = new MemoryStream();
	 using var writer = new BinaryWriter(ms, Encoding.UTF8);

	 // Write AppID: type (0x02), key, null terminator, then 4 raw bytes (no terminator)
	 writer.Write((byte)0x02); // int32 type
	 writer.Write(Encoding.ASCII.GetBytes("appid"));
	 writer.Write((byte)0x00);
	 if (shortcut.AppId != null && shortcut.AppId.Length == 4)
		 writer.Write(shortcut.AppId);
	 else
		 writer.Write(new byte[4] { 0, 0, 0, 0 });

	 WriteStringField(writer, "AppName", shortcut.AppName);
	 WriteStringField(writer, "Exe", shortcut.Exe);
	 WriteStringField(writer, "StartDir", shortcut.StartDir ?? "");
	 WriteStringField(writer, "icon", shortcut.Icon ?? "");
	 WriteStringField(writer, "ShortcutPath", shortcut.ShortcutPath ?? "");
	 WriteStringField(writer, "LaunchOptions", shortcut.LaunchOptions ?? "");
	 WriteBoolField(writer, "IsHidden", shortcut.IsHidden);
	 WriteBoolField(writer, "AllowDesktopConfig", shortcut.AllowDesktopConfig);
	 WriteBoolField(writer, "AllowOverlay", shortcut.AllowOverlay);
	 WriteBoolField(writer, "OpenVR", shortcut.OpenVR);
	 WriteBoolField(writer, "Devkit", shortcut.Devkit);
	 WriteStringField(writer, "DevkitGameID", shortcut.DevkitGameID ?? "");
	 WriteIntField(writer, "DevkitOverrideAppID", GetIntFromBytes(shortcut.DevkitOverrideAppID));
	 WriteIntField(writer, "LastPlayTime", GetIntFromBytes(shortcut.LastPlayTime));
	 WriteStringField(writer, "FlatpakAppID", shortcut.FlatpakAppID ?? "");
	 WriteStringField(writer, "sortas", shortcut.SortAs ?? "");
	 
	 WriteTags(writer, shortcut.Tags);

	 return ms.ToArray();
}

 // Helper to write string fields
 private static void WriteStringField(BinaryWriter writer, string key, string value)
 {
	 writer.Write((byte)0x01); // string type
	 writer.Write(Encoding.ASCII.GetBytes(key));
	 writer.Write((byte)0x00);
	 writer.Write(Encoding.UTF8.GetBytes(value));
	 writer.Write((byte)0x00);
 }

 // Helper to write bool fields (int32 with 0 or 1)
 private static void WriteBoolField(BinaryWriter writer, string key, bool value)
 {
	 writer.Write((byte)0x02); // int32 type
	 writer.Write(Encoding.ASCII.GetBytes(key));
	 writer.Write((byte)0x00);
	 writer.Write(value ? 1 : 0);
 }

 // Helper to write int32 fields
 private static void WriteIntField(BinaryWriter writer, string key, int value)
 {
	 writer.Write((byte)0x02); // int32 type
	 writer.Write(Encoding.ASCII.GetBytes(key));
	 writer.Write((byte)0x00);
	 writer.Write(value);
 }
 private static int GetIntFromBytes(byte[] bytes)
 {
	 if (bytes == null || bytes.Length < 4) return 0;
	 return BitConverter.ToInt32(bytes, 0);
 }

 // Helper to write tags dictionary
 private static void WriteTags(BinaryWriter writer, List<string> tags)
 {
	 writer.Write((byte)0x00); // begin dict
	 writer.Write(Encoding.ASCII.GetBytes("tags"));
	 writer.Write((byte)0x00);

	 for (int i = 0; i < tags.Count; i++)
	 {
		 writer.Write((byte)0x01); // string type
		 writer.Write(Encoding.ASCII.GetBytes(i.ToString()));
		 writer.Write((byte)0x00);
		 writer.Write(Encoding.UTF8.GetBytes(tags[i]));
		 writer.Write((byte)0x00);
	 }

	 writer.Write((byte)0x08); // end dict
 }
 public static void WriteShortcuts(string filePath, List<Shortcut> shortcuts)
 {
	 using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
	 using var writer = new BinaryWriter(fs, Encoding.UTF8);

	 // Write root dictionary start
	 writer.Write((byte)0x00);
	 writer.Write(Encoding.UTF8.GetBytes("shortcuts"));
	 writer.Write((byte)0x00);

	 for (int i = 0; i < shortcuts.Count; i++)
	 {
		 writer.Write((byte)0x00); // dictionary key type (string)
		 writer.Write(Encoding.UTF8.GetBytes(i.ToString()));
		 writer.Write((byte)0x00);

		 // Write shortcut binary data
		 var shortcutBytes = BuildShortcut(shortcuts[i]);
		 writer.Write(shortcutBytes);

		 writer.Write((byte)0x08); // end of shortcut dictionary
	 }

	 // Write final end markers for shortcuts dictionary
	 writer.Write((byte)0x08);
	 writer.Write((byte)0x08);
 }

Get AppId for a shortcut

To display an image in the library, the app id of the shortcut must be known. What has been tested so far is that the app id is given by the following method:

In order to add a steam short cut, Steam must be shut downed or it wont add the shortcut.

private static uint ComputeCRC32(byte[] bytes)
{
	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 uint GenerateAppID(Shortcut shortcut)
{
	string combined = shortcut.AppName + shortcut.Exe + "\0";

	Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
	byte[] data = Encoding.GetEncoding("Windows-1252").GetBytes(combined);

	uint crc = ComputeCRC32(data);
	return crc | 0x80000000;
}

public static string GenerateEncryptedAppID(Shortcut shortcut)
{
	uint appid = GenerateAppID(shortcut);

	byte[] bytes = BitConverter.GetBytes(appid);
	if (!BitConverter.IsLittleEndian)
		Array.Reverse(bytes);

	return Encoding.Latin1.GetString(bytes);
}

Main program for writing shortcuts.vdf

class Program
{
	public static void CreateSteamGrid(uint appid, string sourceFolder, string destinationFolder)
	{
		if (appid == 0)
		{
  	  		Console.WriteLine("Invalid appid provided. Cannot create Steam grid images.");
 	   		return;
		}
		
		try
		{
			// Ensure the destination directory exists
			System.IO.Directory.CreateDirectory(destinationFolder);

			// Define source files and their target names
			var fileMap = new Dictionary<string, string>
			{
				{ "ID_hero.png", $"{appid}_hero.png" },
				{ "ID_600x900.png", $"{appid}.png" },
				{ "ID_logo.png", $"{appid}_logo.png" },
				{ "ID.json", $"{appid}.json" }
			};

			foreach (var pair in fileMap)
			{
				string sourcePath = System.IO.Path.Combine(sourceFolder, pair.Key);
				string destPath = System.IO.Path.Combine(destinationFolder, pair.Value);

				if (System.IO.File.Exists(sourcePath))
				{
					System.IO.File.Copy(sourcePath, destPath, true);
				}
				else
				{
					Console.WriteLine($"Warning: Source file not found: {sourcePath}");
				}
			}
		}
		catch (Exception ex)
		{
			Console.WriteLine($"Error creating Steam grid images: {ex.Message}");
		}
	}

	static void Main()
	{ 
		List<Shortcut> shortcuts = new List<Shortcut> {
			new Shortcut
			{
				AppName = "My Custom Game",
				Exe = @"""C:\Path\to\the.exe""",
				StartDir = @"C:\Path\to\",
				Icon = "",
				ShortcutPath = "",
				LaunchOptions = "",
				IsHidden = false,
				AllowDesktopConfig = true,
				AllowOverlay = true,
				OpenVR = false,
				Devkit = false,
				DevkitGameID = null,
				DevkitOverrideAppID = null,
				FlatpakAppID = null
			}
		};

		uint appId = (YourClassNane.)GenerateAppID(shortcuts[0]);
		shortcuts[0].AppId = BitConverter.GetBytes(appId);

		byte[] shortcutData = (YourClassName.)BuildShortcuts(shortcuts);
		File.WriteAllBytes(@"C:\Program Files (x86)\Steam\userdata\<user id>\config\shortcuts.vdf", shortcutData);
		(YourClassName.)CreateSteamGrid(appId, 
			@"C:\Path\to\template images\",
			@"C:\Program Files (x86)\Steam\userdata\<user id>\config\grid\");
		Console.WriteLine("Proccess done, you can safely close the window.");
	}
}

Main program for reading shortcuts.vdf

class Program
{
	static void Main(string[] args)
	{
		Console.WriteLine("Enter the path to your shortcuts file:");
		string? inputPath = Console.ReadLine();
		inputPath = inputPath?.Replace('\\', '/'); // Normalize path separators
		string filePath = string.IsNullOrWhiteSpace(inputPath)
			? "shortcuts.vdf"
			: (inputPath.EndsWith(".vdf", StringComparison.OrdinalIgnoreCase) ? inputPath : $"{inputPath.TrimEnd('/', '\\')}/shortcuts.vdf");

		Console.WriteLine("Reading from: " + filePath);

		// Read shortcuts from file, returns a list
		List<Shortcut> shortcuts = (YourClassName.)ReadShortcuts(filePath);

		// Display the shortcuts
		if (shortcuts.Count != 0)
		{
			Console.WriteLine("Shortcuts found:");
			foreach (var shortcut in shortcuts)
			{
				Console.WriteLine($"AppId: {BitConverter.ToUInt32(shortcut.AppId, 0)}"); // Decodes the id correctly
				Console.WriteLine($"AppName: {shortcut.AppName}");
				Console.WriteLine($"Exe: {shortcut.Exe}");
				Console.WriteLine($"StartDir: {shortcut.StartDir}");
				Console.WriteLine($"Icon: {shortcut.Icon}");
				Console.WriteLine($"ShortcutPath: {shortcut.ShortcutPath}");
				Console.WriteLine($"LaunchOptions: {shortcut.LaunchOptions}");
				Console.WriteLine($"IsHidden: {shortcut.IsHidden}");
				Console.WriteLine($"AllowDesktopConfig: {shortcut.AllowDesktopConfig}");
				Console.WriteLine($"AllowOverlay: {shortcut.AllowOverlay}");
				Console.WriteLine($"OpenVR: {shortcut.OpenVR}");
				Console.WriteLine($"Devkit: {shortcut.Devkit}");
				Console.WriteLine($"DevkitGameID: {shortcut.DevkitGameID}");
				Console.WriteLine($"DevkitOverrideAppID: {BitConverter.ToInt32(shortcut.DevkitOverrideAppID, 0)}");
				Console.WriteLine($"FlatpakAppID: {shortcut.FlatpakAppID}");
				Console.WriteLine($"LastPlayTime: {BitConverter.ToInt32(shortcut.LastPlayTime, 0)}");
				Console.WriteLine("Tags: " + string.Join(", ", shortcut.Tags));
				Console.WriteLine(new string('-', 40));
			}
		}
		else
		{
			Console.WriteLine("No shortcuts found or file is empty.");
			return;
		}
	}
}

See also