bdf0ae866a
Проект начинается с двух вещей: клиент, который мы действительно собрали, и загрузчик, который является его частью, а не чем-то рядом. Форк - 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 для запуска клиента
233 lines
6.8 KiB
Java
233 lines
6.8 KiB
Java
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());
|
|
}
|
|
}
|
|
} |