Путь к агенту в конфиге лаунчера переписывает сам лаунчер, и однажды он
потерял целый сегмент пути: осталось ...\rsx-mod\rsx-bridge.jar вместо
...\rsx-mod\bridge\rsx-bridge.jar. JVM не смог загрузить агент, и игра падала с
error2 при входе в моды.
Две правки:
- агент кладём в корень модлоадера, а не в bridge\: тогда его каталог и есть
дом, и mods\, libs\ и config\ находятся без угадывания;
- в env пишем слеши вперёд. Лаунчер переписывает конфиг своим properties-писателем
и портит именно обратные слеши, а у прямого слеша экранировать нечего.
Заодно premain теперь поднимается вверх, пока не найдёт каталог с mods\ или
libs\, вместо того чтобы считать родителем jar единственно верным ответом.
И ещё: лаунчер вместе с env потерял gamePath1, установщик теперь проверяет и
восстанавливает его.
Заодно выяснилось, что апдейтер всё же чинит app.jar ("Final repair: app.jar") -
наш фильтр не сработал, потому что маркер с базовым хешем к тому моменту не
совпадал. Сейчас это лечится автоматически, но в логах видно.
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 |
| общая библиотека | sasheg/rsxm-lib |
| эталонный мод | sasheg/rsxm-example |
| HUD | sasheg/rsxm-hud |
Готовый шаблон простого мода — в mod-template\, пример на библиотеке — в
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.