Files
RustNotMC/game-fork/src-overlay/net/rustnotmc/client/RsxClientServices.java
T
sasheg dev 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 для запуска клиента
2026-10-18 21:31:27 +03:00

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