DCS & Lua: «Кликабельность» через стандартный ввод

В предыдущей статье я описал структуру файла clickabledata.lua, который предоставляет информацию о всех кликабельных элементах кокпита.
В статье был описан способ передачи команд в симулятор по средствам export.lua, но существует еще один, менее очевидный, но более "дружественный" с точки зрения конечного пользователя, способ — передача команд по средствам стандартного ввода (спасибо Alex O’kean за предоставленную информацию).
Кратко концепция изложена на форуме.
 
Я немного доработал концеп, предложенный Alex-ом, и вот что у меня получилось — маленький мод для ввода, который позволяет использовать любое устройство, поддерживаемое DCS в качестве устройства ввода, для отправки "кликабельных" команд кокпита в симулятор.
 
Мод добавляет в стандартную настройку ввода в симуляторе действия для отправки команд напрямую "кликабельным" устройствам.
Действия разбиты по категориям, каждая категория — отдельное "кликабельное" устройство.
Тип команды указан в конце названия, в скобках. Также указано системное имя команды, как оно значится в clickabledata.lua
 
Команды (commands) типа BTN:
— могут пересекаться с уже имеющими командами в симуляторе
— отрабатывается как команда нажатия, так и отпускания (т.е. ведет себя как настоящая кнопка)
Команды (commands) типа TUMB
— имеется возможность установить тумблер в конкретное положение, используя кнопку на устройстве ввода
 
Имя действия равно его описанию, которое "всплывает" при наведении мышью на "кликабельном" элемент во время игры.
Также могут встречаться действия с одинаковым описание, но разными системными устройства. Второй вариант в этом случае имеет именование "*-COVER", что соответсвует защитной крышке тумблера или кнопки.
 
Команды типа LEV в текущей реализации не поддерживаются
 
Пара скриншотов:
image

image
 
 
Для установки мода необходимо:
1. Скачать архив (or ENG version)
2. Папку MJOY16 из архива скопировать в \Scripts\Aircrafts\Ka-50\Cockpit\
3. Файл device_init.lua из архива скопировать в "\Scripts\Aircrafts\Ka-50\Cockpit\" с заменой существующего, предварительно сделав резервную копию
4. Переименовать файл default.lua.joy в default.lua и скопировать в "\Config\Input\Aircrafts\ka-50\joystick\", предварительно сделав резервную копию.
5. Переименовать файл default.lua.keyb в default.lua и скопировать в "\Config\Input\Aircrafts\ka-50\keyboard\", предварительно сделав резервную копию.
6. Переименовать все файлы назначений (имеют формат "имя_устройства (GUID-устройства).lua") в папках "\Config\Input\Aircrafts\ka-50\keyboard\" и "\Config\Input\Aircrafts\ka-50\joystick\". ВНИМАНИЕ! После этого все настройки кнопок сбросятся на значение по умолчанию!
6. Запустить игру
 
Решение проблем:
Если у вас вдруг не работает данный мод, действия следующие:
1. Выходим из симулятора
2. Редактируем файл "\Scripts\Aircrafts\Ka-50\Cockpit\MJOY16\MJoy16_macros.lua" (КРАЙНЕ не рекомендуется использовать для этого стандартный Блокнот), заменяем строку "DEBUG = false" на "DEBUG = true"
3. Запускаем симулятор, заходим в настройки ввода, проверяем, что все кнопки назначены правильно и конфликтов нет.
4. Запускаем "Быструю Миссию", пробуем несколько раз нажимать настроенные кнопки.
5. Выходим из игры
5. Находим файл "\Temp\MJoy16_macros.log" и его содержимое отправляем мне в ПМ на форуме или комментарием к этой записи, с описание чего хотели и что получилось/не получилось.
Реклама

DCS & Lua: Разбираем файл clickabledata.lua

Путь относительно папки с игрой: \Scripts\Aircrafts\Ka-50\Cockpit\clickabledata.lua
Назначение файла: содержит в себе все "кликабельные" элементы кокпита
 
Может возникнуть вопрос "Зачем?". Ответ растет вот отсюда: с помощью export.lua можно заставить "кликать" любой элемент в кокпите из внешних команд, что открывает простор для создания "железных" кокпитов.
Для "кликания" в export.lua необходимо выполнить всего 4 строки кода:
local device = GetDevice(devicesID)
if device then
        device:performClickableAction(ActionID,Value)
end
 
 
Но где брать значения переменных DeviceID, ActionID и Value? и что они значат?
Именно этому и посвещена данная статья.
 
Замечания:
1. Понимание статьи требует ознакомления с основами языка Lua, и собственно симулятора DCS.
2. Понятие "Rotary switch" переведено как "галетник" или "поворотный выключатель"
3. В статье часто будет употребляться выражение "кликабельный элемент" и "кликабельное устройство" и "действие" (Action в оригинале). Эти два понятия умышленно разделены по следующему признаку:
      "кликабельный элемент" — элемент кокпита (кнопка, тумблер, галетник и пр.), который может менять состояние
      "кликабельное устройство" — набор кликабельных элементов, объединенных по логическому признаку.
      "Действие" — команда, которую можно передать в кликабельное устройство. У одного устройства может быть одно или более действие. Далее по тексту встречается как понятие "действие", так и "команда". Нужно считать их идентичными.
 
Для понимания содержимого данного файла необходимо изучить также содержимое 2х других:
command_defs.lua — содержит в себе (кроме прочего) описание класса "кликабельного" элемента и команд "кликабельных" устройств. Нас интересуют две последние таблицы — class_type и device_commands.
devices.lua — содержит в себе таблицу devices, которая, фактически, сопоставляет понятное имя кликабельного устройства с его номером (идентификатором).
 
Описание таблиц
 
(здесь и далее используется нотация _имя_таблицы_(_файл_в_котором_она_расположена_):
class_type (command_defs.lua) — вспомогательная таблица, описывающая типы кликабельных устройств. Приведу её полностью:
class_type =
    NULL   = 0, 
    BTN    = 1,
    TUMB   = 2,
    SNGBTN = 3,
    LEV    = 4
}

Описание:

NULL пустое устройство. Нигде не используется (во всяком случае не нашел упоминаний в коде lua-скриптов)
BTN обычная кнопка. Имеет два положение — on (1) и off (0).
TUMB тумблер. 2х или многопозиционный. Также в понятие тумблера входит и галетник (что не лишено логики)
SNGBTN неизвестно. Нигде не используется (во всяком случае не нашел упоминаний в коде lua-скриптов)
LEV энкодер. Забегаю вперед скажу, что в симуляторе используется два типа — "относительные" и "абсолютные". Относительные — при передачи команды в симулятор нам надо передать шаг, на который мы хотим изменить положение енкодера. В "абсолютных" — необходимо передавать значение, в которое необходимо устоновить энкодер.
 
device_commands (command_defs.lua) — вспомогательная таблица, описывающая номера (идентификаторы) команды. В отличии от первой, несет крайне маленькую смысловую нагрузку и используется как справочник, для удобства. Как видно из таблицы, доступно всего 50 команд. Т.е. у каждого "кликабельного" устройства может быть не более 50 команд.
 
devices (devices.lua) — также вспомогательная таблица, описывающая номера (идентификаторы) кликабельных устройств. К этой таблице мы будет общаться постоянно, т.к. именно она дает понять, где искать какой "кликабельный" элемент.
 
elements (clickabledata.lua) — основная таблица, содержит в себе все "кликабельные" элементы.
Разбор файла 
 
Собственно, начнем разбор файла clickabledata.lua.
Первые 3 строки подключают дополнительные файлы. Таблица LockOn_Options содержит в себе различные системные настройки симулятора, конкретно её свойство script_path — полный путь к папке скриптов (\Scripts\Aircrafts\Ka-50\Cockpit\ относительно папки с игрой).
Файл Hint_localizer.lua служит для локализации "всплывающих подсказок" при наведении на кликабельный элемент в процессе игры. В виду его большого объема проводить сопоставление вручную — занятие не для слабонервных. Позже я покажу как это сделать автоматически.
С 5ой по 9ю строку идут объявления внутренних переменных, большого интереса не представляющего. Единственное, стоит запомнить, что переменная direction у нас всегда равна 1.
 
Далее следует самое интересное — таблица elements. Каждый элемент таблицы находится на отдельной строке. В общем случае структура каждого элемента выглядет вот так (ВНИМАНИЕ! Это измененный элемент, специально чтобы показать все возможные свойства.):
elements["GEAR-PTR"] = {
         class = {class_type.TUMB},
         hint = LOCALIZE("Gear lever"),
         device = devices.CPT_MECH,
         action = {device_commands.Button_1},
         stop_action = {device_commands.Button_1},
         arg = {65},
         arg_value = {direction*1.0},
         arg_lim = {{0.0, 1.0}},
         gain = {false},
         relative = {false}        
         updatable = true,
         use_OBB = true,
}
 
Приведен полный набор свойств. В некоторых элементах некоторые свойства могут отсутствовать.
 
elements["GEAR-PTR"] — в кавычках указано системное имя элемента.
 
Начнем с простых свойств таблицы elements:
hint — Подсказка, всплывающая над элементом в игре. Фактически — его понятное описание. Как из английского описания получить русское я расскажу позже.
device — идентификатор устройства. Именно идентификатор, а не имя. Узнать его можно из таблицы devices (devices.lua), н-р в нашем случае он равен 34 (в файле devices.lua строка №38: "devices["CPT_MECH"] = 34")
updatable и use_OBB — к сожалению выяснить назначение этоих свойств не удалось.
 
Описание составных свойств.
Все следующие свойства представляю собой массивы (или таблицы, в понятии Lua. Далее я буду использовать именно понятие массив, т.к. это ближе к истине в данном случае). И количествово элементов этих массивов всегда одинаково для одного элемента. При этом свойство action и class являются обязательными. Например, если в свойстве class содержится 3 элемента (н-р class = {class_type.TUMB}, {class_type.LEV}, {class_type.TUMB}), то и во всех нижеперечисленных свойствах будет по 3 элемента. Индекс элемента массива будет указывать на одно и тоже действие (или по другому — элементы массива в указанных ниже свойствах, имеющие одинаковый индекс, будут принадлежать одной и той же команде). В конце я приведу примеры, из которых это станет более понятно (надеюсь).
 
class — массив  типов кликабельных устройств, используемых в этом кликабельном элементе. В примере выше — это обычная кнопка.
action — массив идетификаторов команда. Сопоставление смотрим в таблице device_commands (command_defs.lua). В примере выше индетификатор равен 3001.
stop_action — массив идентификаторов команд на остановку. Сопоставление смотрим там же, где и для свойства action. Предназначение данного свойства так и осталось для меня загадкой.
arg и gain — некий массив значений, тоже остался не опознанным.
arg_lim — массив максимальных и минимальных значений для данной команды. 1е значение — минимум, второе — максимум. В нашем примере минимум — 0, максимум — 1.
arg_value — массив значений шага изменений значения команды. Об этом чуть позже. Как помним direction у нас всегда равен 1, т.е. его можно не учитывать.
relative — массив, описывающий является ли команда относительной или нет. Применяется только для команд класса LEV (и то не всех).
 
Вспомним представленный в начале статьи код и попробуем прояснить для себя ситуацию. В коде использовались 3 переменные — DeviceID, ActionID и Value. Думаю по первым двум стало всё ясно — это свойства таблицы elements (clickabledata.lua) device и action соответственно. Но вот откуда брать значение Value — не совсем очевидно.
Но и тут всё просто — Value рассчитывается по формуле:

  Value = arg_lim[0] + n * arg_value
  где — n — это позиция тумблера или кнопки, при этом value не может быть больше, чем arg_lim[1] (помним, что arg_lim содержит в себе минимальное и максимально значение команды).
Т.е. для каждой команды значение Value может находится в диапазоне от arg_lim[0] (минимум) до arg_lim[1] (максимум) включительно с шагом arg_value.
В нашем примере — это лишь 0 и 1. Для всех команд класса TUMB это очевидное поведение — им нужно иметь положение ВКЛ и ВЫКЛ (хотя есть исключения).
 
Зная всё это можем составить два куска кода для нашего примера:
— нажимаем кнопку
local device = GetDevice(34)
if device then
        device:performClickableAction(3001,1.0)
end
— отпускаем кнопку

local device = GetDevice(34)
if device then
        device:performClickableAction(3001,0.0)
end
 
или с использованием таблиц и понятных имен (следующий код полностью эквивалентен предыдущему):
— нажимаем кнопку
local device = GetDevice(devices.CPT_MECH)
if device then
        device:performClickableAction(device_commands.Button_1,1.0)
end
— отпускаем кнопку

local device = GetDevice(devices.CPT_MECH)
if device then
        device:performClickableAction(device_commands.Button_1,0.0)
end
Еще одно замечание. В таблице elements (clickabledata.lua) встречается огромное элементов, имеющие 2 команды классы BTN или TUMB с одинаковыми action, arg и пр, имеющие лишь разные arg_value, обычно такого вида:
          arg_value = {direction*1.0,-direction*1.0}
т.е. фактически различающимися только направлением. Для использования в export.lua нам это не понадобиться, и второе значение можно просто проигнорировать.
Вот пример элемента "GEAR-PTR", взятого без изменений из файла:
elements["GEAR-PTR"] = {
       class = {class_type.TUMB,class_type.TUMB},
       hint = LOCALIZE("Gear lever"),
       device = devices.CPT_MECH,
       action = {device_commands.Button_1,device_commands.Button_1},
       arg = {65,65},
       arg_value = {direction*1.0,-direction*1.0},
       arg_lim = {{0.0, 1.0},{0.0, 1.0}},
       updatable = true,
       use_OBB = true
}
Как видно, все свойства массивы совпадают, кроме arg_value. В таких случаях надо брать только первые значения свойств.
Но и тут надо быть внимательным — есть элементы с двумя классами BTN (или TUMB), но с разными action. Их уже нельзя игнорировать.
Н-р вот описание элемента CLOCK-LEFT-LEVER-PTR, отвечающего за левую головку часов:
elements["CLOCK-LEFT-LEVER-PTR"] = {
          class = {class_type.BTN, class_type.BTN, class_type.LEV},
          hint = LOCALIZE("Mech clock left lever"),
          device = devices.CLOCK,
          action = {device_commands.Button_1, device_commands.Button_2, device_commands.Button_3},
          stop_action = {device_commands.Button_1, device_commands.Button_2, 0},
          is_repeatable = {},
          arg = {76, 76, 511},
          arg_value = {1.0, -1.0, 0.04},
          arg_lim = {{0, 1}, {-1, 0}, {0, 1}},
          relative = {false,false,true},
          gain = {1.0, 1.0, 0.4},
          use_release_message = {true, true, false},
          use_OBB = true
}
Как видно, мы имеем три команды, две из которых класса BTN, и у них указаны разные action. И действительно, в игре мы можем как нажать на левую головку (action = device_commands.Button_1), так и потянуть её на себя (action = device_commands.Button_2)
 

Описание классов команд

Теперь немного по классам:
BTN

ведет себя всегда однозначно — имеет два возможных значение (обычно 0.0 и 1.0, но имеются исключения). И всё.

Примеры BTN были приведены выше.
TUMB

подразделяется на 2 подтипа — многопозиционные тумблеры и "галетники". Хотя логика их работы абсолютно одинаковая — чтобы поставить тублер в какое-либо положение необходимо установить определенное значение value. При этом кол-во возможных значений всегда равно кол-ву возможных положений тумблера. Т.е. в двухпозиционном может быть два возможных значения, в трехпозиционном 3 и т.д.

Тумблеры хорошо рассматривать на примере системы вооружений (device = device.WEAP_INTERFACE)
Н-р "Главный Выключатель СУО" (двухпозиционный тумблер) описан вот так:
elements["MASTER-ARM-PTR"]   = {
         class = {class_type.TUMB, class_type.TUMB},
         hint = LOCALIZE("Master Arm"),
         device = devices.WEAP_INTERFACE,
         action = {device_commands.Button_1,device_commands.Button_1},
         stop_action = {},
         arg = {387,387},
         arg_value = {-direction*1.0,direction*1.0},
         arg_lim = {{0.0, 1.0},{0.0, 1.0}},
         use_OBB = true,
         updatable = true
}
Помним, что не смотря на то, что у нас две команды — одну мы игнорируем, т.к. action у них одинаковый. Как видно имеется всего два возможный значений — 1.0 и 0.0 (см. формулу arg_lim[0] + n * arg_value)
А вот так описан галетник "Выбор Режимов" пульта ПВР:
elements["PVR-MODE-PTR"]  = {
          class = {class_type.TUMB, class_type.TUMB},
          hint = LOCALIZE("Weapon system mode selector"),   
          device = devices.WEAP_INTERFACE,
          action = {device_commands.Button_14, device_commands.Button_15}, 
          arg = {431, 431},
          arg_value = {-direction*0.1, direction*0.1},
          arg_lim = {{0.0, 0.4}, {0.0, 0.4}}
}
Как видно, у нас здесь аж 5 возможных вариантов: 0.0, 0.1, 0.2, 0.3, 0.4
Каждое из значений используется для выставления галетника в определенное пложение. Как узнать в какое? Только тестированием.
Тут нас ждем еще один "сюрприз" — в кокпите данный голетник представлен в одном экземпляре. Но у нас две команды, причем с разным action. Сей феномен остался для меня загадкой, ибо обе команды действуют абсолютно одинаково (хотя есть подозрение, что это сделано для того, чтобы разными кнопками мыши — левой и правой, переключать галетник в разные стороны. Для "железного" кокпита, полностью эмитирующего реальный, это не актуально)
 
LEV
Также, как и TUMB, делится на 2 типа — relative (относительные)и non-relative (абсолютные). Но если TUMB-подтипы отличались только внешне и имели одинаковую логику, то LEV-подтипы ведут себя в точность до наоборот — имеют одинаковый вид, но абсолютно разную логику.
За разделение на подтипы отвечает свойство relative таблицы elements (clickabledata.lua). Соответственно, если он установлен в true — команда относительная, если в false — абсолютная.
 
Для объяснения логики начну сразу с примеров: ручек управления АБРИС яркостью (абсолютный) и положение курсора (относительный).
Ручка управления яркостью АБРИС описана вот так:
elements["ABRIS_BRIGHTNESS_PTR"]= {
         class = {class_type.LEV},
         hint = LOCALIZE("ABRIS Brightness"),      
         device = devices.ABRIS,
         action = {device_commands.Button_8} ,
         arg = {517}    ,
         arg_value = {0.05}  ,
         arg_lim = {{0,1}}
}
Как видно, в этом элементе присутствует одна команда класса LEV, и свойство relative не установлено, что означает что оно равно false.
Поведение данного элементо следующее — для установки положения данной ручки нужно указать точное значение, в которое мы её хотим поставить. Такое поведение соответсвует абсолютному энкодеру.
Доступно 20 положений ручки (от 0 до 1 с шагом 0.05). Яркость меняется от нулевой (экран АБРИС выключен, значение 0) до полной (значение 1).
В случае конструирования кокпита необходимо либо брать галетник с 20 позициями, либо абсолютный энкодер, либо на уровне ПО, посылающего команды от кокпита в симулятор, отслеживать положение обычного энкодера, и при изменении его положения передавать нужное значение в симулятор (я лично выбрал второй вариант, т.к. с MJoy нельзя использовать абсолютный энкодер).
 
Теперь приведу описание относительного подтипа, а именно ручку Манипулятор курсора АБРИС:
elements["ABRIS_SHUNT_PTR"] = {
         class = {class_type.LEV, class_type.BTN},
         hint = LOCALIZE("ABRIS Cursor сontrol (rot/push)"),
         device = devices.ABRIS ,
         action = {device_commands.Button_6,device_commands.Button_7},
         stop_action = {0,device_commands.Button_7} ,
         is_repeatable = {} ,
         arg = {518,523},
         arg_value = {0.04,1},
         arg_lim = {{0,1},{0,1}},
         relative = {true,false},
         gain = {1,0}
}
Видно, что в элементе 2 команды, одна типа LEV, вторая — BTN (и действительно, кроме того, что мы можем вращать эту рукоятку, мы можем на неё и нажать). Также видно, что свойство  relative для команды LEV установлено в истину, что соответствует относительному подтипу.
Для относительного LEV-подтипа мы должны передавать не точное значение, а лишь смещение, что соответствует классическому поведению энкодера. И чем больше будет значение — тем больше будет изменяться величина, меняемая данной командой (в нашем случае — тем с более большим шагом будет двигаться курсор). Данное поведение отлично поддерживается MJoy-ем.
Несколько примеров:
— Сдвигаем курсор на самую малую величину, н-р в режиме карты — это пара пикселей вправо, не больше
local device = GetDevice(devices.ABRIS)
if device then
        device:performClickableAction(device_commands.Button_6,0.04)
end
 
— двигаем курсор влево в режиме карты, также на самую малую величину
local device = GetDevice(devices.ABRIS)
if device then
        device:performClickableAction(device_commands.Button_6,-0.04)
end
 
— двигаем курсор влево в режиме карты, на половину максимального шага
local device = GetDevice(devices.ABRIS)
if device then
        device:performClickableAction(device_commands.Button_6,-0.5)
end
 
В реальных условиях, при постройки "железного" кокпита мы можем использовать инкрементальные энкодеры (которые изменяют своё напряжение от скорости вращения). В случае использования MJoy – при разной скорости вращения будет генерироваться разное нажатие кнопок. Например для кнопки, срабатывающей при для медленном вращении, мы можем поставить малый шаг, а для кнопки, срабатывающей при быстром вращении – на большой шаг.

DCS & Lua: введение

 

С сегодняшнего дня я несколько переквалифицирую свой блог, и буду писать заметки про симуляторы серии Digital Combat Simulator и скриптоый язык Lua.

Собственно, о чем речь:
Digital Combat Simulator — серия симуляторов военной авиации с глубочайшей проработкой всех систем летального аппарата. Пока что состоит из одной игры — Ка-50, посвященной одноименному вертолета КБ "Камова"
 
Скриптовый язык Lua — один из самых "быстрых" скриптовых языков, с которым мне приходилось сталкиваться. Написан полностью на C, выполнен в виде библиотеки с открытым кодом. Кроме прочих достоинств отмечу отдельные:
— позволяет из скрипта выполнять код в процессе приложения
— имеет библиотеку для работы с .NET (LuaInterface)
— позволяет компилировать скрипт, что не только скрывает от посторонних глаз логику скрипта, но и несколько ускоряет выполнение.
Более подробно о Lua можно почитать в  wiki
В дальнейших статьях я, в основном, буду описывать использование Lua в рамках его совместного сипользования с .NET