[2026-05-27] doc: fix VDF shortcuts - use vdf.binary_load, not manual bytes
This commit is contained in:
@@ -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, чтобы он подхватил изменения.
|
||||
|
||||
Reference in New Issue
Block a user