4 Commits

Author SHA1 Message Date
SashegDev b5ffe0e9d7 Пинг и потери: считаем сами, потому что считать нечем
Вендор заменил сетевой слой на свой Netty-обработчик, поля задержки в нём нет,
а сам класс в маппингах не восстановлен. Ванильный getLag() не достать, и игра
пинг нигде не показывает.

Остаётся то, что есть всегда: сервер шлёт keep-alive раз в 15 секунд по своему
собственному расписанию. Отклонение интервала прихода - это и есть сеть, и
скользящее среднее по нему даёт оценку на любом сервере. Грубые интервалы
(потерянный пакет удваивает интервал) в среднее не попадают.

Число помечается как оценка: Rsx.pingExact() false, в строке "ping~". Эхо со
своим nonce в имени канала осталось в коде, но в этой сборке обработчик
кастомного payload у клиента не найден, поэтому профиль его не объявляет и эхо
просто недоступно - точность пока такая, какая есть.

Период keep-alive читается из -Drsx.keepalivePeriod: прокси иногда шлют иначе, и
стенду нужен короткий, чтобы проверить математику за секунды.

Заодно: Resolver игнорировал "принудительный" скан и отдавал устаревший индекс
замет позапрошенных классов. HUD-гейт читался в первый кадр и не находил экран.

Стенд кормитkeep-alive с интервалом 240 мс при периоде 200 и ждёт оценку около
50 мс - ровно то отклонение, которое он и создал.
2026-10-17 03:42:32 +03:00
SashegDev 419b2d0a6b HUD: моды рисуют через собственный API игры
Хук в отрисовку компаса. Выбор не случайный: виджет рисуется только пока виден
HUD-сло�� игры, поэтому панель бесплатно наследует и видимость, и масштаб, и
состояние горячей клавиши, которая HUD прячет. Дублировать логику клавиш не
пришлось.

Бридж зашивает ASM и регистрирует ClassFileTransformer, который дописывает вызов
rsx.HudHook.render перед возвратом метода. Профиль диктует цель, обфусцированных
имён в коде нет. Трансформер отдаёт контекст как Object: так один дескриптор
работает при любом переименовании класса контекста.

Привязка к API отрисовки трёхуровневая, и каждая ступень умеет отвалиться
отдельно: точное совпадение по дескриптору, затем по имени и числу аргументов
(класс текстуры грузится позже, чем привязка), затем по runtime-классу
билдера, который мы держим в руках. Не нашлось - пишем в лог один раз и гасим
только эту возможность.

Текст рисуется ванильной текстурой textures/font/ascii.png: у игры нет
однострочного текстового API, а для цифр и меток атласа хватает. Своим TTF
конвейером мы не занимались.

Стенд учит подделки: батч, билдер, команду, реестр текстур, масштаб, синглтон
экрана и виджет-компас. Проверяет, что панель рисует плашку и глифы с верными
координатами и что замолкает, когда игра прячет HUD. С отключённым хуком падают
пять проверок из двадцати пяти.

Профиль: hook.hud, экран HUD, батч, реестр текстур, масштаб и сетевые методы
NetHandlerPlayClient. У каждой строки - рецепт поиска после реобфускации.
2026-10-17 03:29:00 +03:00
SashegDev cc1eaece40 Несколько модов одновременно и живая работа с игрой
Главное: модлоадер физически не умел запускать больше одного мода. Родной
загрузчик игры запоминает тик-хук в статическом поле, которое перезаписывается
на каждой загрузке:

    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 на все моды,
и такой тест не мог поймать проблему с изоляцией.
2026-10-17 02:25:27 +03:00
SashegDev 9f43d35f44 Патчить апдейтер, а не только лаунчер
Вендорный 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 файлов.
2026-10-17 01:48:24 +03:00