Lua API
Здесь приводится краткий обзор Lua, на котором в основном написан Jester, и объясняется сам Jester API.
Начало работы с Lua
Полезные ссылки:
- Документация по Lua: https://www.lua.org/docs.html
Таблицы
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
- любоедействие
, основа APIThen
- анонимная функцияWait
- времяWaitUntil
- предикатSay
- фразаRoger
CantDo
Click
- имя, состояниеClickFast
- имя, состояниеClickShort
- имя, состояниеClickShortFast
- имя, состояние
Подробнее см. Task.lua
.
Интерфейс
Jester предоставляет два типа пользовательских интерфейсов. Колесо с выбираемыми опциями и диалоговое окно с вопросами и выбираемыми ответами, которые отображаются по требованию. Подробнее см. Wheel UI и Dialog UI.