Lua API

Здесь приводится краткий обзор Lua, на котором в основном написан Jester, и объясняется сам Jester API.

Начало работы с Lua

Полезные ссылки:

Таблицы

Lua сам по себе является довольно простым языком с не слишком большим количеством функций. Он в основном вращается вокруг использования таблиц. Таблицы можно сравнить с массивами, списками и словарями или картами.

-- словарь
local person = {
  name = "John",
  age = 20,
}

-- массив/список
local fruits = { "Orange", "Apple", "Lemon" }

Массивы также неявно преобразуются в словари с возрастающими ключами 1, 2 и т.д.

Доступ может быть либо в стиле поиска person["age"] = 21, либо как к полям person.age = 21.

В Lua индексы начинаются с 1:

print(fruits[1]) -- Orange

К длине таблицы можно получить доступ с помощью #:

-- appending to a table
fruits[#fruits + 1] = "Cherry"

Всё, что явно не установлено, получает значение nil.

Пример синтаксиса

function ageCheck(name, age)
  if age < 18 then
    print("Sorry", name)
  else
    print("Okay", name)
  end
end

Классы Lua сам по себе не предоставляет классы. Однако мы создали фреймворк для добавления классоподобных структур в Lua:

local Class = require('base.Class')

local Person = Class()
Person.name = nil -- fields
Person.age = nil

function Person:Constructor(name, age)
  self.name = name
  self.age = age
end

Person:Seal() -- Запретить добавление/удаление дополнительных значений/функций

Фреймворк также поддерживает наследование:

local Class = require('base.Class')
local Behavior = require('base.Behavior')

local AssistAAR = Class(Behavior) -- inherits from Behavior

Отладка

К сожалению, у нас нет настроенного отладчика Lua. Приходится полагаться на отладку с помощью вывода:

print("hello world")
print("Person:", person)
print("Check:", foo, bar, baz)

Который затем можно увидеть в консоли. Обратите внимание, что для того, чтобы увидеть вывод, может потребоваться периодически вызывать io.flush().

Мы также предоставляем игровую площадку Lua в WizardJester.lua, которая всегда выполняется непосредственно при запуске.

Также можно редактировать файлы Lua во время работы DCS, без перезапуска игры. Просто отредактируйте файл LUA, а затем перезагрузите миссию DCS с помощью CTRL+R, и новый файл Lua вступит в силу.

Jester API

Логика Jester разделена на 6 уровней абстракции:

  • Намерение (WIP)
  • План (WIP)
  • Ситуация
  • Поведение
  • Задача
  • Действие

Код размещается в папке Mod, например:

G:\DCS World OpenBeta\Mods\aircraft\F-4E\Jester

Пример

В качестве примера, затрагивающего большинство уровней, мы хотим создать функцию, которая позволит Jester сообщать текущую скорость каждые несколько секунд во время полёта.

Поэтому мы начнём с ситуации. Ситуации необходимо условие активации и деактивации:

-- Airborne.lua
local Class = require 'base.Class'
local Condition = require 'base.Condition'

local Airborne = {}
Airborne.True = Class(Condition)
Airborne.False = Class(Condition)

function IsAirborne()
  -- details on observations later
  return GetJester().awareness:GetObservation("airborne") or false
end

function Airborne.True:Check()
  return IsAirborne() -- activation condition
end

function Airborne.False:Check()
  return not IsAirborne() -- deactivation condition
end

Airborne.True:Seal()
Airborne.False:Seal()
return Airborne

Условия активации и деактивации не обязательно должны быть одинаковыми.

Теперь мы можем использовать это условие в нашей ситуации Flight и добавить желаемое поведение:

-- Flight.lua
local Class = require 'base.Class'
local Situation = require 'base.Situation'
local Airborne = require 'conditions.Airborne'
local ReportSpeed = require 'behaviors.ReportSpeed'
-- поведение будет определено на следующем шаге

local Flight = Class(Situation)

-- it simply expects a class with a :Check() method
Flight:AddActivationConditions(Airborne.True:new())
Flight:AddDeactivationConditions(Airborne.False:new())

function Flight:OnActivation()
  self:AddBehavior(ReportSpeed) -- запустить наше поведение
end

function Flight:OnDeactivation()
  self:RemoveBehavior(ReportSpeed) -- остановить наше поведение
end

Flight:Seal()
return Flight

Ситуацию также необходимо зарегистрировать в F-4E_WSO.lua (WIP):

-- in F-4E_WSO.lua
...
function CreateF4E_WSOJester()
  ...
  wso::AddSituations(Flight:new())
  ...
end

Теперь мы можем определить наше поведение:

-- ReportSpeed.lua
local Class = require('base.Class')
local Behavior = require('base.Behavior')
local SaySpeed = require('tasks.common.SaySpeed')
-- Задача будет определена на следующем шаге

local ReportSpeed = Class(Behavior)

function ReportSpeed:Constructor()
  Behavior.Constructor(self)
end

function ReportSpeed:Tick()
  -- это вызывается периодически
  local task = SaySpeed:new(...) -- доступ к скорости будет объяснён позже
  GetJester():AddTask(task)
end

ReportSpeed:Seal()
return ReportSpeed

Теперь это заставит Jester говорить что-то на каждом тике, немного слишком многословно. Чтобы улучшить это, была создана система Urge. Мы можем обернуть нашу задачу в Urge, и она будет вызываться только через заданный интервал (который автоматически применяется с некоторым отклонением в зависимости от фиксации и уровня стресса Jester):

-- ReportSpeed.lua
local Class = require('base.Class')
local Behavior = require('base.Behavior')
local Urge = require('base.Urge') -- добавлено
local StressReaction = require('base.StressReaction') -- добавлено
local SaySpeed = require('tasks.common.SaySpeed')

local ReportSpeed = Class(Behavior)

function ReportSpeed:Constructor()
  Behavior.Constructor(self)

  -- логика поведения
  local say_speed = function ()
    -- очень просто в этом случае,
    -- но также может запускать несколько задач в зависимости от условий, если это необходимо
    local task = SaySpeed:new(...)
    GetJester():AddTask(task)
    return {task}
  end

  -- определить побуждение
  self.urge = Urge:new({
    time_to_release = s(10), -- базовый интервал (сейчас 10 секунд)
    on_release_function = say_speed, -- что выполнять
    stress_reaction = StressReaction.ignorance, -- насколько это важно для Jester
  })
  self.urge:Restart() -- запустить его
end

function ReportSpeed:Tick()
  -- мы также можем изменить побуждение сейчас, если это необходимо
  -- например, увеличить уровень стресса
  self.urge:Tick() -- выполнить его
end

ReportSpeed:Seal()
return ReportSpeed

Следующий шаг - создать фактическую задачу, которая будет отвечать за сообщение заданной скорости:

-- SaySpeed.lua
local Class = require('base.Class')
local Task = require('base.Task')
local SayAction = require('actions.SayAction')

local SaySpeed = Class(Task)

function SaySpeed:Constructor(speed)
  Task.Constructor(self)

  local on_activation = function()
    if speed < kt(500) then
      -- см. PhrasesList.txt для всех поддерживаемых голосовых реплик
      self:AddAction(SayAction('awareness/wereslow'))
    else
      self:AddAction(SayAction('awareness/werefast'))
    end
  end

  self:AddOnActivationCallback(on_activation)
end

SaySpeed:Seal()
return SaySpeed

Последняя часть - это финальное действие, в нашем случае SayAction. Действия обычно очень общие и базовые. В большинстве случаев существующего SayAction будет достаточно. Обратитесь к SayAction.lua чтобы узнать, как он работает.

Если поведение не нуждается в специальной задаче и просто хочет произнести фразу, можно также напрямую использовать SayTask:

-- в логике поведения
...
local task = SayTask:new('misc/outoffuel')
GetJester():AddTask(task)
...

LReal и единицы измерения

Очень часто возникает необходимость работать с реальными значениями и единицами измерения, такими как скорость или время. Для этого у нас есть LReal с единицами измерения, определёнными в LUnit.

local time = min(15)
local speed = kt(500)
local fuel = lb(12000)

if time > s(10) then
  print("foo")
end

time = time - s(40)

Будьте осторожны при выполнении скалярных операций:

-- правильно
time *= 2

-- неправильно
time *= s(2)

Последнее приведёт к недопустимому LReal, который можно проверить с помощью time:IsValid().

При необходимости значения можно преобразовать в другую единицу измерения:

local timeInSeconds = time:ConvertTo(s)
print("Time:", timeInSeconds)

time.value будет получать доступ к необработанному базовому числу.

Доступ к свойствам

Lua имеет полный доступ ко всем Property определённым в наших компонентах, и может легко получать к ним доступ с помощью GetProperty:

function GetTotalFuelQuantity()
  local gauge_readout = GetProperty(
    "/Pilot Fuel Quantity Indicator/Fuel Meter", -- путь
    "Internal Fuel Quantity Indication" -- имя свойства
  ).value

  return gauge_readout or lb(10000)
end

GetProperty ожидает полный путь к компоненту в дереве компонентов (это все имена родительских компонентов), они должны начинаться с / чтобы указать абсолютный путь.

Возвращаемое значение - это объект-обёртка Property Доступ к базовому значению (в данном случае LReal с единицей измерения Pounds) предоставляется через GetProperty(...).value.

Наблюдения и чувства

В дополнение к прямому доступу к свойствам Jester имеет систему наблюдения. Система позволяет сделать часто используемые данные легкодоступными, а также предоставлять более сложные данные, например, поступающие из DCS SDK.

Наблюдения являются частью чувств, которых у Jester несколько (глаза, уши, …). На данный момент большинство из них находятся в разработке.

local isAirborne = GetJester().awareness:GetObservation("airborne") or false

Взаимодействия

Одним из ключевых аспектов Jester является то, что он может взаимодействовать с кабиной, щёлкая переключатели, кнопки и вращая ручки.

Для этого API предлагает два подхода.

Взаимодействия с компонентами

Предпочтительный способ взаимодействия с кабиной - через систему компонентов.

Чтобы разрешить взаимодействие, необходимо зарегистрировать манипулятор в F_4E_WSO_Cockpit.lua:

-- ChaffMode: OFF, SGL, MULT, PROG
self:AddManipulator(
  "Chaff Mode",
  {component_path = "/WSO Cockpit/WSO Left Console/AN_ALE-40 CCU/Chaff Mode Knob"}
)

После этого с ним можно легко взаимодействовать, например:

task:AddAction(SwitchAction:new("Chaff Mode", "MULT"))
-- or in short
task:Click("Chaff Mode", "MULT")

или считывать его текущее значение:

local cockpit = GetJester():GetCockpit()
local chaff_mode = cockpit:GetManipulator("Chaff Mode"):GetState()

Необработанные взаимодействия

Если нужный переключатель ещё не поддерживает интерфейс компонентов, можно вместо этого использовать необработанный интерфейс, который вызывает команды DCS напрямую, как если бы игрок вручную нажал привязку.

-- отправляет значение 1 через команду WSO_EJECT_INSTANT на устройство EJECTION_SEAT_SYSTEM
ClickRaw(devices.EJECTION_SEAT_SYSTEM, device_commands.WSO_EJECT_INSTANT, 1)

-- отправляет значение, соответствующее положению 2 на 7-позиционной ручке
ClickRawKnob(devices.HUD_AN_ASG_26, device_commands.HUD_SelectHUDMode, 2, 7)

См. devices.lua для всех доступных устройств и аналогично command_defs.lua для команд.

Как правило, ручки и 2-позиционные переключатели используют диапазон [0, 1] Как правило, ручки и 2-позиционные переключатели используют диапазон [-1, +1]. Для 3-позиционных переключателей -1 обычно используется для перемещения 3-позиционного переключателя вниз, +1 - для перемещения его вверх, но некоторые переключатели имеют другую ориентацию. См. default.lua и clickabledata.lua чтобы узнать больше о конкретном переключателе и о том, как он реагирует на значения.

События

Помимо нажатия переключателей, Jester может реагировать на события, отправляемые как из C++, так и из Lua. Система следует простому шаблону наблюдатель/слушатель:

ListenTo("go_silent", function(task)
  task:Click("Radar Power", "STBY")
end)

with:

if is_aar then
  DispatchEvent("go_silent")
end

Task API

Ключевым аспектом написания логики для Jester является использование класса Task. Задачи состоят из последовательности действий. Задача может быть приостановлена, возобновлена или полностью отменена системой, если это необходимо.

Действия по своей природе выполняются асинхронно. Выполнение щелчка займёт некоторое время и не будет выполнено мгновенно. В частности, добавление действия щелчка к задаче не блокирует код, оно просто добавляется в цепочку действий, которые будут выполнены в конечном итоге.

Эта концепция аналогична Future-API в других языках, и Task предлагает fluent-API для удобной работы с ней.

Рассмотрим следующий пример:

local task = Task:new()
task:Roger()
  :Click("Radar Power", "OPER")
  :Wait(min(4))
  :Click("Screen Mode", "radar")
  :Say("phrases/radar_ready")
  :Then(function()
    self.scan_for_bandits = true
  end)

Among other functions, the API offers:

  • AddAction - любое действие, основа API
  • Then - анонимная функция
  • Wait - время
  • WaitUntil - предикат
  • Say - фраза
  • Roger
  • CantDo
  • Click - имя, состояние
  • ClickFast - имя, состояние
  • ClickShort - имя, состояние
  • ClickShortFast - имя, состояние

Подробнее см. Task.lua.

Интерфейс

Jester предоставляет два типа пользовательских интерфейсов. Колесо с выбираемыми опциями и диалоговое окно с вопросами и выбираемыми ответами, которые отображаются по требованию. Подробнее см. Wheel UI и Dialog UI.