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/ExpressionColorizer.java b/src/main/java/de/zonlykroks/advancedscripts/lexer/ExpressionColorizer.java new file mode 100644 index 0000000..feaea4a --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/ExpressionColorizer.java @@ -0,0 +1,134 @@ +package de.zonlykroks.advancedscripts.lexer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ExpressionColorizer { + + private ExpressionColorizer() { + throw new IllegalStateException("Utility class"); + } + + public static List colorize(String expression) { + List parts = tokenize(expression); + List tokens = new ArrayList<>(); + for (int i = 0; i < parts.size(); i++) { + String part = parts.get(i); + if ("{".equals(part) || "}".equals(part)) { + tokens.add(new Token(part, TokenTypeColors.OTHER)); + continue; + } + if ("true".equalsIgnoreCase(part) || "false".equalsIgnoreCase(part)) { + tokens.add(new Token(part, TokenTypeColors.BOOLEAN)); + continue; + } + try { + Double.parseDouble(part); + tokens.add(new Token(part, TokenTypeColors.NUMBER)); + continue; + } catch (NumberFormatException ignored) { + } + try { + Long.parseLong(part); + tokens.add(new Token(part, TokenTypeColors.NUMBER)); + continue; + } catch (NumberFormatException ignored) { + } + if (part.contains(".")) { + String[] split = part.split("\\."); + if (split.length == 1) { + tokens.add(new Token(part, TokenTypeColors.VARIABLE)); + continue; + } + if (VariablePrefixes.RPEFIXES.contains(split[0])) { + tokens.add(new Token(split[0], TokenTypeColors.OTHER)); + tokens.add(new Token(".", TokenTypeColors.OTHER)); + split = Arrays.copyOfRange(split, 1, split.length); + } + tokens.add(new Token(split[0], TokenTypeColors.VARIABLE)); + for (int j = 1; j < split.length; j++) { + String s = split[j]; + tokens.add(new Token(".", TokenTypeColors.OTHER)); + if (VariableSuffixes.SUFFIXES.contains(s)) { + tokens.add(new Token(s, TokenTypeColors.OTHER)); + } else { + tokens.add(new Token(s, TokenTypeColors.ERROR)); + } + } + continue; + } + if (Operators.OPERATORS.contains(part)) { + String previous = get(parts, i, -1); + String next = get(parts, i, 1); + if (previous == null || next == null) { + tokens.add(new Token(part, TokenTypeColors.ERROR)); + continue; + } + if (Operators.OPERATORS.contains(previous) || Operators.OPERATORS.contains(next)) { + tokens.add(new Token(part, TokenTypeColors.ERROR)); + continue; + } + if ("{".equals(previous) || "}".equals(next)) { + tokens.add(new Token(part, TokenTypeColors.ERROR)); + continue; + } + tokens.add(new Token(part, TokenTypeColors.OTHER)); + continue; + } + if (part.matches("[+\\-*/%^&|<>=!]+")) { + tokens.add(new Token(part, TokenTypeColors.ERROR)); + continue; + } + tokens.add(new Token(part, TokenTypeColors.VARIABLE)); + } + return tokens; + } + + private static String get(List parts, int index, int direction) { + for (int i = index + direction; i >= 0 && i < parts.size(); i += direction) { + String part = parts.get(i); + if (!part.isBlank()) return part; + } + return null; + } + + private static List tokenize(String s) { + List tokens = new ArrayList<>(); + StringBuilder token = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '{' || c == '}' || c == ' ') { + if (token.length() > 0) { + tokens.add(token.toString()); + token = new StringBuilder(); + } + tokens.add(c + ""); + continue; + } + StringBuilder op = new StringBuilder(); + for (int j = i; j < s.length(); j++) { + char k = s.charAt(j); + if (k == '+' || k == '-' || k == '*' || k == '/' || k == '%' || k == '^' || k == '&' || k == '|' || k == '>' || k == '<' || k == '=' || k == '!') { + op.append(k); + } else { + break; + } + } + if (op.length() > 0) { + if (token.length() > 0) { + tokens.add(token.toString()); + token = new StringBuilder(); + } + tokens.add(op.toString()); + i += op.length() - 1; + continue; + } + token.append(c); + } + if (token.length() > 0) { + tokens.add(token.toString()); + } + return tokens; + } +} 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..6086486 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/ScriptColorizer.java @@ -0,0 +1,278 @@ +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); + 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)); + return ExpressionColorizer.colorize(current.substring(0, index)); + } + + 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) { + current = current.substring(0, index); + } + List tokens = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < current.length(); i++) { + char c = current.charAt(i); + if (c == '&' && i + 1 < current.length()) { + char color = current.charAt(i + 1); + if (color >= '0' && color <= '9' || color >= 'a' && color <= 'f' || color >= 'A' && color <= 'F') { + if (sb.length() > 0) { + tokens.add(new Token(sb.toString(), TokenTypeColors.TEXT)); + sb = new StringBuilder(); + } + i++; + switch (color) { + case '0' -> tokens.add(new Token("&0", 0xFF000000)); + case '1' -> tokens.add(new Token("&1", 0xFF0000AA)); + case '2' -> tokens.add(new Token("&2", 0xFF00AA00)); + case '3' -> tokens.add(new Token("&3", 0xFF00AAAA)); + case '4' -> tokens.add(new Token("&4", 0xFFAA0000)); + case '5' -> tokens.add(new Token("&5", 0xFFAA00AA)); + case '6' -> tokens.add(new Token("&6", 0xFFFFAA00)); + case '7' -> tokens.add(new Token("&7", 0xFFAAAAAA)); + case '8' -> tokens.add(new Token("&8", 0xFF555555)); + case '9' -> tokens.add(new Token("&9", 0xFF5555FF)); + case 'a', 'A' -> tokens.add(new Token("&a", 0xFF55FF55)); + case 'b', 'B' -> tokens.add(new Token("&b", 0xFF55FFFF)); + case 'c', 'C' -> tokens.add(new Token("&c", 0xFFFF5555)); + case 'd', 'D' -> tokens.add(new Token("&d", 0xFFFF55FF)); + case 'e', 'E' -> tokens.add(new Token("&e", 0xFFFFFF55)); + case 'f', 'F' -> tokens.add(new Token("&f", 0xFFFFFFFF)); + default -> tokens.add(new Token("&" + color, TokenTypeColors.TEXT)); + } + } else { + sb.append(c); + } + } else { + sb.append(c); + } + } + if (sb.length() > 0) { + tokens.add(new Token(sb.toString(), TokenTypeColors.TEXT)); + } + return tokens; + } + + 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..71a5643 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/lexer/TokenTypeColors.java @@ -0,0 +1,23 @@ +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 SELECTION = 0xFF23437F; + 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..b4daf11 --- /dev/null +++ b/src/main/java/de/zonlykroks/advancedscripts/screen/ScriptEditScreen.java @@ -0,0 +1,616 @@ +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.DrawableHelper; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.BookEditScreen; +import net.minecraft.client.gui.screen.ingame.BookScreen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.PressableWidget; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.util.NarratorManager; +import net.minecraft.client.util.SelectionManager; +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.nbt.NbtList; +import net.minecraft.nbt.NbtString; +import net.minecraft.network.packet.c2s.play.BookUpdateC2SPacket; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Hand; +import org.apache.commons.lang3.mutable.MutableInt; + +import java.util.*; +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<>(); + private int scroll = 0; + private int cursorY = 0; + private int cursorX = 0; + + private int savedCursorY = -1; + private int savedCursorX = -1; + + private int tickCounter; + + private int keyCode = -1; + private long time; + + 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 -> { + String[] split = s.split("\n"); + for (String s1 : split) { + if (s1.equals(" ")) { + lines.add(""); + } else { + lines.add(s1); + } + } + }); + } + if (lines.isEmpty()) { + lines.add(""); + } + } + + @Override + protected void init() { + this.addDrawableChild( + new Button(DONE, this.width / 2 - 98 / 2, height - 55, () -> { + this.client.setScreen(null); + finalizeBook(); + }) + ); + this.addDrawableChild( + new Button(BOOK, this.width - 98 - 5, height - 20 - 5, () -> { + finalizeBook(); + this.client.setScreen(new BookEditScreen(player, itemStack, hand)); + }) + ); + } + + public static final Text DONE = Text.translatable("gui.done"); + public static final Text BOOK = Text.translatable("item.minecraft.book"); + + private static class Button extends PressableWidget { + private Runnable onPress; + + public Button(Text text, int x, int y, Runnable onPress) { + super(x, y, 98, 20, text); + visible = true; + this.onPress = onPress; + } + + @Override + public void onPress() { + onPress.run(); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + } + + @Override + public boolean isNarratable() { + return false; + } + } + + private void setClipboard(String clipboard) { + if (this.client != null) { + SelectionManager.setClipboard(this.client, clipboard); + } + + } + + private String getClipboard() { + return this.client != null ? SelectionManager.getClipboard(this.client) : ""; + } + + @Override + public void tick() { + super.tick(); + ++this.tickCounter; + if (keyCode != -1 && System.currentTimeMillis() - time > 500) { + key(keyCode); + } + } + + @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 - 63, TokenTypeColors.BACKGROUND); + + int lineNumberLength = textRenderer.getWidth(lines.size() + ""); + + boolean hasSelection = savedCursorY != -1 && savedCursorX != -1; + int minSelectionY = Math.min(cursorY, savedCursorY); + int maxSelectionY = Math.max(cursorY, savedCursorY); + int minSelectionX = (minSelectionY == maxSelectionY ? Math.min(cursorX, savedCursorX) : (minSelectionY == cursorY ? cursorX : savedCursorX)); + int maxSelectionX = (minSelectionY == maxSelectionY ? Math.max(cursorX, savedCursorX) : (maxSelectionY == cursorY ? cursorX : savedCursorX)); + + int lineNumberText = scroll + 1; + MutableInt lineNumber = new MutableInt(); + TextHandler textHandler = this.textRenderer.getTextHandler(); + for (int i = scroll; i < lines.size(); i++) { + String s = lines.get(i); + + if (lineNumber.getValue() * 9 + 25 > this.height - 66) { + break; + } + + if (s.isEmpty() && i == cursorY) { + drawCursor(matrices, 25 + lineNumberLength + 5, lineNumber.getValue() * 9 + 25, true); + } + + // Line number + int height = this.textRenderer.getWrappedLinesHeight(s, this.width - 50 - lineNumberLength - 5); + if (lineTooLong(s)) { + fill(matrices, 25 + lineNumberLength + 2, 25 + lineNumber.getValue() * 9, 25 + lineNumberLength + 3, 25 + lineNumber.getValue() * 9 + height, TokenTypeColors.ERROR); + } + 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); + AtomicInteger currentXIndex = new AtomicInteger(0); + for (Token token : tokens) { + int finalI = i; + 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 - 66) { + return; + } + + String line = token.text.substring(start, end); + int previousXIndex = currentXIndex.get(); + currentXIndex.addAndGet(line.length()); + + if (hasSelection) { + int x1 = x.get(); + int x2 = x.get() + textRenderer.getWidth(line); + + if (finalI == minSelectionY) { + if (minSelectionX > currentXIndex.get()) { + x2 = 0; + } else if (minSelectionX <= currentXIndex.get() && minSelectionX >= previousXIndex) { + int startInLine = minSelectionX - previousXIndex; + x1 += textRenderer.getWidth(line.substring(0, startInLine)); + } + } + if (finalI == maxSelectionY) { + if (maxSelectionX < previousXIndex) { + x2 = 0; + } else if (maxSelectionX <= currentXIndex.get()) { + int endInLine = maxSelectionX - previousXIndex; + x2 = x.get() + textRenderer.getWidth(line.substring(0, endInLine)); + } + } + + if (finalI >= minSelectionY && finalI <= maxSelectionY && x2 > x1) { + fill(matrices, x1, y + 25, x2, y + 25 + 9, TokenTypeColors.SELECTION); + } + } + + if (finalI == cursorY && currentXIndex.get() >= cursorX && previousXIndex <= cursorX) { + drawCursor(matrices, x.get() + textRenderer.getWidth(line.substring(0, cursorX - previousXIndex)) - 1, 25 + y, isAtEndOfLine()); + } + + 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); + } + + private boolean lineTooLong(String s) { + if (s.length() >= 1024) { + return true; + } + return textRenderer.getWrappedLinesHeight(s, 114) > 128; + } + + private void drawCursor(MatrixStack matrices, int x, int y, boolean atEnd) { + if (this.tickCounter / 6 % 2 == 0) { + if (!atEnd) { + Objects.requireNonNull(this.textRenderer); + DrawableHelper.fill(matrices, x, y - 1, x + 1, y + 9, 0xFFFFFFFF); + } else { + this.textRenderer.draw(matrices, "_", (float) x, (float) y, 0xFFFFFFFF); + } + } + + } + + private void key(int keyCode) { + if (Screen.isSelectAll(keyCode)) { + this.cursorX = 0; + this.cursorY = 0; + this.savedCursorX = lines.get(lines.size() - 1).length(); + this.savedCursorY = lines.size() - 1; + return; + } else if (Screen.isCopy(keyCode)) { + String copied = selection(false); + if (copied != null) { + setClipboard(copied); + } + return; + } else if (Screen.isPaste(keyCode)) { + String copied = getClipboard(); + if (copied != null) { + insert(copied); + } + return; + } else if (Screen.isCut(keyCode)) { + String copied = selection(true); + if (copied != null) { + setClipboard(copied); + } + return; + } + SelectionManager.SelectionType selectionType = Screen.hasControlDown() ? SelectionManager.SelectionType.WORD : SelectionManager.SelectionType.CHARACTER; + boolean valid = true; + int previousCursorX = cursorX; + int previousCursorY = cursorY; + switch (keyCode) { + case 257: + case 335: + selection(true); + newLine(); + valid = false; + break; + case 259: + if (selection(true) == null) backspace(selectionType); + valid = false; + break; + case 261: + if (selection(true) == null) delete(selectionType); + valid = false; + break; + case 262: + moveCursor(1, selectionType); + valid = Screen.hasShiftDown(); + break; + case 263: + moveCursor(-1, selectionType); + valid = Screen.hasShiftDown(); + break; + case 264: + moveDown(); + valid = Screen.hasShiftDown(); + break; + case 265: + moveUp(); + valid = Screen.hasShiftDown(); + break; + case 268: + cursorX = 0; + valid = Screen.hasShiftDown(); + break; + case 269: + cursorX = lines.get(cursorY).length(); + valid = Screen.hasShiftDown(); + break; + default: + break; + } + if (valid) { + if (Screen.hasShiftDown() && savedCursorX == -1 && savedCursorY == -1) { + savedCursorX = previousCursorX; + savedCursorY = previousCursorY; + } + } else { + savedCursorY = -1; + savedCursorX = -1; + } + autoScroll(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (super.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + key(keyCode); + this.keyCode = keyCode; + this.time = System.currentTimeMillis(); + return true; + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + this.keyCode = -1; + return super.keyReleased(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (super.charTyped(chr, modifiers)) { + return true; + } + selection(true); + boolean valid = insert(chr + ""); + savedCursorY = -1; + savedCursorX = -1; + autoScroll(); + return valid; + } + + private String selection(boolean remove) { + if (savedCursorX == -1 || savedCursorY == -1) { + return null; + } + int minSelectionY = Math.min(savedCursorY, cursorY); + int maxSelectionY = Math.max(savedCursorY, cursorY); + int minSelectionX = (minSelectionY == maxSelectionY ? Math.min(cursorX, savedCursorX) : (minSelectionY == cursorY ? cursorX : savedCursorX)); + int maxSelectionX = (minSelectionY == maxSelectionY ? Math.max(cursorX, savedCursorX) : (maxSelectionY == cursorY ? cursorX : savedCursorX)); + + StringBuilder builder = new StringBuilder(); + for (int i = minSelectionY; i <= maxSelectionY; i++) { + String line = lines.get(i); + if (i == minSelectionY && i == maxSelectionY) { + builder.append(line, minSelectionX, maxSelectionX); + } else if (i == minSelectionY) { + builder.append(line, minSelectionX, line.length()); + } else if (i == maxSelectionY) { + builder.append(line, 0, Math.min(maxSelectionX, line.length())); + } else { + builder.append(line); + } + if (i != maxSelectionY) { + builder.append("\n"); + } + } + if (remove) { + for (int i = maxSelectionY; i >= minSelectionY; i--) { + String line = lines.get(i); + if (i == minSelectionY && i == maxSelectionY) { + lines.set(i, line.substring(0, minSelectionX) + line.substring(maxSelectionX)); + } else if (i == minSelectionY) { + lines.set(i, line.substring(0, minSelectionX)); + } else if (i == maxSelectionY) { + lines.set(i, line.substring(Math.min(maxSelectionX, line.length()))); + } else { + lines.remove(i); + } + } + if (minSelectionY != maxSelectionY) { + lines.set(minSelectionY, lines.get(minSelectionY) + lines.get(minSelectionY + 1)); + lines.remove(minSelectionY + 1); + } + cursorX = minSelectionX; + cursorY = minSelectionY; + savedCursorX = -1; + savedCursorY = -1; + } + return builder.toString(); + } + + private boolean insert(String s) { + String[] split = s.split("\n"); + for (int i = 0; i < split.length; i++) { + String line = lines.get(cursorY); + if (cursorX == line.length()) { + line += split[i]; + } else { + line = line.substring(0, cursorX) + split[i] + line.substring(cursorX); + } + lines.set(cursorY, line); + cursorX += split[i].length(); + if (i != split.length - 1) { + newLine(); + } + } + return true; + } + + private boolean newLine() { + String line = lines.get(cursorY); + String newLine = line.substring(cursorX); + line = line.substring(0, cursorX); + lines.set(cursorY, line); + lines.add(cursorY + 1, newLine); + cursorY++; + cursorX = 0; + return true; + } + + private boolean backspace(SelectionManager.SelectionType selectionType) { + if (cursorX == 0) { + if (cursorY == 0) { + return true; + } + String previousLine = lines.get(cursorY - 1); + lines.set(cursorY - 1, lines.get(cursorY - 1) + lines.remove(cursorY)); + cursorY--; + cursorX = previousLine.length(); + } else { + String line = lines.get(cursorY); + int remove = selectionType == SelectionManager.SelectionType.CHARACTER ? 1 : getWordLength(line, cursorX, -1); + line = line.substring(0, cursorX - remove) + line.substring(cursorX); + lines.set(cursorY, line); + cursorX -= remove; + } + return true; + } + + private boolean delete(SelectionManager.SelectionType selectionType) { + if (cursorX == lines.get(cursorY).length()) { + if (cursorY == lines.size() - 1) { + return true; + } + String nextLine = lines.get(cursorY + 1); + lines.remove(cursorY); + lines.set(cursorY, lines.get(cursorY) + nextLine); + } else { + String line = lines.get(cursorY); + int remove = selectionType == SelectionManager.SelectionType.CHARACTER ? 1 : getWordLength(line, cursorX, 1); + line = line.substring(0, cursorX) + line.substring(cursorX + remove); + lines.set(cursorY, line); + } + return true; + } + + private int getWordLength(String line, int cursorX, int direction) { + int i = cursorX; + while (i >= 0 && i < line.length()) { + char c = line.charAt(i); + if (Character.isLetterOrDigit(c)) { + i += direction; + } else { + break; + } + } + return Math.abs(i - cursorX); + } + + private boolean moveCursor(int offset, SelectionManager.SelectionType selectionType) { + if (offset == 0) { + return true; + } + String line = lines.get(cursorY); + if (offset > 0) { + if (cursorX == line.length()) { + if (cursorY == lines.size() - 1) { + return true; + } + cursorY++; + cursorX = 0; + } else { + cursorX += selectionType == SelectionManager.SelectionType.CHARACTER ? 1 : getWordLength(line, cursorX, 1); + } + } else { + if (cursorX == 0) { + if (cursorY == 0) { + return true; + } + cursorY--; + cursorX = lines.get(cursorY).length(); + } else { + cursorX -= selectionType == SelectionManager.SelectionType.CHARACTER ? 1 : getWordLength(line, cursorX, -1); + } + } + return true; + } + + private boolean moveDown() { + if (cursorY == lines.size() - 1) { + cursorX = lines.get(cursorY).length(); + return true; + } + cursorY++; + cursorX = Math.min(cursorX, lines.get(cursorY).length()); + return true; + } + + private boolean moveUp() { + if (cursorY == 0) { + cursorX = 0; + return true; + } + cursorY--; + cursorX = Math.min(cursorX, lines.get(cursorY).length()); + return true; + } + + private void autoScroll() { + if (cursorY < scroll) { + scroll = cursorY; + } else if (cursorY >= scroll + ((this.height - 25 - 66) / 9)) { + scroll = cursorY - ((this.height - 25 - 66) / 9); + } + } + + private boolean isAtEndOfLine() { + return cursorX == lines.get(cursorY).length(); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + scroll -= Math.signum(amount); + if (scroll > lines.size() - 1) { + scroll = lines.size() - 1; + } + if (scroll < 0) { + scroll = 0; + } + return true; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return super.mouseClicked(mouseX, mouseY, button); + } + + private List toPages() { + List pages = new ArrayList<>(); + StringBuilder page = new StringBuilder(); + for (String line : lines) { + if (!page.isEmpty()) { + page.append("\n"); + } + if (page.length() + line.length() > 1024) { + pages.add(page.toString()); + page = new StringBuilder(); + } + String temp = page + line; + if (textRenderer.getWrappedLinesHeight(temp, 114) > 128) { + pages.add(page.toString()); + page = new StringBuilder(); + } + if (line.isEmpty()) { + page.append(" "); + } else { + page.append(line); + } + } + if (!page.isEmpty()) { + pages.add(page.toString()); + } + return pages; + } + + private void finalizeBook() { + List pages = toPages(); + this.writeNbtData(pages); + int i = this.hand == Hand.MAIN_HAND ? this.player.getInventory().selectedSlot : 40; + this.client.getNetworkHandler().sendPacket(new BookUpdateC2SPacket(i, pages, Optional.empty())); + } + + private void writeNbtData(List pages) { + NbtList nbtList = new NbtList(); + pages.stream().map(NbtString::of).forEach(nbtList::add); + if (!pages.isEmpty()) { + this.itemStack.setSubNbt("pages", nbtList); + } + } +} diff --git a/src/main/resources/advancedscripts.mixins.json b/src/main/resources/advancedscripts.mixins.json index a4b8220..4f89400 100644 --- a/src/main/resources/advancedscripts.mixins.json +++ b/src/main/resources/advancedscripts.mixins.json @@ -6,7 +6,9 @@ "mixins": [ ], "client": [ - "KeyboardMixin" + "KeyboardMixin", + "ClientPlayerEntityMixin", + "ClientPlayNetworkHandlerMixin" ], "injectors": { "defaultRequire": 1