diff --git a/core/src/main/kotlin/com/mineinabyss/emojy/EmojyHelpers.kt b/core/src/main/kotlin/com/mineinabyss/emojy/EmojyHelpers.kt index 17af042..c6de4e9 100644 --- a/core/src/main/kotlin/com/mineinabyss/emojy/EmojyHelpers.kt +++ b/core/src/main/kotlin/com/mineinabyss/emojy/EmojyHelpers.kt @@ -17,8 +17,8 @@ fun Component.transform(player: Player?, insert: Boolean, unescape: Boolean = tr val legacy = LegacyComponentSerializer.legacySection() private val spaceRegex: Regex = "(?() DI.add(config("config", dataPath, EmojyConfig(), onLoad = { @@ -70,7 +58,7 @@ class EmojyPlugin : JavaPlugin() { config>(it, dataPath / "languages", mapOf()).getOrLoad()) }.toSet() override val logger by plugin.observeLogger() - override val handler: IEmojyNMSHandler = EmojyNMSHandlers.setup() + override val handler: IEmojyNMSHandler = EmojyNMSHandlers.setup(this@EmojyPlugin) }) GlobalTranslator.translator().sources().filter { it.name() == EmojyTranslator.key }.forEach(GlobalTranslator.translator()::removeSource) diff --git a/core/src/main/kotlin/com/mineinabyss/emojy/nms/EmojyNMSHandlers.kt b/core/src/main/kotlin/com/mineinabyss/emojy/nms/EmojyNMSHandlers.kt index 2acac7e..e812c1f 100644 --- a/core/src/main/kotlin/com/mineinabyss/emojy/nms/EmojyNMSHandlers.kt +++ b/core/src/main/kotlin/com/mineinabyss/emojy/nms/EmojyNMSHandlers.kt @@ -2,6 +2,7 @@ package com.mineinabyss.emojy.nms import com.google.gson.JsonObject import com.google.gson.JsonParser +import com.mineinabyss.emojy.EmojyPlugin import com.mineinabyss.emojy.escapeEmoteIDs import com.mineinabyss.emojy.transform import com.mineinabyss.emojy.transformEmoteIDs @@ -9,61 +10,21 @@ import com.mineinabyss.idofront.textcomponents.miniMsg import com.mineinabyss.idofront.textcomponents.serialize import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import org.bukkit.Bukkit import org.bukkit.entity.Player object EmojyNMSHandlers { - private val SUPPORTED_VERSION = arrayOf(/*"v1_19_R1", "v1_19_R2", "v1_19_R3", "v1_20_R1", "v1_20_R2", "v1_20_R3", */"v1_20_R4") + private val SUPPORTED_VERSION = arrayOf("v1_20_R4") - fun setup(): IEmojyNMSHandler { + fun setup(emojy: EmojyPlugin): IEmojyNMSHandler { SUPPORTED_VERSION.forEach { version -> runCatching { - //Class.forName("org.bukkit.craftbukkit.entity.CraftPlayer") - return Class.forName("com.mineinabyss.emojy.nms.${version}.EmojyNMSHandler").getConstructor() - .newInstance() as IEmojyNMSHandler - } + return Class.forName("com.mineinabyss.emojy.nms.${version}.EmojyNMSHandler").getConstructor(EmojyPlugin::class.java) + .newInstance(emojy) as IEmojyNMSHandler + }.onFailure { it.printStackTrace() } } throw IllegalStateException("Unsupported server version") } - - private val gson = GsonComponentSerializer.gson() - private val plain = PlainTextComponentSerializer.plainText() - //TODO toPlainText fixes the anvil issue with tags being escaped - // It does break all other formatting everywhere else though by removing tags - // serialize() doesnt but keeps the escaped tag from gson - // anvil goes like this, inputItem -> readUtf -> renameField -> writeNbt from raw string, which is why it breaks - fun JsonObject.formatString(player: Player? = null) : String { - return if (this.has("args") || this.has("text") || this.has("extra") || this.has("translate")) { - gson.serialize(gson.deserializeFromTree(this)/*.toPlainText()*/.serialize().miniMsg().transform(player, true, true)) - } else this.toString() - } - - fun writeTransformer(player: Player?, insert: Boolean, unescape: Boolean) = { string: String -> - runCatching { - val jsonObject = JsonParser.parseString(string).takeIf { it.isJsonObject }?.asJsonObject ?: return@runCatching string - if (jsonObject.has("args") || jsonObject.has("text") || jsonObject.has("extra") || jsonObject.has("translate")) { - val formatted = gson.deserializeFromTree(jsonObject).transformEmoteIDs(player, insert, unescape) - gson.serialize(formatted) - } else string - }.getOrNull() ?: string - } - - fun readTransformer(player: Player?) = { string: String -> - runCatching { - val jsonObject = JsonParser.parseString(string).takeIf { it.isJsonObject }?.asJsonObject ?: return@runCatching string - if (jsonObject.has("args") || jsonObject.has("text") || jsonObject.has("extra") || jsonObject.has("translate")) { - val formatted = gson.deserializeFromTree(jsonObject).escapeEmoteIDs(player) - gson.serialize(formatted) - } else string - }.getOrNull() ?: string - } - - fun transformer(player: Player? = null) = { string: String -> - runCatching { - val element = JsonParser.parseString(string) - if (element.isJsonObject) element.asJsonObject.formatString(player) - else string - }.getOrNull() ?: string - } } diff --git a/core/src/main/kotlin/com/mineinabyss/emojy/nms/IEmojyNMSHandler.kt b/core/src/main/kotlin/com/mineinabyss/emojy/nms/IEmojyNMSHandler.kt index a25cb37..608d2e4 100644 --- a/core/src/main/kotlin/com/mineinabyss/emojy/nms/IEmojyNMSHandler.kt +++ b/core/src/main/kotlin/com/mineinabyss/emojy/nms/IEmojyNMSHandler.kt @@ -1,12 +1,13 @@ package com.mineinabyss.emojy.nms import org.bukkit.entity.Player +import java.util.Locale interface IEmojyNMSHandler { - fun inject(player: Player) + val locals: MutableSet - fun uninject(player: Player) + fun addLocaleCodec(locale: Locale) val supported get() = false } diff --git a/settings.gradle.kts b/settings.gradle.kts index 67f995e..f8e20d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,4 +35,4 @@ dependencyResolutionManagement { } } -include("core", /*"v1_19_R1", "v1_19_R2", "v1_19_R3", "v1_20_R1", "v1_20_R2", */"v1_20_R3", "v1_20_R4") +include("core", "v1_20_R4") diff --git a/core/src/main/kotlin/com/mineinabyss/emojy/EmojyListener.kt b/v1_20_R4/src/main/kotlin/com/mineinabyss/emojy/nms/v1_20_R4/EmojyListener.kt similarity index 83% rename from core/src/main/kotlin/com/mineinabyss/emojy/EmojyListener.kt rename to v1_20_R4/src/main/kotlin/com/mineinabyss/emojy/nms/v1_20_R4/EmojyListener.kt index 6324a9b..f8697dc 100644 --- a/core/src/main/kotlin/com/mineinabyss/emojy/EmojyListener.kt +++ b/v1_20_R4/src/main/kotlin/com/mineinabyss/emojy/nms/v1_20_R4/EmojyListener.kt @@ -1,6 +1,9 @@ -package com.mineinabyss.emojy +package com.mineinabyss.emojy.nms.v1_20_R4 +import com.mineinabyss.emojy.emojy +import com.mineinabyss.emojy.escapeEmoteIDs import com.mineinabyss.emojy.nms.EmojyNMSHandlers +import com.mineinabyss.emojy.transform import com.mineinabyss.idofront.items.editItemMeta import com.mineinabyss.idofront.messaging.logError import com.mineinabyss.idofront.messaging.logVal @@ -22,14 +25,14 @@ import org.bukkit.inventory.AnvilInventory class EmojyListener : Listener { @EventHandler - fun PlayerJoinEvent.injectPlayer() { - emojy.handler.inject(player) + fun PlayerJoinEvent.onJoin() { + emojy.handler.addLocaleCodec(player.locale()) } // Replace with result not original message to avoid borking other chat formatting @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) fun AsyncChatDecorateEvent.onPlayerChat() { - result(result().transform(player(), true, true)) + result(result().escapeEmoteIDs(player())) } @EventHandler diff --git a/v1_20_R4/src/main/kotlin/com/mineinabyss/emojy/nms/v1_20_R4/EmojyNMSHandler.kt b/v1_20_R4/src/main/kotlin/com/mineinabyss/emojy/nms/v1_20_R4/EmojyNMSHandler.kt index 06b3dc4..820702f 100644 --- a/v1_20_R4/src/main/kotlin/com/mineinabyss/emojy/nms/v1_20_R4/EmojyNMSHandler.kt +++ b/v1_20_R4/src/main/kotlin/com/mineinabyss/emojy/nms/v1_20_R4/EmojyNMSHandler.kt @@ -2,312 +2,91 @@ package com.mineinabyss.emojy.nms.v1_20_R4 -import com.mineinabyss.emojy.emojy -import com.mineinabyss.emojy.escapeEmoteIDs -import com.mineinabyss.emojy.legacy -import com.mineinabyss.emojy.nms.EmojyNMSHandlers +import com.mineinabyss.emojy.* import com.mineinabyss.emojy.nms.IEmojyNMSHandler -import com.mineinabyss.emojy.transformEmoteIDs -import com.mineinabyss.idofront.messaging.broadcastVal -import com.mineinabyss.idofront.messaging.logVal -import com.mineinabyss.idofront.textcomponents.miniMsg +import com.mineinabyss.idofront.messaging.idofrontLogger +import com.mineinabyss.idofront.plugin.listeners import com.mineinabyss.idofront.textcomponents.serialize +import com.mojang.serialization.Codec import io.netty.buffer.ByteBuf -import io.netty.buffer.Unpooled -import io.netty.channel.ChannelHandlerContext -import io.papermc.paper.adventure.AdventureComponent +import io.netty.handler.codec.DecoderException +import io.netty.handler.codec.EncoderException +import io.papermc.paper.adventure.AdventureCodecs import io.papermc.paper.adventure.PaperAdventure import net.kyori.adventure.text.Component -import net.minecraft.core.RegistryAccess -import net.minecraft.nbt.* -import net.minecraft.network.* +import net.kyori.adventure.text.TextReplacementConfig +import net.kyori.adventure.translation.GlobalTranslator +import net.minecraft.nbt.NbtAccounter +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.Tag +import net.minecraft.network.RegistryFriendlyByteBuf import net.minecraft.network.chat.ComponentSerialization -import net.minecraft.network.protocol.Packet -import net.minecraft.network.protocol.game.GameProtocols -import org.bukkit.craftbukkit.CraftServer -import org.bukkit.craftbukkit.entity.CraftPlayer -import org.bukkit.entity.Player -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.DataInputStream -import java.io.DataOutputStream -import java.util.function.Function +import net.minecraft.network.codec.ByteBufCodecs +import net.minecraft.network.codec.StreamCodec +import java.lang.reflect.Field +import java.lang.reflect.Modifier +import java.util.* +import java.util.function.Supplier -class EmojyNMSHandler : IEmojyNMSHandler { +class EmojyNMSHandler(emojy: EmojyPlugin) : IEmojyNMSHandler { - private var INFO_BY_ENCODER: Function, ProtocolInfo<*>> - private var INFO_BY_DECODER: Function, ProtocolInfo<*>> + override val locals: MutableSet = mutableSetOf() - companion object { - private fun Player.connection() = (this as CraftPlayer).handle.connection.connection - private fun Player.channel() = (this as CraftPlayer).handle.connection.connection.channel - private fun registryAccess(): RegistryAccess = - (emojy.plugin.server as CraftServer).handle.server.registryAccess() - } - - init { - val encoder = PacketEncoder::class.java.declaredFields.firstOrNull { - ProtocolInfo::class.java.isAssignableFrom(it.type) - }?.apply { isAccessible = true } ?: throw NullPointerException() - - val decoder = PacketDecoder::class.java.declaredFields.firstOrNull { - ProtocolInfo::class.java.isAssignableFrom(it.type) - }?.apply { isAccessible = true } ?: throw NullPointerException() - - INFO_BY_ENCODER = Function, ProtocolInfo<*>> { e: PacketEncoder<*> -> - return@Function run { encoder.get(e) } as ProtocolInfo<*> - } - INFO_BY_DECODER = Function, ProtocolInfo<*>> { e: PacketDecoder<*> -> - return@Function run { decoder.get(e) } as ProtocolInfo<*> - } - } + override fun addLocaleCodec(locale: Locale) { + val codecs = (PaperAdventure::class.java.getDeclaredField("LOCALIZED_CODECS").apply { isAccessible = true } + .get(null) as MutableMap>) - override fun inject(player: Player) { - val channel = player.channel() - val handler = PlayerHandler(player) - channel.eventLoop().submit { - val pipeline = channel.pipeline() - pipeline.forEach { - pipeline.replace( - it.key, it.key, when (it.value) { - is PacketEncoder<*> -> handler.encoder(INFO_BY_ENCODER.apply(it.value as PacketEncoder<*>)) - is PacketDecoder<*> -> handler.decoder(INFO_BY_DECODER.apply(it.value as PacketDecoder<*>)) - else -> return@forEach - } - ) - } - } + codecs[locale] = AdventureCodecs.COMPONENT_CODEC.xmap( + { component -> component }, // decode + { component -> GlobalTranslator.render(component.transformEmotes(), locale) } // encode + ) } - override fun uninject(player: Player) { - val channel = player.channel() - val pipeline = channel.pipeline() - pipeline.forEach { - pipeline.replace( - it.key, it.key, when (it.value) { - is PlayerHandler.EmojyEncoder -> PacketEncoder((it.value as PlayerHandler.EmojyEncoder).protocolInfo) - is PlayerHandler.EmojyDecoder -> PacketDecoder((it.value as PlayerHandler.EmojyDecoder).protocolInfo) - else -> return@forEach - } - ) - } - } - - private class EmojyBuffer(val originalBuffer: ByteBuf, val player: Player?) : RegistryFriendlyByteBuf(originalBuffer, registryAccess()) { - override fun copy(): EmojyBuffer { - return EmojyBuffer(Unpooled.copiedBuffer(originalBuffer), player) - } - - override fun writeUtf(string: String, maxLength: Int): FriendlyByteBuf { - return EmojyNMSHandlers.writeTransformer(player, true, true).invoke(string).let { super.writeUtf(it, maxLength) } - } - - override fun readUtf(maxLength: Int): String { - return super.readUtf(maxLength).let { string -> - runCatching { string.miniMsg() }.recover { legacy.deserialize(string) } - .getOrNull()?.escapeEmoteIDs(player)?.serialize() ?: string - } - } - - override fun writeNbt(compound: Tag?): FriendlyByteBuf { - return super.writeNbt(compound?.apply { - when (this) { - is CompoundTag -> transform(this, EmojyNMSHandlers.writeTransformer(player, false, true)) - is StringTag -> transform(this, EmojyNMSHandlers.writeTransformer(player, false,true)) - } - }) - } - - override fun readNbt(): CompoundTag? { - return super.readNbt()?.apply { - transform(this, EmojyNMSHandlers.readTransformer(player)) - } - } - - private fun transform(compound: CompoundTag, transformer: Function) { - for (key in compound.allKeys) when (val base = compound.get(key)) { - is CompoundTag -> transform(base, transformer) - is ListTag -> transform(base, transformer) - is StringTag -> compound.put(key, StringTag.valueOf(transformer.apply(base.asString))) - } - } - - private fun transform(list: ListTag, transformer: Function) { - val listCopy = list.toList() - for (base in listCopy) when (base) { - is CompoundTag -> transform(base, transformer) - is ListTag -> transform(base, transformer) - is StringTag -> list.indexOf(base).let { index -> - list[index] = StringTag.valueOf(transformer.apply(base.asString)) - } - } - } - - private fun transform(string: StringTag, transformer: Function) { - transformer.apply(string.asString) - } + init { + emojy.listeners(EmojyListener()) } - @Suppress("UNCHECKED_CAST") - private data class PlayerHandler(val player: Player) { + companion object { - private fun hasComponent(clazz: Class<*>): Boolean { - return clazz.declaredFields.any { net.minecraft.network.chat.Component::class.java.isAssignableFrom(it.type) } - || clazz.declaredFields.any { hasComponent(it::class.java) } + fun net.minecraft.network.chat.Component.transformEmotes(): net.minecraft.network.chat.Component { + return PaperAdventure.asVanilla(PaperAdventure.asAdventure(this).transformEmotes()) } - fun decoder(protocolInfo: ProtocolInfo<*>) = EmojyDecoder(protocolInfo as ProtocolInfo) - fun encoder(protocolInfo: ProtocolInfo<*>) = EmojyEncoder(protocolInfo as ProtocolInfo) - private fun writeNewNbt( - original: EmojyBuffer, - clientbound: Boolean - ): EmojyBuffer { - val newBuffer = EmojyBuffer(Unpooled.buffer(), player) - val id = VarInt.read(original) - VarInt.write(newBuffer, id) + fun Component.transformEmotes(): Component { + var component = this + val serialized = this.serialize() - val bytes = ByteArray(original.readableBytes()) - original.readBytes(bytes) - val list: MutableList = ArrayList(bytes.size) - var index = 0 - try { - loop@ while (index < bytes.size) { - val aByte = bytes[index++] - list.add(aByte) - val size = bytes.size - index - val nbtByte = ByteArray(size) - System.arraycopy(bytes, index, nbtByte, 0, nbtByte.size) + for (emote in emojy.emotes) emote.baseRegex.findAll(serialized).forEach { match -> - when (TagTypes.getType(aByte.toInt())) { - ListTag.TYPE -> { - emojy.logger.s("list: " + clientbound) - /*val size = bytes.size - index - val nbtByte = ByteArray(size) - System.arraycopy(bytes, index, nbtByte, 0, nbtByte.size) + val colorable = colorableRegex in match.value + val bitmapIndex = bitmapIndexRegex.find(match.value)?.groupValues?.get(1)?.toIntOrNull() ?: -1 - runCatching { - DataInputStream(ByteArrayInputStream(nbtByte)).use { input -> - val getTag = ListTag.TYPE.load(input) - } - }*/ - } - StringTag.TYPE -> index = handleString(index, nbtByte, clientbound, list) - CompoundTag.TYPE -> index = handleCompound(index, nbtByte, clientbound, list) - } - } - val newByte = ByteArray(list.size) - var i = 0 - for (b in list) newByte[i++] = b - newBuffer.writeBytes(newByte) - return newBuffer - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - private fun handleString(index: Int, nbtByte: ByteArray, clientbound: Boolean, list: MutableList): Int { - emojy.logger.e("string: " + clientbound) - var index = index - runCatching { - DataInputStream(ByteArrayInputStream(nbtByte)).use { input -> - val getTag = StringTag.TYPE.load(input, NbtAccounter.create(FriendlyByteBuf.DEFAULT_NBT_QUOTA.toLong())).takeIf { it.asString.isNotBlank() } ?: return@use - val oldComponent = PaperAdventure.asAdventure(ComponentSerialization.CODEC.decode(NbtOps.INSTANCE, getTag).getOrThrow().first) - val newComponent: Component = when (clientbound) { - true -> oldComponent.logVal("").transformEmoteIDs(player, true, true).logVal("") - false -> oldComponent.escapeEmoteIDs(player) - } - val newTag = - ComponentSerialization.CODEC.encode( - PaperAdventure.asVanilla(newComponent), - NbtOps.INSTANCE, - StringTag.valueOf("") - ).getOrThrow() as StringTag - val stream = ByteArrayOutputStream() - DataOutputStream(stream).use { output -> - newTag.write(output) - index += nbtByte.size - input.available() - for (b in stream.toByteArray()) { - list.add(b) - } - } - } - } - return index - } - - private fun handleCompound(index: Int, nbtByte: ByteArray, clientbound: Boolean, list: MutableList): Int { - emojy.logger.w("compoundtag: " + clientbound) - var index = index - runCatching { - DataInputStream(ByteArrayInputStream(nbtByte)).use { input -> - val getTag = CompoundTag.TYPE.load( - input, - NbtAccounter.create(FriendlyByteBuf.DEFAULT_NBT_QUOTA.toLong()) - ) - if (getTag.isEmpty) return@use - val oldComponent = - PaperAdventure.asAdventure( - ComponentSerialization.CODEC.decode( - NbtOps.INSTANCE, - getTag - ).getOrThrow().first + component = component.replaceText( + TextReplacementConfig.builder() + .match(emote.baseRegex.pattern).once() + .replacement( + emote.formattedUnicode( + insert = false, + colorable = colorable, + bitmapIndex = bitmapIndex + ) ) - val newComponent: Component = when (clientbound) { - true -> oldComponent.logVal("").transformEmoteIDs(player, true, true).logVal("") - false -> oldComponent.escapeEmoteIDs(player) - } - val newTag = - ComponentSerialization.CODEC.encode( - PaperAdventure.asVanilla(newComponent), - NbtOps.INSTANCE, - CompoundTag() - ).getOrThrow() as CompoundTag - val stream = ByteArrayOutputStream() - DataOutputStream(stream).use { output -> - newTag.write(output) - index += nbtByte.size - input.available() - for (b in stream.toByteArray()) { - list.add(b) - } - } - } + .build() + ) } - return index - } - - inner class EmojyEncoder(var protocolInfo: ProtocolInfo) : - PacketEncoder(protocolInfo) { - - - init { - protocolInfo = (GameProtocols.CLIENTBOUND.bind { EmojyBuffer(it, null) }.takeIf { protocolInfo.id() == ConnectionProtocol.PLAY } ?: protocolInfo) as ProtocolInfo - } - - override fun encode(ctx: ChannelHandlerContext, packet: Packet, byteBuf: ByteBuf) { - if (hasComponent(packet.javaClass)) runCatching { - val newBuffer = EmojyBuffer(Unpooled.buffer(), player) - protocolInfo.codec().encode(newBuffer, packet) - byteBuf.writeBytes(writeNewNbt(newBuffer, true)) - ProtocolSwapHandler.handleOutboundTerminalPacket(ctx, packet) - } else super.encode(ctx, packet, byteBuf) + for (gif in emojy.gifs) gif.baseRegex.findAll(serialized).forEach { _ -> + component = component.replaceText( + TextReplacementConfig.builder() + .match(gif.baseRegex.pattern).once() + .replacement(gif.formattedUnicode(insert = false)) + .build() + ) } - } - - inner class EmojyDecoder(val protocolInfo: ProtocolInfo) : - PacketDecoder(protocolInfo) { - override fun decode(ctx: ChannelHandlerContext, byteBuf: ByteBuf, list: MutableList) { - - runCatching { - val packet = protocolInfo.codec().decode(writeNewNbt(EmojyBuffer(byteBuf, player), false)) - list += packet - ProtocolSwapHandler.handleInboundTerminalPacket(ctx, packet) - }.onFailure { super.decode(ctx, byteBuf, list) } - } + return component } } - override val supported get() = true }