Skip to content

Rinku 3.0.4: краш GUI на 26.2, несуществующий флаг Chromium, dev-сервер и объявление зависимости - #33

Closed
KoSHeroff wants to merge 4 commits into
chore/deps-refresh-mc26-stablefrom
fix/dev-server-26
Closed

KoSHeroff wants to merge 4 commits into
chore/deps-refresh-mc26-stablefrom
fix/dev-server-26

Conversation

@KoSHeroff

@KoSHeroff KoSHeroff commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

Closes #28. Closes #29. Closes #31. Closes #34.

Поверх #30 — там переименование slug на rinku, без которого зависимость не резолвится. Мержить после него.

Вобрал в себя #32: тот PR отдельно нежизнеспособен, потому что миграция на Rinku 3 ломает runServer на 1.21.11-neoforge, а чинит это уже коммит про dev-сервер. Разделять их значило оставить в истории коммит с красным CI.

Что чинится

было стало
Краш клиента на 26.2 после нескольких открытий GUI падал на 4-м цикле 25/25
Игра на NeoForge без браузерной библиотеки краш на старте понятный экран FML + лаунчер ставит сам
Браузер на современном Chromium INITIALIZATION_FAILED работает
runServer на 26.x и 1.21.11-neoforge не стартовал стартует

Четыре находки

1. Краш GUI на 26.2 (#31). Texture view Sampler0 (MCEF Browser Texture 1x1) has been closed! — детерминированно на 4-м открытии, независимо от задержки перед закрытием. Rinku 3.0.4 его снимает; обходной путь с browser-preload-enabled=false больше не нужен.

2. Мина в нашем коде. CefUtilMixin подмешивал в CefApp.startup флаг --use-gl=desktop, удалённый из Chromium несколько лет назад. Пока Rinku тянул старый Chromium, он проходил незамеченным; на Chromium 151 валит инициализацию CEF целиком. Рвануло бы при любом обновлении Chromium, независимо от этой миграции.

3. Игра падала на старте на NeoForge (#34). Воспроизведено на релизной 1.6.2 с Modrinth в Prism: библиотека не была объявлена нигде — ни в neoforge.mods.toml, ни в метаданных Modrinth, ни вложена в jar. Лаунчер не ставил ничего, FML не знал, что мод нужен, пользователь получал NoClassDefFoundError: org/cef/handler/CefDisplayHandler вместо экрана «не хватает мода». Fabric не затронут — там библиотека внутри jar.

4. dev runServer (#29). Rinku попадала в runtime обоих dev-ранов. Аккуратного решения не нашлось: moddev убрал per-run classpath на современных Minecraft. Теперь она не на classpath вовсе, а приезжает в клиентский ран jar'ом в run/mods — что заодно точнее моделирует продакшн.

Миграция API

com.cinemamod.mcef.MCEF → de.keksuccino.rinku.Rinku, MCEFBrowser → RinkuBrowser, цель миксина → de.keksuccino.rinku.util.CefUtil, getTextureLocation() → getTextureIdentifier(), свойство mcef_version → rinku_version.

Обошлось 12 файлами механической замены: RinkuInitListener — функциональный интерфейс, RinkuBrowser extends CefBrowserOsr, CefUtil.init() всё ещё зовёт CefApp.startup(String[]).

Обход апстримной непоследовательности

NeoForge-сборки Rinku 3.0.4 упакованы по-разному: у 26.2 классы сверху, у 1.21.1, 1.21.11 и 26.1.2 это jarJar-обёртки. В рантайме NeoForge распаковывает сам, javac видит пустой jar — сборка компилируется против того, где реально лежат классы.

CI

smoke: true добавлен обоим таргетам MC 26. Баг с dev-сервером прожил месяцы ровно потому, что там никто не поднимал сервер.

Проверено

Все 7 таргетов собираются. Стендом mc-webgui/testkit, по 10/10 на 1.21.11-fabric и 26.2-neoforge: CEF инициализируется, страница грузится, мост инжектится, каналы page→game доходят, GUI переоткрывается без краша, скриншот снимается. Серверы: 26.2, 26.1.2, 1.21.11-neoforge, 1.21.1-neoforge — все стартуют.

Не проверено: подтянет ли лаунчер Rinku по объявленной зависимости на реальной установке. Локально подтверждено только то, что зависимость попала в jar.

Версия

mod_version = 1.7.0, CHANGELOG заполнен. Мерж в main запустит публикацию.

Кирилл Гринев added 3 commits September 8, 2026 19:02
Fixes the 26.2 client crash: with Rinku 2.2.1 a GUI open/close loop died
on the fourth iteration with "Texture view Sampler0 (MCEF Browser
Texture 1x1) has been closed!", and on 3.0.4 the same loop runs 25 times
with the browser preload pool left enabled.

The API move is mostly mechanical — com.cinemamod.mcef.MCEF becomes
de.keksuccino.rinku.Rinku, MCEFBrowser becomes RinkuBrowser, the mixin
follows CefUtil to de.keksuccino.rinku.util, and getTextureLocation is
now getTextureIdentifier. Three things kept it small: RinkuInitListener
is a functional interface so the scheduleForInit lambdas are untouched,
RinkuBrowser still extends CefBrowserOsr so setFocus/getURL/
executeJavaScript/loadURL/setZoomLevel come along for free, and
CefUtil.init still calls CefApp.startup(String[]) so the GPU-flag
ModifyArg survives the move. mcef_version is renamed to rinku_version
since MCEF is gone from the project entirely.

CefUtilMixin no longer injects --use-gl=desktop. That flag was removed
from Chromium years ago; it slipped through while Rinku shipped an older
build, but against Chromium 151 it fails CefApp.startup outright, so CEF
reached INITIALIZATION_FAILED and no browser was ever created. It was
our own mine, not something the migration broke — any Chromium bump
would have set it off.

Rinku publishes its NeoForge builds inconsistently: 26.2 carries classes
at the top level while 1.21.1, 1.21.11 and 26.1.2 are jarJar wrappers
whose real mod sits in META-INF/jarjar/. NeoForge unpacks that at
runtime, but javac sees an empty jar, so the build now compiles against
whatever actually holds the classes and leaves the published artifact on
the runtime classpath untouched.

All seven targets build. Verified end to end on 1.21.11-fabric and
26.2-neoforge: CEF initializes, the page loads, the bridge is injected,
page-to-game channels arrive, and the GUI reopens without crashing.

Closes #28
Closes #31
Both Minecraft 26 dev servers died during mod loading:

    ModLoadingException: Rinku (rinku) has failed to load correctly
      java.lang.NoClassDefFoundError: net/minecraft/client/gui/screens/Screen

Rinku is a client-side library, and NeoForge's dev dist cleaner strips
the client classes it constructs against. A plain runtimeOnly dependency
feeds both dev runs, so runServer loaded it and died. Production was
never affected — WebGUI does not declare Rinku in neoforge.mods.toml, so
a real server never loads it — which is why this only ever bit
developers, and only on 26.x where Rinku's builds are not dist-safe.

The obvious fix does not work: moddev has dropped per-run classpaths on
modern Minecraft, and clientAdditionalRuntimeClasspath answers "no
additional classpath anymore for Minecraft 1.21.11". So Rinku now stays
off the runtime classpath entirely and reaches the client run as a jar
in its mods folder, installed by a task bound to runClient alone. That
also matches how the mod is actually used: a player installs Rinku, a
server operator does not.

Minecraft 26 gets the server smoke test in CI as well. The dev server
was broken on both 26 targets for months only because nothing ever
booted one there.

Verified: both 26 servers reach "Done" with WebGUI initialised, 1.21.1
and 1.21.11 servers still do, and the 26.2 client still brings up
Chromium and the page bridge with Rinku coming from the mods folder.

Closes #29
Covers the whole stack: the Rinku rename and dependency refresh, the
Rinku 3.0.4 migration with its 26.2 crash fix, and the dev server fix.

The upgrade is not transparent for everyone, so the entry says who has
to act. Rinku's mod id changed from mcef to rinku, and WebGUI bundles it
on Fabric via jar-in-jar but not on NeoForge — so Fabric players do
nothing while NeoForge players must swap the jar in their mods folder.

The README claimed NeoForge "needs no extra dependencies", which was
never true: nothing bundles the browser library there. The installation
steps now say so per loader.
@KoSHeroff

Copy link
Copy Markdown
Member Author

Добавил в эту же ветку CHANGELOG и бамп версии до 1.7.0 — запись покрывает весь стек (#30, #32, #33).

По ходу вскрылось расхождение, которое стоит отметить отдельно: Rinku бандлится в jar WebGUI только на Fabric (include(...) → META-INF/jars/), а на NeoForge не бандлится ничего. Значит апгрейд не прозрачный: игрокам на Fabric делать нечего, а на NeoForge придётся заменить jar MCEF на Rinku, иначе браузера не будет — id мода сменился с mcef на rinku.

Заодно поправил README: там значилось «NeoForge needs no extra dependencies», что не было правдой никогда — браузерную библиотеку туда всегда надо ставить руками.

Installing WebGUI alone on NeoForge kills the game during mod loading:

    @mixin target com.cinemamod.mcef.CefUtil was not found
    NoClassDefFoundError: org/cef/handler/CefDisplayHandler
      at land.webgui.WebGUIMod.<init>

Reproduced on the released 1.6.2 from Modrinth with Prism, so this is
not a regression — it is shipping today. WebGUI declared the browser
library nowhere: not in neoforge.mods.toml, not in the Modrinth version
metadata, and nothing is bundled into the NeoForge jar. So the launcher
installs nothing, FML does not know the mod is needed, and the player
gets a crash report instead of a missing-dependency screen.

Fabric was never affected: there the library rides inside the jar via
jar-in-jar, which is why it feels like it installs itself.

Declaring it required does both jobs — launchers resolve and install
Rinku, and a hand-copied install fails with FML's readable screen rather
than a stack trace. Bundling it with jarJar the way Fabric does is worth
considering too, but Rinku's NeoForge artifacts are themselves jarJar
wrappers on several versions, so that needs more care than a one-liner.

Refs #34
@KoSHeroff KoSHeroff changed the title Убрать Rinku из серверного dev-рана — чинит runServer на MC 26 Rinku 3.0.4: краш GUI на 26.2, несуществующий флаг Chromium, dev-сервер и объявление зависимости Sep 8, 2026
@KoSHeroff
KoSHeroff changed the base branch from feat/rinku-3 to chore/deps-refresh-mc26-stable September 8, 2026 14:41
@KoSHeroff
KoSHeroff deleted the branch chore/deps-refresh-mc26-stable September 8, 2026 14:46
@KoSHeroff KoSHeroff closed this Sep 8, 2026
@KoSHeroff

Copy link
Copy Markdown
Member Author

GitHub закрыл этот PR автоматически: его базовая ветка chore/deps-refresh-mc26-stable была удалена при мерже #30. Содержимое переехало в новый PR без изменений — та же ветка fix/dev-server-26, те же 4 коммита, база main.

KoSHeroff added a commit that referenced this pull request Sep 9, 2026
…tools (#38)

* Keep Rinku out of the server run once the client has run

The fix in #33 was incomplete. It kept Rinku off the runtime classpath
and installed it into the client run's mods folder instead — but both
dev runs share versions/<target>/run, so once anyone has started the dev
client, the jar sitting in mods/ is loaded by the dev server too and it
dies exactly as before:

    ModLoadingException: Rinku (rinku) has failed to load correctly
      java.lang.NoClassDefFoundError: net/minecraft/client/gui/screens/Screen

That is why it looked fixed: the run directories were untouched when I
verified it, so mods/ was empty and the server booted. It breaks again
the moment a developer runs the client first, which is always.

Separate game directories per run would be tidier, but they move worlds,
configs and log paths, and every tool around them. Deleting
the jar before the server starts says what is actually meant — the server
must not see Rinku — and touches nothing else.

Refs #29

* Add an opt-in custom death screen

Set deathScreenUrl in server.json and the vanilla death screen is
replaced by that page; leave it empty and nothing changes. Opt-in on
purpose: replacing that screen takes away the player's only way to
respawn, so it happens only when a server explicitly asks for it.

The page is told what happened through a webgui:death event — killer
(player, mob or environment), damage cause, the vanilla death message,
and hardcore/canRespawn flags — and gets window.webgui.respawn() to
stand in for the button it replaced.

Where to hook in was the main design decision. The obvious target is the
death packet handler, but that is onDeathMessage on Yarn and
handlePlayerCombatKill on Mojang mappings and has moved between
versions, which is the shape of target that has broken MouseMixin twice.
Instead Fabric injects into MinecraftClient.setScreen, which has one
signature across every Fabric target here, and NeoForge uses
ScreenEvent.Opening and needs no mixin at all — so neither side carries a
single version conditional, and 26.2 renaming setScreen to
setScreenAndShow costs nothing.

Three details that only showed up in a running game:

The url is pushed on join, not at death. Deciding whether to suppress the
vanilla screen has to happen the instant it opens, and learning the url
at death time races the vanilla death packet.

The death payload is held on the client and replayed after the document
loads. It arrives while the page is still being created, so an immediate
emit lands before any listener exists.

Respawn does not clear the screen itself. The client still counts as dead
until the server answers, so setScreen(null) makes vanilla reopen the
death screen, which this very interceptor then replaces with a second
browser nothing closes. Vanilla closes the screen on its own once the
respawn lands.

The screen is also left alone while Chromium is still starting.
WebViewScreen closes itself when there is no browser, vanilla reopens the
death screen because the player is still dead, and replacing it again
recursed until the client crashed — reachable by dying in the first
seconds of a session.

Verified in a running game on all seven targets: real dedicated server,
join, death, screen replacement, killer data in the page, respawn from
the page, and the browser actually released afterwards.

* Report the killer's entity type as a registry id

entityType came from EntityType.toString(), which yields the translation
key — "entity.minecraft.zombie". A page matching on the killer wants the
id it would use anywhere else in the game, so send "minecraft:zombie".

Yarn spells the lookup EntityType.getId and Mojang mappings spell it
getKey; both are static and both return the identifier.

Verified on 1.21.11-fabric and 26.2-neoforge with a real attributed mob
kill, since the two mappings build this payload through separate code.

* Leave the death payload on window.webgui.death

The death event fires once, right after the document loads, so a page whose
bundle only subscribes from a mounted component never hears it. Snapshot it on
the namespace the way client and entity already are, and the late subscriber
has something to read.

* Tell clients what the server runs instead of refusing them

A NeoForge server registered its channels as required, so a client without
WebGUI - or with an older build of it - was disconnected with "Incompatible
client! Please use NeoForge <version>", which names a mod that is not the
problem. Fabric did the opposite and admitted the player, then silently dropped
every packet, so nothing opened and nothing said why.

The channels are optional now. The server introduces itself on join, and a
client whose protocol differs gets a message naming both versions. Servers that
really must require the mod set requireClientMod and get a kick message that
explains itself.

Two things found along the way: the death screen URL was pushed on join on
Fabric but not on NeoForge, where it only arrived with the death packet itself,
and Fabric pushed it twice.

Also caps page events per player per second. That channel is the only thing a
client can push at the server, every message is dispatched on the server thread,
and a page is web content - a loop in someone's JavaScript can fire it as fast
as the socket allows.

* release: 1.7.1

* Let a server host its own pages

Getting an interface in front of players meant finding a web host, a domain and
a deploy pipeline before you could change a button. Now the files go in
config/webgui/web/ and a URL setting points at them with webgui:/index.html.

They travel down the connection the player is already on, so there is no port to
open and it works behind NAT like everything else a server sends. Content is
addressed by SHA-256: a file the client already has is never fetched again, and
a changed file is a different name, so a page cannot be stale. External hosting
is untouched - http(s) URLs behave exactly as before and the two mix freely.

The browser reads them over loopback rather than a custom scheme. A scheme would
avoid the socket, but non-standard schemes are second-class in Chromium and
debugging that through a game window is miserable. The port is fixed by default
because a bundled page's origin is what a backend has to name in CORS, and one
that moved every launch could not be configured at all.

Serving the pages ourselves also fixes something older: window.webgui was
injected only after a document finished loading, so a plain inline script
calling it threw "Cannot read properties of undefined" - only pages that waited
for DOMContentLoaded ever worked. Injecting at load start is not enough on its
own, because executeJavaScript is a message to another process and the page's
own scripts routinely win that race. For pages served here the bridge is woven
into the document itself, which cannot race.

Limits, so a mistake here cannot take a server down: 8 MiB per file, 64 MiB in
total, 2000 files, a file list that fits one packet, and a per-player bandwidth
budget alongside the existing page-event one.

* Let a page hand the player a file, and take one from them

Both were dead controls. CEF asks the host where to put a download and, hearing
nothing, throws it away, so an invoice or a CSV export was unclickable. A
windowless browser cannot put up its own file chooser either, so
<input type="file"> did nothing at all and gave no hint why.

Downloads land in webgui-downloads/ under the game directory and the player is
told the name, because a file that arrives with no trace is one they will never
find. The page does not choose where its bytes go: the suggested name is the one
part of a download web content fully controls, so the path is stripped out of it,
refused characters are replaced, and names Windows reserves are sidestepped -
writing to CON succeeds and goes nowhere, which would look like the download
working and leaving nothing behind.

The chooser is LWJGL's, not Swing's: Rinku boots AWT headless so the game can own
the window, and JFileChooser throws outright in that state.

Both naming rules live in WebviewDownloadNames, apart from the handlers that use
them. They are the only thing between attacker-controlled text and a filesystem
call, and free of browser classes they can be tested on every version - the
handlers themselves cannot even be loaded on some of them.

Popups are deliberately not handled. CEF documents a callback for it, but JCEF
never routes a popup to it on a windowless browser: with Chromium's popup blocker
off and a page calling window.open outright, onBeforePopup did not fire once.
Pages written for this environment can navigate with location.href instead.

* Show an icon and an author in the Mods list, and declare Rinku everywhere

The Mods list is where a player looks to see what they installed and who wrote
it, and WebGUI had no icon on either loader and no author on NeoForge at all.
The icon is the project logo rasterised from the site's SVG, shipped at 128 and
256 because the two loaders ask for different sizes.

More importantly, no CurseForge upload declared any dependency - including the
NeoForge ones, which need Rinku installed separately. A NeoForge player who
downloaded from CurseForge got no Rinku and the game died during mod loading
with NoClassDefFoundError: exactly the failure the Modrinth declaration was
added to prevent for 1.7.0, missed on the other platform.

Fabric uploads now say rinku(embedded) rather than nothing. Rinku ships inside
the Fabric jar, so asking a launcher to install it would fetch a second copy;
embedded states the relationship without that.

Drops the native file chooser. It took the client down with an access violation
inside libcef's message loop - twice, and not from the thread it ran on: moving
it to the render thread changed nothing, and the same crash then happened with
no file dialog involved at all. Five clean single-client runs could not
reproduce it, so the cause is still unknown, and a whole-client crash is not
something to ship on the strength of "it did not happen to me".

Downloads stay. They are verified repeatedly, by hand and unattended, and were
never part of a crash.

* Tell the page the field of view

A page already knows where the player is and which way they are facing, and it can
read its own viewport, but not the field of view - that lives in the player's
settings and nowhere a page can reach. Without it there is no way to turn a point
in the world into a point on the screen, so anything anchored to the world was out
of reach.

It is the setting, not the momentary value. The renderer stretches the field of
view while sprinting or under a speed effect, and following that would mean reading
the camera every frame; a marker placed from this is exact standing still and drifts
a little on the move.

Verified on all seven targets by writing a non-default fov into options.txt and
requiring the matching degrees in the page - asserting a plausible number would
have passed on a hardcoded default.

* Fix client data never updating after the page opened on NeoForge 1.21.11

A page there received exactly one payload — the one pushed when its document
loaded — and then nothing, ever. Position, health, game mode: frozen at whatever
they were when the page opened, with nothing in the log to say so. From the
outside it looked like a page that had stopped re-rendering, which is the wrong
place to go looking.

The per-tick push asks whether the browser's texture is ready, and on NeoForge
that test was getRenderer().getTextureID() != 0. Rinku 3 keeps the texture as a
Blaze3D GpuTexture, which need not have a raw GL id at all; on this target the id
stayed 0, so the test never passed. The document-load push skips the check
entirely, which is why a page got one payload and no more.

Both loaders now use isTextureReady(), Rinku's own accessor, which the Fabric side
was already using.

Caught by a check that changes the world and expects the payload to
follow, where every earlier test was satisfied by a single payload and passed.

* Tell the page what the crosshair is on

Alongside where the player is and which way they face, a page can now ask what
they are actually looking at: a block with its registry id, whole coordinates and
the face being approached, or an entity with its type, name and uuid, each with
the distance to the point of contact.

This is the game's own answer — the one it uses to draw the block outline and the
name above a mob — not a second ray cast with its own idea of reach, which would
disagree with what the player sees.

type is always present, "none" included. A field that disappears makes every page
write the same optional-chaining dance before it can ask the only question that
matters.

Verified on all seven targets against a world the test builds itself: the player is
teleported to a known spot facing a known direction, a gold block goes exactly three
away, and the payload has to name those coordinates, that block and the south face.
Then the block is replaced by a zombie, and finally the player looks at the sky.

* Apply the whole config on /webgui reload

The bundled page list was only ever sent when a player joined, so after a
reload the server served new bytes while the client still checked them
against the old hash. trustedCommandOrigins and mainMenuUrl were never
re-sent at all: dropping an origin from the trusted list did nothing for
anyone already online until they reconnected.

Join and reload now go through one sendConfigSnapshot, so they cannot
drift apart again.

The client keeps its session token for the life of the session instead of
minting a new one per manifest — rotating it moved the origin out from
under every open page, which turned a reload into a 404. Leaving a world
still clears it, so a page from the previous server cannot read the next
one's files. An open bundled page is reloaded when the revision actually
changed; pages from a web host are left alone.

Also rescan the web folder when server.json is missing or empty, since
pages are served by default and the folder can still have changed.

* Say in the log why a page asset could not be served

A bundled page whose build emitted absolute paths came up blank with
nothing anywhere to explain it: the 404 went to the browser, and the only
trace on our side was a debug line nobody has enabled. Both misses now
warn once per distinct path, name the document that asked, and say that a
relative base is the fix.

resolve() also stopped blaming the wrong thing. It reported "this server
ships no pages" whether the server shipped none or no loopback port was
free, which sent an operator to check a config file that was fine.

* Add a settings screen with page developer tools

The mod had no settings of its own and nowhere to put any: NeoForge's mod
list shows a Config button when a mod registers a screen factory, and on
Fabric Mod Menu does the same, so both now open one.

The first setting on it turns Chromium's own view of the page into log
lines - console calls with the file and line that made them, uncaught
exceptions, failed requests with their url and status, and the browser
warnings nobody could see before. A page in the game has no inspector,
and a blank one left a developer with nothing at all to go on.

It reads the DevTools protocol, which needs neither a window nor a
debugging port: openDevTools() wants a desktop window a game does not
have, and the real inspector needs --remote-debugging-port, which is
fixed on the command line before any mod runs. That argument does work,
so the screen reports whether someone passed it and prints the address to
open. Capped at 20 messages a second, and off by default.

Also stop advising a rebuild when Chromium asks the origin root for
favicon.ico, which it does on every page and which is nobody's mistake.

* release: 1.8.0

Everything since 1.7.0 ships as one version. 1.7.1 was prepared but never
published, so its entries belong here rather than to a version nobody
could install.

Also documents what the browser does not do - window.open, and what
window.webgui.isHud is for - and lists the limitations worth knowing
before installing rather than after.

---------

Co-authored-by: Кирилл Гринев <k.grinev@pravo.tech>
@KoSHeroff
KoSHeroff deleted the fix/dev-server-26 branch September 9, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant