×
Меню
Индекс

Scripting under MWSE 2.1 LUA

mintmike (C) 01 2020
https://www.fullrest.ru/forum/topic/41708-mwse-21-uroki-po-lua-skriptam/
 
Примечание!
от 10 2022
Данные, приведенные в статье, несколько устарели.
Т.к. эти два года, МВСЕ активно развивалось!
Отчего, новейшие версии МВСЕ 2.0 могут содержать гораздо больше команд и заменять означенные здесь.
Рекомендуется посетить сайт разработчиков и свериться с текущей документацией!
 
И не забудьте проверить плагины этого автора на Nexuse!
https://www.nexusmods.com/morrowind/mods/47015
содержат огромное кол-во всевозможных примеров!

MWSE - "расширитель скриптов Мирровинда" или просто МыВСЕ:)
 
Описание функций MWSE:
https://mwse.readthedocs.io/en/latest/mwscript/functions/index.html
 
Константы игрового мира:
https://mwse.readthedocs.io/en/latest/mwscript/references.html
 
Настройки игры, Game Settings (глобальные переменные):
https://mwse.readthedocs.io/en/latest/mwscript/GMSTs.html
 
MWSE, начиная с версии 2.1, поддерживает простой и быстрый скриптовый язык Lua, для чего имеет несколько библиотек:
  event    - библиотека событий, при которых будут вызываться lua-функции
    json     - взаимодействие с очень удобным json-форматом сохранения/загрузки игровых данных
    mge      - вызов функций MGE XE
    mwscript - вызов оригинальных Морровинд-скриптов. Используйте только в случае необходимости
    mwse     - небольшое количество общих функций MWSE
    string   - обращение со строкой
    table    - взаимодействие с таблицами
    tes3     - основной функционал для управления Морровиндом
    tes3ui   - управление пользовательским интерфейсом игры (все эти окна и меню игрока)
    timer    - удобные таймеры
 
Об использовании этих возможностей у нас и пойдёт с вами речь. Уроки задумывались как справочник, идут в целом по усложнению, но читать можно с любого места.
 
Самая лучшая книга по Lua:  https://vk.com/doc-127857710_437836162
 
Руководство на русском языке:  https://lua.org.ru/contents_ru.html
 
Сокращенное руководство: https://lua.org.ru/main.html#the-little-lua-book
 
А мы с вами давайте лучше напишем и запустим в игре наш первый скрипт!

*** Урок 1: Сообщения ***  
Для запуска lua-скриптов, вам не нужно создавать мод в Construction Set. Достаточно открыть блокнот и записать, к примеру, вот такую функцию:
 
local function myFun(e)
    mwse.log("Запущено сохранение: %s", e.filename)
end
event.register("loaded", myFun)
 
Мы создали функцию local myFun(e), которая пишет имя загруженного в игре сохранения в лог-файл (по адресу <Путь к папке Morrowind>\MWSE.log).
 
local  - означает, что функция локальная (не может быть вызвана из других файлов),
 
e - информация события (типа event), которая будет передаваться в вашу функцию,
 
%s - команда форматирования, которая вставляет вместо "%s" строку из переменной e.filename.
 
event.register - регистрирует вашу функцию для события (привязывает функцию к событию)
 
"loaded" - имя события, данное событие вызывается при окончании загрузки сохранения в игре. Каждый раз, при возникновении события "loaded"(загрузке сохранения) будет запускаться ваша функция.
 
Итак, первый lua-скрипт к Морровинду написан. Сохраняем файл как:
 
<Путь к папке Morrowind>\Data Files\MWSE\mods\<ваш мод>\main.lua
 
Имя основного файла должно быть обязательно "main.lua", таковы требования МыВСЕ.
Имя подкаталога <ваш мод> может быть любым. При старте игры MWSE сканирует все подкаталоги ..\Data Files\MWSE\ и компилирует найденные скрипты. Ваш скрипт также скомпилируется, и если синтаксических ошибок нет, скрипт запустится один раз. Что означает "скрипт запустится один раз"? Это значит, что ваш файл исполнится при старте игры, лишь раз. Но так как фунция "myFun" будет зарегистрирована для события"loaded", то и Запускаться она будет каждый раз по событию. При старте игры функция "myFun" вызвана не будет.
 
Заходим в Морровинд, загружаем любое сохранение и выходим. Где-то в середине файле логов MWSE.log должна появится запись:
 
"Запущено сохранение: <имя файл сохранения>"
 
Если допущена ошибка, вы получите сообщение в файле логов:
 
<Путь к папке Morrowind>\MWSE.log
 
Используйте поиск по тексту с именем <вашего мода>, чтобы быстро отыскать сообщения об ошибках в вашем lua-коде.
В логах после слова Error указано описание ошибки и строка, в которой она возникла. Исправляйте ошибку и запускайте
Морровинд снова. Ну что же. Поздравляю, первый lua-скрипт для Морровинда создан!
 
Давайте запустим ещё какой-нибудь пример. Замените текст в вашем файле, или просто добавьте ниже:
 
local function showMessage(e)
    tes3.messageBox("Вы взяли " .. tes3.getMobilePlayer().readiedWeapon.object.id)
end
event.register("weaponReadied", showMessage)
 
Здесь при экипировке оружия будет выведено сообщение на экран. Событие "weaponReadied" вызывается, когда орудие взято в руки готово к атаке.
 
.. - две точки есть операция слияния строк. id является строкой
 
getMobilePlayer() - возвращает ссылку на mobile игрока (на вашу мобилку :), ваш игровой болванчик, а точнее тело)
 
Подробнее о типах объектов, актеров, предметов в следующих уроках.
 
Ну вот, оказывается, создавать моды очень просто. Только идеи подавай!

*** Урок 2: События ***  
 
Вот все типы событий: https://mwse.readthedocs.io/en/latest/lua/event.html
 
Рассмотрим с дюжину основных и как они работают:
    "loaded" - игра загружена (из сохранения)
    "initialized" - игра инициализирована, .esm и .esp файлы загружены
    "simulate" - начало каждого игрового кадра (не пауза и не меню)
    "enterFrame" - начало любого кадра
    "skillRaised" - произошло повышение скила
    "attack" - ближняя физ атака (с участием актера или существа, не обязательно игрока)
    "activate" - активация (кто-либо нажал на активатор)
    "activationTargetChanged" - в прицеле игрока объект для активации изменился
    "spellCast" - кто-либо начал произносить заклинание
    "magicCasted" - заклинание успешно произнесено или алхимия дала эффект
    "equip" - момент экипировки предмета (начало момента)
    "keyDown" - нажата клавиша на клавиатуре
    "menuEnter" - вызвано меню (на пр.кн. мыши)
    "weaponReadied" - экипировано оружие
На событиях держится все обращения от MWSE к вашему коду.
 
event.register(<событие>, <функция>)  -- регистрирует имя вашей функции в специальной таблице
 
Событие одно, а привязанных к нему функций может быть много. Функции даже могут иметь одни и те же имена в разных файлах.
Все они будет вызываться при каждом наступлении условий и возниконвении связанного с ними события.
Таким образом, для каждого события MWSE вызывает свои, зарегистрированные именно для него функции.
Мы можем очистить таблицу для конкретного события, написав:
 
event.clear("имя события") Все запомненные для события функции будут отменены.

*** Урок 3: Клавиши ***
Коды всех клавиш есть тут:
https://mwse.readthedocs.io/en/latest/lua/guide/scancodes.html
 
Для клавиатуры нам достаточно двух событий:
 
event.register("keyDown", checkKeys) - вызывается при нажатии клавиши
event.register("keyUp", checkKeys)   - вызывается при отпускании клавиши
 
Как сделать так, чтобы нажатие на кнопку клавиатуры что-нибудь совершало?
 
local function checkKeys(e)
-- pressed 'V'
if (e.keyCode == 47) then
tes3.messageBox("You pressed 'V'")
end
end
event.register("keyDown", checkKeys)
 
Функция checkKeys(e) проверяет код нажатой клавиши. Если это код клавиши 'v', то будет выведено сообщение.
 
Когда несколько клавиш задействовано, то удобнее оформить так:
 
local function dance()
 ...
end
 
local function savePoint()
 ...
end
 
local function createItem()
 ...
end
 
local function checkKeys(e)
 -- pressed 'U'
 if (e.keyCode == 22) then dance(); return end
 -- pressed 'I'
 if (e.keyCode == 23) then createItem(); return end
 -- pressed 'O'
 if (e.keyCode == 24) then savePoint(); return end
end
 
local function onLoaded()
   event.register("keyDown", checkKeys)
end
event.register("loaded", onLoaded)
 
Ну что, готовы к чему-то побольше, навроде циклов?
*** Урок 4: Казначей в локации ***  
Давайте напишем функцию, которая просканирует локацию и выведет в лог имена и координаты всех и всего найденного в ней. Зарегистрируйте функцию самостоятельно, на событие "loaded".
 
    local function makeLog()
       
        local cell = tes3.getPlayerCell() -- получаем ячейку нахождения игрока
        local step = 1                    -- это счетчик шагов цикла
        local str = tostring(step)        -- запишем значение счётчика в строку str
       
        for ref in cell:iterateReferences() do
          if (ref.object) then
           str = str .. " type: " .. tostring(ref.object.objectType)
           str = str .. " name: " .. ref.object.name
           str = str .. " id: " .. ref.object.id
              str = str .. " pos: " .. tostring(ref.position)
           if (ref.object.objectType == tes3.objectType.npc) then
               str = str .. " " .. tostring(ref.object.race.name)
           end
           print(str)              -- печатам нашу строку в лог
              step = step + 1
              str = tostring(step) -- перезапишем всю строку, записав туда снова значение нашего счётчика
          end
        end
    end
 
Данный цикл на каждом шаге печатает в лог-файл новую заполненную строку.
 
Функция print() после вывода сама переходит в начало след.строки без доп.команд.
ref - пустая новая переменная-ссылка.
 
iterateReferences() - метод для перебора ссылок на все объекты в ячейке. Методы вызываются через двоеточие.
 
Цикл for ref in cell:iterateReferences() do при каждом шаге кладёт в пустую переменную ref новую ссылку на очередной объект до тех пор, пока объекты не закончатся.
 
do ... end - тело цикла. В теле цикла мы проверяем if (ref.object) - содержит ли наша ссылка объект. Если объект пустой (nil то есть нуль), то условие будет ложным (false), и мы переходим далее, на новый шаг. Если ref.object отлично от нуля, условие истинно (true), и выполняется всё, что между then ... end.
 
tostring(ref.object.objectType) - дополняет нашу строку типом объекта. objectType является числом, поэтому мы переводим его в строку c помощью tostring()
 
if (ref.object.objectType == tes3.objectType.npc) then - проверяет, равен ли тип объекта npc. Если истина, то дополним нашу строку расой объекта.
 
objectType - по сути одно из чисел из таблицы ниже. Таблица находится по адресу tes3.objectType. Это таблица типов (неполная, на самом деле типов больше, см. 7 урок).
 
          Value    Code     Meaning
    1230259009     ACTI     Activator
    1212369985     ACTI     Alchemy
    1330466113     AMMO     Ammunition
    1095782465     ACTI     Apparatus
    1330467393     ARMO     Armor
    1263488834     BOOK     Book
    1414483011     CLOT     Clothing
    1414418243     CONT     Container
    1095062083     CREA     Creature
    1380929348     DOOR     Door
    1380404809     INGR     Ingredient
    1129727308     LEVC     Levelled Creature
    1230390604     LEVI     Levelled Item
    1212631372     LIGH     Light
    1262702412     LOCK     Lockpick
    1129531725     MISC     Misc Item
    1598246990     NPC_     NPC               -- наш случай
    1112494672     PROB     Probe
    1095779666     REPA     Repair Item
    1414546259     SCPT     Script
    1195658835     SNDG     Sound Generator
    1279610963     SPEL     Spell
    1413567571     STAT     Static
    1346454871     WEAP     Weapon

***Урок 5 Кровати-Двери-Сундуки ***
 
 
В предыдущем уроке мы нашли не одну сотню объектов. Давайте присмотримся к некоторой мебели. Вдруг не для каждого нпс найдется кровать или спальник? Где им спать, в сундуках? Ах да, в Морровинде время же заморожено, никто не спит, кроме нашего героя. Вот беда. Может быть, мы поправим это в последнем уроке?
 
local beds = 0
local doors = 0
local chests = 0
 
Три локальные переменные для мебели. Если убрать слово local, то переменные станут глобальными (доступными из любого модуля). Локальные переменные могут быть свободно доступны только функциям, определенным в их зоне видимости (то есть в нашем файле).
 
Как выделить из всех предметов, например, кровати? У них имя object.name = 'Кровать'. Но это ненадежно, вдруг имена на английском. Удобнее ориентироваться по id. id для каждого предмета уникален.
 
Как найти слово в строке id? Для поиска в строке существует:
 
 string.find(id,'[Bb]ed')
 -- [Bb] - означает один любой из символов 'B' 'b' 
 
Если в строке id есть слово "bed" или "Bed", функция вернёт нам 2 числа (или nil, если ничего не найдено):
 
    -- pos1 - на каком символе начинается наше слово
    -- pos2 - на каком символе заканчивается наше слово
 
    local pos1, pos2 = string.find(id,'[Bb]ed')
 
Теперь давайте дополним функцию makeLog:
 
    local function makeLog()
       
        local cell = tes3.getPlayerCell()
        local step = 1
        local str = tostring(step)
       
        for ref in cell:iterateReferences() do
          if (ref.object) then
        if (ref.object.name) then              -- не у всех предметов имеется поле name
            str = str .. " " .. ref.object.name
        end
        str = str .. " id: " .. ref.object.id  
        if (ref.object.class) then             -- не у всех предметов имеется поле class
            str = str .. " class: " .. ref.object.class.name
        end
           if (ref.object.objectType == tes3.objectType.npc) then
               str = str .. " " .. tostring(ref.object.race.name)
           end
                    if (string.find(ref.object.id,'[Bb]ed')) then       -- если находим кровать
                     beds = beds + 1
                     str = str .. " BED#" .. tostring(beds) .. ""
                    elseif (string.find(ref.object.id,'[Dd]oor')) then  -- иначе если находим дверь
                     doors = doors + 1
                     str = str .. " DOOR#" .. tostring(doors)
                    elseif (string.find(ref.object.id,'[Cc]hest')) then -- иначе если находим сундук
                     chests = chests + 1
                     str = str .. " Chest#" .. tostring(chests)
                    end
           print(str)
              step = step + 1
              str = tostring(step)
          end
        end
        tes3.messageBox("Найдено кроватей %d, дверей %d, сундуков %d", beds, doors, chests)
    end
 
%d - на это место будет вставлено число из переменной, переданной в функцию messageBox следом за строкой (процесс называется "форматирование строки").
 
Запускаем, проверяем! Если появились хорошие идеи, пишите в комментариях.

Кого создать?
*** Урок 6: Bторой муж для Тойвалэ Отрален***
 
Из урока 4 у нас есть лог объектов в локации, из него можно узнать id - уникальные идентификаторы мировых персонажей, тварей, мебели, и прочих вещей. Давайте заглянем к Арриллу на огонёк и клонируем его (: Надеюсь, его жена не упадёт в обморок. Или это не его жена стоит рядом?
 
    --[[ Комментарий.
         tes3.createReference({...}) - это функция для создания кого-либо, чего-либо,
         на вход получает таблицу {...}
         Можно записать без круглых скобок tes3.createReference{...}
         Делает то же, что и mwscript.PlaceAtPC или xPlace.
      ]]
            local ref = tes3.createReference                  
            {                                                  -- начало таблицы
                object = "arrille00000000",                    -- первый элемент таблицыб можно элементы менять местами, порядок не важен
                position = tes3.getPlayerRef().position,   
                orientation = tes3.getPlayerRef().orientation,
                cell = tes3.getPlayerCell(),
                scale = 1                                     
            }                                                  -- конец таблицы
            tes3.messageBox("Привет, Тойвалэ!")
    end
 
Не вздумайте запускать эту функцию не в начальной локации. Аррилл против такого! Нужны данные из незагруженной ячейки игрового мира. Morrowind чего-то не находит в доступе и крашится. Жалко.
 
Ладно. Тут у нас между фигурными скобками целая таблица с полями
 
object,        - сюда подойдет id объекта или объект типа tes3physicalObject
position,     - позиция создаваемого объекта,         tes3vector3(x,y,z)
orientation, - отриентация создаваемого объекта, tes3vector3(x,y,z)
cell,            - id ячейки или ячейка tes3cell
scale          - масштаб, обычно у всех = 1
 
Как нам усовершенствовать наш код, чтобы клонировать кого угодно от бутылки до Вивека? Сделаем в следующих уроках ^^)

Типы tes3объектов (tes3.objectType)
 
    -- '>>' обозначим наследование (в исходниках MWSE)
    GMST                                        (objectType: 'TSMG')
    PathGrid                                    (objectType: 'DRGP')
    Region                                      (objectType: 'NGER')
    Birthsign                                   (objectType: 'NGSB')
    
    BaseObject >> Object >> Reference           (objectType: 'RFER')
    BaseObject >> Object >> Spell               (objectType: 'LEPS')
    BaseObject >> Object >> Enchantment         (objectType: 'HCNE')
    BaseObject >> Object >> PhisicalObject
    
    PhisicalObject >> Activator                 (objectType: 'ITCA')    
    PhisicalObject >> Actor >> Container        (objectType: 'TNOC')
    PhisicalObject >> Actor >> Creature         (objectType: 'AERC')
    PhisicalObject >> Actor >> NPC              (objectType: '_CPN')
    PhisicalObject >> BodyPart                  (objectType: 'YDOB')
    PhisicalObject >> Door                      (objectType: 'ROOD')
    PhisicalObject >> Land                      (objectType: 'DNAL')
    PhisicalObject >> LandTexture               (objectType: 'XETL')
    PhisicalObject >> LeveledCreature           (objectType: 'CVEL')
    PhisicalObject >> LeveledItem               (objectType: 'IVEL')
    PhisicalObject >> Static                    (objectType: 'TATS')
    PhisicalObject >> Item >> Armor             (objectType: 'OMRA')
    PhisicalObject >> Item >> Alchemy           (objectType: 'HCLA')
    PhisicalObject >> Item >> Apparatus         (objectType: 'APPA')
    PhisicalObject >> Item >> Book              (objectType: 'KOOB')
    PhisicalObject >> Item >> Clothing          (objectType: 'TOLC')
    PhisicalObject >> Item >> Ingredient        (objectType: 'RGNI')
    PhisicalObject >> Item >> Light             (objectType: 'HGIL')
    PhisicalObject >> Item >> Lockpick          (objectType: 'KCOL')
    PhisicalObject >> Item >> Lockpick >> Probe (objectType: 'BORP')
    PhisicalObject >> Item >> Misc              (objectType: 'CSIM')
    PhisicalObject >> Item >> Weapon            (objectType: 'PEAW')
                              Ammo              (objectType: 'OMMA')
                              Repair            (objectType: 'APER')
    
    BaseObject >> AnimationGroup
    BaseObject >> Class                         (objectType: 'SALC')
    BaseObject >> Cell                          (objectType: 'LLEC')
    BaseObject >> Dialogue                      (objectType: 'LAID')
    BaseObject >> DialogueInfo                  (objectType: 'OFNI')
    BaseObject >> Faction                       (objectType: 'TCAF')
    BaseObject >> MagicEffect                   (objectType: 'FEGM')
    BaseObject >> Quest                         (objectType: 'SEUQ')
    BaseObject >> Global                        (objectType: 'BOLG')
    BaseObject >> MagicSourceInstance             (objectType: 'LLPS')
    BaseObject >> Race                          (objectType: 'ECAR')
    BaseObject >> Script                        (objectType: 'TPCS')
    BaseObject >> Skill                         (objectType: 'LIKS')
    BaseObject >> Sound                         (objectType: 'NUOS')
    BaseObject >> SoundGenerator                (objectType: 'GDNS')
    
    MobileObject >> MobileActor >> MobileNPC                  (objectType: 'HCAM')
    MobileObject >> MobileActor >> MobileNPC >> MobilePlayer  (objectType: 'PCAM')
    MobileObject >> MobileActor >> MobileCreature             (objectType: 'RCAM')
    MobileObject >> MobileProjectile >> MobileSpellProjectile (objectType: 'JRPM')

***Урок 7: Что от чего происходит в мире tes3 ***
 
Все что мы видим и слышим в мире tes3, MWSE разделяет на tes3объекты разных типов (см. таблицу tes3типов выше).
 
    GMST          -- глобальные переменные игры        (objectType: 'TSMG')
    PathGrid      -- сетка для поиска пути актёрами    (objectType: 'DRGP')
    Region        -- задаёт погоду и звуки региона     (objectType: 'NGER')
    Birthsign     -- знак рождения героя               (objectType: 'NGSB')
    BaseObject    -- базовый объект для остальных объектов
    MobileObject  -- мобильный объект - это подвижная модель, 'болванка', ваше tes3 (или чьё-то) бренное тельце, а также снаряд
 
(objectType: 'XXXX') - это 4 символа, которые также можно представить уникальным числом. Таблица с типами находится по адресу tes3.objectType. Тип необходим для выведения разновидностей объектов и проверки в программе, с каким объектом мы имеем дело. Давайте рассмотрим основные важные типы поближе.
 
tes3.objectType.object
 
    BaseObject >> Object >> Reference           (objectType: 'RFER')
    BaseObject >> Object >> Spell               (objectType: 'LEPS')
    BaseObject >> Object >> Enchantment         (objectType: 'HCNE')
    BaseObject >> Object >> PhisicalObject
 
Как мы видим, объект (tes3object) происходит от базового объекта и может быть ссылкой (reference), заклинанием, зачарованием или физическим объектом. Физические объекты подразделяются на (см.таблицу): активаторы, актёров, части тела, двери, мировые поверхности, их текстуры, статики (неподвижные объекты), вещи, а также уровневые существа и уровневые вещи. Почти всё, что передставлено в Construction Set, является физическим объектом (tes3.objectType.phisicalObject).
 
Все объекты типа tes3object имеют три поля:
 
object.id          (string)    -- "айди", идентификатор
object.objectType  (number)    -- tes3-тип это просто уникальное число из таблицы
object.sourceMod   (string)    -- мод-источник объекта
 
Кстати объекты actor, npc не являются видимыми моделями, движущимися игре, а являются описаниями, содержащими имя и прочие свойства. "Болванчиков" представляют объекты tes3mobileObject.
 
tes3.objectType.mobileObject
 
Представляет свойства подвижной модели персонажа, существа, выпущенной стрелы. Имеет свойства для управления движением и положением в пространстве.
https://mwse.readthedocs.io/en/latest/lua/type/tes3mobileObject.html
    MobileObject >> MobileActor >> MobileNPC                  (objectType: 'HCAM') -- тело персонажа
    MobileObject >> MobileActor >> MobileNPC >> MobilePlayer  (objectType: 'PCAM') -- тело персонажа игрока
    MobileObject >> MobileActor >> MobileCreature             (objectType: 'RCAM') -- тело сщество
    MobileObject >> MobileProjectile >> MobileSpellProjectile (objectType: 'JRPM') -- тело снаряда
 
Тело персонажа имеет:
 
инвентарь
атрибуты (сила, ловкость, и т.д.)
скилы
наложенные эффекты
список друзей
список врагов
управляющие флаги
AI данные
 
tes3.objectType.reference
 
Очень нужный и важный тип tes3reference, или tes3-ссылка (далее просто "ссылка").
https://mwse.readthedocs.io/en/latest/lua/type/tes3reference.html
 
С помощью этого объекта из функции в функцию передаётся информация об остальных tes3объектах: вещах, актёрах (tes3actor), заклинаниях, генераторах звука и т.п. Давайте взглянем на её происхождение:
 
    BaseObject >> Object >> Reference           (objectType: 'RFER')
 
Как мы видим, ссылка имеет tes3тип (tes3reference) и происходит от объекта, а он в свою очередь от базового объекта. Это означает, что можно написать:
 
    ref.id             -- то же, что и ref.object.id
    ref.objectType     -- а вот тип у ссылки свой (objectType: 'RFER')
    ref.sourceMod      -- то же, что и ref.object.sourceMod
 
Поскольку ссылка - объект, настроенный на другой объект, то в её свойствах мы айди самого объекта, мод и т.п. также. Все кроме типа, тип у ссылки собственный.
 
Ссылок на один и тот же объект может быть много. Обращаться через ссылку очень удобно. Например, мы можем проверить ref, на кого или что она настроена:
 
    if (ref.objectType == tes3.objectType.сontainer) then ... end          -- выполнится, если ref это контейнер
    if (ref.objectType == tes3.objectType.phisicalObject) then ... end     -- выполнится, если ref физ.объект
    if (ref.objectType == tes3.objectType.light) then ... end              -- выполнится, если ref источник света
 
Тут последняя версия уроков, а также рассортированное описание TES3 классов:
https://yadi.sk/d/pNaDhws2m1-6AA
 
3.4 – Выражения, все виды операторов
3.4.9 – Конструкторы таблиц (как создавать {...} таблицы)
3.4.8 – Приоритет операторов (что быстрее выполнится + или >= и т.п)
3.5 – Правила видимости (создание/удаление локальных переменных)

Использует недокументированную (ещё) функцию addMagicEffect, которая позволяет добавлять новые эффекты. Примеры использования можно посмотреть в скриптах мода.
 
    tes3.addMagicEffect
    {
    name,             -- string,
    id,               -- number, EffectID
    description,      -- string, описние
    lighting          -- vector3, цвет свечения, RGB
    icon,             -- string, путь к иконке
    particleTexture , -- string, путь к текстуре частиц
    baseCost,         -- 1.0f по умолчанию
    school,           -- 0 по умолчанию
    size,             -- 1.0f по умолчанию
    sizeCap,          -- 1.0f по умолчанию
    speed,            -- 1.0f по умолчанию
    castSound,        -- string, звуки
    boltSound
    hitSound,
    areaSound,
    castVFX,          -- сами эффекты, PhysicalObject
    boltVFX,
    hitVFX,
    areaVFX,
    allowEnchanting,  -- дальше идут флаги (true/false)
    allowSpellmaking,
    appliesOnce,
    canCastSelf,
    canCastTarget,
    canCastTouch,
    casterLinked,
    hasContinuousVFX,
    hasNoDuration,
    hasNoMagnitude,
    illegalDaedra,
    isHarmful,
    nonRecastable,
    targetsAttributes,
    targetsSkills,
    unreflectable,
    usesNegativeLighting
    onTick,
    onCollision
    }
Можно надеяться, что появится и функция типа addNewSkill ...

Урок 8: "Как создать / прочитать текстовый файл"
 
--=== Функции Lua для работы с файлами ===--
io.input()    -- задаём файл для чтения
io.output()   -- задаём файл для записи
io.read()     -- читаем строку из файла
io.write()    -- пишем строку в файл
 
Для примера посчитаем количество строк и слов в интересующем нас файле, пусть он называется inp.txt
 
Запускать программу будем на компиляторе lua53.exe (последняя версия).
 
1 Скачаем, если у вас его нет. Скачать можно тут: http://luabinaries.s...t/download.html
 
2. Создаём в одном каталоге с lua53.exe новый текстовый файл с текстом программы, например "prog.lua":
 
io.output('out.txt') -- задаём файл с результатом
io.input('inp.txt')  -- откуда будем читать
 
local str = io.read() -- читаем первую строку из файла
local snum = 0 -- количество строк
local wnum = 0 -- количество слов
 
while str do        -- цикл исполняется пока str не равно nil
  snum = snum + 1   -- увеличиваем счётчик строк на 1
  for s in str:gmatch("[%w'-]+") do -- для каждой найденной в строке цепочки цифро-букв, "'" и "-"
    if s:find("%a") then            -- если цепочка имеет хотя бы одну букву
      wnum = wnum + 1               -- увеличиваем счётчик слов на 1
    end
  end
  str = io.read()   -- читаем следующую строку
end
io.write('Всего строк в файле ',snum,'\n') -- выводим результат в файл
io.write('Bсего слов в файле ',wnum,'\n')  -- \n -- служебный символ перехода на новую строку
print('Всего строк в файле',snum,' всего слов в файле',wnum)
 
Замечание: все файлы должны лежать в одной папке.
 
3. Перетаскиваем мышкой нашу программу "prog.lua" на lua53.exe и отпускаем, рядом появляется наш файл с результатом "res.txt".
 
4. Для повторных запусков можно создать prog.bat файл, и запускать нашу програму нажатием на него
 
lua53 prog.lua
cmd

Про что сделать следующий сложный урок? (не более 2 вариантов в ответе, или свои варианты)
 
1. Как правильно подключить и использовать mcm (Настройка Модов в Главном меню)
2. Как создавать окошки и кнопочки. Внедрим кнопку в окно с диалогом с нпс. По нажатию он будет нам отвечать про погоду и силу ветра.
3. Как заставить нпс`а: упасть на кровать и лежать на ней ничком.
4. Появляется и исчезает бог (Вивек, например), все сбегаются к месту силы.
5. Как работает самый частый пакет ии: AIWander (шататься окрест)
6. Как работает  AIActivate. Как заставить нпс`а выйти на улицу, а Фаргота бегать по кругу.
7. Выделить стат-ку из редактора и посмотреть: сколько и каджитов и орков на Ввардэнфелле и чем они занимаются, какой ср.уровень, скорость, магия.
 
Но сначала простой урок номер 9: "Как создавать окна, меню, кнопки в Морровинд"
 
Самый короткий вариант создать окошко. Назовём его Винни. Короткий плагин из десятка строчек:
 
local winny
 
local function showWinny()
  winny = tes3ui.createMenu -- создается новое меню (просто окно)
  {
  id = tes3ui.registerID('winny'), -- нашему окну выдаётся id, по которому можно затем к нему обращаться
  fixedFrame = true, -- с жёстко фиксированной рамкой
  }
  winny:updateLayout() -- обновить элементы на экране
end
event.register("keyDown", showWinny, {filter = 44}) -- при нажатии на "z" будет появляться Винни
 
Запускаем Главное меню Морровинд. Жмакаем на "z".
 
Появляясь, наше окно не хочет исчезать. Давайте добавим текст в наше окно, а также кнопку "назад":
 
    local winny
    
    local function hideWinny()
      winny:destroy() -- функция для удаления Винни
    end
    
    local function showWinny()
      winny = tes3ui.createMenu
      {
       id = tes3ui.registerID('winny'),
       fixedFrame = true,
      }
      winny:createLabel{ text = 'winny'} -- добавляем текст в окно
      local button = winny:createButton -- добавляем кнопку
      {
       id = tes3ui.registerID('Cancel'),
       text = 'Назад',
      }
      button:register("mouseClick", hideWinny) -- при щелчке по кнопке будет вызываться hideWinny()
      winny:updateLayout()
    end
    event.register("keyDown", showWinny, {filter = 44}) -- z
 
Теперь, если нажать "z" в игре, окно появляется и маячит посреди экрана. Как перейти в режим меню?
 
tes3ui.enterMenuMode(winny) -- попадаем в режим, когда игра на паузе и появляется указатель мыши для управления в меню
 
 Добавим ещё несколько полезных строк:
 
    local winny
    local winny_id -- сохраним id окна отдельно
    
    local function hideWinny()
      winny:destroy()
      if tes3ui.menuMode then tes3ui.leaveMenuMode() end -- выходим из режима меню
    end
    
    local function showWinny()
      if tes3ui.findMenu(winny_id) then return end -- если окно уже есть, не создаём его повторно
      winny_id = tes3ui.registerID('winny')
      winny = tes3ui.createMenu
      {
       id = winny_id,
       fixedFrame = true,
      }
      winny:createLabel { text = 'winny'}
      local button = winny:createButton
      {
       id = tes3ui.registerID('Cancel'),
       text = 'Назад',
      }
      button:register("mouseClick", hideWinny)
      winny:updateLayout()
      tes3ui.enterMenuMode(winny) -- режим меню
    end
    event.register("keyDown", showWinny, {filter = 44}) -- z
 
Готово. Также, в окно можо добавлять блоки (свободное пространство для элементов), а в блоки уже добавлять тексты, изображения и кнопки.
 
      local block = winny:createBlock{}
      block.width = 1000       -- ширина
      block.autoHeight = true  -- высота выбирается автоматически
      block.childAlignX = 0.5  -- горизонт. выравнивание элементов по центру
      block.childAlignY = 0.5  -- верт. выравнивание элементов по центру
      block.flowDirection = 'top_to_bottom' -- разместить элементы сверху вниз
    
      local l = block:createLabel{ ... }   -- добавить запись
      local b = block:createButton{ ... }  -- добавить кнопку
      local img = block:createImage{ ... } -- добавить изображение
      local nif = block:createNif{ ... }   -- добавить модель
 
Теперь вы можете создавать меню в Морровинд под свои нужды.
Полные возможности см. в описании TES3 классов (последняя ссылка в шапке темы)
Урок 10: "Разговоры"
Сегодня мы:
 
1. Создадим модуль, который будет штамповать нам окна
2. Используя этот модуль, сотворим окно для разговора с НИП'ом (Не Игровым Персонажем)
3. Внедрим кнопку в MenuDalogue (в основное окно для диалогов в Морровинде)
4. Добавим функции для ответа: "кто вы?", "который час?", "как погода?"
 
Для понимания этого материала нужно основательно усвоить предыдущий урок.
Пусть у нас есть каталог с модом MyMod. А в нём основной модуль main.lua
Создадим вспомогательный модуль (файл) win.lua. Он нужен нам для того, чтобы потом затем пользоваться им, не повторяя один и тот же код  over9000+ раз.
 
win.lua будет иметь всего одну функцию new(name) - создавать новое окно:
 
    local this = {}
    
    this.new = function(name)      -- функция, создающая новое окно и возвращающая нам его данные
    
    local m = {                    -- данные нашего нового окна
     id = tes3ui.registerID(name),
     ltext = ' Enter: ',
     block = {[1] = ''},
    }
    m.menu = tes3ui.createMenu{id = m.id, fixedFrame = true}
    m.menu.alpha = 1.0             -- нулевая прозрачность
    
    m.show = function()            -- отобразить окно
      m.menu:updateLayout()
      tes3ui.enterMenuMode(m.menu)
    end
    
    m.hide = function()            -- спрятать окно
      if not tes3ui.findMenu(m.id) then return end
      m.menu:destroy()
      if tes3ui.menuMode then tes3ui.leaveMenuMode() end
    end
    
    m.addBlock = function(n, width, ltext)  -- добавить блок с номером n, шириной [и необязательным текстом надписи]
      m.block[n] = m.menu:createBlock{}
      m.block[n].width = width or 360
      m.block[n].autoHeight = true
      m.block[n].childAlignX = 0.5
      m.block[n].childAlignY = 0.5
      m.block[n].flowDirection = 'top_to_bottom'
     
      if ltext then                         -- если указан 3 параметр - текст надписи, то добавляем её
        m.block[n]:createLabel {text = ltext}
      end
    end
    
    m.addBlockButton = function(n, buttext, func)  -- добавить в блок номер n кнопку с текстом buttext
      local button = m.block[n]:createButton
      {
        id = tes3ui.registerID(buttext),
        text = buttext,
      }
      button:register("mouseClick", func)  -- при нажатии на кнопку будет вызываться функция func
    end
    
      return m  -- возвращаем созданную таблицу с данными и функциями для нашего нового окна
    end
    
    return this
 
Добавим этот модуль в наш основной модуль main.lua
 
local win = require("MyMod.win")
 
Теперь мы можем:
 
w = win.new(...),              -- создавать новые окна
 
w.addBlock (...),               -- добавлять в них болки
 
w.addBlockButton (...)      -- конопки
 
w.show (...)                       -- показывать
 
w.hide (...)                        -- и прятать их
 
Что мы и сделаем:
 
    local win = require("Clever Actors Dialogues.win")
    local winny
    
    local function winnyShow()
      local width = 360
    
      winny = win.new('winny')                -- создадим новое окно
      winny.addBlock(1, width, 'Сказать...')  -- добавим блок номер 1
      winny.addBlock(2, width, '----------')  -- добавим блок номер 2
      winny.addBlockButton(2, 'назад', winny.hide) -- добавим в блок номер 2 кнопку назад
      winny.show()                            -- а теперь выведем окно
    end
    event.register("keyDown", winnyShow, {filter = 44}) -- z
 
При нажатии на "z" окно покажется, при нажатии "назад"  - спрячется.
 
 
Теперь создадим функцию, которая внедрит в меню диалога (MenuDialog) новую кнопку "Диалог с  <имя персонажа>"
 
Есть такое событие "menuEnter". Оно возникает, когда на экран выводится любое окно (меню). В данных этого события отправляется id этого нового окна (просто число).
 
 Подробнее о всех события тут:
 
https://mwse.readthedocs.io/en/latest/lua/event.html
 
Это нам и нужно. Зарегистрируем на событие "menuEnter" новую функцию в main.lua:
 
    local md_id                     -- id MenuDialog
    local md_db                     -- это будет наша кнопка "Диалог с npc"
    local md_db_id                  -- id нашей кнопки
    
    local function dlgButton(e)  -- е - это данные события "menuEnter"
    
      md_id = tes3ui.registerID("MenuDialog")
      if e.menu.id ~= md_id then return end -- выходим, если id окна не совпадает с MenuDialog (открыто что-то другое)
      md = tes3ui.findMenu(md_id)           -- иначе, продолжаем. Сохраняем меню MenuDialog как md
    
      md_db_id = tes3ui.registerID("md_db")
      md_db = md:createButton {             -- добавляем нашу кнопку в MenuDialog (появится внизу окна)
        id = md_db_id,
        text = 'Диалог c '..tes3ui.getServiceActor().object.name,
      }
      md_db.widthProportional = 1.0
      md_db:register("mouseClick", winnyShow) -- при нажатии на кнопку будет вызываться наше окно Winny
    end
    
    event.register("menuEnter", dlgButton) -- регистрируем функцию на событие "menuEnter"
 
Можно пробовать запускать. Теперь не хватает вопросов и ответов от нпс.
 
 
При использовании tes3.messageBox(...) и открытом MenuDialog текст выводится прямо в него, а не появляется отдельно.
 
Этим и воспользуемся.
 
НИП будет нам отвечать:
 
    local function selfDialog()  -- говорит своё имя, расу и класс
      local mbl = tes3ui.getServiceActor()
      local str = 'Это я, '..mbl.object.name..', '.. mbl.object.race.name ..', '..mbl.object.class.id
      tes3.messageBox(str)
    end
    
    local function daytimeDialog()  -- говорит сколько часов до конца дня
      local wc = tes3.getCurrentWeather().controller
      local str = tostring(wc.daysRemaining)
      str = str ..' день , час ' .. tostring(wc.hoursRemaining)
      tes3.messageBox(str)
    end
    
    local function weatherDialog()  -- проверяет состояние погоды, озвучивает
      local w = tes3.getCurrentWeather()
      local str = 'Нынче'
      if w.index == 1 then str = str..' ясно'
      else str = str..'... что там, не знаю'
      end
      tes3.messageBox(str)
    end
 
Теперь нужно вызывать эти функции из нашего winny.
 
Дополним получившийся main.lua
 
    local function winnyShow()
      local width = 360
      local chlist = -- лист ответов ("Choiсe list")
      {
       ['Кто вы?'] = selfDialog,
       ['Который час?'] = daytimeDialog,
       ['О погоде'] = weatherDialog,
      }
    
      winny = win.new('winny')
    
      winny.addBlock(1, width, 'Сказать...')
      for chtext,fun in pairs(chlist) do   -- для каждой записи из chlist
          winny.addBlockButton(1, chtext, function()  -- добавить функцию
            winny.hide()                   -- прячущую окно winny
            fun()                          -- и вызывающую соответствующую функцию
          end)
        end
      winny.addBlock(2, width, '----------')
      winny.addBlockButton(2, 'назад', winny.hide)
      winny.show()
    end
    
    local md_id                     -- Menu Dialogue id
    local md_db                     -- Menu Dialogue _ Dialogue Button
    local md_db_id
    
    local win = require("Clever Actors Dialogues.win")
    local winny
    
    local function dlgButton(e)
      --collectgarbage('collect')
      md_id = tes3ui.registerID("MenuDialog")
      if e.menu.id ~= md_id then return end
      md = tes3ui.findMenu(md_id)
    
      md_db_id = tes3ui.registerID("cleveractors:md_db")
      md_db = md:createButton {
        id = md_db_id,
        text = 'Диалог c '..tes3ui.getServiceActor().object.name,
      }
      md_db.widthProportional = 1.0
      md_db:register("mouseClick", winnyShow)
    end
    
    local function onLoad()
      event.register("menuEnter", dlgButton)
    end
    event.register("loaded", onLoad)
 
После добавления нашей кнопки в меню диалога, его нужно обновить (updateLayout). Иначе наша кнопка будет появляться не сразу, а только после первого ответа нип`а на любую из тем, или любого др. обновляющего окно события.
 
 
Если есть идеи, как улучшить ответы нпс, предлагайте. Или, ещё лучше, дополняйте плагин самостоятельно. Публикую opensource :)
 
Последняя версия планина "Morrowind-Dialogues" https://gitlab.com/Scoommich/morrowind-dialogues