[2026-05-27] doc: non-Steam binary VDF shortcuts guide
This commit is contained in:
@@ -0,0 +1,95 @@
|
|||||||
|
# Non-Steam Shortcuts через Binary VDF
|
||||||
|
|
||||||
|
## Формат файла
|
||||||
|
|
||||||
|
Shortcuts хранятся в бинарном VDF:
|
||||||
|
`~/.steam/steam/userdata/147839491/config/shortcuts.vdf`
|
||||||
|
|
||||||
|
**Это НЕ текстовый файл** — редактировать через обычную замену строк нельзя, потому что:
|
||||||
|
- Каждое поле хранится с длиной строки
|
||||||
|
- Замена строки на другую длины ломает весь файл
|
||||||
|
- Steam после запуска может перезаписать сломанный файл, удалив все non-Steam игры
|
||||||
|
|
||||||
|
## Безопасный способ — Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os, struct
|
||||||
|
|
||||||
|
vdf_path = os.path.expanduser('~/.steam/steam/userdata/147839491/config/shortcuts.vdf')
|
||||||
|
|
||||||
|
# 1. Всегда делать бэкап
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(vdf_path, vdf_path + '.bak')
|
||||||
|
|
||||||
|
with open(vdf_path, 'rb') as f:
|
||||||
|
data = bytearray(f.read())
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Build the entire entry with all fields Steam expects
|
||||||
|
entry = b''
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Добавление нового shortcut (к существующим)
|
||||||
|
|
||||||
|
1. Найти начало и конец последней записи
|
||||||
|
2. Вставить новую запись перед закрывающими байтами файла
|
||||||
|
3. Записать файл целиком
|
||||||
|
|
||||||
|
Структура файла: `\x00shortcuts\x00\x00` + `\x30\x00` + [entry1] + [entry2] + ... + `\x08\x08`
|
||||||
|
|
||||||
|
## Перезагрузка Steam без рестарта
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DISPLAY=:0 steam 'steam://reloadshortcuts'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно:** если Steam уже переписал сломанный файл — изменения не откатятся. Нужно восстанавливать из бэкапа, а потом форсить reload.
|
||||||
|
|
||||||
|
## Важно
|
||||||
|
|
||||||
|
- Никогда не делать replace строк разной длины
|
||||||
|
- Всегда держать бэкап
|
||||||
|
- Если non-Steam игры пропали — восстановить из бэкапа, сделать `reloadshortcuts`
|
||||||
|
- AppID можно взять любой unsigned 32-bit (лучше > 2000000000 чтобы не конфликтовать с реальными Steam AppID)
|
||||||
@@ -31,3 +31,11 @@ _Snapshot generated at 13:31 local (after SINCE 13:01). No task changes or comme
|
|||||||
Inbox quiet — nothing new.
|
Inbox quiet — nothing new.
|
||||||
|
|
||||||
_No tasks modified since SINCE (Asana MCP: assigned + followed, both empty). No active meetings (MS365 clean). Alex AFK today (holiday)._
|
_No tasks modified since SINCE (Asana MCP: assigned + followed, both empty). No active meetings (MS365 clean). Alex AFK today (holiday)._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inbox Triage — 2026-05-27 15:32 (since 2026-05-27T16:01:02)
|
||||||
|
|
||||||
|
Inbox quiet — nothing new.
|
||||||
|
|
||||||
|
_No tasks modified since SINCE (Asana MCP: assigned + followed, both empty). No active meetings (MS365 clean). Alex AFK today (holiday). Not posting to #inbox per user directive._
|
||||||
|
|||||||
Reference in New Issue
Block a user