diff --git a/README.md b/README.md
index 59a57e4..4f1538b 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,6 @@
## TODO
- 客户端的Shader渲染明显还有很多问题。
- - Web Click GUI尚未实现。
- 部分UI错位。
- 直接构建为热注入DLL。
- 你来提出Issue,我来解决。
diff --git a/src/main/java/shit/zen/modules/impl/world/WebUI.java b/src/main/java/shit/zen/modules/impl/world/WebUI.java
index 98eb029..c3ebcca 100644
--- a/src/main/java/shit/zen/modules/impl/world/WebUI.java
+++ b/src/main/java/shit/zen/modules/impl/world/WebUI.java
@@ -1,22 +1,74 @@
package shit.zen.modules.impl.world;
+import com.sun.net.httpserver.HttpServer;
+import java.awt.Desktop;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
import shit.zen.modules.Category;
import shit.zen.modules.Module;
+import shit.zen.network.webui.CategoriesHandler;
+import shit.zen.network.webui.ModulesHandler;
+import shit.zen.network.webui.SetSettingHandler;
+import shit.zen.network.webui.SettingsHandler;
+import shit.zen.network.webui.StaticFileHandler;
+import shit.zen.network.webui.ToggleModuleHandler;
+import shit.zen.settings.impl.BooleanSetting;
+import shit.zen.settings.impl.NumberSetting;
import shit.zen.utils.misc.ChatUtil;
public class WebUI extends Module {
+ private final NumberSetting port = new NumberSetting("Port", 8089, 1024, 65535, 1);
+ private final BooleanSetting openBrowser = new BooleanSetting("Open Browser", true);
+
+ private HttpServer httpServer;
+
public WebUI() {
super("WebUI", Category.WORLD);
- this.setEnabled(false);
}
@Override
public void onEnable() {
- ChatUtil.print("WebUI is unavailable in this build.");
- this.setEnabled(false);
+ try {
+ this.httpServer = this.createHttpServer();
+ ChatUtil.print("WebUI started at http://127.0.0.1:" + this.port.getValue().intValue());
+ if (this.openBrowser.getValue() && mc.player != null) {
+ try {
+ System.setProperty("java.awt.headless", "false");
+ if (Desktop.isDesktopSupported()
+ && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
+ Desktop.getDesktop().browse(new URI("http://127.0.0.1:" + this.port.getValue().intValue()));
+ }
+ } catch (URISyntaxException | IOException ex) {
+ ChatUtil.print("Failed to open browser: " + ex.getMessage());
+ }
+ }
+ } catch (IOException ioException) {
+ ChatUtil.print("Failed to start http server because " + ioException.getMessage());
+ ioException.printStackTrace();
+ this.setEnabled(false);
+ }
}
@Override
public void onDisable() {
+ if (this.httpServer != null) {
+ this.httpServer.stop(0);
+ this.httpServer = null;
+ ChatUtil.print("WebUI stopped");
+ }
+ }
+
+ private HttpServer createHttpServer() throws IOException {
+ HttpServer server = HttpServer.create(new InetSocketAddress(this.port.getValue().intValue()), 0);
+ server.createContext("/api/modulesList", new ModulesHandler());
+ server.createContext("/api/categoriesList", new CategoriesHandler());
+ server.createContext("/api/setStatus", new ToggleModuleHandler());
+ server.createContext("/api/setModuleSettingValue", new SetSettingHandler());
+ server.createContext("/api/getModuleSetting", new SettingsHandler());
+ server.createContext("/", new StaticFileHandler("/webui", "/"));
+ server.start();
+ return server;
}
}
diff --git a/src/main/java/shit/zen/network/webui/AbstractHttpHandler.java b/src/main/java/shit/zen/network/webui/AbstractHttpHandler.java
new file mode 100644
index 0000000..e7447a5
--- /dev/null
+++ b/src/main/java/shit/zen/network/webui/AbstractHttpHandler.java
@@ -0,0 +1,50 @@
+package shit.zen.network.webui;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+public abstract class AbstractHttpHandler implements HttpHandler {
+
+ @Override
+ public final void handle(HttpExchange exchange) throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ try {
+ int status = this.handleRequest(exchange.getRequestBody(), buffer, exchange);
+ if (buffer.size() == 0) {
+ this.sendResponse(status, buffer, exchange);
+ }
+ if (!(this instanceof StaticFileHandler)
+ && exchange.getResponseHeaders().getFirst("Content-Type") == null) {
+ exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
+ }
+ exchange.sendResponseHeaders(status, buffer.size());
+ if (buffer.size() != 0) {
+ exchange.getResponseBody().write(buffer.toByteArray());
+ }
+ } catch (Throwable throwable) {
+ int status = 500;
+ buffer = new ByteArrayOutputStream();
+ exchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8");
+ OutputStreamWriter writer = new OutputStreamWriter(buffer, StandardCharsets.UTF_8);
+ writer.append("\r\n\r\n
\r\n\tSorry!\r\n\t\r\n\r\n\r\n\tSorry!
\r\n\tThis request could not be completed due to an unexpected exception.
\r\n\tHere is stack trace:\r\n\t");
+ throwable.printStackTrace(new PrintWriter(writer));
+ writer.append("\r\n\r\n");
+ writer.flush();
+ exchange.sendResponseHeaders(status, buffer.size());
+ exchange.getResponseBody().write(buffer.toByteArray());
+ }
+ exchange.close();
+ }
+
+ public abstract int handleRequest(InputStream in, OutputStream out, HttpExchange exchange) throws Throwable;
+
+ public void sendResponse(int status, OutputStream out, HttpExchange exchange) throws IOException {
+ }
+}
diff --git a/src/main/java/shit/zen/network/webui/ActionEntry.java b/src/main/java/shit/zen/network/webui/ActionEntry.java
new file mode 100644
index 0000000..c2079a5
--- /dev/null
+++ b/src/main/java/shit/zen/network/webui/ActionEntry.java
@@ -0,0 +1,26 @@
+package shit.zen.network.webui;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+
+@Getter
+public class ActionEntry {
+ @SerializedName("a")
+ private final String name;
+ @SerializedName("b")
+ private final ActionType type;
+ @SerializedName("c")
+ private final double current;
+ @SerializedName("d")
+ private final double min;
+ @SerializedName("e")
+ private final double max;
+
+ public ActionEntry(String name, ActionType type, double current, double min, double max) {
+ this.name = name;
+ this.type = type;
+ this.current = current;
+ this.min = min;
+ this.max = max;
+ }
+}
diff --git a/src/main/java/shit/zen/network/webui/ActionType.java b/src/main/java/shit/zen/network/webui/ActionType.java
new file mode 100644
index 0000000..2984868
--- /dev/null
+++ b/src/main/java/shit/zen/network/webui/ActionType.java
@@ -0,0 +1,12 @@
+package shit.zen.network.webui;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum ActionType {
+ @SerializedName("a")
+ BUTTON,
+ @SerializedName("b")
+ SLIDER,
+ @SerializedName("c")
+ TOGGLE
+}
diff --git a/src/main/java/shit/zen/network/webui/CategoriesHandler.java b/src/main/java/shit/zen/network/webui/CategoriesHandler.java
new file mode 100644
index 0000000..91694e7
--- /dev/null
+++ b/src/main/java/shit/zen/network/webui/CategoriesHandler.java
@@ -0,0 +1,32 @@
+package shit.zen.network.webui;
+
+import com.google.gson.Gson;
+import com.sun.net.httpserver.HttpExchange;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import shit.zen.modules.Category;
+
+public class CategoriesHandler extends AbstractHttpHandler {
+
+ @Override
+ public int handleRequest(InputStream in, OutputStream out, HttpExchange exchange) throws Throwable {
+ exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
+ OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
+ Map response = new HashMap<>();
+ List categories = new ArrayList<>();
+ response.put("success", true);
+ response.put("result", categories);
+ for (Category category : Category.values()) {
+ categories.add(category.displayName);
+ }
+ writer.write(new Gson().toJson(response));
+ writer.flush();
+ return 200;
+ }
+}
diff --git a/src/main/java/shit/zen/network/webui/ModulesHandler.java b/src/main/java/shit/zen/network/webui/ModulesHandler.java
new file mode 100644
index 0000000..5c9a7e3
--- /dev/null
+++ b/src/main/java/shit/zen/network/webui/ModulesHandler.java
@@ -0,0 +1,52 @@
+package shit.zen.network.webui;
+
+import com.google.gson.Gson;
+import com.sun.net.httpserver.HttpExchange;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import shit.zen.ZenClient;
+import shit.zen.modules.Category;
+import shit.zen.modules.Module;
+import shit.zen.utils.render.TextureUtil;
+
+public class ModulesHandler extends AbstractHttpHandler {
+ private final Gson gson = new Gson();
+
+ @Override
+ public int handleRequest(InputStream in, OutputStream out, HttpExchange exchange) throws Throwable {
+ exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
+ OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
+ Map response = new HashMap<>();
+ response.put("success", true);
+
+ Category filter = null;
+ try {
+ Map query = TextureUtil.parseQueryString(exchange.getRequestURI().getQuery());
+ String requested = query.get("category");
+ if (requested != null && !"null".equalsIgnoreCase(requested)) {
+ filter = Category.fromString(requested);
+ }
+ } catch (Throwable ignored) {
+ }
+
+ Map> result = new HashMap<>();
+ for (Module module : ZenClient.getInstance().getModuleManager().getModules()) {
+ if (filter != null && module.getCategory() != filter) {
+ continue;
+ }
+ Map entry = new HashMap<>();
+ entry.put("desc", "");
+ entry.put("state", module.isEnabled());
+ entry.put("settings", !module.getSettings().isEmpty());
+ result.put(module.getName(), entry);
+ }
+ response.put("result", result);
+ writer.write(this.gson.toJson(response));
+ writer.flush();
+ return 200;
+ }
+}
diff --git a/src/main/java/shit/zen/network/webui/SetSettingHandler.java b/src/main/java/shit/zen/network/webui/SetSettingHandler.java
new file mode 100644
index 0000000..9d2c6ca
--- /dev/null
+++ b/src/main/java/shit/zen/network/webui/SetSettingHandler.java
@@ -0,0 +1,98 @@
+package shit.zen.network.webui;
+
+import com.google.gson.Gson;
+import com.sun.net.httpserver.HttpExchange;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+import shit.zen.ZenClient;
+import shit.zen.exception.ModuleNotFoundException;
+import shit.zen.modules.Module;
+import shit.zen.modules.impl.world.WebUI;
+import shit.zen.settings.Setting;
+import shit.zen.settings.impl.BooleanSetting;
+import shit.zen.settings.impl.ModeSetting;
+import shit.zen.settings.impl.NumberSetting;
+import shit.zen.utils.render.TextureUtil;
+
+public class SetSettingHandler extends AbstractHttpHandler {
+
+ @Override
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public int handleRequest(InputStream in, OutputStream out, HttpExchange exchange) throws Throwable {
+ Map query = TextureUtil.parseQueryString(exchange.getRequestURI().getQuery());
+ Map response = new HashMap<>();
+ boolean success = false;
+ String reason = null;
+ Object result = null;
+ if (query.containsKey("module") && query.containsKey("name") && query.containsKey("value")) {
+ try {
+ Module module = lookupModule(query.get("module"));
+ if (module == null) {
+ reason = "找不到模块";
+ } else if (module instanceof WebUI) {
+ reason = "sb";
+ } else {
+ String settingName = query.get("name");
+ Optional> match = module.getSettings().stream()
+ .filter(setting -> setting.getName().equals(settingName))
+ .findFirst();
+ if (match.isEmpty()) {
+ reason = "找不到参数";
+ } else {
+ Setting setting = match.get();
+ String raw = query.get("value");
+ if (setting instanceof NumberSetting) {
+ try {
+ setting.setValue(Double.valueOf(raw));
+ success = true;
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (setting instanceof BooleanSetting) {
+ setting.setValue(Boolean.valueOf(raw));
+ success = true;
+ } else if (setting instanceof ModeSetting modeSetting) {
+ String matchedMode = Stream.of(modeSetting.getModes())
+ .filter(mode -> mode.equals(raw))
+ .findFirst()
+ .orElse(null);
+ if (matchedMode != null) {
+ modeSetting.setValue(matchedMode);
+ success = true;
+ }
+ }
+ if (success) {
+ result = setting.getValue();
+ } else {
+ reason = "无效的值";
+ }
+ }
+ }
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ success = false;
+ reason = throwable.toString();
+ }
+ } else {
+ result = false;
+ reason = "参数不足";
+ }
+ response.put("success", success);
+ response.put("reason", reason);
+ response.put("result", result);
+ out.write(new Gson().toJson(response).getBytes(StandardCharsets.UTF_8));
+ return 200;
+ }
+
+ private static Module lookupModule(String name) {
+ try {
+ return ZenClient.getInstance().getModuleManager().getModule(name);
+ } catch (ModuleNotFoundException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/shit/zen/network/webui/SettingsHandler.java b/src/main/java/shit/zen/network/webui/SettingsHandler.java
new file mode 100644
index 0000000..84bb6d6
--- /dev/null
+++ b/src/main/java/shit/zen/network/webui/SettingsHandler.java
@@ -0,0 +1,81 @@
+package shit.zen.network.webui;
+
+import com.google.gson.Gson;
+import com.sun.net.httpserver.HttpExchange;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import shit.zen.ZenClient;
+import shit.zen.exception.ModuleNotFoundException;
+import shit.zen.modules.Module;
+import shit.zen.settings.Setting;
+import shit.zen.settings.impl.BooleanSetting;
+import shit.zen.settings.impl.ModeSetting;
+import shit.zen.settings.impl.NumberSetting;
+import shit.zen.utils.render.TextureUtil;
+
+public class SettingsHandler extends AbstractHttpHandler {
+
+ @Override
+ public int handleRequest(InputStream in, OutputStream out, HttpExchange exchange) throws Throwable {
+ Map query = TextureUtil.parseQueryString(exchange.getRequestURI().getQuery());
+ Map response = new HashMap<>();
+ boolean success = false;
+ String reason = null;
+ if (query.containsKey("module")) {
+ try {
+ Module module = lookupModule(query.get("module"));
+ if (module == null) {
+ reason = "找不到模块";
+ } else {
+ List> settings = module.getSettings();
+ List