From 67bca8c9e0d7ade6a3b2579d707dd5de3b68d923 Mon Sep 17 00:00:00 2001 From: TopchetoEU <36534413+TopchetoEU@users.noreply.github.com> Date: Sat, 10 Feb 2024 12:12:02 +0200 Subject: [PATCH] initial commit --- .github/workflows/build.yml | 40 ++ .gitignore | 21 + README.md | 0 build.gradle | 46 ++ gradle.properties | 19 + gradle/wrapper/gradle-wrapper.properties | 7 + settings.gradle | 12 + src/assets/fabric.mod.json | 24 + src/assets/mcscript.mixins.json | 15 + src/assets/mcscript/icon.png | Bin 0 -> 449 bytes .../ClientCommandMessageReceiver.java | 24 + .../mcscript/ClientMessageReceiver.java | 21 + .../mcscript/EnvironmentFactory.java | 12 + src/java/me/topchetoeu/mcscript/McScript.java | 141 ++++++ .../topchetoeu/mcscript/MessageReceiver.java | 6 + .../ServerCommandMessageReceiver.java | 21 + .../mcscript/events/ChatMessageCallback.java | 17 + .../topchetoeu/mcscript/gui/ButtonWidget.java | 40 ++ .../mcscript/gui/DevEditorWidget.java | 82 ++++ .../me/topchetoeu/mcscript/gui/DevScreen.java | 165 +++++++ .../mcscript/gui/DevTabListWidget.java | 121 +++++ .../mcscript/gui/DevTextBoxWidget.java | 95 ++++ .../mcscript/gui/FileDialogScreen.java | 94 ++++ .../mcscript/gui/FileListWidget.java | 146 ++++++ .../topchetoeu/mcscript/gui/FileTracker.java | 65 +++ .../mcscript/gui/OpenFileScreen.java | 33 ++ .../mcscript/gui/SaveFileScreen.java | 33 ++ .../mcscript/gui/TextBoxWidget.java | 425 ++++++++++++++++++ .../me/topchetoeu/mcscript/gui/Theme.java | 53 +++ .../me/topchetoeu/mcscript/gui/Widget.java | 101 +++++ .../topchetoeu/mcscript/loader/ModLoader.java | 5 + .../mcscript/mixin/ChatScreenMixin.java | 37 ++ .../mcscript/mixin/KeyboardMixin.java | 22 + .../mcscript/mixin/MinecraftClientMixin.java | 12 + 34 files changed, 1955 insertions(+) create mode 100755 .github/workflows/build.yml create mode 100755 .gitignore create mode 100755 README.md create mode 100755 build.gradle create mode 100755 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 settings.gradle create mode 100755 src/assets/fabric.mod.json create mode 100755 src/assets/mcscript.mixins.json create mode 100755 src/assets/mcscript/icon.png create mode 100755 src/java/me/topchetoeu/mcscript/ClientCommandMessageReceiver.java create mode 100755 src/java/me/topchetoeu/mcscript/ClientMessageReceiver.java create mode 100644 src/java/me/topchetoeu/mcscript/EnvironmentFactory.java create mode 100755 src/java/me/topchetoeu/mcscript/McScript.java create mode 100755 src/java/me/topchetoeu/mcscript/MessageReceiver.java create mode 100755 src/java/me/topchetoeu/mcscript/ServerCommandMessageReceiver.java create mode 100755 src/java/me/topchetoeu/mcscript/events/ChatMessageCallback.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/ButtonWidget.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/DevEditorWidget.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/DevScreen.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/DevTabListWidget.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/DevTextBoxWidget.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/FileDialogScreen.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/FileListWidget.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/FileTracker.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/OpenFileScreen.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/SaveFileScreen.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/TextBoxWidget.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/Theme.java create mode 100755 src/java/me/topchetoeu/mcscript/gui/Widget.java create mode 100644 src/java/me/topchetoeu/mcscript/loader/ModLoader.java create mode 100755 src/java/me/topchetoeu/mcscript/mixin/ChatScreenMixin.java create mode 100755 src/java/me/topchetoeu/mcscript/mixin/KeyboardMixin.java create mode 100755 src/java/me/topchetoeu/mcscript/mixin/MinecraftClientMixin.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100755 index 0000000..e2fa68a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,40 @@ +# Automatically build the project and run any configured tests for every push +# and submitted pull request. This can help catch issues that only occur on +# certain platforms or Java versions, and provides a first line of defence +# against bad commits. + +name: build +on: [pull_request, push] + +jobs: + build: + strategy: + matrix: + # Use these Java versions + java: [ + 17, # Current Java LTS & minimum supported by Minecraft + ] + # and run on both Linux and Windows + os: [ubuntu-22.04, windows-2022] + runs-on: ${{ matrix.os }} + steps: + - name: checkout repository + uses: actions/checkout@v3 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v1 + - name: setup jdk ${{ matrix.java }} + uses: actions/setup-java@v3 + with: + java-version: ${{ matrix.java }} + distribution: 'microsoft' + - name: make gradle wrapper executable + if: ${{ runner.os != 'Windows' }} + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: capture build artifacts + if: ${{ runner.os == 'Linux' && matrix.java == '17' }} # Only upload artifacts built from latest java on one OS + uses: actions/upload-artifact@v3 + with: + name: Artifacts + path: build/libs/ diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..f14d2c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +* + +!/src +!/src/**/* + +!/doc +!/doc/**/* + +!/.github +!/.github/**/* + +!/.gitignore +!/.gitattributes +!/LICENSE +!/README.md +!/settings.gradle +!/build.gradle +!/gradle.properties +!/gradle +!/gradle/wrapper +!/gradle/wrapper/gradle-wrapper.properties \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..e69de29 diff --git a/build.gradle b/build.gradle new file mode 100755 index 0000000..3397689 --- /dev/null +++ b/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'fabric-loom' version '1.0-SNAPSHOT' + id 'maven-publish' +} + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } + maven { url 'https://maven.terraformersmc.com/releases' } +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + + implementation 'com.github.topchetoeu:jscript:v0.8.7-beta' + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" + // modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_version}" +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + toolchain.languageVersion = JavaLanguageVersion.of(17) + withSourcesJar() +} + +sourceSets { + main.java.srcDirs = [ "src/java" ] + main.resources.srcDirs = [ "src/assets" ] +} + +processResources { + filesMatching("fabric.mod.json") { + expand ( + version: project.project_version, + name: project.project_name + ) + } +} + +base.archivesName = project.project_name +version = project.project_version +group = project.project_group diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 0000000..dba254b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx4G +org.gradle.parallel=true + +# Dependencies + +minecraft_version=1.19.4 +yarn_mappings=1.19.4+build.2 +loader_version=0.15.3 +fabric_version=0.87.2+1.19.4 +modmenu_version=6.3.1 + +# Project properties + +project_version=0.0.1-alpha +project_name = mcscript +project_group me.topchetoeu + +# Dependencies diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1af9e09 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle new file mode 100755 index 0000000..dd2c4c8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = properties.project_name \ No newline at end of file diff --git a/src/assets/fabric.mod.json b/src/assets/fabric.mod.json new file mode 100755 index 0000000..14b7a86 --- /dev/null +++ b/src/assets/fabric.mod.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "id": "${name}", + "version": "${version}", + "name": "MC-Jscript", + "description": "A simple mod loader for Javascript mods.", + "authors": [ "TopcehtoEU" ], + "license": "MIT", + "icon": "${name}/icon.png", + "environment": "*", + "entrypoints": { + "client": [ "me.topchetoeu.mcscript.McScript" ], + "main": [ "me.topchetoeu.mcscript.McScript" ] + }, + "mixins": [ "mcscript.mixins.json" ], + "depends": { + "fabricloader": ">=0.14.11", + "fabric-api": "*", + "minecraft": "~1.19.3", + "modmenu": "~1.19.3", + "java": ">=17" + }, + "suggests": { } +} \ No newline at end of file diff --git a/src/assets/mcscript.mixins.json b/src/assets/mcscript.mixins.json new file mode 100755 index 0000000..1e3f22d --- /dev/null +++ b/src/assets/mcscript.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "me.topchetoeu.mcscript.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [], + "client": [ + "ChatScreenMixin", + "MinecraftClientMixin", + "KeyboardMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/src/assets/mcscript/icon.png b/src/assets/mcscript/icon.png new file mode 100755 index 0000000000000000000000000000000000000000..e687f85ac7ce7f1c1a9476ce2285c56b3009bb0f GIT binary patch literal 449 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRdwrjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4Pl-xjv*CsZ*O1BJ!Bx!dU3_(EwW0?sdCFs z8u*rHPQGR1(!{p9uYSAjbJhw6r|**wC>Q~aLVyqAzMqZ!AJ34#c*mr^W}@N=QwJsI z;@=FNJiK?gCY-o%%sS4b65aO4lcSc0M;PkAL|pGVhCTd_S1~aO1r%UuW3$ zfH*KhL)w|*4&vO?o z*Xn dependancies(); + + String name(); + void apply(Context ctx, Map dependancies); +} diff --git a/src/java/me/topchetoeu/mcscript/McScript.java b/src/java/me/topchetoeu/mcscript/McScript.java new file mode 100755 index 0000000..995e584 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/McScript.java @@ -0,0 +1,141 @@ +package me.topchetoeu.mcscript; + +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import me.topchetoeu.jscript.common.Filename; +import me.topchetoeu.jscript.core.engine.Context; +import me.topchetoeu.jscript.core.engine.Engine; +import me.topchetoeu.jscript.core.engine.Environment; +import me.topchetoeu.jscript.core.engine.values.Values; +import me.topchetoeu.jscript.lib.Internals; +import me.topchetoeu.jscript.utils.filesystem.File; +import me.topchetoeu.jscript.utils.filesystem.Filesystem; +import me.topchetoeu.jscript.utils.filesystem.LineReader; +import me.topchetoeu.jscript.utils.filesystem.LineWriter; +import me.topchetoeu.jscript.utils.filesystem.RootFilesystem; +import me.topchetoeu.jscript.utils.filesystem.STDFilesystem; +import me.topchetoeu.jscript.utils.permissions.PermissionsManager; +import me.topchetoeu.jscript.utils.permissions.PermissionsProvider; +import me.topchetoeu.mcscript.events.ChatMessageCallback; +import me.topchetoeu.mcscript.gui.DevScreen; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +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.event.lifecycle.v1.ServerLifecycleEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.server.command.CommandManager; +import net.minecraft.text.Text; + +public class McScript implements ModInitializer, ClientModInitializer { + private boolean openDev = false; + public static final Logger LOGGER = LoggerFactory.getLogger("jscript"); + + private void execute(int i, Environment env, Engine engine, String raw, MessageReceiver msg) { + try { + var res = engine.pushMsg(false, env, new Filename("repl", i + ".js"), raw, null).await(); + msg.sendInfo(Values.toReadable(new Context(engine, env), res)); + } + catch (RuntimeException e) { + msg.sendError(Values.errorToReadable(e, "")); + } + } + + private Environment createEnv(String location, LineReader in, LineWriter out) { + var env = new Environment(); + var inFile = File.ofLineReader(in); + var outFile = File.ofLineWriter(out); + Internals.apply(env); + + var fs = new RootFilesystem(PermissionsProvider.get(env)); + fs.protocols.put("std", new STDFilesystem().add("in", inFile).add("out", outFile)); + + var perms = new PermissionsManager(); + + env.add(PermissionsProvider.ENV_KEY, perms); + env.add(Filesystem.ENV_KEY, fs); + + env.global.define(null, "env", true, location); + return env; + } + + @Override public void onInitialize() { + var engine = new Engine(); + + ServerLifecycleEvents.SERVER_STARTING.register(server -> { + engine.start(); + + var env = createEnv("SERVER", () -> null, value -> { + for (var line : value.split("\n", -1)) LOGGER.info(line); + }); + + var i = new int[1]; + + server.getCommandManager().getDispatcher().register(CommandManager.literal("msc") + .then(CommandManager.argument("code", greedyString()).executes(c -> { + String str = getString(c, "code"); + var receiver = new ServerCommandMessageReceiver(c.getSource()); + execute(i[0]++, env, engine, str, receiver); + return 1; + })) + ); + }); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> { + engine.stop(); + }); + } + + @net.fabricmc.api.Environment(EnvType.CLIENT) + @Override public void onInitializeClient() { + var dev = new DevScreen(); + var engine = new Engine(); + + ClientLifecycleEvents.CLIENT_STARTED.register((client) -> { + try { + Files.createDirectories(Path.of("scripts")); + } + catch (IOException e) { /* so be it */ } + + var receiver = new ClientMessageReceiver(); + var env = createEnv("CLIENT", () -> null, value -> MinecraftClient.getInstance().player.sendMessage(Text.of(value))); + engine.start(); + + var i = new int[1]; + ChatMessageCallback.EVENT.register(args -> { + if (args.message.startsWith("#")) { + execute(i[0]++, env, engine, args.message.substring(1), receiver); + args.cancelled = true; + } + }); + ClientTickEvents.END_CLIENT_TICK.register((_1) -> { + if (openDev) { + client.setScreenAndRender(dev); + openDev = false; + } + }); + ClientCommandRegistrationCallback.EVENT.register((disp, _1) -> { + disp.register(ClientCommandManager.literal("dev") + .executes(c -> { + openDev = true; + return 1; + }) + ); + }); + }); + + ClientLifecycleEvents.CLIENT_STOPPING.register((client) -> { + engine.stop(); + }); + } +} diff --git a/src/java/me/topchetoeu/mcscript/MessageReceiver.java b/src/java/me/topchetoeu/mcscript/MessageReceiver.java new file mode 100755 index 0000000..55a424f --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/MessageReceiver.java @@ -0,0 +1,6 @@ +package me.topchetoeu.mcscript; + +public interface MessageReceiver { + public void sendError(String msg); + public void sendInfo(String msg); +} \ No newline at end of file diff --git a/src/java/me/topchetoeu/mcscript/ServerCommandMessageReceiver.java b/src/java/me/topchetoeu/mcscript/ServerCommandMessageReceiver.java new file mode 100755 index 0000000..0cc3215 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/ServerCommandMessageReceiver.java @@ -0,0 +1,21 @@ +package me.topchetoeu.mcscript; + +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +public class ServerCommandMessageReceiver implements MessageReceiver { + public final ServerCommandSource receiver; + + @Override + public void sendInfo(String msg) { + receiver.sendFeedback(Text.of(msg), false); + } + @Override + public void sendError(String msg) { + receiver.sendError(Text.of(msg)); + } + + public ServerCommandMessageReceiver(ServerCommandSource receiver) { + this.receiver = receiver; + } +} \ No newline at end of file diff --git a/src/java/me/topchetoeu/mcscript/events/ChatMessageCallback.java b/src/java/me/topchetoeu/mcscript/events/ChatMessageCallback.java new file mode 100755 index 0000000..6d2a9d0 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/events/ChatMessageCallback.java @@ -0,0 +1,17 @@ +package me.topchetoeu.mcscript.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +public interface ChatMessageCallback { + public static class ChatArgs { + public String message; + public boolean cancelled; + } + + void execute(ChatArgs args); + + public static final Event EVENT = EventFactory.createArrayBacked(ChatMessageCallback.class, arr -> args -> { + for (var el : arr) el.execute(args); + }); +} diff --git a/src/java/me/topchetoeu/mcscript/gui/ButtonWidget.java b/src/java/me/topchetoeu/mcscript/gui/ButtonWidget.java new file mode 100755 index 0000000..aa958af --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/ButtonWidget.java @@ -0,0 +1,40 @@ +package me.topchetoeu.mcscript.gui; + +import net.minecraft.client.util.math.MatrixStack; + +public class ButtonWidget extends Widget { + public interface ClickHandler { + void click(); + } + + public final ClickHandler handler; + public final String text; + + @Override + public void render(MatrixStack mat, int mx, int my, float delta) { + if (isMouseOver(mx, my)) fill(mat, x, y, x + w, y + h, theme.get("bg-2")); + else fill(mat, x, y, x + w, y + h, theme.get("bg-1")); + + theme.font.draw(mat, text, x + (w - theme.font.getWidth(text)) / 2, y + (h - theme.fontSize()) / 2, theme.get("text")); + + super.render(mat, mx, my, delta); + } + + @Override + public boolean mouseClicked(double mx, double my, int button) { + if (isMouseOver(mx, my) && button == 0) { + handler.click(); + return true; + } + return false; + } + + @Override + protected boolean hasBorders() { return true; } + + public ButtonWidget(Theme theme, String text, ClickHandler handler) { + super(theme); + this.handler = handler; + this.text = text; + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/DevEditorWidget.java b/src/java/me/topchetoeu/mcscript/gui/DevEditorWidget.java new file mode 100755 index 0000000..162016b --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/DevEditorWidget.java @@ -0,0 +1,82 @@ +package me.topchetoeu.mcscript.gui; + +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.util.math.MatrixStack; + +public class DevEditorWidget extends Widget { + private DevTextBoxWidget tab; + + private void updateTab() { + if (tab == null) return; + tab.setMultiline(true).setRect(x, y + theme.fontSize() + theme.get("padding-s"), w, h - theme.fontSize() - theme.get("padding-s")); + tab.theme.setTheme(theme); + } + + @Override + protected boolean hasBorders() { return false; } + @Override + public Widget setRect(int x, int y, int w, int h) { + super.setRect(x, y, w, h); + updateTab(); + return this; + } + + public DevTextBoxWidget get() { + return tab; + } + public void set(DevTextBoxWidget tab) { + if (this.tab == tab) return; + this.tab = tab; + updateTab(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (tab == null) return false; + return tab.keyPressed(keyCode, scanCode, modifiers); + } + @Override + public boolean charTyped(char chr, int modifiers) { + if (tab == null) return false; + return tab.charTyped(chr, modifiers); + } + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (tab == null) return false; + return tab.mouseClicked(mouseX, mouseY, button); + } + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + if (tab == null) return false; + return tab.mouseScrolled(mouseX, mouseY, amount); + } + + @Override + public void appendNarrations(NarrationMessageBuilder nmb) { + } + @Override + public SelectionType getType() { + return SelectionType.NONE; + } + + @Override + public void render(MatrixStack mat, int mx, int my, float delta) { + if (tab == null) { + String text = "No file opened :("; + int cx = w - theme.font.getWidth(text); + int cy = h - theme.fontSize(); + + fill(mat, x, y, x + w, y + h, theme.get("bg-1")); + theme.font.draw(mat, text, x + cx / 2, x + cy / 2, theme.get("text")); + } + else { + tab.render(mat, mx, my, delta); + fill(mat, x, y, x + w, y + theme.fontSize() + 2, theme.get("bg-2")); + theme.font.draw(mat, tab.getFile().getAbsoluteFile().toString(), x + 5, y + 2, theme.get("text")); + } + } + + public DevEditorWidget(Theme theme) { + super(theme); + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/DevScreen.java b/src/java/me/topchetoeu/mcscript/gui/DevScreen.java new file mode 100755 index 0000000..e4b5b19 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/DevScreen.java @@ -0,0 +1,165 @@ +package me.topchetoeu.mcscript.gui; + +import java.io.File; +import java.io.IOException; + +import org.lwjgl.glfw.GLFW; + +import me.topchetoeu.mcscript.gui.OpenFileScreen.OpenHandler; +import me.topchetoeu.mcscript.gui.SaveFileScreen.SaveHandler; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +public class DevScreen extends Screen implements OpenHandler, SaveHandler { + private enum Focus { + EDITOR, + MENU, + } + + private Theme theme; + private DevEditorWidget editor; + private DevTabListWidget tabs; + + private Focus focus = Focus.EDITOR; + private int untitledI = 1; + + // private int altI = 0; + + // private void drawFileMenu(MatrixStack matrices) { + // int h = theme.fontSize() + 2; + + // int x = 1; + // int y = height - 15; + // int i = 0; + + // fill(matrices, 0, this.height - h, this.width, this.height, Argb.getArgb(255, 40, 40, 40)); + + // for (var el : ListExt.of("Save", "Open", "Close", "Quit")) { + // int w = font.getWidth(el) + 10; + + // if (focus == Focus.MENU && altI == i++) { + // fill(matrices, x, y, x + w, y + h, Argb.getArgb(255, 80, 80, 80)); + // } + // font.draw(matrices, el, x + 5, y + 1, Argb.getArgb(255, 255, 255, 255)); + // x += w; + // } + // } + + public void fileOpened(File file) { + try { + tabs.add(new DevTextBoxWidget(file, theme)); + } + catch (IOException e) { + e.printStackTrace(); + } + } + public void fileSaved(File file) { + var tab = tabs.getTab(); + if (tab != null) { + try { + tab.saveFileAs(file); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } + + private int getUntitled() { + while (true) { + int i = untitledI++; + if (!new File("untitled" + i + ".ms").exists()) return i; + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + try { + boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; + boolean alt = (modifiers & GLFW.GLFW_MOD_ALT) != 0; + boolean shift = (modifiers & GLFW.GLFW_MOD_SHIFT) != 0; + if (ctrl) { + if (keyCode == GLFW.GLFW_KEY_N) { + tabs.add(new DevTextBoxWidget(new File("untitled" + getUntitled() + ".ms"), theme)); + return true; + } + if (keyCode == GLFW.GLFW_KEY_W) { + tabs.close(); + return true; + } + if (keyCode == GLFW.GLFW_KEY_S) { + if (!alt) tabs.getTab().saveFile(); + else client.setScreen(new SaveFileScreen(this, theme, new File(""), Text.of("Save file:"), this)); + return true; + } + if (keyCode == GLFW.GLFW_KEY_O) { + client.setScreen(new OpenFileScreen(this, theme, new File(""), Text.of("Open file:"), this)); + return true; + } + if (keyCode == GLFW.GLFW_KEY_TAB) { + if (shift) tabs.prev(); + else tabs.next(); + return true; + } + } + else { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + client.setScreen(null); + return true; + } + if (keyCode == GLFW.GLFW_KEY_LEFT_ALT) { + if (focus == Focus.MENU) focus = Focus.EDITOR; + else focus = Focus.MENU; + } + } + } + catch (Throwable e) { + e.printStackTrace(); + } + + return this.getFocused() != null && this.getFocused().keyPressed(keyCode, scanCode, modifiers); + } + + @SuppressWarnings("all") + protected void init() { + if (theme == null) { + theme = Theme.dark(MinecraftClient.getInstance().textRenderer); + editor = new DevEditorWidget(theme); + tabs = new DevTabListWidget(theme, tab -> editor.set(tab)); + } + + addDrawableChild(editor); + addDrawableChild(tabs); + setFocused(editor); + + editor.setRect(0, 0, width - 100, height); + tabs.setRect(width - 100, 0, 100, height); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + var res = super.mouseClicked(mouseX, mouseY, button); + return res; + } + + // @Override + // public boolean changeFocus(boolean lookForwards) { + // return true; + // } + + public DevScreen() { + super(Text.of("McScript IDE")); + + // try { + // this.tabs.add(new DevTab(new File("test1.ms"), font)); + // this.tabs.add(new DevTab(new File("test1.ms"), font)); + // this.tabs.add(new DevTab(new File("test3.ms"), font)); + // } + // catch (IOException e) { + // e.printStackTrace(); + // } + // this.tabs.add(new DevTab(0, "", "test2.ms")); + // this.tabs.add(new DevTab(0, "", "test3.ms")); + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/DevTabListWidget.java b/src/java/me/topchetoeu/mcscript/gui/DevTabListWidget.java new file mode 100755 index 0000000..d6b2d48 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/DevTabListWidget.java @@ -0,0 +1,121 @@ +package me.topchetoeu.mcscript.gui; + +import java.util.ArrayList; + +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.util.math.MatrixStack; + +public class DevTabListWidget extends Widget { + public static interface TabChangeHandle { + void tabChanged(DevTextBoxWidget tab); + } + + @Override + protected boolean hasBorders() { return false; } + + public final TabChangeHandle changeHandle; + + private int tabI = -1; + private ArrayList tabs = new ArrayList<>(); + + + public DevTextBoxWidget getTab() { + if (tabs.size() == 0) return null; + return tabs.get(tabI); + } + public void setTab(DevTextBoxWidget tab) { + if (tabs.contains(tab)) tabs.add(tab); + tabI = tabs.indexOf(tab); + changeHandle.tabChanged(getTab()); + } + + public void next() { + if (tabs.size() < 2) return; + tabI++; + if (tabI >= tabs.size()) tabI = 0; + changeHandle.tabChanged(getTab()); + } + public void prev() { + if (tabs.size() < 2) return; + tabI--; + if (tabI < 0) tabI = tabs.size() - 1; + changeHandle.tabChanged(getTab()); + } + public void add(DevTextBoxWidget tab) { + tabs.add(++tabI, tab); + changeHandle.tabChanged(getTab()); + } + public void close() { + if (tabs.size() == 0) return; + tabs.remove(tabI); + if (tabI == tabs.size()) { + tabI--; + changeHandle.tabChanged(getTab()); + } + } + + @Override + public boolean mouseClicked(double mx, double my, int button) { + if (!isMouseOver(mx, my)) return false; + mx -= x; my -= y; + + int i = (int)((my - theme.get("padding-l")) / (theme.fontSize() + 4)); + if (i >= 0 && i < tabs.size()) { + if (button == 2) { + tabs.remove(tabI); + if (tabI == tabs.size()) { + tabI--; + changeHandle.tabChanged(getTab()); + } + } + else { + tabI = i; + changeHandle.tabChanged(getTab()); + } + } + + return true; + } + @Override + public void render(MatrixStack mat, int mx, int my, float delta) { + int h = theme.fontSize() + 4; + + mat.push(); + mat.translate(x, y, 0); + + fill(mat, 0, 0, w, theme.get("padding-l"), theme.get("bg-3")); + + mat.push(); + mat.translate(0, theme.get("padding-l"), 0); + + for (int i = 0; i < tabs.size(); i++) { + int col = theme.get("bg-3"); + if (tabI == i) col = theme.get("bg-4"); + fill(mat, 0, i * h, w, (i + 1) * h, col); + + var name = tabs.get(i).getFile().getName(); + if (!tabs.get(i).fileIsSaved()) name = "*" + name; + theme.font.draw(mat, name, 5, i * h + 2, theme.get("text")); + } + + mat.pop(); + + fill(mat, 0, tabs.size() * h + theme.get("padding-l"), w, this.h, theme.get("bg-3")); + + mat.pop(); + } + + @Override + public void appendNarrations(NarrationMessageBuilder var1) { + + } + @Override + public SelectionType getType() { + return SelectionType.NONE; + } + + public DevTabListWidget(Theme theme, TabChangeHandle changeHandle) { + super(theme); + this.changeHandle = changeHandle; + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/DevTextBoxWidget.java b/src/java/me/topchetoeu/mcscript/gui/DevTextBoxWidget.java new file mode 100755 index 0000000..696d43d --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/DevTextBoxWidget.java @@ -0,0 +1,95 @@ +package me.topchetoeu.mcscript.gui; + +import java.nio.file.Files; + +import net.minecraft.client.util.math.MatrixStack; + +import java.io.File; +import java.io.IOException; + +public class DevTextBoxWidget extends TextBoxWidget { + private File file; + private FileTracker tracker; + + private boolean saved; + private String conflict = null; + + public File getFile() { + return file; + } + + public boolean fileExists() { + return tracker.exists(); + } + public boolean fileIsSaved() { + return saved; + } + public boolean fileHasConflict() { + return conflict != null; + } + public String getFileConflictReason() { + return conflict; + } + public void saveFile() throws IOException { + Files.writeString(file.toPath(), getValue()); + tracker.wasChanged(); + saved = true; + conflict = null; + } + public void saveFileAs(File file) throws IOException { + file.getParentFile().mkdirs(); + Files.writeString(file.toPath(), getValue()); + tracker.wasChanged(); + this.file = file; + this.tracker = new FileTracker(file); + saved = true; + conflict = null; + } + + + @Override + protected boolean hasBorders() { return false; } + @Override + protected void textChanged() { + saved = false; + } + + public void update() { + if (tracker.wasCreated()) { + conflict = "File was created"; + saved = false; + } + + try { + if (tracker.wasChanged()) { + if (saved) this.setValue(Files.readString(file.toPath())); + else conflict = "File was changed"; + } + } + catch (IOException e) { + e.printStackTrace(); + } + + if (tracker.wasDeleted()) { + saved = false; + conflict = null; + } + } + @Override + public void render(MatrixStack mat, int x, int y, float delta) { + update(); + super.render(mat, x, y, delta); + } + + public DevTextBoxWidget(File file, Theme theme) throws IOException { + super(theme); + this.file = file; + this.tracker = new FileTracker(file); + if (tracker.exists()) { + saved = true; + this.setMultiline(true); + this.setValue(Files.readString(file.toPath())); + } + else saved = false; + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/FileDialogScreen.java b/src/java/me/topchetoeu/mcscript/gui/FileDialogScreen.java new file mode 100755 index 0000000..53a0d5f --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/FileDialogScreen.java @@ -0,0 +1,94 @@ +package me.topchetoeu.mcscript.gui; + +import java.io.File; + +import org.lwjgl.glfw.GLFW; + +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; + +public abstract class FileDialogScreen extends Screen { + public final Screen parent; + public final Theme theme; + protected final FileListWidget list; + protected final ButtonWidget ok; + protected final ButtonWidget cancel; + protected final TextBoxWidget name; + + protected abstract String okButtonText(); + protected abstract void okPressed(File file); + protected abstract void selected(File file); + + private void submit() { + okPressed(new File(list.cd(), name.getValue()).toPath().toAbsolutePath().toFile()); + } + + private int headerHeight() { + return theme.get("padding-m") * 2 + theme.fontSize(); + } + private int footerHeight() { + return (theme.get("padding-m") + theme.get("padding-s")) * 2 + theme.fontSize(); + } + + @Override + protected void init() { + int padl = theme.get("padding-l"); + int padm = theme.get("padding-m"); + int pads = theme.get("padding-s"); + + int btnw = 50; + int btnh = pads * 2 + theme.fontSize(); + int btny = height - padm - btnh; + int inputw = width - 100 - padl * 4; + + list.setRect(padl, headerHeight(), width - padl * 2, height - headerHeight() - footerHeight()); + ok.setRect(padl, btny, btnw, btnh); + cancel.setRect(padl + 50 + padl, btny, btnw, btnh); + name.setPos(padl + 100 + padl * 2, btny).setWidth(inputw); + addDrawableChild(list); + addDrawableChild(ok); + addDrawableChild(cancel); + addDrawableChild(name); + } + + @Override + public void render(MatrixStack mat, int mx, int my, float delta) { + fill(mat, 0, 0, width, height, theme.get("bg-1")); + list.render(mat, mx, my, delta); + fill(mat, list.x, 0, list.w + list.x, list.y - 1, theme.get("bg-1")); + fill(mat, list.x, list.y + list.h + 1, list.w + list.x, height, theme.get("bg-1")); + theme.font.draw(mat, title, theme.get("padding-l"), theme.get("padding-m"), theme.get("text")); + ok.render(mat, mx, my, delta); + cancel.render(mat, mx, my, delta); + name.render(mat, mx, my, delta); + } + + @Override + public void close() { + client.setScreen(parent); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + close(); + return true; + } + if (keyCode == GLFW.GLFW_KEY_ENTER) { + submit(); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + protected FileDialogScreen(Screen parent, Theme theme, File root, Text title) { + super(title); + this.parent = parent; + this.theme = theme; + list = new FileListWidget(theme, root, this::selected); + name = new TextBoxWidget(theme); + ok = new ButtonWidget(theme, okButtonText(), this::submit); + cancel = new ButtonWidget(theme, "Cancel", this::close); + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/FileListWidget.java b/src/java/me/topchetoeu/mcscript/gui/FileListWidget.java new file mode 100755 index 0000000..12094b0 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/FileListWidget.java @@ -0,0 +1,146 @@ +package me.topchetoeu.mcscript.gui; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.util.math.MatrixStack; + +public class FileListWidget extends Widget { + public interface SelectHandler { + void selected(File f); + } + + public final SelectHandler handler; + + private File res = null; + private File currFile; + private List entries = new ArrayList<>(); + + private float scroll = 0; + + + private void setEntries() { + if (currFile == null) { + entries.clear(); + for (var root : File.listRoots()) { + entries.add(root.toString()); + } + } + else if (currFile.isFile() || currFile.listFiles() == null) { + res = currFile; + handler.selected(currFile); + currFile = currFile.getParentFile(); + return; + } + else { + this.entries.clear(); + this.entries.add("(drives)"); + this.entries.add(".."); + for (var el : currFile.listFiles()) { + if (el.isDirectory()) this.entries.add(el.getName() + "/"); + } + for (var el : currFile.listFiles()) { + if (el.isFile()) this.entries.add(el.getName()); + } + } + + updateScroll(); + } + private void updateScroll() { + float _h = entries.size() * (theme.fontSize() + 4) - h; + if (scroll > _h) scroll = _h; + if (scroll < 0) scroll = 0; + } + private int entryHeight() { + return theme.fontSize() + 4; + } + + @Override + protected boolean hasBorders() { return true; } + + public File result() { + return res.toPath().toAbsolutePath().toFile(); + } + public File cd() { + return currFile.toPath().toAbsolutePath().toFile(); + } + + @Override + public void render(MatrixStack mat, int mx, int my, float delta) { + renderSetup(); + + mat.push(); + mat.translate(x, y, 0); + mx -= x; my -= y; + + fill(mat, 0, 0, w, h, theme.get("bg-1")); + + mat.push(); + mat.translate(0, -scroll, 0); + my += scroll; + + for (int i = 0; i < entries.size(); i++) { + int x1 = 0, y1 = i * entryHeight(), x2 = w, y2 = (i + 1) * entryHeight(); + if (mx >= x1 && mx < x2 && my >= y1 && my < y2) { + fill(mat, x1, y1, x2, y2, theme.get("bg-3")); + } + else if (i % 2 == 0) { + fill(mat, x1, y1, x2, y2, theme.get("bg-2")); + } + theme.font.draw(mat, entries.get(i), theme.get("padding-l"), y1 + 3, theme.get("text")); + } + + mat.pop(); + mat.pop(); + + renderFinalize(); + + super.render(mat, mx, my, delta); + } + + @Override + public boolean mouseClicked(double mx, double my, int button) { + if (button != 0 || !isMouseOver(mx, my)) return false; + + mx -= x; my -= y - scroll; + + int i = (int)(my / entryHeight()); + + if (i < 0 || i >= entries.size()) return false; + + if (currFile == null) { + currFile = new File(entries.get(i)); + } + else { + if (i == 0) currFile = null; + else if (i == 1) currFile = currFile.getParentFile(); + else currFile = new File(currFile, entries.get(i)); + } + + setEntries(); + + return true; + } + @Override + public boolean mouseScrolled(double mx, double my, double amount) { + if (isMouseOver(mx, my)) { + scroll -= amount * 10; + updateScroll(); + return true; + } + return false; + } + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + return false; + } + + public FileListWidget(Theme theme, File root, SelectHandler handler) { + super(theme); + this.handler = handler; + this.currFile = root.toPath().toAbsolutePath().toFile(); + if (this.currFile.isFile()) throw new RuntimeException("wtf"); + setEntries(); + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/FileTracker.java b/src/java/me/topchetoeu/mcscript/gui/FileTracker.java new file mode 100755 index 0000000..3682645 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/FileTracker.java @@ -0,0 +1,65 @@ +package me.topchetoeu.mcscript.gui; + +import java.io.File; + +public class FileTracker { + public final File file; + + private long lastChange = 0; + private boolean exists = false; + private boolean created = false; + private boolean deleted = false; + private boolean changed = false; + + public void update() { + if (file.exists()) { + if (!exists) created = changed = true; + exists = true; + + if (lastChange < file.lastModified()) { + changed = true; + lastChange = file.lastModified(); + } + } + else { + if (exists) changed = deleted = true; + exists = created = false; + lastChange = 0; + } + } + + public boolean exists() { + update(); + return exists; + } + public boolean wasChanged() { + update(); + if (changed) { + changed = false; + return true; + } + else return false; + } + public boolean wasCreated() { + update(); + if (created) { + created = false; + return true; + } + else return false; + } + public boolean wasDeleted() { + update(); + if (deleted) { + deleted = false; + return true; + } + else return false; + } + + public FileTracker(File file) { + this.file = file; + if (exists = file.exists()) lastChange = file.lastModified(); + else lastChange = 0; + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/OpenFileScreen.java b/src/java/me/topchetoeu/mcscript/gui/OpenFileScreen.java new file mode 100755 index 0000000..c8a941e --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/OpenFileScreen.java @@ -0,0 +1,33 @@ +package me.topchetoeu.mcscript.gui; + +import java.io.File; + +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +public class OpenFileScreen extends FileDialogScreen { + public interface OpenHandler { + void fileOpened(File file); + } + + public final OpenHandler handler; + + @Override + protected String okButtonText() { return "Open"; } + + @Override + protected void okPressed(File file) { + handler.fileOpened(file); + close(); + } + @Override + protected void selected(File file) { + handler.fileOpened(file); + close(); + } + + protected OpenFileScreen(Screen parent, Theme theme, File root, Text title, OpenHandler handler) { + super(parent, theme, root, title); + this.handler = handler; + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/SaveFileScreen.java b/src/java/me/topchetoeu/mcscript/gui/SaveFileScreen.java new file mode 100755 index 0000000..2fa33ce --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/SaveFileScreen.java @@ -0,0 +1,33 @@ +package me.topchetoeu.mcscript.gui; + +import java.io.File; + +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +public class SaveFileScreen extends FileDialogScreen { + public interface SaveHandler { + void fileSaved(File file); + } + + public final SaveHandler handler; + + @Override + protected String okButtonText() { return "Save"; } + + @Override + protected void okPressed(File file) { + handler.fileSaved(file); + close(); + } + @Override + protected void selected(File file) { + handler.fileSaved(file); + close(); + } + + protected SaveFileScreen(Screen parent, Theme theme, File root, Text title, SaveHandler handler) { + super(parent, theme, root, title); + this.handler = handler; + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/TextBoxWidget.java b/src/java/me/topchetoeu/mcscript/gui/TextBoxWidget.java new file mode 100755 index 0000000..f38734e --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/TextBoxWidget.java @@ -0,0 +1,425 @@ +package me.topchetoeu.mcscript.gui; + +import java.util.ArrayList; +import java.util.List; + +import org.lwjgl.glfw.GLFW; + +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.ColorHelper.Argb; + +public class TextBoxWidget extends Widget { + private boolean multiline = false; + private ArrayList> lines = new ArrayList<>(); + private String value; + + private int xPos = 0, yPos = 0; + private float xScroll = 0, yScroll = 0; + private int maxLineLen = 0; + + private int fontWidth() { + return 6; + } + private int fontHeight() { + return theme.fontSize(); + } + + private void updateValue() { + var res = new StringBuilder(); + + for (int i = 0; i < lines.size(); i++) { + if (i != 0) res.append('\n'); + for (var c : lines.get(i)) { + res.append(c); + } + if (maxLineLen < lines.get(i).size()) maxLineLen = lines.get(i).size(); + } + + value = res.toString(); + } + private void updatePos() { + int realX = xPos; + if (yPos >= lines.size()) yPos = lines.size() - 1; + if (yPos < 0) yPos = 0; + + if (realX > lines.get(yPos).size()) realX = lines.get(yPos).size(); + if (xPos < 0) xPos = 0; + + xScroll = Math.min(xScroll, (maxLineLen) * fontWidth() - fontWidth()); + yScroll = Math.min(yScroll, (lines.size()) * fontHeight() - fontHeight()); + + if (xScroll < 0) xScroll = 0; + if (yScroll < 0) yScroll = 0; + } + private void updateScroll() { + updatePos(); + + int realX = getCursorX(); + + yScroll = Math.min(yScroll, yPos * fontHeight()); + yScroll = Math.max(yScroll, (yPos + 1) * fontHeight() - h + theme.get("padding-s") * 2); + + xScroll = Math.min(xScroll, realX * fontWidth()); + xScroll = Math.max(xScroll, realX * fontWidth() - w + theme.get("padding-s") * 2); + } + + private List getLine() { + updatePos(); + return lines.get(yPos); + } + + public String getValue() { return value; } + public TextBoxWidget setValue(String val) { + lines.clear(); + for (var line : val.split("\n")) { + var chars = new ArrayList(); + + for (var c : line.toCharArray()) { + if (c == '\r') continue; + chars.add(c); + } + + lines.add(chars); + } + + value = val; + + updatePos(); + textChanged(); + + return this; + } + + public int getCursorX() { + updatePos(); + if (xPos > getLine().size()) return getLine().size(); + else return xPos; + } + public void setCursorX(int val) { + xPos = val; + + if (xPos < 0) xPos = 0; + if (xPos > getLine().size()) xPos = getLine().size(); + + updateScroll(); + } + public void changeCursorX(int delta) { + setCursorX(getCursorX() + delta); + } + + public int getCursorY() { + updatePos(); + return yPos; + } + public void setCursorY(int val) { + yPos = val; + + if (yPos < 0) yPos = 0; + if (yPos >= lines.size()) yPos = lines.size() - 1; + + updateScroll(); + } + public void changeCursorY(int delta) { + setCursorY(yPos + delta); + } + + public float getScrollX() { + updatePos(); + + return xScroll; + } + public void setScrollX(float val) { + xScroll = val; + updatePos(); + } + public void changeScrollX(float delta) { + setScrollX(getScrollX() + delta); + } + + public float getScrollY() { + updatePos(); + return yScroll; + } + public void setScrollY(float val) { + yScroll = val; + updatePos(); + } + public void changeScrollY(float delta) { + setScrollY(getScrollY() + delta); + } + + public boolean isMultiline() { return multiline; } + public TextBoxWidget setMultiline(boolean value) { + if (value == multiline) return this; + + if (value) { + setHeight(theme.get("padding-s") * 2 + theme.fontSize()); + var first = lines.get(0); + lines.clear(); + lines.add(first); + textChanged(); + } + + multiline = value; + return this; + } + + @Override + protected boolean hasBorders() { return true; } + + public void input(char c) { + updatePos(); + xPos = getCursorX(); + getLine().add(xPos, c); + updateValue(); + xPos++; + updateScroll(); + textChanged(); + } + public void newLine() { + if (!multiline) return; + updatePos(); + + var newLine = new ArrayList(); + var currLine = getLine(); + + while (currLine.size() > xPos) { + newLine.add(currLine.remove(xPos)); + } + + lines.add(yPos + 1, newLine); + + changeCursorY(1); + setCursorX(0); + + updateScroll(); + updateValue(); + textChanged(); + } + public void backspace() { + updatePos(); + + if (xPos == 0) { + if (yPos == 0) return; + var line = lines.remove(yPos); + changeCursorY(-1); + setCursorX(getLine().size()); + getLine().addAll(line); + } + else { + changeCursorX(-1); + getLine().remove(xPos); + } + + updateScroll(); + updateValue(); + textChanged(); + } + public void delete() { + updatePos(); + + if (xPos == getLine().size()) { + if (yPos == lines.size() - 1) return; + getLine().addAll(lines.remove(yPos + 1)); + } + else { + getLine().remove(xPos); + } + + updateScroll(); + updateValue(); + textChanged(); + } + public void home() { + setCursorX(0); + updateScroll(); + } + public void end() { + setCursorX(getLine().size()); + updateScroll(); + } + public void tab() { + for (int i = getCursorX() % 4; i < 4; i++) { + input(' '); + } + } + public void right(boolean words) { + if (words) { + var line = getLine(); + + if (getCursorX() >= getLine().size()) { + if (getCursorY() == lines.size() - 1) return; + changeCursorY(1); + setCursorX(0); + } + else { + if (line.get(getCursorX()) == ' ') { + for (; getCursorX() < getLine().size(); changeCursorX(1)) { + if (line.get(getCursorX()) != ' ') break; + } + } + else { + for (; getCursorX() < getLine().size(); changeCursorX(1)) { + if (line.get(getCursorX()) == ' ') break; + } + for (; getCursorX() < getLine().size(); changeCursorX(1)) { + if (line.get(getCursorX()) != ' ') break; + } + } + } + } + else if (getCursorX() >= getLine().size()) { + if (getCursorY() == lines.size() - 1) return; + changeCursorY(1); + setCursorX(0); + } + else changeCursorX(1); + updateScroll(); + } + public void left(boolean words) { + if (getCursorX() == 0) { + if (getCursorY() == 0) return; + changeCursorY(-1); + setCursorX(getLine().size()); + } + else if (words) { + var line = getLine(); + + if (line.get(getCursorX() - 1) == ' ') { + for (; getCursorX() > 0; changeCursorX(-1)) { + if (line.get(getCursorX() - 1) != ' ') break; + } + } + else { + for (; getCursorX() > 0; changeCursorX(-1)) { + if (line.get(getCursorX() - 1) == ' ') break; + } + for (; getCursorX() > 0; changeCursorX(-1)) { + if (line.get(getCursorX() - 1) != ' ') break; + } + } + } + else changeCursorX(-1); + updateScroll(); + } + + @Override + public Widget setRect(int x, int y, int w, int h) { + super.setRect(x, y, w, h); + updateScroll(); + return this; + } + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + try { + boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; + + switch (keyCode) { + case GLFW.GLFW_KEY_BACKSPACE: backspace(); return true; + case GLFW.GLFW_KEY_DELETE: delete(); return true; + case GLFW.GLFW_KEY_ENTER: newLine(); return true; + case GLFW.GLFW_KEY_TAB: tab(); return true; + + case GLFW.GLFW_KEY_UP: changeCursorY(-1); return true; + case GLFW.GLFW_KEY_DOWN: changeCursorY(1); return true; + case GLFW.GLFW_KEY_LEFT: left(ctrl); return true; + case GLFW.GLFW_KEY_RIGHT: right(ctrl); return true; + + case GLFW.GLFW_KEY_END: end(); return true; + case GLFW.GLFW_KEY_HOME: home(); return true; + + case GLFW.GLFW_KEY_PAGE_DOWN: { + if (ctrl) changeScrollX(w / 2); + else changeScrollY(h / 2); + return true; + } + case GLFW.GLFW_KEY_PAGE_UP: { + if (ctrl) changeScrollX(-w / 2); + else changeScrollY(-h / 2); + return true; + } + } + } + catch (Throwable e) { + e.printStackTrace(); + } + + return false; + } + @Override + public boolean charTyped(char c, int modifiers) { + if (c == '\n') newLine(); + input(c); + return true; + } + @Override + public boolean mouseClicked(double mx, double my, int button) { + if (!isMouseOver(mx, my)) return false; + mx -= x - xScroll + theme.get("padding-s"); + my -= y - yScroll + theme.get("padding-s"); + + setCursorY((int)Math.floor(my / fontHeight())); + setCursorX((int)Math.round(mx / fontWidth())); + return true; + } + @Override + public boolean mouseScrolled(double mx, double my, double amount) { + if (!isMouseOver(mx, my)) return false; + changeScrollY(-(float)amount * 10); + return true; + } + @Override + public void render(MatrixStack mat, int mouseX, int mouseY, float delta) { + renderSetup(); + + mat.push(); + mat.translate(x, y, 0); + + var width = 6; + var height = theme.fontSize(); + + fill(mat, 0, 0, this.w, this.h, theme.get("bg-1")); + + mat.push(); + mat.translate(theme.get("padding-s") - xScroll, theme.get("padding-s") - yScroll, 0); + + for (int i = 0; i < lines.size(); i++) { + var line = lines.get(i); + + for (int j = 0; j < line.size(); j++) { + var c = line.get(j); + float offX = (width - theme.font.getWidth(c + "")) / 2f; + theme.font.draw(mat, c + "", j * width + offX, i * height, theme.get("text")); + } + } + + drawVerticalLine(mat, xPos * width, yPos * height - 1, (yPos + 1) * height, Argb.getArgb(255, 255, 0, 0)); + + mat.pop(); + mat.pop(); + + renderFinalize(); + + super.render(mat, mouseX, mouseY, delta); + } + + protected void textChanged() { } + + @Override + public void appendNarrations(NarrationMessageBuilder nmb) { + nmb.put(NarrationPart.USAGE, "Text box"); + } + @Override + public SelectionType getType() { + return SelectionType.NONE; + } + + public TextBoxWidget(Theme theme) { + super(theme); + this.h = theme.fontSize() + theme.get("padding-s") * 2; + this.setValue(""); + updateScroll(); + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/Theme.java b/src/java/me/topchetoeu/mcscript/gui/Theme.java new file mode 100755 index 0000000..ceb78e4 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/Theme.java @@ -0,0 +1,53 @@ +package me.topchetoeu.mcscript.gui; + +import java.util.HashMap; +import java.util.Map; + +import net.minecraft.client.font.TextRenderer; +import net.minecraft.util.math.ColorHelper.Argb; + +public class Theme { + public final Map colors = new HashMap<>(); + public TextRenderer font; + public int fontSize() { return font.fontHeight; } + + public Theme add(String name, int col) { + colors.put(name, col); + return this; + } + public Theme add(String name, int r, int g, int b, int a) { + colors.put(name, Argb.getArgb(a, r, g, b)); + return this; + } + public int get(String name) { + var res = colors.get(name); + if (res == null) return 0; + else return res; + } + + public Theme setTheme(Theme t) { + if (t == this) return this; + colors.clear(); + colors.putAll(colors); + return this; + } + public Theme setFont(TextRenderer font) { + this.font = font; + return this; + } + + + public static Theme dark(TextRenderer font) { + return new Theme() + .setFont(font) + .add("padding-s", 2) + .add("padding-m", 3) + .add("padding-l", 5) + .add("bg-1", 0, 0, 0, 255) + .add("bg-2", 40, 40, 40, 255) + .add("bg-3", 80, 80, 80, 255) + .add("bg-4", 100, 100, 100, 255) + .add("border", 255, 255, 255, 255) + .add("text", 255, 255, 255, 255); + } +} diff --git a/src/java/me/topchetoeu/mcscript/gui/Widget.java b/src/java/me/topchetoeu/mcscript/gui/Widget.java new file mode 100755 index 0000000..656a3c2 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/gui/Widget.java @@ -0,0 +1,101 @@ +package me.topchetoeu.mcscript.gui; + +import org.joml.Vector4f; + +import com.mojang.blaze3d.systems.RenderSystem; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.util.math.MatrixStack; + +public abstract class Widget extends DrawableHelper implements Element, Drawable, Selectable { + public final Theme theme; + protected int x, y, w, h; + + protected abstract boolean hasBorders(); + + public final int getX() { return x; } + public final Widget setX(int val) { return setPos(val, y); } + + public final int getY() { return y; } + public final Widget setY(int val) { return setPos(x, val); } + + public final int getWidth() { return w; } + public final Widget setWidth(int value) { return setSize(value, h); } + + public final int getHeight() { return h; } + public final Widget setHeight(int value) { return setSize(w, value); } + + public final Widget setPos(int x, int y) { + return setRect(x, y, w, h); + } + public final Widget setSize(int w, int h) { + return setRect(x, y, w, h); + } + public Widget setRect(int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + return this; + } + + @Override + public void setFocused(boolean focused) { + } + @Override + public boolean isFocused() { + return false; + } + + @Override + public boolean isMouseOver(double mx, double my) { + return mx >= x && mx < x + w && my >= y && my < y + h; + } + + @Override + public void appendNarrations(NarrationMessageBuilder var1) { + } + @Override + public SelectionType getType() { + return SelectionType.NONE; + } + + protected void renderSetup() { + var m = RenderSystem.getProjectionMatrix(); + var pos = m.transform(new Vector4f(x, y, 0, 1)); + var scale = m.transform(new Vector4f(w, h, 0, 0)); + + int wndw = MinecraftClient.getInstance().getWindow().getWidth(); + int wndh = MinecraftClient.getInstance().getWindow().getHeight(); + + int x = (int)Math.round((pos.x + 1) / 2 * wndw); + int y = (int)Math.round((-pos.y + 1) / 2 * wndh); + int w = (int)Math.round(scale.x / 2 * wndw); + int h = (int)Math.round(-scale.y / 2 * wndh); + + y = wndh - y - h; + + RenderSystem.enableScissor(x, y, w, h); + } + protected void renderFinalize() { + RenderSystem.disableScissor(); + } + + @Override + public void render(MatrixStack mat, int mx, int my, float delta) { + int col = theme.get("border"); + drawHorizontalLine(mat, x - 1, x + w, y - 1, col); + drawHorizontalLine(mat, x - 1, x + w, y + h, col); + drawVerticalLine(mat, x - 1, y - 1, y + h, col); + drawVerticalLine(mat, x + w, y - 1, y + h, col); + } + + public Widget(Theme theme) { + this.theme = theme; + } +} diff --git a/src/java/me/topchetoeu/mcscript/loader/ModLoader.java b/src/java/me/topchetoeu/mcscript/loader/ModLoader.java new file mode 100644 index 0000000..2d96922 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/loader/ModLoader.java @@ -0,0 +1,5 @@ +package me.topchetoeu.mcscript.loader; + +public interface ModLoader { + +} diff --git a/src/java/me/topchetoeu/mcscript/mixin/ChatScreenMixin.java b/src/java/me/topchetoeu/mcscript/mixin/ChatScreenMixin.java new file mode 100755 index 0000000..1bfeba9 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/mixin/ChatScreenMixin.java @@ -0,0 +1,37 @@ +package me.topchetoeu.mcscript.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import me.topchetoeu.mcscript.events.ChatMessageCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ChatScreen; + +@Mixin(ChatScreen.class) +public abstract class ChatScreenMixin { + + @Inject(method = "sendMessage", at = @At(value = "INVOKE", target = "Ljava/lang/String;startsWith(Ljava/lang/String;)Z"), cancellable = true) + private void onSendMessage(String chatText, boolean addToHistory, CallbackInfoReturnable cbi) { + var client = MinecraftClient.getInstance(); + + var args = new ChatMessageCallback.ChatArgs(); + args.message = chatText; + args.cancelled = false; + ChatMessageCallback.EVENT.invoker().execute(args); + + if (!args.cancelled) { + chatText = args.message; + + if (chatText.startsWith("/")) { + client.player.networkHandler.sendChatCommand(chatText.substring(1)); + } else { + client.player.networkHandler.sendChatMessage(chatText); + } + } + + cbi.setReturnValue(true); + cbi.cancel(); + } +} diff --git a/src/java/me/topchetoeu/mcscript/mixin/KeyboardMixin.java b/src/java/me/topchetoeu/mcscript/mixin/KeyboardMixin.java new file mode 100755 index 0000000..1ef73ec --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/mixin/KeyboardMixin.java @@ -0,0 +1,22 @@ +package me.topchetoeu.mcscript.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.client.Keyboard; +import net.minecraft.client.MinecraftClient; + +@Mixin(Keyboard.class) +public class KeyboardMixin { + @Inject(method = "onChar", at = @At("HEAD")) + private void onChar(long window, int codePoint, int modifiers, CallbackInfo cbi) { + var client = MinecraftClient.getInstance(); + for (var ch : Character.toChars(codePoint)) { + if (client.currentScreen == null && client.getOverlay() == null && ch == '#') { + ((MinecraftClientMixin)client).invokeOpenChatScreen(""); + } + } + } +} diff --git a/src/java/me/topchetoeu/mcscript/mixin/MinecraftClientMixin.java b/src/java/me/topchetoeu/mcscript/mixin/MinecraftClientMixin.java new file mode 100755 index 0000000..99f9410 --- /dev/null +++ b/src/java/me/topchetoeu/mcscript/mixin/MinecraftClientMixin.java @@ -0,0 +1,12 @@ +package me.topchetoeu.mcscript.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import net.minecraft.client.MinecraftClient; + +@Mixin(MinecraftClient.class) +public interface MinecraftClientMixin { + @Invoker + void invokeOpenChatScreen(String text); +}