From 2f2a89b26ac1df92528db334a5bd9b87be3c32a7 Mon Sep 17 00:00:00 2001 From: Astatin3 <77305074+Astatin3@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:15:00 -0600 Subject: [PATCH] Basically getting this thing to work --- .gitignore | 3 + README.md | 85 +----- .../resources/main/addon-template.mixins.json | 4 +- .../addon/mixin/HeadlessMinecraftClient.java | 284 ++++++++++++++++++ .../addon/mixin/HeadlessWidgetAccessor.java | 15 + .../example/addon/mixin/HeadlessWindow.java | 43 +++ src/main/resources/addon-template.mixins.json | 4 +- 7 files changed, 356 insertions(+), 82 deletions(-) create mode 100644 src/main/java/com/example/addon/mixin/HeadlessMinecraftClient.java create mode 100644 src/main/java/com/example/addon/mixin/HeadlessWidgetAccessor.java create mode 100644 src/main/java/com/example/addon/mixin/HeadlessWindow.java diff --git a/.gitignore b/.gitignore index 524f096..a95f6f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +run/ +build/ + # Compiled class file *.class diff --git a/README.md b/README.md index c246d08..25b5a09 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,6 @@ -# Meteor Addon Template +# No More Render +A mod that will run the latest version of Fabric without any gui! -A template to allow easy usage of the Meteor Addon API. - -### How to use - -- Clone this project -- Use this template to create new modules/commands -- Build the executable using the gradle `build` task. -- Run the mod with Meteor. - -### Project structure - -```text -. -│── .github -│ ╰── workflows -│ │── dev_build.yml -│ ╰── pull_request.yml -│── gradle -│ ╰── wrapper -│ │── gradle-wrapper.jar -│ ╰── gradle-wrapper.properties -│── src -│ ╰── main -│ │── java -│ │ ╰── com -│ │ ╰── example -│ │ ╰── addon -│ │ │── commands -│ │ │ ╰── CommandExample -│ │ │── hud -│ │ │ ╰── HudExample -│ │ │── modules -│ │ │ ╰── ModuleExample -│ │ ╰── AddonTemplate -│ ╰── resources -│ │── assets -│ │ ╰── template -│ │ ╰── icon.png -│ │── addon-template.mixins.json -│ ╰── fabric.mod.json -│── .editorconfig -│── .gitignore -│── build.gradle -│── gradle.properties -│── gradlew -│── gradlew.bat -│── LICENSE -│── README.md -╰── settings.gradle -``` - -This is the default project structure. Each folder/file has a specific purpose. -Here is a brief explanation of the ones you might need to modify: - -- `.github/workflows`: Contains the GitHub Actions configuration files. -- `gradle`: Contains the Gradle wrapper files. - Edit the `gradle.properties` file to change the version of the Gradle wrapper. -- `src/main/java/com/example/addon`: Contains the main class of the addon. - Here you can register your custom commands, modules, and HUDs. - Edit the `getPackage` method to reflect the package of your addon. -- `src/main/resources`: Contains the resources of the addon. - - `assets`: Contains the assets of the addon. - You can add your own assets here, separated in subfolders. - - `template`: Contains the assets of the template. - You can replace the `icon.png` file with your own addon icon. - Also, rename this folder to reflect the name of your addon. - - `addon-template.mixins.json`: Contains the Mixin configuration for the addon. - You can add your own mixins in the `client` array. - - `fabric.mod.json`: Contains the metadata of the addon. - Edit the various fields to reflect the metadata of your addon. -- `build.gradle`: Contains the Gradle build script. - You can manage the dependencies of the addon here. - Remember to keep the `fabric-loom` version up-to-date. -- `gradle.properties`: Contains the properties of the Gradle build. - These will be used by the build script. -- `LICENSE`: Contains the license of the addon. - You can edit this file to change the license of your addon. -- `README.md`: Contains the documentation of the addon. - You can edit this file to reflect the documentation of your addon, and showcase its features. +### Todo: +- Remove meteorclient refrences +- Make the CLI better \ No newline at end of file diff --git a/build/resources/main/addon-template.mixins.json b/build/resources/main/addon-template.mixins.json index e1c5ddd..48e5186 100644 --- a/build/resources/main/addon-template.mixins.json +++ b/build/resources/main/addon-template.mixins.json @@ -3,7 +3,9 @@ "package": "com.example.addon.mixin", "compatibilityLevel": "JAVA_21", "client": [ - "MixinGameRenderer" + "HeadlessWindow", + "HeadlessMinecraftClient", + "HeadlessWidgetAccessor" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/java/com/example/addon/mixin/HeadlessMinecraftClient.java b/src/main/java/com/example/addon/mixin/HeadlessMinecraftClient.java new file mode 100644 index 0000000..3a2db63 --- /dev/null +++ b/src/main/java/com/example/addon/mixin/HeadlessMinecraftClient.java @@ -0,0 +1,284 @@ +package com.example.addon.mixin; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.RunArgs; +import net.minecraft.client.gl.WindowFramebuffer; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.world.WorldListWidget; +import net.minecraft.client.gui.widget.*; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +@Mixin(MinecraftClient.class) +public class HeadlessMinecraftClient { + +// @Inject(method = "", at = @At("RETURN")) +// private void onInit(CallbackInfo ci) { +// startCommandThread(); +// System.out.println("#########################################"); +// } + + @Shadow private Thread thread; + + @Inject(method = "", at = @At("TAIL")) + private void onGameLoaded(RunArgs args, CallbackInfo ci) { +// AddonTemplate.LOG.info("Hello from ExampleMixin!"); + System.out.println("#########################################"); + startCommandThread(); + } + + + private void startCommandThread() { + Thread commandThread = new Thread(() -> { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + String line; + try { + while ((line = reader.readLine()) != null) { + parseCommand(line); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + commandThread.setName("Headless Command Thread"); + commandThread.setDaemon(true); + commandThread.start(); + } + + MinecraftClient self = (MinecraftClient)(Object)this; + + + private void parseCommand(String command) { + String[] split = command.split(" "); + try { + switch (split[0].toLowerCase()) { + case "quit": + case "exit": + self.stop(); + break; + case "tick": + self.tick(); + break; + case "listelements": + case "elems": + listElements(); + break; + case "clickelement": + case "celem": + clickElement(Integer.parseInt(split[1])); + break; + case "writeelement": + case "welem": + setElemText(Integer.parseInt(split[1]), command.substring(split[0].length()+split[1].length()-2)); + break; + case "key": + if (split.length > 1) { + simulateKeyPress(split[1]); + } + break; + case "keydown": + if (split.length > 1) { + simulateKeyDown(split[1]); + } + break; + case "keyup": + if (split.length > 1) { + simulateKeyUp(split[1]); + } + break; + default: + System.out.println("Unknown command: " + command); + } + }catch(Exception e){ + e.printStackTrace(); + } + } + + private void listElements() { + Screen currentScreen = self.currentScreen; + if (currentScreen == null) { + System.out.println("No screen is currently open."); + return; + } + + AtomicInteger index = new AtomicInteger(0); + printElements(currentScreen.children(), 0, index); + } + + + private void printElements(List elements, int depth, AtomicInteger index) { + String indent = " ".repeat(depth); + for (Element element : elements) { + System.out.printf("%s%d: %s%n", indent, index.getAndIncrement(), describeElement(element)); + if (element instanceof ClickableWidget) { + ClickableWidget widget = (ClickableWidget) element; + System.out.printf("%s Message: %s%n", indent, widget.getMessage().getString()); + } + if (element instanceof net.minecraft.client.gui.ParentElement pe) { + printElements(pe.children(), depth + 1, index); + } else if (element instanceof WorldListWidget wlw) { + printElements(wlw.children(), depth+1, index); + }// else if (element instanceof WorldListWidget.WorldEntry elw) { +// printElements(elw.get, depth+1, index); +// } + } + } + + private String describeElement(Element element) { + return String.format("%s@%s", element.getClass().getSimpleName(), Integer.toHexString(element.hashCode())); + } + + private Element findElement(int targetIndex) { + Screen currentScreen = self.currentScreen; + if (currentScreen == null) { + System.out.println("No screen is currently open."); + return null; + } + + AtomicInteger index = new AtomicInteger(0); + return findElementByIndex(currentScreen.children(), targetIndex, index); + } + + private void clickElement(int targetIndex) { + Element targetElement = findElement(targetIndex); + + if (targetElement == null) { + System.out.println("Invalid element index."); + return; + } + + if (targetElement instanceof ClickableWidget widget) { + self.execute(() -> { + widget.onClick(widget.getX(), widget.getY()); + }); + System.out.println("Clicked element: " + describeElement(widget)); + if (widget.getMessage() != null) { + System.out.println("Message: " + widget.getMessage().getString()); + } + } else { + System.out.println("Element is not clickable: " + describeElement(targetElement)); + } + } + + private void setElemText(int targetIndex, String text) { + Element targetElement = findElement(targetIndex); + + if (targetElement == null) { + System.out.println("Invalid element index."); + return; + } + + + + if (targetElement instanceof TextFieldWidget widget) { + self.execute(() -> { + widget.write(text); + }); + System.out.println("Wrote in element: " + describeElement(widget)); + } else { + System.out.println("Element is not a TextFieldWidget: " + describeElement(targetElement)); + } + } + + private Element findElementByIndex(List elements, int targetIndex, AtomicInteger currentIndex) { + for (Element element : elements) { + if (currentIndex.getAndIncrement() == targetIndex) { + return element; + } + if (element instanceof net.minecraft.client.gui.ParentElement) { + Element found = findElementByIndex(((net.minecraft.client.gui.ParentElement) element).children(), targetIndex, currentIndex); + if (found != null) { + return found; + } + } + } + return null; + } + + + + private void simulateKeyPress(String keyName) { + int keyCode = getKeyCode(keyName); + if (keyCode != GLFW.GLFW_KEY_UNKNOWN) { + long handle = self.getWindow().getHandle(); + self.execute(() -> { + self.keyboard.onKey(handle, keyCode, 0, GLFW.GLFW_PRESS, 0); + self.keyboard.onKey(handle, keyCode, 0, GLFW.GLFW_RELEASE, 0); + }); + System.out.println("Pressed key: " + keyName); + } else { + System.out.println("Unknown key: " + keyName); + } + } + + private void simulateKeyDown(String keyName) { + int keyCode = getKeyCode(keyName); + if (keyCode != GLFW.GLFW_KEY_UNKNOWN) { + long handle = self.getWindow().getHandle(); + self.execute(() -> { + self.keyboard.onKey(handle, keyCode, 0, GLFW.GLFW_PRESS, 0); + }); + System.out.println("Key down: " + keyName); + } else { + System.out.println("Unknown key: " + keyName); + } + } + + private void simulateKeyUp(String keyName) { + int keyCode = getKeyCode(keyName); + if (keyCode != GLFW.GLFW_KEY_UNKNOWN) { + long handle = self.getWindow().getHandle(); + self.execute(() -> { + self.keyboard.onKey(handle, keyCode, 0, GLFW.GLFW_RELEASE, 0); + }); + System.out.println("Key up: " + keyName); + } else { + System.out.println("Unknown key: " + keyName); + } + } + + private static int getKeyCode(String keyName) { + // Handle special cases + switch (keyName.toLowerCase()) { + case "space": return GLFW.GLFW_KEY_SPACE; + case "enter": return GLFW.GLFW_KEY_ENTER; + case "tab": return GLFW.GLFW_KEY_TAB; + case "escape": return GLFW.GLFW_KEY_ESCAPE; + case "backspace": return GLFW.GLFW_KEY_BACKSPACE; +// case "rightclick": return GLFW.CLI; +// case "leftclick": return GLFW.GLFW_KEY_BACKSPACE; + // Add more special cases as needed + } + + // For single characters, use their ASCII value + if (keyName.length() == 1) { + char c = Character.toUpperCase(keyName.charAt(0)); + if (c >= 'A' && c <= 'Z') { + return GLFW.GLFW_KEY_A + (c - 'A'); + } + if (c >= '0' && c <= '9') { + return GLFW.GLFW_KEY_0 + (c - '0'); + } + } + + // If not found, return unknown key + return GLFW.GLFW_KEY_UNKNOWN; + } + +} diff --git a/src/main/java/com/example/addon/mixin/HeadlessWidgetAccessor.java b/src/main/java/com/example/addon/mixin/HeadlessWidgetAccessor.java new file mode 100644 index 0000000..7ae56c7 --- /dev/null +++ b/src/main/java/com/example/addon/mixin/HeadlessWidgetAccessor.java @@ -0,0 +1,15 @@ +package com.example.addon.mixin; + +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ClickableWidget; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.List; + +@Mixin(Screen.class) +public interface HeadlessWidgetAccessor { + @Accessor("children") List getChildren(); +} diff --git a/src/main/java/com/example/addon/mixin/HeadlessWindow.java b/src/main/java/com/example/addon/mixin/HeadlessWindow.java new file mode 100644 index 0000000..1391c27 --- /dev/null +++ b/src/main/java/com/example/addon/mixin/HeadlessWindow.java @@ -0,0 +1,43 @@ +package com.example.addon.mixin; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.util.Window; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.atomic.AtomicLong; + +@Mixin(Window.class) +public class HeadlessWindow { + @Final @Shadow private long handle; + + @Inject(method = "", at = @At("RETURN")) + private void onInit(CallbackInfo ci) { + // Do nothing, effectively preventing window creation + } + + @Inject(method = "updateWindowRegion", at = @At("HEAD"), cancellable = true) + private void onUpdateWindowRegion(CallbackInfo ci) { + ci.cancel(); // Prevent window updates + } + + @Inject(method = "swapBuffers", at = @At("HEAD"), cancellable = true) + private void onSwapBuffers(CallbackInfo ci) { + RenderSystem.replayQueue(); + ci.cancel(); // Prevent buffer swapping + } + + @Inject(method = "setTitle", at = @At("HEAD"), cancellable = true) + private void onSetTitle(String title, CallbackInfo ci) { + ci.cancel(); // Prevent title updates + } + + @Inject(method = "toggleFullscreen", at = @At("HEAD"), cancellable = true) + private void onToggleFullscreen(CallbackInfo ci) { + ci.cancel(); // Prevent fullscreen toggling + } +} diff --git a/src/main/resources/addon-template.mixins.json b/src/main/resources/addon-template.mixins.json index e1c5ddd..48e5186 100644 --- a/src/main/resources/addon-template.mixins.json +++ b/src/main/resources/addon-template.mixins.json @@ -3,7 +3,9 @@ "package": "com.example.addon.mixin", "compatibilityLevel": "JAVA_21", "client": [ - "MixinGameRenderer" + "HeadlessWindow", + "HeadlessMinecraftClient", + "HeadlessWidgetAccessor" ], "injectors": { "defaultRequire": 1