server side support
ci/woodpecker/push/jfmod Pipeline failed Details
ci/woodpecker/tag/jfmod Pipeline failed Details

This commit is contained in:
Johannes Frohnmeyer 2023-03-22 20:06:25 +01:00
parent d07a8dabd4
commit d8cd4ccf08
Signed by: Johannes
GPG Key ID: E76429612C2929F4
15 changed files with 308 additions and 138 deletions

View File

@ -7,6 +7,9 @@ plugins {
dependencies {
modImplementation("io.gitlab.jfronny.libjf:libjf-config-core-v1:${prop("libjf_version")}")
include(modImplementation(fabricApi.module("fabric-message-api-v1", prop("fabric_version")))!!)
include(modImplementation(fabricApi.module("fabric-command-api-v2", prop("fabric_version")))!!)
modImplementation(fabricApi.module("fabric-networking-api-v1", prop("fabric_version")))
// Dev env
modLocalRuntime("io.gitlab.jfronny.libjf:libjf-config-ui-tiny-v1:${prop("libjf_version")}")

View File

@ -1,13 +0,0 @@
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

@ -1,4 +1,4 @@
package io.gitlab.jfronny.chattransform;
package io.gitlab.jfronny.chattransform.client;
public interface ITextFieldWidget {
void chattransform$activate();

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.chattransform;
package io.gitlab.jfronny.chattransform.client;
public record Substitution(int start, int end, long time) {
public Substitution(int start, int end) {

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.chattransform;
package io.gitlab.jfronny.chattransform.client;
public class TransformStart {
private boolean available = false;

View File

@ -0,0 +1,27 @@
package io.gitlab.jfronny.chattransform.client.mixin;
import io.gitlab.jfronny.chattransform.*;
import io.gitlab.jfronny.chattransform.client.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;
@Mixin(ChatScreen.class)
public abstract class ChatScreenMixin {
@Shadow protected TextFieldWidget chatField;
@Inject(at = @At("RETURN"), method = "init()V")
void init(CallbackInfo ci) {
if (Cfg.Client.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 text = ((ITextFieldWidget) chatField).chattransform$finalize();
if (Cfg.Client.mode == Cfg.Mode.OnSend) text = ChatTransform.transform(text);
return text;
}
}

View File

@ -1,8 +1,8 @@
package io.gitlab.jfronny.chattransform.mixin;
package io.gitlab.jfronny.chattransform.client.mixin;
import io.gitlab.jfronny.chattransform.*;
import io.gitlab.jfronny.chattransform.client.*;
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;

View File

@ -1,44 +0,0 @@
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

@ -2,10 +2,20 @@
"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.visualize": "Visualize",
"chat-transform.jfconfig.visualize.tooltip": "Visualize transformations as you type in live mode",
"chat-transform.jfconfig.client.title": "Client Settings",
"chat-transform.jfconfig.client.mode": "Mode",
"chat-transform.jfconfig.client.mode.tooltip": "When to perform replacements",
"chat-transform.jfconfig.enum.Mode.Live": "Live",
"chat-transform.jfconfig.enum.Mode.OnSend": "On Send"
"chat-transform.jfconfig.enum.Mode.OnSend": "On Send",
"chat-transform.jfconfig.client.visualize": "Visualize",
"chat-transform.jfconfig.client.visualize.tooltip": "Visualize transformations as you type in live mode",
"chat-transform.jfconfig.server.title": "Server Settings",
"chat-transform.jfconfig.server.messageOnFirstConnect": "Message on first connect",
"chat-transform.jfconfig.server.messageOnFirstConnect.tooltip": "Whether to send a chat message to players about chat-transform when they first connect",
"chat-transform.jfconfig.server.playerStates": "Player States",
"chat-transform.jfconfig.server.playerStates.tooltip": "For which players messages should be transformed",
"chat-transform.jfconfig.server.playerConfigurable": "Player Configurable",
"chat-transform.jfconfig.server.playerConfigurable.tooltip": "Whether players should be able to toggle chat transformation",
"chat-transform.jfconfig.server.defaultEnable": "Default Enable",
"chat-transform.jfconfig.server.defaultEnable.tooltip": "Whether players' chat should be transformed by default"
}

View File

@ -1,7 +1,7 @@
{
"required": true,
"minVersion": "0.8",
"package": "io.gitlab.jfronny.chattransform.mixin",
"package": "io.gitlab.jfronny.chattransform.client.mixin",
"compatibilityLevel": "JAVA_17",
"client": [
"ChatScreenMixin",

View File

@ -0,0 +1,78 @@
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.*;
@JfConfig
public class Cfg {
@Entry public static Map<String, String> substitutions = Map.of();
@Category
public static class Client {
@Entry public static Mode mode = Mode.Live;
@Entry public static boolean visualize = true;
}
@Category
public static class Server {
@Entry public static boolean messageOnFirstConnect = true;
@Entry public static Map<String, Boolean> playerStates = new HashMap<>();
@Entry public static boolean playerConfigurable = true;
@Entry public static boolean defaultEnable = true;
}
public enum Mode {
Live, OnSend
}
@Preset
public static void owo() {
substitutions = new LinkedHashMap<>();
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 ");
}
@Preset
public static void demonstrationPurposesOnly() {
substitutions = new LinkedHashMap<>();
substitutions.put("chat-transform", "Chat-Transform (this mod)");
substitutions.put(":tm:", "");
substitutions.put("(c)", "©");
}
@Ignore private static final char[] consonants = "bcdfghjklmprstwz".toCharArray();
@Preset
public static void katakana() {
substitutions = new LinkedHashMap<>();
substitutions.put("-", "");
for (char c : consonants) {
substitutions.put("" + c + c, "" + c);
}
substitutions.put("nn", "");
JapaneseMap.fromTable(JapaneseMap.katakanaTable, substitutions::put);
System.out.println("katakana " + substitutions);
}
@Preset
public static void hiragana() {
substitutions = new LinkedHashMap<>();
substitutions.put("-", "");
for (char c : consonants) {
substitutions.put("" + c + c, "" + c);
}
substitutions.put("nn", "");
JapaneseMap.fromTable(JapaneseMap.hiraganaTable, substitutions::put);
System.out.println("hiragana " + substitutions);
}
}

View File

@ -0,0 +1,28 @@
package io.gitlab.jfronny.chattransform;
import io.gitlab.jfronny.commons.log.Logger;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class ChatTransform {
public static final String MOD_ID = "chat-transform";
public static final Logger LOG = Logger.forName(MOD_ID);
public static String transform(String text) {
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()) {
text = text.replaceAll(Pattern.quote(e.getKey()), e.getValue());
}
return text;
}
}

View File

@ -1,45 +1,9 @@
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.function.BiConsumer;
import java.util.LinkedHashMap;
import java.util.Map;
@JfConfig
public class Cfg {
@Entry public static Map<String, String> substitutions = Map.of();
@Entry public static Mode mode = Mode.Live;
@Entry public static boolean visualize = true;
public enum Mode {
Live, OnSend
}
@Preset
public static void owo() {
substitutions = new LinkedHashMap<>();
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 ");
}
@Preset
public static void demonstrationPurposesOnly() {
substitutions = new LinkedHashMap<>();
substitutions.put("chat-transform", "Chat-Transform (this mod)");
substitutions.put(":tm:", "");
substitutions.put("(c)", "©");
}
@Ignore private static final String katakanaTable = """
public class JapaneseMap {
public static final String katakanaTable = """
a i u e o n
x
@ -72,7 +36,7 @@ public class Cfg {
py ピャ ピュ ピョ
v """;
@Ignore private static final String hiraganaTable = """
public static final String hiraganaTable = """
a i u e o n
x
@ -105,9 +69,7 @@ public class Cfg {
py ぴゃ ぴゅ ぴょ
v """;
@Ignore private static final char[] consonants = "bcdfghjklmprstwz".toCharArray();
private static void fromTable(String table) {
public static void fromTable(String table, BiConsumer<String, String> target) {
String[] rows = table.split("\n");
String[] colNames = rows[0].split("\t");
for (int i = 1; i < rows.length; i++) {
@ -120,34 +82,10 @@ public class Cfg {
String[] kanas = kana.split("/");
for (String singleKana : kanas) {
if (!singleKana.isEmpty()) {
substitutions.put(value, singleKana);
target.accept(value, singleKana);
}
}
}
}
}
@Preset
public static void katakana() {
substitutions = new LinkedHashMap<>();
substitutions.put("-", "");
for (char c : consonants) {
substitutions.put("" + c + c, "" + c);
}
substitutions.put("nn", "");
fromTable(katakanaTable);
System.out.println("katakana " + substitutions);
}
@Preset
public static void hiragana() {
substitutions = new LinkedHashMap<>();
substitutions.put("-", "");
for (char c : consonants) {
substitutions.put("" + c + c, "" + c);
}
substitutions.put("nn", "");
fromTable(hiraganaTable);
System.out.println("hiragana " + substitutions);
}
}

View File

@ -0,0 +1,143 @@
package io.gitlab.jfronny.chattransform.server;
import com.mojang.brigadier.Command;
import io.gitlab.jfronny.chattransform.Cfg;
import io.gitlab.jfronny.chattransform.ChatTransform;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigInstance;
import net.fabricmc.api.DedicatedServerModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.message.v1.ServerMessageDecoratorEvent;
import net.fabricmc.fabric.api.networking.v1.ServerLoginConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.network.packet.s2c.play.ChatMessageS2CPacket;
import net.minecraft.network.packet.s2c.play.GameMessageS2CPacket;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.*;
import org.spongepowered.asm.mixin.Mutable;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import static net.minecraft.server.command.CommandManager.*;
public class ChatTransformServer implements DedicatedServerModInitializer {
public static final ConfigInstance CONFIG_INSTANCE = Objects.requireNonNull(ConfigInstance.get(ChatTransform.MOD_ID));
@Override
public void onInitializeServer() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
var root = literal(ChatTransform.MOD_ID).executes(context -> {
context.getSource().sendMessage(Text.literal(ChatTransform.MOD_ID + " is " + (get(context.getSource().getPlayer()) ? "enabled" : "disabled") + " for you."));
return Command.SINGLE_SUCCESS;
});
root.then(literal("opt-in").requires(src -> Cfg.Server.playerConfigurable).executes(context -> {
if (context.getSource().getPlayer() == null) {
context.getSource().sendError(Text.literal("You are not a player."));
return Command.SINGLE_SUCCESS;
}
if (get(context.getSource().getPlayer())) {
context.getSource().sendError(Text.literal("You are already opted in to using chat-transform."));
return Command.SINGLE_SUCCESS;
}
set(context.getSource().getPlayer(), true);
context.getSource().sendMessage(Text.literal("Toggled " + ChatTransform.MOD_ID + " on"));
return Command.SINGLE_SUCCESS;
}));
root.then(literal("opt-out").requires(src -> Cfg.Server.playerConfigurable).executes(context -> {
if (context.getSource().getPlayer() == null) {
context.getSource().sendError(Text.literal("You are not a player."));
return Command.SINGLE_SUCCESS;
}
if (!get(context.getSource().getPlayer())) {
context.getSource().sendError(Text.literal("You are already opted out of using chat-transform."));
return Command.SINGLE_SUCCESS;
}
set(context.getSource().getPlayer(), false);
context.getSource().sendMessage(Text.literal("Toggled " + ChatTransform.MOD_ID + " off"));
return Command.SINGLE_SUCCESS;
}));
root.then(literal("reset").requires(src -> Cfg.Server.playerConfigurable).executes(context -> {
if (context.getSource().getPlayer() == null) {
context.getSource().sendError(Text.literal("You are not a player."));
return Command.SINGLE_SUCCESS;
}
reset(context.getSource().getPlayer());
context.getSource().sendMessage(Text.literal("Reset " + ChatTransform.MOD_ID + " to " + (Cfg.Server.defaultEnable ? "on" : "off")));
return Command.SINGLE_SUCCESS;
}));
root.then(literal("reset-all").requires(src -> src.hasPermissionLevel(4)).executes(context -> {
Cfg.Server.playerStates.clear();
CONFIG_INSTANCE.write();
context.getSource().sendMessage(Text.literal("Reset all player states"));
return Command.SINGLE_SUCCESS;
}));
dispatcher.register(root);
});
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
if (Cfg.Server.messageOnFirstConnect && !has(handler.getPlayer())) {
set(handler.getPlayer(), Cfg.Server.defaultEnable);
MutableText text = Text.literal("This server uses " + ChatTransform.MOD_ID + " to transform messages you send.");
if (Cfg.Server.playerConfigurable) {
text.append(" To turn it " + (Cfg.Server.defaultEnable ? "off" : "on") + ", use ");
String command = "/" + ChatTransform.MOD_ID + " opt-" + (Cfg.Server.defaultEnable ? "out" : "in");
text.append(Text.literal(command)
.styled(s -> s.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command)))
);
text.append(".");
}
handler.getPlayer().sendMessage(text);
}
});
ServerMessageDecoratorEvent.EVENT.register(ServerMessageDecoratorEvent.CONTENT_PHASE, (sender, message) -> {
return CompletableFuture.completedFuture(sender != null && get(sender) ? transform(message) : message);
});
}
private boolean has(ServerPlayerEntity player) {
return Cfg.Server.playerStates.containsKey(player.getUuidAsString());
}
private boolean get(ServerPlayerEntity player) {
return Cfg.Server.playerStates.computeIfAbsent(player.getUuidAsString(), k -> Cfg.Server.defaultEnable);
}
private void set(ServerPlayerEntity player, boolean value) {
Cfg.Server.playerStates.put(player.getUuidAsString(), value);
CONFIG_INSTANCE.write();
}
private void reset(ServerPlayerEntity player) {
Cfg.Server.playerStates.remove(player.getUuidAsString());
CONFIG_INSTANCE.write();
}
private Text transform(Text source) {
MutableText result = MutableText.of(transform(source.getContent()))
.setStyle(source.getStyle());
for (Text sibling : source.getSiblings()) {
result.append(transform(sibling));
}
return result;
}
private TextContent transform(TextContent source) {
if (source instanceof TranslatableTextContent tx) {
Object[] args = tx.getArgs();
args = Arrays.copyOf(args, args.length);
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Text tx1) args[i] = transform(tx1);
else if (args[i] instanceof TextContent tx1) args[i] = transform(tx1);
else if (args[i] instanceof String tx1) args[i] = ChatTransform.transform(tx1);
else args[i] = args[i];
}
return new TranslatableTextContent(tx.getKey(), ChatTransform.transform(tx.getFallback()), args);
} else if (source instanceof LiteralTextContent tx) {
return new LiteralTextContent(ChatTransform.transform(tx.string()));
} else {
return source;
}
}
}

View File

@ -13,9 +13,9 @@
},
"license": "MIT",
"icon": "assets/chat-transform/icon.png",
"environment": "client",
"environment": "*",
"entrypoints": {
"client": ["io.gitlab.jfronny.chattransform.ChatTransform"],
"server": ["io.gitlab.jfronny.chattransform.server.ChatTransformServer"],
"libjf:config": ["io.gitlab.jfronny.chattransform.JFC_Cfg"]
},
"mixins": [