Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

更新Chat #5

Merged
merged 7 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;

import io.github.sakurawald.Fuji;
import io.github.sakurawald.config.Configs;
import io.github.sakurawald.module.ModuleManager;
Expand All @@ -20,7 +21,10 @@
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.event.ClickCallback;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Formatter;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
Expand All @@ -38,12 +42,16 @@
import java.util.Arrays;
import java.util.Comparator;
import java.util.Queue;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;

public class ChatModule extends ModuleInitializer {

private static final Pattern xaero_waypoint_pattern = Pattern.compile(
"^xaero-waypoint:([^:]+):.+:(-?\\d+):(~?-?\\d*):(-?\\d+).*?-(.*)-waypoints$");
private static final Pattern pos_pattern = Pattern.compile("^xaero-waypoint:|pos");
private final MiniMessage miniMessage = MiniMessage.builder().build();
private final MainStatsModule mainStatsModule = ModuleManager.getInitializer(MainStatsModule.class);
@Getter
Expand Down Expand Up @@ -71,28 +79,103 @@ public void onReload() {
@Override
public void registerCommand(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext registryAccess, Commands.CommandSelection environment) {
dispatcher.register(
Commands.literal("chat")
.then(literal("format")
.then(argument("format", StringArgumentType.greedyString())
.executes(this::$format)
)));
Commands.literal("chat")
.then(Commands.literal("format")
.then(Commands.literal("reset")
.executes(this::resetFormat)
)
.then(Commands.literal("set")
.then(argument("format", StringArgumentType.greedyString())
.executes(this::format)
)
)
)
);
}

private int $format(CommandContext<CommandSourceStack> ctx) {

private int format(CommandContext<CommandSourceStack> ctx) {
return CommandUtil.playerOnlyCommand(ctx, player -> {
String name = player.getGameProfile().getName();
String format = StringArgumentType.getString(ctx, "format");
Configs.chatHandler.model().format.player2format.put(name, format);
Configs.chatHandler.saveToDisk();
format = MessageUtil.ofString(player,"chat.format.set").replace("%s",format);
Component formatComponent = miniMessage.deserialize(format, Formatter.date("date", LocalDateTime.now(ZoneId.systemDefault()))).asComponent();
Component component = formatComponent
.replaceText("%player%",Component.text(name))
.replaceText("%message%",MessageUtil.ofComponent(player, "chat.format.show"));
player.sendMessage(component);
return Command.SINGLE_SUCCESS;
});
}


private int resetFormat(CommandContext<CommandSourceStack> ctx)
{
return CommandUtil.playerOnlyCommand(ctx, player -> {
String name = player.getGameProfile().getName();
Configs.chatHandler.model().format.player2format.remove(name);
Configs.chatHandler.saveToDisk();
MessageUtil.sendMessage(player, "chat.format.reset");
return Command.SINGLE_SUCCESS;
});
}

private Component resolvePositionTag(ServerPlayer player, Component component) {
Component replacement = Component.text("%s (%d %d %d) %s".formatted(player.serverLevel().dimension().location(),
player.getBlockX(), player.getBlockY(), player.getBlockZ(), player.chunkPosition().toString())).color(NamedTextColor.GOLD);
return component.replaceText(TextReplacementConfig.builder().match("(?<=^|\\s)pos(?=\\s|$)").replacement(replacement).build());
String str = PlainTextComponentSerializer.plainText().serialize(component);
Matcher posmatcher = pos_pattern.matcher(str);
if (posmatcher.find()) {
String hoverText;
String click_command;
int x,z;
String y;
String dim_name;
Matcher xaeroMap_matcher = xaero_waypoint_pattern.matcher(str);
if (xaeroMap_matcher.find()) { //小地图路径点
hoverText = xaeroMap_matcher.group(1).replaceAll("^col^",":").replaceAll("^ast^","*"); //xaeroMap使用^col^代替:,^ast^代替*以确保解析正确
x = Integer.parseInt(xaeroMap_matcher.group(2));
y = xaeroMap_matcher.group(3);
z = Integer.parseInt(xaeroMap_matcher.group(4));
dim_name = xaeroMap_matcher.group(5).replaceFirst(".*\\$",""); //在部分自定义纬度,xaeroMap使用类似dim%minecraft$开头
click_command = str
.replaceFirst("xaero-waypoint","/xaero_waypoint_add") //小地图分享格式:xaero-waypoint:name:n:0:~:0:0:false:0:Internal-overworld-waypoints
.replaceAll(":Internal-",":Internal_") //小地图指令格式:/xaero_waypoint:name:n:0:~:0:0:false:0:Internal_overworld_waypoints
.replaceAll("-waypoints$","_waypoints");
} else {
hoverText = MessageUtil.ofString(player,"chat.current_pos");
dim_name = player.serverLevel().dimension().location().toString().replaceFirst("minecraft:","");
x = player.getBlockX();
y = Integer.toString(player.getBlockY());
z = player.getBlockZ();
click_command = MessageUtil.ofString(player,"chat.xaero_waypoint_add.command",x, y, z,dim_name.replaceAll(":","$"));
}
switch (dim_name) {
case "overworld":
hoverText += "\n"+MessageUtil.ofString(player,"the_nether")
+": %d %s %d\n".formatted(x/8, y, z/8);
break;
case "the_nether":
hoverText += "\n"+MessageUtil.ofString(player,"overworld")
+": %d %s %d\n".formatted(x*8, y, z*8);
break;
}
String dim_display_name;
if (MessageUtil.containsKey(player,dim_name)) {
dim_display_name = MessageUtil.ofString(player,dim_name);
} else {
dim_display_name = dim_name;
}
Component replacement = Component.text("[%d %s %d, %s]".formatted(x, y, z, dim_display_name))
.decoration(TextDecoration.ITALIC, true)
.clickEvent(ClickEvent.runCommand(click_command))
.hoverEvent(Component.text(hoverText).append(MessageUtil.ofComponent(player,"chat.xaero_waypoint_add")));
return component.replaceText(TextReplacementConfig.builder()
.match("^xaero-waypoint:.*|pos")
.replacement(replacement)
.build());
}
return component;
}

private Component resolveItemTag(ServerPlayer player, Component component) {
Expand All @@ -101,7 +184,7 @@ private Component resolveItemTag(ServerPlayer player, Component component) {
player.getMainHandItem().getDisplayName().asComponent()
.hoverEvent(MessageUtil.ofComponent(player, "display.click.prompt"))
.clickEvent(displayCallback(displayUUID));
return component.replaceText(TextReplacementConfig.builder().match("(?<=^|\\s)item(?=\\s|$)").replacement(replacement).build());
return component.replaceText(TextReplacementConfig.builder().match("item").replacement(replacement).build());
}

private Component resolveInvTag(ServerPlayer player, Component component) {
Expand All @@ -110,7 +193,7 @@ private Component resolveInvTag(ServerPlayer player, Component component) {
MessageUtil.ofComponent(player, "display.inventory.text")
.hoverEvent(MessageUtil.ofComponent(player, "display.click.prompt"))
.clickEvent(displayCallback(displayUUID));
return component.replaceText(TextReplacementConfig.builder().match("(?<=^|\\s)inv(?=\\s|$)").replacement(replacement).build());
return component.replaceText(TextReplacementConfig.builder().match("inv").replacement(replacement).build());
}

private Component resolveEnderTag(ServerPlayer player, Component component) {
Expand All @@ -119,7 +202,7 @@ private Component resolveEnderTag(ServerPlayer player, Component component) {
MessageUtil.ofComponent(player, "display.ender_chest.text")
.hoverEvent(MessageUtil.ofComponent(player, "display.click.prompt"))
.clickEvent(displayCallback(displayUUID));
return component.replaceText(TextReplacementConfig.builder().match("(?<=^|\\s)ender(?=\\s|$)").replacement(replacement).build());
return component.replaceText(TextReplacementConfig.builder().match("ender").replacement(replacement).build());
}

@NotNull
Expand All @@ -132,50 +215,93 @@ private ClickEvent displayCallback(String displayUUID) {
.uses(Integer.MAX_VALUE).build());
}

@SuppressWarnings("unused")
private String resolveMentionTag(ServerPlayer player, String str) {
private Component resolveMentionTag(ServerPlayer player, Component component) {
/* resolve player tag */
ArrayList<ServerPlayer> mentionedPlayers = new ArrayList<>();

String[] playerNames = Fuji.SERVER.getPlayerNames();
// fix: mention the longest name first
Arrays.sort(playerNames, Comparator.comparingInt(String::length).reversed());

String str = PlainTextComponentSerializer.plainText().serialize(component);
for (String playerName : playerNames) {
// here we must continue so that mentionPlayers will not be added
if (!str.contains(playerName)) continue;
str = str.replace(playerName, "<aqua>%s</aqua>".formatted(playerName));
Pattern pattern = Pattern.compile("(?:(?<=\\s)|^|@)" + Pattern.quote(playerName));
Matcher matcher = pattern.matcher(str);
if (!matcher.find()) continue;
component = component.replaceText(matcher.group(),
Component.text("@",NamedTextColor.GREEN)
.append(Component.text(playerName,NamedTextColor.DARK_GREEN)));
mentionedPlayers.add(Fuji.SERVER.getPlayerList().getPlayerByName(playerName));
}

/* run mention player task */
if (!mentionedPlayers.isEmpty()) {
MentionPlayersJob.scheduleJob(mentionedPlayers);
}
return str;

return component;

}

public Component resolveLinks(ServerPlayer player, Component component) {

String str = PlainTextComponentSerializer.plainText().serialize(component);

//BV号替换
Matcher bvmatcher = Pattern.compile("(?<=[^/]|^)BV\\w{10}",Pattern.CASE_INSENSITIVE).matcher(str);
while (bvmatcher.find()) {
String bvNumber = bvmatcher.group();
Component textBuilder = Component.text("bilibili")
.decoration(TextDecoration.UNDERLINED, true)
.hoverEvent(HoverEvent.showText(Component.text(bvNumber)))
.clickEvent(ClickEvent.openUrl("https://www.bilibili.com/video/"+bvNumber));
component = component.replaceText(bvNumber, textBuilder);
}

//网址替换
Matcher urlmatcher = Pattern.compile("(https?)://[^\\s/$.?#].[^\\s]*").matcher(str);
Pattern displayPattern = Pattern.compile("(?<=https?://)[^/\\s]{0,15}(?=\\.[^./]{0,20}(/|$))");
while (urlmatcher.find()) {
String url = urlmatcher.group();
Matcher displayMatcher = displayPattern.matcher(url);
String displayText = url;
if (displayMatcher.find()) {
displayText = displayMatcher.group().replaceFirst("www.|m.","");
}
Component textBuilder = Component.text(displayText)
.decoration(TextDecoration.UNDERLINED, true)
.hoverEvent(HoverEvent.showText(Component.text(url)))
.clickEvent(ClickEvent.openUrl(url));
component = component.replaceText(url, textBuilder);
}

return component;
}

public void broadcastChatMessage(ServerPlayer player, String message) {
/* resolve format */
message = Configs.chatHandler.model().format.player2format.getOrDefault(player.getGameProfile().getName(), message)
.replace("%message%", message);
message = resolveMentionTag(player, message);
String format = Configs.configHandler.model().modules.chat.format;
format = format.replace("%message%", message);
format = format.replace("%player%", player.getGameProfile().getName());

//输入文本处理
Component msgComponent = Component.text(message);
msgComponent = resolveLinks(player, msgComponent);
msgComponent = resolveMentionTag(player, msgComponent);
msgComponent = resolveItemTag(player, msgComponent);
msgComponent = resolveInvTag(player, msgComponent);
msgComponent = resolveEnderTag(player, msgComponent);
msgComponent = resolvePositionTag(player, msgComponent);

//玩家发言样式
String format = Configs.chatHandler.model().format.player2format.getOrDefault(
player.getGameProfile().getName(), //获取玩家名对应的样式
Configs.configHandler.model().modules.chat.format); //若无,使用默认样式
/* resolve stats */
if (mainStatsModule != null) {
MainStats stats = MainStats.uuid2stats.getOrDefault(player.getUUID().toString(), new MainStats());
format = stats.update(player).resolve(Fuji.SERVER, format);
}
Component formatComponent = miniMessage.deserialize(
format, Formatter.date("date", LocalDateTime.now(ZoneId.systemDefault()))).asComponent();
//合并
Component component = formatComponent
.replaceText("%player%",Component.text(player.getGameProfile().getName()))
.replaceText("%message%",msgComponent);

/* resolve tags */
Component component = miniMessage.deserialize(format, Formatter.date("date", LocalDateTime.now(ZoneId.systemDefault()))).asComponent();
component = resolveItemTag(player, component);
component = resolveInvTag(player, component);
component = resolveEnderTag(player, component);
component = resolvePositionTag(player, component);
chatHistory.add(component);
// info so that it can be seen in the console
Fuji.LOGGER.info(PlainTextComponentSerializer.plainText().serialize(component));
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/io/github/sakurawald/util/MessageUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ else if (audience instanceof CommandSourceStack source && source.getPlayer() !=
return formatString(value, args);
}

public boolean containsKey(Audience audience, String key) {
ServerPlayer player;
if (audience instanceof ServerPlayer) player = (ServerPlayer) audience;
else if (audience instanceof CommandSourceStack source && source.getPlayer() != null)
player = source.getPlayer();
else player = null;

/* get lang */
String lang;
if (player != null) {
lang = player2lang.getOrDefault(player.getGameProfile().getName(), DEFAULT_LANG);
} else {
lang = DEFAULT_LANG;
}
loadLanguageIfAbsent(lang);
JsonObject json;
json = lang2json.get(!lang2json.containsKey(lang) ? DEFAULT_LANG : lang);

return json.has(key);
}
public static String formatString(String string, Object... args) {
if (args.length > 0) {
return String.format(string, args);
Expand Down
13 changes: 11 additions & 2 deletions src/main/resources/assets/fuji/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"back": "<#FFA1F5>Back",
"empty": "<#FFA1F5>Empty",
"confirm": "<#FFA1F5>Confirm",
"overworld":"overworld",
"the_nether":"the nether",
"the_end":"the end",
"command.player_only": "<red>This command can only be executed by a player",
"level.no_exists": "<red>Level %s doesn't exist",
"input.syntax.error": "<red>Input Syntax Error",
Expand Down Expand Up @@ -162,5 +165,11 @@
"ping.player": "<gold>Ping of %s: %d ms",
"ping.target.no_found": "<red>Ping target no found.",
"bed.not_found": "<red>No bed found.",
"bed.success": "<gold>Teleported to bed."
}
"bed.success": "<gold>Teleported to bed.",
"chat.format.set": "<gold>Chat format has been modified, current style:</gold><newline>%s<reset><newline><red><click:run_command:/chat format reset> [Reset]</click></red>",
"chat.format.reset": "<gold>Chat format has been reset</gold>",
"chat.format.show": "Current style",
"chat.current_pos": "Current Position",
"chat.xaero_waypoint_add": "Click to add a Xaero's minimap waypoint",
"chat.xaero_waypoint_add.command": "/xaero_waypoint_add:Waypoint:·:%d:%s:%d:0:false:0:Internal_%s_waypoints"
}
13 changes: 11 additions & 2 deletions src/main/resources/assets/fuji/lang/zh_cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"back": "<#FFA1F5>返回",
"empty": "<#FFA1F5>空",
"confirm": "<#FFA1F5>确认",
"overworld":"主世界",
"the_nether":"下界",
"the_end":"末地",
"command.player_only": "<red>该命名只能由玩家执行",
"level.no_exists": "<red>维度 %s 不存在",
"input.syntax.error": "<red>输入的格式错误",
Expand Down Expand Up @@ -162,5 +165,11 @@
"ping.player": "<gold>玩家 %s 的延迟: %d ms",
"ping.target.no_found": "<red>目标玩家未找到",
"bed.not_found": "<red>未找到床",
"bed.success": "<gold>已传送到床"
}
"bed.success": "<gold>已传送到床",
"chat.format.set":"<gold>已修改聊天样式, 当前样式:</gold><newline>%s<reset><newline><red><click:run_command:/chat format reset> [重置]</click></red>",
"chat.format.reset":"<gold>已重置聊天样式</gold>",
"chat.format.show":"当前样式",
"chat.current_pos":"当前位置",
"chat.xaero_waypoint_add":"点击添加小地图路径点",
"chat.xaero_waypoint_add.command":"/xaero_waypoint_add:路径点:·:%d:%s:%d:0:false:0:Internal_%s_waypoints"
}
Loading