From cb64a580d960ed91160c3acf81dc914cc1ad66d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Sun, 13 Sep 2026 00:09:40 +0500 Subject: [PATCH 01/14] =?UTF-8?q?feat(devtools):=20=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B8=D0=BD=D1=81=D0=BF?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BE=D1=80=20=D0=B2=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=BC=20=D0=BE=D0=BA=D0=BD=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BE=20F12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chromium сам открывает окно инструментов — это его окно, не AWT и не игры, и порт отладки для этого не нужен. Внутри веб-экрана F12 перехватывается до страницы, вне экрана работает как кейбинд и открывает инспектор для HUD. --- .../land/webgui/WebGUIDevToolsWindow.java | 33 +++++++++++++++++++ src/main/java/land/webgui/WebGUIKeys.java | 26 ++++++++++++++- src/main/java/land/webgui/WebViewScreen.java | 13 ++++++++ .../resources/assets/webgui/lang/en_us.json | 1 + .../resources/assets/webgui/lang/ru_ru.json | 1 + 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/main/java/land/webgui/WebGUIDevToolsWindow.java diff --git a/src/main/java/land/webgui/WebGUIDevToolsWindow.java b/src/main/java/land/webgui/WebGUIDevToolsWindow.java new file mode 100644 index 0000000..45001a7 --- /dev/null +++ b/src/main/java/land/webgui/WebGUIDevToolsWindow.java @@ -0,0 +1,33 @@ +package land.webgui; + +import de.keksuccino.rinku.RinkuBrowser; + +/** + * The inspector, in a window of its own, the way a browser opens it. + * + * Chromium opens that window itself: the request goes to its own ShowDevTools, which is a + * Chromium pop-up and not something the game or AWT has to host, and it paints from the + * message loop the client already drives. So there is nothing here but the asking. + * + * Different from {@link WebGUIDevTools}, which puts the page's own complaints in the game log + * and needs no window at all. + */ +public final class WebGUIDevToolsWindow { + + private WebGUIDevToolsWindow() {} + + /** Whether there was a page to open them for. */ + public static boolean open(RinkuBrowser browser) { + if (browser == null) { + return false; + } + try { + browser.openDevTools(); + WebGUIMod.LOGGER.info("webgui devtools: opened the inspector for {}", browser.getURL()); + return true; + } catch (Throwable t) { + WebGUIMod.LOGGER.warn("webgui devtools: could not open the inspector: {}", t.toString()); + return false; + } + } +} diff --git a/src/main/java/land/webgui/WebGUIKeys.java b/src/main/java/land/webgui/WebGUIKeys.java index 3ca0fa8..742f0f9 100644 --- a/src/main/java/land/webgui/WebGUIKeys.java +++ b/src/main/java/land/webgui/WebGUIKeys.java @@ -30,9 +30,11 @@ public final class WebGUIKeys { //? if fabric { private static KeyBinding mainMenu; private static KeyBinding hudInteractive; + private static KeyBinding devtools; //? } else { /*private static KeyMapping mainMenu; - private static KeyMapping hudInteractive;*/ + private static KeyMapping hudInteractive; + private static KeyMapping devtools;*/ //? } private WebGUIKeys() {} @@ -51,6 +53,11 @@ public static void register() { InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_GRAVE_ACCENT, cat)); + devtools = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "key.webgui.devtools", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_F12, + cat)); //? } else { /*mainMenu = KeyBindingHelper.registerKeyBinding(new KeyBinding( "key.webgui.main_menu", @@ -61,6 +68,11 @@ public static void register() { "key.webgui.hud_interactive", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_GRAVE_ACCENT, + WebGUIMod.MOD_ID + ".web")); + devtools = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "key.webgui.devtools", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_F12, WebGUIMod.MOD_ID + ".web"));*/ //? } } @@ -74,12 +86,15 @@ private static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) { 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); + devtools = new KeyMapping("key.webgui.devtools", InputConstants.Type.KEYSYM, GLFW.GLFW_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"); + devtools = new KeyMapping("key.webgui.devtools", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_F12, WebGUIMod.MOD_ID + ".web"); //? } event.register(mainMenu); event.register(hudInteractive); + event.register(devtools); }*/ //? } @@ -110,6 +125,15 @@ public static void tick(MinecraftClient client) { } WebHudOverlay.toggleInteractive(client); } + //? if fabric { + while (devtools.wasPressed()) { + //? } else { + /*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 fabric { diff --git a/src/main/java/land/webgui/WebViewScreen.java b/src/main/java/land/webgui/WebViewScreen.java index 1a8a12d..c07c0ee 100644 --- a/src/main/java/land/webgui/WebViewScreen.java +++ b/src/main/java/land/webgui/WebViewScreen.java @@ -2,6 +2,7 @@ import de.keksuccino.rinku.Rinku; import de.keksuccino.rinku.RinkuBrowser; +import org.lwjgl.glfw.GLFW; //? if >=1.21.5 { //? if fabric { import net.minecraft.client.gl.RenderPipelines; @@ -380,6 +381,10 @@ public boolean mouseScrolled(double mouseX, double mouseY, double amount) { public boolean keyPressed(KeyInput input) { if (super.keyPressed(input)) return true; if (browser == null) return false; + if (input.key() == GLFW.GLFW_KEY_F12) { + WebGUIDevToolsWindow.open(browser); + return true; + } browser.sendKeyPress(input.key(), input.scancode(), input.modifiers()); browser.setFocus(true); return true; @@ -409,6 +414,10 @@ public boolean charTyped(CharInput input) { public boolean keyPressed(KeyEvent input) { if (super.keyPressed(input)) return true; if (browser == null) return false; + if (input.key() == GLFW.GLFW_KEY_F12) { + WebGUIDevToolsWindow.open(browser); + return true; + } browser.sendKeyPress(input.key(), input.scancode(), input.modifiers()); browser.setFocus(true); return true; @@ -447,6 +456,10 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (browser == null) { return false; } + if (keyCode == GLFW.GLFW_KEY_F12) { + WebGUIDevToolsWindow.open(browser); + return true; + } browser.sendKeyPress(keyCode, scanCode, modifiers); browser.setFocus(true); return true; diff --git a/src/main/resources/assets/webgui/lang/en_us.json b/src/main/resources/assets/webgui/lang/en_us.json index f869498..853f07e 100644 --- a/src/main/resources/assets/webgui/lang/en_us.json +++ b/src/main/resources/assets/webgui/lang/en_us.json @@ -1,6 +1,7 @@ { "key.webgui.main_menu": "Main menu", "key.webgui.hud_interactive": "HUD: interactive mode", + "key.webgui.devtools": "Developer tools", "key.categories.webgui.web": "WebGUI", "screen.webgui.title": "Web", "screen.webgui.url": "URL", diff --git a/src/main/resources/assets/webgui/lang/ru_ru.json b/src/main/resources/assets/webgui/lang/ru_ru.json index 9cf3954..27239a5 100644 --- a/src/main/resources/assets/webgui/lang/ru_ru.json +++ b/src/main/resources/assets/webgui/lang/ru_ru.json @@ -1,6 +1,7 @@ { "key.webgui.main_menu": "Главное меню", "key.webgui.hud_interactive": "HUD: интерактивный режим", + "key.webgui.devtools": "Инструменты разработчика", "key.categories.webgui.web": "WebGUI", "screen.webgui.title": "Веб", "screen.webgui.url": "URL", From 14dd0453ea092f15ff3e8028db3b960eb6c4a407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 02:39:23 +0500 Subject: [PATCH 02/14] =?UTF-8?q?feat(browser):=20=D0=B2=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B8=D1=82=D1=8C=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=BE=D0=BA?= =?UTF-8?q?=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=B7=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=20=D1=81=D1=82=D0=BE=D1=80=D0=BE=D0=BD=D0=BD=D0=B5=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Движок и адаптер к нему лежат внутри мода, отдельная мод-библиотека больше не нужна. Chromium по-прежнему скачивается при первом запуске один раз на машину, вне папки игры. Добавлены таргеты 26.2-fabric, 26.3-fabric и 26.3-neoforge. Обращения к игре, переехавшие между версиями, собраны в land.webgui.compat, чтобы при следующем переходе править одно место. Найдено ручной проверкой и исправлено здесь же: - миксин экрана смерти целился в Minecraft.setScreen, которого нет с 26.2, из-за чего игра не стартовала на Fabric 26.2 и 26.3; - клиент не завершался при выходе: сервер страниц и Chromium держали процесс живым; - на 26.3 не доходил текстовый ввод (SDL отдаёт символы только при включённом text input) и не менялся курсор (форма запрашивается покадрово); - Enter не давал перевод строки и не отправлял форму ни на одной версии. --- build.gradle | 213 +++-- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 11 +- .../webgui/EntityInteractionListener.java | 23 +- .../java/land/webgui/WebGUIAssetServer.java | 14 + .../java/land/webgui/WebGUIBrowserTuning.java | 46 ++ src/main/java/land/webgui/WebGUIClient.java | 128 +-- .../java/land/webgui/WebGUIClientConfig.java | 32 + src/main/java/land/webgui/WebGUIDevTools.java | 24 +- .../land/webgui/WebGUIDevToolsWindow.java | 23 +- .../java/land/webgui/WebGUIHandshake.java | 33 +- src/main/java/land/webgui/WebGUIKeys.java | 187 ++--- .../land/webgui/WebGUISettingsScreen.java | 140 +--- src/main/java/land/webgui/WebHudOverlay.java | 347 +++----- src/main/java/land/webgui/WebSession.java | 21 +- src/main/java/land/webgui/WebViewLayout.java | 18 +- src/main/java/land/webgui/WebViewScreen.java | 254 ++---- .../webgui/WebviewBrowserConsoleLogger.java | 40 +- .../java/land/webgui/WebviewClientBridge.java | 185 +---- .../java/land/webgui/WebviewClientEmit.java | 10 +- .../java/land/webgui/WebviewCommands.java | 116 +-- .../land/webgui/WebviewDownloadHandler.java | 114 +-- src/main/java/land/webgui/WebviewJoinHud.java | 9 +- .../java/land/webgui/WebviewNetworking.java | 143 ++-- .../land/webgui/WebviewPageLoadHooks.java | 42 +- .../webgui/WebviewPageToClientBridge.java | 78 +- .../java/land/webgui/WebviewPayloads.java | 278 +------ .../java/land/webgui/WebviewScriptInject.java | 40 + src/main/java/land/webgui/api/WebviewApi.java | 67 +- .../java/land/webgui/browser/BrowserBoot.java | 91 ++ .../land/webgui/browser/BrowserCursor.java | 141 ++++ .../land/webgui/browser/BrowserTexture.java | 280 +++++++ .../java/land/webgui/browser/SdlKeys.java | 196 +++++ .../java/land/webgui/browser/WebBrowser.java | 543 ++++++++++++ src/main/java/land/webgui/compat/Ids.java | 55 ++ src/main/java/land/webgui/compat/Mc.java | 90 ++ .../java/land/webgui/compat/TextInput.java | 34 + .../java/land/webgui/mixin/CefUtilMixin.java | 32 - .../land/webgui/mixin/DeathScreenMixin.java | 36 +- .../java/land/webgui/mixin/MouseMixin.java | 192 ++--- .../webgui/server/WebviewClientCheck.java | 42 +- .../webgui/server/WebviewDeathListener.java | 35 +- .../webgui/server/WebviewEntityContext.java | 21 +- .../webgui/server/WebviewPlaceholders.java | 27 +- .../webgui/server/WebviewServerEvents.java | 11 +- .../webgui/server/WebviewSignedToken.java | 18 +- .../org/cef/browser/OffscreenBrowser.java | 622 ++++++++++++++ .../java/space/webgui/wmc/api/Channel.java | 66 ++ .../space/webgui/wmc/api/ChannelPolicy.java | 105 +++ .../space/webgui/wmc/api/ConsoleLevel.java | 9 + .../java/space/webgui/wmc/api/Damage.java | 27 + .../java/space/webgui/wmc/api/Devtools.java | 73 ++ .../java/space/webgui/wmc/api/Download.java | 43 + src/main/java/space/webgui/wmc/api/Frame.java | 92 +++ .../java/space/webgui/wmc/api/ImeEvent.java | 35 + src/main/java/space/webgui/wmc/api/Input.java | 72 ++ .../space/webgui/wmc/api/MouseButton.java | 8 + .../java/space/webgui/wmc/api/Platform.java | 36 + .../java/space/webgui/wmc/api/Resource.java | 66 ++ .../space/webgui/wmc/api/RuntimeHolder.java | 77 ++ .../space/webgui/wmc/api/RuntimeInfo.java | 16 + .../space/webgui/wmc/api/RuntimeListener.java | 27 + .../space/webgui/wmc/api/RuntimeState.java | 23 + .../space/webgui/wmc/api/SchemeHandler.java | 24 + .../space/webgui/wmc/api/SchemeRequest.java | 34 + .../java/space/webgui/wmc/api/WebRuntime.java | 50 ++ .../java/space/webgui/wmc/api/WebView.java | 101 +++ .../space/webgui/wmc/api/WebViewListener.java | 56 ++ .../space/webgui/wmc/api/WebViewSpec.java | 81 ++ .../space/webgui/wmc/api/package-info.java | 21 + .../java/space/webgui/wmc/core/Archives.java | 193 +++++ .../space/webgui/wmc/core/BundleLayout.java | 195 +++++ .../space/webgui/wmc/core/CefCursors.java | 54 ++ .../space/webgui/wmc/core/CefDevtools.java | 215 +++++ .../java/space/webgui/wmc/core/CefFrame.java | 312 +++++++ .../java/space/webgui/wmc/core/CefHost.java | 781 ++++++++++++++++++ .../java/space/webgui/wmc/core/CefInput.java | 168 ++++ .../java/space/webgui/wmc/core/CefLoader.java | 70 ++ .../space/webgui/wmc/core/CefRuntime.java | 396 +++++++++ .../space/webgui/wmc/core/CefWebView.java | 224 +++++ .../java/space/webgui/wmc/core/Checksum.java | 56 ++ .../java/space/webgui/wmc/core/GlfwKeys.java | 123 +++ .../java/space/webgui/wmc/core/HttpFetch.java | 58 ++ .../webgui/wmc/core/HttpNativeSource.java | 42 + .../webgui/wmc/core/InstallException.java | 18 + .../webgui/wmc/core/MavenNativeSource.java | 91 ++ .../space/webgui/wmc/core/NativeArtifact.java | 28 + .../space/webgui/wmc/core/NativeBundles.java | 76 ++ .../webgui/wmc/core/NativeInstaller.java | 152 ++++ .../space/webgui/wmc/core/NativeSource.java | 30 + .../wmc/core/RangeIgnoredException.java | 32 + .../space/webgui/wmc/core/RuntimePaths.java | 85 ++ .../webgui/wmc/core/RuntimeSettings.java | 183 ++++ .../java/space/webgui/wmc/core/ScanCodes.java | 83 ++ src/main/java/space/webgui/wmc/core/Tar.java | 340 ++++++++ .../resources/META-INF/neoforge.mods.toml | 14 +- .../services/space.webgui.wmc.api.WebRuntime | 1 + .../resources/assets/webgui/lang/en_us.json | 6 +- .../resources/assets/webgui/lang/ru_ru.json | 6 +- src/main/resources/fabric.mod.json | 2 +- src/main/resources/webgui.mixins.json | 3 +- .../land/webgui/WebGUIModMetadataTest.java | 6 +- .../webgui/wmc/api/ChannelPolicyTest.java | 31 + .../space/webgui/wmc/api/ImeEventTest.java | 28 + .../space/webgui/wmc/api/WebViewSpecTest.java | 32 + .../space/webgui/wmc/core/ArchivesTest.java | 162 ++++ .../webgui/wmc/core/BundleLayoutTest.java | 116 +++ .../space/webgui/wmc/core/CefCursorsTest.java | 54 ++ .../space/webgui/wmc/core/GlfwKeysTest.java | 91 ++ src/test/java/space/webgui/wmc/core/Jars.java | 37 + .../webgui/wmc/core/NativeBundlesTest.java | 67 ++ .../webgui/wmc/core/RuntimePathsTest.java | 54 ++ .../java/space/webgui/wmc/core/TarGz.java | 58 ++ .../java/space/webgui/wmc/core/TarTest.java | 147 ++++ versions/1.20.1-fabric/gradle.properties | 2 - versions/1.20.1-neoforge/gradle.properties | 1 - versions/1.21.1-fabric/gradle.properties | 2 - versions/1.21.1-neoforge/gradle.properties | 3 +- versions/1.21.11-fabric/gradle.properties | 4 +- versions/1.21.11-neoforge/gradle.properties | 1 - versions/26.1.2-neoforge/gradle.properties | 3 +- versions/26.2-fabric/gradle.properties | 9 + versions/26.2-neoforge/gradle.properties | 3 +- versions/26.3-fabric/gradle.properties | 9 + versions/26.3-neoforge/gradle.properties | 6 + 126 files changed, 9149 insertions(+), 2062 deletions(-) create mode 100644 src/main/java/land/webgui/WebGUIBrowserTuning.java create mode 100644 src/main/java/land/webgui/browser/BrowserBoot.java create mode 100644 src/main/java/land/webgui/browser/BrowserCursor.java create mode 100644 src/main/java/land/webgui/browser/BrowserTexture.java create mode 100644 src/main/java/land/webgui/browser/SdlKeys.java create mode 100644 src/main/java/land/webgui/browser/WebBrowser.java create mode 100644 src/main/java/land/webgui/compat/Ids.java create mode 100644 src/main/java/land/webgui/compat/Mc.java create mode 100644 src/main/java/land/webgui/compat/TextInput.java delete mode 100644 src/main/java/land/webgui/mixin/CefUtilMixin.java create mode 100644 src/main/java/org/cef/browser/OffscreenBrowser.java create mode 100644 src/main/java/space/webgui/wmc/api/Channel.java create mode 100644 src/main/java/space/webgui/wmc/api/ChannelPolicy.java create mode 100644 src/main/java/space/webgui/wmc/api/ConsoleLevel.java create mode 100644 src/main/java/space/webgui/wmc/api/Damage.java create mode 100644 src/main/java/space/webgui/wmc/api/Devtools.java create mode 100644 src/main/java/space/webgui/wmc/api/Download.java create mode 100644 src/main/java/space/webgui/wmc/api/Frame.java create mode 100644 src/main/java/space/webgui/wmc/api/ImeEvent.java create mode 100644 src/main/java/space/webgui/wmc/api/Input.java create mode 100644 src/main/java/space/webgui/wmc/api/MouseButton.java create mode 100644 src/main/java/space/webgui/wmc/api/Platform.java create mode 100644 src/main/java/space/webgui/wmc/api/Resource.java create mode 100644 src/main/java/space/webgui/wmc/api/RuntimeHolder.java create mode 100644 src/main/java/space/webgui/wmc/api/RuntimeInfo.java create mode 100644 src/main/java/space/webgui/wmc/api/RuntimeListener.java create mode 100644 src/main/java/space/webgui/wmc/api/RuntimeState.java create mode 100644 src/main/java/space/webgui/wmc/api/SchemeHandler.java create mode 100644 src/main/java/space/webgui/wmc/api/SchemeRequest.java create mode 100644 src/main/java/space/webgui/wmc/api/WebRuntime.java create mode 100644 src/main/java/space/webgui/wmc/api/WebView.java create mode 100644 src/main/java/space/webgui/wmc/api/WebViewListener.java create mode 100644 src/main/java/space/webgui/wmc/api/WebViewSpec.java create mode 100644 src/main/java/space/webgui/wmc/api/package-info.java create mode 100644 src/main/java/space/webgui/wmc/core/Archives.java create mode 100644 src/main/java/space/webgui/wmc/core/BundleLayout.java create mode 100644 src/main/java/space/webgui/wmc/core/CefCursors.java create mode 100644 src/main/java/space/webgui/wmc/core/CefDevtools.java create mode 100644 src/main/java/space/webgui/wmc/core/CefFrame.java create mode 100644 src/main/java/space/webgui/wmc/core/CefHost.java create mode 100644 src/main/java/space/webgui/wmc/core/CefInput.java create mode 100644 src/main/java/space/webgui/wmc/core/CefLoader.java create mode 100644 src/main/java/space/webgui/wmc/core/CefRuntime.java create mode 100644 src/main/java/space/webgui/wmc/core/CefWebView.java create mode 100644 src/main/java/space/webgui/wmc/core/Checksum.java create mode 100644 src/main/java/space/webgui/wmc/core/GlfwKeys.java create mode 100644 src/main/java/space/webgui/wmc/core/HttpFetch.java create mode 100644 src/main/java/space/webgui/wmc/core/HttpNativeSource.java create mode 100644 src/main/java/space/webgui/wmc/core/InstallException.java create mode 100644 src/main/java/space/webgui/wmc/core/MavenNativeSource.java create mode 100644 src/main/java/space/webgui/wmc/core/NativeArtifact.java create mode 100644 src/main/java/space/webgui/wmc/core/NativeBundles.java create mode 100644 src/main/java/space/webgui/wmc/core/NativeInstaller.java create mode 100644 src/main/java/space/webgui/wmc/core/NativeSource.java create mode 100644 src/main/java/space/webgui/wmc/core/RangeIgnoredException.java create mode 100644 src/main/java/space/webgui/wmc/core/RuntimePaths.java create mode 100644 src/main/java/space/webgui/wmc/core/RuntimeSettings.java create mode 100644 src/main/java/space/webgui/wmc/core/ScanCodes.java create mode 100644 src/main/java/space/webgui/wmc/core/Tar.java create mode 100644 src/main/resources/META-INF/services/space.webgui.wmc.api.WebRuntime create mode 100644 src/test/java/space/webgui/wmc/api/ChannelPolicyTest.java create mode 100644 src/test/java/space/webgui/wmc/api/ImeEventTest.java create mode 100644 src/test/java/space/webgui/wmc/api/WebViewSpecTest.java create mode 100644 src/test/java/space/webgui/wmc/core/ArchivesTest.java create mode 100644 src/test/java/space/webgui/wmc/core/BundleLayoutTest.java create mode 100644 src/test/java/space/webgui/wmc/core/CefCursorsTest.java create mode 100644 src/test/java/space/webgui/wmc/core/GlfwKeysTest.java create mode 100644 src/test/java/space/webgui/wmc/core/Jars.java create mode 100644 src/test/java/space/webgui/wmc/core/NativeBundlesTest.java create mode 100644 src/test/java/space/webgui/wmc/core/RuntimePathsTest.java create mode 100644 src/test/java/space/webgui/wmc/core/TarGz.java create mode 100644 src/test/java/space/webgui/wmc/core/TarTest.java create mode 100644 versions/26.2-fabric/gradle.properties create mode 100644 versions/26.3-fabric/gradle.properties create mode 100644 versions/26.3-neoforge/gradle.properties diff --git a/build.gradle b/build.gradle index 5b08e02..d52ff87 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,19 @@ plugins { - id 'fabric-loom' version '1.16.+' apply false + // Two Loom plugins, one artifact and one version. Minecraft 26.1 ships unobfuscated, so + // there is nothing left to remap and no mapping file to ask for; the older versions still + // need both. Fabric splits that into two plugin ids rather than one plugin with a switch, + // and because they are the same artifact they sit on the build classpath together — + // which is what makes one Gradle build able to cover both eras at all. + id 'net.fabricmc.fabric-loom-remap' version '1.18.4' apply false + id 'net.fabricmc.fabric-loom' version '1.18.4' apply false id 'net.neoforged.moddev' version '2.0.+' apply false id 'maven-publish' } +// Whether this Minecraft ships unobfuscated. Set in the version's own gradle.properties, not +// worked out from the number: the line is where Mojang drew it, not where a comparison would. +def unobfuscated = project.findProperty('unobfuscated') == 'true' + // Version-specific properties are loaded automatically by Stonecutter // from versions/{active}/gradle.properties. version = "${mod_version}+mc${minecraft_version}" @@ -15,11 +25,9 @@ stonecutter.constants { put "fabric", (sc_loader == "fabric") } -// NeoForge uses Mojang mappings: currentScreen → screen -if (sc_loader != "fabric") { - stonecutter.swaps { - put "currentScreen", "screen" - } +// Both loaders are on Mojang mappings now, so this is no longer loader-specific. +stonecutter.swaps { + put "currentScreen", "screen" } base { @@ -31,7 +39,7 @@ base { // Apply the loader-specific plugin after the stonecutter constants are set if (sc_loader == "fabric") { - apply plugin: 'fabric-loom' + apply plugin: unobfuscated ? 'net.fabricmc.fabric-loom' : 'net.fabricmc.fabric-loom-remap' } else { apply plugin: 'net.neoforged.moddev' } @@ -57,37 +65,26 @@ repositories { if (sc_loader == "fabric") { dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" - mappings "net.fabricmc:yarn:${yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${loader_version}" - modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}" - include(modImplementation("maven.modrinth:rinku:${rinku_version}")) - // Compiled against, never shipped and never required: it is only needed to - // implement the entrypoint that puts a Config button in its list. - modCompileOnly("maven.modrinth:modmenu:${modmenu_version}") + if (unobfuscated) { + // No mappings at all, and nothing is remapped: from 26.1 the game carries its own + // names and its parameter names with it. A dependency needs no `mod` prefix for + // the same reason — there is nothing to translate on the way in. + implementation "net.fabricmc:fabric-loader:${loader_version}" + implementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}" + compileOnly("maven.modrinth:modmenu:${modmenu_version}") + } else { + // Mojang's own, not Yarn. Yarn stopped at 1.21.11 and is no longer supported by + // Fabric, which tells mods to move before going near Minecraft 26 — so both + // loaders now speak the same names and this mod has one set, not two. + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}" + // Compiled against, never shipped and never required: it is only needed to + // implement the entrypoint that puts a Config button in its list. + modCompileOnly("maven.modrinth:modmenu:${modmenu_version}") + } } } else { - configurations { - rinkuArtifact - } - - // Rinku is a client-side library, and NeoForge's dev dist cleaner strips - // the client classes it constructs against. A plain `runtimeOnly` feeds - // BOTH dev runs, so runServer used to die during mod loading with - // NoClassDefFoundError on Screen — on every Minecraft 26 build, and never - // caught because 26.x had no server smoke test. moddev's per-run classpath - // is gone on modern Minecraft ("no additional classpath anymore"), so scope - // it by installing the jar into the client run's mods folder instead. That - // also mirrors production, where a server operator simply never installs - // Rinku and WebGUI never declares it as a dependency. - def installRinkuForClientRun = tasks.register('installRinkuForClientRun', Copy) { - from configurations.rinkuArtifact - into layout.projectDirectory.dir('run/mods') - } - - def removeRinkuForServerRun = tasks.register('removeRinkuForServerRun', Delete) { - delete fileTree(layout.projectDirectory.dir('run/mods')) { include 'rinku*.jar' } - } - neoForge { version = project.neoforge_version // Register the mod so moddev's JUnit runtime can load it during tests. @@ -106,72 +103,95 @@ if (sc_loader == "fabric") { runs { client { client() - // Rinku reaches the client the way a player installs it: as a - // jar in the mods folder, not on the runtime classpath. Keeping - // it off the classpath is what keeps runServer alive — see the - // installRinkuForClientRun task below. - taskBefore(installRinkuForClientRun) } server { server() - // Both dev runs share versions//run, so the jar dropped in - // for the client is also visible to the server — which is exactly - // what kills it. Take it back out before the server starts. - taskBefore(removeRinkuForServerRun) } } } +} +// --- The browser --- +// +// Chromium is ours now: there is no browser mod to install beside this one, and nothing +// here depends on one. What is needed at build time is the java-cef Java classes — a couple +// of hundred kilobytes of JNI declarations — and they are baked into the jar below rather +// than shipped as a library of their own. +// +// Nothing is relocated on the way in. The native side binds to these classes by name, so +// `org.cef.Foo` moved to `land.webgui.shadow.org.cef.Foo` links against nothing at the first +// JNI call. It also has to land in the same jar as our own `org.cef.browser.OffscreenBrowser`, +// which reaches package-private state upstream never exposed: split across two jars, NeoForge's +// module layer refuses the package outright. +// +// The version must stay equal to NativeBundles.JCEF_VERSION — the classes are the JNI +// declarations for the libraries in that exact bundle, and a mismatch is a missing symbol at +// the first call rather than an error here. WmcNativeBundlesTest asserts the two agree. +def jcefVersion = 'jcef-ca49ada+cef-135.0.20+ge7de5c3+chromium-135.0.7049.85' - // Rinku ships its NeoForge builds inconsistently: 26.2 has classes at the - // top level, but 1.21.1 and 26.1.2 are jarJar wrappers whose real mod sits - // in META-INF/jarjar/. NeoForge unpacks that at runtime, so the game is - // happy either way — javac is not, and sees an empty jar. Compile against - // whatever actually holds the classes; the published artifact stays on the - // runtime classpath untouched. - dependencies { - rinkuArtifact "maven.modrinth:rinku:${rinku_version}" - } +configurations { + jcef + // Compiled against, never handed to the loader as a library: the classes are unpacked + // into this mod's own output below, so they are part of it everywhere. + compileOnly.extendsFrom jcef +} - def rinkuClassesDir = layout.buildDirectory.dir('rinku-compile') - def unwrapRinku = tasks.register('unwrapRinku') { - def artifact = configurations.rinkuArtifact - inputs.files(artifact) - outputs.dir(rinkuClassesDir) - doLast { - def dir = rinkuClassesDir.get().asFile - dir.deleteDir() - dir.mkdirs() - def outer = artifact.singleFile - def target = new File(dir, 'rinku.jar') - new java.util.zip.ZipFile(outer).withCloseable { zip -> - def nested = zip.entries().toList().find { - it.name.startsWith('META-INF/jarjar/') && it.name.endsWith('.jar') - } - if (nested != null) { - target.withOutputStream { out -> out << zip.getInputStream(nested) } - logger.lifecycle("rinku: compiling against nested ${nested.name}") - } else { - target.bytes = outer.bytes - } - } - } +dependencies { + // Its OpenGL dependencies exist for the AWT-canvas browser upstream provides, which is + // the one thing a Minecraft integration can never use: ten megabytes for a class that is + // never loaded. + jcef("me.friwi:jcef-api:${jcefVersion}") { + exclude group: 'me.friwi', module: 'jogl-all' + exclude group: 'me.friwi', module: 'gluegen-rt' } +} - dependencies { - // Point at the jar itself, not the directory holding it — a directory - // on the compile classpath is searched for loose .class files only. - compileOnly files(rinkuClassesDir.map { it.file('rinku.jar') }) { - builtBy unwrapRinku - } +// Unpacked into this mod's own class output rather than kept as a library beside it. +// +// One mechanism covering three places at once: the jar gets the classes, a dev run gets them, +// and — the reason it has to be this way — NeoForge puts every mod in a JPMS module, where a +// package may live in only one. Our own org.cef.browser.OffscreenBrowser reaches +// package-private state upstream never exposed, so it must sit in that package; with +// jcef-api beside the mod as a module of its own, the game refuses to start: +// +// Module webgui contains package org.cef.browser, module jcef.api... exports package +// org.cef.browser to webgui +// +// Fabric's flat class loader does not care, which is why this only showed up on the other +// loader, three targets in. +def unpackJcef = tasks.register('unpackJcef', Copy) { + from({ configurations.jcef.collect { zipTree(it) } }) { + // Upstream's own metadata, which says nothing about this mod: an index that is now + // wrong, signatures that no longer match, and a module declaration that would put + // these classes straight back into a module of their own. + exclude 'META-INF/INDEX.LIST' + exclude 'META-INF/*.SF' + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' + exclude 'META-INF/MANIFEST.MF' + exclude 'module-info.class' + // Kept, and renamed so it does not sit on top of ours: java-cef is BSD-3-Clause and + // its terms travel with the bytes. + rename(~/^LICENSE\.txt$/, 'META-INF/licenses/jcef-LICENSE.txt') + rename(~/^README\.txt$/, 'META-INF/licenses/jcef-README.txt') } + into layout.buildDirectory.dir('jcef-classes') } +sourceSets.main.output.dir(layout.buildDirectory.dir('jcef-classes'), builtBy: unpackJcef) + // --- Testing --- dependencies { testImplementation platform("org.junit:junit-bom:5.10.2") testImplementation "org.junit.jupiter:junit-jupiter" testRuntimeOnly "org.junit.platform:junit-platform-launcher" + // Test-only, and deliberately: the tar reader is checked against archives a real tar + // writer produced, not against ones written by the same code that reads them. Nothing + // of this reaches the jar. + // Pinned old and kept non-transitive on purpose. Only the tar writer is wanted, and + // newer releases drag in commons-io and commons-codec at versions Minecraft pins + // strictly to older ones — which resolves to nothing at all on NeoForge 1.21.1. + testImplementation("org.apache.commons:commons-compress:1.21") { transitive = false } if (sc_loader == "fabric") { // Bootstraps Minecraft + the Fabric loader inside JUnit so the launch // smoke test (@Tag("launch")) can assert the mod actually loads. @@ -181,6 +201,8 @@ dependencies { } test { + // So a test can insist the Java classes and the pinned bundle name the same build. + systemProperty 'wmc.jcefVersion', jcefVersion useJUnitPlatform { if (sc_loader == "fabric") { // Fabric runs the launch test in the separate `launchTest` task so @@ -210,9 +232,25 @@ tasks.register('launchTest', Test) { } } +// By number, so that "26.3" sorts after "1.21.11" rather than before it. +static boolean atLeast(String version, String floor) { + def have = version.tokenize('.').collect { it.toInteger() } + def want = floor.tokenize('.').collect { it.toInteger() } + for (int i = 0; i < Math.max(have.size(), want.size()); i++) { + int a = i < have.size() ? have[i] : 0 + int b = i < want.size() ? want[i] : 0 + if (a != b) { + return a > b + } + } + return true +} + processResources { inputs.property 'version', project.version inputs.property 'minecraft_range', project.minecraft_range + // Declared, or the rename below is invisible to the up-to-date check. + inputs.property 'iconKey', atLeast(project.minecraft_version, '26.3') if (sc_loader == "fabric") { // Don't ship NeoForge metadata in the Fabric jar, or loaders/Modrinth // mis-detect it as a NeoForge mod too. @@ -223,12 +261,16 @@ processResources { } else { // Likewise, keep Fabric metadata out of the NeoForge jar. The death-screen // mixin goes with it: NeoForge replaces the screen through ScreenEvent - // instead, and the mixin targets Yarn's MinecraftClient, which does not - // exist under Mojang mappings. + // instead, so applying the mixin there would swap the screen twice. exclude 'fabric.mod.json' exclude 'webgui.fabric.mixins.json' filesMatching('META-INF/neoforge.mods.toml') { expand version: project.version, minecraft_range: project.minecraft_range + // NeoForge renamed logoFile to iconFile in 26.3 and warns about the old name. + // Renamed here, since earlier versions still need the file as it is. + if (atLeast(project.minecraft_version, '26.3')) { + filter { line -> line.replaceFirst(/^logoFile(\s*)=/, 'iconFile$1=') } + } } filesMatching('pack.mcmeta') { expand version: project.version } } @@ -268,6 +310,7 @@ def javaVersion = (project.findProperty('java_version') ?: '21').toString().toIn tasks.withType(JavaCompile).configureEach { it.options.encoding = 'UTF-8' + it.options.compilerArgs << '-Xmaxerrs' << '3000' it.options.release = javaVersion } diff --git a/gradle.properties b/gradle.properties index a74b415..990990c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx4G org.gradle.parallel=true dev.kikugie.stonecutter.hard_mode=true -mod_version=1.8.1 +mod_version=2.0.0 maven_group=land.webgui archives_base_name=webgui diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5dd3c01..1e922f4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index 0f04619..ae68c7d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,10 +16,15 @@ stonecutter { centralScript = 'build.gradle' create(rootProject) { - // MC 26 (NeoForge only — Yarn mappings for 26.x are not published). - // Both lines are stable: 26.2 is Mojang's current release and NeoForge - // dropped the -beta suffix on the 26.2 line at build 26.2.0.57. + // MC 26, both loaders. The game is unobfuscated from 26.1, so Fabric here builds + // with a different Loom plugin than the versions below — see the plugins block in + // build.gradle. One Gradle build still covers both eras. + // 26.3 is Mojang's newest stable. NeoForge has only a beta on that line, so the + // release workflow publishes both 26.3 targets as beta rather than release. + version '26.3-neoforge', '26.3-neoforge' + version '26.3-fabric', '26.3-fabric' version '26.2-neoforge', '26.2-neoforge' + version '26.2-fabric', '26.2-fabric' version '26.1.2-neoforge', '26.1.2-neoforge' version '1.21.11-fabric', '1.21.11-fabric' version '1.21.11-neoforge', '1.21.11-neoforge' diff --git a/src/main/java/land/webgui/EntityInteractionListener.java b/src/main/java/land/webgui/EntityInteractionListener.java index f0877b1..e70eb09 100644 --- a/src/main/java/land/webgui/EntityInteractionListener.java +++ b/src/main/java/land/webgui/EntityInteractionListener.java @@ -3,16 +3,13 @@ import land.webgui.server.EntityBinding; import land.webgui.server.WebviewEntityContext; import land.webgui.server.WebviewPlaceholders; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; //? if fabric { import net.fabricmc.fabric.api.event.player.UseEntityCallback; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.util.ActionResult; -import net.minecraft.util.Hand; //? } else { -/*import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.neoforged.neoforge.common.NeoForge; +/*import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;*/ //? } @@ -22,19 +19,19 @@ private EntityInteractionListener() {} //? if fabric { public static void register() { UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { - if (hand != Hand.MAIN_HAND) return ActionResult.PASS; - if (world.isClient()) return ActionResult.PASS; + if (hand != InteractionHand.MAIN_HAND) return InteractionResult.PASS; + if (world.isClientSide()) return InteractionResult.PASS; - var opt = EntityBindingStore.get(entity.getUuid()); - if (opt.isEmpty()) return ActionResult.PASS; + var opt = EntityBindingStore.get(entity.getUUID()); + if (opt.isEmpty()) return InteractionResult.PASS; - ServerPlayerEntity sp = (ServerPlayerEntity) player; + ServerPlayer sp = (ServerPlayer) player; EntityBinding b = opt.get(); String url = WebviewPlaceholders.resolve(b.urlTemplate(), sp, entity); String entityJson = WebviewEntityContext.buildJson(entity); WebviewNetworking.openGuiForEntity(sp, url, entityJson); - return b.cancelInteraction() ? ActionResult.SUCCESS : ActionResult.PASS; + return b.cancelInteraction() ? InteractionResult.SUCCESS : InteractionResult.PASS; }); } //? } else { diff --git a/src/main/java/land/webgui/WebGUIAssetServer.java b/src/main/java/land/webgui/WebGUIAssetServer.java index 20b6f3f..765e7b4 100644 --- a/src/main/java/land/webgui/WebGUIAssetServer.java +++ b/src/main/java/land/webgui/WebGUIAssetServer.java @@ -79,6 +79,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 diff --git a/src/main/java/land/webgui/WebGUIBrowserTuning.java b/src/main/java/land/webgui/WebGUIBrowserTuning.java new file mode 100644 index 0000000..a919f23 --- /dev/null +++ b/src/main/java/land/webgui/WebGUIBrowserTuning.java @@ -0,0 +1,46 @@ +package land.webgui; + +import land.webgui.browser.WebBrowser; + +/** + * How often the browser is allowed to paint. + * + * A windowless browser has a frame rate of its own, and the browser library never sets it, + * so the page repaints at whatever CEF defaults to. Sending data more often cannot help + * past that ceiling: the page would hold fresh numbers and still be drawn at the old rate, + * which reads as the overlay trailing the world. + */ +public final class WebGUIBrowserTuning { + + private static final java.util.Map TUNED = new java.util.WeakHashMap<>(); + + private WebGUIBrowserTuning() {} + + public static void applyOnce(WebBrowser browser) { + if (browser == null) { + return; + } + synchronized (TUNED) { + if (TUNED.containsKey(browser)) { + return; + } + TUNED.put(browser, Boolean.TRUE); + } + + try { + int target = WebGUIClientConfig.browserFrameRate(); + if (target > 0) { + browser.setWindowlessFrameRate(target); + WebGUIMod.LOGGER.info("webgui: browser paint rate asked for {} fps", target); + } + } catch (Throwable t) { + WebGUIMod.LOGGER.warn("webgui: could not set the browser paint rate: {}", t.toString()); + } + } + + static void forget() { + synchronized (TUNED) { + TUNED.clear(); + } + } +} diff --git a/src/main/java/land/webgui/WebGUIClient.java b/src/main/java/land/webgui/WebGUIClient.java index b8ca4ae..c7474da 100644 --- a/src/main/java/land/webgui/WebGUIClient.java +++ b/src/main/java/land/webgui/WebGUIClient.java @@ -1,18 +1,22 @@ package land.webgui; -import de.keksuccino.rinku.Rinku; +import land.webgui.compat.Mc; +import land.webgui.browser.BrowserBoot; +import land.webgui.browser.WebBrowser; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.DeathScreen; //? if fabric { import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; //? } else { -/*import net.minecraft.client.Minecraft; -import net.neoforged.bus.api.IEventBus; +/*import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.event.GameShuttingDownEvent; import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.neoforge.client.event.ScreenEvent; -import net.minecraft.client.gui.screens.DeathScreen; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;*/ //? } @@ -26,55 +30,57 @@ public final class WebGUIClient //? if fabric { @Override public void onInitializeClient() { - Rinku.scheduleForInit(success -> { - if (!success) { - WebGUIMod.LOGGER.error("Rinku (Chromium) failed to initialize — web GUI will not work."); - return; - } - Rinku.getClient().addDisplayHandler(new WebviewBrowserConsoleLogger()); + BrowserBoot.start(); + + // Attached before the engine is up: they are per-browser, and the first browser + // cannot exist before this returns. + WebBrowser.addListener(new WebviewBrowserConsoleLogger()); + WebviewPageLoadHooks.register(); + WebviewDownloadHandler.register(); + + // The message router needs a CEF client, which exists only once the engine is up. + WebBrowser.whenReady(() -> { WebviewPageToClientBridge.register(); - WebviewPageLoadHooks.register(); - WebviewDownloadHandler.register(); WebGUIMod.LOGGER.info("WebGUI bridge ready (console log, page↔game, client data)."); }); //? if >=1.20.5 { - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.OpenWebS2CPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.OpenWebS2CPayload.TYPE, (payload, context) -> { if (payload.protocolVersion() != WebviewNetworking.PROTOCOL_VERSION) { return; } context.client().execute(() -> handleOpenPayload(context.client(), payload.displayMode(), payload.url())); }); - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebUIMainMenuPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebUIMainMenuPayload.TYPE, (payload, context) -> { context.client().execute(() -> WebGUIMainMenuUrl.setUrl(payload.url())); }); - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewEmitS2CPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewEmitS2CPayload.TYPE, (payload, context) -> { context.client().execute(() -> WebviewClientEmit.dispatch(payload.eventName(), payload.jsonPayload())); }); - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewEntityContextS2CPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewEntityContextS2CPayload.TYPE, (payload, context) -> { context.client().execute(() -> WebviewClientBridge.setEntityContext(payload.entityJson())); }); - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewTrustedOriginsS2CPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewTrustedOriginsS2CPayload.TYPE, (payload, context) -> { context.client().execute(() -> WebGUITrustedOrigins.set(payload.origins())); }); - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewDeathS2CPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewDeathS2CPayload.TYPE, (payload, context) -> { context.client().execute(() -> onDeathPayload(payload.url(), payload.infoJson())); }); - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewHelloS2CPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewHelloS2CPayload.TYPE, (payload, context) -> { context.client().execute(() -> WebGUIHandshake.onHello(payload.protocolVersion(), payload.modVersion())); }); - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewAssetManifestS2CPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewAssetManifestS2CPayload.TYPE, (payload, context) -> { context.client().execute(() -> onAssetManifest(payload.revision(), payload.manifest())); }); - ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewAssetChunkS2CPayload.ID, (payload, context) -> { + ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.WebviewAssetChunkS2CPayload.TYPE, (payload, context) -> { // Straight through, off the render thread: a page pulls dozens of files and // every one of them would otherwise queue behind a frame. WebGUIAssetCache.onChunk(payload.path(), payload.chunkIndex(), payload.chunkCount(), payload.bytes()); @@ -83,7 +89,7 @@ public void onInitializeClient() { /*ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.OPEN_WEB_CHANNEL, (client, handler, buf, responseSender) -> { int protocolVersion = buf.readVarInt(); int displayMode = buf.readVarInt(); - String url = buf.readString(WebviewNetworking.MAX_URL_LENGTH); + String url = buf.readUtf(WebviewNetworking.MAX_URL_LENGTH); if (protocolVersion != WebviewNetworking.PROTOCOL_VERSION) { return; } @@ -91,41 +97,41 @@ public void onInitializeClient() { }); ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.MAIN_MENU_CHANNEL, (client, handler, buf, responseSender) -> { - String url = buf.readString(WebviewNetworking.MAX_URL_LENGTH); + String url = buf.readUtf(WebviewNetworking.MAX_URL_LENGTH); client.execute(() -> WebGUIMainMenuUrl.setUrl(url)); }); ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.EMIT_TO_PAGE_CHANNEL, (client, handler, buf, responseSender) -> { - String eventName = buf.readString(WebviewPayloads.MAX_EVENT_NAME_LENGTH); - String jsonPayload = buf.readString(WebviewPayloads.MAX_EVENT_DATA_LENGTH); + String eventName = buf.readUtf(WebviewPayloads.MAX_EVENT_NAME_LENGTH); + String jsonPayload = buf.readUtf(WebviewPayloads.MAX_EVENT_DATA_LENGTH); client.execute(() -> WebviewClientEmit.dispatch(eventName, jsonPayload)); }); ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.TRUSTED_ORIGINS_CHANNEL, (client, handler, buf, responseSender) -> { - String origins = buf.readString(WebviewPayloads.MAX_EVENT_DATA_LENGTH); + String origins = buf.readUtf(WebviewPayloads.MAX_EVENT_DATA_LENGTH); client.execute(() -> WebGUITrustedOrigins.set(origins)); }); ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.DEATH_SCREEN_CHANNEL, (client, handler, buf, responseSender) -> { - String url = buf.readString(WebviewNetworking.MAX_URL_LENGTH); - String info = buf.readString(WebviewPayloads.MAX_EVENT_DATA_LENGTH); + String url = buf.readUtf(WebviewNetworking.MAX_URL_LENGTH); + String info = buf.readUtf(WebviewPayloads.MAX_EVENT_DATA_LENGTH); client.execute(() -> onDeathPayload(url, info)); }); ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.HELLO_CHANNEL, (client, handler, buf, responseSender) -> { int protocol = buf.readVarInt(); - String version = buf.readString(WebviewPayloads.MAX_VERSION_LENGTH); + String version = buf.readUtf(WebviewPayloads.MAX_VERSION_LENGTH); client.execute(() -> WebGUIHandshake.onHello(protocol, version)); }); ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.ASSET_MANIFEST_CHANNEL, (client, handler, buf, responseSender) -> { - String rev = buf.readString(WebviewPayloads.MAX_VERSION_LENGTH); - String manifest = buf.readString(WebviewPayloads.MAX_MANIFEST_LENGTH); + String rev = buf.readUtf(WebviewPayloads.MAX_VERSION_LENGTH); + String manifest = buf.readUtf(WebviewPayloads.MAX_MANIFEST_LENGTH); client.execute(() -> onAssetManifest(rev, manifest)); }); ClientPlayNetworking.registerGlobalReceiver(WebviewPayloads.ASSET_CHUNK_CHANNEL, (client, handler, buf, responseSender) -> { - String path = buf.readString(WebviewPayloads.MAX_ASSET_PATH_LENGTH); + String path = buf.readUtf(WebviewPayloads.MAX_ASSET_PATH_LENGTH); int index = buf.readVarInt(); int count = buf.readVarInt(); byte[] data = buf.readByteArray(WebviewPayloads.ASSET_CHUNK_BYTES); @@ -145,6 +151,8 @@ public void onInitializeClient() { WebviewClientBridge.tick(client); }); + ClientLifecycleEvents.CLIENT_STOPPING.register(client -> WebBrowser.shutdown()); + // Leaving a world (disconnect / exit to title) tears down any server-opened HUD or GUI // browser so it doesn't keep rendering in the background on the main menu. ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> client.execute(WebGUIClient::onLeaveWorld)); @@ -173,31 +181,37 @@ static void onDeathPayload(String url, String infoJson) { } } - private static void handleOpenPayload(net.minecraft.client.MinecraftClient client, int mode, String url) { - if (!Rinku.isInitialized()) { + private static void handleOpenPayload(net.minecraft.client.Minecraft client, int mode, String url) { + if (!WebBrowser.ready()) { if (client.player != null) { - client.player.sendMessage(net.minecraft.text.Text.translatable("message.webgui.mcef_not_ready"), false); + //? if >=26 { + /*client.player.sendSystemMessage(net.minecraft.network.chat.Component.translatable("message.webgui.mcef_not_ready")); + *///? } else { + client.player.displayClientMessage(net.minecraft.network.chat.Component.translatable("message.webgui.mcef_not_ready"), false); + //? } } return; } String u = url == null || url.isBlank() ? StartUrls.primary() : url; if (mode == WebviewNetworking.MODE_GUI) { - client.setScreen(new WebViewScreen(u)); + Mc.openScreen(client, new WebViewScreen(u)); } else if (mode == WebviewNetworking.MODE_HUD) { WebHudOverlay.applyServerOpen(client, u); } } //? } else { /*public static void initClient(IEventBus modBus) { - Rinku.scheduleForInit(success -> { - if (!success) { - WebGUIMod.LOGGER.error("Rinku (Chromium) failed to initialize - web GUI will not work."); - return; - } - Rinku.getClient().addDisplayHandler(new WebviewBrowserConsoleLogger()); + BrowserBoot.start(); + + // Attached before the engine is up: they are per-browser, and the first browser + // cannot exist before this returns. + WebBrowser.addListener(new WebviewBrowserConsoleLogger()); + WebviewPageLoadHooks.register(); + WebviewDownloadHandler.register(); + + // The message router needs a CEF client, which exists only once the engine is up. + WebBrowser.whenReady(() -> { WebviewPageToClientBridge.register(); - WebviewPageLoadHooks.register(); - WebviewDownloadHandler.register(); WebGUIMod.LOGGER.info("WebGUI bridge ready (console log, page<->game, client data)."); }); @@ -215,6 +229,14 @@ private static void handleOpenPayload(net.minecraft.client.MinecraftClient clien NeoForge.EVENT_BUS.addListener(WebGUIClient::onClientTick); NeoForge.EVENT_BUS.addListener(WebGUIClient::onLoggingOut); NeoForge.EVENT_BUS.addListener(WebGUIClient::onScreenOpening); + // GameShuttingDownEvent, not ClientStoppingEvent: it has sat in the same package on + // every NeoForge this mod builds for, while the client one does not exist at all on + // the oldest of them. + NeoForge.EVENT_BUS.addListener(WebGUIClient::onGameShuttingDown); + } + + private static void onGameShuttingDown(GameShuttingDownEvent event) { + WebBrowser.shutdown(); } private static void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) { @@ -286,7 +308,7 @@ private static void onScreenOpening(ScreenEvent.Opening event) { // No browser, no page. WebViewScreen closes itself when Chromium is not // ready, vanilla reopens the death screen because the player is still // dead, and we would replace it again — a loop that crashes the client. - if (!Rinku.isInitialized()) { + if (!WebBrowser.ready()) { return; } WebGUIDeathScreen.setActive(true); @@ -301,7 +323,7 @@ private static void onClientTick(ClientTickEvent.Post event) { } private static void handleOpenPayload(Minecraft client, int mode, String url) { - if (!Rinku.isInitialized()) { + if (!WebBrowser.ready()) { if (client.player != null) { //? if >=26 { client.player.sendSystemMessage( @@ -315,7 +337,7 @@ private static void handleOpenPayload(Minecraft client, int mode, String url) { } String u = url == null || url.isBlank() ? StartUrls.primary() : url; if (mode == WebviewNetworking.MODE_GUI) { - //? if >=26.2-neoforge { + //? if >26.1.2 { client.gui.setScreen(new WebViewScreen(u)); //? } else { client.setScreen(new WebViewScreen(u)); @@ -377,22 +399,18 @@ private static java.nio.file.Path assetCacheDir() { * every bundled page silently failing to load on NeoForge while Fabric was fine. */ public static void requestAsset(String path) { - //? if fabric { - net.minecraft.client.MinecraftClient.getInstance().execute(() -> sendAssetRequest(path)); - //? } else { - /*net.minecraft.client.Minecraft.getInstance().execute(() -> sendAssetRequest(path));*/ - //? } + net.minecraft.client.Minecraft.getInstance().execute(() -> sendAssetRequest(path)); } private static void sendAssetRequest(String path) { //? if fabric { //? if >=1.20.5 { - if (ClientPlayNetworking.canSend(WebviewPayloads.WebviewAssetRequestC2SPayload.ID)) { + if (ClientPlayNetworking.canSend(WebviewPayloads.WebviewAssetRequestC2SPayload.TYPE)) { ClientPlayNetworking.send(new WebviewPayloads.WebviewAssetRequestC2SPayload(path)); } //? } else { - /*net.minecraft.network.PacketByteBuf buf = net.fabricmc.fabric.api.networking.v1.PacketByteBufs.create(); - buf.writeString(path, WebviewPayloads.MAX_ASSET_PATH_LENGTH); + /*net.minecraft.network.FriendlyByteBuf buf = net.fabricmc.fabric.api.networking.v1.PacketByteBufs.create(); + buf.writeUtf(path, WebviewPayloads.MAX_ASSET_PATH_LENGTH); ClientPlayNetworking.send(WebviewPayloads.ASSET_REQUEST_CHANNEL, buf);*/ //? } //? } else { diff --git a/src/main/java/land/webgui/WebGUIClientConfig.java b/src/main/java/land/webgui/WebGUIClientConfig.java index 14c3c31..a048010 100644 --- a/src/main/java/land/webgui/WebGUIClientConfig.java +++ b/src/main/java/land/webgui/WebGUIClientConfig.java @@ -23,9 +23,23 @@ public final class WebGUIClientConfig { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + /** As fast as this setting goes. Past it a notch throws a page across its own length. */ + public static final int SCROLL_MAX = 20; + /** Off by default: a developer's tool, and it makes the log noisier. */ private boolean devTools = false; + /** How often the browser may paint. 0 leaves whatever CEF defaults to. */ + private int browserFrameRate = 144; + + /** + * How many wheel notches a page is told about for each one the game reports. + * + * One for one is what a browser does, and in a page it reads as barely moving — the game + * counts a notch where a desktop browser counts several lines. + */ + private int scrollNotches = 7; + private static WebGUIClientConfig instance = new WebGUIClientConfig(); private WebGUIClientConfig() {} @@ -68,6 +82,24 @@ public static boolean devTools() { return instance.devTools; } + public static int browserFrameRate() { + return Math.max(0, instance.browserFrameRate); + } + + /** Clamped, because a hand-edited zero would stop a page scrolling at all. */ + public static int scrollNotches() { + return Math.min(SCROLL_MAX, Math.max(1, instance.scrollNotches)); + } + + public static void setScrollNotches(int value) { + int wanted = Math.min(SCROLL_MAX, Math.max(1, value)); + if (instance.scrollNotches == wanted) { + return; + } + instance.scrollNotches = wanted; + save(); + } + public static void setDevTools(boolean value) { if (instance.devTools == value) { return; diff --git a/src/main/java/land/webgui/WebGUIDevTools.java b/src/main/java/land/webgui/WebGUIDevTools.java index 0514bb2..b2205e5 100644 --- a/src/main/java/land/webgui/WebGUIDevTools.java +++ b/src/main/java/land/webgui/WebGUIDevTools.java @@ -1,11 +1,10 @@ package land.webgui; +import land.webgui.browser.WebBrowser; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import de.keksuccino.rinku.RinkuBrowser; -import org.cef.browser.CefDevToolsClient; /** * Chromium's own view of a page, in the game log: failed requests with their status, @@ -22,7 +21,7 @@ public final class WebGUIDevTools { /** A page in a loop could produce thousands of these a second. */ private static final int MAX_PER_SECOND = 20; /** Browsers already being watched, so a navigation does not attach a second time. */ - private static final java.util.Map ATTACHED = new java.util.WeakHashMap<>(); + private static final java.util.Map ATTACHED = new java.util.WeakHashMap<>(); private static long windowStartedAt; private static int inWindow; @@ -37,7 +36,7 @@ private WebGUIDevTools() {} * natively — but attaching twice would double every line in the log. Does nothing * with the setting off. */ - public static void attachOnce(RinkuBrowser browser) { + public static void attachOnce(WebBrowser browser) { if (browser == null || !WebGUIClientConfig.devTools()) { return; } @@ -47,20 +46,13 @@ public static void attachOnce(RinkuBrowser browser) { } ATTACHED.put(browser, Boolean.TRUE); } - CefDevToolsClient client; - try { - client = browser.getDevToolsClient(); - } catch (Throwable t) { - WebGUIMod.LOGGER.warn("webgui devtools: no protocol client for this browser: {}", t.toString()); - return; - } - if (client == null) { - return; - } - client.addEventListener(WebGUIDevTools::onEvent); + // Through the runtime's own protocol connection rather than a CEF client of our + // own: it waits for the browser to exist and for its first load, which is the race + // the old code lost whenever a page was asked about too early. + browser.onDevToolsEvent(WebGUIDevTools::onEvent); // Each domain says nothing until it is asked for. for (String domain : new String[] {"Log.enable", "Runtime.enable", "Network.enable"}) { - client.executeDevToolsMethod(domain); + browser.callDevTools(domain); } WebGUIMod.LOGGER.info("webgui devtools: watching {}", browser.getURL()); } diff --git a/src/main/java/land/webgui/WebGUIDevToolsWindow.java b/src/main/java/land/webgui/WebGUIDevToolsWindow.java index 45001a7..3b988d8 100644 --- a/src/main/java/land/webgui/WebGUIDevToolsWindow.java +++ b/src/main/java/land/webgui/WebGUIDevToolsWindow.java @@ -1,6 +1,6 @@ package land.webgui; -import de.keksuccino.rinku.RinkuBrowser; +import land.webgui.browser.WebBrowser; /** * The inspector, in a window of its own, the way a browser opens it. @@ -16,18 +16,19 @@ public final class WebGUIDevToolsWindow { private WebGUIDevToolsWindow() {} - /** Whether there was a page to open them for. */ - public static boolean open(RinkuBrowser browser) { + /** Whether there was a page to ask about. The browser opens a moment later. */ + public static boolean open(WebBrowser browser) { if (browser == null) { return false; } - try { - browser.openDevTools(); - WebGUIMod.LOGGER.info("webgui devtools: opened the inspector for {}", browser.getURL()); - return true; - } catch (Throwable t) { - WebGUIMod.LOGGER.warn("webgui devtools: could not open the inspector: {}", t.toString()); - return false; - } + browser.openDevTools(why -> { + WebGUIMod.LOGGER.warn("webgui devtools: no inspector to open ({})", why); + // Said to the player, not only to the log. A key that answers nothing at all is + // indistinguishable from a key that is broken, and this one is off by default. + land.webgui.compat.Mc.tell("Inspector is off. Turn on \"Page developer tools\" in the mod settings" + + " and restart the game."); + }); + return true; } + } diff --git a/src/main/java/land/webgui/WebGUIHandshake.java b/src/main/java/land/webgui/WebGUIHandshake.java index e4324d7..76b49e9 100644 --- a/src/main/java/land/webgui/WebGUIHandshake.java +++ b/src/main/java/land/webgui/WebGUIHandshake.java @@ -1,14 +1,8 @@ package land.webgui; -//? if fabric { -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; -//? } else { -/*import net.minecraft.ChatFormatting; +import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; -import net.minecraft.network.chat.Component;*/ -//? } +import net.minecraft.network.chat.Component; /** * Client half of the join handshake: what the server said it is running. @@ -46,23 +40,7 @@ private static String mismatchText(int protocol, String version) { + " versions match."; } - //? if fabric { public static void onHello(int protocol, String version) { - serverProtocol = protocol; - serverVersion = version == null ? "" : version; - if (protocol == WebviewNetworking.PROTOCOL_VERSION) { - return; - } - WebGUIMod.LOGGER.warn("webgui: server runs {} (protocol {}), client is {} (protocol {})", - serverVersion, protocol, WebGUIVersion.current(), WebviewNetworking.PROTOCOL_VERSION); - MinecraftClient client = MinecraftClient.getInstance(); - if (client.player != null) { - client.player.sendMessage( - Text.literal(mismatchText(protocol, serverVersion)).formatted(Formatting.RED), false); - } - } - //? } else { - /*public static void onHello(int protocol, String version) { serverProtocol = protocol; serverVersion = version == null ? "" : version; if (protocol == WebviewNetworking.PROTOCOL_VERSION) { @@ -75,13 +53,12 @@ public static void onHello(int protocol, String version) { // 26 dropped Player.displayClientMessage and gave LocalPlayer its own // sendSystemMessage instead. //? if >=26 { - client.player.sendSystemMessage( + /*client.player.sendSystemMessage( Component.literal(mismatchText(protocol, serverVersion)).withStyle(ChatFormatting.RED)); - //? } else { + *///? } else { client.player.displayClientMessage( Component.literal(mismatchText(protocol, serverVersion)).withStyle(ChatFormatting.RED), false); //? } } - }*/ - //? } + } } diff --git a/src/main/java/land/webgui/WebGUIKeys.java b/src/main/java/land/webgui/WebGUIKeys.java index 742f0f9..50a3159 100644 --- a/src/main/java/land/webgui/WebGUIKeys.java +++ b/src/main/java/land/webgui/WebGUIKeys.java @@ -1,78 +1,103 @@ package land.webgui; -import de.keksuccino.rinku.Rinku; +import land.webgui.compat.Mc; +import land.webgui.browser.WebBrowser; +import net.minecraft.client.Minecraft; //? if fabric { +//? if >=26 { +/*import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper; +*///? } else { import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; -import net.minecraft.client.MinecraftClient; +//? } //? } else { -/*import net.minecraft.client.Minecraft; -import net.neoforged.bus.api.IEventBus; +/*import net.neoforged.bus.api.IEventBus; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent;*/ //? } -//? if fabric { -import net.minecraft.client.option.KeyBinding; -import net.minecraft.client.util.InputUtil; -import net.minecraft.text.Text; -//? if >=1.21.5 { -import net.minecraft.util.Identifier; -//? } -//? } else { -/*import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.platform.InputConstants; import net.minecraft.client.KeyMapping; import net.minecraft.network.chat.Component; //? if >=1.21.5 { import net.minecraft.resources.Identifier; -//? }*/ //? } +//? if <=26.2 { import org.lwjgl.glfw.GLFW; +//? } public final class WebGUIKeys { - //? if fabric { - private static KeyBinding mainMenu; - private static KeyBinding hudInteractive; - private static KeyBinding devtools; - //? } else { - /*private static KeyMapping mainMenu; - private static KeyMapping hudInteractive; - private static KeyMapping devtools;*/ + + // The keys this mod binds, in whatever numbering the game is speaking. Up to 26.2 that + // is GLFW's; from 26.3 the window is SDL's and the numbers are its scan codes, so these + // are taken from Minecraft rather than written down — the game knows which it is on. + //? if >26.2 { + /*static final InputConstants.Type KEY_TYPE = InputConstants.Type.KEYBOARD; + static final int KEY_F6 = InputConstants.KEY_F6; + static final int KEY_F12 = InputConstants.KEY_F12; + static final int KEY_GRAVE = InputConstants.KEY_GRAVE; + *///? } else { + static final InputConstants.Type KEY_TYPE = InputConstants.Type.KEYSYM; + static final int KEY_F6 = GLFW.GLFW_KEY_F6; + static final int KEY_F12 = GLFW.GLFW_KEY_F12; + static final int KEY_GRAVE = GLFW.GLFW_KEY_GRAVE_ACCENT; //? } + private static KeyMapping mainMenu; + private static KeyMapping hudInteractive; + private static KeyMapping devtools; private WebGUIKeys() {} + //? if fabric { + /** + * Registers a key with the loader. + * + *

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 = KeyBindingHelper.registerKeyBinding(new KeyBinding( + devtools = bind(new KeyMapping( "key.webgui.devtools", - InputUtil.Type.KEYSYM, - GLFW.GLFW_KEY_F12, + 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 = KeyBindingHelper.registerKeyBinding(new KeyBinding( + devtools = bind(new KeyMapping( "key.webgui.devtools", - InputUtil.Type.KEYSYM, - GLFW.GLFW_KEY_F12, + KEY_TYPE, + WebGUIKeys.KEY_F12, WebGUIMod.MOD_ID + ".web"));*/ //? } } @@ -84,13 +109,13 @@ 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); - devtools = new KeyMapping("key.webgui.devtools", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_F12, 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"); - devtools = new KeyMapping("key.webgui.devtools", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_F12, 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); @@ -98,81 +123,37 @@ private static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) { }*/ //? } - //? 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 { - while (devtools.wasPressed()) { - //? } else { - /*while (devtools.consumeClick()) {*/ - //? } + 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 fabric { - private static void tryOpenMainMenu(MinecraftClient client) { - if (client.currentScreen instanceof WebViewScreen) { + private static void tryOpenMainMenu(Minecraft client) { + if (Mc.screen(client) instanceof WebViewScreen) { return; } - 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) { - //? } - 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/WebGUISettingsScreen.java b/src/main/java/land/webgui/WebGUISettingsScreen.java index c896cdb..64a94d7 100644 --- a/src/main/java/land/webgui/WebGUISettingsScreen.java +++ b/src/main/java/land/webgui/WebGUISettingsScreen.java @@ -1,20 +1,14 @@ 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.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. @@ -22,32 +16,23 @@ * Not a page in a browser: this is where a player goes when the browser is what is not * working. */ -//? 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 final Screen parent; - //? if fabric { - private ButtonWidget devToolsButton; - - public WebGUISettingsScreen(Screen parent) { - super(Text.translatable("screen.webgui.settings.title")); - this.parent = parent; - } - //? } else { - /*private Button devToolsButton; + /** What the button steps through. A slider's constructor differs on every other + * Minecraft version; a button that cycles does not. */ + private static final int[] SCROLL_STEPS = {1, 3, 5, 7, 10, 15, 20}; + private Button devToolsButton; + private Button scrollButton; public WebGUISettingsScreen(Screen parent) { super(Component.translatable("screen.webgui.settings.title")); this.parent = parent; - }*/ - //? } + } @Override protected void init() { @@ -55,90 +40,54 @@ protected void init() { int left = this.width / 2 - BUTTON_WIDTH / 2; int top = this.height / 4 + 24; - //? 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 = Button.builder(scrollLabel(), b -> { + WebGUIClientConfig.setScrollNotches(nextScrollStep()); + this.scrollButton.setMessage(scrollLabel()); + }).bounds(left, top, BUTTON_WIDTH, 20).build(); + this.addRenderableWidget(this.scrollButton); - 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.devToolsButton = Button.builder(devToolsLabel(), b -> { WebGUIClientConfig.setDevTools(!WebGUIClientConfig.devTools()); this.devToolsButton.setMessage(devToolsLabel()); - }).bounds(left, top, BUTTON_WIDTH, 20).build(); + }).bounds(left, top + ROW_HEIGHT, 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());*/ - //? } + .bounds(left, this.height - 32, BUTTON_WIDTH, 20).build()); + } + + private static int nextScrollStep() { + int current = WebGUIClientConfig.scrollNotches(); + for (int step : SCROLL_STEPS) { + if (step > current) { + return step; + } + } + return SCROLL_STEPS[0]; } - //? if fabric { - private Text devToolsLabel() { - return Text.translatable("screen.webgui.settings.devtools") + private Component scrollLabel() { + return Component.translatable("screen.webgui.settings.scroll") .append(": ") - .append(Text.translatable(WebGUIClientConfig.devTools() ? "options.on" : "options.off")); + .append(Component.literal(String.valueOf(WebGUIClientConfig.scrollNotches()))); } - //? } else { - /*private Component devToolsLabel() { + + private Component devToolsLabel() { return Component.translatable("screen.webgui.settings.devtools") .append(": ") .append(Component.translatable(WebGUIClientConfig.devTools() ? "options.on" : "options.off")); - }*/ - //? } - - //? 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); - } } - @Override - public void close() { - if (this.client != null) { - this.client.setScreen(this.parent); - } - } - //? } else { - /*//? if >=26 { - @Override + //? 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; + int under = this.height / 4 + 24 + ROW_HEIGHT * 2; 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; @@ -153,18 +102,17 @@ public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mou centre, inspector + 12, 0xFFFFFF80); } } - //? } 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; + int under = this.height / 4 + 24 + ROW_HEIGHT * 2; 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; @@ -180,16 +128,10 @@ public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { } } //? } - @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 c07c0ee..f5f8b87 100644 --- a/src/main/java/land/webgui/WebViewScreen.java +++ b/src/main/java/land/webgui/WebViewScreen.java @@ -1,30 +1,15 @@ package land.webgui; -import de.keksuccino.rinku.Rinku; -import de.keksuccino.rinku.RinkuBrowser; -import org.lwjgl.glfw.GLFW; +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; @@ -32,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; } @@ -92,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() { @@ -108,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()); } @@ -140,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 @@ -189,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; } @@ -201,7 +140,7 @@ static void onGuiBrowserLoadStart(RinkuBrowser browser) { } } - static void onGuiBrowserLoadFinished(RinkuBrowser browser) { + static void onGuiBrowserLoadFinished(WebBrowser browser) { if (browser == null) { return; } @@ -211,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 { @@ -288,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) { @@ -376,62 +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; - if (input.key() == GLFW.GLFW_KEY_F12) { - WebGUIDevToolsWindow.open(browser); - return true; - } - 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() == GLFW.GLFW_KEY_F12) { + 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; @@ -439,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) { @@ -456,7 +320,7 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (browser == null) { return false; } - if (keyCode == GLFW.GLFW_KEY_F12) { + if (keyCode == WebGUIKeys.KEY_F12) { WebGUIDevToolsWindow.open(browser); return true; } @@ -494,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 ctx, boolean cancel) - throws com.mojang.brigadier.exceptions.CommandSyntaxException { - Collection entities = EntityArgumentType.getEntities(ctx, "selector"); - String url = StringArgumentType.getString(ctx, "url"); - EntityBinding binding = new EntityBinding(url, cancel); - for (Entity e : entities) { - EntityBindingStore.bind(e.getUuid(), binding); - } - final int count = entities.size(); - ctx.getSource().sendFeedback( - () -> Text.literal("WebGUI: bound " + count + " entity/entities → " + url), - true); - return count; - } - //? } else { - /*private static int bindEntities(com.mojang.brigadier.context.CommandContext ctx, boolean cancel) + private static int bindEntities(com.mojang.brigadier.context.CommandContext ctx, boolean cancel) throws com.mojang.brigadier.exceptions.CommandSyntaxException { Collection entities = EntityArgument.getEntities(ctx, "selector"); String url = StringArgumentType.getString(ctx, "url"); @@ -61,95 +42,82 @@ private static int bindEntities(com.mojang.brigadier.context.CommandContext Component.literal("WebGUI: bound " + count + " entity/entities -> " + url), true); return count; - }*/ - //? } - - //? if fabric { - //? if >=1.21.5 { - private static boolean hasOp2(ServerCommandSource s) { - return s.getPermissions().hasPermission(net.minecraft.command.DefaultPermissions.GAMEMASTERS); } - //? } else { - /*private static boolean hasOp2(ServerCommandSource s) { - return s.hasPermissionLevel(2); - }*/ - //? } - //? } else { - /*private static boolean hasOp2(CommandSourceStack s) { + + private static boolean hasOp2(CommandSourceStack s) { //? if >=1.21.5 { return net.minecraft.commands.Commands.LEVEL_GAMEMASTERS.check(s.permissions()); //? } else { - return s.hasPermission(2); - //? } - }*/ - //? } + /*return s.hasPermission(2); + *///? } + } //? if fabric { public static void register() { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register( - CommandManager.literal("webgui") + Commands.literal("webgui") .requires(WebviewCommands::hasOp2) - .then(CommandManager.literal("gui") - .then(CommandManager.argument("targets", EntityArgumentType.players()) - .then(CommandManager.argument("url", StringArgumentType.greedyString()) + .then(Commands.literal("gui") + .then(Commands.argument("targets", EntityArgument.players()) + .then(Commands.argument("url", StringArgumentType.greedyString()) .executes(ctx -> { - Collection players = EntityArgumentType.getPlayers(ctx, "targets"); + Collection players = EntityArgument.getPlayers(ctx, "targets"); String url = StringArgumentType.getString(ctx, "url"); - for (ServerPlayerEntity p : players) { + for (ServerPlayer p : players) { WebviewNetworking.openGui(p, url); } - ctx.getSource().sendFeedback( - () -> Text.literal("Web GUI → " + players.size() + " player(s)"), + ctx.getSource().sendSuccess( + () -> Component.literal("Web GUI → " + players.size() + " player(s)"), true); return players.size(); })))) - .then(CommandManager.literal("bind") - .then(CommandManager.literal("entity") - .then(CommandManager.argument("selector", EntityArgumentType.entities()) - .then(CommandManager.argument("url", StringArgumentType.string()) + .then(Commands.literal("bind") + .then(Commands.literal("entity") + .then(Commands.argument("selector", EntityArgument.entities()) + .then(Commands.argument("url", StringArgumentType.string()) .executes(ctx -> bindEntities(ctx, false)) - .then(CommandManager.argument("cancel_interaction", BoolArgumentType.bool()) + .then(Commands.argument("cancel_interaction", BoolArgumentType.bool()) .executes(ctx -> bindEntities(ctx, BoolArgumentType.getBool(ctx, "cancel_interaction")))))))) - .then(CommandManager.literal("unbind") - .then(CommandManager.literal("entity") - .then(CommandManager.argument("selector", EntityArgumentType.entities()) + .then(Commands.literal("unbind") + .then(Commands.literal("entity") + .then(Commands.argument("selector", EntityArgument.entities()) .executes(ctx -> { - Collection entities = EntityArgumentType.getEntities(ctx, "selector"); + Collection entities = EntityArgument.getEntities(ctx, "selector"); int count = 0; for (Entity e : entities) { - if (EntityBindingStore.unbind(e.getUuid())) count++; + if (EntityBindingStore.unbind(e.getUUID())) count++; } final int removed = count; - ctx.getSource().sendFeedback( - () -> Text.literal("WebGUI: unbound " + removed + " entity/entities"), + ctx.getSource().sendSuccess( + () -> Component.literal("WebGUI: unbound " + removed + " entity/entities"), true); return removed; })))) - .then(CommandManager.literal("reload") + .then(Commands.literal("reload") .executes(ctx -> { String msg = WebviewServerConfig.reload(); EntityBindingStore.load(); // Every setting the client holds a copy of has to be // pushed again, or online players keep the old one. - for (ServerPlayerEntity p : ctx.getSource().getServer().getPlayerManager().getPlayerList()) { + for (ServerPlayer p : ctx.getSource().getServer().getPlayerList().getPlayers()) { WebviewNetworking.sendConfigSnapshot(p); } - ctx.getSource().sendFeedback(() -> Text.literal(msg), true); + ctx.getSource().sendSuccess(() -> Component.literal(msg), true); return 1; })) - .then(CommandManager.literal("hud") - .then(CommandManager.argument("targets", EntityArgumentType.players()) - .then(CommandManager.argument("url", StringArgumentType.greedyString()) + .then(Commands.literal("hud") + .then(Commands.argument("targets", EntityArgument.players()) + .then(Commands.argument("url", StringArgumentType.greedyString()) .executes(ctx -> { - Collection players = EntityArgumentType.getPlayers(ctx, "targets"); + Collection players = EntityArgument.getPlayers(ctx, "targets"); String url = StringArgumentType.getString(ctx, "url"); - for (ServerPlayerEntity p : players) { + for (ServerPlayer p : players) { WebviewNetworking.openHud(p, url); } - ctx.getSource().sendFeedback( - () -> Text.literal("Web HUD → " + players.size() + " player(s)"), + ctx.getSource().sendSuccess( + () -> Component.literal("Web HUD → " + players.size() + " player(s)"), true); return players.size(); })))))); diff --git a/src/main/java/land/webgui/WebviewDownloadHandler.java b/src/main/java/land/webgui/WebviewDownloadHandler.java index a8ec368..4672b59 100644 --- a/src/main/java/land/webgui/WebviewDownloadHandler.java +++ b/src/main/java/land/webgui/WebviewDownloadHandler.java @@ -1,24 +1,19 @@ package land.webgui; -import de.keksuccino.rinku.Rinku; -import org.cef.browser.CefBrowser; -import org.cef.callback.CefBeforeDownloadCallback; -import org.cef.callback.CefDownloadItem; -import org.cef.callback.CefDownloadItemCallback; -import org.cef.handler.CefDownloadHandler; - -import java.nio.file.Files; -import java.nio.file.Path; +import land.webgui.browser.WebBrowser; +import space.webgui.wmc.api.Download; /** - * Lets a page hand the player a file. + * Tells the player about a file a page handed them. * - * With no handler CEF asks the host where to put a download and, hearing nothing, throws - * it away — so an invoice, a log, a schematic or a CSV export was simply unclickable. + *

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..f3b521a 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,7 +17,7 @@ private WebviewJoinHud() {} public static void register() { ServerPlayConnectionEvents.JOIN.register( (handler, sender, server) -> { - ServerPlayerEntity player = handler.player; + ServerPlayer player = handler.player; if (player == null) { return; } @@ -46,7 +45,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()); } }); } 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 chunks = land.webgui.server.WebviewAssets.chunksFor( - player.getUuid(), player.getName().getString(), path); + player.getUUID(), player.getName().getString(), path); if (chunks.isEmpty()) { sendAssetChunk(player, path, 0, 0, new byte[0]); @@ -363,12 +370,12 @@ public static void serveAsset(ServerPlayerEntity player, String requested) { } } - private static void sendAssetChunk(ServerPlayerEntity player, String path, int index, int count, byte[] bytes) { + private static void sendAssetChunk(ServerPlayer player, String path, int index, int count, byte[] bytes) { //? if >=1.20.5 { ServerPlayNetworking.send(player, new WebviewPayloads.WebviewAssetChunkS2CPayload(path, index, count, bytes)); //? } else { - /*PacketByteBuf buf = PacketByteBufs.create(); - buf.writeString(path, WebviewPayloads.MAX_ASSET_PATH_LENGTH); + /*FriendlyByteBuf buf = PacketByteBufs.create(); + buf.writeUtf(path, WebviewPayloads.MAX_ASSET_PATH_LENGTH); buf.writeVarInt(index); buf.writeVarInt(count); buf.writeByteArray(bytes); @@ -385,7 +392,7 @@ private static void sendAssetChunk(ServerPlayerEntity player, String path, int i * re-send was a setting that silently did not apply until the player reconnected — * which is how an edited page kept showing its old self. */ - public static void sendConfigSnapshot(ServerPlayerEntity player) { + public static void sendConfigSnapshot(ServerPlayer player) { sendTrustedOrigins(player, WebviewServerConfig.trustedCommandOriginsJoined()); // Before any page URL: a page pointed at webgui:/ cannot resolve anything until // the client knows what the server ships. @@ -397,18 +404,18 @@ public static void sendConfigSnapshot(ServerPlayerEntity player) { sendMainMenuUrl(player, WebviewServerConfig.mainMenuUrl()); } - public static void sendTrustedOrigins(ServerPlayerEntity player, String origins) { + public static void sendTrustedOrigins(ServerPlayer player, String origins) { String o = origins == null ? "" : origins; //? if >=1.20.5 { ServerPlayNetworking.send(player, new WebviewPayloads.WebviewTrustedOriginsS2CPayload(o)); //? } else { - /*PacketByteBuf buf = PacketByteBufs.create(); - buf.writeString(o, WebviewPayloads.MAX_EVENT_DATA_LENGTH); + /*FriendlyByteBuf buf = PacketByteBufs.create(); + buf.writeUtf(o, WebviewPayloads.MAX_EVENT_DATA_LENGTH); ServerPlayNetworking.send(player, WebviewPayloads.TRUSTED_ORIGINS_CHANNEL, buf);*/ //? } } - private static String withPlayerToken(ServerPlayerEntity player, String url) { + private static String withPlayerToken(ServerPlayer player, String url) { if (!WebviewServerConfig.enableTokens()) { return sanitizeUrl(url); } diff --git a/src/main/java/land/webgui/WebviewPageLoadHooks.java b/src/main/java/land/webgui/WebviewPageLoadHooks.java index 6b2b79d..14b2fb9 100644 --- a/src/main/java/land/webgui/WebviewPageLoadHooks.java +++ b/src/main/java/land/webgui/WebviewPageLoadHooks.java @@ -1,33 +1,26 @@ package land.webgui; -import de.keksuccino.rinku.Rinku; -import de.keksuccino.rinku.RinkuBrowser; -//? if fabric { -import net.minecraft.client.MinecraftClient; -//? } else { -/*import net.minecraft.client.Minecraft;*/ -//? } -import org.cef.browser.CefBrowser; -import org.cef.browser.CefFrame; -import org.cef.handler.CefLoadHandler; -import org.cef.handler.CefLoadHandlerAdapter; +import land.webgui.browser.WebBrowser; +import net.minecraft.client.Minecraft; public final class WebviewPageLoadHooks { private WebviewPageLoadHooks() {} public static void register() { - Rinku.getClient().addLoadHandler(new CefLoadHandlerAdapter() { + // Attached to every browser as it opens, rather than to the engine once: java-cef + // keeps a single load handler per client and the engine's own is in that slot. + WebBrowser.addListener(new WebBrowser.Listener() { @Override - public void onLoadStart(CefBrowser browser, CefFrame frame, - org.cef.network.CefRequest.TransitionType transitionType) { - RinkuBrowser active = WebSession.browser(); + public void onLoadStart(WebBrowser browser, String url) { + WebBrowser active = WebSession.browser(); if (active == null || browser != active) return; // Here rather than where the browser is created: the protocol needs a // browser that exists natively, and the first load is the earliest point // where that is certainly true. WebGUIDevTools.attachOnce(active); + WebGUIBrowserTuning.applyOnce(active); // Before the document's own scripts, not after them. Injecting only at // load end meant a plain + + diff --git a/dev/testpages/death.html b/dev/testpages/death.html new file mode 100644 index 0000000..3559605 --- /dev/null +++ b/dev/testpages/death.html @@ -0,0 +1,82 @@ + + + + +WebGUI death screen + + + + +

+

You died.

+

waiting for the death payload…

+ +
+

What the server told the page

+
+
cause
—
+
killer.type
—
+
killer.name
—
+
killer.entityType
—
+
hardcore
—
+
canRespawn
—
+
+
+ +
+ + the only way out — Escape must not close this screen +
+ +

+ 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. +

+
+ + + + diff --git a/dev/testpages/download.html b/dev/testpages/download.html new file mode 100644 index 0000000..2267113 --- /dev/null +++ b/dev/testpages/download.html @@ -0,0 +1,102 @@ + + + + +Downloads — WebGUI test + + + +
+

Downloads

+

← back

+ +
+

What should happen

+

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.

+
+ +
+

A file this page makes

+

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.

+
+ +
+

A file from the web

+

Needs the game to have internet.

+
+ 1Mb.dat + an ordinary link to a binary file +
+
+ +
+

Log

+
+
+
+ + + + diff --git a/dev/testpages/hud.html b/dev/testpages/hud.html new file mode 100644 index 0000000..7b75ed6 --- /dev/null +++ b/dev/testpages/hud.html @@ -0,0 +1,79 @@ + + + + +HUD — WebGUI test + + + +
+
+

WebGUI HUD

+
+
mode
—
+
health
—
+
food
—
+
pos
—
+
look
—
+
dimension
—
+
fps-ish
—
+
+

+ Open with /webgui hud @s webgui:hud.html. + Press ` to make it interactive — the button below only works then. +

+
+ +
+
+
+ + + + diff --git a/dev/testpages/index.html b/dev/testpages/index.html new file mode 100644 index 0000000..40afd67 --- /dev/null +++ b/dev/testpages/index.html @@ -0,0 +1,83 @@ + + + + +WebGUI test pages + + + +
+

WebGUI test pages

+

Each page checks one part of the mod. Open with + /webgui gui @s webgui:index.html.

+ +
+

This page

+
+
bridge
checking…
+
mode
—
+
assetsBase
—
+
location
—
+
+

+ 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. +

+
+ + + +
+

Close

+
+ + should shut the screen, same as Escape +
+
+
+ + + + diff --git a/dev/testpages/input.html b/dev/testpages/input.html new file mode 100644 index 0000000..c9e9f24 --- /dev/null +++ b/dev/testpages/input.html @@ -0,0 +1,157 @@ + + + + +Input & cursors — WebGUI test + + + +
+

Input & cursors

+

← back

+ +
+

Typing

+

Letters, digits, backspace, arrows, Home/End, Ctrl+A, Ctrl+C/V. + Escape should close the screen rather than reach the field.

+
+ + +
+

+
+
last key
—
+
value
—
+
+

+ + — keydown, keypress, beforeinput, input, keyup; one line each +

+
+ +
+

Form

+

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.

+
+
+ + +
+
+
+
submits
0
+
last value
—
+
+
+ +
+

Selection

+

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.

+
+
selected
—
+
+
+ +
+

Dropdown

+

A native select. Chromium paints this into a surface of its own, so it + checks that popups are drawn over the page at all.

+ + +
+ +
+

Cursors

+

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.

+
+
pointer
+
text
+
crosshair
+
ew-resize
+
ns-resize
+
n-resize
+
s-resize
+
move
+
not-allowed n/a
+
grab n/a
+
+
+ +
+

Scrolling

+

Spin the wheel. Speed is a setting — mod list → WebGUI → Config.

+
+
+
+
+
+ + + + diff --git a/dev/testpages/style.css b/dev/testpages/style.css new file mode 100644 index 0000000..840a729 --- /dev/null +++ b/dev/testpages/style.css @@ -0,0 +1,118 @@ +:root { + --bg: #11141a; + --panel: #1a1f29; + --line: #2c3444; + --text: #e6e9ef; + --dim: #99a1b3; + --ok: #68d391; + --warn: #f6c177; + --bad: #f07178; + --link: #7aa2f7; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + min-height: 100%; + background: var(--bg); + color: var(--text); + font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; +} + +/* The HUD is drawn over the world, so it must not paint a background of its own. + Both elements, not just body: a background on html paints the canvas all the same, + and a page that sets one covers the game. This is the usual first mistake. */ +html.hud, body.hud { background: transparent; } +body.hud .wrap { max-width: none; padding: 10px; } +body.hud .panel { background: rgba(17, 20, 26, .78); } + +.wrap { max-width: 860px; margin: 0 auto; padding: 20px; } + +h1 { font-size: 20px; margin: 0 0 4px; } +h2 { font-size: 15px; margin: 0 0 10px; color: var(--dim); font-weight: 600; } +p { margin: 0 0 10px; } + +.sub { color: var(--dim); margin-bottom: 18px; } + +.panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + padding: 14px 16px; + margin-bottom: 14px; +} + +.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } + +button { + font: inherit; + color: var(--text); + background: #273041; + border: 1px solid var(--line); + border-radius: 6px; + padding: 7px 12px; + cursor: pointer; +} +button:hover { background: #313c51; } +button:active { background: #202836; } + +input[type="text"], input[type="password"], textarea, select { + font: inherit; + color: var(--text); + background: #0e1218; + border: 1px solid var(--line); + border-radius: 6px; + padding: 7px 10px; +} +textarea { width: 100%; min-height: 70px; resize: vertical; } + +a { color: var(--link); } + +.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 10px; } + +.card { + display: block; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + padding: 14px 16px; + text-decoration: none; + color: var(--text); +} +.card:hover { border-color: var(--link); } +.card b { display: block; margin-bottom: 4px; } +.card span { color: var(--dim); font-size: 13px; } + +.kv { display: grid; grid-template-columns: 150px 1fr; gap: 4px 12px; font-family: ui-monospace, Consolas, monospace; font-size: 13px; } +.kv div:nth-child(odd) { color: var(--dim); } + +.log { + font-family: ui-monospace, Consolas, monospace; + font-size: 12px; + background: #0e1218; + border: 1px solid var(--line); + border-radius: 6px; + padding: 10px; + height: 150px; + overflow: auto; + white-space: pre-wrap; +} + +.ok { color: var(--ok); } +.bad { color: var(--bad); } +.dim { color: var(--dim); } + +.tall { height: 1400px; background: linear-gradient(#1a1f29, #11141a 40%, #1a1f29); border-radius: 6px; padding: 12px; } +.ruler { color: var(--dim); font-family: ui-monospace, Consolas, monospace; font-size: 12px; } + +.cursors { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; } +.cursors div { + border: 1px dashed var(--line); + border-radius: 6px; + padding: 14px 8px; + text-align: center; + font-family: ui-monospace, Consolas, monospace; + font-size: 12px; + color: var(--dim); +} From a90737e81af68b446fa625e0c70fc80587248c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 02:39:44 +0500 Subject: [PATCH 05/14] docs: changelog 2.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Мажорная из-за того, что мод лишился зависимости от стороннего мода. Страничный API и протокол не менялись. --- CHANGELOG.md | 20 ++++++++++++++ MODRINTH.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 MODRINTH.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d8f98..40af4a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 2.0.0 - 2026-10-13 + +A major version for one reason: WebGUI no longer depends on another mod. Nothing in the page +API changed and the network protocol is the same, so existing pages keep working and a 2.0.0 +client still talks to a 1.8.x server. + +### Added +- **Minecraft 26.2 on Fabric, and 26.3 on both loaders.** 26.3 is published as a beta: the game itself is still in snapshots, and it replaced GLFW with SDL and moved the GPU API out of `blaze3d`, so the parts of this mod that touch the window and the screen are new there. + +### Changed +- **WebGUI no longer needs another mod to run.** The embedded browser is part of WebGUI now instead of being provided by a separate library mod, so installing WebGUI is one file and nothing else. Chromium itself is still downloaded once per machine on first start, outside the game directory, and shared by every instance. + +### Fixed +- **The custom death screen on Fabric 26.2 and later.** It hooked a method the game no longer has, which the mixin configuration treats as fatal — the game would not start at all on those versions. +- **The game can be closed again.** Quitting left two threads running — the page server and Chromium — so the process never exited: silently on 1.21.1 and earlier, and from 1.21.11 as a crash report written by the game's own shutdown watchdog. +- **Typing into a page on 26.3.** SDL only delivers typed characters while text input has been started, and the game starts it for its own text fields. Keys arrived and shortcuts such as Ctrl+A and Ctrl+V worked, but letters did not reach the page. +- **Cursor shapes on 26.3.** The cursor is now decided once per frame by the game, so setting it directly was overwritten before it could be seen. +- **Enter in a page.** It never produced a character event, on any version, so a `textarea` took every key but this one and a form could not be submitted with it. +- A NeoForge warning on every launch of 26.3 about deprecated mod metadata. + ## 1.8.1 - 2026-09-12 ### Fixed diff --git a/MODRINTH.md b/MODRINTH.md new file mode 100644 index 0000000..beab4a5 --- /dev/null +++ b/MODRINTH.md @@ -0,0 +1,75 @@ +**Your web app, running inside Minecraft.** + +WebGUI opens a real web page in the game — as a full-screen interface or as a transparent overlay on top of gameplay. Not a reskinned inventory, not another UI framework to learn: actual HTML, CSS and JavaScript, rendered by actual Chromium. + +If you can build a website, you can build a Minecraft interface. + + + +## What people build with it + +* **Server shops** that look like an online store, with images, search and a cart +* **Quest journals and codexes** with real typography and layout +* **Live maps** — Leaflet or your own canvas, following the player as they walk +* **HUD overlays** — team scores, timers, health bars, a minimap, drawn over the world +* **Admin panels** for staff, with the same dashboard you already run in a browser +* **Custom death screens** that say who killed you and let you respawn +* **Launch menus** and lobby screens with your server's own branding +* **Entire in-game apps** — anything you would otherwise build as a web app + +## Why it feels different + +Everything you already use keeps working. React, Vue, Svelte or plain JavaScript. Tailwind. Your component library. Your animations, your API, your WebSocket. You develop in a browser with hot reload, then point the mod at the page — and it looks exactly the same in the game. + +The page is not a decoration, either. It knows who is looking at it and what is happening around them — the player's name, health, food, position, where they are looking, which block or mob is under the crosshair — updated as they move. And it can talk back: a click in the page becomes an event on the server, so a shop can actually sell, and a button can actually run a command. + +```js +// This is the whole integration +window.webgui.on('client', player => { + document.title = `${player.username} — ${player.health} HP`; +}); + +window.webgui.postToGame({ channel: 'shop:buy', item: 'diamond' }); +``` + +For React, Vue and Svelte there is one package with hooks and stores ready to go: + +```bash +npm i @webgui/client +``` + +## You don't even need a web host + +Put your files in a folder on the server and point the mod at them. They travel down the same connection the player is already on — no domain, no hosting bill, no port to open, works behind NAT. Edit a file, run one command, and the page a player has open refreshes in front of them. + +Prefer your own host? Nothing changes: an `https://` address works exactly as it always did, and the two mix freely. + +## For server owners + +Opening a page is one command: + +``` +/webgui gui @a https://your-site.example/shop +/webgui hud @a webgui:/hud.html +``` + +Pages can also be bound to an entity, so right-clicking a villager opens your shop with that villager's context. Every URL the mod opens carries a signed token, so your backend knows which player is asking without a login screen. Players without the mod are told what they are missing instead of quietly seeing nothing. + +## Compatibility + +| Minecraft | Loader | +|-----------|--------| +| 26\.2 · 26\.1.2 | NeoForge | +| 1\.21.5 – 1.21.11 | Fabric · NeoForge | +| 1\.21 – 1.21.1 | Fabric · NeoForge | +| 1\.20.1 | Fabric | + +Minecraft 26 is NeoForge-only for now — Fabric is waiting on Yarn mappings for 26.x. + +**Installing:** on Fabric you need Fabric API, and that is it — the browser ships inside the jar. On NeoForge, add [Rinku](https://modrinth.com/mod/rinku) alongside it; launchers pick it up automatically. Chromium (~150 MB) downloads itself on first launch. Java 21, or Java 25 for Minecraft 26. + +## Links + +* **Documentation** — — guides, the full API, and examples you can paste +* **GitHub** — — issues and source +* **npm** — From 971313973ae497aafc03548eae0025d39c98b53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 02:48:45 +0500 Subject: [PATCH 06/14] =?UTF-8?q?ci:=20=D1=81=D0=BE=D0=B1=D0=B8=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B0=20Java=2025,=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=B8=D0=BB=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D1=82=D0=B0=D1=80=D1=8B=D0=B5=20=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B3=D0=B5=D1=82=D1=8B=20=D0=BF=D0=BE=D0=B4=2021?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loom 1.18.4 требует JVM 25 уже на этапе конфигурации, а оба плагина объявлены в общем блоке plugins — Gradle резолвит их classpath для каждого проекта, даже если применяется только один. Из-за этого на Java 21 падала конфигурация всех таргетов, включая 1.20.1. CI теперь ставит оба JDK, а org.gradle.java.installations.fromEnv указывает Gradle, где их искать: байткод старых версий по-прежнему собирается под 21 через options.release и toolchain. Заодно в матрицу добавлены 26.2-fabric, 26.3-fabric и 26.3-neoforge — они собирались только в релизном workflow и на PR не проверялись. --- .github/workflows/build.yml | 10 +++++++--- gradle.properties | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fcd2c4c..1c80238 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,17 +30,21 @@ jobs: # broken on both 26 targets for months precisely because nothing # booted one in CI. - version: 26.2-neoforge - java: '25' smoke: true - version: 26.1.2-neoforge - java: '25' smoke: true + - version: 26.2-fabric + - version: 26.3-fabric + - version: 26.3-neoforge steps: - uses: actions/checkout@v4 + # Both: Loom needs 25 to configure any target, the pre-26 ones compile against 21. - uses: actions/setup-java@v4 with: - java-version: ${{ matrix.java || '21' }} + java-version: | + 21 + 25 distribution: temurin cache: gradle diff --git a/gradle.properties b/gradle.properties index 990990c..42aa994 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,10 @@ org.gradle.jvmargs=-Xmx4G org.gradle.parallel=true dev.kikugie.stonecutter.hard_mode=true +# Loom 1.18.4 needs a Java 25 JVM to configure, while the pre-26 targets still compile +# against 21, so both JDKs have to be visible. On CI they come from setup-java; locally +# these variables do not exist and Gradle falls back to its own detection. +org.gradle.java.installations.fromEnv=JAVA_HOME_21_X64,JAVA_HOME_25_X64 mod_version=2.0.0 maven_group=land.webgui From be78552775b7ec501514bc2c7338c5f52cbd82d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 02:58:29 +0500 Subject: [PATCH 07/14] =?UTF-8?q?style:=20=D1=81=D0=BE=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=B8=D0=B8=20=D0=B4=D0=BE=20=D1=81=D1=83?= =?UTF-8?q?=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пять мест пересказывали ход отладки: под каким отладчиком ловился SIGSEGV, какие сигналы прилетали на каких раннерах, как именно заметили подмену курсора. Осталось то, что нужно читающему код: что ломается, почему и чего делать нельзя. Javadoc на интерфейсах движка не трогал — для публичного API объём нормальный. --- .../java/land/webgui/browser/SdlKeys.java | 24 ++++--------- src/main/java/land/webgui/compat/Mc.java | 17 +++------ .../space/webgui/wmc/core/CefCursors.java | 23 ++++-------- .../java/space/webgui/wmc/core/CefHost.java | 36 ++++++------------- .../java/space/webgui/wmc/core/ScanCodes.java | 26 ++++---------- 5 files changed, 34 insertions(+), 92 deletions(-) diff --git a/src/main/java/land/webgui/browser/SdlKeys.java b/src/main/java/land/webgui/browser/SdlKeys.java index 27769f7..5f754d6 100644 --- a/src/main/java/land/webgui/browser/SdlKeys.java +++ b/src/main/java/land/webgui/browser/SdlKeys.java @@ -1,26 +1,14 @@ package land.webgui.browser; /** - * What the game says about a key, said again in the numbers the browser expects. + * SDL key numbers, in the GLFW ones the engine speaks. * - *

From 26.3 Minecraft's window is SDL's, and the key it hands a mod is an SDL scan code — - * a USB HID usage, where {@code A} is 4. The engine underneath speaks GLFW, where {@code A} is - * 65, because that is what every Minecraft before 26.3 spoke and what java-cef was taught. So - * the translation happens here, at the edge of the mod: the engine keeps one contract and - * never learns there was a second windowing library. + *

From 26.3 the game hands out SDL scan codes — USB HID usages, where {@code A} is 4 — + * while the engine expects GLFW, where {@code A} is 65. Translating at the edge of the mod + * keeps the engine unaware that a second windowing library exists. * - *

Two numbers come out of each key, and they are not the same thing: - * - *

    - *
  • the GLFW key code, which decides what the page sees as - * {@code event.code};
  • - *
  • the native scan code, which on Windows is the only thing java-cef - * reads — it maps it with {@code MapVirtualKey}, and a key event without one arrives at - * the page as no key at all.
  • - *
- * - *

Unknown keys answer zero rather than guessing. A key the page never hears about is a - * missing feature; a key it hears about as the wrong one is a bug nobody can explain. + *

Each key yields two numbers: the GLFW key code, and the native scan code, which on + * Windows is the only one java-cef reads. Unknown keys answer zero rather than guessing. */ final class SdlKeys { diff --git a/src/main/java/land/webgui/compat/Mc.java b/src/main/java/land/webgui/compat/Mc.java index 8462e89..0eb9df0 100644 --- a/src/main/java/land/webgui/compat/Mc.java +++ b/src/main/java/land/webgui/compat/Mc.java @@ -7,20 +7,11 @@ /** * The handful of things Minecraft renamed, in one place. * - *

Every one of these is a line that would otherwise be written twice at every call site — - * once for the old name and once for the new — and the cost of that is not the typing. It is - * that a new Minecraft version means finding all of them, and the ones that are missed do not - * fail to compile: they take the wrong branch and misbehave quietly. That happened in this mod - * already, with predicates written as {@code >=26.2-neoforge}: correct until a Fabric target - * on 26.2 existed, then silently false. + *

If the game renamed it and this mod calls it more than once, it belongs here: a missed + * call site does not fail to compile, it takes the wrong branch quietly. * - *

So the rule here is: if the game renamed it and this mod calls it more than once, it - * belongs in this class. Porting to the next version then means reading one file. - * - *

What does not belong here is anything a loader differs on — that is a different - * axis and lives in the loader branches — and anything with a return type that changes, since - * hiding the difference is the one thing a method cannot do. Identifiers are the example: - * {@link Ids} exists for them and has to be branched at its own signature. + *

Loader differences do not belong here — those live in the loader branches — nor does + * anything whose return type changes, which a method cannot hide. See {@link Ids}. */ public final class Mc { diff --git a/src/main/java/space/webgui/wmc/core/CefCursors.java b/src/main/java/space/webgui/wmc/core/CefCursors.java index 00284f8..d9b1240 100644 --- a/src/main/java/space/webgui/wmc/core/CefCursors.java +++ b/src/main/java/space/webgui/wmc/core/CefCursors.java @@ -3,25 +3,14 @@ import java.awt.Cursor; /** - * What Chromium asks the cursor to be, said in the language a page uses. + * Chromium's cursor, as the CSS name a page would have written. * - *

The API says cursors in CSS names — {@code pointer}, {@code text}, {@code ew-resize} — - * because that is what the page itself wrote, and because a number out of someone else's header - * has no business in a contract that names no Chromium types. + *

The number java-cef reports is an AWT cursor, not a Chromium one — it translates on the + * way up. Reading it against Chromium's list gives names shifted by a few: a link reports + * {@code sw-resize}, text reports {@code pointer}. * - *

The number is an AWT cursor, not a Chromium one. java-cef translates - * Chromium's cursor into one of AWT's fourteen on the way up; upstream's own off-screen browser - * gives the game away by passing it straight to {@code new Cursor(int)}, which accepts nothing - * else. Read as Chromium's list instead — which is what the other embedder of this library - * does, and what this did first — the names come out shifted: a link reports {@code sw-resize} - * and text reports {@code pointer}, so the cursor changes over everything and is wrong over - * everything. It was caught by logging the names and noticing that {@code text} never appeared - * on a page made mostly of text. - * - *

What that costs. AWT has no name for {@code not-allowed}, {@code grab}, - * {@code zoom-in}, {@code progress} or {@code cell}, so Chromium's are flattened to the default - * arrow before this project ever sees them. A page asking for those gets an arrow, and there is - * nothing to be done about it here: the distinction is lost below us. + *

AWT has no name for {@code not-allowed}, {@code grab}, {@code zoom-in} or {@code cell}, + * so those are already flattened to the arrow before they reach here. */ final class CefCursors { diff --git a/src/main/java/space/webgui/wmc/core/CefHost.java b/src/main/java/space/webgui/wmc/core/CefHost.java index 2a8ada2..a1bf422 100644 --- a/src/main/java/space/webgui/wmc/core/CefHost.java +++ b/src/main/java/space/webgui/wmc/core/CefHost.java @@ -69,33 +69,19 @@ public final class CefHost implements AutoCloseable { /** * The one place the message loop is pumped from. * - *

java-cef pumps through {@code CefApp.doMessageLoopWork}, which posts a runnable that - * checks the app's state when it is scheduled rather than when it runs, arms a timer that - * checks nothing at all, and re-arms itself after every tick. Chromium asks for a pump - * after every task it posts, and during a shutdown it posts constantly - so a pump lands - * between the client going away and CefShutdown and dereferences what is no longer there. - * Under gdb that is SIGSEGV inside libcef in exactly that runnable, which the JVM reports - * as an abort; on the runners it was SIGABRT on Linux, SIGTRAP on macOS, and a trace-less - * exception on Windows, whenever the pump happened to land in the window. + *

java-cef's own {@code CefApp.doMessageLoopWork} checks the app state when a pump is + * scheduled rather than when it runs, so a pump landing between the client going away and + * CefShutdown crashes inside libcef. Chromium asks for pumps constantly during shutdown, + * so that window is hit often. Here every pump re-checks on the event thread, at the + * moment it would run. The native call is reached by reflection; nothing else in the + * process pumps. * - *

So Chromium's requests are answered here and never by java-cef's adapter, and every - * pump checks on the event thread, at the moment it would run, whether pumping is still - * wanted. The native call is java-cef's own {@code N_DoMessageLoopWork}, reached by - * reflection because there is no other way to it; MCEF solves the same problem by patching - * the class. Nothing else in the process pumps: the adapter is never asked, so its timer - * is never made. + *

Pace comes from whoever calls {@link CefHost#pump()}: a game turning the loop once a + * frame gets a page that answers at its frame rate, and Chromium's own requests are not + * armed while such a driver is at work. A runtime nobody drives turns the loop itself, and + * a driver that stops is noticed within {@link #LAPSE_MILLIS}. * - *

Who sets the pace. Whoever calls {@link CefHost#pump()} does, while - * they keep calling: a game that turns the loop once a frame gets a page that answers at - * the rate it draws at, rather than at whatever period a timer was given. While such a - * driver is at work Chromium's requests are not armed at all - the next turn is already - * coming, and arming them as well would pump the loop twice for one frame of work. A - * runtime nobody drives turns it itself, which is what a test and a headless process need, - * and a driver that stops being called is noticed within {@link #LAPSE_MILLIS} and taken - * over from. The two modes are one field and neither has to be configured. - * - *

Static, like the app handler that feeds it: java-cef keeps one app handler per - * process, installed before the host exists. + *

Static, like the app handler that feeds it: java-cef keeps one per process. */ private static final class Pump { diff --git a/src/main/java/space/webgui/wmc/core/ScanCodes.java b/src/main/java/space/webgui/wmc/core/ScanCodes.java index 874f499..1537bae 100644 --- a/src/main/java/space/webgui/wmc/core/ScanCodes.java +++ b/src/main/java/space/webgui/wmc/core/ScanCodes.java @@ -4,27 +4,15 @@ import java.lang.reflect.Field; /** - * Puts the scan code into a key event, because on Windows nothing else will do. + * Puts the scan code into an AWT key event. * - *

java-cef builds Chromium's key event out of an AWT one, and on Windows it does not read - * {@code getKeyCode()}. It reads a private field named {@code scancode} and maps that to a - * virtual key with {@code MapVirtualKey}. A key event made in Java has that field at zero, the - * mapping gives zero back, and every key a page is sent arrives as no key at all: pages saw - * {@code code=Escape key=Unidentified which=0} for W, A, S and D alike. Typing worked - * throughout, because a typed character goes by another route and carries the character itself - * - which is why this went unnoticed. + *

On Windows java-cef ignores {@code getKeyCode()} and reads a private {@code scancode} + * field, mapping it with {@code MapVirtualKey}. Left at zero, every key reaches the page as no + * key at all. * - *

Reaching the field. It is private in {@code java.awt.event}, a package the - * desktop module exports and does not open, so ordinary reflection is refused - measured, in a - * game: {@code module java.desktop does not "opens java.awt.event"}. What is left is - * {@code sun.misc.Unsafe}, whose module opens itself to everyone and whose field accessors ask - * no questions. That is what this uses, and it is not a happy answer: those accessors are - * deprecated for removal, so this works until a JDK decides otherwise. When that day comes the - * choice is a build of java-cef of our own, which is the thing this project exists not to have. - * - *

Refusing to work is not an option it takes. A key event with no scan code is what there - * was before; if the field cannot be reached, keys go out as they went out then, and pages - * receive the typed characters that always worked. + *

{@code java.awt.event} is exported but not opened, so the field is reached through + * {@code sun.misc.Unsafe}, whose accessors are deprecated for removal. If it cannot be reached, + * keys go out without a scan code, as they did before, and typed characters still work. */ final class ScanCodes { From beeb0b1caa16b08e8b3230432a6a2059d7c98a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 03:03:32 +0500 Subject: [PATCH 08/14] =?UTF-8?q?chore(log):=20=D1=83=D0=B2=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D1=83=20=D0=BD=D0=B0=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На INFO оставались строки, которые пишутся на каждое действие игрока: выбор формы курсора, частота отрисовки при создании браузера и нераспознанные сообщения страницы. Для игрока это шум, а в логе он перекрывает то, ради чего лог читают. Уровнем ниже они никуда не делись — включается отладочным логом. Сообщения жизненного цикла и вывод консоли страницы, который и так за настройкой, оставлены как были. --- src/main/java/land/webgui/WebGUIBrowserTuning.java | 2 +- src/main/java/land/webgui/WebviewPageToClientBridge.java | 4 ++-- src/main/java/land/webgui/browser/BrowserCursor.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/land/webgui/WebGUIBrowserTuning.java b/src/main/java/land/webgui/WebGUIBrowserTuning.java index a919f23..0e56857 100644 --- a/src/main/java/land/webgui/WebGUIBrowserTuning.java +++ b/src/main/java/land/webgui/WebGUIBrowserTuning.java @@ -31,7 +31,7 @@ public static void applyOnce(WebBrowser browser) { int target = WebGUIClientConfig.browserFrameRate(); if (target > 0) { browser.setWindowlessFrameRate(target); - WebGUIMod.LOGGER.info("webgui: browser paint rate asked for {} fps", target); + WebGUIMod.LOGGER.debug("webgui: browser paint rate asked for {} fps", target); } } catch (Throwable t) { WebGUIMod.LOGGER.warn("webgui: could not set the browser paint rate: {}", t.toString()); diff --git a/src/main/java/land/webgui/WebviewPageToClientBridge.java b/src/main/java/land/webgui/WebviewPageToClientBridge.java index 662ef8e..ea11244 100644 --- a/src/main/java/land/webgui/WebviewPageToClientBridge.java +++ b/src/main/java/land/webgui/WebviewPageToClientBridge.java @@ -58,7 +58,7 @@ private static void dispatch(CefFrame frame, String request, CefQueryCallback ca JsonObject obj = tryParseObject(request); if (obj == null) { - WebGUIMod.LOGGER.info("[webgui page→game] {}", request); + WebGUIMod.LOGGER.debug("[webgui page→game] {}", request); callback.success("{\"ok\":true}"); return; } @@ -145,7 +145,7 @@ private static void dispatch(CefFrame frame, String request, CefQueryCallback ca String pay = request; mc.execute(() -> sendToServer(ch, pay)); } else { - WebGUIMod.LOGGER.info("[webgui page→game] [{}] {}", channel, request); + WebGUIMod.LOGGER.debug("[webgui page→game] [{}] {}", channel, request); } } } diff --git a/src/main/java/land/webgui/browser/BrowserCursor.java b/src/main/java/land/webgui/browser/BrowserCursor.java index 021c145..d7c88df 100644 --- a/src/main/java/land/webgui/browser/BrowserCursor.java +++ b/src/main/java/land/webgui/browser/BrowserCursor.java @@ -52,12 +52,12 @@ void set(String cssCursor) { //? if >26.2 { /*shape = shapeFor(wanted); if (SEEN.add(wanted)) { - land.webgui.WebGUIMod.LOGGER.info("webgui: cursor \"{}\" -> {}", wanted, shape); + land.webgui.WebGUIMod.LOGGER.debug("webgui: cursor \"{}\" -> {}", wanted, shape); } *///? } else { long handle = shapeFor(wanted); if (SEEN.add(wanted)) { - land.webgui.WebGUIMod.LOGGER.info("webgui: cursor \"{}\" -> {}", + land.webgui.WebGUIMod.LOGGER.debug("webgui: cursor \"{}\" -> {}", wanted, handle == 0L ? "no shape, left as the arrow" : "a shape of its own"); } apply(handle); From bc99ff37590ce1ef2d39a67ba4ee187d25879034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 03:14:54 +0500 Subject: [PATCH 09/14] =?UTF-8?q?feat(update):=20=D0=B2=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B0=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20Mod?= =?UTF-8?q?rinth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка больше не требует настройки и выполняется всегда при старте сервера. Спрашиваем Modrinth по своей версии игры и лоадеру — он не предложит сборку, которую сюда не поставить. Если Modrinth недоступен, спрашиваем последний релиз на GitHub: ответ грубее, но лучше, чем ничего. Раньше проверка молчала, пока оператор не найдёт updateCheckUrl и не впишет туда свой адрес. Настройку убрал, адрес зафиксирован в коде. Оператору, который зашёл на сервер, теперь говорим в чат — иначе про обновление знает только лог при старте. Выключается через updateNotifyInChat; обычным игрокам не пишем никогда. --- CHANGELOG.md | 3 + src/main/java/land/webgui/WebviewJoinHud.java | 8 + .../webgui/server/WebGUIUpdateChecker.java | 280 ++++++++++++------ .../webgui/server/WebviewServerConfig.java | 19 +- 4 files changed, 216 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40af4a7..5974a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ client still talks to a 1.8.x server. - **Minecraft 26.2 on Fabric, and 26.3 on both loaders.** 26.3 is published as a beta: the game itself is still in snapshots, and it replaced GLFW with SDL and moved the GPU API out of `blaze3d`, so the parts of this mod that touch the window and the screen are new there. ### Changed +- **The update check is built in and always runs.** The server asks Modrinth on start whether a newer WebGUI exists for its Minecraft version and loader, and falls back to GitHub releases if Modrinth cannot be reached. It used to do nothing at all unless an operator found `updateCheckUrl` and filled in an endpoint of their own, which almost nobody did. + - Operators are also told in chat when they join, so an update is not something only the start-up log knows. Set `updateNotifyInChat` to `false` to keep the console notice and drop the chat one. Ordinary players are never told. + - **`updateCheckUrl` is gone.** The address is fixed in the mod now. A config that still has the key is not an error; it is ignored. - **WebGUI no longer needs another mod to run.** The embedded browser is part of WebGUI now instead of being provided by a separate library mod, so installing WebGUI is one file and nothing else. Chromium itself is still downloaded once per machine on first start, outside the game directory, and shared by every instance. ### Fixed diff --git a/src/main/java/land/webgui/WebviewJoinHud.java b/src/main/java/land/webgui/WebviewJoinHud.java index f3b521a..3238e2a 100644 --- a/src/main/java/land/webgui/WebviewJoinHud.java +++ b/src/main/java/land/webgui/WebviewJoinHud.java @@ -22,6 +22,10 @@ public static void register() { 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)) { @@ -59,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/server/WebGUIUpdateChecker.java b/src/main/java/land/webgui/server/WebGUIUpdateChecker.java index 5726d2c..7747fd8 100644 --- a/src/main/java/land/webgui/server/WebGUIUpdateChecker.java +++ b/src/main/java/land/webgui/server/WebGUIUpdateChecker.java @@ -1,130 +1,244 @@ package land.webgui.server; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import land.webgui.WebGUIMod; +import land.webgui.WebGUIVersion; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; //? if fabric { -import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.SemanticVersion; import net.fabricmc.loader.api.VersionParsingException; //? } else { -/*import net.neoforged.fml.ModList; -import org.apache.maven.artifact.versioning.DefaultArtifactVersion;*/ +/*import org.apache.maven.artifact.versioning.DefaultArtifactVersion;*/ //? } import java.net.URI; +import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.time.Duration; +/** + * Asks whether a newer WebGUI exists, once, when the server starts. + * + *

Modrinth first: it knows which builds exist for this Minecraft version and this loader, + * so it will not offer one that cannot be installed here. GitHub answers only what is newest + * overall, so it is the fallback for when Modrinth cannot be reached. + * + *

The console notice is not optional. Telling operators in chat on join is, through + * {@code updateNotifyInChat}. + */ public final class WebGUIUpdateChecker { + + private static final String MODRINTH_PROJECT = "tDw1xQja"; + private static final String GITHUB_REPO = "mc-webgui/webgui"; + + //? if fabric { + private static final String LOADER = "fabric"; + //? } else { + /*private static final String LOADER = "neoforge"; + *///? } + + /** What was found, or null while nothing newer is known. Read from the join thread. */ + private static volatile Update available; + private WebGUIUpdateChecker() {} + /** A version newer than the one running. */ + public record Update(String version, String url, String channel) {} + public static void checkAsync() { - String url = WebviewServerConfig.updateCheckUrl(); - if (url.isEmpty()) { - return; - } - //? if fabric { - String current = FabricLoader.getInstance() - .getModContainer(WebGUIMod.MOD_ID) - .map(c -> c.getMetadata().getVersion().getFriendlyString()) - .orElse(""); - //? } else { - /*String current = ModList.get() - .getModContainerById(WebGUIMod.MOD_ID) - .map(c -> c.getModInfo().getVersion().toString()) - .orElse("");*/ - //? } - if (current.isEmpty()) { + String full = WebGUIVersion.current(); + String current = stripBuildMeta(full); + if (current.isEmpty() || "unknown".equals(current)) { return; } + String game = gameVersion(full); Thread t = new Thread(() -> { - try { - HttpClient http = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) - .followRedirects(HttpClient.Redirect.NORMAL) - .build(); - HttpRequest req = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(10)) - .header("User-Agent", "WebGUI-Mod/" + current) - .header("Accept", "application/json") - .GET() - .build(); - HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString()); - if (resp.statusCode() != 200) { - WebGUIMod.LOGGER.debug("webgui: update check returned HTTP {}", resp.statusCode()); - return; - } - parseAndLog(current, resp.body(), url); - } catch (Exception e) { - WebGUIMod.LOGGER.debug("webgui: update check failed: {}", e.getMessage()); + Update found = fromModrinth(current, game); + if (found == null) { + found = fromGitHub(current); } + if (found == null) { + return; + } + available = found; + WebGUIMod.LOGGER.info("+------------------------------------------------"); + WebGUIMod.LOGGER.info("| WebGUI update available: {} -> {}{}", + current, found.version(), channelSuffix(found)); + if (!found.url().isEmpty()) { + WebGUIMod.LOGGER.info("| Download: {}", found.url()); + } + WebGUIMod.LOGGER.info("+------------------------------------------------"); }, "webgui-update-check"); t.setDaemon(true); t.start(); } - private static void parseAndLog(String current, String body, String checkUrl) { - String remoteRaw; - String downloadUrl = ""; + /** Says it to an operator who just joined, if the server was told to. */ + public static void notifyOnJoin(ServerPlayer player) { + Update found = available; + if (found == null || player == null || !WebviewServerConfig.updateNotifyInChat()) { + return; + } + //? if >=1.21.5 { + boolean operator = net.minecraft.commands.Commands.LEVEL_GAMEMASTERS.check(player.permissions()); + //? } else { + /*boolean operator = player.hasPermissions(2); + *///? } + if (!operator) { + return; + } + player.sendSystemMessage(Component.literal("[WebGUI] Update available: " + + stripBuildMeta(WebGUIVersion.current()) + " -> " + found.version() + + channelSuffix(found)) + .withStyle(ChatFormatting.YELLOW)); + if (!found.url().isEmpty()) { + player.sendSystemMessage(Component.literal("[WebGUI] " + found.url()) + .withStyle(ChatFormatting.GRAY)); + } + } + + // Filtered by loader and game version, so anything that comes back can be installed here. + private static Update fromModrinth(String current, String game) { + if (game.isEmpty()) { + return null; + } + String url = "https://api.modrinth.com/v2/project/" + MODRINTH_PROJECT + "/version" + + "?loaders=" + encode("[\"" + LOADER + "\"]") + + "&game_versions=" + encode("[\"" + game + "\"]"); + String body = get(url); + if (body == null) { + return null; + } try { - JsonObject o = JsonParser.parseString(body).getAsJsonObject(); - if (o.has("version") && !o.get("version").isJsonNull()) { - // {"version":"1.2.3","url":"..."} - remoteRaw = o.get("version").getAsString(); - if (o.has("url") && !o.get("url").isJsonNull()) { - downloadUrl = o.get("url").getAsString(); + JsonArray versions = JsonParser.parseString(body).getAsJsonArray(); + Update best = null; + for (JsonElement element : versions) { + JsonObject o = element.getAsJsonObject(); + String number = stripBuildMeta(text(o, "version_number")); + if (number.isEmpty() || !isNewer(number, current)) { + continue; } - } else if (o.has("tag_name") && !o.get("tag_name").isJsonNull()) { - // GitHub Releases API: {"tag_name":"v1.2.3","html_url":"..."} - remoteRaw = o.get("tag_name").getAsString(); - if (o.has("html_url") && !o.get("html_url").isJsonNull()) { - downloadUrl = o.get("html_url").getAsString(); + if (best != null && !isNewer(number, best.version())) { + continue; } - } else { - WebGUIMod.LOGGER.debug("webgui: update check: unrecognised response format"); - return; + best = new Update(number, pageUrl(o), text(o, "version_type")); } - } catch (Exception e) { - WebGUIMod.LOGGER.debug("webgui: update check: could not parse response: {}", e.getMessage()); - return; + return best; + } catch (Exception malformed) { + WebGUIMod.LOGGER.debug("webgui: update check: Modrinth answered unexpectedly: {}", + malformed.getMessage()); + return null; } + } - String remote = stripBuildMeta(remoteRaw.trim().replaceFirst("^[vV]", "")); - String local = stripBuildMeta(current.trim().replaceFirst("^[vV]", "")); + // Newest release overall, which may have no build for this Minecraft version. Only reached + // when Modrinth is unavailable, where an imprecise answer beats none. + private static Update fromGitHub(String current) { + String body = get("https://api.github.com/repos/" + GITHUB_REPO + "/releases/latest"); + if (body == null) { + return null; + } + try { + JsonObject o = JsonParser.parseString(body).getAsJsonObject(); + String number = stripBuildMeta(text(o, "tag_name")); + if (number.isEmpty() || !isNewer(number, current)) { + return null; + } + return new Update(number, text(o, "html_url"), "release"); + } catch (Exception malformed) { + WebGUIMod.LOGGER.debug("webgui: update check: GitHub answered unexpectedly: {}", + malformed.getMessage()); + return null; + } + } - boolean newer; + private static String get(String url) { + try { + HttpClient http = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(10)) + .header("User-Agent", "WebGUI-Mod/" + WebGUIVersion.current()) + .header("Accept", "application/json") + .GET() + .build(); + HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + WebGUIMod.LOGGER.debug("webgui: update check: HTTP {} from {}", response.statusCode(), url); + return null; + } + return response.body(); + } catch (Exception unreachable) { + WebGUIMod.LOGGER.debug("webgui: update check failed: {}", unreachable.getMessage()); + return null; + } + } + + private static boolean isNewer(String remote, String local) { //? if fabric { try { - newer = SemanticVersion.parse(remote).compareTo(SemanticVersion.parse(local)) > 0; - } catch (VersionParsingException e) { - // fallback: simple string compare is not reliable, skip - WebGUIMod.LOGGER.debug("webgui: update check: could not compare versions '{}' vs '{}': {}", remote, local, e.getMessage()); - return; + return SemanticVersion.parse(remote).compareTo(SemanticVersion.parse(local)) > 0; + } catch (VersionParsingException unparseable) { + WebGUIMod.LOGGER.debug("webgui: update check: cannot compare {} with {}", remote, local); + return false; } //? } else { - /*newer = new DefaultArtifactVersion(remote).compareTo(new DefaultArtifactVersion(local)) > 0;*/ - //? } - - if (newer) { - WebGUIMod.LOGGER.info("┌─────────────────────────────────────────────────"); - WebGUIMod.LOGGER.info("│ WebGUI update available: {} → {}", local, remote); - if (!downloadUrl.isEmpty()) { - WebGUIMod.LOGGER.info("│ Download: {}", downloadUrl); - } - WebGUIMod.LOGGER.info("└─────────────────────────────────────────────────"); - } else { - WebGUIMod.LOGGER.debug("webgui: up to date ({} >= {})", local, remote); - } + /*return new DefaultArtifactVersion(remote).compareTo(new DefaultArtifactVersion(local)) > 0; + *///? } + } + + private static String channelSuffix(Update found) { + return "release".equals(found.channel()) || found.channel().isEmpty() + ? "" : " (" + found.channel() + ")"; + } + + private static String pageUrl(JsonObject version) { + String id = text(version, "id"); + return id.isEmpty() + ? "https://modrinth.com/mod/webgui/versions" + : "https://modrinth.com/mod/webgui/version/" + id; + } + + private static String text(JsonObject o, String key) { + return o.has(key) && !o.get(key).isJsonNull() + ? o.get(key).getAsString().trim().replaceFirst("^[vV]", "") + : ""; } - /** Strips the +mcX.Y.Z build-metadata suffix added by our Gradle build. */ + /** This mod reports {@code 2.0.0+mc26.3}; Modrinth adds the loader after that. */ private static String stripBuildMeta(String version) { - int plus = version.indexOf('+'); - return plus >= 0 ? version.substring(0, plus) : version; + String v = version == null ? "" : version.trim().replaceFirst("^[vV]", ""); + int plus = v.indexOf('+'); + return plus >= 0 ? v.substring(0, plus) : v; + } + + /** The Minecraft version out of our own, which carries it as {@code +mc26.3}. */ + private static String gameVersion(String full) { + if (full == null) { + return ""; + } + int mc = full.indexOf("+mc"); + if (mc < 0) { + return ""; + } + String rest = full.substring(mc + 3); + int dash = rest.indexOf('-'); + return dash >= 0 ? rest.substring(0, dash) : rest; + } + + private static String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); } } diff --git a/src/main/java/land/webgui/server/WebviewServerConfig.java b/src/main/java/land/webgui/server/WebviewServerConfig.java index 77574dd..093fc90 100644 --- a/src/main/java/land/webgui/server/WebviewServerConfig.java +++ b/src/main/java/land/webgui/server/WebviewServerConfig.java @@ -62,8 +62,9 @@ private static Path configPath() { // Bundled page bytes one player may pull per second. 0 = no limit. private int assetBytesPerSecond = 4 * 1024 * 1024; - // {"version":"1.2.3"} or GitHub Releases {"tag_name":"v1.2.3","html_url":"..."}; empty = disabled - private String updateCheckUrl = ""; + // Tell operators in chat, on join, when a newer version exists. The notice the server + // writes to its console at start is not optional. + private Boolean updateNotifyInChat; // Origins (scheme://host[:port]) whose pages may run commands as the player. Sent to the client // on join; the client rejects command requests from any other origin. Empty = no page may run commands. @@ -102,7 +103,7 @@ private static void writeExample() { o.addProperty("requireClientMod", false); o.addProperty("pageEventsPerSecond", 20); - o.addProperty("updateCheckUrl", ""); + o.addProperty("updateNotifyInChat", true); JsonArray origins = new JsonArray(); origins.add("https://your-site.example"); @@ -159,11 +160,6 @@ private void applyDefaultsAfterLoad() { } else { mainMenuUrl = mainMenuUrl.trim(); } - if (updateCheckUrl == null) { - updateCheckUrl = ""; - } else { - updateCheckUrl = updateCheckUrl.trim(); - } List cleaned = new ArrayList<>(); if (trustedCommandOrigins != null) { for (String o : trustedCommandOrigins) { @@ -286,9 +282,10 @@ public static int assetBytesPerSecond() { return Math.max(0, instance.assetBytesPerSecond); } - public static String updateCheckUrl() { - String u = instance.updateCheckUrl; - return u == null ? "" : u.trim(); + /** Whether operators are told in chat on join. True unless the file says otherwise. */ + public static boolean updateNotifyInChat() { + Boolean v = instance.updateNotifyInChat; + return v == null || v; } /** Origins whose pages may run commands as the player. Never null. */ From 683cbe1b935ea7c0a1777eefd67e9d9b3b3c10ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 03:21:09 +0500 Subject: [PATCH 10/14] =?UTF-8?q?feat(url):=20=D0=BF=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=B0=D1=82=D1=8C=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA?= =?UTF-8?q?=D0=BE=20webgui://,=20http(s)://=20=D0=B8=20ws(s)://?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Резолвер возвращал нетронутым всё, что не начиналось с webgui: — то есть открыл бы и file://, и javascript:, и data:. URL страницы приходит с сервера, так что это граница доверия, а не удобство: file:// читает диск игрока, javascript: выполняется в открытой странице. Теперь чужая схема не открывается, а в лог идёт причина с перечнем допустимых. Заодно одна форма вместо трёх: webgui:name.html и webgui:/name.html раньше значили то же, что webgui://name.html. Проверять одно написание проще, чем три. Так же поправлен разбор на стороне страницы, иначе ссылка вела бы себя иначе, чем настройка. Поведение закрыто тестом: шесть случаев, включая те формы, что раньше работали. --- CHANGELOG.md | 3 + MODRINTH.md | 2 +- dev/testpages/README.md | 2 +- dev/testpages/index.html | 2 +- .../java/land/webgui/WebGUIAssetServer.java | 28 +++++--- .../java/land/webgui/WebviewScriptInject.java | 11 ++- .../land/webgui/server/WebviewAssets.java | 2 +- .../webgui/server/WebviewServerConfig.java | 4 +- .../webgui/WebGUIAssetServerSchemeTest.java | 71 +++++++++++++++++++ 9 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 src/test/java/land/webgui/WebGUIAssetServerSchemeTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5974a96..c06a8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ client still talks to a 1.8.x server. - **The update check is built in and always runs.** The server asks Modrinth on start whether a newer WebGUI exists for its Minecraft version and loader, and falls back to GitHub releases if Modrinth cannot be reached. It used to do nothing at all unless an operator found `updateCheckUrl` and filled in an endpoint of their own, which almost nobody did. - Operators are also told in chat when they join, so an update is not something only the start-up log knows. Set `updateNotifyInChat` to `false` to keep the console notice and drop the chat one. Ordinary players are never told. - **`updateCheckUrl` is gone.** The address is fixed in the mod now. A config that still has the key is not an error; it is ignored. +- **A page URL must now name a scheme the mod will open:** `webgui://`, `https://`, `http://`, `wss://` or `ws://`. Anything else is refused with a line in the log instead of being handed to the browser — a URL arrives from a server, and `file://` would read the player's disk while `javascript:` would run inside whatever page is open. + - `webgui:name.html` and `webgui:/name.html` used to mean the same as `webgui://name.html` and no longer do. One spelling is easier to check than three. Settings that use the old form need the two slashes. +- **`mainMenuUrl` now points at the server's own page out of the box.** A fresh server writes `webgui://index.html` into its config, which is the starter page it also creates, so the key that opens the main menu shows something of the server's from the first launch instead of `example.com`. A config that already has the setting is left alone. - **WebGUI no longer needs another mod to run.** The embedded browser is part of WebGUI now instead of being provided by a separate library mod, so installing WebGUI is one file and nothing else. Chromium itself is still downloaded once per machine on first start, outside the game directory, and shared by every instance. ### Fixed diff --git a/MODRINTH.md b/MODRINTH.md index beab4a5..6f8fb05 100644 --- a/MODRINTH.md +++ b/MODRINTH.md @@ -50,7 +50,7 @@ Opening a page is one command: ``` /webgui gui @a https://your-site.example/shop -/webgui hud @a webgui:/hud.html +/webgui hud @a webgui://hud.html ``` Pages can also be bound to an entity, so right-clicking a villager opens your shop with that villager's context. Every URL the mod opens carries a signed token, so your backend knows which player is asking without a login screen. Players without the mod are told what they are missing instead of quietly seeing nothing. diff --git a/dev/testpages/README.md b/dev/testpages/README.md index 5c28427..7049bca 100644 --- a/dev/testpages/README.md +++ b/dev/testpages/README.md @@ -27,6 +27,6 @@ The death page is the one that needs more than copying: the server only suppress vanilla screen when it has been asked to, so set it in `config/webgui/server.json` and reload. No reconnect — `reload` pushes the url to everyone already online. - "deathScreenUrl": "webgui:/death.html" + "deathScreenUrl": "webgui://death.html" /webgui reload diff --git a/dev/testpages/index.html b/dev/testpages/index.html index 40afd67..9e8ca68 100644 --- a/dev/testpages/index.html +++ b/dev/testpages/index.html @@ -41,7 +41,7 @@

This page

Death screen → - set deathScreenUrl to webgui:/death.html, then die + set deathScreenUrl to webgui://death.html, then die HUD page → diff --git a/src/main/java/land/webgui/WebGUIAssetServer.java b/src/main/java/land/webgui/WebGUIAssetServer.java index 765e7b4..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; /** @@ -133,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 REMOTE_SCHEMES = List.of("https://", "http://", "wss://", "ws://"); + /** - * Turns {@code webgui:/path} into a URL the browser can load. + * Turns {@code webgui://path} into a URL the browser can load, and refuses anything that + * is not a page. * - * Anything else is returned untouched — an http(s) URL is a page hosted the ordinary - * way, and that has to keep working exactly as it did. + * A remote URL passes through untouched — that is a page hosted the ordinary way. Any + * other scheme is refused rather than loaded: a page URL arrives from a server, and + * {@code file://} would read the player's disk while {@code javascript:} would run in + * whatever page is open. */ public static String resolve(String url) { if (url == null || url.isBlank()) { return url; } String trimmed = url.trim(); - if (!trimmed.regionMatches(true, 0, "webgui:", 0, 7)) { - return trimmed; + for (String scheme : REMOTE_SCHEMES) { + if (trimmed.regionMatches(true, 0, scheme, 0, scheme.length())) { + return trimmed; + } + } + if (!trimmed.regionMatches(true, 0, "webgui://", 0, 9)) { + WebGUIMod.LOGGER.warn("webgui: refusing to open {} - a page must be webgui://," + + " https://, http://, wss:// or ws://", trimmed); + return "about:blank"; } - String rest = trimmed.substring(7); - // Accept webgui:/index.html, webgui://index.html and webgui:index.html alike: - // the difference is invisible in a config file and nobody would guess right. + String rest = trimmed.substring(9); String query = ""; int mark = rest.indexOf('?'); if (mark >= 0) { diff --git a/src/main/java/land/webgui/WebviewScriptInject.java b/src/main/java/land/webgui/WebviewScriptInject.java index 4975515..5ec14a1 100644 --- a/src/main/java/land/webgui/WebviewScriptInject.java +++ b/src/main/java/land/webgui/WebviewScriptInject.java @@ -31,7 +31,7 @@ public static String bridgeSetup(String assetsBase, boolean hudMode) { // needs to know which one it is in. window.webgui.isHud = %s;""".formatted(base, hudMode) + """ - // webgui:whatever.html, as a link a page can actually click. + // webgui://whatever.html, as a link a page can actually click. // // The scheme is this mod's shorthand for "a file this server ships"; the // browser knows nothing about it, so written in an href it would simply @@ -42,13 +42,10 @@ public static String bridgeSetup(String assetsBase, boolean hudMode) { window.webgui.resolve = function (url) { if (typeof url !== 'string') return url; var trimmed = url.trim(); - if (!/^webgui:/i.test(trimmed)) return trimmed; + if (!/^webgui:[/][/]/i.test(trimmed)) return trimmed; var base = window.webgui.assetsBase || ''; if (!base) return trimmed; - // webgui:x, webgui:/x and webgui://x mean the same thing: the - // difference is invisible in a config file and nobody would guess right. - var rest = trimmed.slice(7); - while (rest.charAt(0) === '/') rest = rest.slice(1); + var rest = trimmed.slice(9); if (rest === '' || rest.charAt(0) === '?') rest = 'index.html' + rest; while (base.charAt(base.length - 1) === '/') base = base.slice(0, -1); return base + '/' + rest; @@ -64,7 +61,7 @@ public static String bridgeSetup(String assetsBase, boolean hudMode) { while (node && node.nodeType === 1 && node.tagName !== 'A') node = node.parentNode; if (!node || node.nodeType !== 1) return; var href = node.getAttribute('href'); - if (!href || !/^webgui:/i.test(href)) return; + if (!href || !/^webgui:[/][/]/i.test(href)) return; var resolved = window.webgui.resolve(href); if (resolved === href) return; ev.preventDefault(); diff --git a/src/main/java/land/webgui/server/WebviewAssets.java b/src/main/java/land/webgui/server/WebviewAssets.java index aad5799..1a33608 100644 --- a/src/main/java/land/webgui/server/WebviewAssets.java +++ b/src/main/java/land/webgui/server/WebviewAssets.java @@ -134,7 +134,7 @@ private static void writeStarterPage(Path root) throws IOException {

It works.

This page came from config/webgui/web/index.html on the server.

-

Point mainMenuUrl at webgui:/index.html and edit away.

+

mainMenuUrl already points here. Edit this file and reload.

", + "ftp://example.com/x", + "chrome://settings", + "example.com/no-scheme-at-all" }) { + assertEquals(REFUSED, WebGUIAssetServer.resolve(url), url + " must not be opened"); + } + } + + @Test + void refusesTheFormsThatUsedToBeAccepted() { + // webgui:x and webgui:/x were taken as the same thing as webgui://x. One spelling is + // easier to check than three, and these now say so rather than working by accident. + assertEquals(REFUSED, WebGUIAssetServer.resolve("webgui:index.html")); + assertEquals(REFUSED, WebGUIAssetServer.resolve("webgui:/index.html")); + } + + @Test + void nothingIsLeftAloneRatherThanRefused() { + assertNull(WebGUIAssetServer.resolve(null)); + assertEquals(" ", WebGUIAssetServer.resolve(" ")); + } +} From 532feed6231cdd8aca225bc33f59ead5d2571f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 03:24:20 +0500 Subject: [PATCH 11/14] =?UTF-8?q?feat(assets):=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D1=83=D1=8E=20hud.html=20=D1=80=D1=8F=D0=B4=D0=BE?= =?UTF-8?q?=D0=BC=20=D1=81=20index.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сервер отдавал на старте только полноэкранную страницу, а HUD оператору приходилось писать с нуля — и первая же ошибка там одна и та же: фон на html или body закрывает игру, потому что оверлей рисуется поверх мира. Теперь рядом создаётся hud.html: карточка в углу с горячими клавишами (F6, `, F12) и ссылкой на webgui.space, прозрачная как положено. autoHudUrl по умолчанию указывает на неё, но показ при входе по-прежнему выключен — само ничего не появится. --- CHANGELOG.md | 2 + .../land/webgui/server/WebviewAssets.java | 40 +++++++++++++++++++ .../webgui/server/WebviewServerConfig.java | 4 +- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c06a8e0..ae55b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ client still talks to a 1.8.x server. - **`updateCheckUrl` is gone.** The address is fixed in the mod now. A config that still has the key is not an error; it is ignored. - **A page URL must now name a scheme the mod will open:** `webgui://`, `https://`, `http://`, `wss://` or `ws://`. Anything else is refused with a line in the log instead of being handed to the browser — a URL arrives from a server, and `file://` would read the player's disk while `javascript:` would run inside whatever page is open. - `webgui:name.html` and `webgui:/name.html` used to mean the same as `webgui://name.html` and no longer do. One spelling is easier to check than three. Settings that use the old form need the two slashes. +- **A server now starts with a HUD page as well as a full-screen one.** `config/webgui/web/hud.html` is created beside `index.html` on first start: a small corner card naming the keys — F6 for the main menu, ` to interact with the overlay, F12 for developer tools — and linking to webgui.space. It paints no background of its own, which is what an overlay drawn over the world has to do. + - `autoHudUrl` points at it by default. Showing a HUD on join is still off unless `autoHudOnJoin` is set, so nothing appears uninvited; a server that had `autoHudOnJoin` on without a URL used to log a complaint and now shows this page. - **`mainMenuUrl` now points at the server's own page out of the box.** A fresh server writes `webgui://index.html` into its config, which is the starter page it also creates, so the key that opens the main menu shows something of the server's from the first launch instead of `example.com`. A config that already has the setting is left alone. - **WebGUI no longer needs another mod to run.** The embedded browser is part of WebGUI now instead of being provided by a separate library mod, so installing WebGUI is one file and nothing else. Chromium itself is still downloaded once per machine on first start, outside the game directory, and shared by every instance. diff --git a/src/main/java/land/webgui/server/WebviewAssets.java b/src/main/java/land/webgui/server/WebviewAssets.java index 1a33608..2780398 100644 --- a/src/main/java/land/webgui/server/WebviewAssets.java +++ b/src/main/java/land/webgui/server/WebviewAssets.java @@ -52,6 +52,7 @@ public static void reload() { if (!Files.exists(root)) { Files.createDirectories(root); writeStarterPage(root); + writeStarterHud(root); } } catch (IOException e) { WebGUIMod.LOGGER.warn("webgui: could not create {}: {}", root, e.getMessage()); @@ -145,4 +146,43 @@ private static void writeStarterPage(Path root) throws IOException { """); WebGUIMod.LOGGER.info("webgui: created a starter page at {}", index); } + + // An overlay, not a screen: it is drawn over the world, so it paints no background of its + // own and keeps to a corner. A background on html or body would hide the game. + private static void writeStarterHud(Path root) throws IOException { + Path hud = root.resolve("hud.html"); + if (Files.exists(hud)) { + return; + } + Files.writeString(hud, """ + + + WebGUI HUD + +
+ """); + WebGUIMod.LOGGER.info("webgui: created a starter HUD page at {}", hud); + } } diff --git a/src/main/java/land/webgui/server/WebviewServerConfig.java b/src/main/java/land/webgui/server/WebviewServerConfig.java index 94eb4fd..b0551d0 100644 --- a/src/main/java/land/webgui/server/WebviewServerConfig.java +++ b/src/main/java/land/webgui/server/WebviewServerConfig.java @@ -40,7 +40,7 @@ private static Path configPath() { private String tokenSecretBase64 = ""; private Boolean autoHudOnJoin; - private String autoHudUrl = ""; + private String autoHudUrl = "webgui://hud.html"; private String mainMenuUrl = "webgui://index.html"; @@ -91,7 +91,7 @@ private static void writeExample() { o.addProperty("tokenSecretBase64", ""); o.addProperty("autoHudOnJoin", false); - o.addProperty("autoHudUrl", "http://your-site.example/hud"); + o.addProperty("autoHudUrl", "webgui://hud.html"); o.addProperty("mainMenuUrl", "webgui://index.html"); From a88418bab9f7e9c03c2540ec101b0207ea10548a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 03:29:47 +0500 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B9=20HUD=20=D0=BF=D1=80=D0=B8=20=D0=B2?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=D0=B5=20=D0=B8=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20example.com?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit autoHudOnJoin теперь включён по умолчанию: страницу для HUD сервер создаёт сам, а показывать её было некому. autoHudUrl уже указывал на неё. Сервер, где любая из двух настроек задана явно, сохраняет своё. Запасной адрес был https://example.com — заглушка, и именно её видел человек, нажавший F6 на титульном экране, где сервер ещё ничего не прислал. Теперь это сайт проекта. --- CHANGELOG.md | 3 ++- src/main/java/land/webgui/StartUrls.java | 11 ++++++++++- .../java/land/webgui/server/WebviewServerConfig.java | 6 ++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae55b3d..1f34c6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ client still talks to a 1.8.x server. - **A page URL must now name a scheme the mod will open:** `webgui://`, `https://`, `http://`, `wss://` or `ws://`. Anything else is refused with a line in the log instead of being handed to the browser — a URL arrives from a server, and `file://` would read the player's disk while `javascript:` would run inside whatever page is open. - `webgui:name.html` and `webgui:/name.html` used to mean the same as `webgui://name.html` and no longer do. One spelling is easier to check than three. Settings that use the old form need the two slashes. - **A server now starts with a HUD page as well as a full-screen one.** `config/webgui/web/hud.html` is created beside `index.html` on first start: a small corner card naming the keys — F6 for the main menu, ` to interact with the overlay, F12 for developer tools — and linking to webgui.space. It paints no background of its own, which is what an overlay drawn over the world has to do. - - `autoHudUrl` points at it by default. Showing a HUD on join is still off unless `autoHudOnJoin` is set, so nothing appears uninvited; a server that had `autoHudOnJoin` on without a URL used to log a complaint and now shows this page. + - It is shown on join out of the box: `autoHudUrl` points at it and `autoHudOnJoin` is now on unless the config says otherwise. A server that set either of them keeps what it set. +- **The main-menu key no longer opens `example.com`.** That placeholder was what a player saw after installing the mod and pressing the key on the title screen, where no server has sent any settings yet. It now opens webgui.space, which at least says what the key is for. - **`mainMenuUrl` now points at the server's own page out of the box.** A fresh server writes `webgui://index.html` into its config, which is the starter page it also creates, so the key that opens the main menu shows something of the server's from the first launch instead of `example.com`. A config that already has the setting is left alone. - **WebGUI no longer needs another mod to run.** The embedded browser is part of WebGUI now instead of being provided by a separate library mod, so installing WebGUI is one file and nothing else. Chromium itself is still downloaded once per machine on first start, outside the game directory, and shared by every instance. diff --git a/src/main/java/land/webgui/StartUrls.java b/src/main/java/land/webgui/StartUrls.java index 73511d0..8e052a2 100644 --- a/src/main/java/land/webgui/StartUrls.java +++ b/src/main/java/land/webgui/StartUrls.java @@ -1,7 +1,16 @@ package land.webgui; +/** + * Where a page opens when nobody said where. + * + *

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/server/WebviewServerConfig.java b/src/main/java/land/webgui/server/WebviewServerConfig.java index b0551d0..b0f3e87 100644 --- a/src/main/java/land/webgui/server/WebviewServerConfig.java +++ b/src/main/java/land/webgui/server/WebviewServerConfig.java @@ -39,6 +39,8 @@ private static Path configPath() { private String queryParamName = "webgui_token"; private String tokenSecretBase64 = ""; + // On unless the file says otherwise, and autoHudUrl points at the page the server + // creates for itself, so a fresh server shows something rather than nothing. private Boolean autoHudOnJoin; private String autoHudUrl = "webgui://hud.html"; @@ -90,7 +92,7 @@ private static void writeExample() { o.addProperty("queryParamName", "webgui_token"); o.addProperty("tokenSecretBase64", ""); - o.addProperty("autoHudOnJoin", false); + o.addProperty("autoHudOnJoin", true); o.addProperty("autoHudUrl", "webgui://hud.html"); o.addProperty("mainMenuUrl", "webgui://index.html"); @@ -206,7 +208,7 @@ public static byte[] tokenSecretBytes() { public static boolean autoHudOnJoin() { Boolean v = instance.autoHudOnJoin; - return v != null && v; + return v == null || v; } public static String autoHudUrl() { From f5af353e3a2b303e00838b59fac148087bd2dcde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 03:34:05 +0500 Subject: [PATCH 13/14] =?UTF-8?q?feat(input):=20=D1=81=D0=BA=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D0=BB=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8E=2025,=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=2040?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Семь ставили, когда колесо почти не двигало страницу. На длинной странице это всё ещё читается как медленно. Шаги в настройках пересобраны под новый диапазон. Клиент, у которого значение уже сохранено, остаётся при своём: менять чужой выбор при обновлении неправильно. --- CHANGELOG.md | 1 + src/main/java/land/webgui/WebGUIClientConfig.java | 4 ++-- src/main/java/land/webgui/WebGUISettingsScreen.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f34c6b..1dd570f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ client still talks to a 1.8.x server. - `webgui:name.html` and `webgui:/name.html` used to mean the same as `webgui://name.html` and no longer do. One spelling is easier to check than three. Settings that use the old form need the two slashes. - **A server now starts with a HUD page as well as a full-screen one.** `config/webgui/web/hud.html` is created beside `index.html` on first start: a small corner card naming the keys — F6 for the main menu, ` to interact with the overlay, F12 for developer tools — and linking to webgui.space. It paints no background of its own, which is what an overlay drawn over the world has to do. - It is shown on join out of the box: `autoHudUrl` points at it and `autoHudOnJoin` is now on unless the config says otherwise. A server that set either of them keeps what it set. +- **Scrolling a page starts at 25 notches instead of 7,** and the setting now goes to 40. Seven was picked to fix a wheel that barely moved, and it still read as slow on a long page. A client that already has the setting saved keeps its own value; it is in the mod's settings screen either way. - **The main-menu key no longer opens `example.com`.** That placeholder was what a player saw after installing the mod and pressing the key on the title screen, where no server has sent any settings yet. It now opens webgui.space, which at least says what the key is for. - **`mainMenuUrl` now points at the server's own page out of the box.** A fresh server writes `webgui://index.html` into its config, which is the starter page it also creates, so the key that opens the main menu shows something of the server's from the first launch instead of `example.com`. A config that already has the setting is left alone. - **WebGUI no longer needs another mod to run.** The embedded browser is part of WebGUI now instead of being provided by a separate library mod, so installing WebGUI is one file and nothing else. Chromium itself is still downloaded once per machine on first start, outside the game directory, and shared by every instance. diff --git a/src/main/java/land/webgui/WebGUIClientConfig.java b/src/main/java/land/webgui/WebGUIClientConfig.java index a048010..d7bea8e 100644 --- a/src/main/java/land/webgui/WebGUIClientConfig.java +++ b/src/main/java/land/webgui/WebGUIClientConfig.java @@ -24,7 +24,7 @@ public final class WebGUIClientConfig { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); /** As fast as this setting goes. Past it a notch throws a page across its own length. */ - public static final int SCROLL_MAX = 20; + public static final int SCROLL_MAX = 40; /** Off by default: a developer's tool, and it makes the log noisier. */ private boolean devTools = false; @@ -38,7 +38,7 @@ public final class WebGUIClientConfig { * One for one is what a browser does, and in a page it reads as barely moving — the game * counts a notch where a desktop browser counts several lines. */ - private int scrollNotches = 7; + private int scrollNotches = 25; private static WebGUIClientConfig instance = new WebGUIClientConfig(); diff --git a/src/main/java/land/webgui/WebGUISettingsScreen.java b/src/main/java/land/webgui/WebGUISettingsScreen.java index 64a94d7..f1759db 100644 --- a/src/main/java/land/webgui/WebGUISettingsScreen.java +++ b/src/main/java/land/webgui/WebGUISettingsScreen.java @@ -25,7 +25,7 @@ public class WebGUISettingsScreen extends Screen { /** What the button steps through. A slider's constructor differs on every other * Minecraft version; a button that cycles does not. */ - private static final int[] SCROLL_STEPS = {1, 3, 5, 7, 10, 15, 20}; + private static final int[] SCROLL_STEPS = {1, 3, 5, 10, 15, 20, 25, 30, 40}; private Button devToolsButton; private Button scrollButton; From 9cef6eb86df7d900cf781b2aec43afdefa619c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=B2?= Date: Tue, 13 Oct 2026 05:04:17 +0500 Subject: [PATCH 14/14] =?UTF-8?q?feat(settings):=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D0=B7=D0=BA=D0=B8=20=D1=83=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=86=D0=B8=D0=B9=20=D0=B8=20=D0=B4=D0=B2=D0=B5=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20jso?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Экран показывал две опции из трёх существующих и объяснял их текстом внизу, привязанным к одной из них. Теперь у каждой опции подсказка: что делает и чем за это платишь — как в настройках графики самой игры. Добавлены две настройки, которые движок умел, а мод жёстко задавал: спрашивать место для скачиваемого файла и разрешать странице включать звук самой. Обе читаются при старте Chromium, и подсказки об этом говорят. Частота отрисовки была только в json — вынесена в экран. Убрана неправда: экран советовал добавить --remote-debugging-port в лаунчер. Так было со сторонней библиотекой, где командная строка фиксировалась до старта мода; теперь порт назначаем мы сами, и адрес инспектора показывается, когда он действительно поднят. --- CHANGELOG.md | 5 + .../java/land/webgui/WebGUIClientConfig.java | 39 ++++ .../land/webgui/WebGUIRemoteDebugging.java | 23 ++- .../land/webgui/WebGUISettingsScreen.java | 170 ++++++++++++------ .../java/land/webgui/browser/BrowserBoot.java | 12 +- .../resources/assets/webgui/lang/en_us.json | 15 +- .../resources/assets/webgui/lang/ru_ru.json | 17 +- 7 files changed, 208 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd570f..6eb5124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ client still talks to a 1.8.x server. - `webgui:name.html` and `webgui:/name.html` used to mean the same as `webgui://name.html` and no longer do. One spelling is easier to check than three. Settings that use the old form need the two slashes. - **A server now starts with a HUD page as well as a full-screen one.** `config/webgui/web/hud.html` is created beside `index.html` on first start: a small corner card naming the keys — F6 for the main menu, ` to interact with the overlay, F12 for developer tools — and linking to webgui.space. It paints no background of its own, which is what an overlay drawn over the world has to do. - It is shown on join out of the box: `autoHudUrl` points at it and `autoHudOnJoin` is now on unless the config says otherwise. A server that set either of them keeps what it set. +- **The mod's settings screen now explains itself.** Every option has a tooltip saying what it does and what it costs, in the spirit of the game's own video settings, and two more settings are there instead of only in the config file: + - **Ask where to save downloads.** Off, a file goes into `webgui-downloads/` and the name is said in chat; on, the system save dialog opens for each one. The engine could always do this and the mod never offered it. + - **Let pages play sound on their own.** Off by default, as it already was: video and audio wait for a click, the way a browser treats a tab you did not open. + - **Page frame rate** was in the config file and nowhere else; it is now a setting like the rest. + - The screen used to tell everyone to add `--remote-debugging-port=9222` to their launcher. That was true of the old browser library, whose command line was fixed before any mod ran; this one takes the port from the mod, so the advice was for a problem that no longer exists. The inspector's address is shown when it is actually listening, and only while the developer tools are on. - **Scrolling a page starts at 25 notches instead of 7,** and the setting now goes to 40. Seven was picked to fix a wheel that barely moved, and it still read as slow on a long page. A client that already has the setting saved keeps its own value; it is in the mod's settings screen either way. - **The main-menu key no longer opens `example.com`.** That placeholder was what a player saw after installing the mod and pressing the key on the title screen, where no server has sent any settings yet. It now opens webgui.space, which at least says what the key is for. - **`mainMenuUrl` now points at the server's own page out of the box.** A fresh server writes `webgui://index.html` into its config, which is the starter page it also creates, so the key that opens the main menu shows something of the server's from the first launch instead of `example.com`. A config that already has the setting is left alone. diff --git a/src/main/java/land/webgui/WebGUIClientConfig.java b/src/main/java/land/webgui/WebGUIClientConfig.java index d7bea8e..b9583aa 100644 --- a/src/main/java/land/webgui/WebGUIClientConfig.java +++ b/src/main/java/land/webgui/WebGUIClientConfig.java @@ -40,6 +40,12 @@ public final class WebGUIClientConfig { */ private int scrollNotches = 25; + /** Whether Chromium opens the system save dialog instead of filing downloads itself. */ + private boolean askWhereToSaveDownloads = false; + + /** Whether a page may start audio or video without the player touching it first. */ + private boolean allowPageAutoplay = false; + private static WebGUIClientConfig instance = new WebGUIClientConfig(); private WebGUIClientConfig() {} @@ -107,4 +113,37 @@ public static void setDevTools(boolean value) { instance.devTools = value; save(); } + + public static boolean askWhereToSaveDownloads() { + return instance.askWhereToSaveDownloads; + } + + public static void setAskWhereToSaveDownloads(boolean value) { + if (instance.askWhereToSaveDownloads == value) { + return; + } + instance.askWhereToSaveDownloads = value; + save(); + } + + public static boolean allowPageAutoplay() { + return instance.allowPageAutoplay; + } + + public static void setAllowPageAutoplay(boolean value) { + if (instance.allowPageAutoplay == value) { + return; + } + instance.allowPageAutoplay = value; + save(); + } + + public static void setBrowserFrameRate(int value) { + int wanted = Math.max(0, value); + if (instance.browserFrameRate == wanted) { + return; + } + instance.browserFrameRate = wanted; + save(); + } } 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 f1759db..3637acb 100644 --- a/src/main/java/land/webgui/WebGUISettingsScreen.java +++ b/src/main/java/land/webgui/WebGUISettingsScreen.java @@ -1,12 +1,14 @@ package land.webgui; 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; @@ -15,20 +17,31 @@ * * 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. */ public class WebGUISettingsScreen extends Screen { private static final int ROW_HEIGHT = 24; - private static final int BUTTON_WIDTH = 200; - - private final Screen parent; + private static final int BUTTON_WIDTH = 220; + private static final int ROWS = 5; - /** What the button steps through. A slider's constructor differs on every other - * Minecraft version; a button that cycles does not. */ private static final int[] SCROLL_STEPS = {1, 3, 5, 10, 15, 20, 25, 30, 40}; - private Button 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}; + + 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; @@ -38,43 +51,101 @@ public WebGUISettingsScreen(Screen parent) { 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; - this.scrollButton = Button.builder(scrollLabel(), b -> { - WebGUIClientConfig.setScrollNotches(nextScrollStep()); + this.scrollButton = row(left, top, scrollLabel(), "scroll", b -> { + WebGUIClientConfig.setScrollNotches(next(SCROLL_STEPS, WebGUIClientConfig.scrollNotches())); this.scrollButton.setMessage(scrollLabel()); - }).bounds(left, top, BUTTON_WIDTH, 20).build(); - this.addRenderableWidget(this.scrollButton); + }); + + this.frameRateButton = row(left, top + ROW_HEIGHT, frameRateLabel(), "framerate", b -> { + WebGUIClientConfig.setBrowserFrameRate(nextFrameRate()); + this.frameRateButton.setMessage(frameRateLabel()); + }); - this.devToolsButton = Button.builder(devToolsLabel(), b -> { + 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 + ROW_HEIGHT, 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()); } - private static int nextScrollStep() { - int current = WebGUIClientConfig.scrollNotches(); - for (int step : SCROLL_STEPS) { + 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 SCROLL_STEPS[0]; + return steps[0]; + } + + // 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]; + } + + 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 Component.translatable("screen.webgui.settings.scroll") - .append(": ") - .append(Component.literal(String.valueOf(WebGUIClientConfig.scrollNotches()))); + 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 Component.translatable("screen.webgui.settings.devtools") - .append(": ") - .append(Component.translatable(WebGUIClientConfig.devTools() ? "options.on" : "options.off")); + 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 { @@ -82,56 +153,37 @@ private Component devToolsLabel() { 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 * 2; - 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 { @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 * 2; - 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) { - Mc.openScreen(this.minecraft, this.parent); - } + Mc.openScreen(this.minecraft, this.parent); } } diff --git a/src/main/java/land/webgui/browser/BrowserBoot.java b/src/main/java/land/webgui/browser/BrowserBoot.java index ffb586d..74a5401 100644 --- a/src/main/java/land/webgui/browser/BrowserBoot.java +++ b/src/main/java/land/webgui/browser/BrowserBoot.java @@ -56,14 +56,21 @@ public static synchronized void start() { Path gameDirectory = Paths.get("").toAbsolutePath().normalize(); RuntimeSettings settings = RuntimeSettings.defaults() .gameDirectory(gameDirectory) - .extraArguments(CHROMIUM_FLAGS) - .downloads(RuntimeSettings.Downloads.defaults().into(gameDirectory.resolve(DOWNLOADS))); + .extraArguments(CHROMIUM_FLAGS); // Read here rather than waited for: this runs before the client finishes starting, // and Chromium takes its debugging port once, when it starts. Loading twice is // harmless — it re-reads the same file. land.webgui.WebGUIClientConfig.load(); + // Both are read once, here, because Chromium takes them when it starts. The settings + // screen says so rather than letting a player wonder why nothing changed. + settings = settings + .allowAutoplay(land.webgui.WebGUIClientConfig.allowPageAutoplay()) + .downloads(RuntimeSettings.Downloads.defaults() + .into(gameDirectory.resolve(DOWNLOADS)) + .asking(land.webgui.WebGUIClientConfig.askWhereToSaveDownloads())); + // Set rather than read. With Rinku this port could only be reported, because the one // command line that mattered was fixed before any mod ran; here it is ours to choose — // so the setting in the mod's own screen is enough and no launch argument is needed. @@ -76,6 +83,7 @@ public static synchronized void start() { } if (port > 0) { settings = settings.remoteDebuggingPort(port); + land.webgui.WebGUIRemoteDebugging.started(port); land.webgui.WebGUIMod.LOGGER.info( "webgui: the page inspector is available on http://127.0.0.1:{}", port); } diff --git a/src/main/resources/assets/webgui/lang/en_us.json b/src/main/resources/assets/webgui/lang/en_us.json index 92c9929..c5cb960 100644 --- a/src/main/resources/assets/webgui/lang/en_us.json +++ b/src/main/resources/assets/webgui/lang/en_us.json @@ -10,10 +10,17 @@ "message.webgui.hud_interactive_off": "Web HUD: view only. Press the interactive mode key to enable input.", "screen.webgui.settings.title": "WebGUI Settings", "screen.webgui.settings.devtools": "Page developer tools", - "screen.webgui.settings.devtools.hint1": "Writes browser errors, failed requests and console output", - "screen.webgui.settings.devtools.hint2": "Also opens the inspector on F12. Takes effect after a restart.", "screen.webgui.settings.inspector.on": "Chrome DevTools: open %s in a browser", - "screen.webgui.settings.inspector.off": "For the full inspector, add this launcher game argument:", + "screen.webgui.settings.inspector.off": "The inspector opens after a restart.", "screen.webgui.settings.scroll": "Scroll speed", - "screen.webgui.settings.scroll.hint": "How far one notch of the wheel carries a page." + "screen.webgui.settings.scroll.tip": "How far one notch of the wheel carries a page. The game counts one notch where a browser counts several lines, so at low values a page moves less than the hand expects.", + "screen.webgui.settings.framerate": "Page frame rate", + "screen.webgui.settings.framerate.value": "%s fps", + "screen.webgui.settings.framerate.unlimited": "Unlimited", + "screen.webgui.settings.framerate.tip": "How often a page may repaint. Higher is smoother and costs more CPU and GPU; a page that only shows text gains nothing from it. Unlimited leaves the choice to the browser.", + "screen.webgui.settings.downloads": "Ask where to save downloads", + "screen.webgui.settings.downloads.tip": "Off: a file a page offers is saved into webgui-downloads/ beside the game and you are told the name. On: the system save dialog opens for each one. Takes effect after a restart.", + "screen.webgui.settings.autoplay": "Let pages play sound on their own", + "screen.webgui.settings.autoplay.tip": "Off: video and audio on a page wait until you click it, the way a browser treats a tab you did not open. On: a page may start playing by itself. Takes effect after a restart.", + "screen.webgui.settings.devtools.tip": "Writes browser errors, failed requests and console output to the game log, and opens the inspector on F12. Off by default: it makes the log noisy. Takes effect after a restart." } diff --git a/src/main/resources/assets/webgui/lang/ru_ru.json b/src/main/resources/assets/webgui/lang/ru_ru.json index 239946f..8eae612 100644 --- a/src/main/resources/assets/webgui/lang/ru_ru.json +++ b/src/main/resources/assets/webgui/lang/ru_ru.json @@ -9,11 +9,18 @@ "message.webgui.hud_interactive_on": "Веб-HUD: интерактивный режим (псевдо-GUI). Нажмите снова для выхода.", "message.webgui.hud_interactive_off": "Веб-HUD: только просмотр. Нажмите клавишу интерактивного режима для ввода.", "screen.webgui.settings.title": "Настройки WebGUI", - "screen.webgui.settings.devtools": "Инструменты разработчика страниц", - "screen.webgui.settings.devtools.hint1": "Пишет ошибки браузера, упавшие запросы и вывод консоли", - "screen.webgui.settings.devtools.hint2": "С ней же F12 открывает инспектор. Применяется после перезапуска.", + "screen.webgui.settings.devtools": "Инструменты разработчика", "screen.webgui.settings.inspector.on": "Chrome DevTools: откройте %s в браузере", - "screen.webgui.settings.inspector.off": "Для полного инспектора добавьте аргумент запуска игры:", + "screen.webgui.settings.inspector.off": "Инспектор откроется после перезапуска.", "screen.webgui.settings.scroll": "Скорость прокрутки", - "screen.webgui.settings.scroll.hint": "Насколько далеко страницу уносит один щелчок колеса." + "screen.webgui.settings.scroll.tip": "На сколько страницу уносит один щелчок колеса. Игра считает щелчки, браузер — строки, поэтому при малых значениях страница двигается меньше, чем ждёт рука.", + "screen.webgui.settings.framerate": "Частота отрисовки страницы", + "screen.webgui.settings.framerate.value": "%s кадр/с", + "screen.webgui.settings.framerate.unlimited": "Без ограничения", + "screen.webgui.settings.framerate.tip": "Как часто странице разрешено перерисовываться. Выше — плавнее и дороже по процессору и видеокарте; странице из одного текста это ничего не даёт. «Без ограничения» оставляет выбор браузеру.", + "screen.webgui.settings.downloads": "Спрашивать, куда сохранять", + "screen.webgui.settings.downloads.tip": "Выключено: файл со страницы попадает в webgui-downloads рядом с игрой, имя сообщается в чат. Включено: на каждый файл открывается системное окно сохранения. Применится после перезапуска.", + "screen.webgui.settings.autoplay": "Разрешить страницам звук без спроса", + "screen.webgui.settings.autoplay.tip": "Выключено: видео и звук на странице ждут вашего клика, как браузер поступает с вкладкой, которую вы не открывали. Включено: страница может заиграть сама. Применится после перезапуска.", + "screen.webgui.settings.devtools.tip": "Пишет в игровой лог ошибки браузера, неудавшиеся запросы и вывод консоли, а также открывает инспектор по F12. По умолчанию выключено: лог становится шумным. Применится после перезапуска." }