MetrostroiExtensionsLib добавляет библиотеку для инжекта в поезда аддона Metrostroi.
Многие вещи, о которых забывают создатели инжектов MetrostroiExtensionsLib учитывает за вас - сохраните себе нервы!
Быстрое начало (мини-туториал)¶
Давайте попробуем сделать достаточно простой рецепт:
- Добавим новый проп в салон
- Сделаем новое выпадающее меню в спавнере, которое позволит выбирать модель для пропа в салоне
А потом:
- Добавим кнопку, которая будет печатать сообщение в чат
- Заставим эту кнопку менять цвет нашего пропа в салоне
Дисклеймер¶
Данное руководство подразумевает, что вы имеете базовые знания программирования и языка Lua. Также данное руководство предполагает, что вы ранее имели опыт разработки под Garry's Mod GLUA и понимаете его базовые сущности (по типу проп, энтити и т.д.).
Основные термины¶
- Инжект - изменение существующего состава, путем "вставки" нового контента уже после его создания без изменения его исходного кода
- Рецепт - код, который определяет, как и каким образом нужно изменить что либо в составе
- ClientProp (или клиентский проп) - клиентская модель, тоесть моделька которая отрисовывается на клиенте
- ButtonMap (или баттнмапа) - специальная карта, определяющая местоположение и функции кнопок
- MEL - сокращенное от MetrostroiExtensionsLib - используется как сокращенное название в коде и в документации.
Создание нового рецепта¶
Начало¶
Для создания рецепта нам необходимо создать новый аддон. Рекомендуем поднять свой выделенный сервер (srcds), с установленным Metrostroi, MetrostroiExtensionsLib и Turbostroi. (см. установка srcds Garry's Mod)
Создание локального аддона¶
Если вы создали выделенный сервер, то локальный аддон необходимо создавать на нем - Garry's Mod автоматически скачает ваши рецепты (из вашего аддона) к вам на клиент
Давайте перейдем в папку с аддонами: <что-то до этого>\steamapps\common\GarrysMod\garrysmod\addons
(в случае с выделенным сервером - <что-то до этого>\garrysmod\addons
)
Внутри данной папки будут находится все локальные аддоны. Создадим наш новый локальный аддон - для этого просто создадим новую папку с любым именем.
Так как мы работаем с lua кодом, то внутри локального аддона необходимо будет создать папку lua
- по итогу у вас должен получится примерно вот такой путь: <что-то до этого>\garrysmod\addons\<имя аддона>\lua
Создание и инициализация рецепта MEL¶
MetrostroiExtensionsLib автоматически загрузит ваши рецепты, если они будут лежать в папке recipies.
Внутри папки recipies может быть сколько угодно вложенных папок. К примеру, для нашего удобства, если мы будем изменять пульт 81-717, можно создать следующую структуру папок:
<что-то до этого>\garrysmod\addons\<имя аддона>\lua\recipies\717\pult\
Tip
Есть особая "магическая" папка - disabled. Она может находится в любой другой папке, но все рецепты внутри неё не будут загружены.
Создадим наш первый рецепт - в папке recepies
(или в другой папке внутри recepies
) создадим файл с именем hello_world.lua
. Внутрь него вставим следующий код:
Lua | |
---|---|
1 2 |
|
Хм, не густо. Но именно так выглядит самый простой рецепт, который можно только себе представить. Да, он (почти) ничего не делает. Давайте разберем каждую строчку по отдельности:
MEL.DefineRecipe("hello_world", "717")
- данная строчка инициализирует наш рецепт. Она дает MEL понять, что это рецепт с именемhello_world
и его необходимо инжектить во все 717 типа МВМ и ЛВЗ (в метрострое МВМ - это МСК, ЛВЗ - это СПБ). Помимо717
есть множество других способов задать вагоны, в которые необходимо инжектится. См. MEL.DefineRecipeRECIPE.Description = "This recipe adds new prop into interior and simple example button"
- данная строчка добавляет описание данному рецепту. Это описание будет полезно как для вас, так и для администраторов серверов и других разработчиков. Этот описание будет отображаться в MEL ConVars, с помощью которых можно отключить каждый рецепт по отдельности.
Но ведь наш рецепт ничего не делает! Давайте вдохнем в него жизни. Для начала попробуем сделать самую банальную (как по мне) вещь - добавим статичную модель в наш состав.
Для этого определим функцию RECIPE:Inject(ent, entclass)
- данная функция будет выполнена на каждом вагоне, соотвествующему типу, определенному в MEL.DefineRecipe (заспавненные энтити тоже считаются!).
Эта функция получает на вход как само энтити, так и класс (название) энтити
Получим следующий код:
Lua | |
---|---|
1 2 3 4 5 6 |
|
Добавление статичного ClientProp¶
И мы получили... рецепт, который ничего не делает ;(
Давайте наконец вдохнем в него жизнь - воспользуемся функцией MEL.NewClientProp
, передав в неё энтити, название пропа и описание пропа, чтобы создать новый клиентпроп:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
И... (после спавна 81-717) опять ничего!
На самом деле - данный рецепт полностью валидный и рабочий. Но нам нужно вызвать реинжект - заставить MEL заново внести все наши изменения в составы.
Для этого включим Debug-режим командой metrostroi_ext_debug 1
(работает только на сервере), а после этого вызовем консольную команду metrostroi_ext_reload
(работает и на клиенте, и на сервере).
Tip
Вызов данной команды можно автоматизировать на каждое сохрание файла - см. автоматизация metrostroi_ext_reload
Теперь внутри всех головных вагонах 81-717, в самом центре салона появился арбуз. Хорошо, но что если мы хотим видеть тут не только арбуз?
Добавление поля в спавнер¶
Попробуем поработать со спавнером. MEL позволяет удобно и быстро работать со спавнером Metrostroi, учитывая все сложные моменты за вас. Давайте же создадим новое выпадающее меню с выбором модельки:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Ого, что-то новое. Давайте разбираться:
RECIPE:InjectSpawner
- специальная функция, внутри которой нам нужно инжектится в спавнер. На вход получает класс энтити, вызывается ровно один раз на каждый класс энтитиMEL.AddSpawnerField
- функция для инжекта в спавнер. На вход получает класс энтити, в который необходимо инжектится и описание поля (см. Формат полей спавнера Metrostroi).
Внимательный читатель заметит, что в MEL.AddSpawnerField
последним аргументом мы передаем true
. Данный аргумент является флагом того, что первый аргумент в данном списке - рандом.
Если до этого вы разрабатывали инжекты в Metrostroi, то вы возможно знаете, какая головная боль добавить рандом в спавнере. MEL решает данную проблему за вас. См. подробнее AutoRandom.
Если теперь мы вызовем metrostroi_ext_reload
, то в спавнере действительно появилось выпадающее меню. Но в данный есть пара моментов:
- Оно не работает
- У него странное название и вообще всё на английском
Давайте для начала разберемся с переводами
Переводы в Metrostroi¶
В Metrostroi можно добавлять переводы для разных языков. Metrostroi Extensions позволяет легко и просто добавлять их для кастомных полей в спавнере. Создадим новый файл в папке lua/metrostroi_data/languages
и назовем его en_test.lua
- это будет файл английских переводов для нашего тестового аддона. Внутри напишем следующее:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
Надеюсь, формат примерно очевиден :)
Создадим такой же файл, только для российских переводов - ru_test.lua
, он будет очень похож на файл с английскими переводами:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
Вызовем metrostroi_language_reload
в консоли, переоткроем спавнер... И о чудо, теперь у нас появились переводы!
Вдохнем жизнь в клиентпроп...¶
Давайте разберемся с самым основным - заставим его работать:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
metrostroi_ext_reload
) У пропа появилось интересное поле modelcallback
- в данном пропе хранится callback, который будет вызываться для того, чтобы выяснить, какую-же модельку нам надо заспавнить.
Внутри данного callback мы просто получаем согласно индексу из нашего спавнера модель (спавнер для выпадающих менюшек передает нам индексы - первый элемент это 1, второй это 2 и т.д.)
Но... Оно не работает! Точнее работает, но для того, чтобы получить новую модель, необходимо полностью переспавнить состав. Не порядок - давайте думать, как это исправить:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Погодите-ка... странно как-то, так ведь? Почему мы передаем строчку в MEL.NewClientProp
, и это заставляет данный код работать правильно?
Давайте разбираться:
MEL добавляет ещё один удобный функционал, связанный со спавнером - Автоперезагрузку пропов. Данный функционал будет пересоздавать ваш клиентпроп при изменении определенного поля в спавнере (и не только поля - на самом деле при изменении любой сетевой переменной вагона).
Передав четвертым аргументом имя сетевой переменной (которая равна имени поля в спавнере) мы пометили данный клиентпроп для пересоздания при изменении этого поля в спавнере.
Мы также можем пометить данный клиентпроп отдельно вне функции MEL.NewClientProp
- с помощью фукнкии MEL.MarkClientPropForReload
Пробуем добавить кнопку¶
Давайте теперь добавим простую кнопку, которая будет приветствовать мир в консоли нашего сервера
Для этого создадим новую баттнмапу - карту кнопок, используя функцию MEL.NewButtonMap
:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
(включите отладочную информацию в Q > Утилиты > Metrostroi > Клиент (дополнительно)). О чудо, в середине нашего салона появилась "кнопка", правда без модельки и... она ничего не делает :(
Вдохнем в неё жизнь!
Инжект в стандартные функции¶
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
MEL.InjectIntoServerFunction
позволяет нам заинжектится в код любой серверной функции. В данном примере мы инжектимся в код функции OnButtonPress
- данная функция вызывается на каждое нажатие кнопки внутри вагона - в неё передается имя кнопки и игрок, нажавший на неё.
Так как нам интересна только наша добавленная кнопка с именем GreetWorld
, то добавим if. Внутри этого if напишем игроку в чат Hello world
.
Внимательный читатель спросит - а что делает return MEL.Return
? Нужно ли его писать в каждом инжекте в функцию?
MEL.Return
позволяет сделать return из исходной функции. Тоесть, если вызвать MEL.Return
, то выполнение функции вовсе прекратится. Но если не вызывать MEL.Return
, оставшийся код функции (как исходной, так и код инжектов в эту функцию других рецептов) будет продолжать выполнятся.
В данном случае мы используем MEL.Return
из-за того, что мы уже нашли нужную нам кнопку - нет смысла искать ещё кнопки, ведь мы их не найдем. Но также MEL.Return
можно использовать для возврата какого-то значения из исходной функции. Для этого необходимо передать сначала возвращаемые аргументы, а последним значением - MEL.Return
К примеру, в Metrostroi есть функция GetDriverName()
- она возвращает имя текущего машиниста. Представим, что мы хотим немного изменить её поведение - мы будем всегда возвращать слово <REDACTED>
- на нашем сервере это секретная информация.
Lua | |
---|---|
1 2 3 |
|
Обратите внимание на то, что мы инжектимся с приоритетом -1. Таким образом, мы выполняем наш код до начала выполнения стандартного кода. А из-за того, что мы возвращаем MEL.Return, мы даже не доходим до стандартного кода.
Также инжектится можно не только в серверные функции - а ещё в клиентские, shared и даже функции систем! См. инжект в функции
Модель у кнопки¶
Сделаем уже наконец нашу кнопку - кнопкой.
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
Теперь (предварительно залетев в модельку бочки/арбуза/конуса) у кнопки есть модель, но нет анимации. Давайте же исправим это!
О анимациях и системах в Metrostroi¶
Небольшой ликбез:
- Анимация - это какое-то изменение положения чего-либо в игре
- В Metrostroi мы можем анимировать модели, у которых уже присутствует костная анимация (подробнее по ссылке)
- В Metrostroi из коробки есть функция Animate
- Для кнопок на баттонмапах есть встроенный механизм анимации
Для того, чтобы анимация у нас была синхронизирована на всех клиентах, нам нужна сетевая переменная. Конечно, мы можем добавить сетевую переменную в Initialize на сервере, в Think её передавать всем клиентам, отдельно добавить OnButtonPress и реализовать логику нашей кнопки, но это слишком объемно (и тупо)
В Metrostroi для этого обычно используются системы - переиспользуемые "строительные блоки" с какой-либо логикой. И для нас уже написали систему кнопок и переключателей - можно даже посмотреть, как она реализована
Причем, система может загрузить другую систему. К примеру, существует система 81_717_Panel - система, в которой только лишь загружаются другие системы кнопок.
Но системы могут быть загружены во внутрь Turbostroi.
Кто такой этот ваш турбострой?¶
Как мы знаем, большинство процессоров в современном мире имеет несколько ядер. В современном процессоре их может быть 12, 24, 36, и даже намного больше!
Но Source, на котором работает Garry's Mod (да и сам движок Lua Garry's mod), из-за того, что был написан в 2000-ых годах (когда даже четыре ядра было редкостью) не умеет разделять нагрузку на несколько ядер. Именно поэтому нам необходим Turbostroi - это отдельная программа, которая по сути - выполняет тот же Lua код, просто разделяя его на разные ядра.
Но из-за того, что Turbostroi - это отдельная программа, мы не можем также гибко коммуницировать с ней, как внутри процесса Garry's mod. Поэтому и изменять системы, подгруженные во внутрь Turbostroi с помощью Metrostroi Extensions - невозможно (пока что :))
А причем тут кнопки?¶
К сожалению, 81_717_Panel подгруженна в Turbostroi... В идеальном мире мы бы смогли заинжектится с помощью MEL.InjectIntoSystemFunction в 81_717_Panel и добавить нашу новую кнопку. Но нам придется делать это в другом месте...
И сделаем мы это в функции InitializeSystems! Именно эта функция при спавне состава загружает все базовые системы, и, кстати, 81_717_Panel в том же числе. Просто возьмем и...
Lua | |
---|---|
1 2 3 |
|
- Заинжектились в InitializeSystems
- Загрузили новую систему Relay в переменную GreetWorld с параметром "Switch" (тем самым указав, что это простой "тупой" переключатель)
- Также указали, что для него стоит добавить логику звуков
Также нам надо чуть-чуть модифицировать нашу ButtonMap с нашей кнопкой. С системой кнопки в Metrostroi мы можем взаимодействовать по разному - мы можем использовать её как переключатель (тумблер), можем как кнопку, а можем вообще как кнопку с фиксацией, или как трехпозиционный переключатель, или как... Короче, вариантов использования - масса :)
Но для того, чтобы указать, что мы будем делать с нашей кнопкой, мы должны менять имя (ID) самой кнопки.
Diff | |
---|---|
1 2 |
|
Set
мы сказали, что данная кнопка на нашей баттнмапе будет систему-кнопку с именем GreetWorld задавать в ровно то значение, в котором сейчас кнопка на баттнмапе. Простым языком, если кнопку в игре мы зажмем, то кнопка-система тоже будет зажата, а как только мы её отпустим - она тоже отпустится. Ещё мы можем переключать её (Toggle
), зажимать или отпускать (Open
, Close
), блокировать (Block
) и делать ещё много крутых вещей.
Давайте же применим это в нашем рецепте:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
Ииии... Кнопка все ещё не анимирована! Почему так? Все просто: мы не добавили переменную нашей кнопки в "таблицу синхронизации" - эта таблица синхронизации отправляет новые значения из систем на клиент при каждом из изменении.
В Metrostroi Extensions для этого есть удобная функция - MEL.AddToSyncTable
Lua | |
---|---|
1 |
|
Полный рецепт:
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
Ура! У нас есть кнопка, по нажатии на которую проигрывается анимация, правильно работает её логика, а ещё целый проп с выпадайкой в спавнере и рандомом.
Но давайте же попробуем заставить нашу кнопку как нибудь взаимодействовать с нашим пропом. Допустим, сделаем так, что она будет случайно менять цвет нашего пропа.
Для этого мы:
- Заинжектимся в клиентский Think
- Получим наш проп, проверим, что в данный момент он существует в мире (игрок находится достаточно близко к нему, чтобы игра его отрисовывала)
- При изменении значения сетевой переменной GreetWorld (при нажатии на кнопку) - зададим пропу случайный цвет
Lua | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
Надеюсь, к этому моменту вы уже понимаете, что делает данный код. Если нет - есть место для роста :)
На данном примере данное руководство подходит к концу. Но это только вершина айсберга из того, что умеет делать Metrostroi Extensions.
К примеру, ещё можно:
- изменять существующую кнопку или даже целую баттнмапу (MEL.ModifyButtonMap, MEL.MoveButtonMap или MEL.MoveButtonMapButton)
- добавлять новые кнопки на существующие баттнмапы (MEL.NewButtonMapButton)
- гибко работать со спавнером
- перезаписывать стандартные значения, которые возвращают Animate, ShowHide и даже HidePanel и многое-многое другое...