Files
RustNotMC/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java
T
sasheg dev 966a144a64 Тик мода должен идти по игровому тику, HUD должен виден при загрузке
Тик. Моды тикали из покадрового хука, то есть получали столько тиков,
сколько кадров успевает нарисовать машина. На 200 fps это в семь раз
быстрее игры: таймер мода уезжал, а счётчик тиков модов расходился с
миром, поверх которого он нарисован. Теперь тик вызывается рядом с
runTick(), внутри того же цикла, так что у мода ровно двадцать шагов в
секунду независимо от частоты кадров.

HUD. Экран загрузки мира рисует сам, мимо хвоста кадра. Пока генерируется
ландшафт, LoadingScreenRenderer рисует в собственный framebuffer, блитит
его в окно и сам вызывает updateDisplay() — до десяти раз в секунду, из
потока, который занят генерацией. Всё, что нарисовано из покадрового хука,
стирается следующим таким выводом, поэтому оверлей пропадал на всю
загрузку и возвращался уже в мире. Добавлен отдельный вход в тот же код
рисования оверлеев, вызываемый после блита и до вывода в окно.

Заодно разделены две ошибки в RsxBootstrap: сбой тика и сбой отрисовки
больше не выключают друг друга, у каждого своя защита.

Проверено вживую: оверлей виден на экране генерации ландшафта.
2026-10-19 01:02:30 +03:00

238 lines
8.3 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() {
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);
}
}
}