-
Notifications
You must be signed in to change notification settings - Fork 3
Базовое руководство TL Pro для моддеров
- Программа
- Структура мода
- Дамп
-
Код
4.1. NativeClass
4.2. NativeObject
4.3. NativeMethod
4.4. NativeArray
4.5. require
Первое, что вам нужно сделать, это выбрать программу, в которой вы будете писать код. По сути, есть два варианта: текстовый редактор или IDE.
- Notepad++
- Sublime Text
- Atom
- Visual Studio Code
- Если вы собираетесь писать код на телефоне, хорошим выбором будет - Code Editor
- Visual Studio
- Webstorm (Бесплатная версия 30 дней)
Основной код мода пишется в файле с названием main и расширением js - main.js
Все классы, функции и поля Террарии находятся в дампе. Без дампа вам будет сложно создавать моды. Если вы собираетесь делать моды, обязательно качайте дамп.
Код игры вы можете скачать тут или получить самостоятельно через исполняемый файл игры с помощью любого c# декомпилятора.
Важное примечание - кодовая база игры достаточно большая и декомпиляция некоторых классов/функций потребляет достаточное количество оперативной памяти.
Моды пишутся на языке JavaScript, в интернете можно найти больше количество туториалов по его основам, например вот. Это руководство будет отчасти поверхностным, я не буду сильно вдаваться в подробности. TL Pro позволяет получать классы Террарии, вызывать/хукать функции и получать/менять значения переменных.
Чтобы начать работать с классом его нужно получить, это делается через объект NativeClass
.
const Player = new NativeClass("Terraria", "Player");
Этот код запишет в переменную Player объект класса Terraria.Player
Первый аргумент это Namespace
класса, т.е. путь к классу. Второй аргумент это Class
- имя класса. Всю эту информацию можно найти в дампе или в исходном коде игры.
После того как мы получили класс, мы можем вызывать его статичные/публичные/приватные функции и получать\редактировать статичные/публичные/приватные поля. Статические функции и поля обозначаются модификатором доступа static
. Публичные функции и поля обозначаются модификатором доступа public
. Приватные функции и поля обозначаются модификатором доступа private
.
Как работать с полями:
Player.defaultItemGrabRange = 10; // Устанавливаем полю defaultItemGrabRange значение 10
let grabRange = Player.defaultItemGrabRange; // Записываем в переменную grabRange значение из поля defaultItemGrabRange
Как работать с функциями:
Для начала нужно получить саму функцию, есть два варианта как это сделать:
const GetClosestRollLuck = Player["float GetClosestRollLuck(int x, int y, int range)"]; // Первый вариант
const GetClosestRollLuck = Player.GetClosestRollLuck; // Второй вариант
После получения функции мы можем её вызвать:
let result = GetClosestRollLuck(434, 256, 10);
После вызова, результат выполнения функции будет записан в переменную result
.
Рекомендуется всегда использовать первый вариант, так как могут быть функции с одинаковым названием, но разными аргументами (перегрузка функции). Пример такой функции.
const SetDefaults = Item['void SetDefaults(int Type)'];
const SetDefaults = Item["void SetDefaults(int Type, bool noMatCheck)"];
const SetDefaults = Item['void SetDefaults(int Type)']; // Хорошо
const SetDefaults = Item.SetDefaults; // Плохо, такой вариант может привести к сбою игры и заставить игру перестать запускаться
Второй способ можно использовать если вы уверены в том, что делаете.
Важное примечание - если в функции указаны default
значения, то следует их удалить.
const Dust = new NativeClass('Terraria', 'Dust');
const NewDust = Dust['int NewDust(Vector2 Position, int Width, int Height, int Type, float SpeedX = 0f, float SpeedY = 0f, int Alpha = 0, Color newColor = default(Color), float Scale = 1f)']; // Плохо
const NewDust = Dust['int NewDust(Vector2 Position, int Width, int Height, int Type, float SpeedX, float SpeedY, int Alpha, Color newColor, float Scale)']; // Хорошо
NativeClass нужен для работы с нативными классами и у него даже есть конструктор - new NativeClass('Namespace', 'Class')
. Так же, как и у NativeObject, поля зависят от сохранённого в нём класса, эти поля могут быть обычными типами js или же NativeArray, NativeMethod, NativeObject или NativeClass.
Стоит упомянуть, что NativeClass не хранит экземпляр объекта, все его поля статические.
У NativeClass есть функция - new()
.
Она не принимает аргументов и служит для инициализации нового экземпляра класса NativeObject. Стоит помнить, что после вызова new()
нужно вызывать конструктор - .ctor()
, чтобы до конца инициализировать объект.
const Vector2 = new NativeClass('Microsoft.Xna.Framework', 'Vector2'); // получаем экземпляр класса Vector2
const newVector = Vector2.new(); //Создаем новый объект Vector2
newVector['void .ctor(float x, float y)'](5.0, 10.0); //Инициализируем наш новый объект и указываем значения
Этот js код равен коду написанному на c#
Vector2 newVector = new Vector2(5f, 10f);
NativeObject нужен для работы с нативными объектами (экземплярами классов), у него нет конструктора. Поля NativeObject меняются в зависимости от экземпляра, которые в нём хранятся. Поля могут являться как обычными типами js, так и NativeArray, NativeObject или NativeMethod.
NativeMethod нужен для работы с нативными функциями, у него нет конструктора и его можно получить из NativeClass или NativeObject. NativeMethod можно вызывать точно так же, как и обычную функцию в js. Так же у NativeMethod есть функция hook()
, аргументом в которую передается callback функция. Хуки нужны для изменения хода выполнения нативных функций. Первым аргументом callback идёт original — это NativeMethod, который нужен для вызова оригинальной версии функции. Если мы хукаем не статическую функцию, то вторым аргументом указываем экземпляр класса (self
).
Recipe.SetupRecipes.hook((original) => { // хукаем статическую функцию SetupRecipes()
original();
});
Player.OpenFishingCrate.hook((original, self, crateItemID) => { // хукаем не статичесикую функцию OpenFishingCrate()
original(self, crateItemID);
});
Хук - это функция, которая позволяет изменять поведение других функций, добавлять в них свой код, изменять возвращаемое значение и так далее. Хукать можно любые функции.
Пример мода, который после открытия инвентаря будет телепортировать игрока в случайное место на карте
const Player = new NativeClass("Terraria", "Player");
Player.ToggleInv.hook((original, self) => { // Хукаем функцию ToggleInv, первым аргументом передается нативная функция (original), вторым передается instance(self)
original(self); // Вызываем нативную функцию
self.TeleportationPotion(); // Вызываем instance функцию TeleportationPotion(), которая находится в классе Player
});
NativeArray используется для работы с нативными массивами, у него нет конструктора и работает так же, как обычный массив в js. Имеет параметр length в котором содержится информация о длине массива. Его может вернуть как функция, так и поле полученное из NativeClass или NativeObject.
По мере роста вашего мода, вы обычно хотите разделить его на множество файлов, так называемых «модулей». Модуль обычно содержит класс или библиотеку с функциями. Модуль – это просто файл. Один скрипт – это один модуль.
require - функция, которая работает точно так же, как в node.js. Она нужна для импорта объектов из других файлов.
Создаём новый модуль, например с именем Utilities.js. Этот модуль хранит одну функцию, которая уничтожает все вражеские снаряды.
function KillHostileProjectile() {
for (let i = 0; i < Main.maxProjectiles; i++) {
const proj = Main.projectile[i];
if (proj.active && proj.hostile && !proj.friendly && proj.damage > 0) {
proj.Kill();
}
}
}
exports.KillHostileProjectile = KillHostileProjectile; // отмечаем функцию, которая будет доступна вне текущего модуля.
Добавляем require в main.js
const Utils = require('./Utilities.js'); // указываем модуль, который импортирует абсолютно все экспортируемые функции и переменные в этом модуле
Utils.KillHostileProjectile(); // вызываем функцию из импортированого модуля
const KillProj = require('./Utilities.js').KillHostileProjectile; // указываем модуль и конкретную функцию, которую импортируем
Пример простого мода, изменяющий скорострельность и скорость снарядов Мини-акулы
const Item = new NativeClass('Terraria', 'Item');
const ItemID = new NativeClass('Terraria.ID', 'ItemID');
const SetDefaults = Item["void SetDefaults(int Type, bool noMatCheck)"];
SetDefaults.hook((original, self, type, noMatCheck) => {
original(self, type, noMatCheck);
if (type == ItemID.Minishark) { // Если предмет это Мини-акула
self.useTime = 4; // Сколько времени нужно для использования предмета
self.useAnimation = 4; // Как долго длится анимация предмета
self.shootSpeed = 10.0; // Как быстро стреляет снаряд
}
});