Форк клиента собирается из исходников, модзагрузчик встроен в игру
Проект начинается с двух вещей: клиент, который мы действительно собрали, и загрузчик, который является его частью, а не чем-то рядом. Форк - prepare-game.ps1 пересобирает клиент целиком: ванильный jar, SRG и MCP, SpecialSource, FernFlower, патчи, автофиксеры, javac. Идемпотентен. - Свой код игры лежит в src-overlay, потому что src пересоздаётся при каждой сборке и всё написанное вручную там погибает. - 116 патчей в patches/decompiler-fixes.txt. Каждый сверялся с оригинальным байткодом через javap: компиляция не ловит ошибки декомпиляции. - struct_diff.py сравнивает исходный байткод с нашей сборкой по классам, интерфейсам, суперклассам, методам и полям, и отдельно проверяет, что пропавшие классы действительно никому не нужны. Загрузчик - Собирается и тестируется без игры: build.ps1 не использует classpath вообще, поэтому SelfTest из 128 проверок проходит без запущенного форка. - Клиентский API не содержит ни одного игрового типа, реализацию даёт игра. - Политика: мод меняет то, что клиент показывает, но никогда то, что истинно в мире. Мир, предметы, игроки и сеть принадлежат серверу. - Capability приведены в соответствие с этой политикой: осталось шесть, и за каждым стоит реальная проверка. Прежние WORLD, PLAYER, NETWORK, TEXT, INPUT и COMMAND выдавались, но ничего не проверяли. - Команды модов имеют область CLIENT, SERVER или BOTH. Серверные не выполняются на клиенте никогда. - Свой jar загрузчика лежит в classpath игры и сборки форка. Инструменты - add_patch.py, чтобы каждое исправление переживало передекомпиляцию - find_self_comparisons.py, find_remove_index_bugs.py, find_callers.py - stage_run.py и fetch_missing.py для запуска клиента
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
package net.rustnotmc.client;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.rustnotmc.api.client.ClientHost;
|
||||
import net.rustnotmc.api.hud.HudLayout;
|
||||
import net.rustnotmc.loader.Log;
|
||||
import net.rustnotmc.loader.ModContainer;
|
||||
import net.rustnotmc.loader.ModLoader;
|
||||
|
||||
/**
|
||||
* Where the client and the mod loader meet.
|
||||
*
|
||||
* The loader is a library, not an agent. The game calls in at two points: once
|
||||
* during startup, and once per frame. Everything a mod does hangs off those two
|
||||
* calls, so the loader never has to know how a game loop is shaped.
|
||||
*
|
||||
* Every entry point is wrapped. A mod that throws must not be able to take the
|
||||
* client down with it, and a loader that cannot start must not stop the game
|
||||
* from being a game.
|
||||
*
|
||||
* This file lives in game-fork/src-overlay because src is regenerated from the
|
||||
* vanilla jar on every build and anything written there is lost.
|
||||
*/
|
||||
public final class RsxBootstrap {
|
||||
|
||||
private static ModLoader loader;
|
||||
private static RsxHudSurface surface;
|
||||
private static RsxClientServices client;
|
||||
private static boolean broken;
|
||||
private static boolean frameFailed;
|
||||
|
||||
private RsxBootstrap() {
|
||||
}
|
||||
|
||||
/** True once mods have been discovered and enabled. */
|
||||
public static boolean ready() {
|
||||
return loader != null && !broken;
|
||||
}
|
||||
|
||||
public static ModLoader loader() {
|
||||
return loader;
|
||||
}
|
||||
|
||||
public static int loadedMods() {
|
||||
return loader == null ? 0 : loader.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers and enables mods. Called once, after the client has a display,
|
||||
* a font and a resource manager, so a mod may touch any of them.
|
||||
*/
|
||||
public static synchronized void start(Minecraft minecraft) {
|
||||
if (loader != null || broken) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// the client API goes up before any mod runs, so onConstruct can use
|
||||
// it: a mod registers commands and reads the screen there
|
||||
client = new RsxClientServices(minecraft);
|
||||
ClientHost.install(client);
|
||||
|
||||
File directory = new File(minecraft.gameDir, "mods");
|
||||
loader = new ModLoader(directory);
|
||||
|
||||
int found = loader.discover();
|
||||
Log.info("RustNotMC: scanning " + directory + ", " + found + " mod jar(s)");
|
||||
|
||||
if (!loader.resolve()) {
|
||||
reportProblems();
|
||||
broken = true;
|
||||
loader = null;
|
||||
return;
|
||||
}
|
||||
|
||||
loader.loadAll();
|
||||
surface = new RsxHudSurface(minecraft);
|
||||
reportProblems();
|
||||
|
||||
if (loader.count() == 0) {
|
||||
Log.info("RustNotMC: no mods loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder line = new StringBuilder("RustNotMC: loaded ");
|
||||
List<ModContainer> mods = loader.mods();
|
||||
for (int i = 0; i < mods.size(); i++) {
|
||||
if (i > 0) {
|
||||
line.append(", ");
|
||||
}
|
||||
ModContainer mod = mods.get(i);
|
||||
line.append(mod.id()).append(' ').append(mod.version());
|
||||
}
|
||||
Log.info(line.toString());
|
||||
} catch (Throwable throwable) {
|
||||
broken = true;
|
||||
loader = null;
|
||||
Log.error("RustNotMC: mod loader failed to start", throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offers a typed chat line to the mod commands.
|
||||
*
|
||||
* Called from the point where a line leaves the client. Returns true when a
|
||||
* CLIENT scoped command carried it out and the line must not be sent on.
|
||||
* SERVER and BOTH commands return false on purpose: the server is the one
|
||||
* that has to see them and decide.
|
||||
*/
|
||||
public static boolean handleChatCommand(String line) {
|
||||
if (client == null || !ClientHost.present()) {
|
||||
return false;
|
||||
}
|
||||
return client.dispatchCommand(line, localName());
|
||||
}
|
||||
|
||||
private static String localName() {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
if (mc != null && mc.getSession() != null) {
|
||||
return mc.getSession().getUsername();
|
||||
}
|
||||
return "local";
|
||||
}
|
||||
|
||||
private static void reportProblems() {
|
||||
List<String> problems = loader.problems();
|
||||
for (int i = 0; i < problems.size(); i++) {
|
||||
Log.error("RustNotMC: " + problems.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs scheduled mod work and draws registered overlays.
|
||||
*
|
||||
* Called once per frame with the main framebuffer bound and the game's own
|
||||
* matrices already unwound, so the overlay sets up its own projection and
|
||||
* leaves the state it did not set alone.
|
||||
*/
|
||||
public static void renderFrame(Minecraft minecraft) {
|
||||
if (loader == null || surface == null || !surface.available()) {
|
||||
return;
|
||||
}
|
||||
if (frameFailed) {
|
||||
return;
|
||||
}
|
||||
if (client != null) {
|
||||
client.advanceFrame();
|
||||
}
|
||||
try {
|
||||
loader.tick();
|
||||
if (client != null) {
|
||||
client.advanceTick();
|
||||
}
|
||||
|
||||
ScaledResolution scaled = new ScaledResolution(minecraft);
|
||||
int width = scaled.getScaledWidth();
|
||||
int height = scaled.getScaledHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// the overlay draws in flat screen space, so depth would fight it
|
||||
// and the blend state has to be ours for the duration
|
||||
GlStateManager.disableDepth();
|
||||
|
||||
GlStateManager.pushMatrix();
|
||||
GlStateManager.matrixMode(5889);
|
||||
GlStateManager.loadIdentity();
|
||||
GlStateManager.ortho(0.0D, width, height, 0.0D, 1000.0D, 3000.0D);
|
||||
GlStateManager.matrixMode(5888);
|
||||
GlStateManager.loadIdentity();
|
||||
GlStateManager.translate(0.0F, 0.0F, -2000.0F);
|
||||
|
||||
GlStateManager.enableBlend();
|
||||
GlStateManager.tryBlendFuncSeparate(
|
||||
GlStateManager.SourceFactor.SRC_ALPHA,
|
||||
GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA,
|
||||
GlStateManager.SourceFactor.ONE,
|
||||
GlStateManager.DestFactor.ZERO);
|
||||
|
||||
try {
|
||||
HudLayout.render(surface);
|
||||
} finally {
|
||||
GlStateManager.disableBlend();
|
||||
GlStateManager.enableDepth();
|
||||
GlStateManager.popMatrix();
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
// one bad frame is a bug worth seeing, but it must not become a
|
||||
// per-frame stack trace or a dead client
|
||||
frameFailed = true;
|
||||
Log.error("RustNotMC: overlay rendering failed, disabling overlays", throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package net.rustnotmc.client;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiChat;
|
||||
import net.minecraft.client.gui.GuiIngameMenu;
|
||||
import net.minecraft.client.gui.GuiMainMenu;
|
||||
import net.minecraft.client.gui.GuiOptions;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.gui.GuiWorldSelection;
|
||||
import net.minecraft.client.gui.achievement.GuiStats;
|
||||
import net.minecraft.client.gui.advancements.GuiScreenAdvancements;
|
||||
import net.minecraft.client.gui.inventory.GuiContainer;
|
||||
import net.minecraft.client.settings.KeyBinding;
|
||||
import net.rustnotmc.api.client.ClientServices;
|
||||
import net.rustnotmc.api.client.Commands;
|
||||
import net.rustnotmc.api.client.Input;
|
||||
import net.rustnotmc.api.client.Screen;
|
||||
import net.rustnotmc.api.client.ScreenKind;
|
||||
import org.lwjgl.input.Mouse;
|
||||
|
||||
/**
|
||||
* The client's implementation of the mod API.
|
||||
*
|
||||
* Everything here reads presentation state the client already has: what screen
|
||||
* is open, what the window measures, which key is held. Nothing reaches into
|
||||
* the world, a player or a packet, because the server owns those and a client
|
||||
* mod is not allowed to decide them.
|
||||
*
|
||||
* The values are recomputed per call rather than cached. A mod may ask once a
|
||||
* frame, and a stale answer would be worse than a cheap one.
|
||||
*
|
||||
* Lives in game-fork/src-overlay; src is regenerated on every build.
|
||||
*/
|
||||
public final class RsxClientServices implements ClientServices {
|
||||
|
||||
private final Minecraft minecraft;
|
||||
private final RsxScreen screen = new RsxScreen();
|
||||
private final RsxInput input = new RsxInput();
|
||||
private final RsxCommands commands = new RsxCommands();
|
||||
|
||||
private long tick;
|
||||
private long frame;
|
||||
|
||||
public RsxClientServices(Minecraft minecraft) {
|
||||
this.minecraft = minecraft;
|
||||
}
|
||||
|
||||
/** Called once per frame by the bootstrap, before anything is asked. */
|
||||
public void advanceFrame() {
|
||||
this.frame++;
|
||||
}
|
||||
|
||||
/** Called once per tick by the bootstrap. */
|
||||
public void advanceTick() {
|
||||
this.tick++;
|
||||
}
|
||||
|
||||
boolean dispatchCommand(String line, String sender) {
|
||||
return this.commands.dispatch(line, sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Screen screen() {
|
||||
return this.screen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Input input() {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Commands commands() {
|
||||
return this.commands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long tick() {
|
||||
return this.tick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long frame() {
|
||||
return this.frame;
|
||||
}
|
||||
|
||||
private final class RsxScreen implements Screen {
|
||||
|
||||
@Override
|
||||
public ScreenKind kind() {
|
||||
GuiScreen current = minecraft.currentScreen;
|
||||
if (current == null) {
|
||||
return minecraft.world == null ? ScreenKind.UNKNOWN : ScreenKind.IN_GAME;
|
||||
}
|
||||
if (current instanceof GuiChat) {
|
||||
return ScreenKind.CHAT;
|
||||
}
|
||||
if (current instanceof GuiIngameMenu) {
|
||||
return ScreenKind.PAUSED;
|
||||
}
|
||||
if (current instanceof GuiMainMenu) {
|
||||
return ScreenKind.MAIN_MENU;
|
||||
}
|
||||
if (current instanceof GuiWorldSelection) {
|
||||
return ScreenKind.WORLD_SELECTION;
|
||||
}
|
||||
if (current instanceof GuiOptions) {
|
||||
return ScreenKind.OPTIONS;
|
||||
}
|
||||
if (current instanceof GuiContainer) {
|
||||
return ScreenKind.INVENTORY;
|
||||
}
|
||||
if (current instanceof GuiScreenAdvancements || current instanceof GuiStats) {
|
||||
return ScreenKind.ADVANCEMENTS;
|
||||
}
|
||||
// a screen this build has no name for, over a loaded world or not
|
||||
return minecraft.world == null ? ScreenKind.UNKNOWN : ScreenKind.PAUSED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int width() {
|
||||
return minecraft.displayWidth / Math.max(1, scaleFactor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int height() {
|
||||
return minecraft.displayHeight / Math.max(1, scaleFactor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public float scale() {
|
||||
return (float) scaleFactor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int windowWidth() {
|
||||
return minecraft.displayWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int windowHeight() {
|
||||
return minecraft.displayHeight;
|
||||
}
|
||||
|
||||
private int scaleFactor() {
|
||||
int gui = minecraft.gameSettings == null ? 2 : minecraft.gameSettings.guiScale;
|
||||
if (gui <= 0) {
|
||||
// auto: the client picks the largest scale that keeps the
|
||||
// interface at least this wide
|
||||
int width = minecraft.displayWidth;
|
||||
gui = 1;
|
||||
while (width / (gui + 1) >= 320 && gui < 4) {
|
||||
gui++;
|
||||
}
|
||||
}
|
||||
return gui;
|
||||
}
|
||||
}
|
||||
|
||||
private final class RsxInput implements Input {
|
||||
|
||||
@Override
|
||||
public boolean keyDown(String keyId) {
|
||||
if (keyId == null || minecraft.gameSettings == null) {
|
||||
return false;
|
||||
}
|
||||
KeyBinding[] bindings = minecraft.gameSettings.keyBindings;
|
||||
if (bindings == null) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < bindings.length; i++) {
|
||||
if (keyId.equals(bindings[i].getKeyDescription()) && bindings[i].isKeyDown()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean anyKeyDown() {
|
||||
KeyBinding[] bindings = minecraft.gameSettings == null
|
||||
? null : minecraft.gameSettings.keyBindings;
|
||||
if (bindings == null) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < bindings.length; i++) {
|
||||
if (bindings[i].isKeyDown()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean focused() {
|
||||
return minecraft.inGameHasFocus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int mouseX() {
|
||||
return scaled(Mouse.getX());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int mouseY() {
|
||||
return scaled(Mouse.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int mouseButtons() {
|
||||
int buttons = 0;
|
||||
if (Mouse.isButtonDown(0)) {
|
||||
buttons |= 1;
|
||||
}
|
||||
if (Mouse.isButtonDown(1)) {
|
||||
buttons |= 2;
|
||||
}
|
||||
if (Mouse.isButtonDown(2)) {
|
||||
buttons |= 4;
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDown(int button) {
|
||||
return button >= 0 && button <= 2 && Mouse.isButtonDown(button);
|
||||
}
|
||||
|
||||
private int scaled(int windowCoord) {
|
||||
return windowCoord / Math.max(1, screen.scaleFactor());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package net.rustnotmc.client;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import net.rustnotmc.api.client.ClientCommand;
|
||||
import net.rustnotmc.api.client.CommandArgs;
|
||||
import net.rustnotmc.api.client.CommandScope;
|
||||
import net.rustnotmc.api.client.Commands;
|
||||
import net.rustnotmc.loader.Log;
|
||||
import net.rustnotmc.loader.ModLoader;
|
||||
|
||||
/**
|
||||
* The mod command table.
|
||||
*
|
||||
* Only a CLIENT scoped command is carried out here. SERVER and BOTH are not
|
||||
* consumed by the client at all: the line is left for the server to take, which
|
||||
* is the whole point of the scope. The client never decides an effect the server
|
||||
* owns.
|
||||
*
|
||||
* The table is shared by every mod, so a name can only be claimed once. A second
|
||||
* mod asking for a taken name is refused loudly rather than silently shadowing
|
||||
* the first, which would be miserable to debug.
|
||||
*
|
||||
* Lives in game-fork/src-overlay; src is regenerated on every build.
|
||||
*/
|
||||
final class RsxCommands implements Commands {
|
||||
|
||||
private static final class Entry {
|
||||
final String owner;
|
||||
final ClientCommand handler;
|
||||
final CommandScope scope;
|
||||
|
||||
Entry(String owner, ClientCommand handler, CommandScope scope) {
|
||||
this.owner = owner;
|
||||
this.handler = handler;
|
||||
this.scope = scope;
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<String, Entry> entries = new LinkedHashMap<String, Entry>();
|
||||
|
||||
@Override
|
||||
public boolean register(String name, ClientCommand handler, CommandScope scope) {
|
||||
if (name == null || handler == null || scope == null) {
|
||||
return false;
|
||||
}
|
||||
String key = normalise(name);
|
||||
if (key.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
String owner = registering();
|
||||
Entry clash = this.entries.get(key);
|
||||
if (clash != null) {
|
||||
if (clash.owner.equals(owner) && clash.handler == handler) {
|
||||
return true;
|
||||
}
|
||||
Log.warn("command /" + key + " is already registered by " + clash.owner
|
||||
+ "; the second registration was refused");
|
||||
return false;
|
||||
}
|
||||
this.entries.put(key, new Entry(owner, handler, scope));
|
||||
Log.info("registered command /" + key + " scope=" + scope.name().toLowerCase());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean register(String name, ClientCommand handler) {
|
||||
return register(name, handler, CommandScope.CLIENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String name) {
|
||||
return name != null && this.entries.containsKey(normalise(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandScope scopeOf(String name) {
|
||||
Entry e = name == null ? null : this.entries.get(normalise(name));
|
||||
return e == null ? null : e.scope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count() {
|
||||
return this.entries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Offers a typed line to the mod commands.
|
||||
*
|
||||
* @return true when the client carried it out and the line must not be sent
|
||||
* on. False means the line is not a client command, which is the
|
||||
* answer for SERVER and BOTH: the server has to see those.
|
||||
*/
|
||||
boolean dispatch(String line, String sender) {
|
||||
if (line == null || !line.startsWith("/")) {
|
||||
return false;
|
||||
}
|
||||
String body = line.substring(1).trim();
|
||||
int space = body.indexOf(' ');
|
||||
String name = space < 0 ? body : body.substring(0, space);
|
||||
Entry entry = this.entries.get(normalise(name));
|
||||
if (entry == null || entry.scope != CommandScope.CLIENT) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
entry.handler.run(CommandArgs.parse(body, sender));
|
||||
} catch (Throwable t) {
|
||||
Log.error("command /" + name + " failed", t);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String normalise(String name) {
|
||||
String v = name.trim();
|
||||
if (v.startsWith("/")) {
|
||||
v = v.substring(1);
|
||||
}
|
||||
return v.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Who is registering right now.
|
||||
*
|
||||
* The loader knows this because it is the one calling onConstruct, and the
|
||||
* game can read it without the loader ever depending on the game.
|
||||
*/
|
||||
private static String registering() {
|
||||
String who = ModLoader.REGISTERING.get();
|
||||
return who == null ? "unknown" : who;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package net.rustnotmc.client;
|
||||
|
||||
import net.rustnotmc.loader.Log;
|
||||
|
||||
/**
|
||||
* Temporary diagnostics, added to work out why jumping and flight misbehave.
|
||||
*
|
||||
* Static analysis kept producing confident and wrong answers about this path, so
|
||||
* the state is measured instead. Delete this file and its patch entry once the
|
||||
* movement bugs are closed.
|
||||
*/
|
||||
public final class RsxDebug {
|
||||
|
||||
private static int ticks;
|
||||
|
||||
private RsxDebug() {
|
||||
}
|
||||
|
||||
/** One-off trace for the empty sound registry. */
|
||||
public static void sound(String message) {
|
||||
Log.info("DBG snd " + message);
|
||||
}
|
||||
|
||||
/** Called from EntityPlayerSP.onLivingUpdate, once a second. */
|
||||
public static void movement(Object self) {
|
||||
if (++ticks % 20 != 0) {
|
||||
return;
|
||||
}
|
||||
Log.info("DBG sp onGround=" + read(self, "onGround")
|
||||
+ " inJump=" + read(self, "movementInput")
|
||||
+ " isJumping=" + read(self, "isJumping")
|
||||
+ " riding=" + read(self, "isRiding")
|
||||
+ " srvWorld=" + read(self, "isServerWorld")
|
||||
+ " allowFly=" + read(self, "allowFlying")
|
||||
+ " flying=" + read(self, "isFlying")
|
||||
+ " mY=" + read(self, "motionY")
|
||||
+ " y=" + read(self, "posY"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a field by name.
|
||||
*
|
||||
* Deliberately reflective and deliberately forgiving: a diagnostic that
|
||||
* throws when a field is renamed is worse than one that prints null.
|
||||
*/
|
||||
private static String read(Object target, String field) {
|
||||
if (target == null) {
|
||||
return "?";
|
||||
}
|
||||
try {
|
||||
java.lang.reflect.Field f = findField(target.getClass(), field);
|
||||
if (f == null) {
|
||||
return "?";
|
||||
}
|
||||
f.setAccessible(true);
|
||||
return String.valueOf(f.get(target));
|
||||
} catch (Throwable t) {
|
||||
return "!";
|
||||
}
|
||||
}
|
||||
|
||||
private static java.lang.reflect.Field findField(Class<?> type, String name) {
|
||||
while (type != null) {
|
||||
try {
|
||||
return type.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException e) {
|
||||
type = type.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package net.rustnotmc.client;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.client.renderer.BufferBuilder;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
|
||||
import net.rustnotmc.api.hud.HudSurface;
|
||||
|
||||
/**
|
||||
* The game's implementation of the mod HUD surface.
|
||||
*
|
||||
* A mod draws through this interface and never touches a game type, which is
|
||||
* what lets the layout and the overlay code be tested with the game closed. The
|
||||
* reverse cost is paid once, here: everything a mod needs from Minecraft is
|
||||
* resolved in this class.
|
||||
*
|
||||
* Coordinates are the scaled interface ones, so an element lines up with the
|
||||
* health bar and the chat without the mod knowing about GUI scale.
|
||||
*
|
||||
* Lives in game-fork/src-overlay; src is regenerated on every build.
|
||||
*/
|
||||
public final class RsxHudSurface implements HudSurface {
|
||||
|
||||
private final Minecraft minecraft;
|
||||
|
||||
/**
|
||||
* Rebuilt only when the window or the scale factor actually changes. A new
|
||||
* ScaledResolution per frame would be one short-lived object sixty times a
|
||||
* second for a value that is almost never different.
|
||||
*/
|
||||
private ScaledResolution resolution;
|
||||
private int cachedWidth = -1;
|
||||
private int cachedHeight = -1;
|
||||
private int cachedScale = -1;
|
||||
|
||||
public RsxHudSurface(Minecraft minecraft) {
|
||||
this.minecraft = minecraft;
|
||||
}
|
||||
|
||||
private ScaledResolution resolution() {
|
||||
int width = this.minecraft.displayWidth;
|
||||
int height = this.minecraft.displayHeight;
|
||||
int scale = this.minecraft.gameSettings.guiScale;
|
||||
if (this.resolution == null
|
||||
|| width != this.cachedWidth
|
||||
|| height != this.cachedHeight
|
||||
|| scale != this.cachedScale) {
|
||||
this.resolution = new ScaledResolution(this.minecraft);
|
||||
this.cachedWidth = width;
|
||||
this.cachedHeight = height;
|
||||
this.cachedScale = scale;
|
||||
}
|
||||
return this.resolution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean available() {
|
||||
return this.minecraft != null && this.minecraft.fontRenderer != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int width() {
|
||||
return this.available() ? resolution().getScaledWidth() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int height() {
|
||||
return this.available() ? resolution().getScaledHeight() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float scale() {
|
||||
return (float) resolution().getScaleFactor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rect(float x, float y, float w, float h, int rgb, float alpha) {
|
||||
if (w <= 0.0F || h <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
int a = clampAlpha(alpha);
|
||||
int r = (rgb >> 16) & 0xFF;
|
||||
int g = (rgb >> 8) & 0xFF;
|
||||
int b = rgb & 0xFF;
|
||||
|
||||
// the vanilla debug overlay draws coloured quads the same way; this
|
||||
// GlStateManager has no isTextureEnabled to ask, so the state is set
|
||||
// explicitly the way the game itself does it
|
||||
GlStateManager.disableTexture2D();
|
||||
|
||||
Tessellator tessellator = Tessellator.getInstance();
|
||||
BufferBuilder buffer = tessellator.getBuffer();
|
||||
buffer.begin(7, DefaultVertexFormats.POSITION_COLOR);
|
||||
buffer.pos(x, y + h, 0.0D).color(r, g, b, a).endVertex();
|
||||
buffer.pos(x + w, y + h, 0.0D).color(r, g, b, a).endVertex();
|
||||
buffer.pos(x + w, y, 0.0D).color(r, g, b, a).endVertex();
|
||||
buffer.pos(x, y, 0.0D).color(r, g, b, a).endVertex();
|
||||
tessellator.draw();
|
||||
|
||||
GlStateManager.enableTexture2D();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void text(float x, float y, String value, int rgb, float alpha) {
|
||||
if (value == null || value.isEmpty() || !this.available()) {
|
||||
return;
|
||||
}
|
||||
GlStateManager.enableTexture2D();
|
||||
this.minecraft.fontRenderer.drawStringWithShadow(
|
||||
value, x, y, (rgb & 0x00FFFFFF) | (clampAlpha(alpha) << 24));
|
||||
}
|
||||
|
||||
@Override
|
||||
public float textWidth(String value) {
|
||||
if (value == null || value.isEmpty() || !this.available()) {
|
||||
return 0.0F;
|
||||
}
|
||||
return (float) this.minecraft.fontRenderer.getStringWidth(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int textHeight() {
|
||||
return this.available() ? this.minecraft.fontRenderer.FONT_HEIGHT : 0;
|
||||
}
|
||||
|
||||
private static int clampAlpha(float alpha) {
|
||||
if (alpha < 0.0F) {
|
||||
return 0;
|
||||
}
|
||||
if (alpha > 1.0F) {
|
||||
return 255;
|
||||
}
|
||||
return (int) (alpha * 255.0F + 0.5F);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user