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\t

Sorry!

\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> entries = new ArrayList<>(); + for (Setting setting : settings) { + Map entry = new HashMap<>(); + entry.put("name", setting.getName()); + entry.put("displayName", setting.getName()); + if (setting instanceof NumberSetting numberSetting) { + entry.put("type", "slider"); + entry.put("max", numberSetting.getMax()); + entry.put("min", numberSetting.getMin()); + entry.put("step", numberSetting.getStep()); + entry.put("value", numberSetting.getValue()); + } else if (setting instanceof BooleanSetting) { + entry.put("type", "checkbox"); + entry.put("value", setting.getValue()); + } else if (setting instanceof ModeSetting modeSetting) { + entry.put("type", "selection"); + entry.put("value", modeSetting.getValue()); + entry.put("values", modeSetting.getModes()); + } + entries.add(entry); + } + response.put("result", entries); + success = true; + } + } catch (Throwable throwable) { + throwable.printStackTrace(); + success = false; + reason = throwable.toString(); + } + } else { + reason = "参数不足"; + } + response.put("success", success); + response.put("reason", reason); + 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/StaticFileHandler.java b/src/main/java/shit/zen/network/webui/StaticFileHandler.java new file mode 100644 index 0000000..6deb5bc --- /dev/null +++ b/src/main/java/shit/zen/network/webui/StaticFileHandler.java @@ -0,0 +1,69 @@ +package shit.zen.network.webui; + +import com.sun.net.httpserver.HttpExchange; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +public class StaticFileHandler extends AbstractHttpHandler { + private final String resourcePath; + private final String urlPrefix; + + public StaticFileHandler(String resourcePath, String urlPrefix) { + this.resourcePath = resourcePath; + this.urlPrefix = urlPrefix; + } + + @Override + public int handleRequest(InputStream in, OutputStream out, HttpExchange exchange) throws Throwable { + String requestPath = exchange.getRequestURI().getPath(); + if (!requestPath.startsWith(this.urlPrefix)) { + return 404; + } + String relative = requestPath.substring(this.urlPrefix.length()); + relative = URLDecoder.decode(relative, StandardCharsets.UTF_8); + if (relative.isEmpty() || relative.endsWith("/")) { + relative += "index.html"; + } + String classpath = this.resourcePath + "/" + relative; + InputStream resource = StaticFileHandler.class.getResourceAsStream(classpath); + if (resource == null) { + return 404; + } + try (resource) { + exchange.getResponseHeaders().add("Content-Type", guessContentType(relative)); + resource.transferTo(out); + } + return 200; + } + + @Override + public void sendResponse(int status, OutputStream out, HttpExchange exchange) throws IOException { + InputStream resource = StaticFileHandler.class.getResourceAsStream(this.resourcePath + "/" + status + ".html"); + if (resource != null) { + try (resource) { + exchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8"); + resource.transferTo(out); + } + } + } + + private static String guessContentType(String path) { + String lower = path.toLowerCase(Locale.ROOT); + if (lower.endsWith(".html") || lower.endsWith(".htm")) return "text/html; charset=utf-8"; + if (lower.endsWith(".css")) return "text/css; charset=utf-8"; + if (lower.endsWith(".js")) return "application/javascript; charset=utf-8"; + if (lower.endsWith(".json")) return "application/json; charset=utf-8"; + if (lower.endsWith(".svg")) return "image/svg+xml"; + if (lower.endsWith(".png")) return "image/png"; + if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; + if (lower.endsWith(".ico")) return "image/x-icon"; + if (lower.endsWith(".woff")) return "font/woff"; + if (lower.endsWith(".woff2")) return "font/woff2"; + if (lower.endsWith(".ttf")) return "font/ttf"; + return "application/octet-stream"; + } +} diff --git a/src/main/java/shit/zen/network/webui/ToggleModuleHandler.java b/src/main/java/shit/zen/network/webui/ToggleModuleHandler.java new file mode 100644 index 0000000..b66f0c3 --- /dev/null +++ b/src/main/java/shit/zen/network/webui/ToggleModuleHandler.java @@ -0,0 +1,65 @@ +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 shit.zen.ZenClient; +import shit.zen.exception.ModuleNotFoundException; +import shit.zen.modules.Module; +import shit.zen.modules.impl.world.WebUI; +import shit.zen.utils.render.TextureUtil; + +public class ToggleModuleHandler 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<>(); + String reason = null; + boolean state; + boolean success; + if (query.containsKey("module") && query.containsKey("state")) { + try { + Module module = lookupModule(query.get("module")); + if (module == null) { + state = false; + success = false; + reason = "找不到模块"; + } else if (module instanceof WebUI) { + state = true; + success = true; + } else { + module.setEnabled(Boolean.parseBoolean(query.get("state"))); + state = module.isEnabled(); + success = true; + } + } catch (Throwable throwable) { + throwable.printStackTrace(); + state = false; + success = false; + reason = throwable.toString(); + } + } else { + state = false; + success = false; + reason = "参数不足"; + } + response.put("success", success); + response.put("reason", reason); + response.put("result", state); + 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/resources/index.html b/src/main/resources/webui/index.html old mode 100755 new mode 100644 similarity index 100% rename from src/main/resources/index.html rename to src/main/resources/webui/index.html