Steam Library Shortcuts: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
No edit summary
(Adding a new property)
Line 12: Line 12:
public class Shortcut
public class Shortcut
{
{
    public byte[]? AppId { get; set; }
public byte[]? AppId { get; set; }
    public string? AppName { get; set; }
public string? AppName { get; set; }
    public string? Exe { get; set; }
public string? Exe { get; set; }
    public string? StartDir { get; set; }
public string? StartDir { get; set; }
    public string? Icon { get; set; }
public string? Icon { get; set; }
    public string? ShortcutPath { get; set; }
public string? ShortcutPath { get; set; }
    public string? LaunchOptions { get; set; }
public string? LaunchOptions { get; set; }
    public bool IsHidden { get; set; }
public bool IsHidden { get; set; }
    public bool AllowDesktopConfig { get; set; }
public bool AllowDesktopConfig { get; set; }
    public bool AllowOverlay { get; set; }
public bool AllowOverlay { get; set; }
    public bool OpenVR { get; set; }
public bool OpenVR { get; set; }
    public bool Devkit { get; set; }
public bool Devkit { get; set; }
    public string? DevkitGameID { get; set; }
public string? DevkitGameID { get; set; }
    public byte[]? DevkitOverrideAppID { get; set; }
public byte[]? DevkitOverrideAppID { get; set; }
    public byte[]? 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 string? SortAs { get; set; }
public List<string> Tags { get; set; } = new List<string>();
}
}
</syntaxhighlight>
</syntaxhighlight>
Line 38: Line 39:
public static List<Shortcut> ReadShortcuts(string filepath)
public static List<Shortcut> ReadShortcuts(string filepath)
{
{
    if (!File.Exists(filepath))
if (!File.Exists(filepath) || new FileInfo(filepath).Length == 0)
{
return [];  
    Console.WriteLine($"The file '{filepath}' does not exist.");
 
    return [];  
using var fs = File.OpenRead(filepath);
}
using var reader = new BinaryReader(fs, Encoding.UTF8);


if (new FileInfo(filepath).Length == 0)
byte rootType = reader.ReadByte();
{
string rootKey = ReadNullTerminatedString(reader);
    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();
if (rootType != 0x00 || rootKey != "shortcuts")
    string rootKey = ReadNullTerminatedString(reader);
return[];


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


    var shortcuts = new List<Shortcut>();
while (true)
{
byte type = 0x00;


    while (true)
do {
    {
if (reader.BaseStream.Position >= reader.BaseStream.Length)
        byte type = reader.ReadByte();
break;
        if (type == 0x08) break; // end of shortcuts dictionary
type = reader.ReadByte();
}
while (type == 0x08);


        if (type != 0x00)
if (reader.BaseStream.Position >= reader.BaseStream.Length)
            throw new Exception($"Expected dictionary key (0x00), got 0x{type:X2}");
break;


        string index = ReadNullTerminatedString(reader);
if (type != 0x00)
return [];


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


    return shortcuts;
return shortcuts;
}
}


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


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


        string key = ReadNullTerminatedString(reader);
string key = ReadNullTerminatedString(reader);


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


private static List<string> ParseTags(BinaryReader reader)
private static List<string> ParseTags(BinaryReader reader)
{
{
    var tags = new List<string>();
var tags = new List<string>();
    while (true)
while (true)
    {
{
        byte type = reader.ReadByte();
byte type = reader.ReadByte();
        if (type == 0x08) break; // end of tags dict
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}");


        if (type != 0x01)
string val = ReadNullTerminatedString(reader);
            throw new Exception($"Unexpected type in tags dict: 0x{type:X2}");
tags.Add(val);
 
}
        string val = ReadNullTerminatedString(reader);
return tags;
        tags.Add(val);
    }
    return tags;
}
}


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


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


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


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


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


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


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


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


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


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


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


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


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


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


    return ms.ToArray();
return ms.ToArray();
}
}


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


    // Write AppID: type (0x02), key, null terminator, then 4 raw bytes (no terminator)
// Write AppID: type (0x02), key, null terminator, then 4 raw bytes (no terminator)
    writer.Write((byte)0x02); // int32 type
writer.Write((byte)0x02); // int32 type
    writer.Write(Encoding.ASCII.GetBytes("appid"));
writer.Write(Encoding.ASCII.GetBytes("appid"));
    writer.Write((byte)0x00);
writer.Write((byte)0x00);
    if (shortcut.AppId != null && shortcut.AppId.Length == 4)
if (shortcut.AppId != null && shortcut.AppId.Length == 4)
        writer.Write(shortcut.AppId);
writer.Write(shortcut.AppId);
    else
else
        writer.Write(new byte[4] { 0, 0, 0, 0 });
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, "AppName", shortcut.AppName);
WriteStringField(writer, "Exe", shortcut.Exe);
    WriteStringField(writer, "Exe", shortcut.Exe);
WriteStringField(writer, "StartDir", shortcut.StartDir ?? "");
    WriteStringField(writer, "StartDir", shortcut.StartDir ?? "");
WriteStringField(writer, "icon", shortcut.Icon ?? "");
    WriteStringField(writer, "icon", shortcut.Icon ?? "");
WriteStringField(writer, "ShortcutPath", shortcut.ShortcutPath ?? "");
    WriteStringField(writer, "ShortcutPath", shortcut.ShortcutPath ?? "");
WriteStringField(writer, "LaunchOptions", shortcut.LaunchOptions ?? "");
    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);


    // Write booleans: type (0x02), key, null, 4-byte int (0 or 1)
return ms.ToArray();
    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
  // Helper to write string fields
  private static void WriteStringField(BinaryWriter writer, string key, string value)
  private static void WriteStringField(BinaryWriter writer, string key, string value)
  {
  {
    writer.Write((byte)0x01); // string type
writer.Write((byte)0x01); // string type
    writer.Write(Encoding.ASCII.GetBytes(key));
writer.Write(Encoding.ASCII.GetBytes(key));
    writer.Write((byte)0x00);
writer.Write((byte)0x00);
    writer.Write(Encoding.UTF8.GetBytes(value));
writer.Write(Encoding.UTF8.GetBytes(value));
    writer.Write((byte)0x00);
writer.Write((byte)0x00);
  }
  }


Line 316: Line 304:
  private static void WriteBoolField(BinaryWriter writer, string key, bool value)
  private static void WriteBoolField(BinaryWriter writer, string key, bool value)
  {
  {
    writer.Write((byte)0x02); // int32 type
writer.Write((byte)0x02); // int32 type
    writer.Write(Encoding.ASCII.GetBytes(key));
writer.Write(Encoding.ASCII.GetBytes(key));
    writer.Write((byte)0x00);
writer.Write((byte)0x00);
    writer.Write(value ? 1 : 0);
writer.Write(value ? 1 : 0);
  }
  }


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


Line 339: Line 327:
  private static void WriteTags(BinaryWriter writer, List<string> tags)
  private static void WriteTags(BinaryWriter writer, List<string> tags)
  {
  {
    writer.Write((byte)0x00); // begin dict
writer.Write((byte)0x00); // begin dict
    writer.Write(Encoding.ASCII.GetBytes("tags"));
writer.Write(Encoding.ASCII.GetBytes("tags"));
    writer.Write((byte)0x00);
writer.Write((byte)0x00);


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


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


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


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


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


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


    // Write final end markers for shortcuts dictionary
// Write final end markers for shortcuts dictionary
    writer.Write((byte)0x08);
writer.Write((byte)0x08);
    writer.Write((byte)0x08);
writer.Write((byte)0x08);
  }
  }
</syntaxhighlight>
</syntaxhighlight>
Line 390: Line 378:
private static uint ComputeCRC32(byte[] bytes)
private static uint ComputeCRC32(byte[] bytes)
{
{
    const uint Polynomial = 0xEDB88320;
const uint Polynomial = 0xEDB88320;
    uint[] table = new uint[256];
uint[] table = new uint[256];
    for (uint i = 0; i < 256; i++)
for (uint i = 0; i < 256; i++)
    {
{
        uint temp = i;
uint temp = i;
        for (int j = 0; j < 8; j++)
for (int j = 0; j < 8; j++)
            temp = (temp & 1) == 1 ? (Polynomial ^ (temp >> 1)) : (temp >> 1);
temp = (temp & 1) == 1 ? (Polynomial ^ (temp >> 1)) : (temp >> 1);
        table[i] = temp;
table[i] = temp;
    }
}


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


public static uint GenerateAppID(Shortcut shortcut)
public static uint GenerateAppID(Shortcut shortcut)
{
{
    string combined = shortcut.AppName + shortcut.Exe + "\0";
string combined = shortcut.AppName + shortcut.Exe + "\0";


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


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


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


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


    return Encoding.Latin1.GetString(bytes);
return Encoding.Latin1.GetString(bytes);
}
}
</syntaxhighlight>
</syntaxhighlight>
Line 444: Line 432:
}
}
    try
try
    {
{
        // Ensure the destination directory exists
// Ensure the destination directory exists
        System.IO.Directory.CreateDirectory(destinationFolder);
System.IO.Directory.CreateDirectory(destinationFolder);


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


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


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


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


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


        byte[] shortcutData = (YourClassName.)BuildShortcuts(shortcuts);
byte[] shortcutData = (YourClassName.)BuildShortcuts(shortcuts);
        File.WriteAllBytes(@"C:\Program Files (x86)\Steam\userdata\<user id>\config\shortcuts.vdf", shortcutData);
File.WriteAllBytes(@"C:\Program Files (x86)\Steam\userdata\<user id>\config\shortcuts.vdf", shortcutData);
        (YourClassName.)CreateSteamGrid(appId,  
(YourClassName.)CreateSteamGrid(appId,  
            @"C:\Path\to\template images\",
@"C:\Path\to\template images\",
            @"C:\Program Files (x86)\Steam\userdata\<user id>\config\grid\");
@"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>
Line 520: Line 508:
class Program
class Program
{
{
    static void Main(string[] args)
static void Main(string[] args)
    {
{
        Console.WriteLine("Enter the path to your shortcuts file:");
Console.WriteLine("Enter the path to your shortcuts file:");
        string? inputPath = Console.ReadLine();
string? inputPath = Console.ReadLine();
        inputPath = inputPath?.Replace('\\', '/'); // Normalize path separators
inputPath = inputPath?.Replace('\\', '/'); // Normalize path separators
        string filePath = string.IsNullOrWhiteSpace(inputPath)
string filePath = string.IsNullOrWhiteSpace(inputPath)
            ? "shortcuts.vdf"
? "shortcuts.vdf"
            : (inputPath.EndsWith(".vdf", StringComparison.OrdinalIgnoreCase) ? inputPath : $"{inputPath.TrimEnd('/', '\\')}/shortcuts.vdf");
: (inputPath.EndsWith(".vdf", StringComparison.OrdinalIgnoreCase) ? inputPath : $"{inputPath.TrimEnd('/', '\\')}/shortcuts.vdf");


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


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


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

Revision as of 01:45, 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")
		return[];

	var shortcuts = new List<Shortcut>();

	while (true)
	{
		byte type = 0x00;

		do {
			if (reader.BaseStream.Position >= reader.BaseStream.Length)
				break;
			type = reader.ReadByte();
		}
		while (type == 0x08);

		if (reader.BaseStream.Position >= reader.BaseStream.Length)
			break;

		if (type != 0x00)
			return [];

		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