Несколько модов одновременно и живая работа с игрой

Главное: модлоадер физически не умел запускать больше одного мода. Родной
загрузчик игры запоминает тик-хук в статическом поле, которое перезаписывается
на каждой загрузке:

    do = new ilxq();   // свой загрузчик на мод
    int = method2;     // статическое поле: помнит только ПОСЛЕДНИЙ мод

Из-за этого getRunnableList() вызывался у одного мода. Остальные грузились,
их onLoad отрабатывал, задачи висели в очереди - и их никто никогда не
разбирал. Молча.

Чинится без обфускации: в мосте одна очередь на всех и один drain. Какой бы
special.Main игра ни позвала, он выполняет работу всех модов. Владелец задачи
запоминается в ThreadLocal в момент onLoad, поэтому в логе видно, чей код
упал. Счетчик падений тоже переехал с записи в очереди на саму задачу: очередь
чистится каждый тик, и счетчик на ней обнулялся бы раньше, чем дошел до
предела.

API:
- Rsx.Player и Rsx.World: getName/getX/getY/getZ/getHealth/getMaxHealth/
  isAlive/isDead и getTime/isClient. Раньше player() отдавал Object, и мод
  вытаскивал имя из toString() обфусцированного класса.
- События: Rsx.on("tick"|"worldChange"|"playerJoin"|"playerLeave"|
  "screenOpen"|"screenClose"). Заменяют polling.
- Rsx.everyTicks(n) - периодичность по тикам, не плывет и не двоит после лага.
- RsxMod.apiVersion() и onError(): мод заявляет версию API и сам решает, что
  делать с падением. Без этого сломанный мод заливал лог.
- Задача, упавшая 5 раз подряд, отбрасывается целиком.
- Rsx.error() и именованные every/post - чтобы в логе было видно, чё это.

Профиль перегенерирован: в него попали члены Entity (posX/posY/posZ/isDead,
getName/getEntityId/isEntityAlive), EntityLivingBase (здоровье) и World
(isRemote/getWorldTime), которых раньше не было. Сгенерированы по SRG-именам
через make_profile.py, поэтому переживут очередную реобфускацию.

Тест-стенд переписан под три мода и воспроизводит обе особенности вендорного
загрузчика: новый classloader на jar и перезаписываемое статическое поле.
Контрольный прогон с отключённым drain роняет 10 проверок из 17, так что
проверки действительно завязаны на этот механизм.

Заодно: сломанный фейковый загрузчик делил один classloader на все моды,
и такой тест не мог поймать проблему с изоляцией.
This commit is contained in:
SashegDev
2026-10-17 02:25:27 +03:00
parent 2eaab88809
commit cc1eaece40
19 changed files with 1283 additions and 158 deletions
+57 -4
View File
@@ -24,16 +24,17 @@ third_party\asm\ ASM (BSD-3), зашит внутрь rsx-launcher-patch.j
uninstall.cmd вернуть всё как было
`install.cmd` копирует рантайм в `%LOCALAPPDATA%\rsx-mod`, патчит `app.jar`
лаунчера и прописывает `-javaagent` в его конфиг. Пути можно переопределить:
и `updater.exe` лаунчера и прописывает `-javaagent` в его конфиг. Пути можно
переопределить:
install.cmd --home D:\rsx-mod --game D:\RustexRemake
**Лаунчер должен быть закрыт.** Если он запущен с правами администратора,
`app.jar` не перезаписать — установщик об этом скажет.
Запускай лаунчер через `start-launcher.cmd`, а не ярлыком: `updater.exe`
сверяет хеш `app.jar` с манифестом и при патченом jar заново скачивает
официальный, стирая патч.
Обычный ярлык работает: патч лаунчера и патч апдейтера рассчитаны на то, что
обновление придёт и не снесёт модлоадер. `start-launcher.cmd` остаётся как
запасной путь в обход апдейтера.
## Как это работает
@@ -46,6 +47,7 @@ third_party\asm\ ASM (BSD-3), зашит внутрь rsx-launcher-patch.j
`Timer already cancelled`.
* **Моды** грузятся родным загрузчиком игры (`ilxq` → `special.Main`) — так же,
как моды с сервера, и выполняются в игровом потоке.
* **Несколько модов работают вместе** — см. «Почему один drain» ниже.
* **Переносимость модов.** Мод собирается один раз против стабильного API.
Мост сам переводит логические имена в имена текущей сборки, поэтому
ре-обфускация вендора не ломает уже собранные моды.
@@ -108,6 +110,57 @@ jar (payload начинается с offset 65024 и идёт до EOF). Заг
лаунчер перестаёт находить игру. Все правки идут через
`java.util.Properties` — в патчере есть команды `getprop` и `setprop`.
## Моды
Мод — это jar с манифестом
Rsx-Mod-Entrypoint: com.example.MyMod
и классом, реализующим `RsxMod`. Собирается один раз против `rsx-api.jar` и
дальше не пересобирается: все обфусцированные имена мод не знает, бриф
разрешает их через профиль сборки. Готовый шаблон — в `mod-template\`.
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