Несколько модов одновременно и живая работа с игрой
Главное: модлоадер физически не умел запускать больше одного мода. Родной
загрузчик игры запоминает тик-хук в статическом поле, которое перезаписывается
на каждой загрузке:
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:
@@ -16,9 +16,17 @@ import rsx.Resolver;
|
||||
*/
|
||||
public final class Rsx {
|
||||
|
||||
/** Version of this API, mirrored by RsxMod.apiVersion(). */
|
||||
public static final int API_VERSION = 1;
|
||||
|
||||
private Rsx() {
|
||||
}
|
||||
|
||||
/** The API version this bridge implements. */
|
||||
public static int apiVersion() {
|
||||
return API_VERSION;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- identity
|
||||
public static String buildId() {
|
||||
return Resolver.profile() == null ? "unknown"
|
||||
@@ -39,18 +47,156 @@ public final class Rsx {
|
||||
return Resolver.get("net.minecraft.client.Minecraft", "instance");
|
||||
}
|
||||
|
||||
public static Object player() {
|
||||
return Resolver.get("net.minecraft.client.Minecraft", "player", minecraft());
|
||||
}
|
||||
|
||||
public static Object world() {
|
||||
return Resolver.get("net.minecraft.client.Minecraft", "world", minecraft());
|
||||
}
|
||||
|
||||
public static Object currentScreen() {
|
||||
return Resolver.get("net.minecraft.client.Minecraft", "currentScreen", minecraft());
|
||||
}
|
||||
|
||||
/**
|
||||
* The local player, with the members a mod actually wants.
|
||||
*
|
||||
* Every value goes through the profile, so a mod asks for "name" and never
|
||||
* sees an obfuscated field. Anything the profile does not carry yet comes
|
||||
* back as null/0 and is reported once in the log instead of throwing.
|
||||
*/
|
||||
public static Player player() {
|
||||
return new Player(Resolver.get("net.minecraft.client.Minecraft", "player",
|
||||
minecraft()));
|
||||
}
|
||||
|
||||
/** The world the player is in, with the members a mod actually wants. */
|
||||
public static World world() {
|
||||
return new World(Resolver.get("net.minecraft.client.Minecraft", "world",
|
||||
minecraft()));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- wrappers
|
||||
|
||||
/** Thin typed view over an obfuscated game object. */
|
||||
public static final class Player {
|
||||
|
||||
private final Object raw;
|
||||
private boolean warned;
|
||||
|
||||
Player(Object raw) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
/** The underlying obfuscated object, for the reflective escape hatch. */
|
||||
public Object raw() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
public boolean isPresent() {
|
||||
return raw != null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
Object v = call("net.minecraft.entity.Entity", "getName", raw);
|
||||
return v == null ? null : String.valueOf(v);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
Object v = call("net.minecraft.entity.Entity", "getEntityId", raw);
|
||||
return v instanceof Number ? ((Number) v).intValue() : 0;
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
Object v = call("net.minecraft.entity.Entity", "isEntityAlive", raw);
|
||||
return v instanceof Boolean && ((Boolean) v).booleanValue();
|
||||
}
|
||||
|
||||
public boolean isDead() {
|
||||
return bool("net.minecraft.entity.Entity", "isDead", raw);
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return dbl("net.minecraft.entity.Entity", "posX", raw);
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return dbl("net.minecraft.entity.Entity", "posY", raw);
|
||||
}
|
||||
|
||||
public double getZ() {
|
||||
return dbl("net.minecraft.entity.Entity", "posZ", raw);
|
||||
}
|
||||
|
||||
public float getHealth() {
|
||||
Object v = call("net.minecraft.entity.EntityLivingBase", "getHealth", raw);
|
||||
return v instanceof Number ? ((Number) v).floatValue() : 0f;
|
||||
}
|
||||
|
||||
public float getMaxHealth() {
|
||||
Object v = call("net.minecraft.entity.EntityLivingBase", "getMaxHealth", raw);
|
||||
return v instanceof Number ? ((Number) v).floatValue() : 0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String name = getName();
|
||||
return name != null ? name : String.valueOf(raw);
|
||||
}
|
||||
|
||||
private double dbl(String cls, String member, Object on) {
|
||||
Object v = Resolver.get(cls, member, on);
|
||||
return v instanceof Number ? ((Number) v).doubleValue() : 0d;
|
||||
}
|
||||
|
||||
private boolean bool(String cls, String member, Object on) {
|
||||
Object v = Resolver.get(cls, member, on);
|
||||
return v instanceof Boolean && ((Boolean) v).booleanValue();
|
||||
}
|
||||
|
||||
private Object call(String cls, String member, Object on) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
Object v = Resolver.call(cls, member, on, new Object[0]);
|
||||
if (v == null && !warned) {
|
||||
warned = true;
|
||||
warn("profile has no " + cls + "#" + member);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
/** Thin typed view over the client's world. */
|
||||
public static final class World {
|
||||
|
||||
private final Object raw;
|
||||
|
||||
World(Object raw) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
/** The underlying obfuscated object, for the reflective escape hatch. */
|
||||
public Object raw() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
public boolean isPresent() {
|
||||
return raw != null;
|
||||
}
|
||||
|
||||
/** True on the client, false on a dedicated server. */
|
||||
public boolean isClient() {
|
||||
Object v = Resolver.get("net.minecraft.world.World", "isRemote", raw);
|
||||
return v instanceof Boolean && ((Boolean) v).booleanValue();
|
||||
}
|
||||
|
||||
/** World time in ticks, the number the in-game clock shows. */
|
||||
public long getTime() {
|
||||
Object v = Resolver.call("net.minecraft.world.World", "getWorldTime", raw,
|
||||
new Object[0]);
|
||||
return v instanceof Number ? ((Number) v).longValue() : 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "World(present=" + (raw != null) + ", time=" + getTime() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public static Class<?> classOf(String logical) {
|
||||
return Resolver.cls(logical);
|
||||
}
|
||||
@@ -107,11 +253,49 @@ public final class Rsx {
|
||||
ModBridge.post(task);
|
||||
}
|
||||
|
||||
/** Run a task repeatedly on the game thread. */
|
||||
/** Named variant: a failing task says which one it was. */
|
||||
public static void onGameThread(String name, Runnable task) {
|
||||
ModBridge.post(name, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task repeatedly on the game thread.
|
||||
*
|
||||
* The interval is wall-clock: if the game thread lags, the task runs late and
|
||||
* the period drifts. Use everyTicks for anything that must stay in step.
|
||||
*/
|
||||
public static void every(long millis, final Runnable task) {
|
||||
ModBridge.schedule(millis, task);
|
||||
}
|
||||
|
||||
/** Named variant of every(millis). */
|
||||
public static void every(String name, long millis, Runnable task) {
|
||||
ModBridge.schedule(name, millis, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task every N game ticks. Counted by the game's own tick, so it never
|
||||
* drifts and never fires twice after a lag.
|
||||
*/
|
||||
public static void everyTicks(int everyTicks, Runnable task) {
|
||||
ModBridge.scheduleTicks(everyTicks, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a game event. Handlers run on the game thread.
|
||||
*
|
||||
* Events: "tick" (every tick), "worldChange", "playerJoin", "playerLeave",
|
||||
* "screenOpen", "screenClose".
|
||||
*/
|
||||
public static void on(String event, Runnable handler) {
|
||||
ModBridge.on(event, handler);
|
||||
}
|
||||
|
||||
/** Ticks counted by the bridge since the game came up. */
|
||||
public static long tick() {
|
||||
return ModBridge.tickCount();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ logs
|
||||
public static void info(String msg) {
|
||||
rsx.Log.info(msg);
|
||||
@@ -120,4 +304,14 @@ public final class Rsx {
|
||||
public static void warn(String msg) {
|
||||
rsx.Log.warn(msg);
|
||||
}
|
||||
|
||||
/** Report a problem without throwing: the message lands in bridge.log. */
|
||||
public static void error(String msg) {
|
||||
rsx.Log.error(msg, null);
|
||||
}
|
||||
|
||||
/** Report a problem with its cause. */
|
||||
public static void error(String msg, Throwable cause) {
|
||||
rsx.Log.error(msg, cause);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user