From c5dba5d65d768a2537fe547d7a0edfc90481e76f Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sat, 19 Sep 2026 22:48:18 +0530 Subject: [PATCH] feat(gui): add in-game minimal configuration GUI, keybind, and ModMenu integration --- adapter-26_2/build.gradle | 1 + .../adapter26_2/BaritoneActionExecutor.java | 7 +- .../adapter26_2/FabricInputBridge.java | 10 + .../adapter26_2/FabricWorldSensor.java | 6 +- .../adapter26_2/JevPlayerClientMod.java | 36 +++- .../adapter26_2/JevPlayerConfigScreen.java | 172 ++++++++++++++++++ .../adapter26_2/JevPlayerModMenu.java | 11 ++ .../src/main/resources/fabric.mod.json | 3 + .../core/engine/JevPlayerEngine.java | 24 +++ 9 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerConfigScreen.java create mode 100644 adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerModMenu.java diff --git a/adapter-26_2/build.gradle b/adapter-26_2/build.gradle index bd1ac85..b3cae75 100644 --- a/adapter-26_2/build.gradle +++ b/adapter-26_2/build.gradle @@ -17,6 +17,7 @@ dependencies { compileOnly fileTree(dir: 'libs/fabric-api', include: ['*.jar']) compileOnly fileTree(dir: "${System.getProperty('user.home')}/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries", include: ['**/*.jar']) + compileOnly fileTree(dir: "${System.getProperty('user.home')}/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/Performium/minecraft/mods", include: ['modmenu-*.jar']) } processResources { diff --git a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/BaritoneActionExecutor.java b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/BaritoneActionExecutor.java index 439346a..f98c895 100644 --- a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/BaritoneActionExecutor.java +++ b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/BaritoneActionExecutor.java @@ -13,7 +13,7 @@ import java.util.Objects; public final class BaritoneActionExecutor implements ActionExecutorBridge { private final Minecraft client; - private final boolean fairMode; + private boolean fairMode; private String currentAction = null; private static final String[] WOOD_LOGS = { @@ -36,6 +36,11 @@ public final class BaritoneActionExecutor implements ActionExecutorBridge { configureBaritoneSettings(); } + public void setFairMode(boolean fairMode) { + this.fairMode = fairMode; + configureBaritoneSettings(); + } + public void configureBaritoneSettings() { try { var settings = BaritoneAPI.getSettings(); diff --git a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/FabricInputBridge.java b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/FabricInputBridge.java index 1febaff..da5e0c9 100644 --- a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/FabricInputBridge.java +++ b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/FabricInputBridge.java @@ -11,6 +11,7 @@ public final class FabricInputBridge implements InputBridge { private final Minecraft client; private final KeyMapping killSwitchKey; private final KeyMapping hudToggleKey; + private final KeyMapping configGuiKey; public FabricInputBridge(Minecraft client) { this.client = client; @@ -24,6 +25,11 @@ public final class FabricInputBridge implements InputBridge { GLFW.GLFW_KEY_F9, KeyMapping.Category.MISC )); + this.configGuiKey = KeyMappingHelper.registerKeyMapping(new KeyMapping( + "key.jevplayer.config_gui", + GLFW.GLFW_KEY_F10, + KeyMapping.Category.MISC + )); } public boolean wasKillSwitchPressed() { @@ -34,6 +40,10 @@ public final class FabricInputBridge implements InputBridge { return hudToggleKey.consumeClick(); } + public boolean wasConfigGuiPressed() { + return configGuiKey.consumeClick(); + } + @Override public void releaseAllInputs() { if (client.options == null) return; diff --git a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/FabricWorldSensor.java b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/FabricWorldSensor.java index 0196bd9..32d39ac 100644 --- a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/FabricWorldSensor.java +++ b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/FabricWorldSensor.java @@ -15,13 +15,17 @@ import java.util.*; public final class FabricWorldSensor implements WorldSensor { private final Minecraft client; - private final boolean fairMode; + private boolean fairMode; public FabricWorldSensor(Minecraft client, boolean fairMode) { this.client = client; this.fairMode = fairMode; } + public void setFairMode(boolean fairMode) { + this.fairMode = fairMode; + } + @Override public StateSnapshot captureSnapshot(long nowMs) { var player = client.player; diff --git a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerClientMod.java b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerClientMod.java index a70de0a..bf69edc 100644 --- a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerClientMod.java +++ b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerClientMod.java @@ -68,6 +68,23 @@ public class JevPlayerClientMod implements ClientModInitializer { return instance; } + public JevPlayerConfig getConfig() { + return config; + } + + public void reloadConfig() { + loadConfig(); + if (engine != null) { + engine.reloadConfig(this.config); + } + if (worldSensor != null) { + worldSensor.setFairMode(this.config.fairMode); + } + if (baritoneExecutor != null) { + baritoneExecutor.setFairMode(this.config.fairMode); + } + } + public void loadConfig() { File file = configPath.toFile(); if (file.exists()) { @@ -115,6 +132,11 @@ public class JevPlayerClientMod implements ClientModInitializer { inputBridge.sendChatMessage("§b[JevPlayer] HUD overlay " + (config.hudEnabled ? "§aEnabled" : "§cDisabled")); } + // Check F10 Config GUI toggle + if (inputBridge.wasConfigGuiPressed()) { + client.setScreenAndShow(new JevPlayerConfigScreen(null)); + } + // Advance ticks compositeExecutor.onTick(); engine.tick(); @@ -146,10 +168,22 @@ public class JevPlayerClientMod implements ClientModInitializer { return 1; })) .then(ClientCommands.literal("reload").executes(ctx -> { - loadConfig(); + reloadConfig(); ctx.getSource().sendFeedback(Component.literal("§b[JevPlayer] Configuration reloaded.")); return 1; })) + .then(ClientCommands.literal("config").executes(ctx -> { + Minecraft.getInstance().execute(() -> { + Minecraft.getInstance().setScreenAndShow(new JevPlayerConfigScreen(null)); + }); + return 1; + })) + .then(ClientCommands.literal("gui").executes(ctx -> { + Minecraft.getInstance().execute(() -> { + Minecraft.getInstance().setScreenAndShow(new JevPlayerConfigScreen(null)); + }); + return 1; + })) .then(ClientCommands.literal("test_gather_wood").executes(ctx -> { ctx.getSource().sendFeedback(Component.literal("§e[JevPlayer] Testing Baritone wood gathering...")); baritoneExecutor.startAction("gather_wood", worldSensor.captureSnapshot(System.currentTimeMillis())); diff --git a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerConfigScreen.java b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerConfigScreen.java new file mode 100644 index 0000000..6122f40 --- /dev/null +++ b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerConfigScreen.java @@ -0,0 +1,172 @@ +package com.typesafe.jevplayer.adapter26_2; + +import com.typesafe.jevplayer.core.config.JevPlayerConfig; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class JevPlayerConfigScreen extends Screen { + private final Screen parentScreen; + private final JevPlayerConfig config; + + private boolean isTypeSafe; + private boolean fairMode; + + private EditBox apiKeyEdit; + private EditBox modelEdit; + private EditBox baseUrlEdit; + private EditBox intervalEdit; + private EditBox maxSpendEdit; + + private Button providerButton; + private Button fairModeButton; + + private String statusMessage = null; + private int statusColor = 0xFF55FF55; + + public JevPlayerConfigScreen(Screen parentScreen) { + super(Component.literal("JevPlayer Configuration")); + this.parentScreen = parentScreen; + this.config = JevPlayerClientMod.getInstance().getConfig(); + this.isTypeSafe = "typesafe".equalsIgnoreCase(config.provider); + this.fairMode = config.fairMode; + } + + private Component getProviderComponent() { + return Component.literal("Provider: " + (isTypeSafe ? "§bTypeSafe AI (Live)" : "§eMock (Simulated)")); + } + + private Component getFairModeComponent() { + return Component.literal("Fair Perception: " + (fairMode ? "§aON (Player View Only)" : "§cOFF (All-Seeing)")); + } + + @Override + protected void init() { + int centerX = this.width / 2; + int fieldW = 240; + int leftX = centerX - fieldW / 2; + int startY = 32; + + // Provider Button + this.providerButton = addRenderableWidget( + Button.builder(getProviderComponent(), btn -> { + isTypeSafe = !isTypeSafe; + btn.setMessage(getProviderComponent()); + }).bounds(leftX, startY, fieldW, 20).build() + ); + + // API Key Field + this.apiKeyEdit = new EditBox(this.font, leftX, startY + 36, fieldW, 20, Component.literal("API Key")); + this.apiKeyEdit.setMaxLength(256); + this.apiKeyEdit.setValue(config.apiKey != null ? config.apiKey : ""); + addRenderableWidget(this.apiKeyEdit); + + // Model Field + this.modelEdit = new EditBox(this.font, leftX, startY + 72, fieldW, 20, Component.literal("Model")); + this.modelEdit.setMaxLength(64); + this.modelEdit.setValue(config.model != null ? config.model : "jev-latest"); + addRenderableWidget(this.modelEdit); + + // Base URL Field + this.baseUrlEdit = new EditBox(this.font, leftX, startY + 108, fieldW, 20, Component.literal("Base URL")); + this.baseUrlEdit.setMaxLength(256); + this.baseUrlEdit.setValue(config.baseUrl != null ? config.baseUrl : "https://api.typesafe.ai/v1"); + addRenderableWidget(this.baseUrlEdit); + + // Fair Mode Button + this.fairModeButton = addRenderableWidget( + Button.builder(getFairModeComponent(), btn -> { + fairMode = !fairMode; + btn.setMessage(getFairModeComponent()); + }).bounds(leftX, startY + 144, fieldW, 20).build() + ); + + // Interval and Max Spend (Two columns) + int halfW = (fieldW - 10) / 2; + this.intervalEdit = new EditBox(this.font, leftX, startY + 180, halfW, 20, Component.literal("Interval")); + this.intervalEdit.setMaxLength(10); + this.intervalEdit.setValue(String.valueOf(config.minDecisionIntervalMs)); + addRenderableWidget(this.intervalEdit); + + this.maxSpendEdit = new EditBox(this.font, leftX + halfW + 10, startY + 180, halfW, 20, Component.literal("Max Spend")); + this.maxSpendEdit.setMaxLength(10); + this.maxSpendEdit.setValue(String.valueOf(config.maxSpendUsd)); + addRenderableWidget(this.maxSpendEdit); + + // Save & Cancel Buttons + int btnY = Math.max(startY + 214, this.height - 28); + addRenderableWidget( + Button.builder(Component.literal("§aSave & Apply"), btn -> saveAndClose()) + .bounds(leftX, btnY, halfW, 20).build() + ); + + addRenderableWidget( + Button.builder(Component.literal("§cCancel"), btn -> onClose()) + .bounds(leftX + halfW + 10, btnY, halfW, 20).build() + ); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float delta) { + super.extractRenderState(extractor, mouseX, mouseY, delta); + + int centerX = this.width / 2; + int fieldW = 240; + int leftX = centerX - fieldW / 2; + int startY = 32; + + extractor.centeredText(this.font, Component.literal("§6§lJevPlayer Settings"), centerX, 12, 0xFFFFFFFF); + + extractor.text(this.font, Component.literal("§7API Key (or env TYPESAFE_AI_API_KEY):"), leftX, startY + 25, 0xFFAAAAAA); + extractor.text(this.font, Component.literal("§7Model:"), leftX, startY + 61, 0xFFAAAAAA); + extractor.text(this.font, Component.literal("§7Base URL:"), leftX, startY + 97, 0xFFAAAAAA); + extractor.text(this.font, Component.literal("§7Min Interval (ms):"), leftX, startY + 169, 0xFFAAAAAA); + extractor.text(this.font, Component.literal("§7Max Spend ($):"), leftX + (fieldW - 10) / 2 + 10, startY + 169, 0xFFAAAAAA); + + if (statusMessage != null) { + extractor.centeredText(this.font, Component.literal(statusMessage), centerX, this.height - 46, statusColor); + } + } + + @Override + public void onClose() { + if (this.minecraft != null) { + this.minecraft.setScreenAndShow(this.parentScreen); + } + } + + private void saveAndClose() { + try { + config.provider = isTypeSafe ? "typesafe" : "mock"; + config.apiKey = apiKeyEdit.getValue().trim(); + config.model = modelEdit.getValue().trim(); + config.baseUrl = baseUrlEdit.getValue().trim(); + config.fairMode = fairMode; + + try { + config.minDecisionIntervalMs = Long.parseLong(intervalEdit.getValue().trim()); + } catch (NumberFormatException ignored) {} + + try { + config.maxSpendUsd = Double.parseDouble(maxSpendEdit.getValue().trim()); + } catch (NumberFormatException ignored) {} + + config.validate(); + JevPlayerClientMod.getInstance().saveConfig(); + JevPlayerClientMod.getInstance().reloadConfig(); + + if (this.minecraft != null && this.minecraft.player != null) { + this.minecraft.player.sendSystemMessage( + Component.literal("§a[JevPlayer] Settings saved! Active Provider: §f" + config.provider + + (isTypeSafe ? " §7(Key: §e" + config.getRedactedApiKey() + "§7)" : "")) + ); + } + onClose(); + } catch (Exception e) { + this.statusMessage = "§cFailed to save settings: " + e.getMessage(); + this.statusColor = 0xFFFF5555; + } + } +} diff --git a/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerModMenu.java b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerModMenu.java new file mode 100644 index 0000000..d6ecbce --- /dev/null +++ b/adapter-26_2/src/main/java/com/typesafe/jevplayer/adapter26_2/JevPlayerModMenu.java @@ -0,0 +1,11 @@ +package com.typesafe.jevplayer.adapter26_2; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; + +public class JevPlayerModMenu implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return JevPlayerConfigScreen::new; + } +} diff --git a/adapter-26_2/src/main/resources/fabric.mod.json b/adapter-26_2/src/main/resources/fabric.mod.json index bb6c864..615f13c 100644 --- a/adapter-26_2/src/main/resources/fabric.mod.json +++ b/adapter-26_2/src/main/resources/fabric.mod.json @@ -17,6 +17,9 @@ "entrypoints": { "client": [ "com.typesafe.jevplayer.adapter26_2.JevPlayerClientMod" + ], + "modmenu": [ + "com.typesafe.jevplayer.adapter26_2.JevPlayerModMenu" ] }, "depends": { diff --git a/core/src/main/java/com/typesafe/jevplayer/core/engine/JevPlayerEngine.java b/core/src/main/java/com/typesafe/jevplayer/core/engine/JevPlayerEngine.java index 650e779..7bda4c9 100644 --- a/core/src/main/java/com/typesafe/jevplayer/core/engine/JevPlayerEngine.java +++ b/core/src/main/java/com/typesafe/jevplayer/core/engine/JevPlayerEngine.java @@ -100,6 +100,30 @@ public final class JevPlayerEngine implements AutoCloseable { } } + public void reloadConfig(JevPlayerConfig newConfig) { + if (newConfig != null) { + this.config.apiKey = newConfig.apiKey; + this.config.apiKeyEnvVar = newConfig.apiKeyEnvVar; + this.config.provider = newConfig.provider; + this.config.model = newConfig.model; + this.config.baseUrl = newConfig.baseUrl; + this.config.minDecisionIntervalMs = newConfig.minDecisionIntervalMs; + this.config.maxActionAgeMs = newConfig.maxActionAgeMs; + this.config.requestTimeoutMs = newConfig.requestTimeoutMs; + this.config.maxSpendUsd = newConfig.maxSpendUsd; + this.config.fairMode = newConfig.fairMode; + this.config.allowMultiplayer = newConfig.allowMultiplayer; + this.config.hudEnabled = newConfig.hudEnabled; + this.config.hudDebugPage = newConfig.hudDebugPage; + this.config.logLevel = newConfig.logLevel; + if (newConfig.confidenceThresholds != null) { + this.config.confidenceThresholds = new HashMap<>(newConfig.confidenceThresholds); + } + this.config.safeDefaultAction = newConfig.safeDefaultAction; + } + initProvider(); + } + public void setDecisionProvider(DecisionProvider provider) { this.decisionProvider = provider; }