Steam Library Shortcuts: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(im so done with this scripting, as soon as it worked correctly, im done, good enough and i dont even know if these are the right categories)
No edit summary
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
{{LanguageBar}}
{{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 {{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.


Line 7: Line 8:


== Shortcut format ==
== Shortcut format ==
 
This format represents every property used as of 2025.
* string AppName
* string Exe
* string StartDir
* string Icon
* string ShortcutPath
* string LaunchOptions
* bool Hidden
* array<string> Tags
 
== Reading the shortcuts.vdf ==
{{For|the format structure|[[Add_Non-Steam_Game#File_format|Adding Non-Steam Games]]}}
<syntaxhighlight lang=csharp>
<syntaxhighlight lang=csharp>
// 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
public class Shortcut
{
{
// adding '?' to declare as a nullable property, for example "Icon" would be null if the icon wasn't set
     public byte[]? AppId { get; set; }
     // public string? AppId { get; set; }
     public string? AppName { get; set; }
     public byte[]? AppName { get; set; }
     public string? Exe { get; set; }
     public string? Exe { get; set; }
     public string? StartDir { get; set; }
     public string? StartDir { get; set; }
Line 46: Line 25:
     public bool Devkit { get; set; }
     public bool Devkit { get; set; }
     public string? DevkitGameID { get; set; }
     public string? DevkitGameID { get; set; }
     public string? DevkitOverrideAppID { get; set; }
     public byte[]? DevkitOverrideAppID { get; set; }
     // public string? LastPlayTime { get; set; }
     public byte[]? LastPlayTime { get; set; }
     public string? FlatpakAppID { get; set; }
     public string? FlatpakAppID { get; set; }
     public List<string> Tags { get; set; } = new List<string>();
     public List<string> Tags { get; set; } = new List<string>();
}
}
</syntaxhighlight>


public static List<Shortcut> ReadAndReturn(string filepath)
== Reading the shortcuts.vdf ==
{{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>
public static List<Shortcut> ReadShortcuts(string filepath)
{
{
     string shortcutsString = File.ReadAllText(filepath, Encoding.GetEncoding("ISO-8859-1")); // Read the file with the correct encoding
     if (!File.Exists(filepath))
{
    Console.WriteLine($"The file '{filepath}' does not exist.");
    return [];
}


    List<Shortcut> result = new List<Shortcut>();
if (new FileInfo(filepath).Length == 0)
    int start = shortcutsString.IndexOf("\u0000shortcuts\u0000") + "\u0000shortcuts\u0000".Length;
{
     int end = shortcutsString.LastIndexOf("\u0008\u0008");
    Console.WriteLine($"The file '{filepath}' is empty, use 'BuildShortcuts()' with an empty list");
     shortcutsString = shortcutsString.Substring(start, end - start);
    return [];
     Shortcut shortcut = null;
}
     string word = "";
     string key = "";
     using var fs = File.OpenRead(filepath);
    bool readingTags = false;
     using var reader = new BinaryReader(fs, Encoding.UTF8);
     int tagId = -1;
 
   
     byte rootType = reader.ReadByte();
     foreach(char c in shortcutsString.ToCharArray())
     string rootKey = ReadNullTerminatedString(reader);
 
     if (rootType != 0x00 || rootKey != "shortcuts")
        throw new Exception("Unexpected root key or type");
 
     var shortcuts = new List<Shortcut>();
 
     while (true)
     {
     {
         if (c == '\u0000') {
         byte type = reader.ReadByte();
            if (word.EndsWith("\u0001AppName")) {
        if (type == 0x08) break; // end of shortcuts dictionary
                if (shortcut != null)
 
                    result.Add(shortcut);
        if (type != 0x00)
                // New shortcut 
             throw new Exception($"Expected dictionary key (0x00), got 0x{type:X2}");
                shortcut = new Shortcut();
 
                key = "\u0001AppName";
        string index = ReadNullTerminatedString(reader);
             }
 
            else if (
        Shortcut shortcut = ParseShortcut(reader);
                //word == "\u0002appid" || // NOTE: there is no NUL terminated at the end of it, so it is not read correctly
        shortcuts.Add(shortcut);
                word == "\u0001Exe" ||
    }
                word == "\u0001StartDir" ||
 
                word == "\u0001icon" ||
    return shortcuts;
                word == "\u0001ShortcutPath" ||
}
                word == "\u0001LaunchOptions" ||
 
                word == "\u0002IsHidden" ||
private static Shortcut ParseShortcut(BinaryReader reader)
                word == "\u0002AllowDesktopConfig" ||
{
                word == "\u0002AllowOverlay" ||
    var sc = new Shortcut();
                word == "\u0002OpenVR" ||
 
                word == "\u0002Devkit" ||
    while (true)
                word == "\u0002DevkitGameID" ||
    {
                word == "\u0002DevkitOverrideAppID" ||
        byte type = reader.ReadByte();
                //word == "\u0002LastPlayTime" || // NOTE: there is no NUL terminated at the end of it, so it is not read correctly
        if (type == 0x08) break; // end of this shortcut dict
                // Which means that this bottom one is getting picked up by the LastPlayTime
 
                word == "\u0001FlatpakAppID"
        string key = ReadNullTerminatedString(reader);
            ) {
 
                key = word;
        switch (type)
            }
        {
            else if (word == "tags") {
            case 0x00: // nested dict, e.g. tags
                readingTags = true;
                if (key == "tags")
            }
                     sc.Tags = ParseTags(reader);
            else if (key != "") {
                else
                switch (key) {
                     SkipDictionary(reader);
                    //case "\u0002appid": shortcut.AppId = word; break;
                break;
                    case "\u0001AppName": shortcut.AppName = word; break;
            case 0x01: // string
                    case "\u0001Exe": shortcut.Exe = word.Trim('"'); break;
                string val = ReadNullTerminatedString(reader);
                    case "\u0001StartDir": shortcut.StartDir = word; break;
                AssignString(sc, key, val);
                    case "\u0001icon": shortcut.Icon = word; break;
                break;
                    case "\u0001ShortcutPath": shortcut.ShortcutPath = word; break;
            case 0x02: // int32
                    case "\u0001LaunchOptions": shortcut.LaunchOptions = word; break;
                 int intval = reader.ReadInt32();
                    case "\u0002IsHidden": shortcut.IsHidden = (word == "\u0001"); break;
                 AssignInt(sc, key, intval);
                     case "\u0002AllowDesktopConfig": shortcut.AllowDesktopConfig = (word == "\u0001"); break;
                break;
                     case "\u0002AllowOverlay": shortcut.AllowOverlay = (word == "\u0001"); break;
            default:
                    case "\u0002OpenVR": shortcut.OpenVR = (word == "\u0001"); break;
                 throw new Exception($"Unknown type 0x{type:X2} for key '{key}'");
                    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;
 
    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
 
        string key = ReadNullTerminatedString(reader);
 
        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
 
        string key = ReadNullTerminatedString(reader);
 
        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");
         }
         }
     }
     }
}


     if (shortcut != null)
private static void AssignString(Shortcut sc, string key, string val)
         result.Add(shortcut);
{
     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 "DevkitGameID": sc.DevkitGameID = val; break;
        default: break; // unknown string keys ignored
    }
}


     return result;
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:
// 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>
public static string BuildShortcuts(List<Shortcut> shortcuts)
public static byte[] BuildShortcuts(List<Shortcut> shortcuts)
{
{
     string shortcutsString = "\0shortcuts\0";
     var seenAppIds = new HashSet<string>();
 
     for (int i = 0; i < shortcuts.Count; i++)
     for (int i = 0; i < shortcuts.Count; i++)
     {
     {
         shortcutsString += "\0" + i + "\0";
         if (shortcuts[i].AppId == null)
         shortcutsString += BuildShortcut(shortcuts[i]);
            continue;
         shortcutsString += "\u0008";
 
         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);
     }
     }
    shortcutsString += "\u0008\u0008";
    return shortcutsString;
}


private static string BuildShortcut(Shortcut shortcut)
    using var ms = new MemoryStream();
{
     using var writer = new BinaryWriter(ms, Encoding.UTF8);
    string shortcutString = "";
 
     string appIdBytes = shortcut.AppId != null ? Encoding.Latin1.GetString(shortcut.AppId) : "\0\0\0\0";
     writer.Write((byte)0x00); // null terminator
    // somewhere in the code i fucked it up and now i have to use 3 instead of a 4... Too bad!
     writer.Write(Encoding.ASCII.GetBytes("shortcuts"));
     // it should still work on adding a shortcut
     writer.Write((byte)0x00);
    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
     if (shortcuts.Count > 0)
     shortcutString += "\u0001AppName\0" + shortcut.AppName + "\0"; // AppName
     {
     shortcutString += "\u0001Exe\0" + shortcut.Exe + "\0"; // Exe
        for (int i = 0; i < shortcuts.Count; i++)
     shortcutString += "\u0001StartDir\0" + (shortcut.StartDir ?? "") + "\0"; // StartDir
        {
     shortcutString += "\u0001icon\0" + shortcut.Icon + "\0"; // Icon
            writer.Write((byte)0x00); // null terminator
    shortcutString += "\u0001ShortcutPath\0" + shortcut.ShortcutPath + "\0"; // ShortcutPath
            writer.Write(Encoding.ASCII.GetBytes(i.ToString()));
    shortcutString += "\u0001LaunchOptions\0" + shortcut.LaunchOptions + "\0"; // LaunchOptions
            writer.Write((byte)0x00);
    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;
            writer.Write(BuildShortcut(shortcuts[i]));
}


private static string buildTags(List<string> tags)
            writer.Write((byte)0x08); // end of shortcut
{
        }
     var tagString = "\0tags\0";
     }
     for (var i = 0; i < tags.Count; ++i)
     else
     {
     {
         tagString += "\u0001" + i + "\0" + tags[i] + "\0";
         Console.WriteLine("Warning: Empty list recived, no shortcuts to write.");
     }
     }
     tagString += "\u0008";
 
     return tagString;
     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 });


private static char GetBooleanChar(bool value)
    // Write string keys: type (0x01), key, null, value, null
{
    WriteStringField(writer, "AppName", shortcut.AppName);
    return value ? '\u0001' : '\0';
    WriteStringField(writer, "Exe", shortcut.Exe);
}
    WriteStringField(writer, "StartDir", shortcut.StartDir ?? "");
</syntaxhighlight>
    WriteStringField(writer, "icon", shortcut.Icon ?? "");
    WriteStringField(writer, "ShortcutPath", shortcut.ShortcutPath ?? "");
    WriteStringField(writer, "LaunchOptions", shortcut.LaunchOptions ?? "");
 
    // Write booleans: type (0x02), key, null, 4-byte int (0 or 1)
    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 ?? "");
 
    // DevkitOverrideAppID and LastPlayTime are int32 fields, or 0 if null
    WriteIntField(writer, "DevkitOverrideAppID", GetIntFromBytes(shortcut.DevkitOverrideAppID));
    WriteIntField(writer, "LastPlayTime", GetIntFromBytes(shortcut.LastPlayTime));
 
    WriteStringField(writer, "FlatpakAppID", shortcut.FlatpakAppID ?? "");
 
    // Write tags as nested key-values
    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);
}


== Get AppId for a shortcut ==
// Helper to write int32 fields
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:
private static void WriteIntField(BinaryWriter writer, string key, int value)
<syntaxhighlight lang=csharp>
{
  var longValue = new Long(crcValue, crcValue, true);
    writer.Write((byte)0x02); // int32 type
  longValue = longValue.or(0x80000000);
    writer.Write(Encoding.ASCII.GetBytes(key));
  longValue = longValue.shl(32);
    writer.Write((byte)0x00);
  longValue = longValue.or(0x02000000);
    writer.Write(value);
  return result.ToString();
}
private static int GetIntFromBytes(byte[] bytes)
{
    if (bytes == null || bytes.Length < 4) return 0;
    return BitConverter.ToInt32(bytes, 0);
}


  private string GetSHA1()
// Helper to write tags dictionary
  {
private static void WriteTags(BinaryWriter writer, List<string> tags)
      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;
  }
</syntaxhighlight>
This code works, it adds an app on the steam library, Steam must be shut downed in order to append it in the library
<syntaxhighlight lang=csharp>
private static uint ComputeCRC32(byte[] bytes)
  {
  {
     // Your CRC32 implementation here (or use the one you have)
     writer.Write((byte)0x00); // begin dict
    // Simple example:
     writer.Write(Encoding.ASCII.GetBytes("tags"));
     const uint Polynomial = 0xEDB88320;
     writer.Write((byte)0x00);
     uint[] table = new uint[256];
 
     for (uint i = 0; i < 256; i++)
     for (int i = 0; i < tags.Count; i++)
     {
     {
         uint temp = i;
         writer.Write((byte)0x01); // string type
         for (int j = 0; j < 8; j++)
         writer.Write(Encoding.ASCII.GetBytes(i.ToString()));
            temp = (temp & 1) == 1 ? (Polynomial ^ (temp >> 1)) : (temp >> 1);
        writer.Write((byte)0x00);
         table[i] = temp;
         writer.Write(Encoding.UTF8.GetBytes(tags[i]));
        writer.Write((byte)0x00);
     }
     }


     uint crc = 0xFFFFFFFF;
     writer.Write((byte)0x08); // end dict
     foreach (byte b in bytes)
}
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++)
     {
     {
         byte index = (byte)((crc & 0xFF) ^ b);
         writer.Write((byte)0x00); // dictionary key type (string)
         crc = (crc >> 8) ^ table[index];
        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
     }
     }
     return ~crc;
 
     // Write final end markers for shortcuts dictionary
    writer.Write((byte)0x08);
    writer.Write((byte)0x08);
}
</syntaxhighlight>
 
== 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.
<syntaxhighlight lang=csharp>
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 string GenerateEncrypedAppID(Shortcut shortcut)
public static uint GenerateAppID(Shortcut shortcut)
{
{
    // Combine exe + launch options
     string combined = shortcut.AppName + shortcut.Exe + "\0";
     string combined = shortcut.Exe + shortcut.LaunchOptions;


    // Encode in Windows-1252 (Steam uses ANSI encoding)
     Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
     Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
     byte[] data = Encoding.GetEncoding("Windows-1252").GetBytes(combined);
     byte[] data = Encoding.GetEncoding("Windows-1252").GetBytes(combined);


    // Compute CRC32 of combined string
     uint crc = ComputeCRC32(data);
     uint crc = ComputeCRC32(data);
    return crc | 0x80000000;
}


    // Set the shortcut flag bit
public static string GenerateEncryptedAppID(Shortcut shortcut)
     uint encodedValue = crc | 0x80000000;
{
     uint appid = GenerateAppID(shortcut);


     string hexString = encodedValue.ToString("X8");
     byte[] bytes = BitConverter.GetBytes(appid);
    if (!BitConverter.IsLittleEndian)
        Array.Reverse(bytes);


     byte[] bytes = new byte[4];
     return Encoding.Latin1.GetString(bytes);
 
    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;
}
}
</syntaxhighlight>
</syntaxhighlight>
== Main program for writing ==
== Main program for writing shortcuts.vdf ==
{{Expand|1=
<syntaxhighlight lang=csharp>
<syntaxhighlight lang=csharp>
class Program
class Program
{
{
    static void Main()
public static void CreateSteamGrid(uint appid, string sourceFolder, string destinationFolder)
    {
{
        string exe = @"""A:\Path\to\your.exe""";
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" }
        };


        Shortcut shortcut = new Shortcut
        foreach (var pair in fileMap)
        {
        {
            Exe = exe,
            string sourcePath = System.IO.Path.Combine(sourceFolder, pair.Key);
            LaunchOptions = "",
            string destPath = System.IO.Path.Combine(destinationFolder, pair.Value);
            AppName = "App name",
            StartDir = @"A:\Path\to\startDir\"
        };


        string encrypedValue = GenerateEncrypedAppID(shortcut);
            if (System.IO.File.Exists(sourcePath))
        byte[] appIdRawBytes = Encoding.Latin1.GetBytes(encrypedValue);
            {
                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> {             
         List<Shortcut> shortcuts = new List<Shortcut> {             
             new Shortcut
             new Shortcut
             {
             {
                AppId = appIdRawBytes,
                 AppName = "My Custom Game",
                 AppName = "App name",
                 Exe = @"""C:\Path\to\the.exe""",
                 Exe = exe,
                 StartDir = @"C:\Path\to\StartDir\",
                 StartDir = @"A:\Path\to\startDir\",
                 Icon = "",
                 Icon = "",
                 ShortcutPath = "",
                 ShortcutPath = "",
Line 321: Line 497:
                 DevkitGameID = null,
                 DevkitGameID = null,
                 DevkitOverrideAppID = null,
                 DevkitOverrideAppID = null,
                 FlatpakAppID = null,
                 FlatpakAppID = null
             }
             }
         };
         };


         string getStructureData = BuildShortcuts(shortcuts);
uint appId = (YourClassNane.)GenerateAppID(shortcuts[0]);
         File.WriteAllText(@"STEAMROOT\userdata\<user id>\config\shortcuts.vdf", getStructureData);
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.");
         Console.WriteLine("Proccess done, you can safely close the window.");
     }
     }
}
}
</syntaxhighlight>
</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 ==
== See also ==
* [[Add Non-Steam Game|Adding Non-Steam Games]]
* [[Add Non-Steam Game|Adding Non-Steam Games]]
* [[Binary VDF]]


[[Category:Tutorials]]
[[Category:Tutorials]]
[[Category:Steam]]
[[Category:Steam]]

Latest revision as of 13:30, 2 July 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 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))
	{
	    Console.WriteLine($"The file '{filepath}' does not exist.");
	    return []; 
	}

	if (new FileInfo(filepath).Length == 0)
	{
	    Console.WriteLine($"The file '{filepath}' is empty, use 'BuildShortcuts()' with an empty list");
	    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}");

        string index = ReadNullTerminatedString(reader);

        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

        string key = ReadNullTerminatedString(reader);

        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

        string key = ReadNullTerminatedString(reader);

        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 "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 });

     // Write string keys: type (0x01), key, null, value, null
     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 ?? "");

     // Write booleans: type (0x02), key, null, 4-byte int (0 or 1)
     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 ?? "");

     // DevkitOverrideAppID and LastPlayTime are int32 fields, or 0 if null
     WriteIntField(writer, "DevkitOverrideAppID", GetIntFromBytes(shortcut.DevkitOverrideAppID));
     WriteIntField(writer, "LastPlayTime", GetIntFromBytes(shortcut.LastPlayTime));

     WriteStringField(writer, "FlatpakAppID", shortcut.FlatpakAppID ?? "");

     // Write tags as nested key-values
     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\StartDir\",
                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