diff --git a/build.gradle.kts b/build.gradle.kts index 3814785..5fee651 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,11 +2,11 @@ import io.gitlab.jfronny.scripts.* plugins { id("jfmod") version "1.3-SNAPSHOT" + id("io.github.juuxel.loom-quiltflower") version "1.8.0" } dependencies { modImplementation("io.gitlab.jfronny.libjf:libjf-config-core-v1:${prop("libjf_version")}") - modImplementation("io.gitlab.jfronny.libjf:libjf-translate-v1:${prop("libjf_version")}") // Dev env modLocalRuntime("io.gitlab.jfronny.libjf:libjf-config-ui-tiny-v1:${prop("libjf_version")}") diff --git a/src/client/java/io/gitlab/jfronny/chattransform/Cfg.java b/src/client/java/io/gitlab/jfronny/chattransform/Cfg.java new file mode 100644 index 0000000..0867fc6 --- /dev/null +++ b/src/client/java/io/gitlab/jfronny/chattransform/Cfg.java @@ -0,0 +1,266 @@ +package io.gitlab.jfronny.chattransform; + +import io.gitlab.jfronny.commons.serialize.gson.api.v1.Ignore; +import io.gitlab.jfronny.libjf.config.api.v1.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +@JfConfig +public class Cfg { + @Entry public static Map substitutions = new LinkedHashMap<>(); + @Entry public static Mode mode = Mode.Live; + + public enum Mode { + Live, OnSend + } + + @Preset + public static void owo() { + substitutions.clear(); + substitutions.put("r", "w"); + substitutions.put("l", "w"); + substitutions.put("R", "W"); + substitutions.put("L", "W"); + substitutions.put("no", "nu"); + substitutions.put("has", "haz"); + substitutions.put("have", "haz"); + substitutions.put("you", "uu"); + substitutions.put("the ", "da "); + substitutions.put("The ", "Da "); + } + + @Ignore private static final String katakanaTable = """ + a i u e o n + ア イ ウ エ オ ン + x ァ ィ ゥ ェ ォ + k カ キ ク ケ コ + ky キャ キュ キョ + s サ ス セ ソ + sh シャ シ シュ シェ ショ + t タ ティ ツ テ ト + ts ツ + ch チャ チ チュ チェ チョ + n ナ ニ ヌ ネ ノ + ny ニャ ニュ ニョ + h ハ ヒ フ ヘ ホ + hy ヒャ ヒュ ヒョ + f ファ フィ フ フェ フォ + m マ ミ ム メ モ + my ミャ ミュ ミョ + y ヤ ユ ヨ + r ラ リ ル レ ロ + ry リャ リィ リュ リェ リョ + w ワ ウィ ウェ ヲ + g ガ ギ グ ゲ ゴ + gy ギャ ギュ ギョ + z ザ ズ ゼ ゾ/ヂョ + j ジャ/ヂャ ジ/ヂ ジュ/ヂュ ジェ ジョ + d ダ ヅ デ ド + b バ ビ ブ ベ ボ + by ビャ ビュ ビョ + p パ ピ プ ペ ポ + py ピャ ピュ ピョ + v ゔ"""; + + @Ignore private static final String hiraganaTable = """ + a i u e o n + あ い う え お ん + x ぁ ぃ ぅ ぇ ぉ + k か き く け こ + ky きゃ きゅ きょ + s さ す せ そ + sh しゃ し しゅ しょ + t た つ て と + ts つ + ch ちゃ ち ちゅ ちぇ ちょ + n な に ぬ ね の + ny にゃ にゅ にょ + h は ひ ふ へ ほ + hy ひゃ ひゅ ひょ + f ふ + m ま み む め も + my みゃ みゅ みょ + y や ゆ よ + r ら り る れ ろ + ry りゃ りぃ りゅ りぇ りょ + w わ ゐ ゑ を + g が ぎ ぐ げ ご + gy ぎゃ ぎゅ ぎょ + z ざ ず ぜ ぞ/ぢょ + j じゃ/ぢゃ じ/ぢ じゅ/ぢゅ じょ + d だ づ で ど + b ば び ぶ べ ぼ + by びゃ びゅ びょ + p ぱ ぴ ぷ ぺ ぽ + py ぴゃ ぴゅ ぴょ + v ゔ"""; + + @Ignore private static final char[] consonants = "bcdfghjklmprstwz".toCharArray(); + + private static void fromTable(String table) { + String[] rows = table.split("\n"); + String[] colNames = rows[0].split("\t"); + for (int i = 1; i < rows.length; i++) { + String row = rows[i]; + String[] cols = row.split("\t"); + String rowName = cols[0]; + for (int j = 1; j < cols.length; j++) { + String kana = cols[j]; + String value = rowName + colNames[j]; + String[] kanas = kana.split("/"); + for (String singleKana : kanas) { + if (!singleKana.isEmpty()) { + substitutions.put(value, singleKana); + } + } + } + } + } + + @Preset + public static void katakana() { + substitutions.clear(); + substitutions.put("-", "ー"); + // replaceTsus ッ + for (char c : consonants) { + substitutions.put("" + c + c, "ッ" + c); + } + // replaceNs ン + substitutions.put("nn", "ン"); + // romajiToKatakanaTrie.convert + fromTable(katakanaTable); +// substitutions.put("che", "チェ"); +// substitutions.put(".", "。"); +// substitutions.put("shu", "シュ"); +// substitutions.put("sha", "シャ"); +// substitutions.put("\"", "「"); +// substitutions.put("\"", "」"); +// substitutions.put("sho", "ショ"); +// substitutions.put("jo", "ジョ"); +// substitutions.put("ⅴi", "ヴィ"); +// substitutions.put("ju", "ジュ"); +// substitutions.put("ja", "ジャ"); +// substitutions.put("ve", "ヴェ"); +// substitutions.put("ryo", "リョ"); +// substitutions.put("ryu", "リュ"); +// substitutions.put("rya", "リャ"); +// substitutions.put("cho", "チョ"); +// substitutions.put("chu", "チュ"); +// substitutions.put("cha", "チャ"); +// substitutions.put("hyo", "ヒョ"); +// substitutions.put("hyu", "ヒュ"); +// substitutions.put("hya", "ヒャ"); +// substitutions.put("tsi", "ツィ"); +// substitutions.put("dyo", "ヂョ"); +// substitutions.put("dyu", "ヂュ"); +// substitutions.put("dya", "ヂャ"); +// substitutions.put("fi", "フィ"); +// substitutions.put("byo", "ビョ"); +// substitutions.put("byu", "ビュ"); +// substitutions.put("fe", "フェ"); +// substitutions.put("bya", "ビャ"); +// substitutions.put("nyo", "ニョ"); +// substitutions.put("nyu", "ニュ"); +// substitutions.put("nya", "ニャ"); +// substitutions.put("pya", "ピャ"); +// substitutions.put("pyo", "ピョ"); +// substitutions.put("pyu", "ピュ"); +// substitutions.put("ti", "ティ"); +// substitutions.put("a", "ア"); +// substitutions.put("i", "イ"); +// substitutions.put("u", "ウ"); +// substitutions.put("e", "エ"); +// substitutions.put("o", "オ"); +// substitutions.put("ka", "カ"); +// substitutions.put("ga", "ガ"); +// substitutions.put("ki", "キ"); +// substitutions.put("gi", "ギ"); +// substitutions.put("ku", "ク"); +// substitutions.put("gu", "グ"); +// substitutions.put("ke", "ケ"); +// substitutions.put("ge", "ゲ"); +// substitutions.put("ko", "コ"); +// substitutions.put("go", "ゴ"); +// substitutions.put("sa", "サ"); +// substitutions.put("za", "ザ"); +// substitutions.put("shi", "シ"); +// substitutions.put("ji", "ジ"); +// substitutions.put("su", "ス"); +// substitutions.put("zu", "ズ"); +// substitutions.put("di", "ディ"); +// substitutions.put("se", "セ"); +// substitutions.put("wi", "ウィ"); +// substitutions.put("ze", "ゼ"); +// substitutions.put("so", "ソ"); +// substitutions.put("zo", "ゾ"); +// substitutions.put("ta", "タ"); +// substitutions.put("da", "ダ"); +// substitutions.put("chi", "チ"); +// substitutions.put("ji", "ヂ"); +// substitutions.put("tsu", "ツ"); +// substitutions.put("zu", "ヅ"); +// substitutions.put("te", "テ"); +// substitutions.put("de", "デ"); +// substitutions.put("we", "ウェ"); +// substitutions.put("to", "ト"); +// substitutions.put("do", "ド"); +// substitutions.put("na", "ナ"); +// substitutions.put("ni", "ニ"); +// substitutions.put("nu", "ヌ"); +// substitutions.put("ne", "ネ"); +// substitutions.put("no", "ノ"); +// substitutions.put("ha", "ハ"); +// substitutions.put("ba", "バ"); +// substitutions.put("kya", "キャ"); +// substitutions.put("pa", "パ"); +// substitutions.put("hi", "ヒ"); +// substitutions.put("bi", "ビ"); +// substitutions.put("pi", "ピ"); +// substitutions.put("fu", "フ"); +// substitutions.put("bu", "ブ"); +// substitutions.put("pu", "プ"); +// substitutions.put("he", "ヘ"); +// substitutions.put("be", "ベ"); +// substitutions.put("pe", "ペ"); +// substitutions.put("ho", "ホ"); +// substitutions.put("bo", "ボ"); +// substitutions.put("kyo", "キョ"); +// substitutions.put("po", "ポ"); +// substitutions.put("ma", "マ"); +// substitutions.put("kyu", "キュ"); +// substitutions.put("mi", "ミ"); +// substitutions.put("mu", "ム"); +// substitutions.put("me", "メ"); +// substitutions.put("mo", "モ"); +// substitutions.put("ya", "ヤ"); +// substitutions.put("yu", "ユ"); +// substitutions.put("yo", "ヨ"); +// substitutions.put("ra", "ラ"); +// substitutions.put("ri", "リ"); +// substitutions.put("ru", "ル"); +// substitutions.put("re", "レ"); +// substitutions.put("ro", "ロ"); +// substitutions.put("wa", "ワ"); +// substitutions.put("gyu", "ギュ"); +// substitutions.put("wo", "ヲ"); +// substitutions.put("n", "ン"); +// substitutions.put("gya", "ギャ"); +// substitutions.put("-", "ー"); +// substitutions.put("gyo", "ギョ"); + } + + @Preset + public static void hiragana() { + substitutions.clear(); + substitutions.put("-", "ー"); + // replaceTsus っ + for (char c : consonants) { + substitutions.put("" + c + c, "っ" + c); + } + // replaceNs ん + substitutions.put("nn", "ん"); + // romajiToKatakanaTrie.convert + fromTable(hiraganaTable); + } +} diff --git a/src/client/java/io/gitlab/jfronny/chattransform/ChatTransform.java b/src/client/java/io/gitlab/jfronny/chattransform/ChatTransform.java new file mode 100644 index 0000000..dc9c301 --- /dev/null +++ b/src/client/java/io/gitlab/jfronny/chattransform/ChatTransform.java @@ -0,0 +1,13 @@ +package io.gitlab.jfronny.chattransform; + +import io.gitlab.jfronny.commons.log.Logger; +import net.fabricmc.api.ClientModInitializer; + +public class ChatTransform implements ClientModInitializer { + public static final Logger LOG = Logger.forName("chat-transform"); + + @Override + public void onInitializeClient() { + LOG.info("Loaded chat-transform"); + } +} diff --git a/src/client/java/io/gitlab/jfronny/chattransform/ITextFieldWidget.java b/src/client/java/io/gitlab/jfronny/chattransform/ITextFieldWidget.java new file mode 100644 index 0000000..3690188 --- /dev/null +++ b/src/client/java/io/gitlab/jfronny/chattransform/ITextFieldWidget.java @@ -0,0 +1,6 @@ +package io.gitlab.jfronny.chattransform; + +public interface ITextFieldWidget { + void chattransform$activate(); + String chattransform$finalize(); +} diff --git a/src/client/java/io/gitlab/jfronny/chattransform/mixin/ChatScreenMixin.java b/src/client/java/io/gitlab/jfronny/chattransform/mixin/ChatScreenMixin.java new file mode 100644 index 0000000..9dea623 --- /dev/null +++ b/src/client/java/io/gitlab/jfronny/chattransform/mixin/ChatScreenMixin.java @@ -0,0 +1,44 @@ +package io.gitlab.jfronny.chattransform.mixin; + +import io.gitlab.jfronny.chattransform.Cfg; +import io.gitlab.jfronny.chattransform.ITextFieldWidget; +import net.minecraft.client.gui.screen.ChatScreen; +import net.minecraft.client.gui.widget.TextFieldWidget; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Mixin(ChatScreen.class) +public abstract class ChatScreenMixin { + @Shadow protected TextFieldWidget chatField; + + @Inject(at = @At("RETURN"), method = "init()V") + void init(CallbackInfo ci) { + if (Cfg.mode == Cfg.Mode.Live) ((ITextFieldWidget) chatField).chattransform$activate(); + } + + @Redirect(method = "keyPressed(III)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/TextFieldWidget;getText()Ljava/lang/String;")) + String finalizeTransforms(TextFieldWidget instance) { + String str = ((ITextFieldWidget) chatField).chattransform$finalize(); + if (Cfg.mode == Cfg.Mode.OnSend) { + for (Map.Entry e : Cfg.substitutions + .entrySet() + .stream() + .collect(Collectors.groupingBy(s -> s.getKey().length())) + .entrySet() + .stream() + .sorted(Map.Entry.>>comparingByKey().reversed()) + .map(Map.Entry::getValue) + .flatMap(Collection::stream) + .toList()) { + str = str.replaceAll(Pattern.quote(e.getKey()), e.getValue()); + } + } + return str; + } +} diff --git a/src/client/java/io/gitlab/jfronny/chattransform/mixin/TextFieldWidgetMixin.java b/src/client/java/io/gitlab/jfronny/chattransform/mixin/TextFieldWidgetMixin.java new file mode 100644 index 0000000..ecc3530 --- /dev/null +++ b/src/client/java/io/gitlab/jfronny/chattransform/mixin/TextFieldWidgetMixin.java @@ -0,0 +1,141 @@ +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); + } + } +} diff --git a/src/client/resources/assets/chat-transform/lang/en_us.json b/src/client/resources/assets/chat-transform/lang/en_us.json new file mode 100644 index 0000000..32f937c --- /dev/null +++ b/src/client/resources/assets/chat-transform/lang/en_us.json @@ -0,0 +1,9 @@ +{ + "chat-transform.jfconfig.title": "Chat-Transform", + "chat-transform.jfconfig.substitutions": "Substitutions", + "chat-transform.jfconfig.substitutions.tooltip": "The substitutions to perform on text sent in the chat. See the presets for examples", + "chat-transform.jfconfig.mode": "Mode", + "chat-transform.jfconfig.mode.tooltip": "When to perform replacements", + "chat-transform.jfconfig.enum.Mode.Live": "Live", + "chat-transform.jfconfig.enum.Mode.OnSend": "On Send" +} \ No newline at end of file diff --git a/src/main/resources/chat-transform.mixins.json b/src/client/resources/chat-transform.client.mixins.json similarity index 80% rename from src/main/resources/chat-transform.mixins.json rename to src/client/resources/chat-transform.client.mixins.json index 7378614..426ff91 100644 --- a/src/main/resources/chat-transform.mixins.json +++ b/src/client/resources/chat-transform.client.mixins.json @@ -3,9 +3,9 @@ "minVersion": "0.8", "package": "io.gitlab.jfronny.chattransform.mixin", "compatibilityLevel": "JAVA_17", - "mixins": [ - ], "client": [ + "ChatScreenMixin", + "TextFieldWidgetMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/java/io/gitlab/jfronny/chattransform/Chat_transform.java b/src/main/java/io/gitlab/jfronny/chattransform/Chat_transform.java deleted file mode 100644 index 6c41f88..0000000 --- a/src/main/java/io/gitlab/jfronny/chattransform/Chat_transform.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.gitlab.jfronny.chattransform; - -import net.fabricmc.api.ModInitializer; - -public class Chat_transform implements ModInitializer { - @Override - public void onInitialize() { - - } -} diff --git a/src/main/java/io/gitlab/jfronny/chattransform/client/Chat_transformClient.java b/src/main/java/io/gitlab/jfronny/chattransform/client/Chat_transformClient.java deleted file mode 100644 index c78806e..0000000 --- a/src/main/java/io/gitlab/jfronny/chattransform/client/Chat_transformClient.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.gitlab.jfronny.chattransform.client; - -import net.fabricmc.api.ClientModInitializer; - -public class Chat_transformClient implements ClientModInitializer { - @Override - public void onInitializeClient() { - - } -} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 924487a..aca31ae 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -2,23 +2,22 @@ "schemaVersion": 1, "id": "chat-transform", "version": "${version}", - "name": "chat-transform", + "name": "Chat-Transform", "description": "", "authors": [], "contact": {}, "license": "MIT", "icon": "assets/chat-transform/icon.png", - "environment": "*", + "environment": "client", "entrypoints": { - "client": [ - "io.gitlab.jfronny.chattransform.client.Chat_transformClient" - ], - "main": [ - "io.gitlab.jfronny.chattransform.Chat_transform" - ] + "client": ["io.gitlab.jfronny.chattransform.ChatTransform"], + "libjf:config": ["io.gitlab.jfronny.chattransform.JFC_Cfg"] }, "mixins": [ - "chat-transform.mixins.json" + { + "config": "chat-transform.client.mixins.json", + "environment": "client" + } ], "depends": { "fabricloader": ">=0.14.17",