functional prototype

This commit is contained in:
Johannes Frohnmeyer 2023-03-18 19:19:00 +01:00
parent f62046ade0
commit d332c850af
Signed by: Johannes
GPG Key ID: E76429612C2929F4
11 changed files with 490 additions and 32 deletions

View File

@ -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")}")

View File

@ -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<String, String> 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);
}
}

View File

@ -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");
}
}

View File

@ -0,0 +1,6 @@
package io.gitlab.jfronny.chattransform;
public interface ITextFieldWidget {
void chattransform$activate();
String chattransform$finalize();
}

View File

@ -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<String, String> e : Cfg.substitutions
.entrySet()
.stream()
.collect(Collectors.groupingBy(s -> s.getKey().length()))
.entrySet()
.stream()
.sorted(Map.Entry.<Integer, List<Map.Entry<String, String>>>comparingByKey().reversed())
.map(Map.Entry::getValue)
.flatMap(Collection::stream)
.toList()) {
str = str.replaceAll(Pattern.quote(e.getKey()), e.getValue());
}
}
return str;
}
}

View File

@ -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<String> 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<Map.Entry<String, String>> 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<Map.Entry<String, String>> 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);
}
}
}

View File

@ -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"
}

View File

@ -3,9 +3,9 @@
"minVersion": "0.8",
"package": "io.gitlab.jfronny.chattransform.mixin",
"compatibilityLevel": "JAVA_17",
"mixins": [
],
"client": [
"ChatScreenMixin",
"TextFieldWidgetMixin"
],
"injectors": {
"defaultRequire": 1

View File

@ -1,10 +0,0 @@
package io.gitlab.jfronny.chattransform;
import net.fabricmc.api.ModInitializer;
public class Chat_transform implements ModInitializer {
@Override
public void onInitialize() {
}
}

View File

@ -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() {
}
}

View File

@ -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",