InquirerZamboanga del Sur launches tourism website, tourist policeCNN TürkAnTuTu'da rekor kırdıESPNJJ Gabriel and the 'nightmare' battle for Premier League academy talentThe Jerusalem PostIran is renewing progress on its nuclear program, Netanyahu tells UAE President MBZ - reportBollywood HungamaEXCLUSIVE: CBFC orders 45-50 cuts for Dhurandhar The Revenge’s TV Premiere; U/A 16+ version is 10 minutes shorterDaily MaverickPOLITICALLY AWEH: Inside the Gathering 2026 — South Africa’s cities crisis collides with chaotic election preparationPunchKebbi warns civil servants against lateness, absenteeismUOLAdvogado morre após quadra de tênis desabar durante temporal no ParanáThe RegisterUK rail cops' £320K face-scanning spree nets zero matchesThe Straits TimesCommunity initiative in Yishun encourages artists, public to showcase creative works at 8 bus stopsالنهاروفاة زوجة المنتج صادق الصباح.. تفاصيل الدفن والعزاء3DNewsAnthropic признала опасную зависимость от Amazon и Google — они одновременно являются её покупателями, продавцами и конкурентами
The Daily Newsstand · Free, Always
Wednesday, September 30, 2026

Одна раскладка на все игры: как я обошёл ограничение Steam Deck и нашёл контроллер номер 15

Translate

На Steam Deck нельзя взять раскладку управления из одной игры и поставить её в другую. Настроил в одной визуальной новелле удобное управление, сохранил его под названием «Для чтения», открываешь следующую новеллу — а в списке пусто. Valve об этом давно просят, тема в сообществе Steam висит не первый год.

Мне это надоело, и я решил научить этому свою программу DeckDrop. По дороге выяснилось, почему Steam так себя ведёт, где он на самом деле хранит раскладки и что встроенное управление Steam Deck для самого Steam — это контроллер номер 15, а не 0. А ещё — как проверять код, который работает с Steam, когда самого Steam рядом нет.

Раскладки контроллера в карточке игры

Раскладки контроллера в карточке игры

Что за DeckDrop

Коротко, для тех, кто не читал прошлую статью. DeckDrop — маленький веб-сервер, который живёт на самом деке. С телефона или ПК в той же сети кидаешь ссылку, и дек сам скачивает игру, распаковывает и добавляет в Steam с Proton и обложками. Это один файл на Python, только стандартная библиотека, без root и без режима разработчика.

Для этой статьи важно одно: запущенным Steam DeckDrop управляет так же, как Decky Loader. Включает локальный отладочный порт CEF (файл-маркер .cef-enable-remote-debugging в папке Steam) и выполняет JavaScript в контексте SharedJSContext, где живёт объект SteamClient — внутренний API, которым пользуется сам интерфейс Steam.

Как я исследовал Steam

Документации на всё это нет, поэтому я шёл экспериментами: гипотеза, проверка на живом деке, выводы, следующая гипотеза. Для каждой проверки я делал небольшой скрипт-разведчик, который запускается прямо на деке. Главное правило: по умолчанию он только читает. Всё, что он пишет, делается по отдельному флагу и только в его собственные пробные файлы, а для отката есть --cleanup. Портить свой Steam ради эксперимента не хотелось.

Результат скрипт отдаёт по ссылке в локальной сети: открываешь её на телефоне и сразу видишь отчёт, без похода в режим рабочего стола. Отдаёт он только этот отчёт: на любой путь, включая /../../etc/passwd, возвращается тот же файл, это проверено тестом.

Таких раундов понадобилось четыре.

Раунд 1: где Steam хранит раскладки

Раскладки лежат вот здесь:

~/.local/share/Steam/steamapps/common/Steam Controller Configs/<AccountID>/config/<игра>/

И сразу стало понятно, почему раскладка «привязана» к игре. Текущая раскладка игры — файл controller_neptune.vdf (Neptune — внутреннее имя контроллера Steam Deck). Когда сохраняешь свою раскладку под названием, Steam кладёт её файлом <название в нижнем регистре>_<n>.vdf в папку этой же игры. А список «Ваши раскладки» Steam собирает только из папки текущей игры. При этом внутри файла обычные назначения кнопок, к игре он ничем не привязан — просто лежит не там, где его ищут другие игры.

Какая раскладка выбрана, хранится отдельно, в configset_controller_neptune.vdf. Для каждой игры там одно из трёх:

  • "autosave" "1" — раскладка, отредактированная прямо в игре;

  • "template" "CLOUD_<appid>/<имя>_<n>" — выбрана сохранённая раскладка;

  • "workshop" "<id>" — раскладка из мастерской.

Для игр из магазина папка называется по AppID. А вот со сторонними играми интереснее: папка называется по имени ярлыка в нижнем регистре без знаков препинания, при этом пробелы и восклицательный знак остаются. Ярлык «Cool Game: Director's Cut» даст папку cool game directors cut, а ярлык Game.exe — папку gameexe.

Тут же нашлась ловушка. Папка называется по имени ярлыка на момент, когда Steam её впервые создал. Если ярлык потом переименовать, раскладка так и останется в старой папке. У меня был ровно такой случай: игра добавилась как Game.exe, я переименовал её по-человечески, а раскладка так и живёт в gameexe. Угадать папку по текущему имени нельзя, поэтому DeckDrop перебирает варианты: текущее имя ярлыка, имя, под которым он сам добавлял игру, имя exe с расширением и без. Позже нашёлся способ надёжнее — спросить саму Steam, об этом ниже.

Ещё из мелочей:

  • Бывают раскладки не на тип контроллера, а на конкретное устройство: рядом лежат <серийник>.vdf и configset_<серийник>.vdf. Это потом ещё пригодится.

  • У шаблонов Valve в controller_base/templates название читалось как пустое. Причина — BOM в начале файла, читать надо в utf-8-sig.

  • У SteamClient.Input около сотни методов, и у всех length равен 0: это нативные обёртки, сигнатуры по ним не узнать. Зато по названиям видно, что искать: SetSelectedConfigForApp, GetConfigForAppAndController, QueryControllerConfigsForApp.

Раунд 2: шаблоны видны в каждой игре

Раз «свои» раскладки Steam ищет только в папке игры, нужно место, которое он показывает всем играм. Такое место есть — шаблоны. Эксперимент был простой: скопировать раскладку в ~/.local/share/Steam/controller_base/templates/ под именем controller_neptune_deckdrop_probe.vdf и поменять в ней название на «DeckDrop probe».

Проверил на деке: раскладка появилась во вкладке «Шаблоны» у других игр. Это и есть обход. Файл в папке шаблонов Steam предлагает каждой игре, и на самом деке его можно выбрать без всякого DeckDrop.

Раунд 3: кнопка «Применить» не работает

Выбирать шаблон на деке — уже хорошо, но хотелось кнопку «Применить» прямо в карточке игры на телефоне. В описании пулл-реквеста к одному Decky-плагину нашлась подсказка: у SetSelectedConfigForApp пять аргументов, а с четырьмя он возвращается без ошибки и ничего не выбирает. Из того PR я взял только этот факт. Код оттуда не брал, написал свой.

Первая попытка:

SetSelectedConfigForApp(appid, 0, "template://controller_neptune_deckdrop_probe.vdf", false, 1)

Не сработало. Ни ошибки, ни результата. Номер контроллера я взял за 0 по аналогии с другими API — и зря. Стал разбираться, и всё прояснилось:

  • GetConfigForAppAndController(appid, i) для i от 0 до 3 возвращал bConfigurationEnabled: false. Контроллеров с такими номерами просто нет.

  • В глобальном ControllerStore.m_controllerList нашёлся «Steam Deck Controller» с nControllerIndex: 15 и eControllerType: 4. И m_nLastValidActiveControllerIndex тоже 15.

Вот и главная находка: встроенное управление Steam Deck для Steam — контроллер номер 15, а не 0.

Раунд 4: заработало

Теперь номер брался из ControllerStore. Перед переключением я копировал всю папку Steam Controller Configs в резервную, а после смотрел, какие файлы изменил Steam. Результат на деке: раскладка применилась.

До вызова GetConfigForAppAndController(appid, 15) возвращал:

{"Title": "Геймпад с управлением камерой",
 "URL": "autosave:///home/deck/.local/share/Steam/steamapps/common/Steam Controller Configs/<id>/config/<игра>/controller_neptune.vdf",
 "ProgenitorURL": "default://<игра>", "eSelectionType": 0, "bConfigurationEnabled": true}

После:

{"Title": "DeckDrop probe",
 "URL": "template://controller_neptune_deckdrop_probe.vdf",
 "eExportType": 1, "eSelectionType": 1, "bSelected": true}

Изменился только один файл — configset_<серийник>.vdf, где у игры появилось "template" "controller_neptune_deckdrop_probe.vdf". Выбор Steam записывает именно для конкретного устройства, а не для типа контроллера. Файл прежней раскладки игры не тронут, так что к ней можно вернуться.

И бонус: GetConfigForAppAndController отдаёт точный путь к текущей раскладке игры, в поле URL. Так DeckDrop находит папку игры, даже когда до неё не ведёт ни одно имя, — тот самый случай с переименованным ярлыком.

А номер 15 постоянный?

Это был мой первый вопрос. Номер относится к устройству, а не к игре, так что от игры к игре не меняется. А вот от дека к деку и при подключении внешнего геймпада может отличаться. Число 15 я видел только на своём деке. Поэтому DeckDrop его нигде не зашивает, а при каждом применении спрашивает у Steam список контроллеров:

// какой контроллер — встроенное управление дека: не 0, у Steam свой номер (15 на моём деке)
(() => {
  const store = window.ControllerStore || {}, list = store.m_controllerList || [];
  const deck = list.find(c => c.eControllerType === 4) || list[0];
  const index = deck ? deck.nControllerIndex : store.m_nLastValidActiveControllerIndex;
  return index === undefined ? null : index;
})()

А после переключения перечитывает выбор и сообщает об успехе, только если Steam подтвердил новую раскладку:

// выбрать раскладку так же, как это делает окно Steam, и дождаться, пока Steam её подтвердит
await input.SetSelectedConfigForApp(appid, index, url, false, 1);
for (let i = 0; i < 12; i++) {
  const now = await read();                      // GetConfigForAppAndController(appid, index)
  if (now.url === url) return {ok: true, index, ...now};
  await new Promise(r => setTimeout(r, 250));
}
return {ok: false, reason: 'not_switched', index, ...(await read())};

С внешним геймпадом я это не проверял.

Что получилось в 0.5.0

  • В карточке игры появился блок «Раскладки контроллера»: раскладки этой игры в Steam, кнопка «Сохранить в DeckDrop», строка «Сейчас в Steam: …» и «Раскладка из DeckDrop» → «Применить».

  • Хранилище. Сохранённая раскладка — это точная копия исходного файла в ~/.config/deckdrop/layouts/<id>.vdf и рядом <id>.json с названием, игрой и датой.

  • Зеркало в шаблоны Steam: controller_base/templates/controller_neptune_deckdrop_<id>.vdf с названием «DeckDrop: …». Так раскладку можно выбрать и на самом деке, без телефона.

  • Синхронизация. Папка шаблонов лежит внутри файлов клиента Steam, и его обновление может её очистить. Поэтому DeckDrop возвращает свои шаблоны при запуске и при открытии списка, а лишние удаляет — только файлы со своим префиксом и правильным id. Шаблоны Valve и чужие файлы не трогаются.

  • «Применить» — тот самый SetSelectedConfigForApp с проверкой. Выбор записывает сам Steam, DeckDrop в его файлы при этом ничего не пишет.

  • «Раскладка для новых игр» в настройках. Выбранная раскладка ставится каждой игре, которую DeckDrop добавляет в Steam. Если управление Steam сейчас выключено, применение встаёт в очередь вместе с именем и Proton и выполнится, когда управление появится.

  • В настройках раскладки можно переименовать, удалить, скачать как .vdf или загрузить, например с другого дека.

Сохранённые раскладки в настройках

Сохранённые раскладки в настройках

Как доказать, что функция не портит чужие файлы

У меня было одно жёсткое требование: сохранение в DeckDrop и удаление из него должны менять только файлы раскладок и ничего больше. Трогать настройки Steam и чужие раскладки нельзя ни при каком раскладе. Для DeckDrop это вообще принцип: там уже есть обязательный тест на то, что обновление программы не теряет данные пользователя.

Проверяется это так. Настоящий сервер DeckDrop запускается на временной домашней папке с фейковым Steam, устроенным как на моём деке: раскладка по серийнику, сохранённая раскладка с BOM, раскладка для другого контроллера, битый файл, папки других игр, configset, шаблоны Valve. До и после каждого действия снимается SHA-256 всех файлов домашней папки, и сравнивается точная разница: что добавилось, что пропало, что изменилось.

  • сохранение — ровно три новых файла: сама раскладка, её описание и шаблон. Всё остальное байт в байт то же;

  • повторное сохранение с тем же названием — вторая раскладка, первая не переписана;

  • переименование — меняются только своё описание и свой шаблон;

  • удаление — пропадают только свои три файла;

  • раскладка другой игры, другого контроллера или битая, ../ в пути, пустое название, кривой id, слишком большой файл — ошибка и ноль изменений;

  • применение — в файлах Steam не меняется ничего, выбор делает сам Steam.

Всего в этом наборе 20 тестов. Плюс обязательный тест обновления теперь ставит прошлые релизы 0.4.0, 0.4.1 и 0.4.2, «нажимает» в них «Обновить» и проверяет, что сохранённые раскладки пережили обновление байт в байт и снова видны в списке.

Как тестировать Steam без Steam

Тут две половины.

Первая — подставной отладочный порт Steam, около 150 строк на стандартной библиотеке. Он отвечает на /json, выдаёт цель SharedJSContext и поднимает минимальный WebSocket-сервер. Выполнять JavaScript он не умеет, поэтому узнаёт запрос по вызову Steam внутри выражения и отвечает так, как отвечал дек. Есть режимы «Steam переключил», «Steam не переключил» и «нет контроллера». Так проверяется вся цепочка сервера: кнопка, API, CDP, проверка ответа и запись в карточке игры.

Вторая — настоящий браузер для самого JavaScript. Браузерная проверка в CI запускает в Chromium ровно те выражения, которые DeckDrop шлёт в Steam, против заглушки SteamClient, которая ведёт себя как дек: у встроенного управления номер 15, а выбор срабатывает только при пяти аргументах и только для этого контроллера.

А тесты вообще ловят ошибки?

Тесты, которые всегда зелёные, мало что доказывают. Поэтому в код специально вносились поломки, и проверялось, что тесты падают. Поймались все:

Поломка

Какой тест упал

не удалять лишние шаблоны

тесты раскладок

удаление не трогает шаблон

тесты раскладок

искать папку игры без имени exe (случай с переименованным ярлыком)

тесты раскладок

не проверять, что раскладка принадлежит этой игре

тесты раскладок

не отделять раскладки другого контроллера

тесты раскладок

менять все поля title в файле, а не только верхнее

тесты раскладок

синхронизация переписывает сохранённую раскладку при запуске

тест обновления

SetSelectedConfigForApp с четырьмя аргументами вместо пяти

браузерная проверка

номер контроллера 0 вместо найденного

браузерная проверка

не проверять, что Steam подтвердил переключение

браузерная проверка

И пара честных эпизодов, когда тесты нашли ошибки по-настоящему:

  • Баг в самой программе. При пустом названии файл раскладки успевал записаться на диск до проверки названия. Тест «плохой ввод ничего не пишет» это поймал: в папке раскладок появлялся лишний файл. Теперь сначала проверка, потом запись.

  • Баг в тесте. Помощник теста подставлял рабочий id вместо пустого (lid or self.lid), и тест «пустой id отклоняется» получал успешный ответ. Исправлено на is None.

  • Баг в заглушке Steam. Случай «нет контроллера» падал, потому что у заглушки оставался «последний активный контроллер 15», а код намеренно на него откатывается. Правильной оказалась программа, а поправить пришлось заглушку.

Бонус: мои тесты накручивали счётчик скачиваний

Пока делалась 0.5.0, я заметил странное: у последнего релиза на GitHub за несколько часов было под полсотни скачиваний deckdrop.py, а установщик за это время скачали пару раз. Разгадка оказалась смешной. Самопроверка DeckDrop проверяла, доступен ли адрес обновления, запросом HEAD прямо на файл релиза. GitHub засчитывает такой запрос как скачивание. А самопроверку на каждом прогоне CI запускают сразу несколько тестов, и всё это на трёх версиях Python. То есть счётчик скачиваний показывал в основном мои же тесты.

Исправил в том же релизе. Если адрес обновления указывает на файл релиза GitHub, проверка идёт на страницу …/releases/latest, а сам файл не трогается. Тесты получают локальный адрес обновления и к GitHub вообще не ходят. И отдельный тест проверяет, что самопроверка отправляет ровно один запрос на тестовый адрес и больше никуда. Уже накрученные цифры, правда, со счётчика не уходят.

Если у вас в программе есть «проверка обновлений», проверьте, не качает ли она релиз и не бьёт ли по счётчику.

Ограничения

  • Всё про Steam проверено на одном деке с версией SteamOS и Steam на сентябрь 2026 года. SteamClient — внутренний API без документации, Valve может поменять его в любой момент.

  • Номер 15 видели на одном устройстве. Код его не зашивает, но с внешним геймпадом я не проверял.

  • Раскладки игр, которые используют Steam Input API (у игры свои действия вроде «прыжок»), в чужой игре бессмысленны. В моей библиотеке таких не нашлось.

  • Обновление Steam может очистить папку шаблонов. DeckDrop вернёт свои шаблоны, но до его следующего запуска их может не быть.

  • Для кнопки «Применить» нужно управление Steam через отладочный порт, как у Decky. Без него раскладку выбирают на самом деке во вкладке «Шаблоны».

Вместо заключения

Самое интересное в этой истории для меня — насколько по-разному Steam относится к почти одинаковым файлам. Раскладка в папке игры видна только этой игре, та же раскладка в папке шаблонов видна всем, а выбор вообще записывается под серийник конкретного устройства. И всё это держится на внутреннем API, где у сотни методов нет ни одной сигнатуры.

Если у вас есть Steam Deck и DeckDrop 0.5.0, мне очень интересно, какой номер контроллера у вашего дека и работает ли «Применить» с подключённым внешним геймпадом. Пишите в комментариях или в issues: github.com/Aniforka/deckdrop.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.