Rebuild Logic & RGB Color Supported

The text format is now composed by the mod itself, rather than the game.
This commit is contained in:
APairOfMoons 2026-06-30 23:06:42 +08:00
parent b6f91ae87f
commit f160ea6dc4
13 changed files with 501 additions and 50 deletions

View File

@ -2,8 +2,8 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import java.nio.file.Paths import java.nio.file.Paths
plugins { plugins {
id "net.fabricmc.fabric-loom-remap" version "1.16-SNAPSHOT" apply false id "net.fabricmc.fabric-loom-remap" version "1.17-SNAPSHOT" apply false
id "net.fabricmc.fabric-loom" version "1.16-SNAPSHOT" apply false id "net.fabricmc.fabric-loom" version "1.17-SNAPSHOT" apply false
id "maven-publish" id "maven-publish"
// Manifold preprocessor // Manifold preprocessor

View File

@ -6,15 +6,15 @@ org.gradle.parallel=true
version_properties_path=./properties version_properties_path=./properties
# Mod Properties # Mod Properties
mod_version=1.1.0 mod_version=1.2.0
maven_group=cn.revaria.chatplus maven_group=cn.revaria.chatplus
archives_base_name=chat-plus archives_base_name=chat-plus
# Minecraft # Minecraft
minecraft_version=26.1 minecraft_version=26.2
# Override Fabric API version for local testing (runClient/runServer) # Override Fabric API version for local testing (runClient/runServer)
#test_fabric_api_version= #test_fabric_api_version=
# Dependencies # Dependencies
manifold_version=2025.1.9 manifold_version=2025.1.25

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -0,0 +1,24 @@
java_version=25
# Fabric Loom
# - For Minecraft versions <26.1: enable remap
# - For Minecraft versions >=26.1: disable remap
fabric_loom_remap=false
# Fabric Properties
# check these on https://fabricmc.net/develop
fabric_loader_version=0.19.3
min_fabric_loader_version=
fabric_api_version=0.152.2
# Fabric API mod ID conventions:
# - For Minecraft versions <1.19.2: use "fabric"
# - For Minecraft versions >=1.19.2: use "fabric-api"
# Ref: https://wiki.fabricmc.net/tutorial:setup
fabric_api_mod_id=fabric-api
# Mod Compatibility
! Dynmap 3.8
mod_dynmap_version=4UHHD4t6
! Styled Chat 2.13.0+26.2
mod_styled_chat_version=2.13.0+26.2
mod_styled_chat_placeholder_api_version=3.1.0-beta.1+26.2

View File

@ -0,0 +1,7 @@
package cn.revaria.chatplus.format;
public interface ChatFormat {
int startingIndex();
ChatFormatType formatType();
ChatFormat copy();
}

View File

@ -0,0 +1,8 @@
package cn.revaria.chatplus.format;
public enum ChatFormatType {
COLOR,
FONT,
INSERT,
RESET
}

View File

@ -0,0 +1,78 @@
package cn.revaria.chatplus.format.formats;
import cn.revaria.chatplus.format.ChatFormat;
import cn.revaria.chatplus.format.ChatFormatType;
import java.util.Map;
public class ChatColorFormat implements ChatFormat {
private final int R, G, B;
private final int startingIndex;
private final ChatFormatType formatType = ChatFormatType.COLOR;
public ChatColorFormat(int startingIndex, int R, int G, int B) {
this.startingIndex = startingIndex;
this.R = R;
this.G = G;
this.B = B;
}
public ChatColorFormat(int startingIndex, char colorCode) {
this.startingIndex = startingIndex;
if (VANILLA_COLOR_FORMATS.containsKey(colorCode)) {
this.R = VANILLA_COLOR_FORMATS.get(colorCode).getR();
this.G = VANILLA_COLOR_FORMATS.get(colorCode).getG();
this.B = VANILLA_COLOR_FORMATS.get(colorCode).getB();
}
else {
this.R = 0;
this.G = 0;
this.B = 0;
}
}
public int getR() {
return R;
}
public int getG() {
return G;
}
public int getB() {
return B;
}
@Override
public int startingIndex() {
return this.startingIndex;
}
@Override
public ChatFormatType formatType() {
return this.formatType;
}
@Override
public ChatColorFormat copy() {
return new ChatColorFormat(this.startingIndex, this.R, this.G, this.B);
}
public static final Map<Character, ChatColorFormat> VANILLA_COLOR_FORMATS = Map.ofEntries(
Map.entry('0', new ChatColorFormat(0, 0, 0, 0)),
Map.entry('1', new ChatColorFormat(0, 0, 0, 170)),
Map.entry('2', new ChatColorFormat(0, 0, 170, 0)),
Map.entry('3', new ChatColorFormat(0, 0, 170, 170)),
Map.entry('4', new ChatColorFormat(0, 170, 0, 0)),
Map.entry('5', new ChatColorFormat(0, 170, 0, 170)),
Map.entry('6', new ChatColorFormat(0, 255, 170, 0)),
Map.entry('7', new ChatColorFormat(0, 170, 170, 170)),
Map.entry('8', new ChatColorFormat(0, 85, 85, 85)),
Map.entry('9', new ChatColorFormat(0, 85, 85, 255)),
Map.entry('a', new ChatColorFormat(0, 85, 255, 85)),
Map.entry('b', new ChatColorFormat(0, 85, 255, 255)),
Map.entry('c', new ChatColorFormat(0, 255, 85, 85)),
Map.entry('d', new ChatColorFormat(0, 255, 85, 255)),
Map.entry('e', new ChatColorFormat(0, 255, 255, 85)),
Map.entry('f', new ChatColorFormat(0, 255, 255, 255))
);
}

View File

@ -0,0 +1,51 @@
package cn.revaria.chatplus.format.formats;
import cn.revaria.chatplus.format.ChatFormat;
import cn.revaria.chatplus.format.ChatFormatType;
import net.minecraft.ChatFormatting;
import java.util.Map;
public class ChatFontFormat implements ChatFormat {
private final ChatFormatting fontFormat;
private final int startingIndex;
private final ChatFormatType formatType = ChatFormatType.FONT;
public ChatFontFormat(int startingIndex, ChatFormatting fontFormat) {
this.startingIndex = startingIndex;
this.fontFormat = fontFormat;
}
public ChatFontFormat(int startingIndex, char fontCode) {
this.startingIndex = startingIndex;
this.fontFormat = VANILLA_FONT_FORMATS.getOrDefault(fontCode, ChatFormatting.ITALIC);
}
public ChatFormatting getFont() {
return fontFormat;
}
@Override
public int startingIndex() {
return startingIndex;
}
@Override
public ChatFormatType formatType() {
return formatType;
}
@Override
public ChatFontFormat copy() {
return new ChatFontFormat(this.startingIndex, this.fontFormat);
}
public static final Map<Character, ChatFormatting> VANILLA_FONT_FORMATS = Map.ofEntries(
Map.entry('k', ChatFormatting.OBFUSCATED),
Map.entry('l', ChatFormatting.BOLD),
Map.entry('m', ChatFormatting.STRIKETHROUGH),
Map.entry('n', ChatFormatting.UNDERLINE),
Map.entry('o', ChatFormatting.ITALIC)
);
}

View File

@ -0,0 +1,36 @@
package cn.revaria.chatplus.format.formats;
import cn.revaria.chatplus.format.ChatFormat;
import cn.revaria.chatplus.format.ChatFormatType;
import net.minecraft.world.item.ItemStack;
public class ChatInsertItemFormat implements ChatFormat {
private final ItemStack item;
private final int startingIndex;
private final ChatFormatType formatType = ChatFormatType.INSERT;
public ChatInsertItemFormat(int startingIndex, ItemStack item) {
this.startingIndex = startingIndex;
this.item = item;
}
public ItemStack getItem() {
return item;
}
@Override
public int startingIndex() {
return startingIndex;
}
@Override
public ChatFormatType formatType() {
return formatType;
}
@Override
public ChatInsertItemFormat copy() {
return new ChatInsertItemFormat(this.startingIndex, this.item);
}
}

View File

@ -0,0 +1,27 @@
package cn.revaria.chatplus.format.formats;
import cn.revaria.chatplus.format.ChatFormat;
import cn.revaria.chatplus.format.ChatFormatType;
public class ChatResetFormat implements ChatFormat {
private final int startingIndex;
private final ChatFormatType formatType = ChatFormatType.RESET;
public ChatResetFormat(int startingIndex) {
this.startingIndex = startingIndex;
}
@Override
public int startingIndex() {
return startingIndex;
}
@Override
public ChatFormatType formatType() {
return formatType;
}
@Override
public ChatResetFormat copy() {
return new ChatResetFormat(this.startingIndex);
}
}

View File

@ -3,30 +3,26 @@ package cn.revaria.chatplus.util;
#if MC_VER <= MC_1_20 #if MC_VER <= MC_1_20
import net.minecraft.network.chat.contents.LiteralContents; import net.minecraft.network.chat.contents.LiteralContents;
#else #else
import net.minecraft.network.chat.contents.PlainTextContents; import cn.revaria.chatplus.format.ChatFormat;
import cn.revaria.chatplus.format.ChatFormatType;
import cn.revaria.chatplus.format.formats.ChatColorFormat;
import cn.revaria.chatplus.format.formats.ChatFontFormat;
import cn.revaria.chatplus.format.formats.ChatInsertItemFormat;
import cn.revaria.chatplus.format.formats.ChatResetFormat;
#endif #endif
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.MutableComponent;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import java.util.ArrayDeque; import java.util.*;
import java.util.Deque;
import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class TextStyleFormatter { public class TextStyleFormatter {
public static int MAIN_HAND = -1; // Must be smaller than or equal to 0
/** /**
* Processes text styling with two key functions: *
* <p>
* 1. Replaces {@code &} with {@code §}, using {@code &&} to escape literal {@code &}<br>
* 2. Substitutes {@code [item]} with main-hand item hover text and {@code [item=N]} with Nth slot's hover text
* </p>
* Recursively processes nested text components.
* *
* @param sourceText Original text to process (supports nested text components) * @param sourceText Original text to process (supports nested text components)
* @param sourcePlayer Player used for item stack references * @param sourcePlayer Player used for item stack references
@ -35,46 +31,271 @@ public class TextStyleFormatter {
public static MutableComponent applyStyle(Component sourceText, ServerPlayer sourcePlayer) { public static MutableComponent applyStyle(Component sourceText, ServerPlayer sourcePlayer) {
MutableComponent sourceMutableText = sourceText.copy(); MutableComponent sourceMutableText = sourceText.copy();
String sourceRawText = sourceMutableText.toString().substring(8, sourceMutableText.toString().length() - 1);
// These will be motified in translateInput()
TreeMap<Integer, ArrayList<ChatFormat>> formatsTable = new TreeMap<>();
String sourceLiteralText = translateInput(sourceRawText, sourcePlayer, formatsTable);
/* Debug
sourcePlayer.sendSystemMessage(Component.literal(sourceLiteralText));
for (int i : formatsTable.keySet()) {
ArrayList<ChatFormat> formatsList = formatsTable.get(i);
sourcePlayer.sendSystemMessage(Component.literal(i + "."));
for (ChatFormat format : formatsList) {
if (format instanceof ChatInsertItemFormat) {
sourcePlayer.sendSystemMessage(Component.literal(" 物品插入数据"));
}
if (format instanceof ChatResetFormat) {
sourcePlayer.sendSystemMessage(Component.literal(" 清除"));
}
if (format instanceof ChatColorFormat colorFormat) {
sourcePlayer.sendSystemMessage(Component.literal(" 颜色:" + colorFormat.getR() + " " + colorFormat.getG() + " " + colorFormat.getB()));
}
if (format instanceof ChatFontFormat fontFormat) {
sourcePlayer.sendSystemMessage(Component.literal(" 字体").withStyle(fontFormat.getFont()));
}
}
}*/
MutableComponent finalText = Component.empty().setStyle(sourceMutableText.getStyle()); MutableComponent finalText = Component.empty().setStyle(sourceMutableText.getStyle());
ChatColorFormat currentColor = new ChatColorFormat(0, 'f');
HashSet<ChatFontFormat> currentFonts = new HashSet<>();
int startIndex = 0;
if (sourceText.getContents() instanceof #if MC_VER <= MC_1_20 LiteralContents #else PlainTextContents #endif plainTextContent) { for (int i : formatsTable.keySet()) {
String changedMessage = plainTextContent.text()
.replace('&', '§')
.replace("§§", "&");
String regex = "\\[item(?:=([1-9]))?\\]";
String[] messages = changedMessage.split(regex, -1);
Deque<Integer> itemDeque = new ArrayDeque<>();
Matcher matcher = Pattern.compile(regex).matcher(changedMessage); ArrayList<ChatFormat> formatsList = formatsTable.get(i);
while (matcher.find()) {
String digit = matcher.group(1); if (formatsList.getFirst() instanceof ChatInsertItemFormat insertItem) {
if (digit == null) {
itemDeque.addLast(MAIN_HAND); if (i > 0) {
} else { finalText.append(composeChatComponent(
itemDeque.addLast(Integer.parseInt(digit)); sourceLiteralText.substring(startIndex, i),
currentColor,
currentFonts
));
}
finalText.append(insertItem.getItem().getDisplayName());
startIndex = i;
continue;
}
if (formatsList.getFirst() instanceof ChatResetFormat) {
if (i > 0) {
finalText.append(composeChatComponent(
sourceLiteralText.substring(startIndex, i),
currentColor,
currentFonts
));
}
currentColor = new ChatColorFormat(0, 'f');
currentFonts.clear();
startIndex = i;
continue;
}
if (i > 0) {
finalText.append(composeChatComponent(
sourceLiteralText.substring(startIndex, i),
currentColor,
currentFonts
));
}
for (ChatFormat format : formatsList) {
if (format instanceof ChatColorFormat colorFormat) {
currentColor = colorFormat;
}
else if (format instanceof ChatFontFormat fontFormat) {
currentFonts.add(fontFormat);
} }
} }
for (String message : messages) { startIndex = i;
finalText.append(Component.literal(message));
if (!itemDeque.isEmpty()) {
ItemStack itemStack;
if (itemDeque.getFirst() == MAIN_HAND) {
itemStack = sourcePlayer.getMainHandItem();
} else {
itemStack = sourcePlayer.getInventory().getItem(itemDeque.getFirst() - 1);
}
finalText.append(itemStack.getDisplayName());
itemDeque.removeFirst();
}
}
} }
if (startIndex < sourceLiteralText.length() - 1) {
List<Component> sourceTexts = sourceMutableText.getSiblings(); finalText.append(composeChatComponent(
for (Component text : sourceTexts) { sourceLiteralText.substring(startIndex, sourceLiteralText.length()),
finalText.append(applyStyle(text, sourcePlayer)); currentColor,
currentFonts
));
} }
return finalText; return finalText;
} }
private static MutableComponent composeChatComponent(String chatText, ChatColorFormat currentColor, HashSet<ChatFontFormat> currentFonts) {
if (currentColor == null) {
currentColor = new ChatColorFormat(0, 'f');
}
MutableComponent finalChatComponent = Component.literal(chatText);
finalChatComponent.withColor(currentColor.getR() << 16 | currentColor.getG() << 8 | currentColor.getB());
for (ChatFontFormat fontFormat : currentFonts) {
finalChatComponent.withStyle(fontFormat.getFont());
}
return finalChatComponent;
}
private static String translateInput(String sourceRawText, ServerPlayer sourcePlayer, TreeMap<Integer, ArrayList<ChatFormat>> formatsTableOutput) {
StringBuilder textBuilder = new StringBuilder();
String itemRegex = "\\[item(?:=([1-9]))?\\]";
Pattern itemPattern = Pattern.compile(itemRegex);
Matcher matcher = itemPattern.matcher(sourceRawText);
for (int i = 0; i < sourceRawText.length(); ++i) {
char character = sourceRawText.charAt(i);
int currentIndex = textBuilder.length();
if (character == '&' && i < sourceRawText.length() - 1) {
char colorCode = sourceRawText.charAt(i + 1);
if (ChatColorFormat.VANILLA_COLOR_FORMATS.containsKey(colorCode)) {
if (!formatsTableOutput.containsKey(currentIndex)) {
formatsTableOutput.put(currentIndex, new ArrayList<>());
}
formatsTableOutput.get(currentIndex).add(new ChatColorFormat(currentIndex, colorCode));
++i;
continue;
}
}
if (character == '#' && i < sourceRawText.length() - 6) {
boolean legalColorCode = true;
for (int j = i + 1; j <= i + 6 && j < sourceRawText.length(); ++j) {
char c = sourceRawText.charAt(j);
if (!((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))) {
legalColorCode = false;
break;
}
}
if (legalColorCode) {
if (!formatsTableOutput.containsKey(currentIndex)) {
formatsTableOutput.put(currentIndex, new ArrayList<>());
}
int color = Integer.parseInt(sourceRawText.substring(i + 1, i + 7), 16);
formatsTableOutput.get(currentIndex).add(new ChatColorFormat(
currentIndex,
(color >> 16) & 0xFF,
(color >> 8) & 0xFF,
color & 0xFF)
);
i += 6;
continue;
}
}
if (character == '&' && i < sourceRawText.length() - 1) {
char fontCode = sourceRawText.charAt(i + 1);
if (ChatFontFormat.VANILLA_FONT_FORMATS.containsKey(fontCode)) {
if (!formatsTableOutput.containsKey(currentIndex)) {
formatsTableOutput.put(currentIndex, new ArrayList<>());
}
formatsTableOutput.get(currentIndex).add(new ChatFontFormat(currentIndex, fontCode));
++i;
continue;
}
}
if (character == '[' && i < sourceRawText.length() - 5) {
matcher.region(i, Math.min(i + 8, sourceRawText.length()));
if (matcher.lookingAt()) {
if (!formatsTableOutput.containsKey(currentIndex)) {
formatsTableOutput.put(currentIndex, new ArrayList<>());
}
String digit = matcher.group(1);
if (digit == null) {
formatsTableOutput.get(currentIndex).add(new ChatInsertItemFormat(
currentIndex,
sourcePlayer.getMainHandItem()
));
}
else {
formatsTableOutput.get(currentIndex).add(new ChatInsertItemFormat(
currentIndex,
sourcePlayer.getInventory().getItem(Integer.parseInt(digit) - 1)
));
}
while (sourceRawText.charAt(i) != ']' && i < sourceRawText.length()) {
++i;
}
continue;
}
}
if (character == '&' && i < sourceRawText.length() - 1) {
if (sourceRawText.charAt(i + 1) == 'r') {
if (!formatsTableOutput.containsKey(currentIndex)) {
formatsTableOutput.put(currentIndex, new ArrayList<>());
}
formatsTableOutput.get(currentIndex).add(new ChatResetFormat(currentIndex));
++i;
continue;
}
}
textBuilder.append(character);
}
formatsTableOutput.forEach((i, formatsList) -> {
boolean containsResetFormat = false;
ChatInsertItemFormat containedInsertItemFormat = null;
for (ChatFormat format : formatsList) {
if (format.formatType() == ChatFormatType.RESET) {
containsResetFormat = true;
break;
}
if (format instanceof ChatInsertItemFormat insertItemFormat && format.formatType() == ChatFormatType.INSERT) {
containedInsertItemFormat = insertItemFormat.copy();
break;
}
}
if (containsResetFormat) {
formatsList.clear();
formatsList.add(new ChatResetFormat(i));
}
if (containedInsertItemFormat != null) {
formatsList.clear();
formatsList.add(containedInsertItemFormat);
}
});
return textBuilder.toString();
}
} }

View File

@ -4,7 +4,6 @@
"plugin": "cn.revaria.chatplus.plugin.MixinConfigPlugin", "plugin": "cn.revaria.chatplus.plugin.MixinConfigPlugin",
"mixins": [ "mixins": [
"ChatMixin", "ChatMixin",
"compat.DynmapMixin",
"compat.StyledChatMixin" "compat.StyledChatMixin"
], ],
"injectors": { "injectors": {