Несколько модов одновременно и живая работа с игрой
Главное: модлоадер физически не умел запускать больше одного мода. Родной
загрузчик игры запоминает тик-хук в статическом поле, которое перезаписывается
на каждой загрузке:
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user