[2026-05-27] doc: fix VDF shortcuts - use vdf.binary_load, not manual bytes

This commit is contained in:
Alexey Martemyanov
2026-05-27 16:52:20 +06:00
parent 99cedd2344
commit c43a0cd79d
+64 -69
View File
@@ -1,95 +1,90 @@
# Non-Steam Shortcuts через Binary VDF
# Non-Steam Shortcuts через VDF
## Формат файла
## Главное правило
Shortcuts хранятся в бинарном VDF:
`~/.steam/steam/userdata/147839491/config/shortcuts.vdf`
**Steam должен быть выключен** перед записью shortcuts.vdf. Если Steam работает — он перезапишет файл из памяти при следующем запуске.
**Это НЕ текстовый файл** — редактировать через обычную замену строк нельзя, потому что:
- Каждое поле хранится с длиной строки
- Замена строки на другую длины ломает весь файл
- Steam после запуска может перезаписать сломанный файл, удалив все non-Steam игры
## Единственный правильный способ — vdf.binary_load
## Безопасный способ — Python
Использовать модуль `vdf` (установлен на HTPC через pip). **Никогда** не редактировать бинарный VDF через ручную сборку байтов или замену строк — это ломает файл.
```python
import os, struct
import os, vdf, shutil, binascii, struct
vdf_path = os.path.expanduser('~/.steam/steam/userdata/147839491/config/shortcuts.vdf')
# 1. Всегда делать бэкап
import shutil
# 1. Бэкап
shutil.copy2(vdf_path, vdf_path + '.bak')
# 2. Загрузить существующий
with open(vdf_path, 'rb') as f:
data = bytearray(f.read())
data = vdf.binary_load(f)
# Helper: string field
def sf(name, value):
nb = name.encode() + b'\x00'
vb = value.encode() + b'\x00'
return bytes([0x01, len(nb)]) + nb + bytes([len(vb)]) + vb
shortcuts = data.get('shortcuts', {})
# Helper: int field
def zf(name, value):
nb = name.encode() + b'\x00'
vb = struct.pack('<I', value)
return bytes([0x02, len(nb)]) + nb + bytes([len(vb)]) + vb
# 3. Вычислить appid (алгоритм Steam)
def compute_appid(name, exe):
key = exe + name
top = binascii.crc32(key.encode('utf-8')) | 0x80000000
return struct.unpack('i', struct.pack('I', top))[0]
# Build the entire entry with all fields Steam expects
entry = b''
# 4. Создать entry
next_idx = str(len(shortcuts))
shortcuts[next_idx] = {
'AppName': 'Game Name',
'Exe': '/path/to/game.exe',
'StartDir': '/path/to/',
'LaunchOptions': '',
'appid': compute_appid('Game Name', '/path/to/game.exe'),
'IsHidden': 0,
'AllowDesktopConfig': 1,
'AllowOverlay': 1,
'OpenVR': 0,
'Devkit': 0,
'DevkitGameID': '',
'DevkitOverrideAppID': 0,
'LastPlayTime': 0,
'FlatpakAppID': '',
'sortas': '',
'tags': {},
}
# appid (int field, type 0x02)
appid_n = b'appid\x00'
appid_v = struct.pack('<I', APPID) # unsigned 32-bit int
entry += bytes([0x02, len(appid_n)]) + appid_n + bytes([len(appid_v)]) + appid_v
entry += sf('AppName', 'Name in Library')
entry += sf('Exe', '/path/to/game.exe')
entry += sf('StartDir', '/path/to/')
entry += sf('icon', '')
entry += sf('ShortcutPath', '')
entry += sf('LaunchOptions', '')
# Boolean fields (int, 0 or 1)
entry += zf('IsHidden', 0)
entry += zf('AllowDesktopConfig', 1)
entry += zf('AllowOverlay', 1)
entry += zf('OpenVR', 0)
entry += zf('Devkit', 0)
entry += sf('DevkitGameID', '')
entry += zf('DevkitOverrideAppID', 0)
entry += zf('LastPlayTime', 0)
entry += sf('FlatpakAppID', '')
entry += sf('sortas', '')
# tags array (must end with \x08\x08\x08)
tags_n = b'tags\x00'
entry += bytes([0x00, len(tags_n)]) + tags_n + b'\x08'
entry += b'\x08\x08\x08'
# 5. Записать
data['shortcuts'] = shortcuts
with open(vdf_path, 'wb') as f:
vdf.binary_dump(data, f)
```
## Добавление нового shortcut (к существующим)
## CompatToolMapping (Proton) в config.vdf
1. Найти начало и конец последней записи
2. Вставить новую запись перед закрывающими байтами файла
3. Записать файл целиком
После добавления shortcut нужно прописать Proton в текстовом `config.vdf`:
Структура файла: `\x00shortcuts\x00\x00` + `\x30\x00` + [entry1] + [entry2] + ... + `\x08\x08`
```python
import vdf
## Перезагрузка Steam без рестарта
CONFIG_PATH = os.path.expanduser('~/.local/share/Steam/config/config.vdf')
with open(CONFIG_PATH, 'r') as f:
config = vdf.load(f)
```bash
DISPLAY=:0 steam 'steam://reloadshortcuts'
compat_map = config['InstallConfigStore']['Software']['Valve']['Steam'].setdefault('CompatToolMapping', {})
compat_map[str(appid_signed)] = {
'name': 'GE-Proton10-34', # или 'proton_experimental'
'config': '',
'Priority': '250',
}
with open(CONFIG_PATH, 'w') as f:
vdf.dump(config, f)
```
**Важно:** если Steam уже переписал сломанный файл — изменения не откатятся. Нужно восстанавливать из бэкапа, а потом форсить reload.
## Полный скилл
## Важно
Вся процедура (shortcuts + Proton + artwork) описана в скилле `steam-emulator-shortcuts`. Смотреть там.
- Никогда не делать replace строк разной длины
- Всегда держать бэкап
- Если non-Steam игры пропали — восстановить из бэкапа, сделать `reloadshortcuts`
- AppID можно взять любой unsigned 32-bit (лучше > 2000000000 чтобы не конфликтовать с реальными Steam AppID)
## Бэкап
Автоматический бэкап `shortcuts.vdf` делается ежедневно скриптом `backup-saves.sh` на HTPC, версии хранятся в `/run/media/bazzite/Data/backups/saves/shortcuts/` (последние 30 копий).
## Перезагрузка Steam
После записи VDF — перезапустить Steam, чтобы он подхватил изменения.