Эталонный мод RustexModding
Собран так, чтобы его читал человек, который пишет первый мод. Показывает apiVersion, конфиги из rsxm-lib, события, everyTicks, типизированные обёртки Rsx.Player и Rsx.World, onError и onUnload. Ни одного обфусцированного имени: мод спрашивает логическими именами, бриф переводит их в имена текущей сборки. Ни рефлексии, ни чтения файлов игры - всё это либо не нужно, либо делает бриф. Компилируется против rsx-api.jar и rsxm-lib.jar, но не содержит ни одного: библиотека лежит в libs\ и подмешивается в системный class path при старте. Проверяется просто - если в jar мода нет net/rsxm/, он собран правильно.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# сборка
|
||||
build/
|
||||
dist/
|
||||
*.class
|
||||
|
||||
# зависимости: их не коммитим, они собираются из модлоадера и rsxm-lib
|
||||
libs/
|
||||
@@ -0,0 +1,48 @@
|
||||
# rsxm-example
|
||||
|
||||
Эталонный мод RustexModding. Собери его, положи в `%LOCALAPPDATA%\rsx-mod\mods`
|
||||
и посмотри лог — он рассчитан на то, чтобы его читал человек, который пишет
|
||||
первый мод.
|
||||
|
||||
build.cmd
|
||||
|
||||
Нужен только JDK. Скрипт собирает `build\mods\rsxm-example.jar` и кладёт его в
|
||||
папку модов; если путь другой, переопредели:
|
||||
|
||||
set MODS_DIR=D:\path\to\mods && build.cmd
|
||||
|
||||
## Что показывает мод
|
||||
|
||||
* `apiVersion()` — версия API, под которую написан мод;
|
||||
* `Config` из rsxm-lib — настройки в `config\example.json`, значения по
|
||||
умолчанию задаёт сам мод;
|
||||
* события `playerJoin`, `playerLeave`, `worldChange`;
|
||||
* `everyTicks` — периодичность по игровым тикам;
|
||||
* типизированные обёртки `Rsx.Player` и `Rsx.World`;
|
||||
* `onError` — что мод делает, когда задача упала;
|
||||
* `onUnload` — что мод делает на выходе.
|
||||
|
||||
## Чего здесь нет - и почему
|
||||
|
||||
Ни одного обфусцированного имени: ни класса, ни поля, ни метода. Мод
|
||||
спрашивает логическими именами, бриф переводит их в имена текущей сборки.
|
||||
Поэтому мод собирается один раз и переживает очередную реобфускацию игры.
|
||||
|
||||
Нет рефлексии, нет чтения файлов игры, нет работы с class loader-ами. Всё
|
||||
это либо не нужно, либо делает бриф: `Rsx.get()`, `Rsx.call()` и
|
||||
`Rsx.field()` существуют для редких случаев, когда профиль ещё не знает
|
||||
член, и пользоваться ими стоит в последнюю очередь.
|
||||
|
||||
## Зависимости
|
||||
|
||||
Компилируется против двух jar, но не содержит ни одного из них:
|
||||
|
||||
* `libs\rsx-api.jar` - API, идёт в составе модлоадера;
|
||||
* `libs\rsxm-lib.jar` - общая библиотека, лежит в `%LOCALAPPDATA%\rsx-mod\libs`
|
||||
и подмешивается в системный class path при старте игры.
|
||||
|
||||
Загрузчик игры даёт каждому моду свой class loader, поэтому моды не видят друг
|
||||
друга напрямую - но у всех этих загрузчиков родитель системный, а значит всё,
|
||||
что в нём, доступно каждому. Поэтому библиотека одна на всех, и вшивать её в
|
||||
мод не нужно. Проверяется это просто: если в jar мода нет `net/rsxm/`, значит
|
||||
он собрался правильно.
|
||||
@@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
rem Builds the mod and drops the jar into the client's mods folder.
|
||||
rem Only a JDK is needed: no Gradle, no network, nothing but javac and jar.
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
if not exist "build\classes" mkdir "build\classes"
|
||||
if not exist "build\mods" mkdir "build\mods"
|
||||
|
||||
set ENTRY=com.example.Diagnostics
|
||||
|
||||
rem where the modloader looks for mods; override with set MODS_DIR=...
|
||||
if "%MODS_DIR%"=="" set MODS_DIR=%LOCALAPPDATA%\rsx-mod\mods
|
||||
|
||||
echo [1/3] compiling...
|
||||
dir /s /b src\main\java\*.java > build\sources.txt
|
||||
javac -encoding UTF-8 --release 8 -nowarn -cp "libs\rsx-api.jar;libs\rsxm-lib.jar" -d build\classes @build\sources.txt
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo ?????? ?? ???????.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [2/3] packaging rsxm-example.jar...
|
||||
if exist "build\mods\rsxm-example.jar" del "build\mods\rsxm-example.jar"
|
||||
jar cfm "build\mods\rsxm-example.jar" manifest.txt -C build/classes .
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
echo [3/3] installing...
|
||||
if not exist "%MODS_DIR%" (
|
||||
echo.
|
||||
echo ????? ????? ?? ???????: %MODS_DIR%
|
||||
echo ???????? build\mods\rsxm-example.jar ? ??? ???.
|
||||
exit /b 0
|
||||
)
|
||||
copy /y "build\mods\rsxm-example.jar" "%MODS_DIR%\" >nul
|
||||
echo.
|
||||
echo ??????: build\mods\rsxm-example.jar ?????????? ? %MODS_DIR%
|
||||
@@ -0,0 +1,4 @@
|
||||
Manifest-Version: 1.0
|
||||
Rsx-Mod-Entrypoint: com.example.ExampleMod
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.example;
|
||||
|
||||
import net.rustexremake.api.Rsx;
|
||||
import net.rustexremake.api.RsxMod;
|
||||
import net.rsxm.lib.Rsxm;
|
||||
import net.rsxm.lib.config.Config;
|
||||
import net.rsxm.lib.config.Log;
|
||||
|
||||
/**
|
||||
* Эталонный мод RustexModding.
|
||||
*
|
||||
* Здесь нет ни одного обфусцированного имени - ни класса, ни поля, ни метода.
|
||||
* Мод спрашивает логическими именами, а бриф переводит их в имена той сборки,
|
||||
* которая стоит у игрока. Поэтому мод собирается один раз и продолжает
|
||||
* работать после того, как разработчик игры переименует всё заново.
|
||||
*
|
||||
* Чего здесь тоже нет: рефлексии, обфусцированных имён, class loader-ов,
|
||||
* чтения файлов игры. Всё, что нужно, лежит в API и в rsxm-lib.
|
||||
*
|
||||
* Порядок вызовов:
|
||||
* onLoad - один раз, мод загрузился, можно регистрировать события;
|
||||
* onSecond - на игровом потоке, раз в игровую секунду;
|
||||
* onError - если задача мода упала;
|
||||
* onUnload - игра выключается.
|
||||
*
|
||||
* Всё, что здесь выполняется, уже выполняется на игровом потоке: трогать
|
||||
* состояние игры из своего потока нельзя.
|
||||
*/
|
||||
public class ExampleMod implements RsxMod {
|
||||
|
||||
private static final String MOD = "example";
|
||||
|
||||
private Config config;
|
||||
private int ticks;
|
||||
|
||||
@Override
|
||||
public int apiVersion() {
|
||||
// Версия API, под которую написан мод. Бриф предупредит в логе, если
|
||||
// версии разойдутся, - это лучше, чем мод молча перестанет работать.
|
||||
return Rsx.API_VERSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
// Настройки живут в config\example.json. Значения по умолчанию задаёт
|
||||
// сам мод: если файла нет, он создастся с ними при первом save().
|
||||
config = Config.of(MOD)
|
||||
.set("statusEverySeconds", 5)
|
||||
.set("greetOnJoin", true);
|
||||
config.save();
|
||||
int every = clamp(config.integer("statusEverySeconds", 5), 1, 3600);
|
||||
|
||||
Log.info(MOD, "загружен");
|
||||
Log.info(MOD, "сборка игры: " + Rsx.buildId());
|
||||
Log.info(MOD, "API: " + Rsx.apiVersion() + ", библиотека: " + Rsxm.describe());
|
||||
Log.info(MOD, "настройки: " + config.file().getName()
|
||||
+ ", отчёт раз в " + every + " с");
|
||||
Log.info(MOD, "Minecraft это " + Rsx.describe("net.minecraft.client.Minecraft")
|
||||
+ ", игрок это " + Rsx.describe("net.minecraft.client.entity.EntityPlayerSP"));
|
||||
|
||||
// События вместо опроса: срабатывают, когда что-то действительно
|
||||
// изменилось, а не каждый тик.
|
||||
if (config.bool("greetOnJoin", true)) {
|
||||
Rsx.on("playerJoin", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.info(MOD, "игрок вошёл: " + Rsx.player().getName());
|
||||
}
|
||||
});
|
||||
Rsx.on("playerLeave", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.info(MOD, "игрок вышел");
|
||||
}
|
||||
});
|
||||
}
|
||||
Rsx.on("worldChange", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.info(MOD, "сменился мир, время: " + Rsx.world().getTime());
|
||||
}
|
||||
});
|
||||
|
||||
// Периодичность по тикам, а не по миллисекундам: тики не плывут при
|
||||
// лагах и не срабатывают дважды после фриза.
|
||||
final int period = every;
|
||||
Rsx.everyTicks(20 * period, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onSecond(period);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Раз в игровую секунду, на игровом потоке. */
|
||||
private void onSecond(int period) {
|
||||
ticks++;
|
||||
if (ticks % period != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rsx.Player player = Rsx.player();
|
||||
if (!player.isPresent()) {
|
||||
Log.info(MOD, "тик " + Rsx.tick() + ": игрока нет (меню?)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Никаких getName() по обфусцированному полю: обёртка сама знает,
|
||||
// как до него добраться в этой сборке.
|
||||
Log.info(MOD, String.format(
|
||||
"%d/%d | %s (%.1f/%.1f/%.1f) hp %.1f/%.1f | мир t=%d%s | экран: %s",
|
||||
ticks,
|
||||
period,
|
||||
player.getName(),
|
||||
player.getX(), player.getY(), player.getZ(),
|
||||
player.getHealth(), player.getMaxHealth(),
|
||||
Rsx.world().getTime(),
|
||||
Rsx.world().isClient() ? "" : " [выделенный сервер]",
|
||||
Rsx.currentScreen() == null
|
||||
? "нет"
|
||||
: Rsx.currentScreen().getClass().getSimpleName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
// Мод упал. По умолчанию бриф только пишет в лог, но мод может решить,
|
||||
// что делать: например, отключить проблемную задачу.
|
||||
Log.error(MOD, "задача упала: " + error, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnload() {
|
||||
// Игра выключается: освобождаем свои ресурсы, если они есть.
|
||||
Log.info(MOD, "выгружаюсь");
|
||||
if (config != null) {
|
||||
config.save();
|
||||
}
|
||||
}
|
||||
|
||||
private static int clamp(int v, int min, int max) {
|
||||
return v < min ? min : (v > max ? max : v);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user