Files
RustNotMC/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java
T
sasheg dev 450bdecc34 Клиент говорит с сервером по нашему протоколу
Раньше клиент умел только показывать меню. Теперь он заходит на сервер,
получает наш мир и рисует его.

Как это устроено:

- Наш сетевой слой (net/rustnotmc/client/net) читает наш протокол и
  переводит его пакеты в пакеты игры. Игра остаётся игрой: у неё есть
  мир, игрок, сущности и рендер, и все они работают. Не переписывать их
  ради того, чтобы нарисовать тот же мир иначе, незачем.
- Пакеты игры, которые не имеют соответствия у нас, не отправляются, а
  наоборот: перемещение игрока и ломание блока переводятся в наши.
- Экран входа игры (GuiConnecting) заменён наш: он спрашивает у лаунчера,
  кем пришёл игрок, и подставляет это в рукопожатие.
- Соединение качается из тика игры, а не из экрана. Рукопожатие, которое
  зависит от того, что экран ещё на экране, ломается alt-tab'ом.

Вход теперь через лаунчер, а не внутри игры:

- Лаунчер уже умеет логинить игрока и отдаёт игре имя, uuid и токен
  (--username, --uuid, --accessToken). Это та же роль, что у Yggdrasil в
  ванилле, только лаунчер наш.
- Сервер спрашивает лаунчер, один из ли наш этот игрок, и заодно получает
  бан-лист. Токен используется один раз и не хранится.
- Свои аккаунты сервера, когда настроен лаунчер, закрыты: сервер с двумя
  дверями не имеет дверей. Открыть их можно флагом, но намеренно.
- Если игру запустили без лаунчера, экран всё равно спрашивает имя и
  пароль: это путь для своего сервера и для разработки.

Проверено живьём: клиент вошёл по токену лаунчера, сервер спросил
лаунчер, лаунчер сказал "свой", игрок получил мир, 210 чанков пришли и
лежат на диске, рельеф нарисован.

Что чинилось по дороге:

- Minecraft.setIngameFocus() возвращал на главное меню, когда окно
  получало фокус. Игрок как раз кликает в окно, чтобы напечатать пароль, и
  экран входа исчезал в этот самый момент.
- Ответ лаунчера разбирается, а не ищется подстрокой: {"valid": true} и
  {"valid":true} это один и тот же ответ, и сервер, который понимает
  только второй, ломает работающий лаунчер.
2026-10-19 20:10:20 +03:00

243 lines
8.6 KiB
Java

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 three points:
* once during startup, once per game tick and once per frame. Everything a mod
* does hangs off those three 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 static boolean tickFailed;
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 one game tick of mod work.
*
* Called from inside the client's tick loop, next to its own tick, so a mod
* sees the same twenty steps per second the game does. Driving this from
* the frame loop instead would hand a mod as many ticks as the machine can
* draw: at 200 fps a mod would run its timers seven times too fast, and any
* mod that counts ticks would disagree with the world it is drawn over.
*/
public static synchronized void clientTick() {
// the connection is pumped from here, not from the screen that started
// it. A handshake that only runs while a screen is up is a handshake
// that the player can break by moving the window.
net.rustnotmc.client.net.RsxNetworkManager.pump();
if (loader == null || broken || tickFailed) {
return;
}
try {
loader.tick();
if (client != null) {
client.advanceTick();
}
} catch (Throwable throwable) {
tickFailed = true;
Log.error("RustNotMC: mod tick failed, disabling mod ticks", throwable);
}
}
/**
* Counts the frame and draws registered overlays.
*
* Called once per frame after the game has presented its own framebuffer and
* unwound its matrices, so the overlay sets up its own projection and leaves
* the state it did not set alone.
*/
public static void renderFrame(Minecraft minecraft) {
if (client != null) {
client.advanceFrame();
}
drawOverlay(minecraft);
}
/**
* Draws the overlays on top of the world loading screen.
*
* The loading screen is the one place that does not go through the frame
* tail. While terrain is generated it draws into a framebuffer of its own,
* blits that to the window and presents it itself, up to ten times a second,
* straight from the thread doing the generating. Anything drawn from the
* frame hook is wiped by the next of those presents, which is why an overlay
* used to vanish for the whole load and come back afterwards in the world.
*/
public static void renderLoadingScreen(Minecraft minecraft) {
drawOverlay(minecraft);
}
private static void drawOverlay(Minecraft minecraft) {
if (loader == null || surface == null || !surface.available()) {
return;
}
if (frameFailed) {
return;
}
try {
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);
}
}
}