Steam Library Shortcuts

From Valve Developer Community
Jump to navigation Jump to search
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>();
}

VDF keys

This enum represents the mappings of data types to their binary representations

public enum VdfType : byte
{
	Dictionary 	= 0x00,
	Separator 	= Dictionary, // For readability
	String 		= 0x01,
	Int32 		= 0x02,
	End 		= 0x08
}

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.
private static VdfType ReadVdfType(BinaryReader reader)
{
	return (VdfType)reader.ReadByte();
}

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

	VdfType rootType = ReadVdfType(reader);
	string rootKey = ReadNullTerminatedString(reader);

	if (rootType != VdfType.Dictionary || rootKey != "shortcuts")
		throw new Exception("Unexpected root key or type");

	var shortcuts = new List<Shortcut>();

	while (true)
	{
		VdfType type = ReadVdfType(reader);
		if (type == VdfType.End) break; // end of shortcuts dictionary

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

		ReadNullTerminatedString(reader); // Important! Without this, it will throw an exception.

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

	return shortcuts;
}

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

	while (true)
	{
		VdfType type = ReadVdfType(reader);
		if (type == VdfType.End) break; // End of this shortcut dict

		string key = ReadNullTerminatedString(reader);

		switch (type)
		{
			case VdfType.Dictionary: // Nested dict, e.g. tags
				if (key == "tags")
					sc.Tags = ParseTags(reader);
				else
					SkipDictionary(reader);
				break;
			case VdfType.String:
				string val = ReadNullTerminatedString(reader);
				AssignString(sc, key, val);
				break;
			case VdfType.Int32:
				int intval = reader.ReadInt32();
				AssignInt(sc, key, intval);
				break;
			default:
				throw new FormatException($"Unknown type 0x{(byte)type:X2} for key '{key}'");
		}
	}

	return sc;
}

private static List<string> ParseTags(BinaryReader reader)
{
	var tags = new List<string>();
	while (true)
	{
		VdfType type = ReadVdfType(reader);
		if (type == VdfType.End) break; // end of tags dict

		if (type != VdfType.String)
			throw new FormatException($"Unexpected type in tags dict: 0x{(byte)type:X2}");

		ReadNullTerminatedString(reader); // Important! Without this, it will read the key and then the value.
										  // Then it will throw an exception because it will get the value and try to parse it as a key.
		string tag = ReadNullTerminatedString(reader); // only get the value
		tags.Add(tag);
	}
	return tags;
}

private static void SkipDictionary(BinaryReader reader)
{
	while (true)
	{
		VdfType type = ReadVdfType(reader);
		if (type == VdfType.End) break;

		switch (type)
		{
			case VdfType.Dictionary:
				SkipDictionary(reader);
				break;
			case VdfType.String:
				ReadNullTerminatedString(reader);
				break;
			case VdfType.Int32:
				reader.ReadInt32();
				break;
			default:
				throw new FormatException($"Unknown type 0x{(byte)type:X2} while skipping dictionary");
		}
	}
}

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.

private static void WriteVdfType(BinaryWriter writer, VdfType type)
{
	writer.Write((byte)type);
}

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 ?? [0, 0, 0, 0]);
		if (seenAppIds.Contains(appIdString))
			return [];

		seenAppIds.Add(appIdString);
	}

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

	WriteVdfType(writer, VdfType.Separator);
	writer.Write(Encoding.ASCII.GetBytes("shortcuts"));
	WriteVdfType(writer, VdfType.Separator);

	if (shortcuts.Count > 0)
	{
		for (int i = 0; i < shortcuts.Count; i++)
		{
			WriteVdfType(writer, VdfType.Separator);
			writer.Write(Encoding.ASCII.GetBytes(i.ToString()));
			WriteVdfType(writer, VdfType.Separator);

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

	WriteVdfType(writer, VdfType.End); // end of shortcuts dict
	WriteVdfType(writer, VdfType.End); // 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)
	WriteVdfType(writer, VdfType.Int32);
	writer.Write(Encoding.ASCII.GetBytes("appid"));
	WriteVdfType(writer, VdfType.Separator);
	if (shortcut.AppId != null && shortcut.AppId.Length == 4)
		writer.Write(shortcut.AppId);
	else
		writer.Write([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)
{
	WriteVdfType(writer, VdfType.String);
	writer.Write(Encoding.ASCII.GetBytes(key));
	WriteVdfType(writer, VdfType.Separator);
	writer.Write(Encoding.UTF8.GetBytes(value));
	WriteVdfType(writer, VdfType.Separator);
}

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

// Helper to write int32 fields
private static void WriteIntField(BinaryWriter writer, string key, int value)
{
	WriteVdfType(writer, VdfType.Int32);
	writer.Write(Encoding.ASCII.GetBytes(key));
	WriteVdfType(writer, VdfType.Separator);
	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)
{
	WriteVdfType(writer, VdfType.Dictionary);
	writer.Write(Encoding.ASCII.GetBytes("tags"));
	WriteVdfType(writer, VdfType.Separator);

	for (int i = 0; i < tags.Count; i++)
	{
		WriteVdfType(writer, VdfType.String);
		writer.Write(Encoding.ASCII.GetBytes(i.ToString()));
		WriteVdfType(writer, VdfType.Separator);
		writer.Write(Encoding.UTF8.GetBytes(tags[i]));
		WriteVdfType(writer, VdfType.Separator);
	}

	WriteVdfType(writer, VdfType.End);
	WriteVdfType(writer, VdfType.End);
}

// Writes a list of shortcuts to the Steam shortcuts.vdf file.
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);

	WriteVdfType(writer, VdfType.Dictionary);
	writer.Write(Encoding.UTF8.GetBytes("shortcuts"));
	WriteVdfType(writer, VdfType.Separator);

	for (int i = 0; i < shortcuts.Count; i++)
	{
		WriteVdfType(writer, VdfType.Separator);
		writer.Write(Encoding.UTF8.GetBytes(i.ToString()));
		WriteVdfType(writer, VdfType.Separator);

		var shortcutBytes = BuildShortcut(shortcuts[i]);
		writer.Write(shortcutBytes);  
	}

	// Write final end markers for shortcuts dictionary
	WriteVdfType(writer, VdfType.End);
	WriteVdfType(writer, VdfType.End);
}

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