Как я победил ограничения Steam Deck и отыскал идеальный контроллер: опыт игрока

Прокомментировать Просмотры: 6

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

Переносить конфигурации управления между проектами на Steam Deck из коробки не удается. Настроив идеальное управление для одной визуальной новеллы и сохранив его, например, под тегом «Чтение», при переходе к другой игре вы обнаружите пустой список. Пользователи умоляют Valve решить эту проблему годами, соответствующее обсуждение в сообществе Steam не теряет актуальности.

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

Что такое DeckDrop

Для тех, кто пропустил предыдущий материал: DeckDrop представляет собой миниатюрный веб-сервер, функционирующий прямо на консоли. Отправив ссылку со смартфона или компьютера в той же локальной сети, вы запускаете автоматическую загрузку, распаковку и интеграцию игры в Steam с обложками и Proton. Архитектура умещается в единый Python-скрипт на стандартных библиотеках, обходясь без root-прав и активации режима разработчика.

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

Эмпирическое исследование архитектуры Steam

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

Полученные данные скрипт отдавал через локальный веб-интерфейс: отчет открывался на смартфоне в пару кликов, избавляя от необходимости переключаться в десктопный режим. Защита была спроектирована жестко: на любые запросы вроде /../../etc/passwd сервер возвращал один и тот же отчет безопасности.

Всего потребовалось четыре исследовательских цикла.

Раунд первый: где прячутся файлы профилей

Директория хранения раскладок выглядит следующим образом:

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

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

Информация о выбранной схеме управления фиксируется в файле configset_controller_neptune.vdf. Для каждого приложения там прописан один из трех сценариев:

  • "autosave" "1" — правки вносились непосредственно в ходе игрового процесса;

  • "template" "CLOUD_/<имя>_" — задействован пользовательский шаблон;

  • "workshop" "" — пресет загружен из Мастерской Steam.

Для проектов из официального магазина директория именуется по AppID. С non-Steam играми ситуация сложнее: папка получает имя ярлыка в нижнем регистре с удалением знаков препинания, но с сохранением пробелов и восклицательных знаков. Например, ярлык «Cool Game: Director’s Cut» породит каталог cool game directors cut, а Game.exe превратится в gameexe.

Здесь кроется подводный камень. Название папки формируется на основе имени ярлыка в момент первичной инициализации Steam. Переименование ярлыка в дальнейшем не меняет путь к файлам конфигурации. У меня был ровно такой кейс: игра добавилась как Game.exe, позже я дал ей нормальное имя, но профиль продолжил лежать в gameexe. Предсказать путь по текущему названию невозможно, поэтому DeckDrop перебирает сразу несколько вариантов: текущий ярлык, изначальное имя при импорте, а также имя исполняемого файла с расширением и без. Позже обнаружился более элегантный способ запросить эту информацию напрямую у Steam.

Мелкие технические нюансы:

  • Существуют профили, привязанные не к типу устройства, а к конкретному «железу»: рядом соседствуют файлы <серийник>.vdf и configset_<серийник>.vdf.

  • Имена стандартных шаблонов Valve в директории controller_base/templates изначально читались некорректно (пустые строки). Проблема крылась в BOM-маркере, потребовавшем кодировку utf-8-sig.

  • Интерфейс SteamClient.Input насчитывает порядка ста методов, у которых свойство length неизменно возвращает 0 из-за нативной природы оберток. Однако сигнатуры угадываются по говорящим названиям: SetSelectedConfigForApp, GetConfigForAppAndController, QueryControllerConfigsForApp.

Раунд второй: глобальная видимость шаблонов

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

Проверка на консоли подтвердила догадку: профиль моментально появился во вкладке «Шаблоны» для любых других игр. Решение найдено — размещение файла в системном репозитории шаблонов делает его доступным глобально средствами самого интерфейса Deck.

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

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

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

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

Результата ноль. Ошибок нет, конфигурация не меняется. Ошибкой оказалось использование нулевого индекса контроллера по аналогии с другими API. Дальнейший анализ расставил всё по местам:

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

  • В глобальном объекте ControllerStore.m_controllerList обнаружился «Steam Deck Controller» с параметрами nControllerIndex: 15 и eControllerType: 4. При этом m_nLastValidActiveControllerIndex также равнялся 15.

Ключевое озарение: штатный геймпад Steam Deck идентифицируется системой под пятнадцатым номером, а не нулевым.

Раунд четвертый: долгожданный прорыв

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

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

{"Title": "Геймпад с управлением камерой",
 "URL": "autosave:///home/deck/.local/share/Steam/steamapps/common/Steam Controller Configs//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". Привязка осуществляется на уровне конкретного железа, а не класса контроллеров. Оригинальный файл пользовательского конфига остался нетронутым, позволяя в любой момент откатить настройки.

Бонус: поле URL в ответе GetConfigForAppAndController выдает абсолютный путь к активной раскладке. Это позволило DeckDrop безошибочно определять директорию игр даже при расхождениях в именах ярлыков (тот самый случай с переименованным Game.exe).

Постоянен ли индекс 15?

Естественный вопрос. Идентификатор привязан к устройству, а не к тайтлу, поэтому он стабилен в рамках одной системы. Однако на разных Deck или при подключении сторонних геймпадах значение может отличаться (число 15 фиксировалось исключительно на моем экземпляре). По этой причине DeckDrop не хардкодит значение, а запрашивает актуальный список контроллеров на лету:

// динамическое определение встроенного геймпада Deck (не 0, а системный индекс вроде 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 с ожиданием подтверждения
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

  • Карточка игры обзавелась блоком «Раскладки контроллера»: здесь отображаются локальные профили, кнопка сохранения в DeckDrop, статус текущего выбора в Steam и инструмент быстрого применения.

  • Хранилище профилей. Экспортированная схема сохраняется в виде точной копии в ~/.config/deckdrop/layouts/.vdf в сопровождении метаданных .json.

  • Интеграция с шаблонами Steam: файлы controller_base/templates/controller_neptune_deckdrop_.vdf с префиксом «DeckDrop: …» автоматически синхронизируются, позволяя выбирать пресеты прямо из интерфейса Deck.

  • Алгоритм очистки. Обновления клиента Steam могут зачищать папку шаблонов. DeckDrop производит автовосстановление своих пресетов при старте и открытии списков, удаляя только файлы со своим идентификатором и не затрагивая системные файлы Valve.

Команда «Применить» задействует описанный механизм SetSelectedConfigForApp с валидацией. Все изменения записываются исключительно клиентом Steam.

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

  • Менеджер раскладок поддерживает переименование, удаление, экспорт в .vdf или импорт профилей с других устройств.

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

    Гарантии безопасности пользовательских данных

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

    Методика тестирования строилась на создании изолированной временной домашней директории с эмуляцией окружения Deck: профили по серийникам, битые файлы, сторонние директории, шаблоны Valve. До и после каждого действия алгоритма снимались хэши SHA-256 абсолютно всех файлов в окружении с последующим анализом дельты:

    • сохранение порождает ровно три артефакта: схему, метаданные и шаблон, не затрагивая остальное окружение;

    • повторный экспорт с тем же именем создает новый файл без перезаписи старого;

    • переименование модифицирует только служебные метаданные и шаблон;

    • удаление зачищает строго собственную триаду файлов;

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

    • применение профиля не производит прямой записи в конфигурационные файлы Steam.

    Репозиторий включает 20 комплексных тестов. Плюс регрессионный тест обновлений разворачивает сборки 0.4.0–0.4.2, имитирует апдейт и проверяет сохранность пользовательских раскладок байт-в-байт.

    Эмуляция окружения Steam для тестирования

    Тестовая среда разделена на два компонента.

    Первый — мокап отладочного порта Steam (около 150 строк кода на стандартных библиотеках). Он обслуживает запросы /json, транслирует контекст SharedJSContext и поднимает минималистичный WebSocket-сервер. Имитируя поведение реального железа в зависимости от переключаемых флагов («переключено», «ошибка», «нет контроллера»), он позволяет прогнать всю цепочку серверной логики от веб-интерфейса до записи ответа.

    Второй — браузерная песочница для JS-скриптов. В CI-пайплайне Chromium прогоняет те же выражения, которые DeckDrop отправляет в Steam, используя заглушку SteamClient с эмуляцией индекса 15 и проверкой пяти аргументов метода.

    Эффективность тестовой системы

    Чтобы убедиться, что тесты действительно работоспособны, я намеренно внедрял в код дефекты и фиксировал падения пайплайна:

    Внедренный дефект

    Упавший тест

    отказ от очистки устаревших шаблонов

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

    удаление без очистки шаблона

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

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

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

    отсутствие проверки принадлежности раскладки конкретной игре

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

    игнорирование изоляции профилей других контроллеров

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

    перезапись всех полей title в файле вместо точечной правки

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

    затирание сохраненных профилей при старте синхронизации

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

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

    браузерный тест

    использование нулевого индекса контроллера

    браузерный тест

    отсутствие валидации подтверждения со стороны Steam

    браузерный тест

    Реальные баги, отловленные тестами в процессе разработки:

    • Ошибка в бизнес-логике. Пустое имя файла позволяло записать пустую раскладку на диск до прохождения валидации. Тест на некорректный ввод отловил этот мусор, после чего порядок операций был изменен: проверка теперь выполняется строго до записи.

    • Ошибки в тестах. Тестовый хелпер подставлял валидный ID вместо пустого (lid or self.lid), из-за чего сценарий с пустым ID отрабатывал некорректно. Логику исправили на строгое условие is None.

    • Ляп в мокапе Steam. Сценарий отсутствия контроллеров падал, так как заглушка удерживала «последний активный индекс 15». Код оказался верным, исправления потребовал симулятор.

    Забавный курьез: как тесты накручивали статистику загрузок

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

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

    Если ваш софт проверяет апдейты, стоит убедиться, что фоновые пинги не портят вам аналитику загрузок.

    Ограничения и нюансы

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

    • Пятнадцатый индекс контроллера зафиксирован на конкретном устройстве. Код динамически вычисляет это значение, однако поведение со сторонними геймпадами требует дополнительных проверок.

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

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

    Кнопка быстрого применения требует активированного отладочного порта Steam (по аналогии с Decky). В противном случае профили выбираются вручную через штатную вкладку шаблонов на консоли.

    Заключение

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

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

     

    Источник

    Поделиться:

    Похожие статьи

    Поиск по играм, новостям и статьям…

    Введите не менее двух символов

    Введите не менее двух символов