diff --git a/build.gradle.kts b/build.gradle.kts index 65fefa4..3c4f041 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ plugins { id("com.mineinabyss.conventions.autoversion") id("xyz.jpenilla.run-paper") version "2.3.1" // Adds runServer and runMojangMappedServer tasks for testing id("net.minecrell.plugin-yml.paper") version "0.6.0" - id("io.papermc.paperweight.userdev") version "1.7.4" + id("io.papermc.paperweight.userdev") version "1.7.6" } paperweight.reobfArtifactConfiguration.set(ReobfArtifactConfiguration.MOJANG_PRODUCTION) @@ -44,6 +44,7 @@ dependencies { implementation(project(path = ":v1_20_R4")) implementation(project(path = ":v1_21_R1")) implementation(project(path = ":v1_21_R2")) + implementation(project(path = ":v1_21_R3")) } tasks { 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 44fcb08..2e7cc23 100644 --- a/core/src/main/kotlin/com/mineinabyss/emojy/nms/EmojyNMSHandlers.kt +++ b/core/src/main/kotlin/com/mineinabyss/emojy/nms/EmojyNMSHandlers.kt @@ -10,6 +10,7 @@ object EmojyNMSHandlers { "1.20.5", "1.20.6" -> "v1_20_R4" "1.21", "1.21.1" -> "v1_21_R1" "1.21.2", "1.21.3" -> "v1_21_R2" + "1.21.4" -> "v1_21_R3" else -> throw IllegalStateException("Unsupported server version") } runCatching { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 48c0a02..e48eca5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 16ef6ad..9367dc1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,4 +35,4 @@ dependencyResolutionManagement { } } -include("core", "v1_20_R4", "v1_21_R1", "v1_21_R2") +include("core", "v1_20_R4", "v1_21_R1", "v1_21_R2", "v1_21_R3") diff --git a/v1_20_R4/build.gradle.kts b/v1_20_R4/build.gradle.kts index 935cbbb..cf8f5b6 100644 --- a/v1_20_R4/build.gradle.kts +++ b/v1_20_R4/build.gradle.kts @@ -3,7 +3,7 @@ import io.papermc.paperweight.userdev.ReobfArtifactConfiguration plugins { id("com.mineinabyss.conventions.kotlin.jvm") id("com.mineinabyss.conventions.autoversion") - id("io.papermc.paperweight.userdev") version "1.7.4" + id("io.papermc.paperweight.userdev") version "1.7.6" } repositories { diff --git a/v1_21_R1/build.gradle.kts b/v1_21_R1/build.gradle.kts index 1af7d28..3b00084 100644 --- a/v1_21_R1/build.gradle.kts +++ b/v1_21_R1/build.gradle.kts @@ -3,7 +3,7 @@ import io.papermc.paperweight.userdev.ReobfArtifactConfiguration plugins { id("com.mineinabyss.conventions.kotlin.jvm") id("com.mineinabyss.conventions.autoversion") - id("io.papermc.paperweight.userdev") version "1.7.4" + id("io.papermc.paperweight.userdev") version "1.7.6" } repositories { diff --git a/v1_21_R2/build.gradle.kts b/v1_21_R2/build.gradle.kts index d7fe071..6ea1f8a 100644 --- a/v1_21_R2/build.gradle.kts +++ b/v1_21_R2/build.gradle.kts @@ -3,7 +3,7 @@ import io.papermc.paperweight.userdev.ReobfArtifactConfiguration plugins { id("com.mineinabyss.conventions.kotlin.jvm") id("com.mineinabyss.conventions.autoversion") - id("io.papermc.paperweight.userdev") version "1.7.4" + id("io.papermc.paperweight.userdev") version "1.7.6" } repositories { diff --git a/v1_21_R3/build.gradle.kts b/v1_21_R3/build.gradle.kts new file mode 100644 index 0000000..8cfdc6d --- /dev/null +++ b/v1_21_R3/build.gradle.kts @@ -0,0 +1,48 @@ +import io.papermc.paperweight.userdev.ReobfArtifactConfiguration + +plugins { + id("com.mineinabyss.conventions.kotlin.jvm") + id("com.mineinabyss.conventions.autoversion") + id("io.papermc.paperweight.userdev") version "1.7.6" +} + +repositories { + gradlePluginPortal() + maven("https://repo.mineinabyss.com/releases") + maven("https://repo.mineinabyss.com/snapshots") + maven("https://repo.papermc.io/repository/maven-public/") + google() +} + +paperweight.reobfArtifactConfiguration.set(ReobfArtifactConfiguration.MOJANG_PRODUCTION) + +dependencies { + // MineInAbyss platform + compileOnly(idofrontLibs.kotlinx.serialization.json) + compileOnly(idofrontLibs.kotlinx.serialization.kaml) + compileOnly(idofrontLibs.kotlinx.coroutines) + compileOnly(idofrontLibs.minecraft.mccoroutine) + + // Shaded + implementation(idofrontLibs.bundles.idofront.core) + implementation(project(":core")) + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") //NMS +} + +tasks { + + build { + dependsOn(reobfJar) + } + + compileJava { + options.encoding = Charsets.UTF_8.name() + options.release.set(21) + } + javadoc { + options.encoding = Charsets.UTF_8.name() + } + processResources { + filteringCharset = Charsets.UTF_8.name() + } +} diff --git a/v1_21_R3/src/main/kotlin/com/mineinabyss/emojy/nms/v1_21_R3/EmojyListener.kt b/v1_21_R3/src/main/kotlin/com/mineinabyss/emojy/nms/v1_21_R3/EmojyListener.kt new file mode 100644 index 0000000..c58d4e5 --- /dev/null +++ b/v1_21_R3/src/main/kotlin/com/mineinabyss/emojy/nms/v1_21_R3/EmojyListener.kt @@ -0,0 +1,101 @@ +package com.mineinabyss.emojy.nms.v1_21_R3 + +import com.github.shynixn.mccoroutine.bukkit.launch +import com.github.shynixn.mccoroutine.bukkit.ticks +import com.jeff_media.morepersistentdatatypes.DataType +import com.mineinabyss.emojy.* +import com.mineinabyss.idofront.items.editItemMeta +import com.mineinabyss.idofront.textcomponents.miniMsg +import com.mineinabyss.idofront.textcomponents.serialize +import io.papermc.paper.event.player.AsyncChatDecorateEvent +import io.papermc.paper.event.player.PlayerOpenSignEvent +import kotlinx.coroutines.delay +import net.minecraft.core.BlockPos +import net.minecraft.world.level.block.entity.BlockEntityType +import org.bukkit.block.Sign +import org.bukkit.block.sign.Side +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.block.SignChangeEvent +import org.bukkit.event.inventory.PrepareAnvilEvent +import org.bukkit.event.player.PlayerEditBookEvent + +@Suppress("UnstableApiUsage") +class EmojyListener : Listener { + + // Replace with result not original message to avoid borking other chat formatting + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + fun AsyncChatDecorateEvent.onPlayerChat() { + result(result().escapeEmoteIDs(player())) + } + + @EventHandler + fun SignChangeEvent.onSign() { + val state = (block.state as Sign) + val type = DataType.asList(DataType.STRING) + val sideLines = lines().map { it.serialize() }.toList() + val frontLines = if (side == Side.FRONT) sideLines else state.persistentDataContainer.getOrDefault( + ORIGINAL_SIGN_FRONT_LINES, + type, + mutableListOf("", "", "", "") + ) + val backLines = if (side == Side.BACK) sideLines else state.persistentDataContainer.getOrDefault( + ORIGINAL_SIGN_BACK_LINES, + type, + mutableListOf("", "", "", "") + ) + + state.persistentDataContainer.set(ORIGINAL_SIGN_FRONT_LINES, type, frontLines) + state.persistentDataContainer.set(ORIGINAL_SIGN_BACK_LINES, type, backLines) + state.update(true) + + lines().forEachIndexed { index, s -> + line(index, s?.escapeEmoteIDs(player)?.transformEmotes()) + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun PlayerOpenSignEvent.onSignEdit() { + if (cause == PlayerOpenSignEvent.Cause.PLACE) return + + sign.persistentDataContainer.get( + when (sign.getInteractableSideFor(player)) { + Side.FRONT -> ORIGINAL_SIGN_FRONT_LINES + Side.BACK -> ORIGINAL_SIGN_BACK_LINES + }, DataType.asList(DataType.STRING) + )?.forEachIndexed { index, s -> + sign.getSide(side).line(index, s.miniMsg()) + } + sign.update(true) + isCancelled = true + emojy.plugin.launch { + delay(2.ticks) + (player as CraftPlayer).handle.level() + .getBlockEntity(BlockPos(sign.x, sign.y, sign.z), BlockEntityType.SIGN).ifPresent { + it.setAllowedPlayerEditor(player.uniqueId) + (player as CraftPlayer).handle.openTextEdit(it, side == Side.FRONT) + } + } + } + + @EventHandler + fun PrepareAnvilEvent.onAnvil() { + result = result?.editItemMeta { + if (view.renameText == null || result?.itemMeta?.hasDisplayName() != true) { + persistentDataContainer.remove(ORIGINAL_ITEM_RENAME_TEXT) + } else persistentDataContainer.set(ORIGINAL_ITEM_RENAME_TEXT, DataType.STRING, view.renameText!!) + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + fun PlayerEditBookEvent.onBookEdit() { + if (isSigning) newBookMeta = newBookMeta.apply { + pages(pages().map { + it.escapeEmoteIDs(player).transformEmotes().unescapeEmoteIds() + }) + } + } +} + diff --git a/v1_21_R3/src/main/kotlin/com/mineinabyss/emojy/nms/v1_21_R3/EmojyNMSHandler.kt b/v1_21_R3/src/main/kotlin/com/mineinabyss/emojy/nms/v1_21_R3/EmojyNMSHandler.kt new file mode 100644 index 0000000..c6dff71 --- /dev/null +++ b/v1_21_R3/src/main/kotlin/com/mineinabyss/emojy/nms/v1_21_R3/EmojyNMSHandler.kt @@ -0,0 +1,181 @@ +@file:Suppress("unused") + +package com.mineinabyss.emojy.nms.v1_21_R3 + +import com.jeff_media.morepersistentdatatypes.DataType +import com.mineinabyss.emojy.* +import com.mineinabyss.emojy.nms.IEmojyNMSHandler +import com.mineinabyss.idofront.items.editItemMeta +import com.mineinabyss.idofront.plugin.listeners +import com.mineinabyss.idofront.textcomponents.miniMsg +import io.netty.channel.Channel +import io.netty.channel.ChannelDuplexHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelPromise +import io.papermc.paper.adventure.AdventureComponent +import io.papermc.paper.adventure.PaperAdventure +import io.papermc.paper.network.ChannelInitializeListenerHolder +import net.minecraft.core.NonNullList +import net.minecraft.network.Connection +import net.minecraft.network.chat.ChatType +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.contents.PlainTextContents.LiteralContents +import net.minecraft.network.protocol.Packet +import net.minecraft.network.protocol.common.ClientboundDisconnectPacket +import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket +import net.minecraft.network.protocol.common.ClientboundServerLinksPacket +import net.minecraft.network.protocol.game.* +import net.minecraft.network.syncher.EntityDataSerializer +import net.minecraft.network.syncher.EntityDataSerializers +import net.minecraft.network.syncher.SynchedEntityData +import net.minecraft.world.item.ItemStack +import org.bukkit.NamespacedKey +import org.bukkit.craftbukkit.inventory.CraftItemStack +import org.bukkit.entity.Player +import org.bukkit.inventory.AnvilInventory +import java.util.* + +class EmojyNMSHandler(emojy: EmojyPlugin) : IEmojyNMSHandler { + + + init { + emojy.listeners(EmojyListener()) + + val key = NamespacedKey.fromString("packet_listener", emojy) + ChannelInitializeListenerHolder.addListener(key!!) { channel: Channel -> + channel.pipeline().addBefore("packet_handler", key.toString(), object : ChannelDuplexHandler() { + private val connection = channel.pipeline()["packet_handler"] as Connection + + override fun write(ctx: ChannelHandlerContext, packet: Any, promise: ChannelPromise) { + ctx.write(transformPacket(packet, connection), promise) + } + + override fun channelRead(ctx: ChannelHandlerContext, packet: Any) { + ctx.fireChannelRead(when (packet) { + is ServerboundRenameItemPacket -> ServerboundRenameItemPacket(packet.name.escapeEmoteIDs(connection.player.bukkitEntity)) + else -> packet + }) + } + } + ) + } + } + + companion object { + private fun Connection.locale() = player.bukkitEntity.locale() + fun transformPacket(packet: Any, connection: Connection): Any { + return when (packet) { + is ClientboundBundlePacket -> ClientboundBundlePacket(packet.subPackets().map { transformPacket(it, connection) as Packet }) + is ClientboundServerLinksPacket -> ClientboundServerLinksPacket(packet.links.map { net.minecraft.server.ServerLinks.UntrustedEntry(it.type.mapRight { it.transformEmotes(connection.locale()) }, it.link) }) + is ClientboundSetScorePacket -> ClientboundSetScorePacket(packet.owner, packet.objectiveName, packet.score, packet.display.map { it.transformEmotes(connection.locale()) }, packet.numberFormat) + is ClientboundServerDataPacket -> ClientboundServerDataPacket(packet.motd.transformEmotes(connection.locale()), packet.iconBytes) + is ClientboundDisguisedChatPacket -> ClientboundDisguisedChatPacket(packet.message.transformEmotes(connection.locale(), true).unescapeEmoteIds(), packet.chatType) + is ClientboundPlayerChatPacket -> ClientboundPlayerChatPacket(packet.sender, packet.index, packet.signature, packet.body, (packet.unsignedContent ?: PaperAdventure.asVanilla(packet.body.content.miniMsg()))?.transformEmotes(connection.locale(), true)?.unescapeEmoteIds(), packet.filterMask, ChatType.bind(packet.chatType.chatType.unwrapKey().get(), connection.player.registryAccess(), packet.chatType.name.transformEmotes(connection.locale(), true))) + is ClientboundSystemChatPacket -> ClientboundSystemChatPacket(packet.content.transformEmotes(connection.locale(), true).unescapeEmoteIds(), packet.overlay) + is ClientboundSetTitleTextPacket -> ClientboundSetTitleTextPacket(packet.text.transformEmotes(connection.locale())) + is ClientboundSetSubtitleTextPacket -> ClientboundSetSubtitleTextPacket(packet.text.transformEmotes(connection.locale())) + is ClientboundSetActionBarTextPacket -> ClientboundSetActionBarTextPacket(packet.text.transformEmotes(connection.locale())) + is ClientboundOpenScreenPacket -> ClientboundOpenScreenPacket(packet.containerId, packet.type, packet.title.transformEmotes(connection.locale())) + is ClientboundTabListPacket -> ClientboundTabListPacket(packet.header.transformEmotes(connection.locale()), packet.footer.transformEmotes(connection.locale())) + is ClientboundResourcePackPushPacket -> ClientboundResourcePackPushPacket(packet.id, packet.url, packet.hash, packet.required, packet.prompt.map { it.transformEmotes(connection.locale()) }) + is ClientboundDisconnectPacket -> ClientboundDisconnectPacket(packet.reason.transformEmotes(connection.locale())) + is ClientboundSetEntityDataPacket -> ClientboundSetEntityDataPacket(packet.id, packet.packedItems.map { + when (val value = it.value) { + is AdventureComponent -> SynchedEntityData.DataValue(it.id, it.serializer as EntityDataSerializer, + AdventureComponent(value.`adventure$component`().transformEmotes(connection.locale())) + ) + is Component -> SynchedEntityData.DataValue(it.id, EntityDataSerializers.COMPONENT, + value.transformEmotes(connection.locale()) + ) + else -> it + } + }) + is ClientboundContainerSetSlotPacket -> ClientboundContainerSetSlotPacket(packet.containerId, packet.stateId, packet.slot, packet.item.transformItemNameLore(connection.player.bukkitEntity)) + is ClientboundContainerSetContentPacket -> ClientboundContainerSetContentPacket( + packet.containerId, packet.stateId, NonNullList.of(packet.items.first(), + *packet.items.map { + val player = connection.player.bukkitEntity + val inv = player.openInventory.topInventory + val bukkit = CraftItemStack.asBukkitCopy(it) + + // If item is firstItem in AnvilInventory we want to set it to have the plain-text displayname + if (inv is AnvilInventory && inv.firstItem == bukkit) + bukkit.itemMeta?.persistentDataContainer?.get(ORIGINAL_ITEM_RENAME_TEXT, DataType.STRING)?.let { og -> + CraftItemStack.asNMSCopy(bukkit.editItemMeta { + setDisplayName(og) + }) + } ?: it.transformItemNameLore(player) + else it.transformItemNameLore(player) + }.toTypedArray()), packet.carriedItem) + is ClientboundBossEventPacket -> { + // Access the private field 'operation' + val operationField = ClientboundBossEventPacket::class.java.getDeclaredField("operation").apply { isAccessible = true } + val operation = operationField.get(packet) + + when (operation::class.java.simpleName) { + "AddOperation" -> { + val nameField = operation::class.java.getDeclaredField("name").apply { isAccessible = true } + nameField.set(operation, (nameField.get(operation) as Component).transformEmotes(connection.locale())) + } + "UpdateNameOperation" -> { + val accessorMethod = operation::class.java.methods.find { it.name == "name" } + accessorMethod?.isAccessible = true + if (accessorMethod != null) { + val updateNameOperationClass = operation::class.java.enclosingClass.declaredClasses.find { + it.simpleName == "UpdateNameOperation" + } ?: throw IllegalStateException("UpdateNameOperation class not found") + + val constructor = updateNameOperationClass.getDeclaredConstructor(Component::class.java).apply { isAccessible = true } + val name = (accessorMethod.invoke(operation) as Component).transformEmotes(connection.locale()) + val updatedOperation = constructor.newInstance(name) + + operationField.set(packet, updatedOperation) + } + } + } + + packet + } + is ClientboundPlayerInfoUpdatePacket -> + ClientboundPlayerInfoUpdatePacket(packet.actions(), packet.entries().map { + ClientboundPlayerInfoUpdatePacket.Entry( + it.profileId, it.profile, it.listed, it.latency, it.gameMode, + it.displayName?.transformEmotes(connection.locale()), it.listed, it.listOrder, it.chatSession + ) + }) + else -> packet + } + } + + private fun ItemStack.transformItemNameLore(player: Player): ItemStack { + val locale = player.locale() + return CraftItemStack.asNMSCopy(CraftItemStack.asBukkitCopy(this).editItemMeta { + itemName(if (hasItemName()) itemName().transformEmotes(locale) else null) + lore(lore()?.map { l -> l.transformEmotes(locale) }) + persistentDataContainer.get(ORIGINAL_ITEM_RENAME_TEXT, DataType.STRING)?.let { + displayName(it.escapeEmoteIDs(player).transformEmotes().unescapeEmoteIds().miniMsg()) + } + }) + } + + fun Component.transformEmotes(locale: Locale? = null, insert: Boolean = false): Component { + return when { + // Sometimes a NMS component is partially Literal, so ensure entire thing is just one LiteralContent with no extra data + contents is LiteralContents && style.isEmpty && siblings.isEmpty() -> (contents as LiteralContents).text.miniMsg() + else -> PaperAdventure.asAdventure(this) + }.transformEmotes(locale, insert).let(PaperAdventure::asVanilla) + } + + fun Component.escapeEmoteIDs(player: Player?): Component { + return PaperAdventure.asVanilla((PaperAdventure.asAdventure(this)).escapeEmoteIDs(player)) + } + + fun Component.unescapeEmoteIds(): Component { + return PaperAdventure.asVanilla(PaperAdventure.asAdventure(this).unescapeEmoteIds()) + } + + + } + + override val supported get() = true +}