Оригинал: https://forums.lotro.com/showthread.php?428196 (link is external)
От переводчика
Ниже публикуется перевод прекрасной подборки статей Garan'а по написанию плагинов LotRO. Цель перевода – дать как можно большему количеству интересующихся, но не владеющих английским на требуемом уровне, людей необходимые сведения для самостоятельного написания или перевода плагинов. Я считаю, нас должно быть много. Переводится и выкладывается все по мере наличия свободного времени.
Некоторые разделы содержат сведения, которые на текущий момент немного устарели. Поэтому в некоторых случаях [вот в таком виде даются примечания переводчика]. Буду признателен за любые замечания и предложения по качеству перевода.
Содержание
Перед началом работы
Для начала вам понадобится несколько простых инструментов. Во-первых, вам нужен справочник по языку Lua. Лично я пользуюсь https://www.lua.org/manual/5.1/ (link is external), который довольно прост и удобен в обращении.
Во-вторых, вам понадобится документация по API от Турбины. [Последнее обновление 20-ноя-2013 приурочено к выходу Helm's Deep, но с U10 до U12 изменений в основной части API практически не было:
Текущая версия по Хельмовой Пади: https://www.lotrointerface.com/downlo...mentation.html (link is external)
Предыдущая версия по Изенгарду (2011 год): https://www.lotrointerface.com/downlo...mentation.html (link is external)].
Еще вам потребуется редактор, причем хватит даже простого текстового редактора вроде Блокнота [с кириллицей свой геморрой: блокнот умеет сохранять русскоязычные файлы в кодировке utf-8 с начальными символами (BOM, byte ordering mark), поэтому его можно использовать только для .plugin-файлов]. Хотя некоторые предпочитают редакторы с подсветкой синтаксиса и инструменты управления проектом для организации файлов, я обычно обхожусь Блокнотом [см. нижекомментарии Ворожея (link is external), одним словом: для работы с .lua-файлами вам нужен редактор, сохраняющий файлы в кодировке utf-8 без BOM: UltraEdit, Notepad++ и т.д.].
Если вы планируете работать с графикой, вам понадобится редактор изображений, способный сохранять изображения в формате .jpg и/или .tga, поскольку только эти форматы поддерживаются клиентом ВКО.
Последнее, что может вам пригодиться, это примеры плагинов, c которыми можно поразбираться и поиграться. Турбина опубликовала набор файлов с примерами, которые можно скачать в формате 7zip по ссылке (link is external)https://content.turbine.com/sites/lot..._LuaPlugins.7z (link is external). Вам также стоит заглянуть на LoTROInterface.com (link is external) или другой сайт с плагинами. Один из лучших способов научиться чему-то – это покопаться в коде, покрутить-повертеть, потянуть-подергать и посмотреть, что из этого выйдет
Одно правило, о котором стоит помнить: в большинстве случаев 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 (link is external) или Bootstrap от Equendil (link is external)любой разработчик знать обязан.]
«ИмяАвтора» – имя автора плагина. Указывается только с целью организации и документирования плагинов. Никакого влияния на работу плагина не оказывает, но может быть получено программным путем через таблицу “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.]
Загрузка плагинов
..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” находится в примерах кода, предоставляемых Турбиной [скачать можно отсюда (link is external)]. Не стоит рассчитывать на то, что конечный пользователь установит себе эти примеры, поэтому лучше всего скопировать его в своей проект и сослаться на локальную копию файла.
В примере «Здравствуй, мир!» мы создали экземпляр окна, просто вызвав конструктор класса 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);
и получить тот же результат.
(не окончено)