package io.gitlab.jfronny.chattransform.mixin; import io.gitlab.jfronny.chattransform.*; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawableHelper; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.Map; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @Mixin(TextFieldWidget.class) public abstract class TextFieldWidgetMixin extends ClickableWidget implements ITextFieldWidget { public TextFieldWidgetMixin(int x, int y, int width, int height, Text message) { super(x, y, width, height, message); } @Shadow public abstract String getText(); @Shadow private String text; @Shadow private int selectionStart; @Shadow private int selectionEnd; @Shadow private boolean drawsBackground; @Shadow public abstract int getCharacterX(int index); @Shadow public abstract boolean isVisible(); @Shadow public abstract void setCursor(int cursor); @Shadow private Predicate textPredicate; @Shadow @Final private TextRenderer textRenderer; @Unique private boolean chattransform$active = false; @Override public void chattransform$activate() { chattransform$active = true; ChatTransform.LOG.info("Activated widget " + this); } @Unique private boolean chattransform$shouldTransform = false; @Redirect(method = "write(Ljava/lang/String;)V", at = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/widget/TextFieldWidget;text:Ljava/lang/String;", opcode = Opcodes.PUTFIELD)) void transformBeforeWrite(TextFieldWidget instance, String value, String tx) { if (chattransform$active) { if (selectionStart == selectionEnd && tx.length() == 1 && !Screen.hasAltDown() && !getText().startsWith("/")) { chattransform$shouldTransform = true; } else { transformStart.clear(); } } this.text = value; } @Inject(method = "write(Ljava/lang/String;)V", at = @At("TAIL")) void transformAfterWrite(String text, CallbackInfo ci) { if (chattransform$shouldTransform) { chattransform$shouldTransform = false; transform(); } } @Inject(method = "setCursor(I)V", at = @At("TAIL")) void updateStartOnSetCursor(int cursor, CallbackInfo ci) { transformStart.clear(); lastSubstitution = null; } @Unique private final TransformStart transformStart = new TransformStart(); @Unique private Substitution lastSubstitution = null; void transform() { if (!transformStart.isAvailable()) transformStart.set(selectionStart - 1); if (transformStart.get() >= selectionStart) return; String currentString = getText().substring(transformStart.get(), selectionStart); Set> complete = getStartingWith(currentString); // Exact match if (Cfg.substitutions.containsKey(currentString) && complete.size() == 1 && substitute(transformStart.get(), selectionStart, Cfg.substitutions.get(currentString))) { transformStart.set(selectionStart); return; } if (complete.isEmpty()) { if (transformStart.get() == selectionStart - 1) { // Nothing starts with this char transformStart.increment(); } else { // Something previously started with this... String previousString = getText().substring(transformStart.get(), selectionStart - 1); if (Cfg.substitutions.containsKey(previousString) // ...and matched -> replace && substitute(transformStart.get(), selectionStart - 1, Cfg.substitutions.get(previousString))) { setCursor(selectionStart + 1); } else { // ...and didn't match -> move transform start and call transform again (substring might have matched) transformStart.increment(); transform(); } } } } @Override public String chattransform$finalize() { String str = getText(); if (!transformStart.isAvailable() || transformStart.get() >= selectionStart) return str; String currentString = str.substring(transformStart.get(), selectionStart); if (!Cfg.substitutions.containsKey(currentString) || !substitute(transformStart.get(), selectionStart, Cfg.substitutions.get(currentString))) { transformStart.increment(); return chattransform$finalize(); } return str; } boolean substitute(int start, int end, String substitution) { ChatTransform.LOG.info("Transforming " + getText().substring(start, end) + " to " + substitution); String sub = text.substring(0, start) + substitution + text.substring(end); if (textPredicate.test(sub)) { this.text = sub; int oldLen = end - start; int newLen = substitution.length(); if (selectionStart > end) selectionStart += newLen - oldLen; else if (selectionStart > start) selectionStart = start + newLen; selectionEnd = selectionStart; lastSubstitution = new Substitution(start, start + newLen); return true; } else return false; } Set> getStartingWith(String start) { return Cfg.substitutions.entrySet().stream().filter(s -> s.getKey().startsWith(start)).collect(Collectors.toUnmodifiableSet()); } @Inject(method = "renderButton(Lnet/minecraft/client/util/math/MatrixStack;IIF)V", at = @At(value = "TAIL")) void renderTransformStart(MatrixStack matrices, int mouseX, int mouseY, float delta, CallbackInfo ci) { if (isVisible() && Cfg.visualize) { int y = this.drawsBackground ? this.getY() + (this.height - 8) / 2 : this.getY(); if (transformStart.isAvailable()) { int x = getCharacterX(transformStart.get()); chattransform$fill(matrices, x, y - 1, x + 1, y + 1 + 9, 0x7f0000ff); } if (transformStart.showPrevious()) { int x = getCharacterX(transformStart.getPrevious()); chattransform$fill(matrices, x, y - 1, x + 1, y + 1 + 9, 0x7fff0000); } if (lastSubstitution != null && lastSubstitution.shouldShow()) { int start = getCharacterX(lastSubstitution.start()); int end = getCharacterX(Math.min(lastSubstitution.end() + 1, text.length())); chattransform$fill(matrices, start, y - 1, end, y + 1 + 9, 0x7fffff00); } } } @Unique private void chattransform$fill(MatrixStack matrices, int x1, int y1, int x2, int y2, int color) { if (x1 < 0) x1 = 0; if (x2 < 0) x2 = 0; int maxX = getX() + getWidth(); if (x1 > maxX) x1 = maxX; if (x2 > maxX) x2 = maxX; if (x1 < x2) { int i = x1; x1 = x2; x2 = i; } if (y1 < y2) { int i = y1; y1 = y2; y2 = i; } textRenderer.draw(matrices, "X={" + x1 + ";" + x2 + "} Y={" + y1 + ";" + y2 + "}", 0, 0, 0xFF000000); fill(matrices, x1, y1, x2, y2, color); } }