Steam Library Shortcuts: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
mNo edit summary
(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)
Line 1: Line 1:
{{Multiple issues|
{{Dead end|date=January 2024}}
{{Orphan|date=January 2024}}
{{Orphan|date=January 2024}}
}}


Source mods under '''/sourcemods/''', when configured correctly, show up on Steam Library after a restart. Source Mods placed in '''|all_source_engine_paths|''', GoldSrc mods or even Source 2 mods don't show up by default.
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.


== 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 ==
Line 37: Line 34:
// adding '?' to declare as a nullable property, for example "Icon" would be null if the icon wasn't set
// adding '?' to declare as a nullable property, for example "Icon" would be null if the icon wasn't set
     // public string? AppId { get; set; }
     // public 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 156: Line 153:
== Writing the shortcuts.vdf ==
== Writing the shortcuts.vdf ==
<syntaxhighlight lang=csharp>
<syntaxhighlight lang=csharp>
  private static string BuildShortcuts(List<Shortcut> shortcuts)
public static string BuildShortcuts(List<Shortcut> shortcuts)
  {
{
      string shortcutsString = "\u0000shortcuts\u0000";
    string shortcutsString = "\0shortcuts\0";
      for (int i = 0; i < shortcuts.Count; i++)
    for (int i = 0; i < shortcuts.Count; i++)
      {
    {
          shortcutsString += "\u0000" + i + "\u0000";
        shortcutsString += "\0" + i + "\0";
          shortcutsString += BuildShortcut(shortcuts[i]);
        shortcutsString += BuildShortcut(shortcuts[i]);
          shortcutsString += "\u0008";
        shortcutsString += "\u0008";
      }
    }
      shortcutsString += "\u0008\u0008";
    shortcutsString += "\u0008\u0008";
      return shortcutsString;
    return shortcutsString;
  }
}
 
private static string BuildShortcut(Shortcut shortcut)
{
    string shortcutString = "";
    string appIdBytes = shortcut.AppId != null ? Encoding.Latin1.GetString(shortcut.AppId) : "\0\0\0\0";
    // somewhere in the code i fucked it up and now i have to use 3 instead of a 4... Too bad!
    // it should still work on adding a shortcut
    string appIdStr = appIdBytes.Length >= 4 ? appIdBytes.Substring(0, 3) : appIdBytes;
    shortcutString += "\u0002appid\0" + appIdStr; // AppID is a 4-byte integer, so it doesn't have a NUL terminator
    shortcutString += "\u0001AppName\0" + shortcut.AppName + "\0"; // AppName
    shortcutString += "\u0001Exe\0" + shortcut.Exe + "\0"; // Exe
    shortcutString += "\u0001StartDir\0" + (shortcut.StartDir ?? "") + "\0"; // StartDir
    shortcutString += "\u0001icon\0" + shortcut.Icon + "\0"; // Icon
    shortcutString += "\u0001ShortcutPath\0" + shortcut.ShortcutPath + "\0"; // ShortcutPath
    shortcutString += "\u0001LaunchOptions\0" + shortcut.LaunchOptions + "\0"; // LaunchOptions
    shortcutString += "\u0002IsHidden\0" + GetBooleanChar(shortcut.IsHidden) + "\0\0\0"; // IsHidden
    shortcutString += "\u0002AllowDesktopConfig\0" + GetBooleanChar(shortcut.AllowDesktopConfig) + "\0\0\0"; // AllowDesktopConfig
    shortcutString += "\u0002AllowOverlay\0" + GetBooleanChar(shortcut.AllowOverlay) + "\0\0\0"; // AllowOverlay
    shortcutString += "\u0002OpenVR\0" + GetBooleanChar(shortcut.OpenVR) + "\0\0\0"; // OpenVR
    shortcutString += "\u0002Devkit\0" + GetBooleanChar(shortcut.Devkit) + "\0\0\0"; // Devkit
    shortcutString += "\u0001DevkitGameID\0" + (shortcut.DevkitGameID ?? "") + "\0"; // DevkitGameID
    shortcutString += "\u0002DevkitOverrideAppID\0" + (shortcut.DevkitOverrideAppID ?? "\0") + "\0\0\0"; // DevkitOverrideAppID
    shortcutString += "\u0002LastPlayTime\0" + (shortcut.LastPlayTime ?? "") + "\0"; // LastPlayTime
    shortcutString += "\u0001FlatpakAppID\0" + (shortcut.FlatpakAppID ?? "") + "\0"; // FlatpakAppID
    shortcutString += buildTags(shortcut.Tags); // Tags
 
    return shortcutString;
}
 
private static string buildTags(List<string> tags)
{
    var tagString = "\0tags\0";
    for (var i = 0; i < tags.Count; ++i)
    {
        tagString += "\u0001" + i + "\0" + tags[i] + "\0";
    }
    tagString += "\u0008";
    return tagString;
}


  private static string BuildShortcut(Shortcut shortcut)
  {
      string shortcutString = "";
      //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)
private static char GetBooleanChar(bool value)
  {
{
      var tagString = "\u0000tags\u0000";
    return value ? '\u0001' : '\0';
      for (var i = 0; i < tags.Count; ++i)
}
      {
          tagString += "\u0001" + i + "\u0000" + tags[i] + "\u0000";
      }
      tagString += "\u0008";
      return tagString;
  }
</syntaxhighlight>
</syntaxhighlight>


Line 217: Line 233:
   }
   }
</syntaxhighlight>
</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)
    // Simple example:
    const uint Polynomial = 0xEDB88320;
    uint[] table = new uint[256];
    for (uint i = 0; i < 256; i++)
    {
        uint temp = i;
        for (int j = 0; j < 8; j++)
            temp = (temp & 1) == 1 ? (Polynomial ^ (temp >> 1)) : (temp >> 1);
        table[i] = temp;
    }
    uint crc = 0xFFFFFFFF;
    foreach (byte b in bytes)
    {
        byte index = (byte)((crc & 0xFF) ^ b);
        crc = (crc >> 8) ^ table[index];
    }
    return ~crc;
}
public static string GenerateEncrypedAppID(Shortcut shortcut)
{
    // Combine exe + launch options
    string combined = shortcut.Exe + shortcut.LaunchOptions;
    // Encode in Windows-1252 (Steam uses ANSI encoding)
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
    byte[] data = Encoding.GetEncoding("Windows-1252").GetBytes(combined);
    // Compute CRC32 of combined string
    uint crc = ComputeCRC32(data);
    // Set the shortcut flag bit
    uint encodedValue =  crc | 0x80000000;
    string hexString = encodedValue.ToString("X8");
    byte[] bytes = new byte[4];
    for (int i = 0; i < 4; i++)
        bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
    // Convert byte array to a string where each char represents one byte
    string binaryString = Encoding.Latin1.GetString(bytes);
    return binaryString;
}
</syntaxhighlight>
== Main program for writing ==
<syntaxhighlight lang=csharp>
class Program
{
    static void Main()
    {
        string exe = @"""A:\Path\to\your.exe""";
        Shortcut shortcut = new Shortcut
        {
            Exe = exe,
            LaunchOptions = "",
            AppName = "App name",
            StartDir = @"A:\Path\to\startDir\"
        };
        string encrypedValue = GenerateEncrypedAppID(shortcut);
        byte[] appIdRawBytes = Encoding.Latin1.GetBytes(encrypedValue);
        List<Shortcut> shortcuts = new List<Shortcut> {           
            new Shortcut
            {
                AppId = appIdRawBytes,
                AppName = "App name",
                Exe = exe,
                StartDir = @"A:\Path\to\startDir\",
                Icon = "",
                ShortcutPath = "",
                LaunchOptions = "",
                IsHidden = false,
                AllowDesktopConfig = true,
                AllowOverlay = true,
                OpenVR = false,
                Devkit = false,
                DevkitGameID = null,
                DevkitOverrideAppID = null,
                FlatpakAppID = null,
            }
        };
        string getStructureData = BuildShortcuts(shortcuts);
        File.WriteAllText(@"STEAMROOT\userdata\<user id>\config\shortcuts.vdf", getStructureData);
        Console.WriteLine("Proccess done, you can safely close the window.");
    }
}
</syntaxhighlight>
== See also ==
* [[Add Non-Steam Game|Adding Non-Steam Games]]


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

Revision as of 17:01, 26 June 2025

Source mods under /sourcemods/, when configured correctly, show up on Steam Library after a restart. Source Mods placed in |all_source_engine_paths|, GoldSrc mods or even Source 2 mods don't show up by default.

Steam shortcuts file

Any game/mod could by added to the Steam Library by creating a shortcut. The shortcuts file for a given user are located under the directory steam_install_dir\\userdata\\, and the path ends with config. The shortcuts are stored inside this directory, in a file named 🖿shortcuts.vdf.

Shortcut format

  • string AppName
  • string Exe
  • string StartDir
  • string Icon
  • string ShortcutPath
  • string LaunchOptions
  • bool Hidden
  • array<string> Tags

Reading the shortcuts.vdf

For the format structure, see Adding Non-Steam Games.
// 1. Read all bytes from the file at 'filepath'.
// 2. Convert the bytes to a string using the appropriate encoding.
// 3. Assign the string to 'shortcutsString'.
// 4. Return the list with the data
//
// Unicodes:
// \u0000 - NUL
// \u0001 - SOH
// \u0002 - STX

public class Shortcut
{
	// adding '?' to declare as a nullable property, for example "Icon" would be null if the icon wasn't set
    // public string? AppId { get; set; }
    public byte[]? AppName { get; set; }
    public string? Exe { get; set; }
    public string? StartDir { get; set; }
    public string? Icon { get; set; }
    public string? ShortcutPath { get; set; }
    public string? LaunchOptions { get; set; }
    public bool IsHidden { get; set; }
    public bool AllowDesktopConfig { get; set; }
    public bool AllowOverlay { get; set; }
    public bool OpenVR { get; set; }
    public bool Devkit { get; set; }
    public string? DevkitGameID { get; set; }
    public string? DevkitOverrideAppID { get; set; }
    // public string? LastPlayTime { get; set; }
    public string? FlatpakAppID { get; set; }
    public List<string> Tags { get; set; } = new List<string>();
}

public static List<Shortcut> ReadAndReturn(string filepath)
{
    string shortcutsString = File.ReadAllText(filepath, Encoding.GetEncoding("ISO-8859-1")); // Read the file with the correct encoding

    List<Shortcut> result = new List<Shortcut>();
    int start = shortcutsString.IndexOf("\u0000shortcuts\u0000") + "\u0000shortcuts\u0000".Length;
    int end = shortcutsString.LastIndexOf("\u0008\u0008");
    shortcutsString = shortcutsString.Substring(start, end - start);
    Shortcut shortcut = null;
    string word = "";
    string key = "";
    bool readingTags = false;
    int tagId = -1;
    
    foreach(char c in shortcutsString.ToCharArray())
    {
        if (c == '\u0000') {
            if (word.EndsWith("\u0001AppName")) {
                if (shortcut != null)
                    result.Add(shortcut);
                // New shortcut  
                shortcut = new Shortcut();
                key = "\u0001AppName";
            }
            else if (
                //word == "\u0002appid" || // NOTE: there is no NUL terminated at the end of it, so it is not read correctly
                word == "\u0001Exe" ||
                word == "\u0001StartDir" ||
                word == "\u0001icon" ||
                word == "\u0001ShortcutPath" ||
                word == "\u0001LaunchOptions" ||
                word == "\u0002IsHidden" ||
                word == "\u0002AllowDesktopConfig" ||
                word == "\u0002AllowOverlay" ||
                word == "\u0002OpenVR" ||
                word == "\u0002Devkit" ||
                word == "\u0002DevkitGameID" ||
                word == "\u0002DevkitOverrideAppID" ||
                //word == "\u0002LastPlayTime" || // NOTE: there is no NUL terminated at the end of it, so it is not read correctly
                // Which means that this bottom one is getting picked up by the LastPlayTime
                word == "\u0001FlatpakAppID"
            ) {
                key = word;
            }
            else if (word == "tags") {
                readingTags = true;
            }
            else if (key != "") {
                switch (key) {
                    //case "\u0002appid": shortcut.AppId = word; break;
                    case "\u0001AppName": shortcut.AppName = word; break;
                    case "\u0001Exe": shortcut.Exe = word.Trim('"'); break;
                    case "\u0001StartDir": shortcut.StartDir = word; break;
                    case "\u0001icon": shortcut.Icon = word; break;
                    case "\u0001ShortcutPath": shortcut.ShortcutPath = word; break;
                    case "\u0001LaunchOptions": shortcut.LaunchOptions = word; break;
                    case "\u0002IsHidden": shortcut.IsHidden = (word == "\u0001"); break;
                    case "\u0002AllowDesktopConfig": shortcut.AllowDesktopConfig = (word == "\u0001"); break;
                    case "\u0002AllowOverlay": shortcut.AllowOverlay = (word == "\u0001"); break;
                    case "\u0002OpenVR": shortcut.OpenVR = (word == "\u0001"); break;
                    case "\u0002Devkit": shortcut.Devkit = (word == "\u0001"); break;
                    case "\u0002DevkitGameID": shortcut.DevkitGameID = word; break;
                    case "\u0002DevkitOverrideAppID": shortcut.DevkitOverrideAppID = word; break;
                    //case "\u0002LastPlayTime": shortcut.LastPlayTime = word; break;
                    case "\u0001FlatpakAppID": shortcut.FlatpakAppID = word; break;
                    default:
                        break;
                }
                key = "";
            }
            else if (readingTags) {
                if (word.StartsWith("\u0001")) {
                    tagId = int.Parse(word.Substring("\u0001".Length));
                }
                else if (tagId >= 0) {
                    shortcut.Tags.Add(word);
                    tagId = -1;
                }
                else {
                    readingTags = false;
                }
            }
            word = "";
        }
        else {
            word += c;
        }
    }

    if (shortcut != null)
        result.Add(shortcut);

    return result;
}

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

Writing the shortcuts.vdf

public static string BuildShortcuts(List<Shortcut> shortcuts)
{
    string shortcutsString = "\0shortcuts\0";
    for (int i = 0; i < shortcuts.Count; i++)
    {
        shortcutsString += "\0" + i + "\0";
        shortcutsString += BuildShortcut(shortcuts[i]);
        shortcutsString += "\u0008";
    }
    shortcutsString += "\u0008\u0008";
    return shortcutsString;
}

private static string BuildShortcut(Shortcut shortcut)
{
    string shortcutString = "";
    string appIdBytes = shortcut.AppId != null ? Encoding.Latin1.GetString(shortcut.AppId) : "\0\0\0\0";
    // somewhere in the code i fucked it up and now i have to use 3 instead of a 4... Too bad!
    // it should still work on adding a shortcut
    string appIdStr = appIdBytes.Length >= 4 ? appIdBytes.Substring(0, 3) : appIdBytes;
    shortcutString += "\u0002appid\0" + appIdStr; // AppID is a 4-byte integer, so it doesn't have a NUL terminator
    shortcutString += "\u0001AppName\0" + shortcut.AppName + "\0"; // AppName
    shortcutString += "\u0001Exe\0" + shortcut.Exe + "\0"; // Exe
    shortcutString += "\u0001StartDir\0" + (shortcut.StartDir ?? "") + "\0"; // StartDir
    shortcutString += "\u0001icon\0" + shortcut.Icon + "\0"; // Icon
    shortcutString += "\u0001ShortcutPath\0" + shortcut.ShortcutPath + "\0"; // ShortcutPath
    shortcutString += "\u0001LaunchOptions\0" + shortcut.LaunchOptions + "\0"; // LaunchOptions
    shortcutString += "\u0002IsHidden\0" + GetBooleanChar(shortcut.IsHidden) + "\0\0\0"; // IsHidden
    shortcutString += "\u0002AllowDesktopConfig\0" + GetBooleanChar(shortcut.AllowDesktopConfig) + "\0\0\0"; // AllowDesktopConfig
    shortcutString += "\u0002AllowOverlay\0" + GetBooleanChar(shortcut.AllowOverlay) + "\0\0\0"; // AllowOverlay
    shortcutString += "\u0002OpenVR\0" + GetBooleanChar(shortcut.OpenVR) + "\0\0\0"; // OpenVR
    shortcutString += "\u0002Devkit\0" + GetBooleanChar(shortcut.Devkit) + "\0\0\0"; // Devkit
    shortcutString += "\u0001DevkitGameID\0" + (shortcut.DevkitGameID ?? "") + "\0"; // DevkitGameID
    shortcutString += "\u0002DevkitOverrideAppID\0" + (shortcut.DevkitOverrideAppID ?? "\0") + "\0\0\0"; // DevkitOverrideAppID
    shortcutString += "\u0002LastPlayTime\0" + (shortcut.LastPlayTime ?? "") + "\0"; // LastPlayTime
    shortcutString += "\u0001FlatpakAppID\0" + (shortcut.FlatpakAppID ?? "") + "\0"; // FlatpakAppID
    shortcutString += buildTags(shortcut.Tags); // Tags

    return shortcutString;
}

private static string buildTags(List<string> tags)
{
    var tagString = "\0tags\0";
    for (var i = 0; i < tags.Count; ++i)
    {
        tagString += "\u0001" + i + "\0" + tags[i] + "\0";
    }
    tagString += "\u0008";
    return tagString;
}


private static char GetBooleanChar(bool value)
{
    return value ? '\u0001' : '\0';
}

Get AppId for a shortcut

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

   var longValue = new Long(crcValue, crcValue, true);
   longValue = longValue.or(0x80000000);
   longValue = longValue.shl(32);
   longValue = longValue.or(0x02000000);
   return result.ToString();

   private string GetSHA1()
   {
       var data = Encoding.ASCII.GetBytes(Exe);
       var hashData = new SHA1Managed().ComputeHash(data);
       var hash = string.Empty;
       foreach (var b in hashData)
       {
           hash += b.ToString("X2");
       }
       return hash;
   }

This code works, it adds an app on the steam library, Steam must be shut downed in order to append it in the library

private static uint ComputeCRC32(byte[] bytes)
 {
     // Your CRC32 implementation here (or use the one you have)
     // Simple example:
     const uint Polynomial = 0xEDB88320;
     uint[] table = new uint[256];
     for (uint i = 0; i < 256; i++)
     {
         uint temp = i;
         for (int j = 0; j < 8; j++)
             temp = (temp & 1) == 1 ? (Polynomial ^ (temp >> 1)) : (temp >> 1);
         table[i] = temp;
     }

     uint crc = 0xFFFFFFFF;
     foreach (byte b in bytes)
     {
         byte index = (byte)((crc & 0xFF) ^ b);
         crc = (crc >> 8) ^ table[index];
     }
     return ~crc;
}

public static string GenerateEncrypedAppID(Shortcut shortcut)
{
    // Combine exe + launch options
    string combined = shortcut.Exe + shortcut.LaunchOptions;

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

    // Compute CRC32 of combined string
    uint crc = ComputeCRC32(data);

    // Set the shortcut flag bit
    uint encodedValue =  crc | 0x80000000;

    string hexString = encodedValue.ToString("X8");

    byte[] bytes = new byte[4];

    for (int i = 0; i < 4; i++)
        bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);

    // Convert byte array to a string where each char represents one byte
    string binaryString = Encoding.Latin1.GetString(bytes);
    return binaryString;
}

Main program for writing

class Program
{
    static void Main()
    {
        string exe = @"""A:\Path\to\your.exe""";

        Shortcut shortcut = new Shortcut
        {
            Exe = exe,
            LaunchOptions = "",
            AppName = "App name",
            StartDir = @"A:\Path\to\startDir\"
        };

        string encrypedValue = GenerateEncrypedAppID(shortcut);
        byte[] appIdRawBytes = Encoding.Latin1.GetBytes(encrypedValue);

        List<Shortcut> shortcuts = new List<Shortcut> {             
            new Shortcut
            {
                AppId = appIdRawBytes,
                AppName = "App name",
                Exe = exe,
                StartDir = @"A:\Path\to\startDir\",
                Icon = "",
                ShortcutPath = "",
                LaunchOptions = "",
                IsHidden = false,
                AllowDesktopConfig = true,
                AllowOverlay = true,
                OpenVR = false,
                Devkit = false,
                DevkitGameID = null,
                DevkitOverrideAppID = null,
                FlatpakAppID = null,
            }
        };

        string getStructureData = BuildShortcuts(shortcuts);
        File.WriteAllText(@"STEAMROOT\userdata\<user id>\config\shortcuts.vdf", getStructureData);
        Console.WriteLine("Proccess done, you can safely close the window.");
    }
}

See also