diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/Command.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/Command.java new file mode 100644 index 0000000..5930bc9 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/Command.java @@ -0,0 +1,13 @@ +package de.zonlykroks.advancedscripts.lexer; + +import java.util.List; + +public class Command { + public final boolean repeatable; + public final List> arguments; + + public Command(boolean repeatable, List> arguments) { + this.repeatable = repeatable; + this.arguments = arguments; + } +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/Commands.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/Commands.java new file mode 100644 index 0000000..3c2debc --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/Commands.java @@ -0,0 +1,13 @@ +package de.zonlykroks.advancedscripts.lexer; + +import java.util.HashMap; +import java.util.Map; + +public class Commands { + + private Commands() { + throw new IllegalStateException("Utility class"); + } + + public static Map COMMANDS = new HashMap<>(); +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/Headers.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/Headers.java new file mode 100644 index 0000000..2e3c4e7 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/Headers.java @@ -0,0 +1,13 @@ +package de.zonlykroks.advancedscripts.lexer; + +import java.util.HashSet; +import java.util.Set; + +public class Headers { + + private Headers() { + throw new IllegalStateException("Utility class"); + } + + public static final Set HEADERS = new HashSet<>(); +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/Operators.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/Operators.java new file mode 100644 index 0000000..70586f4 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/Operators.java @@ -0,0 +1,13 @@ +package de.zonlykroks.advancedscripts.lexer; + +import java.util.HashSet; +import java.util.Set; + +public class Operators { + + private Operators() { + throw new IllegalStateException("Utility class"); + } + + public static final Set OPERATORS = new HashSet<>(); +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/ScriptColorizer.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/ScriptColorizer.java new file mode 100644 index 0000000..e90fc9e --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/ScriptColorizer.java @@ -0,0 +1,239 @@ +package de.zonlykroks.advancedscripts.lexer; + +import java.util.ArrayList; +import java.util.List; + +public class ScriptColorizer { + + private ScriptColorizer() { + throw new IllegalStateException("Utility class"); + } + + public static List colorize(int lineNumber, String line) { + if (lineNumber == 0) { + List tokens = colorizeHeader(line); + if (tokens != null) return tokens; + } + + List tokens; + tokens = colorizeComment(line); + if (tokens != null) return tokens; + tokens = colorizeJumpPoint(line); + if (tokens != null) return tokens; + return colorizeLine(line); + } + + private static List colorizeHeader(String line) { + if (!line.startsWith("#!")) return null; + List tokens = new ArrayList<>(); + tokens.add(new Token("#!", TokenTypeColors.COMMENT)); + String s = line.substring(2); + + for (String pattern : Headers.HEADERS) { + if (s.matches(pattern)) { + tokens.add(new Token(s, TokenTypeColors.OTHER)); + return tokens; + } + } + tokens.add(new Token(s, TokenTypeColors.ERROR)); + return tokens; + } + + private static List colorizeComment(String line) { + if (!line.startsWith("#")) return null; + return List.of(new Token(line, TokenTypeColors.COMMENT)); + } + + private static List colorizeJumpPoint(String line) { + if (!line.startsWith(".")) return null; + return List.of(new Token(line, TokenTypeColors.JUMP_POINT)); + } + + private static List colorizeLine(String line) { + List tokens = new ArrayList<>(); + + String command = line; + if (line.indexOf(' ') != -1) { + command = line.substring(0, line.indexOf(' ')); + } + boolean repeatable = false; + List> argumentTypes = null; + if (Commands.COMMANDS.containsKey(command)) { + Command c = Commands.COMMANDS.get(command); + repeatable = c.repeatable; + argumentTypes = c.arguments; + tokens.add(new Token(command, TokenTypeColors.LITERAL)); + } else { + repeatable = true; + argumentTypes = new ArrayList<>(); + argumentTypes.add(List.of(TokenType.any)); + tokens.add(new Token(command, TokenTypeColors.OTHER)); + } + if (command.equals(line)) return tokens; + tokens.add(Token.SPACE); + + String args = line.substring(command.length() + 1).trim(); + tokens.addAll(colorizeArgs(args, repeatable, argumentTypes)); + return tokens; + } + + private static List colorizeArgs(String args, boolean repeatable, List> argumentTypes) { + List tokens = new ArrayList<>(); + + for (List tokenTypes : argumentTypes) { + List temp = new ArrayList<>(); + int index = 0; + int argIndex = 0; + try { + while (argIndex < args.length()) { + if (args.charAt(argIndex) == ' ') { + argIndex++; + temp.add(Token.SPACE); + continue; + } + List current = parse(tokenTypes.get(index), args.substring(argIndex)); + if (current.isEmpty()) { + break; + } + temp.addAll(current); + argIndex += current.stream().mapToInt(t -> t.text.length()).sum(); + index++; + if (repeatable && index == tokenTypes.size()) { + index--; + } + if (index == tokenTypes.size()) { + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + if (argIndex != args.length()) { + continue; + } + if (index != tokenTypes.size() - (repeatable ? 1 : 0)) { + continue; + } + + if (!temp.isEmpty()) { + tokens.addAll(temp); + break; + } + } + + if (tokens.isEmpty()) { + tokens.add(new Token(args, TokenTypeColors.OTHER)); + } + return tokens; + } + + private static List parse(TokenType type, String current) { + return switch (type) { + case any -> parseAny(current); + case expression -> parseExpression(current); + case jump_point -> parseJumpPoint(current); + case variable -> parseVariable(current); + case text_type -> parseText(current); + case number_type -> parseNumber(current); + case floating_number_type -> parseFloatingNumber(current); + case boolean_type -> parseBoolean(current); + }; + } + + private static List parseAny(String current) { + List tokens = parseExpression(current); + if (!tokens.isEmpty()) return tokens; + tokens = parseFloatingNumber(current); + if (!tokens.isEmpty()) return tokens; + tokens = parseNumber(current); + if (!tokens.isEmpty()) return tokens; + tokens = parseBoolean(current); + if (!tokens.isEmpty()) return tokens; + return parseText(current); + } + + private static List parseExpression(String current) { + if (!current.startsWith("{")) return new ArrayList<>(); + int depth = 0; + int index = 0; + do { + if (current.charAt(index) == '{') { + depth++; + } else if (current.charAt(index) == '}') { + depth--; + } + index++; + } while (depth != 0 && index <= current.length()); + if (depth != 0) return List.of(new Token(current, TokenTypeColors.ERROR)); + String expression = current.substring(0, index); // TODO: colorize expression + return List.of(new Token(expression, TokenTypeColors.OTHER)); + } + + private static List parseJumpPoint(String current) { + int index = current.indexOf(' '); + if (index == -1) { + return List.of(new Token(current, TokenTypeColors.JUMP_POINT)); + } else { + return List.of(new Token(current.substring(0, index), TokenTypeColors.JUMP_POINT)); + } + } + + private static List parseVariable(String current) { + int index = current.indexOf(' '); + if (index == -1) { + return List.of(new Token(current, TokenTypeColors.VARIABLE)); + } else { + return List.of(new Token(current.substring(0, index), TokenTypeColors.VARIABLE)); + } + } + + private static List parseText(String current) { + int index = current.indexOf(' '); + if (index == -1) { + return List.of(new Token(current, TokenTypeColors.TEXT)); + } else { + return List.of(new Token(current.substring(0, index), TokenTypeColors.TEXT)); + } + } + + private static List parseNumber(String current) { + int index = current.indexOf(' '); + String number = current; + if (index != -1) { + number = current.substring(0, index); + } + try { + Long.parseLong(number); + return List.of(new Token(number, TokenTypeColors.NUMBER)); + } catch (NumberFormatException e) { + return new ArrayList<>(); + } + } + + private static List parseFloatingNumber(String current) { + int index = current.indexOf(' '); + String number = current; + if (index != -1) { + number = current.substring(0, index); + } + try { + Double.parseDouble(number); + return List.of(new Token(number, TokenTypeColors.NUMBER)); + } catch (NumberFormatException e) { + return new ArrayList<>(); + } + } + + private static List parseBoolean(String current) { + int index = current.indexOf(' '); + String bool = current; + if (index != -1) { + bool = current.substring(0, index); + } + if ("true".equalsIgnoreCase(bool) || "false".equalsIgnoreCase(bool)) { + return List.of(new Token(bool, TokenTypeColors.BOOLEAN)); + } else { + return new ArrayList<>(); + } + } +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/ScriptSyntaxPacketParser.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/ScriptSyntaxPacketParser.java new file mode 100644 index 0000000..abf09f6 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/ScriptSyntaxPacketParser.java @@ -0,0 +1,78 @@ +package de.zonlykroks.advancedscripts.lexer; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class ScriptSyntaxPacketParser { + + private ScriptSyntaxPacketParser() { + throw new IllegalStateException("Utility class"); + } + + private static final TokenType[] TOKEN_TYPES = TokenType.values(); + + private static void reset() { + Operators.OPERATORS.clear(); + Headers.HEADERS.clear(); + VariablePrefixes.RPEFIXES.clear(); + VariableSuffixes.SUFFIXES.clear(); + Commands.COMMANDS.clear(); + } + + public static synchronized void parse(String scriptSyntax) { + reset(); + + JsonObject jsonObject = JsonParser.parseString(scriptSyntax).getAsJsonObject(); + for (String key : jsonObject.keySet()) { + JsonArray jsonElements = jsonObject.get(key).getAsJsonArray(); + if (key.startsWith("@")) { + parseSpecial(key, jsonElements); + } else { + parseCommand(key, jsonElements); + } + } + } + + private static void parseCommand(String key, JsonArray value) { + boolean repeating = value.get(0).getAsBoolean(); + List> validArgumentTypes = new ArrayList<>(); + for (int i = 1; i < value.size(); i++) { + JsonArray parameters = value.get(i).getAsJsonArray(); + List parameterTypes = new ArrayList<>(); + for (JsonElement parameter : parameters) { + parameterTypes.add(TOKEN_TYPES[parameter.getAsInt()]); + } + validArgumentTypes.add(parameterTypes); + } + Commands.COMMANDS.put(key, new Command(repeating, validArgumentTypes)); + } + + private static void parseSpecial(String key, JsonArray value) { + Set set; + switch (key) { + case "@operators": + set = Operators.OPERATORS; + break; + case "@headers": + set = Headers.HEADERS; + break; + case "@prefixes": + set = VariablePrefixes.RPEFIXES; + break; + case "@suffixes": + set = VariableSuffixes.SUFFIXES; + break; + default: + return; + } + for (JsonElement element : value) { + set.add(element.getAsString()); + } + } +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/Token.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/Token.java new file mode 100644 index 0000000..6e7d022 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/Token.java @@ -0,0 +1,13 @@ +package de.zonlykroks.advancedscripts.lexer; + +public class Token { + public static final Token SPACE = new Token(" ", 0xFFFFFFFF); + + public final String text; + public final int color; + + public Token(String text, int color) { + this.text = text; + this.color = color; + } +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/TokenType.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/TokenType.java new file mode 100644 index 0000000..075646c --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/TokenType.java @@ -0,0 +1,13 @@ +package de.zonlykroks.advancedscripts.lexer; + +public enum TokenType { // This is copied from the BauSystem2.0 sources. + any, // This does not include jump_point and variable + expression, + jump_point, + variable, + + text_type, + number_type, + floating_number_type, + boolean_type, +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/TokenTypeColors.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/TokenTypeColors.java new file mode 100644 index 0000000..e01fc89 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/TokenTypeColors.java @@ -0,0 +1,22 @@ +package de.zonlykroks.advancedscripts.lexer; + +public class TokenTypeColors { + + private TokenTypeColors() { + throw new IllegalStateException("Utility class"); + } + + public static final int BACKGROUND = 0xFF1E1F22; + public static final int OTHER = 0xFFFFFFFF; + + public static final int ERROR = 0xFFAA0000; + + public static final int VARIABLE = 0xFFFFFFFF; + public static final int LITERAL = 0xFF925F35; + public static final int COMMENT = 0xFF656565; + public static final int JUMP_POINT = 0xFFFFa500; + + public static final int NUMBER = 0xFF61839F; + public static final int BOOLEAN = 0xFF925F35; + public static final int TEXT = 0xFF6F855D; +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/VariablePrefixes.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/VariablePrefixes.java new file mode 100644 index 0000000..631fd15 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/VariablePrefixes.java @@ -0,0 +1,13 @@ +package de.zonlykroks.advancedscripts.lexer; + +import java.util.HashSet; +import java.util.Set; + +public class VariablePrefixes { + + private VariablePrefixes() { + throw new IllegalStateException("Utility class"); + } + + public static final Set RPEFIXES = new HashSet<>(); +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/lexer/VariableSuffixes.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/VariableSuffixes.java new file mode 100644 index 0000000..d4d67f1 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/VariableSuffixes.java @@ -0,0 +1,13 @@ +package de.zonlykroks.advancedscripts.lexer; + +import java.util.HashSet; +import java.util.Set; + +public class VariableSuffixes { + + private VariableSuffixes() { + throw new IllegalStateException("Utility class"); + } + + public static final Set SUFFIXES = new HashSet<>(); +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/zonlykroks/advancedscripts/mixin/ClientPlayNetworkHandlerMixin.java new file mode 100644 index 0000000..3b9c457 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/mixin/ClientPlayNetworkHandlerMixin.java @@ -0,0 +1,31 @@ +package de.zonlykroks.advancedscripts.mixin; + +import de.zonlykroks.advancedscripts.lexer.ScriptSyntaxPacketParser; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket; +import net.minecraft.util.Identifier; +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; + +@Mixin(ClientPlayNetworkHandler.class) +public class ClientPlayNetworkHandlerMixin { + + private static final Identifier CHANNEL = new Identifier("sw:script_syntax"); + + @Inject(method = "onCustomPayload", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/CustomPayloadS2CPacket;getData()Lnet/minecraft/network/PacketByteBuf;"), cancellable = true) + public void onCustomPayload(CustomPayloadS2CPacket packet, CallbackInfo ci) { + if (CHANNEL.equals(packet.getChannel())) { + PacketByteBuf buf = packet.getData(); + int readableBytes = buf.readableBytes(); + StringBuilder st = new StringBuilder(); + for (int i = 0; i < readableBytes; i++) { + st.append((char) buf.readByte()); + } + ScriptSyntaxPacketParser.parse(st.toString()); + ci.cancel(); + } + } +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/mixin/ClientPlayerEntityMixin.java b/src/main/java/de/zonlykroks/advancedscripts/mixin/ClientPlayerEntityMixin.java new file mode 100644 index 0000000..4c00782 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/mixin/ClientPlayerEntityMixin.java @@ -0,0 +1,28 @@ +package de.zonlykroks.advancedscripts.mixin; + +import de.zonlykroks.advancedscripts.screen.ScriptEditScreen; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.util.Hand; +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; + +@Mixin(ClientPlayerEntity.class) +public class ClientPlayerEntityMixin { + + @Shadow @Final protected MinecraftClient client; + + @Inject(method = "useBook", at = @At("HEAD"), cancellable = true) + public void useBookMixin(ItemStack book, Hand hand, CallbackInfo ci) { + if (book.isOf(Items.WRITABLE_BOOK)) { + this.client.setScreen(new ScriptEditScreen(((ClientPlayerEntity)(Object)this), book, hand)); + ci.cancel(); + } + } +} diff --git a/src/main/java/de/zonlykroks/advancedscripts/screen/ScriptEditScreen.java b/src/main/java/de/zonlykroks/advancedscripts/screen/ScriptEditScreen.java new file mode 100644 index 0000000..26c9e76 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/screen/ScriptEditScreen.java @@ -0,0 +1,111 @@ +package de.zonlykroks.advancedscripts.screen; + +import com.mojang.blaze3d.systems.RenderSystem; +import de.zonlykroks.advancedscripts.lexer.ScriptColorizer; +import de.zonlykroks.advancedscripts.lexer.Token; +import de.zonlykroks.advancedscripts.lexer.TokenTypeColors; +import net.minecraft.client.font.TextHandler; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.BookScreen; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.util.NarratorManager; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.text.Style; +import net.minecraft.util.Hand; +import org.apache.commons.lang3.mutable.MutableInt; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class ScriptEditScreen extends Screen { + + private PlayerEntity player; + private ItemStack itemStack; + private Hand hand; + + private List lines = new ArrayList<>(); + + public ScriptEditScreen(PlayerEntity player, ItemStack itemStack, Hand hand) { + super(NarratorManager.EMPTY); + this.player = player; + this.itemStack = itemStack; + this.hand = hand; + + NbtCompound nbtCompound = itemStack.getNbt(); + if (nbtCompound != null) { + BookScreen.filterPages(nbtCompound, s -> { + lines.addAll(Arrays.asList(s.split("\n"))); + }); + } + if (lines.isEmpty()) { + lines.add(""); + } + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + RenderSystem.setShader(GameRenderer::getPositionTexProgram); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + fill(matrices, 23, 23, this.width - 23, this.height - 72, TokenTypeColors.BACKGROUND); + + int lineNumberLength = textRenderer.getWidth(lines.size() + ""); + + // TODO: Implement text rendering + int lineNumberText = 1; + MutableInt lineNumber = new MutableInt(); + TextHandler textHandler = this.textRenderer.getTextHandler(); + for (String s : lines) { + if (lineNumber.getValue() * 9 + 25 > this.height - 75) { + break; + } + + // Line number + this.textRenderer.draw(matrices, lineNumberText + "", 25 + lineNumberLength - textRenderer.getWidth(lineNumberText + ""), 25 + lineNumber.getValue() * 9, 0xFFFFFF); + lineNumberText++; + + // Line text + List tokens = ScriptColorizer.colorize(lineNumber.getValue(), s); + AtomicInteger x = new AtomicInteger(25 + lineNumberLength + 5); + for (Token token : tokens) { + textHandler.wrapLines(token.text, this.width - x.get() - 25, Style.EMPTY, true, (style, start, end) -> { + int y = lineNumber.getValue() * 9; + if (y + 25 > this.height - 75) { + return; + } + + String line = token.text.substring(start, end); + this.textRenderer.draw(matrices, line, x.get(), 25 + y, token.color); + x.addAndGet(textRenderer.getWidth(line)); + if (x.get() > this.width - 50 - lineNumberLength - 5) { + x.set(25 + lineNumberLength + 5); + lineNumber.increment(); + } + }); + } + lineNumber.increment(); + } + + super.render(matrices, mouseX, mouseY, delta); + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + return super.keyReleased(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + return super.charTyped(chr, modifiers); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return super.mouseClicked(mouseX, mouseY, button); + } +} diff --git a/src/main/resources/advancedscripts.mixins.json b/src/main/resources/advancedscripts.mixins.json index c4c8409..8adf065 100644 --- a/src/main/resources/advancedscripts.mixins.json +++ b/src/main/resources/advancedscripts.mixins.json @@ -7,7 +7,9 @@ ], "client": [ "ClientLoginNetworkHandlerMixin", - "KeyboardMixin" + "KeyboardMixin", + "ClientPlayerEntityMixin", + "ClientPlayNetworkHandlerMixin" ], "injectors": { "defaultRequire": 1