package io.gitlab.jfronny.chattransform.mixin; import io.gitlab.jfronny.chattransform.*; 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; @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 { chattransform$start = null; } } 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) { chattransform$start = null; } @Unique private Integer chattransform$start = null; void transform() { if (chattransform$start == null) chattransform$start = selectionStart - 1; if (chattransform$start < 0) chattransform$start = 0; if (chattransform$start >= selectionStart) return; String currentString = getText().substring(chattransform$start, selectionStart); Set> complete = getStartingWith(currentString); // Exact match if (Cfg.substitutions.containsKey(currentString) && complete.size() == 1 && substitute(chattransform$start, selectionStart, Cfg.substitutions.get(currentString))) { chattransform$start = selectionStart; return; } if (complete.isEmpty()) { if (chattransform$start == selectionStart - 1) { // Nothing starts with this char chattransform$start++; } else { // Something previously started with this... String previousString = getText().substring(chattransform$start, selectionStart - 1); if (Cfg.substitutions.containsKey(previousString) // ...and matched -> replace && substitute(chattransform$start, 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) chattransform$start++; transform(); } } } } @Override public String chattransform$finalize() { String str = getText(); if (chattransform$start == null || chattransform$start >= selectionStart) return str; String currentString = str.substring(chattransform$start, selectionStart); if (!Cfg.substitutions.containsKey(currentString) || !substitute(chattransform$start, selectionStart, Cfg.substitutions.get(currentString))) { chattransform$start++; 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 -= oldLen + newLen; else if (selectionStart > start) selectionStart = start + newLen; selectionEnd = selectionStart; 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() && chattransform$start != null) { int x = getCharacterX(chattransform$start); int y = this.drawsBackground ? this.getY() + (this.height - 8) / 2 : this.getY(); DrawableHelper.fill(matrices, x, y - 1, x + 1, y + 1 + 9, 0x7f0000ff); } } }