443 lines
14 KiB
Java
443 lines
14 KiB
Java
package net.rustexremake.api;
|
|
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Method;
|
|
|
|
import rsx.ModBridge;
|
|
import rsx.Resolver;
|
|
|
|
/**
|
|
* Stable API for mods.
|
|
*
|
|
* Nothing in this class mentions an obfuscated name. A mod asks for
|
|
* "net.minecraft.client.Minecraft" and the bridge looks up what that class is
|
|
* called in the build that is currently installed, so a mod compiled once keeps
|
|
* working after the vendor renames everything again.
|
|
*/
|
|
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"
|
|
: Resolver.profile().buildId;
|
|
}
|
|
|
|
public static String describe(String logical) {
|
|
return Resolver.profile() == null ? logical
|
|
: Resolver.profile().describe(logical);
|
|
}
|
|
|
|
public static boolean ready() {
|
|
return Resolver.cls("net.minecraft.client.Minecraft") != null;
|
|
}
|
|
|
|
// ------------------------------------------------------------- accessors
|
|
public static Object minecraft() {
|
|
return Resolver.get("net.minecraft.client.Minecraft", "instance");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
public static Object get(String logicalClass, String member) {
|
|
return Resolver.get(logicalClass, member);
|
|
}
|
|
|
|
public static Object get(String logicalClass, String member, Object instance) {
|
|
return Resolver.get(logicalClass, member, instance);
|
|
}
|
|
|
|
public static void set(String logicalClass, String member, Object instance,
|
|
Object value) {
|
|
Resolver.set(logicalClass, member, instance, value);
|
|
}
|
|
|
|
public static Object call(String logicalClass, String member, Object... args) {
|
|
return Resolver.call(logicalClass, member, args);
|
|
}
|
|
|
|
public static Object call(String logicalClass, String member, Object instance,
|
|
Object... args) {
|
|
return Resolver.call(logicalClass, member, instance, args);
|
|
}
|
|
|
|
/**
|
|
* Reflective escape hatch for members the profile does not carry yet.
|
|
* Mods should prefer the named accessors above.
|
|
*/
|
|
public static Field field(Class<?> owner, String name) {
|
|
try {
|
|
Field f = owner.getDeclaredField(name);
|
|
f.setAccessible(true);
|
|
return f;
|
|
} catch (Throwable t) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static Method method(Class<?> owner, String name, Class<?>... args) {
|
|
try {
|
|
Method m = owner.getDeclaredMethod(name, args);
|
|
m.setAccessible(true);
|
|
return m;
|
|
} catch (Throwable t) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------- threading
|
|
/** Run a task on the game thread, on the next tick. */
|
|
public static void onGameThread(Runnable task) {
|
|
ModBridge.post(task);
|
|
}
|
|
|
|
/** 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();
|
|
}
|
|
|
|
// ------------------------------------------------------------------- ping
|
|
|
|
/**
|
|
* Latency to the server, in milliseconds, or -1 while it is still unknown.
|
|
*
|
|
* The client measures nothing itself on this build, so this is derived from
|
|
* the server's keep-alive cadence; it settles after a few samples. Use
|
|
* pingExact() to know whether the number is a real round trip.
|
|
*/
|
|
public static int ping() {
|
|
return rsx.Ping.rttMillis();
|
|
}
|
|
|
|
/** True when the number came from a measured round trip, not an estimate. */
|
|
public static boolean pingExact() {
|
|
return rsx.Ping.exact();
|
|
}
|
|
|
|
/** Packet loss in percent, measured the same way. */
|
|
public static float packetLoss() {
|
|
return rsx.Ping.lossPercent();
|
|
}
|
|
|
|
/** A ready-to-print line, e.g. "ping~ 42 ms, loss 0.4%". */
|
|
public static String pingLine() {
|
|
return rsx.Ping.describe();
|
|
}
|
|
|
|
// ------------------------------------------------------------------- hud
|
|
|
|
/**
|
|
* The game's HUD, valid only inside a draw callback.
|
|
*
|
|
* Drawing here uses the game's own command pipeline, so a panel sits in the
|
|
* same layer and the same interface scale as the compass and the health
|
|
* bars, and follows the game's own show/hide state - including the key that
|
|
* hides the whole HUD.
|
|
*/
|
|
public static Hud hud() {
|
|
rsx.Hud live = rsx.Hud.current();
|
|
return live == null ? null : new Hud(live);
|
|
}
|
|
|
|
/**
|
|
* Draw something on the game's HUD, every frame, while it is visible.
|
|
*
|
|
* Rsx.onHud(new Rsx.HudDraw() {
|
|
* public void draw(Hud hud) {
|
|
* hud.text(8, 8, "42 ms", 0xFFFFFF, 1f);
|
|
* }
|
|
* });
|
|
*
|
|
* The callback runs on the render thread. Do the cheap thing here: the whole
|
|
* HUD is drawn inside one frame, and work done per frame is work done 60
|
|
* times a second.
|
|
*/
|
|
public static void onHud(HudDraw drawer) {
|
|
rsx.HudHook.add(new rsx.HudHook.Listener() {
|
|
@Override
|
|
public void onHud(rsx.Hud h) {
|
|
drawer.draw(new Hud(h));
|
|
}
|
|
});
|
|
}
|
|
|
|
/** A HUD draw callback. */
|
|
public interface HudDraw {
|
|
void draw(Hud hud);
|
|
}
|
|
|
|
/** The game's HUD, as a mod sees it. */
|
|
public static final class Hud {
|
|
|
|
private final rsx.Hud h;
|
|
|
|
Hud(rsx.Hud h) {
|
|
this.h = h;
|
|
}
|
|
|
|
/** False while the game is not drawing its HUD. */
|
|
public boolean available() {
|
|
return h != null && h.available();
|
|
}
|
|
|
|
/** The interface scale the game draws its own widgets with. */
|
|
public float scale() {
|
|
return h.scale();
|
|
}
|
|
|
|
/** Screen width in pixels, 0 when the game did not say. */
|
|
public int width() {
|
|
return h == null ? 0 : h.width();
|
|
}
|
|
|
|
/** Screen height in pixels, 0 when the game did not say. */
|
|
public int height() {
|
|
return h == null ? 0 : h.height();
|
|
}
|
|
|
|
/** A solid rectangle; alpha 0..1. */
|
|
public void rect(float x, float y, float w, float height, int rgb,
|
|
float alpha) {
|
|
if (h == null) {
|
|
return;
|
|
}
|
|
h.rect(x, y, w, height, rgb, alpha);
|
|
}
|
|
|
|
/**
|
|
* Text from the game's own ASCII atlas: digits and latin letters come
|
|
* out in the same glyphs the game uses. Characters the atlas has no
|
|
* glyph for are skipped.
|
|
*/
|
|
public void text(float x, float y, String value, int rgb, float alpha) {
|
|
if (h == null) {
|
|
return;
|
|
}
|
|
h.text(x, y, value, rgb, alpha);
|
|
}
|
|
|
|
/** Width of a label in HUD units. */
|
|
public float textWidth(String value) {
|
|
return h == null ? 0f : h.textWidth(value);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ logs
|
|
public static void info(String msg) {
|
|
rsx.Log.info(msg);
|
|
}
|
|
|
|
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);
|
|
}
|
|
} |