Each button sends one message. The first four land in the game log; + watch it, not this page.
+Client data is pushed as the game plays. Move, look around or take damage + and these should change.
+Bind one with
+ /webgui bind entity @e[type=pig,limit=1] "webgui:bridge.html",
+ then right-click the pig.
Everything the game dispatches: webgui:client,
+ webgui:entity, webgui:death and anything a server emits.
waiting for the death payload…
+ ++ Three things are under test here: that this page replaces the vanilla screen at all, + that the death payload arrives even though it is sent while the page is still loading, + and that Escape is refused while it is up. +
+The engine saves the file itself, into webgui-downloads beside the game.
+ The mod only says so: a line in the game log and a message in chat.
The name a page suggests is stripped to its last part and cleaned before it + is used — it arrives from outside and is about to become a path.
+No network involved, so this works on a server that ships pages and nothing + else.
+The last one suggests
+ ../../escape me.txt — it must land in the downloads folder under a cleaned
+ name, not anywhere else.
Needs the game to have internet.
+
+ Open with /webgui hud @s webgui:hud.html.
+ Press ` to make it interactive — the button below only works then.
+
Each page checks one part of the mod. Open with
+ /webgui gui @s webgui:index.html.
+ The links below are written as webgui://…. They are the mod's own scheme,
+ which the browser knows nothing about — the bridge rewrites them to the local page
+ server. If they navigate, that rewriting works.
+
deathScreenUrl to webgui://death.html, then die
+
+
+ HUD page →
+ open with /webgui hud — see-through, glanceable
+
+ Letters, digits, backspace, arrows, Home/End, Ctrl+A, Ctrl+C/V. + Escape should close the screen rather than reach the field.
++ + — keydown, keypress, beforeinput, input, keyup; one line each +
+Enter in a single-line field should submit the form rather than type + anything — the browser's own behaviour, and a different path from the newline a + textarea gets.
+ +Double click selects a word, triple click a line — the game reports each + press on its own, so the count is worked out by the mod.
+The quick brown fox jumps over the lazy dog, twice before breakfast.
+A native select. Chromium paints this into a surface of its own, so it + checks that popups are drawn over the page at all.
+ + +Hover each box. The name is decided by the engine, not by this page: + java-cef turns Chromium's cursor into one of AWT's fourteen before the mod sees it, + so anything AWT has no name for arrives as a plain arrow and cannot be recovered. + The boxes marked n/a are expected to stay arrows.
+Spin the wheel. Speed is a setting — mod list → WebGUI → Config.
+Reached whenever a URL is missing rather than wrong: the main-menu key pressed on the
+ * title screen, where no server has sent its settings yet, or a server that opened a page
+ * without naming one. The mod's own site explains what to set; it used to be
+ * {@code example.com}, which explained nothing and looked like a bug.
+ */
public final class StartUrls {
- public static final String HTTPS_FALLBACK = "https://example.com";
+
+ public static final String HTTPS_FALLBACK = "https://webgui.space";
private StartUrls() {}
diff --git a/src/main/java/land/webgui/WebGUIAssetServer.java b/src/main/java/land/webgui/WebGUIAssetServer.java
index 20b6f3f..bb3d028 100644
--- a/src/main/java/land/webgui/WebGUIAssetServer.java
+++ b/src/main/java/land/webgui/WebGUIAssetServer.java
@@ -11,6 +11,7 @@
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.HexFormat;
+import java.util.List;
import java.util.concurrent.Executors;
/**
@@ -79,6 +80,20 @@ public static synchronized void ensureStarted() {
if (server != null) {
return;
}
+ // HttpServer.start() spawns its dispatcher thread internally, inheriting the daemon
+ // flag of the caller. Bound from the render thread it would keep the JVM alive after
+ // the player quits. Joined at once, so callers still get a started server.
+ Thread binder = new Thread(WebGUIAssetServer::bind, "webgui-assets-bind");
+ binder.setDaemon(true);
+ binder.start();
+ try {
+ binder.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private static void bind() {
for (int candidate = DEFAULT_PORT; candidate < DEFAULT_PORT + PORT_ATTEMPTS; candidate++) {
try {
// Explicitly IPv4, not getLoopbackAddress(): with IPv6 present that
@@ -119,23 +134,34 @@ private static void rotateToken() {
sessionToken = HexFormat.of().formatHex(raw);
}
+ /** The schemes a page may be opened from. Everything else is refused. */
+ private static final List A method of its own because the helper was renamed when Fabric API dropped "key
+ * binding" for "key mapping" in 26 — and because a Stonecutter marker inside an
+ * expression is not a thing the preprocessor can make sense of.
+ */
+ //? if >=26 {
+ /*private static KeyMapping bind(KeyMapping mapping) {
+ return KeyMappingHelper.registerKeyMapping(mapping);
+ }
+ *///? } else {
+ private static KeyMapping bind(KeyMapping mapping) {
+ return KeyBindingHelper.registerKeyBinding(mapping);
+ }
+ //? }
+ //? }
+
//? if fabric {
public static void register() {
//? if >=1.21.5 {
- KeyBinding.Category cat = KeyBinding.Category.create(Identifier.of(WebGUIMod.MOD_ID, "web"));
- mainMenu = KeyBindingHelper.registerKeyBinding(new KeyBinding(
+ KeyMapping.Category cat = KeyMapping.Category.register(Identifier.fromNamespaceAndPath(WebGUIMod.MOD_ID, "web"));
+ mainMenu = bind(new KeyMapping(
"key.webgui.main_menu",
- InputUtil.Type.KEYSYM,
- GLFW.GLFW_KEY_F6,
+ KEY_TYPE,
+ WebGUIKeys.KEY_F6,
cat));
- hudInteractive = KeyBindingHelper.registerKeyBinding(new KeyBinding(
+ hudInteractive = bind(new KeyMapping(
"key.webgui.hud_interactive",
- InputUtil.Type.KEYSYM,
- GLFW.GLFW_KEY_GRAVE_ACCENT,
+ KEY_TYPE,
+ WebGUIKeys.KEY_GRAVE,
+ cat));
+ devtools = bind(new KeyMapping(
+ "key.webgui.devtools",
+ KEY_TYPE,
+ WebGUIKeys.KEY_F12,
cat));
//? } else {
- /*mainMenu = KeyBindingHelper.registerKeyBinding(new KeyBinding(
+ /*mainMenu = bind(new KeyMapping(
"key.webgui.main_menu",
- InputUtil.Type.KEYSYM,
- GLFW.GLFW_KEY_F6,
+ KEY_TYPE,
+ WebGUIKeys.KEY_F6,
WebGUIMod.MOD_ID + ".web"));
- hudInteractive = KeyBindingHelper.registerKeyBinding(new KeyBinding(
+ hudInteractive = bind(new KeyMapping(
"key.webgui.hud_interactive",
- InputUtil.Type.KEYSYM,
- GLFW.GLFW_KEY_GRAVE_ACCENT,
+ KEY_TYPE,
+ WebGUIKeys.KEY_GRAVE,
+ WebGUIMod.MOD_ID + ".web"));
+ devtools = bind(new KeyMapping(
+ "key.webgui.devtools",
+ KEY_TYPE,
+ WebGUIKeys.KEY_F12,
WebGUIMod.MOD_ID + ".web"));*/
//? }
}
@@ -72,83 +109,51 @@ public static void register() {
private static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
//? if >=1.21.5 {
KeyMapping.Category cat = KeyMapping.Category.register(Identifier.fromNamespaceAndPath(WebGUIMod.MOD_ID, "web"));
- mainMenu = new KeyMapping("key.webgui.main_menu", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_F6, cat);
- hudInteractive = new KeyMapping("key.webgui.hud_interactive", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_GRAVE_ACCENT, cat);
+ mainMenu = new KeyMapping("key.webgui.main_menu", KEY_TYPE, WebGUIKeys.KEY_F6, cat);
+ hudInteractive = new KeyMapping("key.webgui.hud_interactive", KEY_TYPE, WebGUIKeys.KEY_GRAVE, cat);
+ devtools = new KeyMapping("key.webgui.devtools", KEY_TYPE, WebGUIKeys.KEY_F12, cat);
//? } else {
- mainMenu = new KeyMapping("key.webgui.main_menu", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_F6, WebGUIMod.MOD_ID + ".web");
- hudInteractive = new KeyMapping("key.webgui.hud_interactive", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_GRAVE_ACCENT, WebGUIMod.MOD_ID + ".web");
+ mainMenu = new KeyMapping("key.webgui.main_menu", KEY_TYPE, WebGUIKeys.KEY_F6, WebGUIMod.MOD_ID + ".web");
+ hudInteractive = new KeyMapping("key.webgui.hud_interactive", KEY_TYPE, WebGUIKeys.KEY_GRAVE, WebGUIMod.MOD_ID + ".web");
+ devtools = new KeyMapping("key.webgui.devtools", KEY_TYPE, WebGUIKeys.KEY_F12, WebGUIMod.MOD_ID + ".web");
//? }
event.register(mainMenu);
event.register(hudInteractive);
+ event.register(devtools);
}*/
//? }
- //? if fabric {
- public static void tick(MinecraftClient client) {
- //? } else {
- /*public static void tick(Minecraft client) {*/
- //? }
- //? if fabric {
- while (mainMenu.wasPressed()) {
- //? } else {
- /*while (mainMenu.consumeClick()) {*/
- //? }
+ public static void tick(Minecraft client) {
+ while (mainMenu.consumeClick()) {
tryOpenMainMenu(client);
}
- //? if fabric {
- while (hudInteractive.wasPressed()) {
- if (!WebHudOverlay.isHudVisible() || client.currentScreen != null) {
- //? } else {
- /*while (hudInteractive.consumeClick()) {
- //? if >=26.2-neoforge {
- if (!WebHudOverlay.isHudVisible() || client.gui.screen() != null) {
- //? } else {
- if (!WebHudOverlay.isHudVisible() || client.screen != null) {
- //? }*/
- //? }
+ while (hudInteractive.consumeClick()) {
+ if (!WebHudOverlay.isHudVisible() || Mc.screen(client) != null) {
continue;
}
WebHudOverlay.toggleInteractive(client);
}
- }
-
- //? if fabric {
- private static void tryOpenMainMenu(MinecraftClient client) {
- if (client.currentScreen instanceof WebViewScreen) {
- return;
+ while (devtools.consumeClick()) {
+ // The screen has its own F12: a page in a screen gets the key before any key
+ // binding does, so this is only ever the HUD.
+ WebGUIDevToolsWindow.open(WebSession.hudBrowser());
}
- if (!Rinku.isInitialized()) {
- if (client.player != null) {
- client.player.sendMessage(Text.translatable("message.webgui.mcef_not_ready"), false);
- }
- return;
- }
- client.setScreen(new WebViewScreen(WebGUIMainMenuUrl.getUrl()));
}
- //? } else {
- /*private static void tryOpenMainMenu(Minecraft client) {
- //? if >=26.2-neoforge {
- if (client.gui.screen() instanceof WebViewScreen) {
- //? } else {
- if (client.screen instanceof WebViewScreen) {
- //? }
+
+ private static void tryOpenMainMenu(Minecraft client) {
+ if (Mc.screen(client) instanceof WebViewScreen) {
return;
}
- if (!Rinku.isInitialized()) {
+ if (!WebBrowser.ready()) {
if (client.player != null) {
//? if >=26 {
- client.player.sendSystemMessage(Component.translatable("message.webgui.mcef_not_ready"));
- //? } else {
+ /*client.player.sendSystemMessage(Component.translatable("message.webgui.mcef_not_ready"));
+ *///? } else {
client.player.displayClientMessage(Component.translatable("message.webgui.mcef_not_ready"), false);
//? }
}
return;
}
- //? if >=26.2-neoforge {
- client.gui.setScreen(new WebViewScreen(WebGUIMainMenuUrl.getUrl()));
- //? } else {
- client.setScreen(new WebViewScreen(WebGUIMainMenuUrl.getUrl()));
- //? }
- }*/
- //? }
+ Mc.openScreen(client, new WebViewScreen(WebGUIMainMenuUrl.getUrl()));
+ }
}
diff --git a/src/main/java/land/webgui/WebGUIRemoteDebugging.java b/src/main/java/land/webgui/WebGUIRemoteDebugging.java
index 447547f..01c2442 100644
--- a/src/main/java/land/webgui/WebGUIRemoteDebugging.java
+++ b/src/main/java/land/webgui/WebGUIRemoteDebugging.java
@@ -7,7 +7,8 @@
* arguments it does not recognise straight through, so {@code --remote-debugging-port}
* on the game's own command line is enough to get the full inspector.
*
- * Reported rather than set: by the time any mod runs, that command line is fixed.
+ * The engine is ours, so the mod can also choose the port itself when the settings screen
+ * asks for the tools; {@link #inUse()} is the one Chromium actually got, whichever way.
*/
public final class WebGUIRemoteDebugging {
@@ -17,8 +18,25 @@ public final class WebGUIRemoteDebugging {
private static Integer cached;
+ /** The port Chromium was started with, which is not known until it starts. */
+ private static volatile int inUse;
+
private WebGUIRemoteDebugging() {}
+ /** Told by the boot once Chromium has been configured. */
+ public static void started(int port) {
+ inUse = Math.max(0, port);
+ }
+
+ /** The port the inspector is actually on, or 0 while there is none. */
+ public static int inUse() {
+ return inUse;
+ }
+
+ public static boolean reachable() {
+ return inUse > 0;
+ }
+
/** The port the inspector is listening on, or 0 when the argument was not given. */
public static synchronized int port() {
if (cached == null) {
@@ -33,8 +51,7 @@ public static boolean enabled() {
/** The address to open in a browser, or an empty string when it is off. */
public static String address() {
- int port = port();
- return port > 0 ? "http://127.0.0.1:" + port : "";
+ return inUse > 0 ? "http://127.0.0.1:" + inUse : "";
}
private static int findPort() {
diff --git a/src/main/java/land/webgui/WebGUISettingsScreen.java b/src/main/java/land/webgui/WebGUISettingsScreen.java
index c896cdb..3637acb 100644
--- a/src/main/java/land/webgui/WebGUISettingsScreen.java
+++ b/src/main/java/land/webgui/WebGUISettingsScreen.java
@@ -1,195 +1,189 @@
package land.webgui;
-//? if fabric {
-import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.gui.screen.Screen;
-import net.minecraft.client.gui.widget.ButtonWidget;
-import net.minecraft.text.Text;
-//? } else {
-/*//? if >=26 {
-import net.minecraft.client.gui.GuiGraphicsExtractor;
-//? } else {
+import land.webgui.compat.Mc;
+
+//? if >=26 {
+/*import net.minecraft.client.gui.GuiGraphicsExtractor;
+*///? } else {
import net.minecraft.client.gui.GuiGraphics;
//? }
import net.minecraft.client.gui.components.Button;
+import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.screens.Screen;
-import net.minecraft.network.chat.Component;*/
-//? }
+import net.minecraft.network.chat.Component;
/**
* The mod's own settings, reached from the game's mod list.
*
* Not a page in a browser: this is where a player goes when the browser is what is not
* working.
+ *
+ * Plain cycling buttons rather than sliders or a scrolling list: a slider's constructor
+ * differs on every other Minecraft version and this screen has to build on ten of them.
+ * What each option costs is said in its tooltip instead of on the screen, so the screen
+ * stays the list of settings it is.
*/
-//? if fabric {
public class WebGUISettingsScreen extends Screen {
-//? } else {
-/*public class WebGUISettingsScreen extends Screen {*/
-//? }
private static final int ROW_HEIGHT = 24;
- private static final int BUTTON_WIDTH = 200;
+ private static final int BUTTON_WIDTH = 220;
+ private static final int ROWS = 5;
- private final Screen parent;
+ private static final int[] SCROLL_STEPS = {1, 3, 5, 10, 15, 20, 25, 30, 40};
- //? if fabric {
- private ButtonWidget devToolsButton;
+ /** Zero last, and it means whatever Chromium would do unasked. */
+ private static final int[] FRAME_RATE_STEPS = {30, 60, 90, 120, 144, 240, 0};
- public WebGUISettingsScreen(Screen parent) {
- super(Text.translatable("screen.webgui.settings.title"));
- this.parent = parent;
- }
- //? } else {
- /*private Button devToolsButton;
+ private final Screen parent;
+
+ private Button scrollButton;
+ private Button frameRateButton;
+ private Button downloadsButton;
+ private Button autoplayButton;
+ private Button devToolsButton;
public WebGUISettingsScreen(Screen parent) {
super(Component.translatable("screen.webgui.settings.title"));
this.parent = parent;
- }*/
- //? }
+ }
@Override
protected void init() {
super.init();
int left = this.width / 2 - BUTTON_WIDTH / 2;
- int top = this.height / 4 + 24;
+ int top = this.height / 2 - (ROWS * ROW_HEIGHT) / 2 - 16;
- //? if fabric {
- this.devToolsButton = ButtonWidget.builder(devToolsLabel(), b -> {
- WebGUIClientConfig.setDevTools(!WebGUIClientConfig.devTools());
- this.devToolsButton.setMessage(devToolsLabel());
- }).dimensions(left, top, BUTTON_WIDTH, 20).build();
- this.addDrawableChild(this.devToolsButton);
+ this.scrollButton = row(left, top, scrollLabel(), "scroll", b -> {
+ WebGUIClientConfig.setScrollNotches(next(SCROLL_STEPS, WebGUIClientConfig.scrollNotches()));
+ this.scrollButton.setMessage(scrollLabel());
+ });
- this.addDrawableChild(ButtonWidget.builder(Text.translatable("gui.done"), b -> this.close())
- .dimensions(left, this.height - 32, BUTTON_WIDTH, 20).build());
- //? } else {
- /*this.devToolsButton = Button.builder(devToolsLabel(), b -> {
+ this.frameRateButton = row(left, top + ROW_HEIGHT, frameRateLabel(), "framerate", b -> {
+ WebGUIClientConfig.setBrowserFrameRate(nextFrameRate());
+ this.frameRateButton.setMessage(frameRateLabel());
+ });
+
+ this.downloadsButton = row(left, top + ROW_HEIGHT * 2, downloadsLabel(), "downloads", b -> {
+ WebGUIClientConfig.setAskWhereToSaveDownloads(!WebGUIClientConfig.askWhereToSaveDownloads());
+ this.downloadsButton.setMessage(downloadsLabel());
+ });
+
+ this.autoplayButton = row(left, top + ROW_HEIGHT * 3, autoplayLabel(), "autoplay", b -> {
+ WebGUIClientConfig.setAllowPageAutoplay(!WebGUIClientConfig.allowPageAutoplay());
+ this.autoplayButton.setMessage(autoplayLabel());
+ });
+
+ this.devToolsButton = row(left, top + ROW_HEIGHT * 4, devToolsLabel(), "devtools", b -> {
WebGUIClientConfig.setDevTools(!WebGUIClientConfig.devTools());
this.devToolsButton.setMessage(devToolsLabel());
- }).bounds(left, top, BUTTON_WIDTH, 20).build();
- this.addRenderableWidget(this.devToolsButton);
+ });
this.addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> this.onClose())
- .bounds(left, this.height - 32, BUTTON_WIDTH, 20).build());*/
- //? }
- }
-
- //? if fabric {
- private Text devToolsLabel() {
- return Text.translatable("screen.webgui.settings.devtools")
- .append(": ")
- .append(Text.translatable(WebGUIClientConfig.devTools() ? "options.on" : "options.off"));
- }
- //? } else {
- /*private Component devToolsLabel() {
- return Component.translatable("screen.webgui.settings.devtools")
- .append(": ")
- .append(Component.translatable(WebGUIClientConfig.devTools() ? "options.on" : "options.off"));
- }*/
- //? }
+ .bounds(left, this.height - 32, BUTTON_WIDTH, 20).build());
+ }
- //? if fabric {
- @Override
- public void render(DrawContext context, int mouseX, int mouseY, float delta) {
- //? if <1.20.5 {
- /*this.renderBackground(context);*/
- //? }
- super.render(context, mouseX, mouseY, delta);
- int centre = this.width / 2;
- context.drawCenteredTextWithShadow(this.textRenderer, this.title, centre, this.height / 4 - 8, 0xFFFFFFFF);
- int under = this.height / 4 + 24 + ROW_HEIGHT;
- context.drawCenteredTextWithShadow(this.textRenderer,
- Text.translatable("screen.webgui.settings.devtools.hint1"), centre, under, 0xFFA0A0A0);
- context.drawCenteredTextWithShadow(this.textRenderer,
- Text.translatable("screen.webgui.settings.devtools.hint2"), centre, under + 12, 0xFFA0A0A0);
-
- // The real inspector is a launch argument, fixed before any mod runs; reporting
- // it is the most this screen can do.
- int inspector = under + 34;
- boolean on = WebGUIRemoteDebugging.enabled();
- context.drawCenteredTextWithShadow(this.textRenderer,
- on ? Text.translatable("screen.webgui.settings.inspector.on", WebGUIRemoteDebugging.address())
- : Text.translatable("screen.webgui.settings.inspector.off"),
- centre, inspector, on ? 0xFF80FF80 : 0xFFA0A0A0);
- if (!on) {
- context.drawCenteredTextWithShadow(this.textRenderer,
- Text.literal("--remote-debugging-port=" + WebGUIRemoteDebugging.SUGGESTED_PORT),
- centre, inspector + 12, 0xFFFFFF80);
+ private Button row(int left, int top, Component label, String key, Button.OnPress action) {
+ Button button = Button.builder(label, action)
+ .tooltip(Tooltip.create(Component.translatable("screen.webgui.settings." + key + ".tip")))
+ .bounds(left, top, BUTTON_WIDTH, 20)
+ .build();
+ this.addRenderableWidget(button);
+ return button;
+ }
+
+ private static int next(int[] steps, int current) {
+ for (int step : steps) {
+ if (step > current) {
+ return step;
+ }
}
+ return steps[0];
}
- @Override
- public void close() {
- if (this.client != null) {
- this.client.setScreen(this.parent);
+ // Its own, because zero sorts first but belongs last: it is "no limit", not "slowest".
+ private static int nextFrameRate() {
+ int current = WebGUIClientConfig.browserFrameRate();
+ for (int i = 0; i < FRAME_RATE_STEPS.length; i++) {
+ if (FRAME_RATE_STEPS[i] == current) {
+ return FRAME_RATE_STEPS[(i + 1) % FRAME_RATE_STEPS.length];
+ }
}
+ return FRAME_RATE_STEPS[0];
}
- //? } else {
- /*//? if >=26 {
- @Override
+
+ private static Component value(String key, Component shown) {
+ return Component.translatable("screen.webgui.settings." + key).append(": ").append(shown);
+ }
+
+ private static Component onOff(boolean on) {
+ return Component.translatable(on ? "options.on" : "options.off");
+ }
+
+ private Component scrollLabel() {
+ return value("scroll", Component.literal(String.valueOf(WebGUIClientConfig.scrollNotches())));
+ }
+
+ private Component frameRateLabel() {
+ int rate = WebGUIClientConfig.browserFrameRate();
+ return value("framerate", rate <= 0
+ ? Component.translatable("screen.webgui.settings.framerate.unlimited")
+ : Component.translatable("screen.webgui.settings.framerate.value", rate));
+ }
+
+ private Component downloadsLabel() {
+ return value("downloads", onOff(WebGUIClientConfig.askWhereToSaveDownloads()));
+ }
+
+ private Component autoplayLabel() {
+ return value("autoplay", onOff(WebGUIClientConfig.allowPageAutoplay()));
+ }
+
+ private Component devToolsLabel() {
+ return value("devtools", onOff(WebGUIClientConfig.devTools()));
+ }
+
+ // Only while the tools are on. The inspector is a developer's door, and a player who
+ // never opens it should not be reading about launch arguments.
+ private int inspectorTop() {
+ return this.height / 2 - (ROWS * ROW_HEIGHT) / 2 - 16 + ROWS * ROW_HEIGHT + 6;
+ }
+
+ //? if >=26 {
+ /*@Override
public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) {
super.extractRenderState(context, mouseX, mouseY, delta);
int centre = this.width / 2;
- context.centeredText(this.font, this.title, centre, this.height / 4 - 8, 0xFFFFFFFF);
- int under = this.height / 4 + 24 + ROW_HEIGHT;
- context.centeredText(this.font,
- Component.translatable("screen.webgui.settings.devtools.hint1"), centre, under, 0xFFA0A0A0);
- context.centeredText(this.font,
- Component.translatable("screen.webgui.settings.devtools.hint2"), centre, under + 12, 0xFFA0A0A0);
-
- // The real inspector is a launch argument, fixed before any mod runs; reporting
- // it is the most this screen can do.
- int inspector = under + 34;
- boolean on = WebGUIRemoteDebugging.enabled();
+ context.centeredText(this.font, this.title, centre,
+ this.height / 2 - (ROWS * ROW_HEIGHT) / 2 - 36, 0xFFFFFFFF);
+ if (!WebGUIClientConfig.devTools()) {
+ return;
+ }
+ boolean on = WebGUIRemoteDebugging.reachable();
context.centeredText(this.font,
on ? Component.translatable("screen.webgui.settings.inspector.on", WebGUIRemoteDebugging.address())
: Component.translatable("screen.webgui.settings.inspector.off"),
- centre, inspector, on ? 0xFF80FF80 : 0xFFA0A0A0);
- if (!on) {
- context.centeredText(this.font,
- Component.literal("--remote-debugging-port=" + WebGUIRemoteDebugging.SUGGESTED_PORT),
- centre, inspector + 12, 0xFFFFFF80);
- }
+ centre, inspectorTop(), on ? 0xFF80FF80 : 0xFFA0A0A0);
}
- //? } else {
+ *///? } else {
@Override
public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {
super.render(context, mouseX, mouseY, delta);
int centre = this.width / 2;
- context.drawCenteredString(this.font, this.title, centre, this.height / 4 - 8, 0xFFFFFFFF);
- int under = this.height / 4 + 24 + ROW_HEIGHT;
- context.drawCenteredString(this.font,
- Component.translatable("screen.webgui.settings.devtools.hint1"), centre, under, 0xFFA0A0A0);
- context.drawCenteredString(this.font,
- Component.translatable("screen.webgui.settings.devtools.hint2"), centre, under + 12, 0xFFA0A0A0);
-
- // The real inspector is a launch argument, fixed before any mod runs; reporting
- // it is the most this screen can do.
- int inspector = under + 34;
- boolean on = WebGUIRemoteDebugging.enabled();
+ context.drawCenteredString(this.font, this.title, centre,
+ this.height / 2 - (ROWS * ROW_HEIGHT) / 2 - 36, 0xFFFFFFFF);
+ if (!WebGUIClientConfig.devTools()) {
+ return;
+ }
+ boolean on = WebGUIRemoteDebugging.reachable();
context.drawCenteredString(this.font,
on ? Component.translatable("screen.webgui.settings.inspector.on", WebGUIRemoteDebugging.address())
: Component.translatable("screen.webgui.settings.inspector.off"),
- centre, inspector, on ? 0xFF80FF80 : 0xFFA0A0A0);
- if (!on) {
- context.drawCenteredString(this.font,
- Component.literal("--remote-debugging-port=" + WebGUIRemoteDebugging.SUGGESTED_PORT),
- centre, inspector + 12, 0xFFFFFF80);
- }
+ centre, inspectorTop(), on ? 0xFF80FF80 : 0xFFA0A0A0);
}
//? }
@Override
public void onClose() {
- if (this.minecraft != null) {
- //? if >=26.2-neoforge {
- this.minecraft.gui.setScreen(this.parent);
- //? } else {
- this.minecraft.setScreen(this.parent);
- //? }
- }
- }*/
- //? }
+ Mc.openScreen(this.minecraft, this.parent);
+ }
}
diff --git a/src/main/java/land/webgui/WebHudOverlay.java b/src/main/java/land/webgui/WebHudOverlay.java
index 23aa2cb..9e9263e 100644
--- a/src/main/java/land/webgui/WebHudOverlay.java
+++ b/src/main/java/land/webgui/WebHudOverlay.java
@@ -1,21 +1,32 @@
package land.webgui;
-import de.keksuccino.rinku.Rinku;
-import de.keksuccino.rinku.RinkuBrowser;
+import land.webgui.compat.Mc;
+import land.webgui.browser.WebBrowser;
//? if fabric {
+//? if >=26 {
+/*import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
+*///? } else {
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
-import net.minecraft.client.MinecraftClient;
+//? }
+import net.minecraft.client.Minecraft;
//? if >=1.21.5 {
-import net.minecraft.client.gl.RenderPipelines;
+import net.minecraft.client.renderer.RenderPipelines;
+//? }
+//? if >=26 {
+/*// GuiGraphics is gone from the HUD path in 26: the game extracts render state first.
+*///? } else {
+import net.minecraft.client.gui.GuiGraphics;
//? }
-import net.minecraft.client.gui.DrawContext;
//? if >=1.20.5 {
-import net.minecraft.client.render.RenderTickCounter;
+import net.minecraft.client.DeltaTracker;
//? }
//? if >=1.21.5 {
-import net.minecraft.util.Identifier;
+import net.minecraft.resources.Identifier;
//? }
-import net.minecraft.text.Text;
+//? if >=26 {
+/*import net.minecraft.client.gui.GuiGraphicsExtractor;
+*///? }
+import net.minecraft.network.chat.Component;
//? } else {
/*import net.minecraft.client.Minecraft;
//? if >=1.21.5 {
@@ -55,23 +66,11 @@ public static void setHudInteractive(boolean value) {
hudInteractive = value;
}
- //? if fabric {
- public static boolean shouldDeliverHudBrowserInput(MinecraftClient client) {
- //? } else {
- /*public static boolean shouldDeliverHudBrowserInput(Minecraft client) {*/
- //? }
- if (client == null || !Rinku.isInitialized()) {
+ public static boolean shouldDeliverHudBrowserInput(Minecraft client) {
+ if (client == null || !WebBrowser.ready()) {
return false;
}
- //? if fabric {
- boolean hasGui1 = client.currentScreen != null;
- //? } else {
- /*//? if >=26.2-neoforge {
- boolean hasGui1 = client.gui.screen() != null;
- //? } else {
- boolean hasGui1 = client.screen != null;
- //? }*/
- //? }
+ boolean hasGui1 = Mc.screen(client) != null;
if (!hudVisible || !hudInteractive || hasGui1) {
return false;
}
@@ -81,23 +80,11 @@ public static boolean shouldDeliverHudBrowserInput(MinecraftClient client) {
return WebSession.browser() != null;
}
- //? if fabric {
- public static boolean shouldForwardHudArrowKeys(MinecraftClient client) {
- //? } else {
- /*public static boolean shouldForwardHudArrowKeys(Minecraft client) {*/
- //? }
- if (client == null || !Rinku.isInitialized()) {
+ public static boolean shouldForwardHudArrowKeys(Minecraft client) {
+ if (client == null || !WebBrowser.ready()) {
return false;
}
- //? if fabric {
- boolean hasGui2 = client.currentScreen != null;
- //? } else {
- /*//? if >=26.2-neoforge {
- boolean hasGui2 = client.gui.screen() != null;
- //? } else {
- boolean hasGui2 = client.screen != null;
- //? }*/
- //? }
+ boolean hasGui2 = Mc.screen(client) != null;
if (!hudVisible || hasGui2) {
return false;
}
@@ -110,98 +97,42 @@ public static boolean shouldForwardHudArrowKeys(MinecraftClient client) {
return WebSession.browser() != null;
}
- //? if fabric {
- public static boolean shouldBlockVanillaWorldInteractions(MinecraftClient client) {
- //? } else {
- /*public static boolean shouldBlockVanillaWorldInteractions(Minecraft client) {*/
- //? }
- //? if fabric {
- if (client == null || client.currentScreen != null) {
- //? } else {
- /*//? if >=26.2-neoforge {
- if (client == null || client.gui.screen() != null) {
- //? } else {
- if (client == null || client.screen != null) {
- //? }*/
- //? }
+ public static boolean shouldBlockVanillaWorldInteractions(Minecraft client) {
+ if (client == null || Mc.screen(client) != null) {
return false;
}
if (!hudVisible || !hudInteractive) {
return false;
}
- //? if fabric {
- return client.world != null && client.world.isClient();
- //? } else {
- /*return client.level != null;*/
- //? }
+ return client.level != null;
}
- //? if fabric {
- public static void tickCursor(MinecraftClient client) {
- //? } else {
- /*public static void tickCursor(Minecraft client) {*/
- //? }
- //? if fabric {
- if (client.currentScreen != null) {
- //? } else {
- /*//? if >=26.2-neoforge {
- if (client.gui.screen() != null) {
- //? } else {
- if (client.screen != null) {
- //? }*/
- //? }
+ public static void tickCursor(Minecraft client) {
+ if (Mc.screen(client) != null) {
return;
}
if (!hudVisible) {
if (cursorUnlockedForWebHud) {
- //? if fabric {
- client.mouse.lockCursor();
- //? } else {
- /*client.mouseHandler.grabMouse();*/
- //? }
+ client.mouseHandler.grabMouse();
cursorUnlockedForWebHud = false;
}
return;
}
if (hudInteractive) {
- //? if fabric {
- client.mouse.unlockCursor();
- //? } else {
- /*client.mouseHandler.releaseMouse();*/
- //? }
+ client.mouseHandler.releaseMouse();
cursorUnlockedForWebHud = true;
return;
}
if (cursorUnlockedForWebHud) {
- //? if fabric {
- client.mouse.lockCursor();
- //? } else {
- /*client.mouseHandler.grabMouse();*/
- //? }
+ client.mouseHandler.grabMouse();
cursorUnlockedForWebHud = false;
}
}
- //? if fabric {
- public static void applyServerOpen(MinecraftClient client, String url) {
- //? } else {
- /*public static void applyServerOpen(Minecraft client, String url) {*/
- //? }
+ public static void applyServerOpen(Minecraft client, String url) {
String u = url == null || url.isBlank() ? StartUrls.primary() : url;
- //? if fabric {
- if (client.currentScreen != null) {
- //? } else {
- /*//? if >=26.2-neoforge {
- if (client.gui.screen() != null) {
- //? } else {
- if (client.screen != null) {
- //? }*/
- //? }
- //? if >=26.2-neoforge {
- /*client.gui.setScreen(null);*/
- //? } else {
- client.setScreen(null);
- //? }
+ if (Mc.screen(client) != null) {
+ Mc.openScreen(client, null);
}
restoreHudAfterGuiClose = false;
hudVisible = true;
@@ -213,23 +144,11 @@ public static void applyServerOpen(MinecraftClient client, String url) {
resizeBrowser(client);
}
- //? if fabric {
- public static void toggleHud(MinecraftClient client) {
- //? } else {
- /*public static void toggleHud(Minecraft client) {*/
- //? }
- //? if fabric {
- if (client.currentScreen instanceof WebViewScreen) {
- //? } else {
- /*//? if >=26.2-neoforge {
- if (client.gui.screen() instanceof WebViewScreen) {
- //? } else {
- if (client.screen instanceof WebViewScreen) {
- //? }*/
- //? }
+ public static void toggleHud(Minecraft client) {
+ if (Mc.screen(client) instanceof WebViewScreen) {
return;
}
- if (!Rinku.isInitialized()) {
+ if (!WebBrowser.ready()) {
notifyMcefMissing(client);
return;
}
@@ -241,11 +160,7 @@ public static void toggleHud(MinecraftClient client) {
lastPixelW = -1;
lastPixelH = -1;
if (cursorUnlockedForWebHud) {
- //? if fabric {
- client.mouse.lockCursor();
- //? } else {
- /*client.mouseHandler.grabMouse();*/
- //? }
+ client.mouseHandler.grabMouse();
cursorUnlockedForWebHud = false;
}
} else {
@@ -257,40 +172,20 @@ public static void toggleHud(MinecraftClient client) {
}
}
- //? if fabric {
- public static void toggleInteractive(MinecraftClient client) {
- //? } else {
- /*public static void toggleInteractive(Minecraft client) {*/
- //? }
- if (!Rinku.isInitialized()) {
+ public static void toggleInteractive(Minecraft client) {
+ if (!WebBrowser.ready()) {
return;
}
- //? if fabric {
- boolean hasGui7 = client.currentScreen != null;
- //? } else {
- /*//? if >=26.2-neoforge {
- boolean hasGui7 = client.gui.screen() != null;
- //? } else {
- boolean hasGui7 = client.screen != null;
- //? }*/
- //? }
+ boolean hasGui7 = Mc.screen(client) != null;
if (!hudVisible || hasGui7) {
return;
}
hudInteractive = !hudInteractive;
if (hudInteractive) {
- //? if fabric {
- client.mouse.unlockCursor();
- //? } else {
- /*client.mouseHandler.releaseMouse();*/
- //? }
+ client.mouseHandler.releaseMouse();
cursorUnlockedForWebHud = true;
} else if (cursorUnlockedForWebHud) {
- //? if fabric {
- client.mouse.lockCursor();
- //? } else {
- /*client.mouseHandler.grabMouse();*/
- //? }
+ client.mouseHandler.grabMouse();
cursorUnlockedForWebHud = false;
}
}
@@ -314,11 +209,7 @@ public static void reset() {
lastPixelH = -1;
}
- //? if fabric {
public static void onGuiOpened() {
- //? } else {
- /*public static void onGuiOpened() {*/
- //? }
// Only decided when a gui is opening over the hud. A gui replacing another gui
// runs this too, and by then the mode is already GUI_SCREEN, so recomputing it
// here answered "there is no hud to put back" and threw the parked one away.
@@ -328,26 +219,14 @@ public static void onGuiOpened() {
hudInteractive = false;
lastPixelW = -1;
lastPixelH = -1;
- //? if fabric {
- MinecraftClient c = MinecraftClient.getInstance();
- //? } else {
- /*Minecraft c = Minecraft.getInstance();*/
- //? }
+ Minecraft c = Minecraft.getInstance();
if (cursorUnlockedForWebHud && c != null) {
- //? if fabric {
- c.mouse.lockCursor();
- //? } else {
- /*c.mouseHandler.grabMouse();*/
- //? }
+ c.mouseHandler.grabMouse();
cursorUnlockedForWebHud = false;
}
}
- //? if fabric {
- public static void onGuiClosed(MinecraftClient client) {
- //? } else {
- /*public static void onGuiClosed(Minecraft client) {*/
- //? }
+ public static void onGuiClosed(Minecraft client) {
if (!restoreHudAfterGuiClose) {
return;
}
@@ -361,7 +240,7 @@ public static void onGuiClosed(MinecraftClient client) {
}
}
- static void onHudBrowserLoadStart(RinkuBrowser browser) {
+ static void onHudBrowserLoadStart(WebBrowser browser) {
if (browser == null) {
return;
}
@@ -370,7 +249,7 @@ static void onHudBrowserLoadStart(RinkuBrowser browser) {
}
}
- static void onHudBrowserLoadFinished(RinkuBrowser browser) {
+ static void onHudBrowserLoadFinished(WebBrowser browser) {
if (browser == null) {
return;
}
@@ -381,9 +260,19 @@ static void onHudBrowserLoadFinished(RinkuBrowser browser) {
}
//? if fabric {
+ //? if >=26 {
+ /*public static void register() {
+ // Not an event any more: 26 keeps an ordered list of HUD elements, and a mod adds
+ // one rather than hooking the whole HUD being drawn. Last, so the page sits over
+ // the game's own bars rather than under them.
+ HudElementRegistry.addLast(Identifier.fromNamespaceAndPath(WebGUIMod.MOD_ID, "page"),
+ WebHudOverlay::onHudRender);
+ }
+ *///? } else {
public static void register() {
HudRenderCallback.EVENT.register(WebHudOverlay::onHudRender);
}
+ //? }
//? } else {
/*public static void register() {
NeoForge.EVENT_BUS.addListener(WebHudOverlay::onHudRender);
@@ -391,10 +280,14 @@ public static void register() {
//? }
//? if fabric {
+ //? if >=26 {
+ /*private static void onHudRender(GuiGraphicsExtractor context, DeltaTracker tickCounter) {
+ *///? } else {
//? if >=1.20.5 {
- private static void onHudRender(DrawContext context, RenderTickCounter tickCounter) {
+ private static void onHudRender(GuiGraphics context, DeltaTracker tickCounter) {
//? } else {
- /*private static void onHudRender(DrawContext context, float tickDelta) {*/
+ /*private static void onHudRender(GuiGraphics context, float tickDelta) {*/
+ //? }
//? }
//? } else {
/*//? if >=1.20.5 {
@@ -412,102 +305,60 @@ private static void onHudRender(RenderGuiEvent.Post event) {
if (!hudVisible) {
return;
}
- RinkuBrowser browser = WebSession.hudBrowser();
+ WebBrowser browser = WebSession.hudBrowser();
if (browser == null) {
return;
}
- //? if fabric {
- MinecraftClient client = MinecraftClient.getInstance();
- //? } else {
- /*Minecraft client = Minecraft.getInstance();*/
- //? }
+ Minecraft client = Minecraft.getInstance();
resizeBrowser(client);
if (!hudPageReady) {
return;
}
- //? if fabric {
- if (!browser.isTextureReady()) {
- return;
- }
- //? if >=1.21.5 {
- net.minecraft.util.Identifier tex = browser.getTextureIdentifier();
- //? } else {
- /*net.minecraft.util.Identifier tex = browser.getTextureIdentifier();*/
- //? }
- if (tex == null) {
- return;
- }
- int sw = client.getWindow().getScaledWidth();
- int sh = client.getWindow().getScaledHeight();
- //? if >=1.21.5 {
- context.drawTexture(RenderPipelines.GUI_TEXTURED, tex, 0, 0, 0f, 0f, sw, sh, sw, sh);
- //? } else {
- /*context.drawTexture(tex, 0, 0, 0, 0f, 0f, sw, sh, sw, sh);*/
- //? }
- //? } else {
- /*if (!browser.isTextureReady()) return;
+ if (!browser.isTextureReady()) return;
//? if >=1.21.5 {
net.minecraft.resources.Identifier tex = browser.getTextureIdentifier();
//? } else {
- net.minecraft.resources.ResourceLocation tex = browser.getTextureIdentifier();
- //? }
+ /*net.minecraft.resources.ResourceLocation tex = browser.getTextureIdentifier();
+ *///? }
if (tex == null) return;
int sw = client.getWindow().getGuiScaledWidth();
int sh = client.getWindow().getGuiScaledHeight();
//? if >=1.21.5 {
context.blit(RenderPipelines.GUI_TEXTURED, tex, 0, 0, 0f, 0f, sw, sh, sw, sh);
//? } else {
- context.blit(tex, 0, 0, 0f, 0f, sw, sh, sw, sh);
- //? }*/
- //? }
+ /*context.blit(tex, 0, 0, 0f, 0f, sw, sh, sw, sh);
+ *///? }
+ // Only while the HUD holds the mouse, or a player looking around gets the page's cursor.
+ //? if >26.2 {
+ /*if (hudInteractive) {
+ browser.requestCursor(context);
+ }
+ *///? }
}
- //? if fabric {
- public static boolean containsMouse(double mouseX, double mouseY, MinecraftClient client) {
- //? } else {
- /*public static boolean containsMouse(double mouseX, double mouseY, Minecraft client) {*/
- //? }
+ public static boolean containsMouse(double mouseX, double mouseY, Minecraft client) {
if (!hudVisible) {
return false;
}
- //? if fabric {
- int sw = client.getWindow().getScaledWidth();
- int sh = client.getWindow().getScaledHeight();
- //? } else {
- /*int sw = client.getWindow().getGuiScaledWidth();
- int sh = client.getWindow().getGuiScaledHeight();*/
- //? }
+ int sw = client.getWindow().getGuiScaledWidth();
+ int sh = client.getWindow().getGuiScaledHeight();
return mouseX >= 0 && mouseY >= 0 && mouseX < sw && mouseY < sh;
}
- //? if fabric {
- public static int toBrowserLocalX(double mouseX, MinecraftClient client) {
- int sw = client.getWindow().getScaledWidth();
- //? } else {
- /*public static int toBrowserLocalX(double mouseX, Minecraft client) {
- int sw = client.getWindow().getGuiScaledWidth();*/
- //? }
+ public static int toBrowserLocalX(double mouseX, Minecraft client) {
+ int sw = client.getWindow().getGuiScaledWidth();
return (int) Math.round(mouseX * browserViewSize(client)[0] / (double) Math.max(1, sw));
}
- //? if fabric {
- public static int toBrowserLocalY(double mouseY, MinecraftClient client) {
- int sh = client.getWindow().getScaledHeight();
- //? } else {
- /*public static int toBrowserLocalY(double mouseY, Minecraft client) {
- int sh = client.getWindow().getGuiScaledHeight();*/
- //? }
+ public static int toBrowserLocalY(double mouseY, Minecraft client) {
+ int sh = client.getWindow().getGuiScaledHeight();
return (int) Math.round(mouseY * browserViewSize(client)[1] / (double) Math.max(1, sh));
}
- //? if fabric {
- static void resizeBrowser(MinecraftClient client) {
- //? } else {
- /*static void resizeBrowser(Minecraft client) {*/
- //? }
- RinkuBrowser browser = WebSession.hudBrowser();
+ static void resizeBrowser(Minecraft client) {
+ WebBrowser browser = WebSession.hudBrowser();
if (browser == null) {
return;
}
@@ -523,32 +374,20 @@ static void resizeBrowser(MinecraftClient client) {
}
// Browser view size = physical framebuffer px; page zoom keeps the CSS viewport logical.
- //? if fabric {
- private static int[] browserViewSize(MinecraftClient client) {
- //? } else {
- /*private static int[] browserViewSize(Minecraft client) {*/
- //? }
+ private static int[] browserViewSize(Minecraft client) {
var window = client.getWindow();
return WebViewLayout.browserSize(window.getWidth(), window.getHeight());
}
- //? if fabric {
- private static void notifyMcefMissing(MinecraftClient client) {
- if (client.player != null) {
- client.player.sendMessage(Text.translatable("message.webgui.mcef_not_ready"), false);
- }
- }
- //? } else {
- /*private static void notifyMcefMissing(Minecraft client) {
+ private static void notifyMcefMissing(Minecraft client) {
if (client.player != null) {
//? if >=26 {
- client.player.sendSystemMessage(
+ /*client.player.sendSystemMessage(
Component.translatable("message.webgui.mcef_not_ready"));
- //? } else {
+ *///? } else {
client.player.displayClientMessage(
Component.translatable("message.webgui.mcef_not_ready"), false);
//? }
}
- }*/
- //? }
+ }
}
diff --git a/src/main/java/land/webgui/WebSession.java b/src/main/java/land/webgui/WebSession.java
index 810b167..3dc50af 100644
--- a/src/main/java/land/webgui/WebSession.java
+++ b/src/main/java/land/webgui/WebSession.java
@@ -1,11 +1,10 @@
package land.webgui;
-import de.keksuccino.rinku.Rinku;
-import de.keksuccino.rinku.RinkuBrowser;
+import land.webgui.browser.WebBrowser;
public final class WebSession {
- private static RinkuBrowser browser;
- private static RinkuBrowser suspendedHudBrowser;
+ private static WebBrowser browser;
+ private static WebBrowser suspendedHudBrowser;
private static Mode mode = Mode.NONE;
public enum Mode {
@@ -16,11 +15,11 @@ public enum Mode {
private WebSession() {}
- public static RinkuBrowser browser() {
+ public static WebBrowser browser() {
return browser;
}
- public static RinkuBrowser hudBrowser() {
+ public static WebBrowser hudBrowser() {
if (mode == Mode.HUD_OVERLAY) {
return browser;
}
@@ -70,7 +69,7 @@ public static void closeGuiAndRestoreHud() {
WebviewClientBridge.clearCache();
}
- public static RinkuBrowser openForGui(String url) {
+ public static WebBrowser openForGui(String url) {
if (mode == Mode.HUD_OVERLAY && browser != null) {
// Coming from the hud: park it. Nothing should already be parked in this mode,
// but closing first keeps that from becoming a leak if it ever is.
@@ -84,13 +83,13 @@ public static RinkuBrowser openForGui(String url) {
// hud at all once they escaped out.
closeActiveBrowser();
}
- browser = Rinku.createBrowser(WebGUIAssetServer.resolve(url), true);
+ browser = WebBrowser.create(WebGUIAssetServer.resolve(url), true);
mode = Mode.GUI_SCREEN;
WebviewClientBridge.clearCache();
return browser;
}
- public static RinkuBrowser openForHud(String url) {
+ public static WebBrowser openForHud(String url) {
closeSuspendedHudBrowser();
if (mode == Mode.HUD_OVERLAY && browser != null) {
browser.loadURL(WebGUIAssetServer.resolve(url));
@@ -98,7 +97,7 @@ public static RinkuBrowser openForHud(String url) {
return browser;
}
closeActiveBrowser();
- browser = Rinku.createBrowser(WebGUIAssetServer.resolve(url), true);
+ browser = WebBrowser.create(WebGUIAssetServer.resolve(url), true);
mode = Mode.HUD_OVERLAY;
WebviewClientBridge.clearCache();
return browser;
@@ -122,7 +121,7 @@ public static void reloadBundledPages() {
}
}
- private static boolean reloadIfBundled(RinkuBrowser target, String origin) {
+ private static boolean reloadIfBundled(WebBrowser target, String origin) {
if (target == null) {
return false;
}
diff --git a/src/main/java/land/webgui/WebViewLayout.java b/src/main/java/land/webgui/WebViewLayout.java
index 1003406..02628c7 100644
--- a/src/main/java/land/webgui/WebViewLayout.java
+++ b/src/main/java/land/webgui/WebViewLayout.java
@@ -1,19 +1,30 @@
package land.webgui;
-import de.keksuccino.rinku.RinkuBrowser;
+import land.webgui.browser.WebBrowser;
+//? if <=26.2 {
import org.lwjgl.glfw.GLFW;
+//? }
/**
* Sizing/scale helpers for the embedded browser. The browser view is the physical framebuffer size
* (so its texture draws 1:1 → crisp on HiDPI), and CEF page zoom scales the content up by the OS
* content scale so the page's CSS viewport stays logical and independent of Minecraft's GUI Scale.
- * Uses only the public {@link RinkuBrowser#setZoomLevel} API — no device_scale_factor, no reflection.
+ * Uses only the public {@link WebBrowser#setZoomLevel} API — no device_scale_factor, no reflection.
*/
final class WebViewLayout {
private WebViewLayout() {}
/** OS content scale of the current window (e.g. 2 on a Retina display), or 1 if unknown. */
static float contentScale() {
+ //? if >26.2 {
+ /*// The game answers this itself from 26.3, where there is no GLFW left to ask.
+ var client = net.minecraft.client.Minecraft.getInstance();
+ if (client == null || client.getWindow() == null) {
+ return 1f;
+ }
+ float density = client.getWindow().getPixelDensity();
+ return density > 0f ? density : 1f;
+ *///? } else {
long handle = GLFW.glfwGetCurrentContext();
if (handle == 0L) {
return 1f;
@@ -22,6 +33,7 @@ static float contentScale() {
float[] sy = new float[1];
GLFW.glfwGetWindowContentScale(handle, sx, sy);
return sx[0] > 0f ? sx[0] : 1f;
+ //? }
}
/** Browser view size = physical framebuffer pixels, so the OSR texture is drawn 1:1 (crisp). */
@@ -40,7 +52,7 @@ static double zoomLevel() {
}
/** Applies the content-scale zoom. CEF may reset zoom on navigation, so call after load too. */
- static void applyZoom(RinkuBrowser browser) {
+ static void applyZoom(WebBrowser browser) {
if (browser == null) {
return;
}
diff --git a/src/main/java/land/webgui/WebViewScreen.java b/src/main/java/land/webgui/WebViewScreen.java
index 1a8a12d..f5f8b87 100644
--- a/src/main/java/land/webgui/WebViewScreen.java
+++ b/src/main/java/land/webgui/WebViewScreen.java
@@ -1,29 +1,15 @@
package land.webgui;
-import de.keksuccino.rinku.Rinku;
-import de.keksuccino.rinku.RinkuBrowser;
+import land.webgui.browser.WebBrowser;
//? if >=1.21.5 {
-//? if fabric {
-import net.minecraft.client.gl.RenderPipelines;
-import net.minecraft.client.gui.Click;
-import net.minecraft.client.input.CharInput;
-import net.minecraft.client.input.KeyInput;
-//? } else {
-/*import net.minecraft.client.renderer.RenderPipelines;
+import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.input.CharacterEvent;
import net.minecraft.client.input.KeyEvent;
-import net.minecraft.client.input.MouseButtonEvent;*/
-//? }
+import net.minecraft.client.input.MouseButtonEvent;
//? }
-//? if fabric {
-import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.gui.screen.Screen;
-import net.minecraft.text.Text;
-import net.minecraft.util.Identifier;
-//? } else {
-/*//? if >=26 {
-import net.minecraft.client.gui.GuiGraphicsExtractor;
-//? } else {
+//? if >=26 {
+/*import net.minecraft.client.gui.GuiGraphicsExtractor;
+*///? } else {
import net.minecraft.client.gui.GuiGraphics;
//? }
import net.minecraft.client.gui.screens.Screen;
@@ -31,54 +17,32 @@
//? if >=1.21.5 {
import net.minecraft.resources.Identifier;
//? } else {
-import net.minecraft.resources.ResourceLocation;
-//? }*/
-//? }
+/*import net.minecraft.resources.ResourceLocation;
+*///? }
-//? if fabric {
public class WebViewScreen extends Screen {
-//? } else {
-/*public class WebViewScreen extends Screen {*/
-//? }
private static boolean guiPageReady;
private final String initialUrl;
- private RinkuBrowser browser;
+ private WebBrowser browser;
- //? if fabric {
public WebViewScreen(String startUrl) {
- super(Text.translatable("screen.webgui.title"));
- this.initialUrl = startUrl == null || startUrl.isBlank() ? StartUrls.primary() : startUrl;
- }
- //? } else {
- /*public WebViewScreen(String startUrl) {
super(Component.translatable("screen.webgui.title"));
this.initialUrl = startUrl == null || startUrl.isBlank() ? StartUrls.primary() : startUrl;
- }*/
- //? }
+ }
@Override
protected void init() {
super.init();
- if (!Rinku.isInitialized()) {
- //? if fabric {
- if (this.client != null && this.client.player != null) {
- this.client.player.sendMessage(Text.translatable("message.webgui.mcef_not_ready"), false);
- }
- //? } else {
- /*if (this.minecraft != null && this.minecraft.player != null) {
+ if (!WebBrowser.ready()) {
+ if (this.minecraft != null && this.minecraft.player != null) {
//? if >=26 {
- this.minecraft.player.sendSystemMessage(Component.translatable("message.webgui.mcef_not_ready"));
- //? } else {
+ /*this.minecraft.player.sendSystemMessage(Component.translatable("message.webgui.mcef_not_ready"));
+ *///? } else {
this.minecraft.player.displayClientMessage(Component.translatable("message.webgui.mcef_not_ready"), false);
//? }
- }*/
- //? }
- //? if fabric {
- this.close();
- //? } else {
- /*this.onClose();*/
- //? }
+ }
+ this.onClose();
return;
}
@@ -91,6 +55,13 @@ protected void init() {
guiPageReady = false;
browser = WebSession.openForGui(initialUrl);
resizeBrowser();
+ land.webgui.compat.TextInput.start(this);
+ }
+
+ @Override
+ public void removed() {
+ land.webgui.compat.TextInput.stop(this);
+ super.removed();
}
private int getBrowserWidth() {
@@ -107,11 +78,7 @@ private boolean isInBrowserBounds(double x, double y) {
// Browser view size in logical window points, independent of the GUI Scale setting.
private int[] browserViewSize() {
- //? if fabric {
- var window = this.client.getWindow();
- //? } else {
- /*var window = this.minecraft.getWindow();*/
- //? }
+ var window = this.minecraft.getWindow();
return WebViewLayout.browserSize(window.getWidth(), window.getHeight());
}
@@ -139,41 +106,15 @@ public void resize(int width, int height) {
}
//? } else {
/*@Override
- //? if fabric {
- public void resize(net.minecraft.client.MinecraftClient client, int width, int height) {
- //? } else {
public void resize(net.minecraft.client.Minecraft minecraft, int width, int height) {
- //? }
super.resize(
- //? if fabric {
- client,
- //? } else {
minecraft,
- //? }
width, height);
resizeBrowser();
}*/
//? }
- //? if fabric {
@Override
- public void close() {
- guiPageReady = false;
- // Escaping a death page that never loaded has to respawn, or the player
- // is dropped into the world still dead with nothing to click.
- boolean respawnOnClose = WebGUIDeathScreen.active() && WebGUIDeathScreen.loadFailed();
- WebGUIDeathScreen.setActive(false);
- if (respawnOnClose && this.client != null && this.client.player != null) {
- this.client.player.requestRespawn();
- }
- WebSession.closeGuiAndRestoreHud();
- super.close();
- if (this.client != null) {
- WebHudOverlay.onGuiClosed(this.client);
- }
- }
- //? } else {
- /*@Override
public void onClose() {
guiPageReady = false;
// Escaping a death page that never loaded has to respawn, or the player
@@ -188,10 +129,9 @@ public void onClose() {
if (this.minecraft != null) {
WebHudOverlay.onGuiClosed(this.minecraft);
}
- }*/
- //? }
+ }
- static void onGuiBrowserLoadStart(RinkuBrowser browser) {
+ static void onGuiBrowserLoadStart(WebBrowser browser) {
if (browser == null) {
return;
}
@@ -200,7 +140,7 @@ static void onGuiBrowserLoadStart(RinkuBrowser browser) {
}
}
- static void onGuiBrowserLoadFinished(RinkuBrowser browser) {
+ static void onGuiBrowserLoadFinished(WebBrowser browser) {
if (browser == null) {
return;
}
@@ -210,72 +150,49 @@ static void onGuiBrowserLoadFinished(RinkuBrowser browser) {
}
}
- //? if fabric {
@Override
- public void render(DrawContext context, int mouseX, int mouseY, float delta) {
- if (browser != null && guiPageReady && browser.isTextureReady()) {
- //? if >=1.21.5 {
- Identifier textureLocation = browser.getTextureIdentifier();
- //? } else {
- /*Identifier textureLocation = browser.getTextureIdentifier();*/
- //? }
- if (textureLocation != null) {
- int fw = getBrowserWidth();
- int fh = getBrowserHeight();
- //? if >=1.21.5 {
- context.drawTexture(RenderPipelines.GUI_TEXTURED, textureLocation, 0, 0, 0f, 0f, fw, fh, fw, fh);
- //? } else {
- /*context.drawTexture(textureLocation, 0, 0, 0, 0f, 0f, fw, fh, fw, fh);*/
- //? }
- }
- }
- }
- //? } else {
- /*@Override
//? if >=26 {
- public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) {
- //? } else {
+ /*public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) {
+ *///? } else {
public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {
//? }
if (browser != null && guiPageReady && browser.isTextureReady()) {
//? if >=1.21.5 {
net.minecraft.resources.Identifier textureLocation = browser.getTextureIdentifier();
//? } else {
- net.minecraft.resources.ResourceLocation textureLocation = browser.getTextureIdentifier();
- //? }
+ /*net.minecraft.resources.ResourceLocation textureLocation = browser.getTextureIdentifier();
+ *///? }
if (textureLocation != null) {
int fw = getBrowserWidth();
int fh = getBrowserHeight();
//? if >=1.21.5 {
context.blit(RenderPipelines.GUI_TEXTURED, textureLocation, 0, 0, 0f, 0f, fw, fh, fw, fh);
//? } else {
- context.blit(textureLocation, 0, 0, 0f, 0f, fw, fh, fw, fh);
- //? }
+ /*context.blit(textureLocation, 0, 0, 0f, 0f, fw, fh, fw, fh);
+ *///? }
}
}
- }*/
- //? }
+ // Every frame: the game takes the last request made during one and ignores the rest.
+ //? if >26.2 {
+ /*if (browser != null) {
+ browser.requestCursor(context);
+ }
+ *///? }
+ }
//? if >=1.20.5 {
- //? if fabric {
@Override
- public void renderBackground(DrawContext context, int mouseX, int mouseY, float deltaTicks) {
- // no darkening — web fills the entire screen
- }
- //? } else {
- /*@Override
//? if >=26 {
- public void extractBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float deltaTicks) {
- //? } else {
+ /*public void extractBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float deltaTicks) {
+ *///? } else {
public void renderBackground(GuiGraphics context, int mouseX, int mouseY, float deltaTicks) {
//? }
// no darkening - web fills the entire screen
- }*/
- //? }
+ }
//? } else {
/*//? if fabric {
@Override
- public void renderBackground(DrawContext context) {
+ public void renderBackground(GuiGraphics context) {
// no darkening - web fills the entire screen
}
//? } else {
@@ -287,43 +204,20 @@ public void renderBackground(GuiGraphics context) {
//? }
//? if >=1.21.5 {
- //? if fabric {
@Override
- public boolean mouseClicked(Click click, boolean doubled) {
- if (browser == null || !isInBrowserBounds(click.x(), click.y())) {
- return false;
- }
- browser.sendMousePress(browserLocalMouseX(click.x()), browserLocalMouseY(click.y()), click.button());
- browser.setFocus(true);
- return true;
- }
-
- @Override
- public boolean mouseReleased(Click click) {
- if (browser == null) {
- return false;
- }
- browser.sendMouseRelease(browserLocalMouseX(click.x()), browserLocalMouseY(click.y()), click.button());
- browser.setFocus(true);
- return true;
- }
- //? } else {
- /*@Override
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
if (browser == null || !isInBrowserBounds(click.x(), click.y())) return false;
browser.sendMousePress(browserLocalMouseX(click.x()), browserLocalMouseY(click.y()), click.button());
browser.setFocus(true);
return true;
}
-
@Override
public boolean mouseReleased(MouseButtonEvent click) {
if (browser == null) return false;
browser.sendMouseRelease(browserLocalMouseX(click.x()), browserLocalMouseY(click.y()), click.button());
browser.setFocus(true);
return true;
- }*/
- //? }
+ }
//? } else {
/*@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
@@ -375,54 +269,34 @@ public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
//? }
//? if >=1.21.5 {
- //? if fabric {
@Override
- public boolean keyPressed(KeyInput input) {
- if (super.keyPressed(input)) return true;
- if (browser == null) return false;
- browser.sendKeyPress(input.key(), input.scancode(), input.modifiers());
- browser.setFocus(true);
- return true;
- }
-
- @Override
- public boolean keyReleased(KeyInput input) {
- if (super.keyReleased(input)) return true;
- if (browser == null) return false;
- browser.sendKeyRelease(input.key(), input.scancode(), input.modifiers());
- browser.setFocus(true);
- return true;
- }
-
- @Override
- public boolean charTyped(CharInput input) {
- if (super.charTyped(input)) return true;
- if (browser == null || !input.isValidChar()) return false;
- int cp = input.codepoint();
- if (cp <= 0 || cp > 0xFFFF) return false;
- browser.sendKeyTyped((char) cp, input.modifiers());
- browser.setFocus(true);
- return true;
- }
- //? } else {
- /*@Override
public boolean keyPressed(KeyEvent input) {
if (super.keyPressed(input)) return true;
if (browser == null) return false;
+ if (input.key() == WebGUIKeys.KEY_F12) {
+ WebGUIDevToolsWindow.open(browser);
+ return true;
+ }
+ //? if >26.2 {
+ /*browser.sendKeyPress(input.key(), input.key(), input.modifiers());
+ *///? } else {
browser.sendKeyPress(input.key(), input.scancode(), input.modifiers());
+ //? }
browser.setFocus(true);
return true;
}
-
@Override
public boolean keyReleased(KeyEvent input) {
if (super.keyReleased(input)) return true;
if (browser == null) return false;
+ //? if >26.2 {
+ /*browser.sendKeyRelease(input.key(), input.key(), input.modifiers());
+ *///? } else {
browser.sendKeyRelease(input.key(), input.scancode(), input.modifiers());
+ //? }
browser.setFocus(true);
return true;
}
-
@Override
public boolean charTyped(CharacterEvent input) {
if (super.charTyped(input)) return true;
@@ -430,14 +304,13 @@ public boolean charTyped(CharacterEvent input) {
int cp = input.codepoint();
if (cp <= 0 || cp > 0xFFFF) return false;
//? if >=26 {
- browser.sendKeyTyped((char) cp, 0);
- //? } else {
+ /*browser.sendKeyTyped((char) cp, 0);
+ *///? } else {
browser.sendKeyTyped((char) cp, input.modifiers());
//? }
browser.setFocus(true);
return true;
- }*/
- //? }
+ }
//? } else {
/*@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
@@ -447,6 +320,10 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (browser == null) {
return false;
}
+ if (keyCode == WebGUIKeys.KEY_F12) {
+ WebGUIDevToolsWindow.open(browser);
+ return true;
+ }
browser.sendKeyPress(keyCode, scanCode, modifiers);
browser.setFocus(true);
return true;
@@ -481,7 +358,7 @@ public boolean charTyped(char chr, int modifiers) {
//? if fabric {
@Override
- public boolean shouldPause() {
+ public boolean isPauseScreen() {
return false;
}
//? }
diff --git a/src/main/java/land/webgui/WebviewBrowserConsoleLogger.java b/src/main/java/land/webgui/WebviewBrowserConsoleLogger.java
index 26050ca..96342ee 100644
--- a/src/main/java/land/webgui/WebviewBrowserConsoleLogger.java
+++ b/src/main/java/land/webgui/WebviewBrowserConsoleLogger.java
@@ -1,45 +1,27 @@
package land.webgui;
-import org.cef.CefSettings;
-import org.cef.browser.CefBrowser;
-import org.cef.handler.CefDisplayHandlerAdapter;
+import land.webgui.browser.WebBrowser;
import org.slf4j.Logger;
+import space.webgui.wmc.api.ConsoleLevel;
-public final class WebviewBrowserConsoleLogger extends CefDisplayHandlerAdapter {
+/** Puts what a page writes to its console into the game's log. */
+public final class WebviewBrowserConsoleLogger implements WebBrowser.Listener {
private static final Logger LOG = WebGUIMod.LOGGER;
@Override
- public boolean onConsoleMessage(
- CefBrowser browser,
- CefSettings.LogSeverity level,
- String message,
- String source,
- int line) {
+ public void onConsole(WebBrowser browser, ConsoleLevel level, String message,
+ String source, int line) {
String text = formatMessage(message, source, line);
if (level == null) {
- level = CefSettings.LogSeverity.LOGSEVERITY_DEFAULT;
+ level = ConsoleLevel.INFO;
}
switch (level) {
- case LOGSEVERITY_ERROR:
- case LOGSEVERITY_FATAL:
- LOG.error("[Web GUI] {}", text);
- break;
- case LOGSEVERITY_WARNING:
- LOG.warn("[Web GUI] {}", text);
- break;
- case LOGSEVERITY_VERBOSE:
- LOG.debug("[Web GUI] {}", text);
- break;
- case LOGSEVERITY_DISABLE:
- break;
- case LOGSEVERITY_DEFAULT:
- case LOGSEVERITY_INFO:
- default:
- LOG.info("[Web GUI] {}", text);
- break;
+ case ERROR -> LOG.error("[Web GUI] {}", text);
+ case WARNING -> LOG.warn("[Web GUI] {}", text);
+ case DEBUG -> LOG.debug("[Web GUI] {}", text);
+ default -> LOG.info("[Web GUI] {}", text);
}
- return false;
}
private static String formatMessage(String message, String source, int line) {
diff --git a/src/main/java/land/webgui/WebviewClientBridge.java b/src/main/java/land/webgui/WebviewClientBridge.java
index 36baedb..4e3e422 100644
--- a/src/main/java/land/webgui/WebviewClientBridge.java
+++ b/src/main/java/land/webgui/WebviewClientBridge.java
@@ -1,16 +1,10 @@
package land.webgui;
-import de.keksuccino.rinku.Rinku;
-import de.keksuccino.rinku.RinkuBrowser;
+import land.webgui.browser.WebBrowser;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
-//? if fabric {
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.network.ClientPlayerEntity;
-//? } else {
-/*import net.minecraft.client.Minecraft;
-import net.minecraft.client.player.LocalPlayer;*/
-//? }
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.player.LocalPlayer;
public final class WebviewClientBridge {
private static final Gson GSON = new Gson();
@@ -30,35 +24,21 @@ static void clearCache() {
pendingEntityContextJson = null;
}
- //? if fabric {
- public static void tick(MinecraftClient client) {
- //? } else {
- /*public static void tick(Minecraft client) {*/
- //? }
+ public static void tick(Minecraft client) {
tryPush(client, true, false);
}
- //? if fabric {
- /** Always pushes, regardless of dedup — page must get data on first load. */
- public static void pushAfterDocumentLoad(MinecraftClient client) {
- //? } else {
- /*public static void pushAfterDocumentLoad(Minecraft client) {*/
- //? }
+ public static void pushAfterDocumentLoad(Minecraft client) {
tryPush(client, false, true);
}
- //? if fabric {
- private static void tryPush(MinecraftClient client, boolean requireTexture, boolean ignoreDedup) {
- ClientPlayerEntity player = client.player;
- //? } else {
- /*private static void tryPush(Minecraft client, boolean requireTexture, boolean ignoreDedup) {
- LocalPlayer player = client.player;*/
- //? }
- if (!Rinku.isInitialized()) return;
+ private static void tryPush(Minecraft client, boolean requireTexture, boolean ignoreDedup) {
+ LocalPlayer player = client.player;
+ if (!WebBrowser.ready()) return;
if (player == null) return;
- RinkuBrowser main = WebSession.browser();
- RinkuBrowser hud = WebSession.hudBrowser();
+ WebBrowser main = WebSession.browser();
+ WebBrowser hud = WebSession.hudBrowser();
// isTextureReady on both loaders, not getRenderer().getTextureID() != 0 on one of
// them. Rinku 3 keeps the browser's texture as a Blaze3D GpuTexture, which need
// not have a raw GL id at all — on NeoForge 1.21.11 that id stayed 0, so this test
@@ -92,7 +72,7 @@ private static void tryPush(MinecraftClient client, boolean requireTexture, bool
if (hasHud) executeJs(hud, js);
}
- private static void executeJs(RinkuBrowser browser, String js) {
+ private static void executeJs(WebBrowser browser, String js) {
try {
String url = browser.getURL();
browser.executeJavaScript(js, url != null ? url : "", 0);
@@ -110,172 +90,43 @@ private static String stripLeadingSlash(String address) {
return address.startsWith("/") ? address.substring(1) : address;
}
- //? if fabric {
- private static JsonObject buildPayload(MinecraftClient client, ClientPlayerEntity player) {
+ private static JsonObject buildPayload(Minecraft client, LocalPlayer player) {
JsonObject o = new JsonObject();
-
- o.addProperty("playerUuid", player.getUuid().toString());
- o.addProperty("username", player.getName().getString());
- o.addProperty("webviewMode", WebSession.mode().name());
- o.addProperty("dimension", player.getEntityWorld().getRegistryKey().getValue().toString());
-
- o.addProperty("health", player.getHealth());
- o.addProperty("maxHealth", player.getMaxHealth());
- o.addProperty("food", player.getHungerManager().getFoodLevel());
- o.addProperty("xpLevel", player.experienceLevel);
- var gameMode = client.interactionManager != null ? client.interactionManager.getCurrentGameMode() : null;
- if (gameMode != null) o.addProperty("gamemode", gameMode.asString());
-
- JsonObject pos = new JsonObject();
- pos.addProperty("x", player.getX());
- pos.addProperty("y", player.getY());
- pos.addProperty("z", player.getZ());
- o.add("pos", pos);
-
- JsonObject look = new JsonObject();
- look.addProperty("yaw", player.getYaw());
- look.addProperty("pitch", player.getPitch());
- o.add("look", look);
-
- // The one camera value a page cannot work out for itself. Position and heading are
- // above, and the viewport is window.innerWidth/innerHeight, but the field of view
- // lives only in the player's own settings — and without it there is no way to turn
- // a point in the world into a point on the screen.
- //
- // This is the setting, not the momentary value: the renderer stretches it while
- // sprinting or under a speed effect, and reading that would mean hooking the
- // camera every frame. A marker drawn from this drifts a little during a sprint and
- // is exact when standing still.
- o.addProperty("fov", fov(client));
- o.add("lookingAt", buildLookingAt(client, player));
-
- JsonObject server = buildServerInfo(client);
- if (server != null) o.add("server", server);
-
- return o;
- }
-
- /** Vertical field of view in degrees, as the player set it. */
- private static int fov(MinecraftClient client) {
- return client.options.getFov().getValue();
- }
-
- /**
- * What the crosshair is on, if anything.
- *
- * The game already computes this every frame for its own use — the block outline, the
- * name above a mob, what a click would hit — so this is that same answer rather than a
- * second ray cast with its own idea of reach.
- *
- * Always present, with {@code type: "none"} when the player is looking at nothing. A
- * field that disappears makes every page write the same optional-chaining dance.
- */
- private static JsonObject buildLookingAt(MinecraftClient client, ClientPlayerEntity player) {
- JsonObject o = new JsonObject();
- net.minecraft.util.hit.HitResult hit = client.crosshairTarget;
- if (hit == null || hit.getType() == net.minecraft.util.hit.HitResult.Type.MISS) {
- o.addProperty("type", "none");
- return o;
- }
-
- o.addProperty("distance", Math.sqrt(player.getEyePos().squaredDistanceTo(hit.getPos())));
-
- if (hit instanceof net.minecraft.util.hit.BlockHitResult block) {
- o.addProperty("type", "block");
- net.minecraft.util.math.BlockPos at = block.getBlockPos();
- JsonObject pos = new JsonObject();
- pos.addProperty("x", at.getX());
- pos.addProperty("y", at.getY());
- pos.addProperty("z", at.getZ());
- o.add("pos", pos);
- o.addProperty("face", block.getSide().asString());
- var world = player.getEntityWorld();
- o.addProperty("block", net.minecraft.registry.Registries.BLOCK
- .getId(world.getBlockState(at).getBlock()).toString());
- return o;
- }
-
- if (hit instanceof net.minecraft.util.hit.EntityHitResult entityHit) {
- var entity = entityHit.getEntity();
- o.addProperty("type", "entity");
- o.addProperty("uuid", entity.getUuid().toString());
- o.addProperty("entityType", net.minecraft.entity.EntityType.getId(entity.getType()).toString());
- o.addProperty("name", entity.getName().getString());
- JsonObject pos = new JsonObject();
- pos.addProperty("x", entity.getX());
- pos.addProperty("y", entity.getY());
- pos.addProperty("z", entity.getZ());
- o.add("pos", pos);
- return o;
- }
-
- o.addProperty("type", "none");
- return o;
- }
-
- private static JsonObject buildServerInfo(MinecraftClient client) {
- var nh = client.getNetworkHandler();
- if (nh == null) return null;
- JsonObject s = new JsonObject();
- var entry = client.getCurrentServerEntry();
- if (entry != null) {
- s.addProperty("address", entry.address);
- if (entry.ping >= 0) s.addProperty("ping", entry.ping);
- } else {
- var conn = nh.getConnection();
- if (conn != null && conn.getAddress() != null) {
- s.addProperty("address", stripLeadingSlash(conn.getAddress().toString()));
- }
- }
- return s;
- }
- //? } else {
- /*private static JsonObject buildPayload(Minecraft client, LocalPlayer player) {
- JsonObject o = new JsonObject();
-
o.addProperty("playerUuid", player.getUUID().toString());
o.addProperty("username", player.getName().getString());
o.addProperty("webviewMode", WebSession.mode().name());
//? if >=1.21.5 {
o.addProperty("dimension", player.level().dimension().identifier().toString());
//? } else {
- o.addProperty("dimension", player.level().dimension().location().toString());
- //? }
-
+ /*o.addProperty("dimension", player.level().dimension().location().toString());
+ *///? }
o.addProperty("health", player.getHealth());
o.addProperty("maxHealth", player.getMaxHealth());
o.addProperty("food", player.getFoodData().getFoodLevel());
o.addProperty("xpLevel", player.experienceLevel);
var gameMode = client.gameMode != null ? client.gameMode.getPlayerMode() : null;
if (gameMode != null) o.addProperty("gamemode", gameMode.getName());
-
JsonObject pos = new JsonObject();
pos.addProperty("x", player.getX());
pos.addProperty("y", player.getY());
pos.addProperty("z", player.getZ());
o.add("pos", pos);
-
JsonObject look = new JsonObject();
look.addProperty("yaw", player.getYRot());
look.addProperty("pitch", player.getXRot());
o.add("look", look);
-
// The one camera value a page cannot work out for itself - see the note on the
// Fabric side of this method.
o.addProperty("fov", fov(client));
o.add("lookingAt", buildLookingAt(client, player));
-
JsonObject server = buildServerInfo(client);
if (server != null) o.add("server", server);
-
return o;
}
-
// Vertical field of view in degrees, as the player set it.
private static int fov(Minecraft client) {
return client.options.fov().get();
}
-
// What the crosshair is on, if anything. This is the game's own answer, the one it
// uses for the block outline and the name above a mob, rather than a second ray cast
// with its own idea of reach. Always present, with type "none" for nothing - a field
@@ -287,9 +138,7 @@ private static JsonObject buildLookingAt(Minecraft client, LocalPlayer player) {
o.addProperty("type", "none");
return o;
}
-
o.addProperty("distance", Math.sqrt(player.getEyePosition().distanceToSqr(hit.getLocation())));
-
if (hit instanceof net.minecraft.world.phys.BlockHitResult block) {
o.addProperty("type", "block");
net.minecraft.core.BlockPos at = block.getBlockPos();
@@ -303,7 +152,6 @@ private static JsonObject buildLookingAt(Minecraft client, LocalPlayer player) {
.getKey(player.level().getBlockState(at).getBlock()).toString());
return o;
}
-
if (hit instanceof net.minecraft.world.phys.EntityHitResult entityHit) {
var entity = entityHit.getEntity();
o.addProperty("type", "entity");
@@ -317,11 +165,9 @@ private static JsonObject buildLookingAt(Minecraft client, LocalPlayer player) {
o.add("pos", pos);
return o;
}
-
o.addProperty("type", "none");
return o;
}
-
private static JsonObject buildServerInfo(Minecraft client) {
var nh = client.getConnection();
if (nh == null) return null;
@@ -337,6 +183,5 @@ private static JsonObject buildServerInfo(Minecraft client) {
}
}
return s;
- }*/
- //? }
+ }
}
diff --git a/src/main/java/land/webgui/WebviewClientEmit.java b/src/main/java/land/webgui/WebviewClientEmit.java
index 60323e1..042dffd 100644
--- a/src/main/java/land/webgui/WebviewClientEmit.java
+++ b/src/main/java/land/webgui/WebviewClientEmit.java
@@ -1,6 +1,6 @@
package land.webgui;
-import de.keksuccino.rinku.RinkuBrowser;
+import land.webgui.browser.WebBrowser;
import com.google.gson.Gson;
public final class WebviewClientEmit {
@@ -10,8 +10,8 @@ private WebviewClientEmit() {}
/** Called on the Minecraft client thread after receiving a WebviewEmitS2CPayload. */
public static void dispatch(String eventName, String jsonPayload) {
- RinkuBrowser main = WebSession.browser();
- RinkuBrowser hud = WebSession.hudBrowser();
+ WebBrowser main = WebSession.browser();
+ WebBrowser hud = WebSession.hudBrowser();
if (main == null && hud == null) return;
// GSON.toJson produces a properly escaped JS string literal, e.g. "\"my\\\"event\""
@@ -38,7 +38,7 @@ public static void dispatch(String eventName, String jsonPayload) {
* something to read, exactly as {@code window.webgui.client} and {@code .entity} do.
*/
public static void dispatchDeath(String jsonPayload) {
- RinkuBrowser main = WebSession.browser();
+ WebBrowser main = WebSession.browser();
if (main == null) return;
String data = (jsonPayload == null || jsonPayload.isBlank()) ? "null" : jsonPayload;
String js = "(function(){"
@@ -52,7 +52,7 @@ public static void dispatchDeath(String jsonPayload) {
executeJs(main, js);
}
- private static void executeJs(RinkuBrowser browser, String js) {
+ private static void executeJs(WebBrowser browser, String js) {
try {
String url = browser.getURL();
browser.executeJavaScript(js, url != null ? url : "", 0);
diff --git a/src/main/java/land/webgui/WebviewCommands.java b/src/main/java/land/webgui/WebviewCommands.java
index f9ea6b3..61c6dfb 100644
--- a/src/main/java/land/webgui/WebviewCommands.java
+++ b/src/main/java/land/webgui/WebviewCommands.java
@@ -6,15 +6,12 @@
import land.webgui.server.WebviewServerConfig;
//? if fabric {
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
-import net.minecraft.command.argument.EntityArgumentType;
-//? if >=1.21.5 {
-import net.minecraft.command.DefaultPermissions;
-//? }
-import net.minecraft.entity.Entity;
-import net.minecraft.server.command.CommandManager;
-import net.minecraft.server.command.ServerCommandSource;
-import net.minecraft.server.network.ServerPlayerEntity;
-import net.minecraft.text.Text;
+import net.minecraft.commands.arguments.EntityArgument;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.commands.Commands;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.network.chat.Component;
//? } else {
/*import net.minecraft.commands.Commands;
import net.minecraft.commands.CommandSourceStack;
@@ -32,23 +29,7 @@ public final class WebviewCommands {
private WebviewCommands() {}
- //? if fabric {
- private static int bindEntities(com.mojang.brigadier.context.CommandContext Saving it is the engine's job now, and so is choosing a safe name for it — a suggested
+ * name is the one part of a download that web content fully controls, and it is about to
+ * become a path. What is left here is the part that was always this mod's: saying so. A file
+ * that arrives with no trace is a file the player will never find, and the folder is not one
+ * they know about.
*
- * Everything lands in one folder under the game directory, named by the page's suggestion
- * with the path stripped out of it. The page never chooses where its bytes go: it is web
- * content, and a suggested name is the one part of a download an attacker fully controls.
+ * Where it lands is set once, in {@link land.webgui.browser.BrowserBoot}, rather than
+ * decided per download by a handler racing another mod's for the right to answer.
*/
public final class WebviewDownloadHandler {
@@ -27,73 +22,19 @@ public final class WebviewDownloadHandler {
private WebviewDownloadHandler() {}
public static void register() {
- // Rinku forwards this one itself, through a relay that lets several mods decide.
- Rinku.getClient().addDownloadHandler(new CefDownloadHandler() {
-
- @Override
- public boolean canDownload(CefBrowser browser, String url, String requestMethod) {
- return true;
- }
-
- @Override
- public void onBeforeDownload(CefBrowser browser, CefDownloadItem item,
- String suggestedName, CefBeforeDownloadCallback callback) {
- start(item, suggestedName, callback);
- }
-
- /**
- * Rinku's own variant, which also asks whether this handler took the decision.
- * Answering true stops it falling through to another mod's handler and saving
- * the same file twice.
- */
- @Override
- public boolean onBeforeDownloadWithDecision(CefBrowser browser, CefDownloadItem item,
- String suggestedName, CefBeforeDownloadCallback callback) {
- start(item, suggestedName, callback);
- return true;
- }
-
+ WebBrowser.addListener(new WebBrowser.Listener() {
@Override
- public void onDownloadUpdated(CefBrowser browser, CefDownloadItem item,
- CefDownloadItemCallback callback) {
- if (item != null && item.isComplete()) {
- // Told, not silent. A file that arrives with no trace is a file the
- // player will never find, and the folder is not one they know about.
- WebGUIMod.LOGGER.info("webgui: downloaded {}", item.getFullPath());
- tell("Saved to " + FOLDER + "/" + fileName(item.getFullPath()));
- } else if (item != null && item.isCanceled()) {
- WebGUIMod.LOGGER.info("webgui: download cancelled ({})", item.getURL());
+ public void onDownload(WebBrowser browser, Download download) {
+ if (download.done()) {
+ WebGUIMod.LOGGER.info("webgui: downloaded {}", download.path());
+ land.webgui.compat.Mc.tell("Saved to " + FOLDER + "/" + fileName(String.valueOf(download.path())));
+ } else if (download.canceled()) {
+ WebGUIMod.LOGGER.info("webgui: download cancelled ({})", download.url());
}
}
});
}
- private static void start(CefDownloadItem item, String suggestedName, CefBeforeDownloadCallback callback) {
- if (callback == null) {
- return;
- }
- try {
- Path target = WebviewDownloadNames.free(directory(), WebviewDownloadNames.safeName(suggestedName));
- Files.createDirectories(target.getParent());
- // false: no "where do you want to save this" dialog. There is no window to
- // put one in, and the answer is always the same folder.
- callback.Continue(target.toAbsolutePath().toString(), false);
- WebGUIMod.LOGGER.info("webgui: saving a download to {}", target);
- } catch (Exception e) {
- WebGUIMod.LOGGER.warn("webgui: could not start a download of {}: {}",
- item == null ? "?" : item.getURL(), e.toString());
- callback.Continue("", false);
- }
- }
-
- private static Path directory() {
- //? if fabric {
- return net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve(FOLDER);
- //? } else {
- /*return net.neoforged.fml.loading.FMLPaths.GAMEDIR.get().resolve(FOLDER);*/
- //? }
- }
-
private static String fileName(String fullPath) {
if (fullPath == null) {
return "?";
@@ -102,25 +43,4 @@ private static String fileName(String fullPath) {
return slash >= 0 ? fullPath.substring(slash + 1) : fullPath;
}
- private static void tell(String message) {
- //? if fabric {
- net.minecraft.client.MinecraftClient client = net.minecraft.client.MinecraftClient.getInstance();
- client.execute(() -> {
- if (client.player != null) {
- client.player.sendMessage(net.minecraft.text.Text.literal("[WebGUI] " + message), false);
- }
- });
- //? } else {
- /*net.minecraft.client.Minecraft client = net.minecraft.client.Minecraft.getInstance();
- client.execute(() -> {
- if (client.player != null) {
- //? if >=26 {
- client.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("[WebGUI] " + message));
- //? } else {
- client.player.displayClientMessage(net.minecraft.network.chat.Component.literal("[WebGUI] " + message), false);
- //? }
- }
- });*/
- //? }
- }
}
diff --git a/src/main/java/land/webgui/WebviewJoinHud.java b/src/main/java/land/webgui/WebviewJoinHud.java
index c2ec8f1..3238e2a 100644
--- a/src/main/java/land/webgui/WebviewJoinHud.java
+++ b/src/main/java/land/webgui/WebviewJoinHud.java
@@ -1,12 +1,11 @@
package land.webgui;
import land.webgui.server.WebviewServerConfig;
+import net.minecraft.server.level.ServerPlayer;
//? if fabric {
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
-import net.minecraft.server.network.ServerPlayerEntity;
//? } else {
-/*import net.minecraft.server.level.ServerPlayer;
-import net.neoforged.neoforge.common.NeoForge;
+/*import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;*/
//? }
@@ -18,11 +17,15 @@ private WebviewJoinHud() {}
public static void register() {
ServerPlayConnectionEvents.JOIN.register(
(handler, sender, server) -> {
- ServerPlayerEntity player = handler.player;
+ ServerPlayer player = handler.player;
if (player == null) {
return;
}
+ // Before the client check: this one is about the server's own version and
+ // is worth saying to an operator whose client has no WebGUI at all.
+ land.webgui.server.WebGUIUpdateChecker.notifyOnJoin(player);
+
// First: everything below is pointless for a client that cannot receive it,
// and this is also where such a client is told so.
if (!land.webgui.server.WebviewClientCheck.onJoin(player)) {
@@ -46,7 +49,7 @@ public static void register() {
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> {
if (handler.player != null) {
- land.webgui.server.WebviewRateLimiter.forget(handler.player.getUuid());
+ land.webgui.server.WebviewRateLimiter.forget(handler.player.getUUID());
}
});
}
@@ -60,6 +63,10 @@ public static void register() {
private static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) {
ServerPlayer player = (ServerPlayer) event.getEntity();
+ // Before the client check: this one is about the server's own version and is worth
+ // saying to an operator whose client has no WebGUI at all.
+ land.webgui.server.WebGUIUpdateChecker.notifyOnJoin(player);
+
// First: everything below is pointless for a client that cannot receive it, and
// this is also where such a client is told so.
if (!land.webgui.server.WebviewClientCheck.onJoin(player)) {
diff --git a/src/main/java/land/webgui/WebviewNetworking.java b/src/main/java/land/webgui/WebviewNetworking.java
index 7388531..7d6efab 100644
--- a/src/main/java/land/webgui/WebviewNetworking.java
+++ b/src/main/java/land/webgui/WebviewNetworking.java
@@ -11,9 +11,9 @@
//? } else {
/*import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
-import net.minecraft.network.PacketByteBuf;*/
+import net.minecraft.network.FriendlyByteBuf;*/
//? }
-import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.server.level.ServerPlayer;
//? } else {
/*import net.minecraft.server.level.ServerPlayer;
import net.neoforged.bus.api.IEventBus;
@@ -42,17 +42,24 @@ private WebviewNetworking() {}
//? if fabric {
public static void registerPayloadTypes() {
//? if >=1.20.5 {
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.OpenWebS2CPayload.ID, WebviewPayloads.OpenWebS2CPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.WebUIMainMenuPayload.ID, WebviewPayloads.WebUIMainMenuPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.WebviewEmitS2CPayload.ID, WebviewPayloads.WebviewEmitS2CPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.WebviewEntityContextS2CPayload.ID, WebviewPayloads.WebviewEntityContextS2CPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.WebviewTrustedOriginsS2CPayload.ID, WebviewPayloads.WebviewTrustedOriginsS2CPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.WebviewDeathS2CPayload.ID, WebviewPayloads.WebviewDeathS2CPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.WebviewHelloS2CPayload.ID, WebviewPayloads.WebviewHelloS2CPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.WebviewAssetManifestS2CPayload.ID, WebviewPayloads.WebviewAssetManifestS2CPayload.CODEC);
- PayloadTypeRegistry.playS2C().register(WebviewPayloads.WebviewAssetChunkS2CPayload.ID, WebviewPayloads.WebviewAssetChunkS2CPayload.CODEC);
- PayloadTypeRegistry.playC2S().register(WebviewPayloads.WebviewPageEventC2SPayload.ID, WebviewPayloads.WebviewPageEventC2SPayload.CODEC);
- PayloadTypeRegistry.playC2S().register(WebviewPayloads.WebviewAssetRequestC2SPayload.ID, WebviewPayloads.WebviewAssetRequestC2SPayload.CODEC);
+ //? if >=26 {
+ /*var toClient = PayloadTypeRegistry.clientboundPlay();
+ var toServer = PayloadTypeRegistry.serverboundPlay();
+ *///? } else {
+ var toClient = PayloadTypeRegistry.playS2C();
+ var toServer = PayloadTypeRegistry.playC2S();
+ //? }
+ toClient.register(WebviewPayloads.OpenWebS2CPayload.TYPE, WebviewPayloads.OpenWebS2CPayload.STREAM_CODEC);
+ toClient.register(WebviewPayloads.WebUIMainMenuPayload.TYPE, WebviewPayloads.WebUIMainMenuPayload.STREAM_CODEC);
+ toClient.register(WebviewPayloads.WebviewEmitS2CPayload.TYPE, WebviewPayloads.WebviewEmitS2CPayload.STREAM_CODEC);
+ toClient.register(WebviewPayloads.WebviewEntityContextS2CPayload.TYPE, WebviewPayloads.WebviewEntityContextS2CPayload.STREAM_CODEC);
+ toClient.register(WebviewPayloads.WebviewTrustedOriginsS2CPayload.TYPE, WebviewPayloads.WebviewTrustedOriginsS2CPayload.STREAM_CODEC);
+ toClient.register(WebviewPayloads.WebviewDeathS2CPayload.TYPE, WebviewPayloads.WebviewDeathS2CPayload.STREAM_CODEC);
+ toClient.register(WebviewPayloads.WebviewHelloS2CPayload.TYPE, WebviewPayloads.WebviewHelloS2CPayload.STREAM_CODEC);
+ toClient.register(WebviewPayloads.WebviewAssetManifestS2CPayload.TYPE, WebviewPayloads.WebviewAssetManifestS2CPayload.STREAM_CODEC);
+ toClient.register(WebviewPayloads.WebviewAssetChunkS2CPayload.TYPE, WebviewPayloads.WebviewAssetChunkS2CPayload.STREAM_CODEC);
+ toServer.register(WebviewPayloads.WebviewPageEventC2SPayload.TYPE, WebviewPayloads.WebviewPageEventC2SPayload.STREAM_CODEC);
+ toServer.register(WebviewPayloads.WebviewAssetRequestC2SPayload.TYPE, WebviewPayloads.WebviewAssetRequestC2SPayload.STREAM_CODEC);
//? }
}
//? } else {
@@ -128,21 +135,21 @@ public static void registerPayloadTypes() {
//? if fabric {
public static void registerServerReceivers() {
//? if >=1.20.5 {
- ServerPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewPageEventC2SPayload.ID, (payload, context) -> {
- ServerPlayerEntity player = context.player();
+ ServerPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewPageEventC2SPayload.TYPE, (payload, context) -> {
+ ServerPlayer player = context.player();
String channel = payload.channel();
String json = payload.jsonPayload();
// Checked here, off the server thread: scheduling first would put the flood on
// the tick loop, which is the thing being protected.
- if (player == null || !land.webgui.server.WebviewRateLimiter.allowPageEvent(player.getUuid(), player.getName().getString())) {
+ if (player == null || !land.webgui.server.WebviewRateLimiter.allowPageEvent(player.getUUID(), player.getName().getString())) {
return;
}
context.server().execute(() ->
WebviewServerEvents.PAGE_EVENT.invoker().onPageEvent(player, channel, json));
});
- ServerPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewAssetRequestC2SPayload.ID, (payload, context) -> {
- ServerPlayerEntity player = context.player();
+ ServerPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewAssetRequestC2SPayload.TYPE, (payload, context) -> {
+ ServerPlayer player = context.player();
if (player == null) {
return;
}
@@ -152,18 +159,18 @@ public static void registerServerReceivers() {
});
//? } else {
/*ServerPlayNetworking.registerGlobalReceiver(WebviewPayloads.PAGE_EVENT_CHANNEL, (server, player, handler, buf, responseSender) -> {
- String channel = buf.readString(WebviewPayloads.MAX_EVENT_NAME_LENGTH);
- String json = buf.readString(WebviewPayloads.MAX_EVENT_DATA_LENGTH);
+ String channel = buf.readUtf(WebviewPayloads.MAX_EVENT_NAME_LENGTH);
+ String json = buf.readUtf(WebviewPayloads.MAX_EVENT_DATA_LENGTH);
// Checked here, off the server thread: scheduling first would put the flood on
// the tick loop, which is the thing being protected.
- if (player == null || !land.webgui.server.WebviewRateLimiter.allowPageEvent(player.getUuid(), player.getName().getString())) {
+ if (player == null || !land.webgui.server.WebviewRateLimiter.allowPageEvent(player.getUUID(), player.getName().getString())) {
return;
}
server.execute(() -> WebviewServerEvents.PAGE_EVENT.invoker().onPageEvent(player, channel, json));
});
ServerPlayNetworking.registerGlobalReceiver(WebviewPayloads.ASSET_REQUEST_CHANNEL, (server, player, handler, buf, responseSender) -> {
- String path = buf.readString(WebviewPayloads.MAX_ASSET_PATH_LENGTH);
+ String path = buf.readUtf(WebviewPayloads.MAX_ASSET_PATH_LENGTH);
if (player == null) {
return;
}
@@ -178,40 +185,40 @@ public static void registerServerReceivers() {
//? }
//? if fabric {
- public static void openGui(ServerPlayerEntity player, String url) {
+ public static void openGui(ServerPlayer player, String url) {
clearEntityContext(player);
//? if >=1.20.5 {
ServerPlayNetworking.send(player, new WebviewPayloads.OpenWebS2CPayload(PROTOCOL_VERSION, MODE_GUI, withPlayerToken(player, url)));
//? } else {
- /*PacketByteBuf buf = PacketByteBufs.create();
+ /*FriendlyByteBuf buf = PacketByteBufs.create();
buf.writeVarInt(PROTOCOL_VERSION);
buf.writeVarInt(MODE_GUI);
- buf.writeString(withPlayerToken(player, url), MAX_URL_LENGTH);
+ buf.writeUtf(withPlayerToken(player, url), MAX_URL_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.OPEN_WEB_CHANNEL, buf);*/
//? }
}
- public static void openHud(ServerPlayerEntity player, String url) {
+ public static void openHud(ServerPlayer player, String url) {
clearEntityContext(player);
//? if >=1.20.5 {
ServerPlayNetworking.send(player, new WebviewPayloads.OpenWebS2CPayload(PROTOCOL_VERSION, MODE_HUD, withPlayerToken(player, url)));
//? } else {
- /*PacketByteBuf buf = PacketByteBufs.create();
+ /*FriendlyByteBuf buf = PacketByteBufs.create();
buf.writeVarInt(PROTOCOL_VERSION);
buf.writeVarInt(MODE_HUD);
- buf.writeString(withPlayerToken(player, url), MAX_URL_LENGTH);
+ buf.writeUtf(withPlayerToken(player, url), MAX_URL_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.OPEN_WEB_CHANNEL, buf);*/
//? }
}
- public static void openGuiForEntity(ServerPlayerEntity player, String url, String entityJson) {
+ public static void openGuiForEntity(ServerPlayer player, String url, String entityJson) {
//? if >=1.20.5 {
ServerPlayNetworking.send(player, new WebviewPayloads.OpenWebS2CPayload(PROTOCOL_VERSION, MODE_GUI, withPlayerToken(player, url)));
//? } else {
- /*PacketByteBuf buf = PacketByteBufs.create();
+ /*FriendlyByteBuf buf = PacketByteBufs.create();
buf.writeVarInt(PROTOCOL_VERSION);
buf.writeVarInt(MODE_GUI);
- buf.writeString(withPlayerToken(player, url), MAX_URL_LENGTH);
+ buf.writeUtf(withPlayerToken(player, url), MAX_URL_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.OPEN_WEB_CHANNEL, buf);*/
//? }
// The context goes out after the open, not before it. Opening a gui builds a new
@@ -221,39 +228,39 @@ public static void openGuiForEntity(ServerPlayerEntity player, String url, Strin
sendEntityContext(player, entityJson);
}
- public static void sendEntityContext(ServerPlayerEntity player, String entityJson) {
+ public static void sendEntityContext(ServerPlayer player, String entityJson) {
//? if >=1.20.5 {
ServerPlayNetworking.send(player, new WebviewPayloads.WebviewEntityContextS2CPayload(entityJson));
//? } else {
- /*PacketByteBuf buf = PacketByteBufs.create();
- buf.writeString(entityJson, WebviewPayloads.MAX_EVENT_DATA_LENGTH);
+ /*FriendlyByteBuf buf = PacketByteBufs.create();
+ buf.writeUtf(entityJson, WebviewPayloads.MAX_EVENT_DATA_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.ENTITY_CONTEXT_CHANNEL, buf);*/
//? }
}
- public static void clearEntityContext(ServerPlayerEntity player) {
+ public static void clearEntityContext(ServerPlayer player) {
sendEntityContext(player, "null");
}
- public static void emitToPage(ServerPlayerEntity player, String eventName, String jsonPayload) {
+ public static void emitToPage(ServerPlayer player, String eventName, String jsonPayload) {
String name = sanitizeStr(eventName, WebviewPayloads.MAX_EVENT_NAME_LENGTH);
String data = (jsonPayload == null || jsonPayload.isBlank()) ? "null" : sanitizeStr(jsonPayload, WebviewPayloads.MAX_EVENT_DATA_LENGTH);
//? if >=1.20.5 {
ServerPlayNetworking.send(player, new WebviewPayloads.WebviewEmitS2CPayload(name, data));
//? } else {
- /*PacketByteBuf buf = PacketByteBufs.create();
- buf.writeString(name, WebviewPayloads.MAX_EVENT_NAME_LENGTH);
- buf.writeString(data, WebviewPayloads.MAX_EVENT_DATA_LENGTH);
+ /*FriendlyByteBuf buf = PacketByteBufs.create();
+ buf.writeUtf(name, WebviewPayloads.MAX_EVENT_NAME_LENGTH);
+ buf.writeUtf(data, WebviewPayloads.MAX_EVENT_DATA_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.EMIT_TO_PAGE_CHANNEL, buf);*/
//? }
}
- public static void sendMainMenuUrl(ServerPlayerEntity player, String url) {
+ public static void sendMainMenuUrl(ServerPlayer player, String url) {
//? if >=1.20.5 {
ServerPlayNetworking.send(player, new WebviewPayloads.WebUIMainMenuPayload(sanitizeUrl(url)));
//? } else {
- /*PacketByteBuf buf = PacketByteBufs.create();
- buf.writeString(sanitizeUrl(url), MAX_URL_LENGTH);
+ /*FriendlyByteBuf buf = PacketByteBufs.create();
+ buf.writeUtf(sanitizeUrl(url), MAX_URL_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.MAIN_MENU_CHANNEL, buf);*/
//? }
}
@@ -265,15 +272,15 @@ public static void sendMainMenuUrl(ServerPlayerEntity player, String url) {
* The URL carries a per-player token like any other page the mod opens, so the
* death page can verify who it is talking about.
*/
- public static void sendDeathScreen(ServerPlayerEntity player, String url, String infoJson) {
+ public static void sendDeathScreen(ServerPlayer player, String url, String infoJson) {
String u = (url == null || url.isBlank()) ? "" : withPlayerToken(player, url);
String info = (infoJson == null || infoJson.isBlank()) ? "" : sanitizeStr(infoJson, WebviewPayloads.MAX_EVENT_DATA_LENGTH);
//? if >=1.20.5 {
ServerPlayNetworking.send(player, new WebviewPayloads.WebviewDeathS2CPayload(u, info));
//? } else {
- /*PacketByteBuf buf = PacketByteBufs.create();
- buf.writeString(u, MAX_URL_LENGTH);
- buf.writeString(info, WebviewPayloads.MAX_EVENT_DATA_LENGTH);
+ /*FriendlyByteBuf buf = PacketByteBufs.create();
+ buf.writeUtf(u, MAX_URL_LENGTH);
+ buf.writeUtf(info, WebviewPayloads.MAX_EVENT_DATA_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.DEATH_SCREEN_CHANNEL, buf);*/
//? }
}
@@ -282,9 +289,9 @@ public static void sendDeathScreen(ServerPlayerEntity player, String url, String
* Tells the client which WebGUI the server runs. Silently skipped for a client that
* has no such channel — that case is reported to the player as text instead.
*/
- public static void sendHello(ServerPlayerEntity player) {
+ public static void sendHello(ServerPlayer player) {
//? if >=1.20.5 {
- if (!ServerPlayNetworking.canSend(player, WebviewPayloads.WebviewHelloS2CPayload.ID)) {
+ if (!ServerPlayNetworking.canSend(player, WebviewPayloads.WebviewHelloS2CPayload.TYPE)) {
return;
}
ServerPlayNetworking.send(player, new WebviewPayloads.WebviewHelloS2CPayload(PROTOCOL_VERSION, WebGUIVersion.current()));
@@ -292,21 +299,21 @@ public static void sendHello(ServerPlayerEntity player) {
/*if (!ServerPlayNetworking.canSend(player, WebviewPayloads.HELLO_CHANNEL)) {
return;
}
- PacketByteBuf buf = PacketByteBufs.create();
+ FriendlyByteBuf buf = PacketByteBufs.create();
buf.writeVarInt(PROTOCOL_VERSION);
- buf.writeString(WebGUIVersion.current(), WebviewPayloads.MAX_VERSION_LENGTH);
+ buf.writeUtf(WebGUIVersion.current(), WebviewPayloads.MAX_VERSION_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.HELLO_CHANNEL, buf);*/
//? }
}
/** Whether the player's client has WebGUI, and whether it is new enough to talk to us. */
- public static ClientKind clientKind(ServerPlayerEntity player) {
+ public static ClientKind clientKind(ServerPlayer player) {
//? if >=1.20.5 {
- if (ServerPlayNetworking.canSend(player, WebviewPayloads.WebviewHelloS2CPayload.ID)) {
+ if (ServerPlayNetworking.canSend(player, WebviewPayloads.WebviewHelloS2CPayload.TYPE)) {
return ClientKind.CURRENT;
}
// No handshake channel but the original one is there: WebGUI, just an older build.
- return ServerPlayNetworking.canSend(player, WebviewPayloads.OpenWebS2CPayload.ID)
+ return ServerPlayNetworking.canSend(player, WebviewPayloads.OpenWebS2CPayload.TYPE)
? ClientKind.OUTDATED
: ClientKind.ABSENT;
//? } else {
@@ -320,10 +327,10 @@ public static ClientKind clientKind(ServerPlayerEntity player) {
}
/** Tells the client which pages this server ships. Skipped when it ships none. */
- public static void sendAssetManifest(ServerPlayerEntity player) {
+ public static void sendAssetManifest(ServerPlayer player) {
//? if >=1.20.5 {
if (!land.webgui.server.WebviewAssets.enabled()
- || !ServerPlayNetworking.canSend(player, WebviewPayloads.WebviewAssetManifestS2CPayload.ID)) {
+ || !ServerPlayNetworking.canSend(player, WebviewPayloads.WebviewAssetManifestS2CPayload.TYPE)) {
return;
}
land.webgui.server.WebviewAssetStore store = land.webgui.server.WebviewAssets.store();
@@ -335,9 +342,9 @@ public static void sendAssetManifest(ServerPlayerEntity player) {
return;
}
land.webgui.server.WebviewAssetStore store = land.webgui.server.WebviewAssets.store();
- PacketByteBuf buf = PacketByteBufs.create();
- buf.writeString(store.revision(), WebviewPayloads.MAX_VERSION_LENGTH);
- buf.writeString(store.toManifest(), WebviewPayloads.MAX_MANIFEST_LENGTH);
+ FriendlyByteBuf buf = PacketByteBufs.create();
+ buf.writeUtf(store.revision(), WebviewPayloads.MAX_VERSION_LENGTH);
+ buf.writeUtf(store.toManifest(), WebviewPayloads.MAX_MANIFEST_LENGTH);
ServerPlayNetworking.send(player, WebviewPayloads.ASSET_MANIFEST_CHANNEL, buf);*/
//? }
}
@@ -349,10 +356,10 @@ public static void sendAssetManifest(ServerPlayerEntity player) {
* and a page that hangs forever is far worse to diagnose than one that reports a
* missing file.
*/
- public static void serveAsset(ServerPlayerEntity player, String requested) {
+ public static void serveAsset(ServerPlayer player, String requested) {
String path = land.webgui.server.WebviewAssetStore.normalizeRequest(requested);
java.util.List Built as directories of empty files rather than by unpacking a real bundle: what is being
+ * tested is whether the landmarks are recognised, and an empty file is as recognisable as a
+ * two hundred megabyte one. It also means the macOS shape is covered from any machine.
+ */
+class BundleLayoutTest {
+
+ private static Path tree(Path root, String... entries) throws IOException {
+ for (String entry : entries) {
+ Path path = root.resolve(entry);
+ Files.createDirectories(path.getParent());
+ Files.writeString(path, "");
+ }
+ return root;
+ }
+
+ private static Path windows(Path root) throws IOException {
+ return tree(root, "libcef.dll", "jcef.dll", "chrome_elf.dll", "jcef_helper.exe",
+ "icudtl.dat", "resources.pak", "locales/en-US.pak");
+ }
+
+ private static Path linux(Path root) throws IOException {
+ return tree(root, "libcef.so", "libjcef.so", "jcef_helper",
+ "icudtl.dat", "resources.pak", "locales/en-US.pak");
+ }
+
+ private static Path macos(Path root) throws IOException {
+ return tree(root,
+ "Chromium Embedded Framework.framework/Chromium Embedded Framework",
+ "Chromium Embedded Framework.framework/Resources/icudtl.dat",
+ "Chromium Embedded Framework.framework/Resources/en.lproj/locale.pak",
+ "libjcef.dylib",
+ "jcef Helper.app/Contents/MacOS/jcef Helper",
+ "jcef Helper (GPU).app/Contents/MacOS/jcef Helper (GPU)");
+ }
+
+ @Test
+ void findsEverythingInAWindowsBundle(@TempDir Path root) throws IOException {
+ BundleLayout layout = BundleLayout.discover(windows(root));
+
+ assertEquals(root.resolve("libcef.dll"), layout.library());
+ assertEquals(root.resolve("jcef_helper.exe"), layout.helper());
+ assertEquals(root, layout.resourcesDirectory());
+ assertEquals(root.resolve("locales"), layout.localesDirectory().orElseThrow());
+ assertEquals(root.resolve("jcef.dll"), layout.nativeLibrary("jcef").orElseThrow());
+ }
+
+ @Test
+ void findsEverythingInALinuxBundle(@TempDir Path root) throws IOException {
+ BundleLayout layout = BundleLayout.discover(linux(root));
+
+ assertEquals(root.resolve("libcef.so"), layout.library());
+ assertEquals(root.resolve("jcef_helper"), layout.helper());
+ assertEquals(root, layout.resourcesDirectory());
+ assertTrue(layout.localesDirectory().isPresent());
+ }
+
+ @Test
+ void findsEverythingInAMacosBundle(@TempDir Path root) throws IOException {
+ BundleLayout layout = BundleLayout.discover(macos(root));
+
+ assertEquals(root.resolve("Chromium Embedded Framework.framework")
+ .resolve("Chromium Embedded Framework"), layout.library());
+ // Resources live inside the framework there, not beside the library, which is exactly
+ // the difference a hard-coded per-platform table gets wrong.
+ assertEquals(root.resolve("Chromium Embedded Framework.framework").resolve("Resources"),
+ layout.resourcesDirectory());
+ assertTrue(layout.localesDirectory().isEmpty(),
+ "macOS keeps locales inside the framework resources and CEF finds them itself");
+ }
+
+ @Test
+ void picksTheMainHelperRatherThanOneOfItsVariants(@TempDir Path root) throws IOException {
+ // A macOS bundle ships four helpers. Naming the GPU one as the subprocess path starts
+ // every renderer as a GPU process, which fails in a way that points nowhere useful.
+ BundleLayout layout = BundleLayout.discover(macos(root));
+
+ assertEquals("jcef Helper", layout.helper().getFileName().toString());
+ }
+
+ @Test
+ void saysWhatIsMissingRatherThanFailingLater(@TempDir Path root) throws IOException {
+ tree(root, "libcef.dll", "icudtl.dat");
+
+ InstallException e = assertThrows(InstallException.class, () -> BundleLayout.discover(root));
+ assertTrue(e.getMessage().contains("helper"), e.getMessage());
+ assertTrue(e.getMessage().contains(root.toString()), e.getMessage());
+ }
+
+ @Test
+ void refusesADirectoryThatIsNotThere(@TempDir Path root) {
+ assertThrows(InstallException.class, () -> BundleLayout.discover(root.resolve("nothing")));
+ }
+
+ @Test
+ void findsTheFrameworkOnMacosAndNowhereElse(@TempDir Path root) throws IOException {
+ assertEquals("Chromium Embedded Framework.framework",
+ BundleLayout.discover(macos(root)).frameworkDirectory().orElseThrow()
+ .getFileName().toString());
+ assertTrue(BundleLayout.discover(windows(root.resolve("win"))).frameworkDirectory().isEmpty());
+ }
+}
diff --git a/src/test/java/space/webgui/wmc/core/CefCursorsTest.java b/src/test/java/space/webgui/wmc/core/CefCursorsTest.java
new file mode 100644
index 0000000..ec20e29
--- /dev/null
+++ b/src/test/java/space/webgui/wmc/core/CefCursorsTest.java
@@ -0,0 +1,54 @@
+package space.webgui.wmc.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.awt.Cursor;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Which cursor the page asked for.
+ *
+ * Worth a test for a reason that is not obvious from the code: the number java-cef reports
+ * is an AWT cursor, and it was read as Chromium's own list first. Both lists start with the
+ * ordinary arrow at zero and disagree about nearly everything after it, so the mistake is
+ * invisible in a screenshot — the cursor changes as the mouse moves, and is the wrong shape
+ * everywhere. What gave it away was that {@code text} never once appeared.
+ */
+class CefCursorsTest {
+
+ @Test
+ void theHandIsWhatCssCallsPointer() {
+ // The one that matters most: it is what says a thing can be clicked. Read as
+ // Chromium's list this number is a resize arrow.
+ assertEquals("pointer", CefCursors.cssName(Cursor.HAND_CURSOR));
+ }
+
+ @Test
+ void theBeamOverTextIsText() {
+ // And read as Chromium's list, this one is the hand - the other half of the swap.
+ assertEquals("text", CefCursors.cssName(Cursor.TEXT_CURSOR));
+ }
+
+ @Test
+ void theArrowIsTheDefault() {
+ assertEquals("default", CefCursors.cssName(Cursor.DEFAULT_CURSOR));
+ }
+
+ @Test
+ void theResizeArrowsKeepTheirDirections() {
+ assertEquals("e-resize", CefCursors.cssName(Cursor.E_RESIZE_CURSOR));
+ assertEquals("w-resize", CefCursors.cssName(Cursor.W_RESIZE_CURSOR));
+ assertEquals("n-resize", CefCursors.cssName(Cursor.N_RESIZE_CURSOR));
+ assertEquals("nw-resize", CefCursors.cssName(Cursor.NW_RESIZE_CURSOR));
+ assertEquals("move", CefCursors.cssName(Cursor.MOVE_CURSOR));
+ }
+
+ @Test
+ void anythingElseIsAnArrowRatherThanAGuess() {
+ // AWT has fourteen shapes and CSS has thirty. A number outside the list is either a
+ // cursor AWT has no name for or a library that has changed underneath; either way the
+ // arrow is right and inventing a name is not.
+ assertEquals("default", CefCursors.cssName(99));
+ assertEquals("default", CefCursors.cssName(-1));
+ }
+}
diff --git a/src/test/java/space/webgui/wmc/core/GlfwKeysTest.java b/src/test/java/space/webgui/wmc/core/GlfwKeysTest.java
new file mode 100644
index 0000000..fc88de8
--- /dev/null
+++ b/src/test/java/space/webgui/wmc/core/GlfwKeysTest.java
@@ -0,0 +1,91 @@
+package space.webgui.wmc.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The key translation, which has no other way of being checked.
+ *
+ * A wrong entry here is not a crash. It is a key that does nothing in a text field, or a
+ * shortcut that fires the wrong thing, found by a player rather than by a build — and there is
+ * no test that can be written in a game for "backspace deletes a character" that is cheaper
+ * than this one.
+ */
+class GlfwKeysTest {
+
+ @Test
+ void lettersAndDigitsPassStraightThrough() {
+ // GLFW numbers these by the character on the key and so does AWT, which is why most of
+ // a keyboard needs no table at all.
+ assertEquals(KeyEvent.VK_A, GlfwKeys.toAwtKey('A'));
+ assertEquals(KeyEvent.VK_Z, GlfwKeys.toAwtKey('Z'));
+ assertEquals(KeyEvent.VK_0, GlfwKeys.toAwtKey('0'));
+ assertEquals(KeyEvent.VK_9, GlfwKeys.toAwtKey('9'));
+ }
+
+ @Test
+ void theKeysATextFieldActuallyNeeds() {
+ assertEquals(KeyEvent.VK_BACK_SPACE, GlfwKeys.toAwtKey(259));
+ assertEquals(KeyEvent.VK_DELETE, GlfwKeys.toAwtKey(261));
+ assertEquals(KeyEvent.VK_ENTER, GlfwKeys.toAwtKey(257));
+ assertEquals(KeyEvent.VK_TAB, GlfwKeys.toAwtKey(258));
+ assertEquals(KeyEvent.VK_LEFT, GlfwKeys.toAwtKey(263));
+ assertEquals(KeyEvent.VK_RIGHT, GlfwKeys.toAwtKey(262));
+ assertEquals(KeyEvent.VK_HOME, GlfwKeys.toAwtKey(268));
+ assertEquals(KeyEvent.VK_END, GlfwKeys.toAwtKey(269));
+ assertEquals(KeyEvent.VK_ESCAPE, GlfwKeys.toAwtKey(256));
+ }
+
+ @Test
+ void bothSidesOfTheKeyboardAreTheSameKey() {
+ // Left and right shift are different GLFW codes and one AWT key. A table that only
+ // covers the left one leaves half a keyboard not holding shift.
+ assertEquals(KeyEvent.VK_SHIFT, GlfwKeys.toAwtKey(340));
+ assertEquals(KeyEvent.VK_SHIFT, GlfwKeys.toAwtKey(344));
+ assertEquals(KeyEvent.VK_CONTROL, GlfwKeys.toAwtKey(341));
+ assertEquals(KeyEvent.VK_CONTROL, GlfwKeys.toAwtKey(345));
+ assertEquals(KeyEvent.VK_ALT, GlfwKeys.toAwtKey(342));
+ assertEquals(KeyEvent.VK_ALT, GlfwKeys.toAwtKey(346));
+ }
+
+ @Test
+ void theNumberPadIsContiguous() {
+ for (int digit = 0; digit <= 9; digit++) {
+ assertEquals(KeyEvent.VK_NUMPAD0 + digit, GlfwKeys.toAwtKey(320 + digit),
+ "numpad " + digit);
+ }
+ assertEquals(KeyEvent.VK_ENTER, GlfwKeys.toAwtKey(335), "the numpad enter is still enter");
+ }
+
+ @Test
+ void functionKeysCoverF1ToF12() {
+ for (int index = 0; index < 12; index++) {
+ assertNotEquals(KeyEvent.VK_UNDEFINED, GlfwKeys.toAwtKey(290 + index), "F" + (index + 1));
+ }
+ assertEquals(KeyEvent.VK_F1, GlfwKeys.toAwtKey(290));
+ assertEquals(KeyEvent.VK_F12, GlfwKeys.toAwtKey(301));
+ }
+
+ @Test
+ void anythingUnknownIsUndefinedRatherThanWrong() {
+ // Better than a plausible-looking guess: an undefined key still delivers its character
+ // through typing, which is what a text field reads. A wrong key code does not.
+ assertEquals(KeyEvent.VK_UNDEFINED, GlfwKeys.toAwtKey(9999));
+ assertEquals(KeyEvent.VK_UNDEFINED, GlfwKeys.toAwtKey(-1));
+ }
+
+ @Test
+ void modifiersBecomeTheMasksJavaCefReads() {
+ assertEquals(0, GlfwKeys.toAwtModifiers(0));
+ assertEquals(InputEvent.SHIFT_DOWN_MASK, GlfwKeys.toAwtModifiers(0x1));
+ assertEquals(InputEvent.CTRL_DOWN_MASK, GlfwKeys.toAwtModifiers(0x2));
+ assertEquals(InputEvent.ALT_DOWN_MASK, GlfwKeys.toAwtModifiers(0x4));
+ assertEquals(InputEvent.META_DOWN_MASK, GlfwKeys.toAwtModifiers(0x8));
+ assertEquals(InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK,
+ GlfwKeys.toAwtModifiers(0x3));
+ }
+}
diff --git a/src/test/java/space/webgui/wmc/core/Jars.java b/src/test/java/space/webgui/wmc/core/Jars.java
new file mode 100644
index 0000000..7418d2c
--- /dev/null
+++ b/src/test/java/space/webgui/wmc/core/Jars.java
@@ -0,0 +1,37 @@
+package space.webgui.wmc.core;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/** Wraps bundles in a jar the way the published artifacts do. */
+final class Jars {
+
+ private Jars() {
+ }
+
+ /** A jar holding the given entries, each a name and its bytes. */
+ static byte[] holding(String name, byte[] content) throws IOException {
+ return holding(name, content, null, null);
+ }
+
+ static byte[] holding(String first, byte[] firstContent, String second, byte[] secondContent)
+ throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (ZipOutputStream zip = new ZipOutputStream(bytes)) {
+ zip.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
+ zip.write("Manifest-Version: 1.0\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ zip.closeEntry();
+ zip.putNextEntry(new ZipEntry(first));
+ zip.write(firstContent);
+ zip.closeEntry();
+ if (second != null) {
+ zip.putNextEntry(new ZipEntry(second));
+ zip.write(secondContent);
+ zip.closeEntry();
+ }
+ }
+ return bytes.toByteArray();
+ }
+}
diff --git a/src/test/java/space/webgui/wmc/core/NativeBundlesTest.java b/src/test/java/space/webgui/wmc/core/NativeBundlesTest.java
new file mode 100644
index 0000000..833b93a
--- /dev/null
+++ b/src/test/java/space/webgui/wmc/core/NativeBundlesTest.java
@@ -0,0 +1,67 @@
+package space.webgui.wmc.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import space.webgui.wmc.api.Platform;
+
+class NativeBundlesTest {
+
+ @Test
+ void everyBundleIsPinnedToADigestAndASize() {
+ assertTrue(NativeBundles.all().size() >= 6);
+ for (Map.Entry Separate from {@link ArchivesTest} because what that one checks — the execute bit, a
+ * symbolic link — cannot be checked on a filesystem that has neither, so it is skipped on
+ * Windows, which is where this is most likely to be run. What a header says is readable
+ * anywhere, and the header is the part written here; applying it to a file is not.
+ */
+class TarTest {
+
+ private static Tar open(byte[] archive) throws IOException {
+ return new Tar(new GZIPInputStream(new ByteArrayInputStream(archive)));
+ }
+
+ @Test
+ void readsNamesSizesAndContentInOrder() throws IOException {
+ byte[] archive = new TarGz()
+ .file("first.txt", "one")
+ .file("nested/second.txt", "twotwo")
+ .build();
+
+ try (Tar tar = open(archive)) {
+ Tar.Entry first = tar.next();
+ assertEquals("first.txt", first.name());
+ assertEquals(3, first.size());
+ assertEquals("one", new String(tar.readAllBytes(), StandardCharsets.UTF_8));
+
+ Tar.Entry second = tar.next();
+ assertEquals("nested/second.txt", second.name());
+ assertEquals("twotwo", new String(tar.readAllBytes(), StandardCharsets.UTF_8));
+
+ assertNull(tar.next(), "the archive should end after its entries");
+ }
+ }
+
+ @Test
+ void stopsReadingAtTheEndOfAnEntry() throws IOException {
+ // The whole point of bounding the stream: handed to Files.copy, an entry must not
+ // run on into the padding and the next header.
+ byte[] archive = new TarGz().file("a", "aaa").file("b", "bbb").build();
+
+ try (Tar tar = open(archive)) {
+ tar.next();
+ assertEquals("aaa", new String(tar.readAllBytes(), StandardCharsets.UTF_8));
+ assertEquals(-1, tar.read(), "the entry should be exhausted");
+ assertEquals("b", tar.next().name());
+ }
+ }
+
+ @Test
+ void skipsAnEntryNobodyRead() throws IOException {
+ // A directory or a link is never read from, and the bytes of a file may be skipped
+ // too; the next header still has to be found.
+ byte[] archive = new TarGz().file("big", "x".repeat(1500)).file("after", "y").build();
+
+ try (Tar tar = open(archive)) {
+ assertEquals("big", tar.next().name());
+ assertEquals("after", tar.next().name(), "an unread entry should be stepped over");
+ }
+ }
+
+ @Test
+ void keepsTheMode() throws IOException {
+ byte[] archive = new TarGz()
+ .file("chrome-sandbox", "#!", 0755)
+ .file("icudtl.dat", "data", 0644)
+ .build();
+
+ try (Tar tar = open(archive)) {
+ assertEquals(0755, tar.next().mode(), "an executable's mode should survive");
+ assertEquals(0644, tar.next().mode());
+ }
+ }
+
+ @Test
+ void readsASymbolicLinkAndWhereItPoints() throws IOException {
+ byte[] archive = new TarGz().symlink("Current", "Versions/A").build();
+
+ try (Tar tar = open(archive)) {
+ Tar.Entry link = tar.next();
+ assertTrue(link.isSymbolicLink(), "the type flag should say it is a link");
+ assertFalse(link.isFile());
+ assertEquals("Versions/A", link.linkName());
+ }
+ }
+
+ @Test
+ void readsADirectory() throws IOException {
+ byte[] archive = new TarGz().directory("locales").file("locales/en-US.pak", "p").build();
+
+ try (Tar tar = open(archive)) {
+ Tar.Entry directory = tar.next();
+ assertTrue(directory.isDirectory());
+ assertFalse(directory.isFile());
+ assertEquals("locales/en-US.pak", tar.next().name());
+ }
+ }
+
+ @Test
+ void readsANameThatDoesNotFitTheHeader() throws IOException {
+ // Past a hundred characters the writer moves the name out of the header into a PAX
+ // record, and the header carries a stand-in name instead. Reading the header alone
+ // gives the wrong path for a good part of a Chromium bundle.
+ String deep = "a-directory-with-a-long-name/".repeat(6) + "chrome-sandbox";
+ assertTrue(deep.length() > 100);
+ byte[] archive = new TarGz().file(deep, "x", 0755).build();
+
+ try (Tar tar = open(archive)) {
+ Tar.Entry entry = tar.next();
+ assertEquals(deep, entry.name());
+ assertEquals(0755, entry.mode(), "the mode belongs to the entry, not to the record");
+ }
+ }
+
+ @Test
+ void readsALinkTargetThatDoesNotFitTheHeader() throws IOException {
+ String deep = "Versions/" + "a-long-framework-directory/".repeat(5) + "Helper";
+ assertTrue(deep.length() > 100);
+ byte[] archive = new TarGz().symlink("Current", deep).build();
+
+ try (Tar tar = open(archive)) {
+ Tar.Entry link = tar.next();
+ assertTrue(link.isSymbolicLink());
+ assertEquals(deep, link.linkName());
+ }
+ }
+
+ @Test
+ void endsOnAnArchiveWithNothingInIt() throws IOException {
+ try (Tar tar = open(new TarGz().build())) {
+ assertNull(tar.next());
+ }
+ }
+}
diff --git a/versions/1.20.1-fabric/gradle.properties b/versions/1.20.1-fabric/gradle.properties
index b53c61a..c0946c9 100644
--- a/versions/1.20.1-fabric/gradle.properties
+++ b/versions/1.20.1-fabric/gradle.properties
@@ -1,8 +1,6 @@
minecraft_version=1.20.1
-yarn_mappings=1.20.1+build.10
loader_version=0.19.5
fabric_version=0.92.12+1.20.1
-rinku_version=3.0.4-1.20.1-fabric
minecraft_range=>=1.20.1 <=1.20.1
loader_type=fabric
modmenu_version=7.2.2
diff --git a/versions/1.20.1-neoforge/gradle.properties b/versions/1.20.1-neoforge/gradle.properties
index 5ce3888..4b0a8bd 100644
--- a/versions/1.20.1-neoforge/gradle.properties
+++ b/versions/1.20.1-neoforge/gradle.properties
@@ -1,5 +1,4 @@
minecraft_version=1.20.1
loader_type=neoforge
neoforge_version=47.3.0
-rinku_version=3.0.4-1.20.1-forge
minecraft_range=[1.20.1]
diff --git a/versions/1.21.1-fabric/gradle.properties b/versions/1.21.1-fabric/gradle.properties
index 8e85f5d..a0c1c44 100644
--- a/versions/1.21.1-fabric/gradle.properties
+++ b/versions/1.21.1-fabric/gradle.properties
@@ -1,8 +1,6 @@
minecraft_version=1.21.1
-yarn_mappings=1.21.1+build.3
loader_version=0.19.5
fabric_version=0.116.17+1.21.1
-rinku_version=3.0.4-1.21.1-fabric
minecraft_range=>=1.21 <=1.21.1
loader_type=fabric
modmenu_version=11.0.4
diff --git a/versions/1.21.1-neoforge/gradle.properties b/versions/1.21.1-neoforge/gradle.properties
index f6e27a8..1c2f08d 100644
--- a/versions/1.21.1-neoforge/gradle.properties
+++ b/versions/1.21.1-neoforge/gradle.properties
@@ -1,5 +1,4 @@
minecraft_version=1.21.1
loader_type=neoforge
-neoforge_version=21.1.250
-rinku_version=3.0.4-1.21.1-neoforge
+neoforge_version=21.1.259
minecraft_range=[1.21,1.21.1]
diff --git a/versions/1.21.11-fabric/gradle.properties b/versions/1.21.11-fabric/gradle.properties
index eb81029..87208b7 100644
--- a/versions/1.21.11-fabric/gradle.properties
+++ b/versions/1.21.11-fabric/gradle.properties
@@ -1,8 +1,6 @@
minecraft_version=1.21.11
-yarn_mappings=1.21.11+build.6
loader_version=0.19.5
fabric_version=0.141.6+1.21.11
-rinku_version=3.0.4-1.21.11-fabric
minecraft_range=>=1.21.5 <=1.21.11
loader_type=fabric
-modmenu_version=17.0.0
+modmenu_version=17.0.1
diff --git a/versions/1.21.11-neoforge/gradle.properties b/versions/1.21.11-neoforge/gradle.properties
index 9fb04a6..12c23f3 100644
--- a/versions/1.21.11-neoforge/gradle.properties
+++ b/versions/1.21.11-neoforge/gradle.properties
@@ -1,5 +1,4 @@
minecraft_version=1.21.11
loader_type=neoforge
neoforge_version=21.11.45
-rinku_version=3.0.4-1.21.11-neoforge
minecraft_range=[1.21.5,1.21.11]
diff --git a/versions/26.1.2-neoforge/gradle.properties b/versions/26.1.2-neoforge/gradle.properties
index 1ef1c8e..6094e67 100644
--- a/versions/26.1.2-neoforge/gradle.properties
+++ b/versions/26.1.2-neoforge/gradle.properties
@@ -1,6 +1,5 @@
minecraft_version=26.1.2
loader_type=neoforge
-neoforge_version=26.1.2.107
-rinku_version=3.0.4-26.1.2-neoforge
+neoforge_version=26.1.2.116
minecraft_range=[26.1.2,26.1.2]
java_version=25
diff --git a/versions/26.2-fabric/gradle.properties b/versions/26.2-fabric/gradle.properties
new file mode 100644
index 0000000..b0542f4
--- /dev/null
+++ b/versions/26.2-fabric/gradle.properties
@@ -0,0 +1,9 @@
+minecraft_version=26.2
+loader_type=fabric
+loader_version=0.19.5
+fabric_version=0.161.0+26.2
+modmenu_version=20.0.3
+minecraft_range=>=26.2 <=26.2
+java_version=25
+# From 26.1 the game ships unobfuscated: no mappings, no remapping.
+unobfuscated=true
diff --git a/versions/26.2-neoforge/gradle.properties b/versions/26.2-neoforge/gradle.properties
index 1636ad0..3f4d9d1 100644
--- a/versions/26.2-neoforge/gradle.properties
+++ b/versions/26.2-neoforge/gradle.properties
@@ -1,6 +1,5 @@
minecraft_version=26.2
loader_type=neoforge
-neoforge_version=26.2.0.82
-rinku_version=3.0.4-26.2-neoforge
+neoforge_version=26.2.0.90
minecraft_range=[26.2,26.2]
java_version=25
diff --git a/versions/26.3-fabric/gradle.properties b/versions/26.3-fabric/gradle.properties
new file mode 100644
index 0000000..e0cca34
--- /dev/null
+++ b/versions/26.3-fabric/gradle.properties
@@ -0,0 +1,9 @@
+minecraft_version=26.3
+loader_type=fabric
+loader_version=0.19.5
+fabric_version=0.162.0+26.3
+modmenu_version=21.0.0
+minecraft_range=>=26.3 <=26.3
+java_version=25
+# From 26.1 the game ships unobfuscated: no mappings, no remapping.
+unobfuscated=true
diff --git a/versions/26.3-neoforge/gradle.properties b/versions/26.3-neoforge/gradle.properties
new file mode 100644
index 0000000..4f892ab
--- /dev/null
+++ b/versions/26.3-neoforge/gradle.properties
@@ -0,0 +1,6 @@
+minecraft_version=26.3
+loader_type=neoforge
+# The 26.3 line has no stable NeoForge yet, so this target is published as a beta.
+neoforge_version=26.3.0.73-beta
+minecraft_range=[26.3,26.3]
+java_version=25