From 296ba254ac69b2af5a3bab1d8256f880a7a8dac9 Mon Sep 17 00:00:00 2001 From: XiaoMoMi Date: Fri, 11 Apr 2025 20:52:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9B=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bukkit/loader/src/main/resources/config.yml | 6 +- .../bukkit/font/BukkitFontManager.java | 178 +++++++++--------- .../core/font/AbstractFontManager.java | 13 +- .../core/plugin/config/Config.java | 8 +- .../core/util/AdventureHelper.java | 24 +++ 5 files changed, 131 insertions(+), 98 deletions(-) diff --git a/bukkit/loader/src/main/resources/config.yml b/bukkit/loader/src/main/resources/config.yml index 5efdfb664..3198c7265 100644 --- a/bukkit/loader/src/main/resources/config.yml +++ b/bukkit/loader/src/main/resources/config.yml @@ -237,8 +237,8 @@ gui: performance: # Maximum chain update depth when fixing client visuals max-block-chain-update-limit: 64 - # Maximum emoji parsed each time - max-emoji-parsed-limit: 100 + # Maximum number of emojis to parse per operation + max-emojis-per-parse: 16 light-system: enable: true @@ -249,7 +249,7 @@ chunk-system: # 1 = NONE | Compression Speed | Decompress Speed | Compression Ratio | Memory Usage | # 2 = DEFLATE | Medium-Slow Medium Moderate Low | # 3 = GZIP | Medium-Slow Medium Moderate Low | - # 4 = LAZ4 | Blazing-Fast Blazing-Fast Low Low | + # 4 = LZ4 | Blazing-Fast Blazing-Fast Low Low | # 5 = ZSTD | Medium-Fast Fast High Medium | compression-method: 4 # Disabling this option prevents the plugin from converting custom blocks to vanilla states when chunks are unloaded. diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitFontManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitFontManager.java index 6b3a6a440..0ffafddc4 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitFontManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitFontManager.java @@ -1,7 +1,7 @@ package net.momirealms.craftengine.bukkit.font; import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; +import com.google.gson.JsonObject; import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent; import io.papermc.paper.event.player.AsyncChatDecorateEvent; import net.kyori.adventure.text.Component; @@ -36,10 +36,10 @@ import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.view.AnvilView; +import org.jetbrains.annotations.NotNull; import java.lang.reflect.InvocationTargetException; import java.util.*; -import java.util.function.Consumer; public class BukkitFontManager extends AbstractFontManager implements Listener { private final BukkitCraftEngine plugin; @@ -129,111 +129,67 @@ public class BukkitFontManager extends AbstractFontManager implements Listener { if (renameText == null || renameText.isEmpty()) return; Component itemName = Component.text(renameText); - MutableInt parsedCount = new MutableInt(0); - processComponent(itemName, player, parsedCount, (text) -> { - if (parsedCount.value >= Config.maxEmojiParsed()) return; + EmojiReplaceProcessResult replaceProcessResult = processEmojiNoCopy(player, itemName, renameText); + if (replaceProcessResult.changed()) { Item wrapped = this.plugin.itemManager().wrap(result); - wrapped.customName(AdventureHelper.componentToJson(text)); - event.setResult(wrapped.loadCopy()); - }); + wrapped.customName(AdventureHelper.componentToJson(replaceProcessResult.newText())); + event.setResult(wrapped.load()); + } } - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onSignChange(SignChangeEvent event) { + Player player = event.getPlayer(); + List lines = event.lines(); + for (int i = 0; i < lines.size(); i++) { + JsonElement json = ComponentUtils.paperAdventureToJsonElement(lines.get(i)); + if (json == null) continue; + Component line = AdventureHelper.jsonElementToComponent(json); + EmojiReplaceProcessResult result = processEmojiNoCopy(player, line); + if (result.changed()) { + try { + Reflections.method$SignChangeEvent$line.invoke(event, i, ComponentUtils.jsonElementToPaperAdventure(AdventureHelper.componentToJsonElement(result.newText()))); + } catch (IllegalAccessException | InvocationTargetException e) { + plugin.logger().warn("Failed to set sign line", e); + } + } else if (AdventureHelper.isPureTextComponent(line)) { + String plainText = AdventureHelper.plainTextContent(line); + try { + JsonObject jo = new JsonObject(); + jo.addProperty("text", plainText); + Reflections.method$SignChangeEvent$line.invoke(event, i, ComponentUtils.jsonElementToPaperAdventure(jo)); + } catch (IllegalAccessException | InvocationTargetException e) { + plugin.logger().warn("Failed to reset sign line", e); + } + } + } + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onPlayerEditBook(PlayerEditBookEvent event) { if (!event.isSigning()) return; Player player = event.getPlayer(); BookMeta newBookMeta = event.getNewBookMeta(); List pages = newBookMeta.pages(); - MutableBoolean replacedBookMeta = new MutableBoolean(false); - MutableInt parsedCount = new MutableInt(0); + boolean changed = false; for (int i = 0; i < pages.size(); i++) { - int finalIndex = i; JsonElement json = ComponentUtils.paperAdventureToJsonElement(pages.get(i)); - if (json instanceof JsonPrimitive primitive) { - if (primitive.isString() && primitive.getAsString().isEmpty()) continue; - } Component page = AdventureHelper.jsonElementToComponent(json); - processComponent(page, player, parsedCount, (text) -> { + EmojiReplaceProcessResult result = processEmojiNoCopy(player, page); + if (result.changed()) { + changed = true; try { - replacedBookMeta.value = true; - Reflections.method$BookMeta$page.invoke( - newBookMeta, finalIndex + 1, - ComponentUtils.jsonElementToPaperAdventure(AdventureHelper.componentToJsonElement(text)) - ); + Reflections.method$BookMeta$page.invoke(newBookMeta, i + 1, ComponentUtils.jsonElementToPaperAdventure(AdventureHelper.componentToJsonElement(result.newText()))); } catch (IllegalAccessException | InvocationTargetException e) { this.plugin.logger().warn("Failed to set book page", e); } - }); - if (parsedCount.value > Config.maxEmojiParsed()) break; + } } - if (replacedBookMeta.value) { + if (changed) { event.setNewBookMeta(newBookMeta); } } - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onSignChange(SignChangeEvent event) { - Player player = event.getPlayer(); - List lines = event.lines(); - MutableInt parsedCount = new MutableInt(0); - for (int i = 0; i < lines.size(); i++) { - int finalIndex = i; - JsonElement json = ComponentUtils.paperAdventureToJsonElement(lines.get(i)); - if (json.toString().isEmpty()) continue; - Component line = AdventureHelper.jsonElementToComponent(json); - processComponent(line, player, parsedCount, (text) -> { - try { - Reflections.method$SignChangeEvent$line.invoke( - event, finalIndex, - ComponentUtils.jsonElementToPaperAdventure(AdventureHelper.componentToJsonElement(text)) - ); - } catch (IllegalAccessException | InvocationTargetException e) { - plugin.logger().warn("Failed to set sign line", e); - } - }); - if (parsedCount.value > Config.maxEmojiParsed()) break; - } - } - - private void processComponent(Component text, Player player, MutableInt parsedCount, Consumer consumer) { - if (parsedCount.value > Config.maxEmojiParsed()) return; - Component textReplaced = text; - Set processedKeywords = new HashSet<>(); - for (Token token : super.emojiKeywordTrie.tokenize(AdventureHelper.componentToJson(text))) { - if (!token.isMatch()) continue; - if (parsedCount.value++ > Config.maxEmojiParsed()) return; - String keyword = token.getFragment(); - if (processedKeywords.contains(keyword)) continue; - Emoji emoji = super.emojiMapper.get(keyword); - if (emoji == null) continue; - if (emoji.permission() != null && !player.hasPermission(Objects.requireNonNull(emoji.permission()))) { - continue; - } - textReplaced = textReplaced.replaceText(builder -> { - builder.matchLiteral(keyword) - .replacement(AdventureHelper.miniMessage().deserialize( - emoji.content(), - PlayerContext.of(plugin.adapt(player), ContextHolder.builder() - .withOptionalParameter(EmojiParameters.EMOJI, emoji.emojiImage()) - .withParameter(EmojiParameters.KEYWORD, emoji.keywords().get(0)) - .build()).tagResolvers() - )); - }); - consumer.accept(textReplaced); - processedKeywords.add(keyword); - } - } - - private static final class MutableInt { - int value; - MutableInt(int value) { this.value = value; } - } - - private static final class MutableBoolean { - boolean value; - MutableBoolean(boolean value) { this.value = value; } - } - @SuppressWarnings("UnstableApiUsage") private void processChatEvent(AsyncChatDecorateEvent event) { Player player = event.player(); @@ -261,6 +217,43 @@ public class BukkitFontManager extends AbstractFontManager implements Listener { } } + private EmojiReplaceProcessResult processEmojiNoCopy(Player player, Component text) { + return this.processEmojiNoCopy(player, text, AdventureHelper.plainTextContent(text)); + } + + private EmojiReplaceProcessResult processEmojiNoCopy(Player player, Component text, @NotNull String raw) { + boolean replaced = false; + Set processed = new HashSet<>(); + int times = 0; + for (Token token : super.emojiKeywordTrie.tokenize(raw)) { + if (!token.isMatch()) + continue; + String fragment = token.getFragment(); + if (processed.contains(fragment)) + continue; + Emoji emoji = super.emojiMapper.get(token.getFragment()); + if (emoji == null || (emoji.permission() != null && !player.hasPermission(Objects.requireNonNull(emoji.permission())))) + continue; + text = text.replaceText(builder -> builder.matchLiteral(token.getFragment()) + .replacement(AdventureHelper.miniMessage().deserialize( + emoji.content(), + PlayerContext.of(plugin.adapt(player), + ContextHolder.builder() + .withOptionalParameter(EmojiParameters.EMOJI, emoji.emojiImage()) + .withParameter(EmojiParameters.KEYWORD, emoji.keywords().get(0)) + .build() + ).tagResolvers() + ))); + replaced = true; + processed.add(fragment); + if (++times >= Config.maxEmojisPerParse()) { + break; + } + } + if (!replaced) return EmojiReplaceProcessResult.failed(); + return EmojiReplaceProcessResult.success(text); + } + private IllegalCharacterProcessResult processIllegalCharacters(String raw) { boolean hasIllegal = false; // replace illegal image usage @@ -307,4 +300,15 @@ public class BukkitFontManager extends AbstractFontManager implements Listener { return new IllegalCharacterProcessResult(false, null); } } + + public record EmojiReplaceProcessResult(boolean changed, Component newText) { + + public static EmojiReplaceProcessResult success(Component newText) { + return new EmojiReplaceProcessResult(true, newText); + } + + public static EmojiReplaceProcessResult failed() { + return new EmojiReplaceProcessResult(false, null); + } + } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java b/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java index 3bdc5c77d..dc3cd1e69 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java @@ -15,6 +15,7 @@ import net.momirealms.craftengine.core.util.context.PlayerContext; import org.ahocorasick.trie.Token; import org.ahocorasick.trie.Trie; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -30,11 +31,11 @@ public abstract class AbstractFontManager implements FontManager { private final Set illegalChars = new HashSet<>(); private final ImageParser imageParser; private final EmojiParser emojiParser; - private OffsetFont offsetFont; - private Trie imageTagTrie; + + protected Trie imageTagTrie; protected Trie emojiKeywordTrie; - private Map tagMapper; + protected Map tagMapper; protected Map emojiMapper; // tab补全 protected final Map cachedEmojiSuggestions = new HashMap<>(); @@ -279,8 +280,12 @@ public abstract class AbstractFontManager implements FontManager { return; } List keywords = MiscUtils.getAsStringList(keywordsRaw); - UUID uuid = UUID.randomUUID(); + if (keywords.isEmpty()) { + TranslationManager.instance().log("warning.config.emoji.lack_keywords", path.toString(), id.toString()); + return; + } String keyword = keywords.get(0); + UUID uuid = UUID.nameUUIDFromBytes(keyword.getBytes(StandardCharsets.UTF_8)); cachedEmojiSuggestions.put(uuid, keyword); String content = section.getOrDefault("content", "").toString(); String image = null; diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java index f94baf16f..175a0c02a 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java @@ -96,7 +96,7 @@ public class Config { protected UUID resource_pack$external_host$uuid; protected int performance$max_block_chain_update_limit; - protected int performance$max_emoji_parsed_limit; + protected int performance$max_emojis_per_parse; protected boolean light_system$force_update_light; protected boolean light_system$enable; @@ -260,7 +260,7 @@ public class Config { // performance performance$max_block_chain_update_limit = config.getInt("performance.max-block-chain-update-limit", 64); - performance$max_emoji_parsed_limit = config.getInt("performance.max-emoji-parsed-limit", 100); + performance$max_emojis_per_parse = config.getInt("performance.max-emojis-per-parse", 32); // light light_system$force_update_light = config.getBoolean("light-system.force-update-light", false); @@ -354,8 +354,8 @@ public class Config { return instance.performance$max_block_chain_update_limit; } - public static int maxEmojiParsed() { - return instance.performance$max_emoji_parsed_limit; + public static int maxEmojisPerParse() { + return instance.performance$max_emojis_per_parse; } public static boolean removeInvalidFurniture() { diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/AdventureHelper.java b/core/src/main/java/net/momirealms/craftengine/core/util/AdventureHelper.java index 95cff2708..7d32c3fb6 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/util/AdventureHelper.java +++ b/core/src/main/java/net/momirealms/craftengine/core/util/AdventureHelper.java @@ -5,6 +5,7 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -260,4 +261,27 @@ public class AdventureHelper { } return stringBuilder.toString(); } + + public static String plainTextContent(Component component) { + StringBuilder sb = new StringBuilder(); + if (component instanceof TextComponent textComponent) { + sb.append(textComponent.content()); + } + for (Component child : component.children()) { + sb.append(plainTextContent(child)); + } + return sb.toString(); + } + + public static boolean isPureTextComponent(Component component) { + if (!(component instanceof TextComponent textComponent)) { + return false; + } + for (Component child : textComponent.children()) { + if (!isPureTextComponent(child)) { + return false; + } + } + return true; + } }