Steam Shortcuts Documentation - CorporalQuesadilla/Steam-Shortcut-Manager GitHub Wiki
Introduction
Steam shortcuts allow games and other applications to be launched through Steam. This is beneficial for multiple reasons, such as having a unified library for all games on your computer or easy-to-modify custom controller mappings for non-Steam games.
These shortcuts can also be accessed by following a steam:// URL based on its local path and name - see below for more info.
The miniature database holding all the information about the non-Steam shortcuts for that device are stored locally in Steam\userdata\ID_HERE\config\shortcuts.vdf
File Structure
The file is a simple, single-line database (there are never any line breaks). It uses a few weird ASCII characters as delimiters and to denote data types. I show the UTF-8 (I think?) encoded representation of these characters to represent them in this document, as they typically are rendered completely invisible by most text editors or browsers. This is also the representation used in my program (Python based) to insert/read these characters.
Header
The file always begins with
\x00shortcuts\x00
and is immediately followed by the Body.
Body
This contains all shortcut entries consecutively (see entry structure below) and ends with the Footer.
Footer
Immediately following the body are the two following characters, signifying the end of the file.
\x08\x08
Shortcut Entry Structure
Each entry follows this pattern following pattern. I think making an actual table makes this harder to understand, so I'll show you an excerpt of my code. As long as you understand basic concatenation, you should be able to read this.
# Key # Data Type # Internal Name # Delimiter # Input # Delimiter
full_entryID = '\x00' + var_entryID + '\x00'
full_appName = '\x01' + 'appname' + '\x00' + var_appName + '\x00'
full_quotedPath = '\x01' + 'exe' + '\x00' + var_unquotedPath + '\x00'
full_startDir = '\x01' + 'StartDir' + '\x00' + var_startDir + '\x00'
full_iconPath = '\x01' + 'icon' + '\x00' + var_iconPath + '\x00'
full_shortcutPath = '\x01' + 'ShortcutPath' + '\x00' + var_shortcutPath + '\x00'
full_launchOptions = '\x01' + 'LaunchOptions' + '\x00' + var_launchOptions + '\x00'
full_isHidden = '\x02' + 'IsHidden' + '\x00' + var_isHidden + '\x00\x00\x00'
full_allowDeskConf = '\x02' + 'AllowDesktopConfig' + '\x00' + var_allowDeskConf + '\x00\x00\x00'
full_allowOverlay = '\x02' + 'AllowOverlay' + '\x00' + var_allowOverlay + '\x00\x00\x00'
full_openVR = '\x02' + 'OpenVR' + '\x00' + var_openVR + '\x00\x00\x00'
full_lastPlayTime = '\x02' + 'LastPlayTime' + '\x00' + var_lastPlayTime
full_tags = '\x00' + 'tags' + '\x00' + var_tags + '\x08\x08'
newEntry = full_entryID + full_appName + full_quotedPath + full_startDir + full_iconPath + full_shortcutPath + full_launchOptions + full_isHidden + full_allowDeskConf + full_allowOverlay + full_openVR + full_tags
return newEntry
As you can see, there are several parts to an entry, all on one line using special delimiters. Each entry is consecutive - the delimiters between entries are included with the above formula. As such, my program simply inserts entries before the final two characters of the file. I also try to detect the last known entryID and increment that by 1.
Notes
- The data type always comes before the attribute it refers to. For the record, \x01 indicates String, \x02 indicates boolean, \x00 indicates list..
- Most Strings seem like they must be encapsulated in double quotes. Some, like Launch Options or App Name, seem to work fine without it.
- Bools treat '\x01' as True and '\x00' as False.
- Lists are as follows: '\x01' + index + '\x00' + Contents + '\x00'. Just repeat this for multiple entries and increment the index. The only list is the 'tags' - the categories you want it to appear under. Contents refers to the string representing the category name.
- I have no idea about Date. Not sure why LastPlayTime is marked as a bool - it's just 4 characters, usually ending in '[' (maybe?). All 4 being '\x00' is fine too (default for "Never Played"?).
Attributes
Attribute | Explanation | Data Type |
---|---|---|
Entry ID | The shortcut's ID. First shortcut is 0. Not sure if there's any harm skipping around. | Integer |
App Name | Name of the shortcut | String |
exe | Quoted path to the executable OR its URL. Yes, even steam:// links work! | String - Quoted Path |
StartDir | Quoted path to the directory above lies in. | String - Quoted Path |
icon | Optional place to source the icon. Empty string is fine, but anything else seems to need double quote encapsulation. BTW, I'm not sure where the Grid/BigPicture image comes from. Maybe this path? | String - Quoted Path |
Shortcut Path | No idea what this is. Leaving blank (literally 'the empty string') works fine | String - Quoted Path? |
Launch Options | Any arguments to pass to the executable. Again, probably in quotes. | String - Quoted? |
Is Hidden | If it's in the "Hidden" subset of the library. | Boolean |
Allow Desktop Config | Use controller's Desktop Configurations in this game. | Boolean |
Allow Steam Overlay | Allows hotkey (typically shift+tab) to open Steam overlay in-game. | Boolean |
In VR library | For the 'VR Library' Category. | Boolean |
Last Play Time | Date last played. "Never played" is four consecutive \x00. | DateTime? |
Tags | Any categories you want it to appear in. | List |
URL Structure
Every shortcut, when placed on the desktop, is given a steam:// shortcut. I don't understand quite how this works, just that it's a CRC of App Name and exe. My code can successfully generate this when adding shortcuts. However, it uses an excerpt of code from Scott Rice's ICE program. Below is his explanation - see the getURL function in shortcuts.py for how I use it in context, or his project's file of interest here.
"""
This filename [sic - in this case, refers to URL appID] is a 64bit
integer, where the first 32bits are a CRC32 based off of the name and
target (with the added condition that the first bit is always high), and
the last 32bits are 0x02000000.
"""
# This will seem really strange (where I got all of these values), but I
# got the xor_in and xor_out from disassembling the steamui library for
# OSX. The reflect_in, reflect_out, and poly I figured out via trial and
# error.
algorithm = crc_algorithms.Crc(width = 32, poly = 0x04C11DB7, reflect_in = True, xor_in = 0xffffffff, reflect_out = True, xor_out = 0xffffffff)
input_string = ''.join([inputTuple[2],inputTuple[1]])
top_32 = algorithm.bit_by_bit(input_string) | 0x80000000
full_64 = (top_32 << 32) | 0x02000000
return str(full_64)