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); } }