Создание LUA-плагинов в ВКО - автор bobbisson

Оригинал: https://forums.lotro.com/showthread.php?428196

От переводчика
Ниже публикуется перевод прекрасной подборки статей Garan'а по написанию плагинов LotRO. Цель перевода – дать как можно большему количеству интересующихся, но не владеющих английским на требуемом уровне, людей необходимые сведения для самостоятельного написания или перевода плагинов. Я считаю, нас должно быть много. Переводится и выкладывается все по мере наличия свободного времени.

Некоторые разделы содержат сведения, которые на текущий момент немного устарели. Поэтому в некоторых случаях [вот в таком виде даются примечания переводчика]. Буду признателен за любые замечания и предложения по качеству перевода.

Содержание
 

Ссылка на оригинал

Перед началом работы
Для начала вам понадобится несколько простых инструментов. Во-первых, вам нужен справочник по языку Lua. Лично я пользуюсь https://www.lua.org/manual/5.1/, который довольно прост и удобен в обращении.

Во-вторых, вам понадобится документация по API от Турбины. [Последнее обновление 20-ноя-2013 приурочено к выходу Helm's Deep, но с U10 до U12 изменений в основной части API практически не было:
Текущая версия по Хельмовой Пади: https://www.lotrointerface.com/downlo...mentation.html
Предыдущая версия по Изенгарду (2011 год): https://www.lotrointerface.com/downlo...mentation.html].

Еще вам потребуется редактор, причем хватит даже простого текстового редактора вроде Блокнота [с кириллицей свой геморрой: блокнот умеет сохранять русскоязычные файлы в кодировке utf-8 с начальными символами (BOM, byte ordering mark), поэтому его можно использовать только для .plugin-файлов]. Хотя некоторые предпочитают редакторы с подсветкой синтаксиса и инструменты управления проектом для организации файлов, я обычно обхожусь Блокнотом [см. нижекомментарии Ворожея, одним словом: для работы с .lua-файлами вам нужен редактор, сохраняющий файлы в кодировке utf-8 без BOM: UltraEdit, Notepad++ и т.д.].

Если вы планируете работать с графикой, вам понадобится редактор изображений, способный сохранять изображения в формате .jpg и/или .tga, поскольку только эти форматы поддерживаются клиентом ВКО.

Последнее, что может вам пригодиться, это примеры плагинов, c которыми можно поразбираться и поиграться. Турбина опубликовала набор файлов с примерами, которые можно скачать в формате 7zip по ссылкеhttps://content.turbine.com/sites/lot..._LuaPlugins.7z. Вам также стоит заглянуть на LoTROInterface.com или другой сайт с плагинами. Один из лучших способов научиться чему-то – это покопаться в коде, покрутить-повертеть, потянуть-подергать и посмотреть, что из этого выйдет 

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

Ссылка на оригинал

С чего начать
Любой плагин состоит, по крайней мере, из двух элементов: файла описания плагина .plugin и одного или более файлов скриптов .lua. Файл .plugin должен находиться в подкаталоге каталога «Мои документы\The Lord of the Rings\Plugins» (полный путь до каталога «Мои документы» зависит от версии операционной системы). Общепринятым стандартом для дерева каталогов является следующая структура [в названиях каталогов нельзя использовать русские буквы]:

Plugins\

AuthorName (имя автора)\

PluginName (название плагина)\

Resources

«AuthorName» – это каталог с уникальным именем, в котором находятся все плагины, написанные автором. Каталог «AuthorName» обычно содержит только файлы описания плагина .plugin. Далее, обычно каждый плагин находится в своем подкаталоге с соответствующим именем.

Каталог «PluginName» обычно содержит все файлы скриптов .lua этого плагина – если, конечно, этот плагин не использует файлы из общей библиотеки. Основное преимущество использования общей библиотеки для общих классов заключается в возможности хранения и сопровождения единственного экземпляра общего кода. Основной недостаток в том, что любые изменения общего кода потенциально могут повлечь нежелательные последствия для плагинов. Я в целом предпочитаю держать отдельные копии .lua-файлов в разных каталогах, чтобы не нарваться на проблемы совместимости (дальний родственник той старой эпидемии под названием «DLL-кошмар») в случае, если кому-то захочется обновить только один из плагинов, использующих общий скрипт с классами.

Файл .plugin является xml-файлом со следующей структурой:
 

Код:
<?xml version="1.0"?>
<Plugin>
    <Information>
        <Name>НазваниеПлагина</Name>
        <Author>ИмяАвтора</Author>
        <Version>НомерВерсии</Version>
        <Description>Описание вашего плагина</Description>
        <Image>ФайлКартинки</Image>
    </Information>
    <Package>Путь.До.Основного.Lua.Скрипта</Package>
    <Configuration Apartment="ИмяАпартаментов" />
</Plugin>

«НазваниеПлагина» – название, которое используется для загрузки плагина командой “/plugins load НазваниеПлагина” либо появляется в игре при выполнении команд “/plugins list” или “/plugins refresh”. Если вы используете плагин-менеджер, в нем будет отображено то же название. [Здесь и далее: оригинал гайда писался до U5, когда еще не было штатного турбиновского менеджера плагинов (Меню – Система – Управление плагинами или команда “/plugins manager”), поэтому в переводе морально устаревшие места подчищены. В любом случае, про плагины manager от Shady или Bootstrap от Equendilлюбой разработчик знать обязан.]

«ИмяАвтора» – имя автора плагина. Указывается только с целью организации и документирования плагинов. Никакого влияния на работу плагина не оказывает, но может быть получено программным путем через таблицу “Plugins”.

«НомерВерсии» – версия плагина. Отображается в менеджере плагинов, а также при выполнении команд “/plugins list” или “/plugins refresh”. Это значение также может быть получено программным путем для корректной работы и обновления устаревших данных плагина.

«Описание вашего плагина» – текст описания. Отображается в турбиновском менеджере плагинов.

«ФайлКартинки» – путь до файла .JPG или .TGA. Обратите внимание, что файл с разрешением больше, чем 32x32, будет откадрирован в 32x32. Если изображение имеет размер меньше, чем 32x32, оно будет растянуто. Это изображение будет показано в турбиновском менеджере плагинов.

Значение «Путь.До.Основного.Lua.Файла» – путь до Main-скрипта плагина относительно каталога Plugins. Обратите внимание, что при указании пути вместо “\” или “/” в качестве разделителя используется “.”. Этот файл практически всегда подгружается, парсится и исполняется первым. Исключение составляет файл “__init__.lua”, который будет исполнен первым, если находится в том же каталоге.

Необязательный параметр Configuration позволяет указать, что плагин будет выполняться в своем собственном апартаменте, или адресном пространстве. Это означает, что плагин получит свою собственную копию объектов Turbine и переменных окружения. Наиболее частой причиной для указания настройки Configuration является необходимость выгружать плагин независимо от других плагинов или изолировать глобальные переменные и обработчики событий от других плагинов. Если же ваш плагин не нуждается в выгрузке и использует безопасные обработчики событий (что рассмотрено ниже), то, вероятно, отдельный апартамент ему не нужен. Учтите, что использование отдельных апартаментов существенно увеличивает объем памяти, используемой системой Lua, поскольку влечет создание множества копий окружения и глобальных объектов.

Запомните одну важную деталь: плагины не выгружаются, апартаменты выгружаются. То есть, при выполнении команды “/plugins unload ИмяАпартаментов” вы выгружаете все плагины, которые разделяют эти апартаменты.
 

Ссылка на оригинал

Файлы __init__.lua
Прежде чем приступить к обработке файлов в каком-либо каталоге, движок Lua попытается найти в этом каталоге файл __init__.lua и – если таковой будет найден – обработать его первым. Файл __init__.lua используется для того, чтобы импортировать, или подгрузить, код из нескольких файлов, находящихся в том же каталоге. Это устраняет необходимость повторять одни и те же директивы “import” в каждом .lua-файле, поскольку все файлы, перечисленные в __init__.lua, будут импортированы автоматически. Важно помнить, что импорт файлов будет выполнен в том порядке, в котором они перечислены в __init__.lua, поэтому если какой-либо файл использует объекты или функции, определенные в другом файле, то он должен быть указан последним. Файл __init__.lua обрабатывается прежде, чем основной файл, указанный в параметре “Package” файла .plugin. Файл __init__.lua может подгружать только файлы, находящиеся в том же каталоге. В каждом каталоге может быть свой собственный файл __init__.lua.

[Помимо директив “import”, файл __init__.lua может содержать код, в котором создаются какие-либо общие объекты, производится инициализация переменных и т.п. Например, первые несколько строчек файла __init__.lua плагина BuffBars выглядят следующим образом:
 

Код:
-- настройка локализации плагина
L = PengorosPlugins.Utils.DefaultLocale();
PengorosPlugins.Utils.ImportLocale("PengorosPlugins.BuffBars");

import "PengorosPlugins.BuffBars.Constants";
import "PengorosPlugins.BuffBars.Settings";
-- и еще много-много инклюдов

Напоследок закрепим: выполняется этот файл один раз для каждой директории плагина, и в каждой директории может быть свой __init__.lua.]

Ссылка на оригинал
[Мне показалось нелишним и придать этой главе более упорядоченный вид, а также добавить несколько определений.]

Загрузка плагинов

  • Когда игрок вызывает команду “/plugins load НазваниеПлагина”, начинается обработка того файла .plugin, значение параметра “Name” которого идентично “НазваниеПлагина”.
  • Если в файле .plugin присутствует параметр “Configuration” с уникальным значением атрибута “Apartment”(*), то для плагина будет создано новое окружение; в противном случае для плагина будет использовано стандартное окружение.
  • Движок Lua смотрит в каталог, где находится прописанный в параметре “Package” основной модуль. Если там есть файл __init__.lua, он поступает в обработку.
  • После того как файл __init__.lua обработан, наступает очередь файла из параметра “Package”, который подгружается, парсится и исполняется. Поскольку Lua является скриптовым языком, все команды (каждая из которых может занимать несколько строк) обрабатываются последовательно.
  • Парсер(**) считывает текст из файла, пока не дойдет до конца исполняемой команды(***), после чего команда выполняется. В конце команды можно, но не обязательно, ставить точку с запятой.
  • Если при обработке основного скрипта будет обнаружена ошибка, то парсер выдаст сообщение об ошибке в чат, и плагин не будет загружен. [Каждый хоть раз содрогался, увидев сообщение примерно следующего содержания:]
    Код:


    ..s Online\Plugins\BlahPlugins\WeirdPlugin\Main.lua :12: Unable to parse file!

  • Код функций компилируется, но значения переменных и корректность внешних ссылок не проверяется, пока функция не будет вызвана – так что ошибки вполне могут проявиться в уже давно загруженном и работающем плагине. [Здесь уже вариантов гораздо больше, например:]
    Код:


    ...rd of the Rings Online\Plugins\Equendil\LIP\Main.lua:4: attempt to call field 'MakeLocale' (a nil value)

(*) Apartment (апартамент, подразделение) – термин из COM-архитектуры, указывающий на обособленное адресное пространство, в котором создаются объекты. В Lotro Lua, если речь идет об «окружении» (global environment), имеется в виду глобальная переменная _G, которая содержит все адреса, явки и пароли загруженных в данный «апартамент» модулей.

(**) Parser (парсер, синтаксический анализатор) – (под-)программа, выполняющая синтаксический разбор последовательности лексем в соответствии с правилами формальной грамматики. Проще говоря, это интерпретатор,переводящий написанный текст программы в исполняемые команды.

(***) Statement (команда, инструкция) – наименьшая часть программы, имеющая автономный смысл. Под «операторами» (operator) чаще понимаются присваивание, сложение и пр.; «выражения» (expressions) не всегда являются командами. Поэтому в переводе выбран термин «команда», хотя правильнее было бы думать о statement’ах как о предложениях, из которых состоит текст программы.

 

Ссылка на оригинал

К этому моменту вы уже, вероятно, созрели для написания своего первого плагина. Согласно требованиям традиции, мы должны начать с простого плагина «Здравствуй, мир!». Для этого вначале создадим файл описания .plugin. Назовем его HelloWorld.plugin и сохраним в каталоге Мои документы\The Lord of the Rings Online\Plugins\YourName [кодировка файла utf-8, наличие BOM некритично]:
 

Код:
<?xml version="1.0"?>
<Plugin>
 <Information>
  <Name>ЗдравствуйМир</Name>
  <Author>ВашеИмя</Author>
  <Version>1.00</Version>
 </Information>
 <Package>YourName.HelloWorld.Main</Package>
</Plugin>

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

Следующим шагом создадим файл Main.lua и сохраним его в каталоге Мои документы\The Lord of the Rings Online\Plugins\YourName\HelloWorld [utf-8, строго без BOM]:
 

Код:
import "Turbine.UI"; -- здесь задекларирован элемент интерфейса "статический текст" (label)
import "Turbine.UI.Lotro"; -- здесь задекларирован класс стандартного окна
HelloWindow=Turbine.UI.Lotro.Window(); -- создаем объект стандартного окна, вызывая конструктор
HelloWindow:SetSize(200,200); -- устанавливаем размер окна 200x200 пикселей
HelloWindow:SetPosition(Turbine.UI.Display:GetWidth()/2-100,Turbine.UI.Display:GetHeight()/2-100); -- располагаем окно по центру
HelloWindow:SetText("Превед"); -- устанавливаем заголовок окна
HelloWindow.Message=Turbine.UI.Label(); -- создаем объект label для нашего сообщения
HelloWindow.Message:SetParent(HelloWindow); -- привязываем объект к основному окну как дочерний элемент 
HelloWindow.Message:SetSize(180,20); -- задаем размер сообщения в пикселях
HelloWindow.Message:SetPosition(10,90); -- размещаем сообщение по центру по вертикали с отступом 10 пикселей слева и справа
HelloWindow.Message:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter); -- выравниваем текст сообщения внутри элемента интерфейса по центру
HelloWindow.Message:SetText("Здравствуй, мир!"); -- задаем текст сообщения
HelloWindow:SetVisible(true); -- показываем окно (по умолчанию окна скрыты)

После того как вы создадите эти файлы, зайдите в игру и наберите команду “/plugins list”. Если вы сохранили файлы в правильном месте, в списке вы увидите строчку “ЗдравствуйМир (1.00)”. Это хорошая проверка на предмет правильности пути, где вы сохранили файлы. Если плагина нет в списке, еще раз убедитесь, что вы используете путь до каталога с вашими документами, а НЕ каталог Program Files, куда устанавлен ВКО. [Если клиент был запущен до сохранения файлов, вместо команды “/plugins list” выполните “/plugins refresh”.]

Как только вы убедились, что плагин находится в списке, наберите “/plugins load здравствуймир”. Обратите внимание, что несмотря на то, что в большинстве случаев Lua чувствителен к регистру, название плагина и команды являются исключениями. Если вы все сделали правильно, то в качестве вознаграждения вы получите простенькое окошко в центре экрана с заголовком «Превед», границей, кнопкой закрытия окна и – самое главное! – словами «Здравствуй, мир!» посередине.
 

Ссылка на оригинал

Язык Lua, по правде говоря, не является объектно-ориентированным, но, создав объект ‘Class’, Турбина создала обертку, создающую ощущение некой объектной ориентированности. Файл “Class.lua” находится в примерах кода, предоставляемых Турбиной [скачать можно отсюда]. Не стоит рассчитывать на то, что конечный пользователь установит себе эти примеры, поэтому лучше всего скопировать его в своей проект и сослаться на локальную копию файла.

В примере «Здравствуй, мир!» мы создали экземпляр окна, просто вызвав конструктор класса Window:

Код:
HelloWindow=Turbine.UI.Lotro.Window()

При помощи функции “class” вы можете создавать новые классы, унаследованные от существующих, и затем создавать их экземпляры по мере надобности. Для создания класса окна «здравствуй, мир» вам необходимо вместо этого написать:

Код:
import "YourName.HelloWorld.Class"
HelloWindow=class(Turbine.UI.Lotro.Window)
function HelloWindow:Constructor(x,y)
  if x==nil or y==nil then
    x=Turbine.UI.Display:GetWidth()/2-100;
    y=Turbine.UI.Display:GetHeight()/2-100;
  end
  Turbine.UI.Lotro.Window.Constructor(self);
  self:SetSize(200,200); -- устанавливаем размер окна 200x200 пикселей
  self:SetPosition(x,y);
  self:SetText("Превед"); -- устанавливаем заголовок окна
  self.Message=Turbine.UI.Label(); -- создаем объект label для нашего сообщения
  self.Message:SetParent(self); -- привязываем объект к основному окну как дочерний элемент 
  self.Message:SetSize(180,20); -- задаем размер сообщения в пикселях
  self.Message:SetPosition(10,90); -- размещаем сообщение по центру по вертикали с отступом 10 пикселей слева и справа
  self.Message:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter); -- выравниваем текст сообщения внутри элемента интерфейса по центру
  self.Message:SetText("Здравствуй, мир!"); -- задаем текст сообщения
  self:SetVisible(true); -- показываем окно (по умолчанию окна скрыты)
end

Обратите внимание, что мы немного изменили код, добавив возможность указать координаты x,y в конструкторе. Теперь вы можете создавать столько экземпляров класса, сколько захотите, с различными координатами на экране.

Код:
window1=HelloWindow(10,40);
window2=HelloWindow(); -- по умолчанию в центре экрана
window3=HelloWindow(40,200);

При выполнении приведенного кода 3 экземпляра класса “HelloWorld” будут показаны в 3 разных местах на экране.
 

Ссылка на оригинал

Вызов методов может иметь вид Object.Function() или Object:Function(). Поначалу это может немного путать, но ключевое отличие – в том, что при вызове функции через “:” объект, метод которого вызывается, автоматически добавляется в начало списка аргументов. Функции также можно переопределять. Например, класс-потомок окна может переопределить метод :SetPosition(), если ему нужна дополнительная функциональность, отсутствующая в методе базового класса Window:SetPosition() (или любом другом методе). Например:

Код:
import "Turbine.UI"; -- здесь задекларирован элемент интерфейса "статический текст" (label)
import "Turbine.UI.Lotro"; -- здесь задекларирован класс стандартного окна
import "YourName.HelloWorld.Class" – импортируем функцию Турбины class
HelloWindow=class(Turbine.UI.Lotro.Window)
function HelloWindow:Constructor(x,y)
  if x==nil or y==nil then
    x=Turbine.UI.Display:GetWidth()/2-100;
    y=Turbine.UI.Display:GetHeight()/2-100;
  end
  Turbine.UI.Lotro.Window.Constructor(self);
  -- переопределяем штатную функцию SetPosition, добавив простой вывод на экран. 
  -- Очевидно, этой возможности можно найти множество более полезных применений...
  self.SetPosition=function(sender, left, top)
    Turbine.UI.Window.SetPosition(self, left, top); -- передаем аргументы во встроенную функцию базового класса
    Turbine.Shell.WriteLine("Окно перемещено в точку ("..tostring(left)..","..tostring(top)..")");
    -- делаем что-то еще
  end
  self:SetSize(200,200); -- устанавливаем размер окна 200x200 пикселей
  self:SetPosition(x,y);
  self:SetText("Превед"); -- устанавливаем заголовок окна
  self.Message=Turbine.UI.Label(); -- создаем объект label для нашего сообщения
  self.Message:SetParent(self); -- привязываем объект к основному окну как дочерний элемент
  self.Message:SetSize(180,20); -- задаем размер сообщения в пикселях
  self.Message:SetPosition(10,90); -- размещаем сообщение по центру по вертикали с отступом 10 пикселей слева и справа
  self.Message:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter); -- выравниваем текст сообщения внутри элемента интерфейса по центру
  self.Message:SetText("Здравствуй, мир! "); -- задаем текст сообщения
  self:SetVisible(true); -- показываем окно (по умолчанию окна скрыты)
end
window1=HelloWindow(10,40);
window2=HelloWindow(); -- по умолчанию в центре экрана
window3=HelloWindow(40,200);

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

Обратите внимание, что строчку

Код:
  self:SetPosition(x,y);

можно заменить строчкой

Код:
  self.SetPosition(self,x,y);

и получить тот же результат.

(не окончено)

Внимание!

Эта статья скопирована с официального русскоязычного форума игры, которого больше не существует. Многие ссылки могут не работать или вести не туда, куда вам хотелось бы.