Skip to content

Базовое руководство TL Pro для моддеров

RazzSG edited this page Nov 11, 2021 · 6 revisions

Оглавление

  1. Программа
  2. Структура мода
  3. Дамп
  4. Код
    4.1. NativeClass
    4.2. NativeObject
    4.3. NativeMethod
    4.4. NativeArray
    4.5. require

Выбор программы

Первое, что вам нужно сделать, это выбрать программу, в которой вы будете писать код. По сути, есть два варианта: текстовый редактор или IDE.

Текстовый редактор

IDE

⬆ вернуться наверх

Структура мода

Основной код мода пишется в файле с названием main и расширением js - main.js

⬆ вернуться наверх

Дамп и код игры

Все классы, функции и поля Террарии находятся в дампе. Без дампа вам будет сложно создавать моды. Если вы собираетесь делать моды, обязательно качайте дамп.

Код игры вы можете скачать тут или получить самостоятельно через исполняемый файл игры с помощью любого c# декомпилятора.

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

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 нужен для работы с нативными объектами (экземплярами классов), у него нет конструктора. Поля NativeObject меняются в зависимости от экземпляра, которые в нём хранятся. Поля могут являться как обычными типами js, так и NativeArray, NativeObject или NativeMethod.

⬆ вернуться наверх

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

NativeArray используется для работы с нативными массивами, у него нет конструктора и работает так же, как обычный массив в js. Имеет параметр length в котором содержится информация о длине массива. Его может вернуть как функция, так и поле полученное из NativeClass или NativeObject.

⬆ вернуться наверх

require

По мере роста вашего мода, вы обычно хотите разделить его на множество файлов, так называемых «модулей». Модуль обычно содержит класс или библиотеку с функциями. Модуль – это просто файл. Один скрипт – это один модуль.

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; // Как быстро стреляет снаряд
	}
});

⬆ вернуться наверх