diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitImageManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitImageManager.java new file mode 100644 index 000000000..d8570fd66 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitImageManager.java @@ -0,0 +1,93 @@ +package net.momirealms.craftengine.bukkit.font; + +import com.google.gson.JsonElement; +import io.papermc.paper.event.player.AsyncChatCommandDecorateEvent; +import io.papermc.paper.event.player.AsyncChatDecorateEvent; +import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine; +import net.momirealms.craftengine.bukkit.util.Reflections; +import net.momirealms.craftengine.core.font.AbstractImageManager; +import net.momirealms.craftengine.core.util.AdventureHelper; +import net.momirealms.craftengine.core.util.CharacterUtils; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; + +import java.lang.reflect.InvocationTargetException; + +public class BukkitImageManager extends AbstractImageManager implements Listener { + private final BukkitCraftEngine plugin; + private final Object serializer; + + public BukkitImageManager(BukkitCraftEngine plugin) { + super(plugin); + this.plugin = plugin; + try { + Object builder = Reflections.method$GsonComponentSerializer$builder.invoke(null); + this.serializer = Reflections.method$GsonComponentSerializer$Builder$build.invoke(builder); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void delayedInit() { + Bukkit.getPluginManager().registerEvents(this, plugin.bootstrap()); + } + + @Override + public void disable() { + super.disable(); + HandlerList.unregisterAll(this); + } + + @EventHandler + @SuppressWarnings("UnstableApiUsage") + public void onChat(AsyncChatDecorateEvent event) { + if (event.player() == null) return; + this.processChatMessages(event); + } + + @EventHandler + @SuppressWarnings("UnstableApiUsage") + public void onChatCommand(AsyncChatCommandDecorateEvent event) { + if (event.player() == null) return; + this.processChatMessages(event); + } + + @EventHandler + public void onCommand(PlayerCommandPreprocessEvent event) { + event.setMessage(processIllegalString(event.getMessage())); + } + + @SuppressWarnings("UnstableApiUsage") + private void processChatMessages(AsyncChatDecorateEvent event) { + try { + Object originalMessage = Reflections.clazz$AdventureComponent.cast(Reflections.field$AsyncChatDecorateEvent$originalMessage.get(event)); + JsonElement json = (JsonElement) Reflections.method$GsonComponentSerializer$serializeToTree.invoke(serializer, originalMessage); + String jsonMessage = AdventureHelper.jsonElementToStringJson(json); + if (!this.isDefaultFontInUse()) return; + String str = processIllegalString(jsonMessage); + Object component = Reflections.method$ComponentSerializer$deserialize.invoke(serializer, str); + Reflections.method$AsyncChatDecorateEvent$result.invoke(event, component); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private String processIllegalString(String string) { + char[] chars = string.toCharArray(); + int[] codepoints = CharacterUtils.charsToCodePoints(chars); + int[] newCodepoints = new int[codepoints.length]; + for (int i = 0; i < codepoints.length; i++) { + int codepoint = codepoints[i]; + if (!this.isIllegalCharacter(codepoint)) { + newCodepoints[i] = codepoint; + } else { + newCodepoints[i] = '*'; + } + } + return new String(newCodepoints, 0, newCodepoints.length); + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java index 6104cfcf5..19571f6b8 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java @@ -5,6 +5,7 @@ import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent; import net.momirealms.craftengine.bukkit.block.BukkitBlockManager; import net.momirealms.craftengine.bukkit.block.behavior.BukkitBlockBehaviors; import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurnitureManager; +import net.momirealms.craftengine.bukkit.font.BukkitImageManager; import net.momirealms.craftengine.bukkit.item.BukkitItemManager; import net.momirealms.craftengine.bukkit.item.behavior.BukkitItemBehaviors; import net.momirealms.craftengine.bukkit.item.recipe.BukkitRecipeManager; @@ -49,6 +50,7 @@ import java.lang.reflect.Field; import java.nio.file.Path; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; @SuppressWarnings("unchecked") public class BukkitCraftEngine extends CraftEngine { @@ -56,6 +58,7 @@ public class BukkitCraftEngine extends CraftEngine { private final JavaPlugin bootstrap; private SchedulerTask tickTask; private boolean successfullyLoaded = false; + private boolean successfullyEnabled = false; private boolean requiresRestart = false; private boolean hasMod = false; private AntiGriefLib antiGrief; @@ -103,6 +106,18 @@ public class BukkitCraftEngine extends CraftEngine { @Override public void enable() { + if (successfullyEnabled) { + logger().severe(" "); + logger().severe(" "); + logger().severe(" "); + logger().severe("Please do not restart plugins at runtime."); + logger().severe(" "); + logger().severe(" "); + logger().severe(" "); + Bukkit.getPluginManager().disablePlugin(this.bootstrap); + return; + } + this.successfullyEnabled = true; if (this.hasMod && this.requiresRestart) { logger().warn(" "); logger().warn(" "); @@ -138,6 +153,7 @@ public class BukkitCraftEngine extends CraftEngine { super.worldManager = new BukkitWorldManager(this); super.soundManager = new BukkitSoundManager(this); super.vanillaLootManager = new BukkitVanillaLootManager(this); + this.imageManager = new BukkitImageManager(this); super.enable(); // tick task if (VersionHelper.isFolia()) { @@ -175,6 +191,16 @@ public class BukkitCraftEngine extends CraftEngine { public void disable() { super.disable(); if (this.tickTask != null) this.tickTask.cancel(); + if (!Bukkit.getServer().isStopping()) { + logger().severe(" "); + logger().severe(" "); + logger().severe(" "); + logger().severe("Please do not disable plugins at runtime."); + logger().severe(" "); + logger().severe(" "); + logger().severe(" "); + Bukkit.getServer().shutdown(); + } } @Override diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java index 606f31b5a..8bb11eb90 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java @@ -125,7 +125,6 @@ public class BukkitNetworkManager implements NetworkManager, Listener { registerNMSPacketConsumer(PacketConsumers.MOVE_ENTITY, Reflections.clazz$ClientboundMoveEntityPacket$Pos); registerNMSPacketConsumer(PacketConsumers.PICK_ITEM_FROM_ENTITY, Reflections.clazz$ServerboundPickItemFromEntityPacket); registerNMSPacketConsumer(PacketConsumers.SOUND, Reflections.clazz$ClientboundSoundPacket); - registerNMSPacketConsumer(PacketConsumers.CHAT, Reflections.clazz$ServerboundChatPacket); registerNMSPacketConsumer(PacketConsumers.RENAME_ITEM, Reflections.clazz$ServerboundRenameItemPacket); registerNMSPacketConsumer(PacketConsumers.SIGN_UPDATE, Reflections.clazz$ServerboundSignUpdatePacket); registerByteBufPacketConsumer(PacketConsumers.SECTION_BLOCK_UPDATE, this.packetIds.clientboundSectionBlocksUpdatePacket()); diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java index 7d8048590..0ac89fa5a 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java @@ -663,34 +663,6 @@ public class PacketConsumers { } }; - // we handle it on packet level to prevent it from being captured by plugins (most are chat plugins) - public static final TriConsumer CHAT = (user, event, packet) -> { - try { - String message = (String) Reflections.field$ServerboundChatPacket$message.get(packet); - if (message != null && !message.isEmpty()) { - ImageManager manager = CraftEngine.instance().imageManager(); - if (!manager.isDefaultFontInUse()) return; - runIfContainsIllegalCharacter(message, manager, (s) -> { - event.setCancelled(true); - try { - Object newPacket = Reflections.constructor$ServerboundChatPacket.newInstance( - s, - Reflections.field$ServerboundChatPacket$timeStamp.get(packet), - Reflections.field$ServerboundChatPacket$salt.get(packet), - Reflections.field$ServerboundChatPacket$signature.get(packet), - Reflections.field$ServerboundChatPacket$lastSeenMessages.get(packet) - ); - user.receivePacket(newPacket); - } catch (Exception e) { - CraftEngine.instance().logger().warn("Failed to create replaced chat packet", e); - } - }); - } - } catch (Exception e) { - CraftEngine.instance().logger().warn("Failed to handle ServerboundChatPacket", e); - } - }; - // we handle it on packet level to prevent it from being captured by plugins public static final TriConsumer RENAME_ITEM = (user, event, packet) -> { try { @@ -733,6 +705,7 @@ public class PacketConsumers { }; private static void runIfContainsIllegalCharacter(String string, ImageManager manager, Consumer callback) { + //noinspection DuplicatedCode char[] chars = string.toCharArray(); int[] codepoints = CharacterUtils.charsToCodePoints(chars); int[] newCodepoints = new int[codepoints.length]; diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java index 4ed697c8e..60566bdc6 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java @@ -1,12 +1,14 @@ package net.momirealms.craftengine.bukkit.util; import com.google.common.collect.ImmutableList; +import com.google.gson.JsonElement; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.MessageToByteEncoder; +import io.papermc.paper.event.player.AsyncChatDecorateEvent; import net.kyori.adventure.text.Component; import net.momirealms.craftengine.core.util.ReflectionUtils; import net.momirealms.craftengine.core.util.VersionHelper; @@ -5201,4 +5203,64 @@ public class Reflections { clazz$ServerboundSignUpdatePacket, String[].class, 0 ) ); + + @SuppressWarnings("UnstableApiUsage") + public static final Field field$AsyncChatDecorateEvent$originalMessage = requireNonNull( + ReflectionUtils.getDeclaredField(AsyncChatDecorateEvent.class, "originalMessage") + ); + + public static final Class clazz$ComponentSerializer = requireNonNull( + ReflectionUtils.getClazz( + "net{}kyori{}adventure{}text{}serializer{}ComponentSerializer".replace("{}", ".") + ) + ); + + public static final Class clazz$GsonComponentSerializer = requireNonNull( + ReflectionUtils.getClazz( + "net{}kyori{}adventure{}text{}serializer{}gson{}GsonComponentSerializer".replace("{}", ".") + ) + ); + + public static final Class clazz$GsonComponentSerializer$Builder = requireNonNull( + ReflectionUtils.getClazz( + "net{}kyori{}adventure{}text{}serializer{}gson{}GsonComponentSerializer$Builder".replace("{}", ".") + ) + ); + + public static final Class clazz$AdventureComponent = requireNonNull( + ReflectionUtils.getClazz( + "net{}kyori{}adventure{}text{}Component".replace("{}", ".") + ) + ); + + public static final Method method$GsonComponentSerializer$builder = requireNonNull( + ReflectionUtils.getMethod( + clazz$GsonComponentSerializer, clazz$GsonComponentSerializer$Builder + ) + ); + + public static final Method method$GsonComponentSerializer$Builder$build = requireNonNull( + ReflectionUtils.getMethod( + clazz$GsonComponentSerializer$Builder, clazz$GsonComponentSerializer + ) + ); + + public static final Method method$GsonComponentSerializer$serializeToTree = requireNonNull( + ReflectionUtils.getMethod( + clazz$GsonComponentSerializer, JsonElement.class, clazz$AdventureComponent + ) + ); + + public static final Method method$ComponentSerializer$deserialize = requireNonNull( + ReflectionUtils.getMethod( + clazz$ComponentSerializer, Object.class, Object.class + ) + ); + + @SuppressWarnings("UnstableApiUsage") + public static final Method method$AsyncChatDecorateEvent$result = requireNonNull( + ReflectionUtils.getMethod( + AsyncChatDecorateEvent.class, void.class, clazz$AdventureComponent + ) + ); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/font/ImageManagerImpl.java b/core/src/main/java/net/momirealms/craftengine/core/font/AbstractImageManager.java similarity index 98% rename from core/src/main/java/net/momirealms/craftengine/core/font/ImageManagerImpl.java rename to core/src/main/java/net/momirealms/craftengine/core/font/AbstractImageManager.java index ace8f47e0..59c46871b 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/font/ImageManagerImpl.java +++ b/core/src/main/java/net/momirealms/craftengine/core/font/AbstractImageManager.java @@ -12,7 +12,7 @@ import java.nio.file.Path; import java.util.*; import java.util.function.BiFunction; -public class ImageManagerImpl implements ImageManager { +public abstract class AbstractImageManager implements ImageManager { private final CraftEngine plugin; // namespace:font font private final HashMap fonts = new HashMap<>(); @@ -22,7 +22,7 @@ public class ImageManagerImpl implements ImageManager { private OffsetFont offsetFont; - public ImageManagerImpl(CraftEngine plugin) { + public AbstractImageManager(CraftEngine plugin) { this.plugin = plugin; } diff --git a/core/src/main/java/net/momirealms/craftengine/core/font/ImageManager.java b/core/src/main/java/net/momirealms/craftengine/core/font/ImageManager.java index ac114d578..7c38e8643 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/font/ImageManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/font/ImageManager.java @@ -68,4 +68,6 @@ public interface ImageManager extends Reloadable, ConfigSectionParser { default int loadingSequence() { return LoadingSequence.FONT; } + + void delayedInit(); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java index 931804201..62626b2c3 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java @@ -3,7 +3,7 @@ package net.momirealms.craftengine.core.plugin; import net.momirealms.craftengine.core.block.BlockManager; import net.momirealms.craftengine.core.entity.furniture.FurnitureManager; import net.momirealms.craftengine.core.font.ImageManager; -import net.momirealms.craftengine.core.font.ImageManagerImpl; +import net.momirealms.craftengine.core.font.AbstractImageManager; import net.momirealms.craftengine.core.item.ItemManager; import net.momirealms.craftengine.core.item.recipe.RecipeManager; import net.momirealms.craftengine.core.loot.VanillaLootManager; @@ -120,7 +120,6 @@ public abstract class CraftEngine implements Plugin { @Override public void enable() { this.networkManager.enable(); - this.imageManager = new ImageManagerImpl(this); this.templateManager = new TemplateManagerImpl(this); this.itemBrowserManager = new ItemBrowserManagerImpl(this); this.commandManager.registerDefaultFeatures(); @@ -135,6 +134,7 @@ public abstract class CraftEngine implements Plugin { this.worldManager.delayedInit(); this.packManager.delayedInit(); this.furnitureManager.delayedInit(); + this.imageManager.delayedInit(); }); } 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 d75919cbc..ea801883e 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 @@ -2,6 +2,7 @@ package net.momirealms.craftengine.core.util; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.gson.JsonElement; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; @@ -182,6 +183,24 @@ public class AdventureHelper { return getGson().serialize(component); } + /** + * Converts a JsonElement to a Component. + * @param gson the JsonElement to convert + * @return the resulting Component + */ + public static Component jsonElementToComponent(JsonElement gson) { + return GsonComponentSerializer.gson().deserializeFromTree(gson); + } + + /** + * Converts a JsonElement to a JSON string. + * @param jsonElement the JsonElement to convert + * @return the JSON string representation + */ + public static String jsonElementToStringJson(JsonElement jsonElement) { + return componentToJson(jsonElementToComponent(jsonElement)); + } + /** * Checks if a character is a legacy color code. *