# rsx-modloader Модлоадер для RustexRemake: Java-моды в папке `mods`, подключаются через `-javaagent`, `game.jar` не патчится. Один раз поставил — моды переживают обновление игры. В лаунчере появляется выбор режима прямо на кнопке «Играть»: **с модами** или **ванилла**. ``` mod-template\ пример мода, собирается один раз bridge\ агент (-javaagent) и его исходники launcher-patch\ патч лаунчера: окно выбора и защита от автоапдейта testserver\ локальный сервер, принимает любой ник и токен scripts\ сборка, установка, запуск, удаление third_party\asm\ ASM (BSD-3), зашит внутрь rsx-launcher-patch.jar ``` ## Установка build.cmd собрать dist\ (нужен только JDK) install.cmd поставить модлоадер start-launcher.cmd запускать лаунчер uninstall.cmd вернуть всё как было `install.cmd` копирует рантайм в `%LOCALAPPDATA%\rsx-mod`, патчит `app.jar` и `updater.exe` лаунчера и прописывает `-javaagent` в его конфиг. Пути можно переопределить: install.cmd --home D:\rsx-mod --game D:\RustexRemake **Лаунчер должен быть закрыт.** Если он запущен с правами администратора, `app.jar` не перезаписать — установщик об этом скажет. Обычный ярлык работает: патч лаунчера и патч апдейтера рассчитаны на то, что обновление придёт и не снесёт модлоадер. `start-launcher.cmd` остаётся как запасной путь в обход апдейтера. ## Как это работает * **Агент** (`rsx-bridge.jar`) подключается через `-javaagent` и в `premain` читает профиль сборки игры (`profiles/latest.rsxp`): хеш jar, обфусцированные имена нужных классов и полей. Ничего не хардкодится под конкретную сборку. * **Патчи клиента** — A2 ставит `joml.fastmath` до загрузки JOML, A6 подменяет таймер телеметрии на «глухой». Оба сделаны на живом клиенте: первые версии A6 роняли игру с `NullPointerException` и `Timer already cancelled`. * **Моды** грузятся родным загрузчиком игры (`ilxq` → `special.Main`) — так же, как моды с сервера, и выполняются в игровом потоке. * **Несколько модов работают вместе** — см. «Почему один drain» ниже. * **Переносимость модов.** Мод собирается один раз против стабильного API. Мост сам переводит логические имена в имена текущей сборки, поэтому ре-обфускация вендора не ломает уже собранные моды. ## Патч лаунчера Лаунчер — сторонний продукт, и у него три особенности, которые мешают: * **Гонка за состоянием кнопки.** `xa.run()` вызывает `ha.y()` (ставит `PLAY`), а `fa.loadedWeb()` позже ставит `ERROR`. Состояния — это индексы кнопок в `web/launcher/main.html`, их шесть, поэтому `ERROR=6` скрывает все кнопки. При медленной загрузке страницы `loadedWeb` затирает `PLAY` — кнопки просто нет. Патч восстанавливает состояние после `loadedWeb`. * **Автоапдейтер.** Патченый `app.jar` отличается от манифеста, и лаунчер считает себя устаревшим в двух местах: `sb.q()` (размер и хеш файла) и `ha` (хеш рабочего jar, отсюда «обновление найдено, перезапустите лаунчер»). Оба закрыты через `rsx.Patch`, который действует только на файл с именем `app.jar` и только при наличии маркера `.rsx-patched`. * **Ремонт своего апдейтера.** При старте лаунчер проверяет хеш `updater.exe` и, не найдя вендорскую сборку, скачивает чистую копию поверх (`jb` → `l.ALLATORIxDEMO()` → «updater.exe»). Метод `jb` заменён на `rsx.Patch.skipUpdaterRepair()`. Возврат к обычному поведению: удалить `.rsx-patched` или запустить лаунчер с `-Drsx.allowLauncherUpdate=true`. ## Патч апдейтера `updater.exe` — это PE-заглушка Launch4j, к которой обычным zip просто пришит jar (payload начинается с offset 65024 и идёт до EOF). Заглушка запускает jar из себя, поэтому патчить надо именно хвост, а PE-заголовок не трогаем. В payload вставляются два хука: * `UpdaterGui.runLauncher(Z)V` → `rsx.UpdaterHook.beforeLauncher(...)` — перед запуском лаунчера `app.jar` патчится заново, если он оказался вендорским (апдейтер достаёт его из своего локального кэша даже когда скачивать нечего). * `DownloadEngine.findFilesToDownload` → `rsx.UpdaterHook.filterUpdates(...)` — `app.jar` убирается из списка на скачивание, **только** если хеш в манифесте совпадает с `baseSha256` из `.rsx-patched`. Хеш другой — значит настоящее обновление, и файл скачивается как обычно. `baseSha256` — это хеш нетронутого `app.jar`; патчер пишет его в `.rsx-patched` сам, до того как перезаписать файл. При настоящем обновлении маркер перезаписывается новым значением. Хук идемпотентен: повторный `patchupdater` берёт pristine-копию из `updater.exe.orig`, а если `updater.exe` заменил вендор — обновляет бэкап. Перед записью собранный jar проверяется загрузкой классов, так что битый патч никогда не попадёт на диск. Лог: `%APPDATA%\kjstudio\rsx-launcher\updater-patch.log`. ## Конфиг лаунчера — осторожно `launcher_config.properties` — это properties-файл: слэш экранирует следующий символ, а `\r` читается как перевод строки. Значение `gamePath1=G:\rustexremake` без экранирования превращается в `G:` + перевод строки + `ustexremake`, и лаунчер перестаёт находить игру. Все правки идут через `java.util.Properties` — в патчере есть команды `getprop` и `setprop`. ## Моды Мод — это jar с манифестом Rsx-Mod-Entrypoint: com.example.MyMod и классом, реализующим `RsxMod`. Собирается один раз против `rsx-api.jar` и дальше не пересобирается: все обфусцированные имена мод не знает, бриф разрешает их через профиль сборки. Общая библиотека кладётся в `%LOCALAPPDATA%\rsx-mod\mods\..\libs\` — то есть `%LOCALAPPDATA%\rsx-mod\libs\`: агент подмешивает эти jar в системный class path при старте, и моды видят библиотеку, не вшивая её в себя. Репозитории: | что | где | |---|---| | модлоадер | [sasheg/rsx-modloader](https://git.swe.zernmc.ru/sasheg/rsx-modloader) | | общая библиотека | [sasheg/rsxm-lib](https://git.swe.zernmc.ru/sasheg/rsxm-lib) | | эталонный мод | [sasheg/rsxm-example](https://git.swe.zernmc.ru/sasheg/rsxm-example) | | HUD | [sasheg/rsxm-hud](https://git.swe.zernmc.ru/sasheg/rsxm-hud) | Готовый шаблон простого мода — в `mod-template\`, пример на библиотеке — в [rsxm-example](https://git.swe.zernmc.ru/sasheg/rsxm-example). Rsx.everyTicks(20, this::onSecond); // раз в 20 игровых тиков Rsx.every(5000L, this::poll); // раз в 5 секунд реального времени Rsx.onGameThread(this::doIt); // на следующем тике Rsx.on("playerJoin", this::hello); // по событию События: `tick`, `worldChange`, `playerJoin`, `playerLeave`, `screenOpen`, `screenClose`. Игрок и мир — типизированные обёртки, а не `Object`: Rsx.Player p = Rsx.player(); p.getName(); p.getX(); p.getY(); p.getZ(); p.getHealth(); p.isAlive(); Rsx.World w = Rsx.world(); w.getTime(); w.isClient(); `everyTicks` стоит предпочитать `every`: миллисекунды — это часы, а тик — это тик, и при лагах миллисекундный таймер прыгает, а тиковый нет. Если задача мода падает, мост зовёт `RsxMod.onError`, а после пяти подряд падений вообще отбрасывает задачу — чтобы сломанный мод не залил лог. ### Почему один drain Родной загрузчик игры (`ilxq`) на каждый jar создаёт **новый** загрузчик классов, поэтому моды не видят друг друга и не конфликтуют за `special.Main`. Но тик-хук он запоминает в **статическом поле**, которое перезаписывается при каждой загрузке: do = new ilxq(); // свой загрузчик на мод ... int = method2; // статическое поле: помнит только ПОСЛЕДНИЙ мод Из-за этого `getRunnableList()` вызывается у одного мода — последнего загруженного. Мод, чей хук не запомнили, молча ничего не делает: его `onLoad` отработал, его задачи висят в очереди, и их никто не разбирает. Поэтому в мосте **одна очередь на всех и один drain**: какой бы `Main` игра ни позвала, он выполняет работу всех модов. Собственность задачи запоминается в `ThreadLocal` в момент `onLoad`, поэтому в логе ошибки видно, чей это код. Проверяется тестом: `bridge\src\test\python\run_test.py` грузит три мода и требует, чтобы работу всех трёх видно было в логе. ## Тест-сервер testserver\start.cmd Локальный сервер: принимает любой ник и токен, отвечает на `/server-ping`, слушает порты 25566–25573 и закидывает в пустой мир. Проверка без игры: python testserver\selftest.py Адрес сервера зашит в коде клиента, поэтому для полного захода в мир нужен редирект (PowerShell **от администратора**): scripts\install-hosts.ps1 прописать 127.0.0.1 для адреса клиента scripts\uninstall-hosts.ps1 откатить ## Сборка build.cmd Нужен только JDK. ASM лежит в `third_party\asm` и зашивается внутрь `rsx-launcher-patch.jar`, поэтому устанавливать модлоадер можно без Gradle и без сети. Готовые файлы кладутся в `dist\`: | файл | что это | |---|---| | `rsx-bridge.jar` | агент, Java 8 | | `rsx-api.jar` | API, против которого собирается мод | | `rsx-launcher-patch.jar` | патчер лаунчера, всё внутри | | `mod-template\` | заготовка мода | ## Тесты python bridge\src\test\python\run_test.py Поднимает фейковый клиент, проверяет профиль, A2, A6, загрузку мода и то, что мод выполняется в игровом потоке, а телеметрия не срабатывает. --- RustexRemake и KJST Launcher — сторонние продукты, они не входят в этот проект. Инструмент работает только на машине пользователя. ASM распространяется по лицензии BSD-3, см. `LICENSE`.