Главное: модлоадер физически не умел запускать больше одного мода. Родной
загрузчик игры запоминает тик-хук в статическом поле, которое перезаписывается
на каждой загрузке:
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 на все моды,
и такой тест не мог поймать проблему с изоляцией.
package-release.ps1 кладёт в zip то, что нужно другу: install/uninstall/
start-launcher, dist с артефактами, profiles, README. Сборка запускается
внутри, чтобы архив всегда соответствовал исходникам.
Установщик теперь сначала смотрит профиль в profiles\latest.rsxp пакета, и
только потом в старый путь с моей машины - раньше друг получил бы
"профиля нет".
Вендорный updater.exe перекачивает app.jar, и патч лаунчера после этого
стирался. Теперь патчится и сам апдейтер: в его payload (jar, пришитый к
PE-заглушке Launch4j после offset 65024) вставляются два хука.
- UpdaterGui.runLauncher -> beforeLauncher: перепатчить app.jar перед стартом
лаунчера, если апдейтер достал его из своего кэша.
- DownloadEngine.findFilesToDownload -> filterUpdates: убрать app.jar из
скачивания, но только если хеш манифеста равен baseSha256 из .rsx-patched.
Другой хеш означает настоящее обновление, и файл качается как обычно.
Хук сделан обёрткой вокруг findFilesToDownload, а не правкой call site: у
invokespecial получатель лежит под аргументами, и замена вызова ломала стек.
Ещё лаунчер при старте чинит свой updater.exe (jb -> l качает "updater.exe"),
из-за чего хук жил ровно один запуск. Этот путь закрыт третьим guard.
Ещё:
- .rsx-patched теперь пишет сам патчер: baseSha256 нужен фильтру, чтобы
отличать наш патч от настоящего обновления.
- patchupdater идемпотентен и берёт pristine-копию из updater.exe.orig.
- Перед записью payload проверяется загрузкой классов.
- Установщик патчит апдейтер, а uninstall откатывает и его.
- Маркер и log больше не прибиты к %APPDATA%\kjstudio: папки выводятся из
расположения апдейтера.
Проверено на живой установке два запуска подряд: хук выживает, app.jar
остаётся пропатченным, updater качает 0 файлов.
Моды грузятся через -javaagent, game.jar не патчится, переживают ре-обфускацию.
Что внутри:
- агент rsx-bridge.jar: профиль сборки игры, патчи A2/A6, загрузка модов
через родной загрузчик игры (ilxq -> special.Main)
- патч лаунчера: окно выбора «С модами / Ванилла» по кнопке «Играть»
и защита от автоапдейта лаунчера
- install/uninstall: всё ставится в %LOCALAPPDATA%\rsx-mod, пути не хардкодятся
- build: три готовых артефакта в dist, нужен только JDK
- testserver: локальный сервер, принимает любой ник и токен
Проверено на живом клиенте: мод грузится и выполняется в игровом потоке,
в логе видно игрока и мир.