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

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

    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
+25 -5
View File
@@ -1,5 +1,10 @@
// Drives the fake client the way the game would, so the bridge can be tested
// end to end: bring the client up, hand the mods to the vendor loader, then tick.
//
// The mod jars are NOT loaded here - the agent finds them in build/testrun/mods
// and feeds them to ilxq one by one, exactly as it does in the game. The last
// one loaded is the only one whose getRunnableList() the vendor keeps, so the
// first two only work if the bridge really has a single shared drain.
public class FakeMain {
@@ -9,8 +14,15 @@ public class FakeMain {
wwk client = new wwk();
wwk.WA = client;
client.zb = "PLAYER";
client.ta = "WORLD";
ddvqd player = new ddvqd("Steve", 35789);
player.g = 10.5;
player.F = 64.0;
player.a = -3.25;
client.zb = player;
oisywi world = new oisywi();
world.isRemoteFlag = true;
world.time = 1000L;
client.ta = world;
client.S = null;
client.Zb = "TIMER";
client.jC = "RENDER";
@@ -18,11 +30,19 @@ public class FakeMain {
System.out.println("[fake] client up, telemetry scheduled");
Thread.sleep(2500);
System.out.println("[fake] ticking 5 times");
for (int i = 0; i < 5; i++) {
System.out.println("[fake] ticking 8 times");
for (int i = 0; i < 8; i++) {
if (i == 2) {
client.S = new bsh();
System.out.println("[fake] opened a screen");
}
if (i == 4) {
client.S = null;
System.out.println("[fake] closed the screen");
}
client.tick();
Thread.sleep(200);
}
System.out.println("[fake] done");
}
}
}