From bfda50ae618b9b087a8d7891e54c607e42f336e0 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Sat, 17 Oct 2026 02:55:29 +0300 Subject: [PATCH] =?UTF-8?q?=EF=BB=BF=D0=AD=D1=82=D0=B0=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BC=D0=BE=D0=B4=20RustexModding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Собран так, чтобы его читал человек, который пишет первый мод. Показывает apiVersion, конфиги из rsxm-lib, события, everyTicks, типизированные обёртки Rsx.Player и Rsx.World, onError и onUnload. Ни одного обфусцированного имени: мод спрашивает логическими именами, бриф переводит их в имена текущей сборки. Ни рефлексии, ни чтения файлов игры - всё это либо не нужно, либо делает бриф. Компилируется против rsx-api.jar и rsxm-lib.jar, но не содержит ни одного: библиотека лежит в libs\ и подмешивается в системный class path при старте. Проверяется просто - если в jar мода нет net/rsxm/, он собран правильно. --- .gitignore | 7 ++ README.md | 48 ++++++++ build.cmd | 38 ++++++ manifest.txt | 4 + src/main/java/com/example/ExampleMod.java | 143 ++++++++++++++++++++++ 5 files changed, 240 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.cmd create mode 100644 manifest.txt create mode 100644 src/main/java/com/example/ExampleMod.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82a0066 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# сборка +build/ +dist/ +*.class + +# зависимости: их не коммитим, они собираются из модлоадера и rsxm-lib +libs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..abb499b --- /dev/null +++ b/README.md @@ -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/`, значит +он собрался правильно. diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..a2cb472 --- /dev/null +++ b/build.cmd @@ -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% diff --git a/manifest.txt b/manifest.txt new file mode 100644 index 0000000..0a56115 --- /dev/null +++ b/manifest.txt @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Rsx-Mod-Entrypoint: com.example.ExampleMod + + diff --git a/src/main/java/com/example/ExampleMod.java b/src/main/java/com/example/ExampleMod.java new file mode 100644 index 0000000..681bce5 --- /dev/null +++ b/src/main/java/com/example/ExampleMod.java @@ -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); + } +}