diff --git a/BANNER.md b/BANNER.md index 904666e83..aa8dd8a56 100644 --- a/BANNER.md +++ b/BANNER.md @@ -67,7 +67,7 @@ BetterModel supports **player model with using user's custom skin without textur ## 🏗️ Supported environment -[![](https://img.shields.io/badge/minecraft-1.21.4%7E26.1.x-8FCA5C?style=for-the-badge)](https://www.minecraft.net/en-us/download/server) +[![](https://img.shields.io/badge/minecraft-1.21.4%7E26.2.x-8FCA5C?style=for-the-badge)](https://www.minecraft.net/en-us/download/server) [![](https://img.shields.io/badge/java-25%7E-ED8B00?style=for-the-badge)](https://adoptium.net/) ### Bukkit diff --git a/README.md b/README.md index 9f4cd5416..6a149f87e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ BetterModel aims to be a reliable engine that provides stable, high-quality anim ## 🛠️ Build info -[![](https://img.shields.io/badge/minecraft-1.21.4%7E26.1.x-8FCA5C)](https://www.minecraft.net/en-us/download/server) +[![](https://img.shields.io/badge/minecraft-1.21.4%7E26.2.x-8FCA5C)](https://www.minecraft.net/en-us/download/server) [![](https://img.shields.io/badge/java-25%7E-ED8B00)](https://adoptium.net/) #### Build diff --git a/api/src/main/java/kr/toxicity/model/api/nms/NMSVersion.java b/api/src/main/java/kr/toxicity/model/api/nms/NMSVersion.java index 9fe0cb7cb..4d6cb0a60 100644 --- a/api/src/main/java/kr/toxicity/model/api/nms/NMSVersion.java +++ b/api/src/main/java/kr/toxicity/model/api/nms/NMSVersion.java @@ -51,7 +51,12 @@ public enum NMSVersion { * Minecraft 26.1.x * @since 3.0.0 */ - V26_R1(84) + V26_R1(84), + /** + * Minecraft 26.2.x + * @since 3.2.0 + */ + V26_R2(88) ; /** * The resource pack format version (pack.mcmeta). diff --git a/api/src/main/java/kr/toxicity/model/api/version/MinecraftVersion.java b/api/src/main/java/kr/toxicity/model/api/version/MinecraftVersion.java index 906040521..ca1bc17ac 100644 --- a/api/src/main/java/kr/toxicity/model/api/version/MinecraftVersion.java +++ b/api/src/main/java/kr/toxicity/model/api/version/MinecraftVersion.java @@ -20,6 +20,10 @@ * @param patch minor update */ public record MinecraftVersion(int major, int minor, int patch) implements Comparable { + /** + * 26.2 + */ + public static final MinecraftVersion V26_2 = of(26, 2, 0); /** * 26.1.2 */ diff --git a/buildSrc/src/main/kotlin/Extensions.kt b/buildSrc/src/main/kotlin/Extensions.kt index 753517610..c7ea97de8 100644 --- a/buildSrc/src/main/kotlin/Extensions.kt +++ b/buildSrc/src/main/kotlin/Extensions.kt @@ -8,9 +8,7 @@ val Project.libs get() = rootProject.extensions.getByName("libs") as LibrariesForLibs val LATEST_VERSION = listOf( - "26.1", - "26.1.1", - "26.1.2" + "26.2" ) val SUPPORTED_VERSIONS = buildList { @@ -23,6 +21,9 @@ val SUPPORTED_VERSIONS = buildList { "1.21.9", "1.21.10", "1.21.11", + "26.1", + "26.1.1", + "26.1.2" )) addAll(LATEST_VERSION) } diff --git a/core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/BetterModelProperties.kt b/core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/BetterModelProperties.kt index 3ee826f35..412a8f4ff 100644 --- a/core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/BetterModelProperties.kt +++ b/core/bukkit-core/src/main/kotlin/kr/toxicity/model/bukkit/BetterModelProperties.kt @@ -27,7 +27,7 @@ import org.bstats.bukkit.Metrics import org.bukkit.Bukkit import org.semver4j.Semver -private typealias Latest = kr.toxicity.model.bukkit.nms.v26_R1.NMSImpl +private typealias Latest = kr.toxicity.model.bukkit.nms.v26_R2.NMSImpl internal class BetterModelProperties( private val plugin: AbstractBetterModelPlugin @@ -37,7 +37,8 @@ internal class BetterModelProperties( val version = parse(Bukkit.getBukkitVersion().substringBefore('-')) val nms = when (version) { - V26_1, V26_1_1, V26_1_2 -> Latest() + V26_2 -> Latest() + V26_1, V26_1_1, V26_1_2 -> kr.toxicity.model.bukkit.nms.v26_R1.NMSImpl() V1_21_11 -> kr.toxicity.model.bukkit.nms.v1_21_R7.NMSImpl() V1_21_9, V1_21_10 -> kr.toxicity.model.bukkit.nms.v1_21_R6.NMSImpl() V1_21_6, V1_21_7, V1_21_8 -> kr.toxicity.model.bukkit.nms.v1_21_R5.NMSImpl() diff --git a/gradle.properties b/gradle.properties index 8b617ca8b..050100d59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 project_version=3.2.0 -minecraft_version=26.1.2 +minecraft_version=26.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c642339f7..0d76294ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,11 +7,11 @@ shadow = "9.4.2" minotaur = "2.9.0" hangarPublish = "0.1.4" -fabric-api = "0.151.0+26.1.2" +fabric-api = "0.152.1+26.2" fabric-language-kotlin = "1.13.12+kotlin.2.4.0" cloud-bukkit = "2.0.0-beta.15" -cloud-mod = "2.0.0-beta.16" +cloud-mod = "2.0.0-SNAPSHOT" #TODO cloud-mod = "2.0.0-beta.17" cloud-core = "2.0.0" configurate = "4.2.0" @@ -28,14 +28,14 @@ adventure-api = "net.kyori:adventure-api:4.26.1" adventure-examination = "net.kyori:examination-api:1.3.0" adventure-option = "net.kyori:option:1.1.0" adventure-platform-bukkit = "net.kyori:adventure-platform-bukkit:4.4.1" -adventure-platform-fabric = "net.kyori:adventure-platform-fabric:6.9.0" +adventure-platform-fabric = "net.kyori:adventure-platform-fabric:7.0.0-SNAPSHOT" asm-tree = "org.ow2.asm:asm-tree:9.10.1" fabric-loader = "net.fabricmc:fabric-loader:0.19.3" fabric-language-kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric-language-kotlin" } -polymer-resource-pack = "eu.pb4:polymer-resource-pack:0.16.5+26.1.2" +polymer-resource-pack = "eu.pb4:polymer-resource-pack:0.17.0+26.2-rc-2" configurate-core = { module = "org.spongepowered:configurate-core", version.ref = "configurate" } configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" } diff --git a/nms/v26_R2/build.gradle.kts b/nms/v26_R2/build.gradle.kts new file mode 100644 index 000000000..f792875d3 --- /dev/null +++ b/nms/v26_R2/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.convention.paperweight) +} + +dependencies { + paperweight.paperDevBundle("26.2-rc-2.build.+") +} diff --git a/nms/v26_R2/src/main/java/kr/toxicity/model/bukkit/nms/v26_R2/AbstractHitBox.java b/nms/v26_R2/src/main/java/kr/toxicity/model/bukkit/nms/v26_R2/AbstractHitBox.java new file mode 100644 index 000000000..618d3f6d2 --- /dev/null +++ b/nms/v26_R2/src/main/java/kr/toxicity/model/bukkit/nms/v26_R2/AbstractHitBox.java @@ -0,0 +1,32 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2; + +import kr.toxicity.model.api.nms.HitBox; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +abstract class AbstractHitBox extends ArmorStand implements HitBox { + + AbstractHitBox(@NotNull Level level) { + super(EntityTypes.ARMOR_STAND, level); + } + + @Override //Only for provide compiler hint for Kotlin jvm + public final boolean equals(@Nullable Object other) { + return super.equals(other); + } + + @Override //Only for provide compiler hint for Kotlin jvm + public final int hashCode() { + return super.hashCode(); + } +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BaseEntityImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BaseEntityImpl.kt new file mode 100644 index 000000000..23cc05edb --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BaseEntityImpl.kt @@ -0,0 +1,85 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity +import kr.toxicity.model.api.platform.PlatformEntity +import kr.toxicity.model.api.platform.PlatformLocation +import kr.toxicity.model.api.platform.PlatformPlayer +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.effect.MobEffects +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.ai.attributes.Attributes +import org.bukkit.craftbukkit.entity.CraftEntity +import org.bukkit.persistence.PersistentDataHolder +import org.joml.Vector3f +import java.util.* +import java.util.stream.Stream + +internal data class BaseEntityImpl( + private val delegate: CraftEntity +) : BaseBukkitEntity, PersistentDataHolder by delegate { + override fun customName(): AdventureComponent? = handle().run { + if (this is ServerPlayer) (customName ?: name).asAdventure() else customName?.asAdventure()?.takeIf { + isCustomNameVisible + } + } + + override fun entity(): org.bukkit.entity.Entity = delegate + override fun handle(): Entity = delegate.vanillaEntity + override fun uuid(): UUID = delegate.uniqueId + override fun id(): Int = handle().id + override fun dead(): Boolean = (handle() as? LivingEntity)?.isDeadOrDying == true || handle().removalReason != null || !handle().valid + override fun invisible(): Boolean = handle().isInvisible || (handle() as? LivingEntity)?.hasEffect(MobEffects.INVISIBILITY) == true + override fun glow(): Boolean = handle().isCurrentlyGlowing + + override fun onWalk(): Boolean { + return handle().isWalking() + } + + override fun scale(): Double { + val handle = handle() + return if (handle is LivingEntity) handle.scale.toDouble() else 1.0 + } + + override fun pitch(): Float = handle().xRot + override fun ground(): Boolean = handle().onGround() + override fun bodyYaw(): Float = handle().let { if (it is LivingEntity) it.yBodyRot else it.yRot } + override fun yaw(): Float = handle().yRot + override fun headYaw(): Float = handle().let { if (it is LivingEntity) it.yHeadRot else it.yRot } + override fun fly(): Boolean = handle().isFlying + + override fun damageTick(): Float { + val handle = handle() + if (handle !is LivingEntity) return 0F + val duration = handle.invulnerableDuration.toFloat() + if (duration <= 0F) return 0F + val knockBack = 1 - (handle.getAttribute(Attributes.KNOCKBACK_RESISTANCE)?.value?.toFloat() ?: 0F) + return handle.invulnerableTime.toFloat() / duration * knockBack + } + + override fun walkSpeed(): Float { + val handle = handle() + if (handle !is LivingEntity) return 0F + if (!handle.onGround) return 1F + val speed = handle.getEffect(MobEffects.SPEED)?.amplifier ?: 0 + val slow = handle.getEffect(MobEffects.SLOWNESS)?.amplifier ?: 0 + return (1F + (speed - slow) * 0.2F) + .coerceAtLeast(0.2F) + .coerceAtMost(2F) + } + + override fun passengerPosition(dest: Vector3f): Vector3f { + return handle().passengerPosition(dest) + } + + override fun platform(): PlatformEntity = delegate.wrap() + override fun trackedBy(): Stream = delegate.trackedBy.stream().map { it.wrap() } + override fun location(): PlatformLocation = delegate.location.wrap() +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BasePlayerImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BasePlayerImpl.kt new file mode 100644 index 000000000..ad4f8f5f7 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BasePlayerImpl.kt @@ -0,0 +1,48 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity +import kr.toxicity.model.api.bukkit.entity.BaseBukkitPlayer +import kr.toxicity.model.api.nms.Profiled +import kr.toxicity.model.api.platform.PlatformPlayer +import kr.toxicity.model.api.player.PlayerSkinParts +import kr.toxicity.model.api.profile.ModelProfile +import net.minecraft.util.Mth +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.bukkit.entity.Player +import java.util.stream.Stream + +internal data class BasePlayerImpl( + private val delegate: CraftPlayer, + private val profile: () -> ModelProfile, + private val skinParts: () -> PlayerSkinParts +) : BaseBukkitEntity by BaseEntityImpl(delegate), BaseBukkitPlayer, Profiled by ProfiledImpl(PlayerArmorImpl(delegate), profile, skinParts) { + + override fun entity(): Player = delegate + + override fun updateInventory() { + delegate.handle.containerMenu.sendAllDataToRemote() + } + + override fun platform(): PlatformPlayer = delegate.wrap() + + override fun trackedBy(): Stream = Stream.concat( + Stream.of(delegate), + delegate.trackedBy.stream() + ).map { + it.wrap() + } + + override fun bodyYaw(): Float { + val handle = delegate.handle + var yaw = -45 * handle.xMovement() + if (handle.zMovement() < 0) yaw *= -1 + return Mth.wrapDegrees(handle.yHeadRot + yaw) + } +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BukkitWrappers.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BukkitWrappers.kt new file mode 100644 index 000000000..6b038f36a --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/BukkitWrappers.kt @@ -0,0 +1,36 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.bukkit.platform.* +import kr.toxicity.model.api.bukkit.platform.BukkitAdapter.adapt +import kr.toxicity.model.api.bukkit.platform.BukkitItemStack +import kr.toxicity.model.api.platform.* +import org.bukkit.Location +import org.bukkit.OfflinePlayer +import org.bukkit.World +import org.bukkit.entity.Entity +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack + +internal fun Entity.wrap() = adapt(this) +internal fun LivingEntity.wrap() = adapt(this) +internal fun OfflinePlayer.wrap() = adapt(this) +internal fun Player.wrap() = adapt(this) +internal fun Location.wrap() = adapt(this) +internal fun World.wrap() = adapt(this) +internal fun ItemStack.wrap() = adapt(this) + +internal fun PlatformEntity.unwarp(): Entity = (this as BukkitEntity).source() +internal fun PlatformLivingEntity.unwarp(): LivingEntity = (this as BukkitLivingEntity).source() +internal fun PlatformOfflinePlayer.unwarp(): OfflinePlayer = (this as BukkitOfflinePlayer).source() +internal fun PlatformPlayer.unwarp(): Player = (this as BukkitPlayer).source() +internal fun PlatformLocation.unwarp(): Location = (this as BukkitLocation).source() +internal fun PlatformWorld.unwarp(): World = (this as BukkitWorld).source() +internal fun PlatformItemStack.unwarp(): ItemStack = (this as BukkitItemStack).source() diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/EntityData.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/EntityData.kt new file mode 100644 index 000000000..bd6da9f84 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/EntityData.kt @@ -0,0 +1,126 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.nms.AnimationBundler +import kr.toxicity.model.api.util.MathUtil +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket +import net.minecraft.network.syncher.EntityDataAccessor +import net.minecraft.network.syncher.SynchedEntityData +import net.minecraft.world.entity.Display +import net.minecraft.world.entity.Display.ItemDisplay +import net.minecraft.world.entity.Entity +import org.joml.Quaternionf +import org.joml.Vector3f +import java.lang.reflect.Field + +internal fun Field.toEntityDataAccessor() = run { + isAccessible = true + get(null) as EntityDataAccessor<*> +} + +internal fun Class<*>.accessors() = declaredFields.filter { f -> + EntityDataAccessor::class.java.isAssignableFrom(f.type) +}.map { + it.toEntityDataAccessor() +} + +internal val DISPLAY_SET = Display::class.java.accessors() +internal val SHARED_FLAG = Entity::class.java.accessors().first().id +internal val ITEM_DISPLAY_ID = ItemDisplay::class.java.accessors().map { + it.id +} +internal val ITEM_SERIALIZER = ItemDisplay::class.java.accessors().first() +internal val ITEM_ENTITY_DATA = buildList { + add(SHARED_FLAG) + addAll(ITEM_DISPLAY_ID) + add(Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID.id) + DISPLAY_SET.subList(7, DISPLAY_SET.size).mapTo(this) { it.id } +}.toIntSet() + +@Suppress("UNCHECKED_CAST") +private val DISPLAY_INTERPOLATION_DELAY = (DISPLAY_SET.first() as EntityDataAccessor).run { + SynchedEntityData.DataValue(id, serializer, 0) +} +@Suppress("UNCHECKED_CAST") +internal val DISPLAY_INTERPOLATION_DURATION = DISPLAY_SET[1] as EntityDataAccessor +@Suppress("UNCHECKED_CAST") +internal val DISPLAY_TRANSLATION = DISPLAY_SET[3] as EntityDataAccessor +@Suppress("UNCHECKED_CAST") +internal val DISPLAY_SCALE = DISPLAY_SET[4] as EntityDataAccessor +@Suppress("UNCHECKED_CAST") +internal val DISPLAY_ROTATION = DISPLAY_SET[5] as EntityDataAccessor + + +internal class TransformationData { + + private var _duration = 0 + private val duration get() = SynchedEntityData.DataValue(DISPLAY_INTERPOLATION_DURATION.id, DISPLAY_INTERPOLATION_DURATION.serializer, _duration) + private val translation = Item(Vector3f(), DISPLAY_TRANSLATION, MathUtil::isSimilar, Vector3f::set) + private val scale = Item(Vector3f(), DISPLAY_SCALE, MathUtil::isSimilar, Vector3f::set) + private val rotation = Item(Quaternionf(), DISPLAY_ROTATION, MathUtil::isSimilar, Quaternionf::set) + + fun packDirty(entityId: Int, dest: AnimationBundler) { + val i = translation.cleanIndex + scale.cleanIndex + rotation.cleanIndex + if (i == 0) return + (dest.mod as ModAnimationBundlerImpl).append(entityId) { + dest.standard += ClientboundSetEntityDataPacket(entityId, buildList(i + 2) { + add(DISPLAY_INTERPOLATION_DELAY) + translation.value?.let { appendPosition(it.value); add(it) } + rotation.value?.let { appendRotation(it.value); add(it) } + scale.value?.let { appendScale(it.value); add(it) } + appendDuration(_duration); add(duration) + }) + } + } + + fun transform( + duration: Int, + translation: Vector3f, + scale: Vector3f, + rotation: Quaternionf + ) { + _duration = duration + this.translation.set(translation) + this.scale.set(scale) + this.rotation.set(rotation) + } + + fun pack() = listOf( + DISPLAY_INTERPOLATION_DELAY, + duration, + translation.forceValue, + scale.forceValue, + rotation.forceValue + ) + + private class Item( + initialValue: T, + private val accessor: EntityDataAccessor, + private val dirtyChecker: (T, T) -> Boolean, + private val setter: (T, T) -> Unit + ) { + private val _t: T = initialValue + private var _dirty = false + + val dirty get() = _dirty + val cleanIndex get() = if (dirty) 1 else 0 + val value get() = if (_dirty) { + _dirty = false + forceValue + } else null + val forceValue get() = SynchedEntityData.DataValue(accessor.id, accessor.serializer, _t) + + fun set(other: T) { + if (dirtyChecker(_t, other)) return + _dirty = true + setter(_t, other) + } + } +} + diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/Functions.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/Functions.kt new file mode 100644 index 000000000..1601b8146 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/Functions.kt @@ -0,0 +1,215 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import io.netty.buffer.Unpooled +import io.papermc.paper.adventure.PaperAdventure +import io.papermc.paper.configuration.GlobalConfiguration +import it.unimi.dsi.fastutil.ints.IntSet +import kr.toxicity.model.api.BetterModel +import kr.toxicity.model.api.bukkit.BetterModelBukkit +import kr.toxicity.model.api.tracker.EntityTrackerRegistry +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket +import net.minecraft.network.syncher.SynchedEntityData +import net.minecraft.network.syncher.SynchedEntityData.DataItem +import net.minecraft.network.syncher.SynchedEntityData.DataValue +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.* +import net.minecraft.world.entity.ai.goal.RangedAttackGoal +import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal +import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack +import net.minecraft.world.phys.Vec3 +import org.bukkit.Bukkit +import org.bukkit.craftbukkit.entity.CraftEntity +import org.bukkit.craftbukkit.inventory.CraftItemStack +import org.bukkit.craftbukkit.util.CraftChatMessage +import org.joml.Vector3f +import java.util.* + +internal inline fun createAdaptedFieldGetter(noinline paperGetter: (T) -> R): (T) -> R { + return if (BetterModelBukkit.IS_PAPER) paperGetter else createAdaptedFieldGetter() +} +internal inline fun createAdaptedFieldGetter(): (T) -> R { + return T::class.java.declaredFields.first { + R::class.java.isAssignableFrom(it.type) + }.apply { + isAccessible = true + }.let { getter -> + { t -> + getter[t] as R + } + } +} + +internal fun dirtyChecked(hash: () -> H, function: (H) -> T): () -> T { + val lock = Any() + var h = hash() + var value = function(h) + return { + val newH = hash() + when { + h === newH -> value + h == newH -> value + else -> synchronized(lock) { + h = newH + value = function(h) + value + } + } + } +} + +internal val CONFIG get() = BetterModel.config() +internal val EMPTY_ITEM = VanillaItemStack.EMPTY +internal fun BukkitItemStack.asVanilla() = CraftItemStack.asNMSCopy(this) +internal fun VanillaItemStack.asBukkit() = CraftItemStack.asCraftMirror(this) + +internal val ONLINE_MODE by lazy(LazyThreadSafetyMode.NONE) { + if (BetterModelBukkit.IS_PAPER) GlobalConfiguration.get().proxies.isProxyOnlineMode else Bukkit.getOnlineMode() +} + +internal fun List.toIntSet(): IntSet = IntSet.of(*toIntArray()) + +internal fun Entity.passengerPosition(dest: Vector3f): Vector3f { + return attachments.get(EntityAttachment.PASSENGER, 0, yRot).let { v -> + dest.set(v.x.toFloat(), v.y.toFloat(), v.z.toFloat()) + } +} + +private val DATA_ITEMS = SynchedEntityData::class.java.declaredFields.first { + it.type.isArray +}.apply { + isAccessible = true +} + +internal fun SynchedEntityData.pack( + clean: Boolean = false, + itemFilter: (DataItem<*>) -> Boolean = { true }, + valueFilter: (DataValue<*>) -> Boolean = { true }, + required: (List, DataValue<*>>>) -> Boolean = { it.isNotEmpty() } +): List>? = (DATA_ITEMS[this] as Array<*>) + .mapNotNull map@ { + val item = (it as? DataItem<*>)?.takeIf(itemFilter) ?: return@map null + val value = item.value().takeIf(valueFilter) ?: return@map null + item to value + } + .takeIf(required) + ?.map { + if (clean) it.first.isDirty = false + it.second + } + +internal fun Entity.isWalking(): Boolean { + return controllingPassenger?.isWalking() ?: when (this) { + is Mob -> navigation.isInProgress || goalSelector.availableGoals.any { + it.isRunning && when (it.goal) { + is RangedAttackGoal, is RangedCrossbowAttackGoal<*>, is RangedBowAttackGoal<*> -> true + else -> false + } + } + is ServerPlayer -> xMovement() != 0F || zMovement() != 0F + else -> false + } +} + +internal fun ServerPlayer.xMovement(): Float { + val leftMovement: Boolean = lastClientInput.left() + val rightMovement: Boolean = lastClientInput.right() + return if (leftMovement == rightMovement) 0F else if (leftMovement) 1F else -1F +} + +internal fun ServerPlayer.yMovement(): Float = if (isJump()) 1F else if (lastClientInput.shift) -1F else 0F + +internal fun ServerPlayer.zMovement(): Float { + val forwardMovement: Boolean = lastClientInput.forward() + val backwardMovement: Boolean = lastClientInput.backward() + return if (forwardMovement == backwardMovement) 0F else if (forwardMovement) 1F else -1F +} + +internal fun ServerPlayer.isJump() = lastClientInput.jump() + +internal val Entity.isFlying: Boolean + get() = when (this) { + is Mob -> isNoAi + is Player -> abilities.flying + is LivingEntity -> isFallFlying + else -> false + } + +internal val CraftEntity.vanillaEntity: Entity + get() = if (BetterModelBukkit.IS_PAPER) handleRaw else handle + +internal fun Entity.moveTo(vec: Vec3) = snapTo(vec) +internal fun Entity.moveTo(x: Double, y: Double, z: Double, yaw: Float, pitch: Float) = snapTo(x, y, z, yaw, pitch) + +internal inline fun useByteBuf(block: (FriendlyByteBuf) -> T): T { + val buffer = FriendlyByteBuf(Unpooled.buffer()) + return try { + block(buffer) + } finally { + buffer.release() + } +} + +internal fun EntityTrackerRegistry.entityFlag(uuid: UUID, byte: Byte): Byte { + var b = byte.toInt() + val hideOption = hideOption(uuid) + if (hideOption.fire()) b = b and 1.inv() + if (hideOption.visibility()) b = b or (1 shl 5) + if (hideOption.glowing()) b = b and (1 shl 6).inv() + return b.toByte() +} + +internal fun Vector3f.toVanilla() = Vec3(x.toDouble(), y.toDouble(), z.toDouble()) +internal fun Vec3.toBukkit() = Vector3f(x.toFloat(), y.toFloat(), z.toFloat()) + +internal inline fun LivingEntity.toEquipmentPacket(mapper: (EquipmentSlot) -> ItemStack? = { getItemBySlot(it).takeUnless { item -> item.isEmpty } }): ClientboundSetEquipmentPacket? { + val equip = EquipmentSlot.entries.mapNotNull { + mapper(it)?.let { item -> com.mojang.datafixers.util.Pair.of(it, item) } + } + return if (equip.isNotEmpty()) ClientboundSetEquipmentPacket(id, equip) else null +} +internal fun LivingEntity.toEmptyEquipmentPacket() = toEquipmentPacket { ItemStack.EMPTY } + +internal val Player.hotbarSlot get() = inventory.selectedSlot + 36 +internal val PLAYER_EQUIPMENT_SLOT = IntSet.of(45, 5, 6, 7, 8) +internal fun ClientboundContainerSetSlotPacket.isEquipment(player: Player) = containerId == 0 && (PLAYER_EQUIPMENT_SLOT.contains(slot) || slot == player.hotbarSlot) + +internal fun Entity.toFakeAddPacket() = ClientboundAddEntityPacket( + id, + uuid, + x, + y, + z, + xRot, + yRot, + EntityTypes.ITEM_DISPLAY, + 0, + deltaMovement, + yHeadRot.toDouble() +) + +internal fun Avatar.toCustomisation() = entityData.get(Avatar.DATA_PLAYER_MODE_CUSTOMISATION).toInt() + +internal fun VanillaComponent.asAdventure() = if (BetterModelBukkit.IS_PAPER) { + PaperAdventure.asAdventure(this) +} else { + GsonComponentSerializer.gson().deserialize(CraftChatMessage.toJSON(this)) +} + +internal fun AdventureComponent.asVanilla() = if (BetterModelBukkit.IS_PAPER) { + PaperAdventure.asVanilla(this) +} else { + CraftChatMessage.fromJSON(GsonComponentSerializer.gson().serialize(this)) +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/HitBoxImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/HitBoxImpl.kt new file mode 100644 index 000000000..142775bb4 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/HitBoxImpl.kt @@ -0,0 +1,443 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import io.papermc.paper.event.entity.EntityKnockbackEvent +import kr.toxicity.model.api.BetterModel +import kr.toxicity.model.api.bone.BoneMovement +import kr.toxicity.model.api.bone.RenderedBone +import kr.toxicity.model.api.bukkit.BetterModelBukkit +import kr.toxicity.model.api.config.DebugConfig +import kr.toxicity.model.api.data.blueprint.ModelBoundingBox +import kr.toxicity.model.api.event.hitbox.* +import kr.toxicity.model.api.mount.MountController +import kr.toxicity.model.api.nms.HitBox +import kr.toxicity.model.api.nms.HitBoxListener +import kr.toxicity.model.api.nms.ModelInteractionHand +import kr.toxicity.model.api.platform.PlatformEntity +import kr.toxicity.model.api.platform.PlatformPlayer +import net.minecraft.network.protocol.game.ServerboundInteractPacket +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.InteractionHand +import net.minecraft.world.InteractionHand.MAIN_HAND +import net.minecraft.world.InteractionHand.OFF_HAND +import net.minecraft.world.InteractionResult +import net.minecraft.world.damagesource.DamageSource +import net.minecraft.world.effect.MobEffectInstance +import net.minecraft.world.entity.* +import net.minecraft.world.entity.ai.attributes.Attributes +import net.minecraft.world.entity.player.Player +import net.minecraft.world.entity.projectile.Projectile +import net.minecraft.world.entity.projectile.ProjectileDeflection +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.BlockGetter +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.Vec3 +import org.bukkit.Bukkit +import org.bukkit.Color +import org.bukkit.Particle +import org.bukkit.craftbukkit.CraftServer +import org.bukkit.craftbukkit.entity.CraftArmorStand +import org.bukkit.craftbukkit.entity.CraftLivingEntity +import org.bukkit.event.entity.CreatureSpawnEvent +import org.bukkit.event.entity.EntityPotionEffectEvent +import org.bukkit.event.entity.EntityRemoveEvent +import org.bukkit.plugin.Plugin +import org.joml.Vector3f +import java.util.* + +internal class HitBoxImpl( + private val source: ModelBoundingBox, + private val bone: RenderedBone, + private var listener: HitBoxListener, + private val delegate: Entity, + private var mountController: MountController +) : AbstractHitBox(delegate.level()) { + private val posCache = BoneMovement() + private var initialized = false + private var jumpDelay = 0 + private var mounted = false + private var collision = ifLivingEntity { collides } == true + private var noGravity = if (delegate is Mob) delegate.isNoAi else delegate.isNoGravity + private var forceDismount = false + private var onFly = false + + val craftEntity: HitBox by lazy { + object : CraftArmorStand(Bukkit.getServer() as CraftServer, this), HitBox by this {} + } + val dimensions: EntityDimensions get() = source.run { + EntityDimensions( + (x() + z()).toFloat() / 2, + y().toFloat(), + delegate.eyeHeight, + EntityAttachments.createDefault(0F, 0F), + false + ).scale(bone.hitBoxScale()) + } + private val interaction by lazy { + HitBoxInteraction(this) + } + private val applier = InsideBlockEffectApplier.StepBasedCollector() + + init { + moveTo(delegate.position()) + isInvisible = true + persist = false + isSilent = true + initialized = true + level().addFreshEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM) + level().addFreshEntity(interaction.apply { + moveTo(delegate.position()) + }, CreatureSpawnEvent.SpawnReason.CUSTOM) + interaction.startRiding(this) + listener.handle(HitBoxCreateEvent(this)) + } + + private fun initialSetup() { + if (mounted) { + mounted = false + if (delegate is Mob) delegate.isNoAi = noGravity + else delegate.isNoGravity = noGravity + ifLivingEntity { collides = collision } + } + } + + override fun id(): Int = id + override fun uuid(): UUID = uuid + override fun source(): PlatformEntity = delegate.bukkitEntity.wrap() + override fun positionSource(): RenderedBone = bone + override fun forceDismount(): Boolean = forceDismount + override fun mountController(): MountController = mountController + override fun hasMountDriver(): Boolean = controllingPassenger != null + override fun mountController(controller: MountController) { + this.mountController = controller + } + override fun relativePosition(): Vector3f = delegate.position().run { + bone.hitBoxPosition(posCache).add(x.toFloat(), y.toFloat(), z.toFloat()) + } + override fun listener(): HitBoxListener = listener + override fun listener(listener: HitBoxListener) { + this.listener = listener + } + override fun getItemBySlot(slot: EquipmentSlot): ItemStack = ItemStack.EMPTY + override fun setItemSlot(slot: EquipmentSlot, stack: ItemStack) { + } + override fun getMainArm(): HumanoidArm = HumanoidArm.RIGHT + + override fun mount(entity: PlatformEntity) { + if (controllingPassenger != null) return + if (interaction.bukkitEntity.addPassenger(entity.unwarp())) { + if (mountController.canControl()) { + mounted = true + noGravity = delegate.isNoGravity + ifLivingEntity { + collision = collides + collides = false + } + } + listener.handle(HitBoxMountEvent(this, entity)) + } + } + + override fun dismount(entity: PlatformEntity) { + forceDismount = true + if (interaction.bukkitEntity.removePassenger(entity.unwarp())) listener.handle(HitBoxDismountEvent(this, entity)) + forceDismount = false + } + + override fun dismountAll() { + forceDismount = true + interaction.passengers.forEach { + it.stopRiding(true) + listener.handle(HitBoxDismountEvent(this, it.bukkitEntity.wrap())) + } + forceDismount = false + } + + override fun setRemainingFireTicks(remainingFireTicks: Int) { + delegate.remainingFireTicks = remainingFireTicks + } + + override fun getRemainingFireTicks(): Int { + return delegate.remainingFireTicks + } + + override fun knockback( + power: Double, + xd: Double, + yd: Double, + source: DamageSource, + damage: Float, + comesFromEffect: Boolean, + attacker: Entity?, + cause: EntityKnockbackEvent.Cause + ) { + if (attacker === delegate) return + ifLivingEntity { knockback(power, xd, yd, source, damage, comesFromEffect, attacker, cause) } + } + + override fun push(pushingEntity: Entity) { + if (pushingEntity === delegate) return + delegate.push(pushingEntity) + } + + override fun push(x: Double, y: Double, z: Double, pushingEntity: Entity?) { + if (pushingEntity === delegate) return + delegate.push(x, y, z, pushingEntity) + } + + override fun isCollidable(ignoreClimbing: Boolean): Boolean { + return delegate.isCollidable(ignoreClimbing) + } + + override fun canCollideWith(entity: Entity): Boolean { + return checkCollide(entity) && delegate.canCollideWith(entity) + } + + override fun canCollideWithBukkit(entity: Entity): Boolean { + return checkCollide(entity) && delegate.canCollideWithBukkit(entity) + } + + private fun checkCollide(entity: Entity): Boolean { + return entity !== delegate + && passengers.none { it === entity } + && delegate.passengers.none { it === entity } + && (entity !is HitBoxImpl || entity.delegate !== delegate) + } + + override fun getActiveEffects(): Collection { + return ifLivingEntity { getActiveEffects() } ?: emptyList() + } + + override fun getControllingPassenger(): LivingEntity? { + return if (mounted) interaction.firstPassenger as? LivingEntity ?: super.getControllingPassenger() else null + } + + override fun onWalk(): Boolean { + return isWalking() + } + + private fun mountControl(player: ServerPlayer) { + if (delegate !is LivingEntity) return + val travelVector = Vec3(delegate.xxa.toDouble(), delegate.yya.toDouble(), delegate.zza.toDouble()) + if (!mountController.canFly() && delegate.isFallFlying) return + + updateFlyStatus(player) + val riddenInput = rideInput(player, travelVector) + if (riddenInput.length() > 0.01) { + delegate.yRot = player.yRot + if (onFly) delegate.yHeadRot = player.yRot + delegate.move(MoverType.SELF, Vec3(riddenInput.x.toDouble(), riddenInput.y.toDouble(), riddenInput.z.toDouble())) + } + val dy = delegate.deltaMovement.y + delegate.gravity + if (!onFly && mountController.canJump() && (delegate.horizontalCollision || player.isJump()) && dy in 0.0..0.01 && jumpDelay == 0) { + jumpDelay = 10 + delegate.jumpFromGround() + } + } + + private fun movementSpeed() = ifLivingEntity { + getAttribute(Attributes.MOVEMENT_SPEED)?.value?.toFloat()?.let { + if (!onFly && !shouldDiscardFriction()) level() + .getBlockState(blockPosBelowThatAffectsMyMovement) + .block + .getFriction() * it else it + } ?: 0.0F + } ?: 0.0F + + private fun updateFlyStatus(player: ServerPlayer) { + val fly = (player.isJump() && mountController.canFly()) || noGravity || onFly + if (delegate is Mob) delegate.isNoAi = fly + else delegate.isNoGravity = fly + onFly = fly && !delegate.onGround() + if (onFly) delegate.resetFallDistance() + } + + private fun rideInput(player: ServerPlayer, travelVector: Vec3) = mountController.move( + if (onFly) MountController.MoveType.FLY else MountController.MoveType.DEFAULT, + player.bukkitEntity.wrap(), + (delegate.bukkitEntity as org.bukkit.entity.LivingEntity).wrap(), + Vector3f( + player.xMovement(), + player.yMovement(), + player.zMovement() + ), + Vector3f( + travelVector.x.toFloat(), + travelVector.y.toFloat(), + travelVector.z.toFloat() + ) + ).mul(movementSpeed()).rotateY(-Math.toRadians(player.yRot.toDouble()).toFloat()) + + override fun tick() { + delegate.removalReason?.let { + if (!isRemoved) remove(it) + return + } + val controller = controllingPassenger + if (jumpDelay > 0) jumpDelay-- + interaction.isInvisible = delegate.isInvisible + if (controller is ServerPlayer && !isDeadOrDying && mountController.canControl()) { + if (delegate is Mob) delegate.navigation.stop() + mountControl(controller) + } else initialSetup() + yRot = bone.rotation().y + yHeadRot = yRot + yBodyRot = yRot + val pos = relativePosition() + val minusHeight = source.minY * bone.hitBoxScale() + setPos( + pos.x.toDouble(), + pos.y.toDouble() + minusHeight, + pos.z.toDouble() + ) + BlockGetter.forEachBlockIntersectedBetween( + oldPosition(), + position(), + boundingBox + ) { pos, step -> + if (BetterModelBukkit.IS_PAPER) applier.advanceStep(step, pos) + level().getBlockState(pos).entityInside(level(), pos, delegate, applier, true) + true + } + applier.applyAndClear(delegate) + if (isInLava) delegate.lavaHurt() + firstTick = false + listener.sync(craftEntity) + } + + override fun remove(reason: RemovalReason, cause: EntityRemoveEvent.Cause?) { + initialSetup() + listener.handle(HitBoxRemoveEvent(craftEntity)) + interaction.remove(reason) + super.remove(reason, cause) + } + + override fun getBukkitLivingEntity(): CraftLivingEntity = bukkitEntity + override fun getBukkitEntity(): CraftLivingEntity = craftEntity as CraftLivingEntity + override fun getBukkitEntityRaw(): CraftLivingEntity = bukkitEntity + override fun hasExactlyOnePlayerPassenger(): Boolean = false + + override fun isDeadOrDying(): Boolean { + return ifLivingEntity { isDeadOrDying } == true + } + + override fun hide(player: PlatformPlayer) { + val plugin = BetterModel.platform() as Plugin + player.unwarp().run { + hideEntity(plugin, bukkitEntity) + hideEntity(plugin, interaction.bukkitEntity) + } + } + + override fun show(player: PlatformPlayer) { + val plugin = BetterModel.platform() as Plugin + player.unwarp().run { + showEntity(plugin, bukkitEntity) + showEntity(plugin, interaction.bukkitEntity) + } + } + + override fun interact(player: Player, hand: InteractionHand, vec: Vec3): InteractionResult { + if (player === delegate) return InteractionResult.FAIL + val interact = HitBoxInteractAtEvent( + (player.bukkitEntity as org.bukkit.entity.Player).wrap(), craftEntity, when (hand) { + MAIN_HAND -> ModelInteractionHand.RIGHT + OFF_HAND -> ModelInteractionHand.LEFT + }, vec.toBukkit() + ) + if (!listener.handle(interact)) return InteractionResult.FAIL + (player as ServerPlayer).connection.handleInteract(ServerboundInteractPacket( + delegate.id, + hand, + vec, + player.isShiftKeyDown + )) + return InteractionResult.SUCCESS + } + + override fun addEffect(effectInstance: MobEffectInstance, cause: EntityPotionEffectEvent.Cause): Boolean { + return ifLivingEntity { addEffect(effectInstance, cause) } == true + } + + override fun addEffect(effectInstance: MobEffectInstance, entity: Entity?): Boolean { + if (entity === delegate) return false + return ifLivingEntity { addEffect(effectInstance, entity) } == true + } + + override fun addEffect( + effectInstance: MobEffectInstance, + entity: Entity?, + cause: EntityPotionEffectEvent.Cause + ): Boolean { + if (entity === delegate) return false + return ifLivingEntity { addEffect(effectInstance, entity, cause) } == true + } + + override fun addEffect( + effectInstance: MobEffectInstance, + entity: Entity?, + cause: EntityPotionEffectEvent.Cause, + fireEvent: Boolean + ): Boolean { + if (entity === delegate) return false + return ifLivingEntity { addEffect(effectInstance, entity, cause, fireEvent) } == true + } + + override fun hurtServer(world: ServerLevel, source: DamageSource, amount: Float): Boolean { + if (source.entity === delegate || delegate.isInvulnerable) return false + if (source.entity === controllingPassenger && !mountController.canBeDamagedByRider()) return false + val ds = ModelDamageSourceImpl(source) + val event = HitBoxDamagedEvent(craftEntity, ds, amount) + if (!listener.handle(event)) return false + return ifLivingEntity { hurtServer(world, source, event.damage) } == true + } + + override fun deflection(projectile: Projectile): ProjectileDeflection { + if (projectile.owner?.uuid == delegate.uuid) return ProjectileDeflection.NONE + return ifLivingEntity { deflection(projectile) } ?: ProjectileDeflection.NONE + } + + override fun getHealth(): Float { + return ifLivingEntity { health } ?: super.getHealth() + } + + override fun makeBoundingBox(vec3: Vec3): AABB { + return if (!initialized) { + super.makeBoundingBox(vec3) + } else { + val scale = bone.hitBoxScale() + AABB( + vec3.x + source.minX * scale, + vec3.y, + vec3.z + source.minZ * scale, + vec3.x + source.maxX * scale, + vec3.y + source.y() * scale, + vec3.z + source.maxZ * scale + ).apply { + if (CONFIG.debug().has(DebugConfig.DebugOption.HITBOX)) { + bukkitEntity.world.spawnParticle(Particle.DUST, minX, minY, minZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) + bukkitEntity.world.spawnParticle(Particle.DUST, maxX, maxY, maxZ, 1, 0.0, 0.0, 0.0, 0.0, Particle.DustOptions(Color.RED, 1F)) + } + } + } + } + override fun getDefaultDimensions(pose: Pose): EntityDimensions = if (initialized) dimensions else super.getDefaultDimensions(pose) + + override fun removeHitBox() { + source().task { + dismountAll() + remove(ifLivingEntity { removalReason } ?: RemovalReason.KILLED) + } + } + + private inline fun ifLivingEntity(block: LivingEntity.() -> T): T? { + return if (delegate.valid) (delegate as? LivingEntity)?.block() else null + } +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/HitBoxInteraction.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/HitBoxInteraction.kt new file mode 100644 index 000000000..c9e836420 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/HitBoxInteraction.kt @@ -0,0 +1,59 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.nms.HitBox +import net.minecraft.world.InteractionHand +import net.minecraft.world.InteractionResult +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.EntityTypes +import net.minecraft.world.entity.Interaction +import net.minecraft.world.entity.player.Player +import net.minecraft.world.phys.Vec3 +import org.bukkit.Bukkit +import org.bukkit.craftbukkit.CraftServer +import org.bukkit.craftbukkit.entity.CraftEntity +import org.bukkit.craftbukkit.entity.CraftInteraction + +internal class HitBoxInteraction( + val delegate: HitBoxImpl +) : Interaction(EntityTypes.INTERACTION, delegate.level()) { + + init { + persist = false + } + + private val craftEntity: CraftInteraction by lazy { + object : CraftInteraction(Bukkit.getServer() as CraftServer, this), HitBox by delegate {} + } + + override fun getBukkitEntity(): CraftEntity = craftEntity + override fun getBukkitEntityRaw(): CraftEntity = craftEntity + override fun hasExactlyOnePlayerPassenger(): Boolean = false + + override fun tick() { + val dimension = delegate.dimensions + width = dimension.width + height = dimension.height + yRot = delegate.yRot + xRot = delegate.xRot + setSharedFlagOnFire(delegate.remainingFireTicks > 0) + } + + override fun skipAttackInteraction(entity: Entity): Boolean { + return if (entity is Player) { + entity.attack(delegate) + true + } else false + } + + override fun interact(player: Player, hand: InteractionHand, vec: Vec3): InteractionResult { + delegate.interact(player, hand, vec) + return InteractionResult.FAIL + } +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModAnimationBundlerImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModAnimationBundlerImpl.kt new file mode 100644 index 000000000..3b596cee1 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModAnimationBundlerImpl.kt @@ -0,0 +1,144 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.nms.ModAnimationBundler +import kr.toxicity.model.api.platform.PlatformPlayer +import kr.toxicity.model.api.util.MathUtil +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket +import net.minecraft.server.MinecraftServer +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.joml.Quaternionf +import org.joml.Vector3f + +internal class ModAnimationBundlerImpl(initialCapacity: Int) : ModAnimationBundler { + + companion object { + + const val KEY = "modelengine:bulk_data" + + const val PACKET_TYPE_BULK_DATA = 0x00 + + const val FIELD_TRANSLATION = 1 shl 0 + const val FIELD_LEFT_ROTATION = 1 shl 1 + const val FIELD_SCALE = 1 shl 2 + const val FIELD_TRANSFORM_DURATION = 1 shl 4 + + private val EMPTY_BUILD_TASK: (FriendlyByteBuf) -> Unit = {} + } + + private val packet by lazy { + useByteBuf { buffer -> + ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC.decode( + RegistryFriendlyByteBuf( + buffer, + MinecraftServer.getServer().registryAccess() + ).apply { + writeUtf(KEY) + useByteBuf { + it.writeByte(PACKET_TYPE_BULK_DATA) + it.writeVarInt(builderList.size) + builderList.forEach { builder -> builder(it) } + writeBytes(it) + } + } + ) + } + } + + private val builderList = ArrayList<(FriendlyByteBuf) -> Unit>(initialCapacity) + + override fun send(player: PlatformPlayer) { + (player.unwarp() as CraftPlayer).handle.connection.send(packet) + } + + fun append(id: Int, scope: Appender.() -> Unit) { + val build = Appender(id).apply(scope).build() + if (build !== EMPTY_BUILD_TASK) builderList += build + } + + class Appender( + val entityId: Int, + ) { + private var mask = 0 + private var buildTask = EMPTY_BUILD_TASK + private val isEmpty get() = buildTask === EMPTY_BUILD_TASK + + fun appendPosition(vector: Vector3f) { + mask = mask or FIELD_TRANSLATION + task { + writeFloat(it, vector.x) + writeFloat(it, vector.y) + writeFloat(it, vector.z) + } + } + + fun appendScale(vector: Vector3f) { + mask = mask or FIELD_SCALE + task { + writeFloat(it, vector.x) + writeFloat(it, vector.y) + writeFloat(it, vector.z) + } + } + + fun appendRotation(quaternion: Quaternionf) { + mask = mask or FIELD_LEFT_ROTATION + task { + writeFloat(it, quaternion.x) + writeFloat(it, quaternion.y) + writeFloat(it, quaternion.z) + writeFloat(it, quaternion.w) + } + } + + fun appendDuration(duration: Int) { + mask = mask or FIELD_TRANSFORM_DURATION + task { + writeVarInt(it, duration) + } + } + + fun build(): (FriendlyByteBuf) -> Unit { + if (isEmpty) return EMPTY_BUILD_TASK + val m = mask + val t = buildTask + return { + writeVarInt(it,entityId) + writeByte(it, m) + t(it) + } + } + + private fun task(task: (FriendlyByteBuf) -> Unit) { + if (isEmpty) { + buildTask = task + return + } + val last = buildTask + buildTask = { + last(it) + task(it) + } + } + + private fun writeFloat(buf: FriendlyByteBuf, float: Float) { + buf.writeShort(MathUtil.floatToHalf(float).toInt()) + } + + private fun writeVarInt(buf: FriendlyByteBuf, duration: Int) { + buf.writeVarInt(duration) + } + + private fun writeByte(buf: FriendlyByteBuf, duration: Int) { + buf.writeByte(duration) + } + } +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelDamageSourceImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelDamageSourceImpl.kt new file mode 100644 index 000000000..b875b4b25 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelDamageSourceImpl.kt @@ -0,0 +1,30 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.event.ModelDamageSource +import kr.toxicity.model.api.platform.PlatformEntity +import kr.toxicity.model.api.platform.PlatformLocation +import net.minecraft.world.damagesource.DamageSource +import org.bukkit.craftbukkit.util.CraftLocation + +internal class ModelDamageSourceImpl( + private val source: DamageSource +) : ModelDamageSource { + override fun getCausingEntity(): PlatformEntity? = source.entity?.bukkitEntity?.wrap() + override fun getDirectEntity(): PlatformEntity? = source.directEntity?.bukkitEntity?.wrap() + override fun getDamageLocation(): PlatformLocation? = source.sourcePositionRaw()?.let { + CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() + } + override fun getSourceLocation(): PlatformLocation? = source.sourcePosition?.let { + CraftLocation.toBukkit(it, causingEntity?.unwarp()?.world).wrap() + } + override fun isIndirect(): Boolean = !source.isDirect + override fun getFoodExhaustion(): Float = source.foodExhaustion + override fun scalesWithDifficulty(): Boolean = source.scalesWithDifficulty() +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelDisplayImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelDisplayImpl.kt new file mode 100644 index 000000000..f48a8bde8 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelDisplayImpl.kt @@ -0,0 +1,261 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.entity.BaseEntity +import kr.toxicity.model.api.nms.AnimationBundler +import kr.toxicity.model.api.nms.DisplayTransformer +import kr.toxicity.model.api.nms.ModelDisplay +import kr.toxicity.model.api.nms.PacketBundler +import kr.toxicity.model.api.platform.PlatformBillboard +import kr.toxicity.model.api.platform.PlatformItemStack +import kr.toxicity.model.api.platform.PlatformItemTransform +import kr.toxicity.model.api.platform.PlatformLocation +import kr.toxicity.model.api.tracker.ModelRotation +import kr.toxicity.model.api.util.lock.SingleLock +import net.minecraft.network.protocol.game.* +import net.minecraft.network.syncher.EntityDataSerializers +import net.minecraft.network.syncher.SynchedEntityData +import net.minecraft.util.Brightness +import net.minecraft.world.entity.Display +import net.minecraft.world.entity.Display.ItemDisplay +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.PositionMoveRotation +import net.minecraft.world.item.ItemDisplayContext +import net.minecraft.world.item.Items +import org.joml.Quaternionf +import org.joml.Vector3d +import org.joml.Vector3f +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean + +internal class ModelDisplayImpl( + private val pos: Vector3d, + val display: ItemDisplay, + val yOffset: Double +) : ModelDisplay { + + private val entityData = display.entityData + private val entityDataLock = SingleLock() + private val forceGlow = AtomicBoolean() + private val forceInvisibility = AtomicBoolean() + + private val oldPos = Vector3d(pos) + + override fun id(): Int = display.id + override fun uuid(): UUID = display.uuid + override fun rotate(rotation: ModelRotation, bundler: PacketBundler) { + display.xRot = rotation.x + display.yRot = rotation.y + bundler += ClientboundMoveEntityPacket.Rot( + display.id, + rotation.packedY(), + rotation.packedX(), + display.onGround + ) + } + + override fun invisible(invisible: Boolean) { + if (forceInvisibility.compareAndSet(!invisible, invisible)) { + entityDataLock.accessToLock { + entityData.markDirty(ITEM_SERIALIZER) + } + } + } + + override fun syncPotionEffect(entity: BaseEntity) { + val beforeInvisible = display.isInvisible + val afterInvisible = entity.invisible() + entityDataLock.accessToLock { + display.setGlowingTag(entity.glow() || forceGlow.get()) + if (CONFIG.followMobInvisibility() && beforeInvisible != afterInvisible) { + display.isInvisible = afterInvisible + entityData.markDirty(ITEM_SERIALIZER) + } + } + } + + override fun syncPosition(location: PlatformLocation) { + oldPos.set(pos) + pos.set(location.x(), location.y(), location.z()) + } + + override fun spawn(showItem: Boolean, bundler: PacketBundler) { + bundler += addPacket + } + + override fun remove(bundler: PacketBundler) { + bundler += removePacket + } + + override fun teleport(location: PlatformLocation, bundler: PacketBundler) { + display.moveTo( + location.x(), + location.y(), + location.z(), + location.yaw(), + 0F + ) + bundler += ClientboundTeleportEntityPacket.teleport(display.id, PositionMoveRotation.of(display), emptySet(), display.onGround) + } + + override fun sendPosition(adapter: BaseEntity, bundler: PacketBundler) { + val handle = adapter.handle() as Entity + if (oldPos.distanceSquared(pos) < 1e-8) return + bundler += ClientboundEntityPositionSyncPacket( + display.id, + PositionMoveRotation.of(handle), + handle.onGround() + ) + } + + override fun display(transform: PlatformItemTransform) { + entityDataLock.accessToLock { + display.itemTransform = ItemDisplayContext.BY_ID.apply(transform.ordinal) + } + } + + override fun moveDuration(duration: Int) { + entityDataLock.accessToLock { + entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = duration + } + } + + override fun item(itemStack: PlatformItemStack) { + entityDataLock.accessToLock { + display.itemStack = itemStack.unwarp().asVanilla() + } + } + + override fun brightness(block: Int, sky: Int) { + entityDataLock.accessToLock { + display.brightnessOverride = if (block < 0 && sky < 0) null else Brightness( + block, + sky + ) + } + } + + override fun viewRange(range: Float) { + entityDataLock.accessToLock { + display.viewRange = range + } + } + + override fun shadowRadius(radius: Float) { + entityDataLock.accessToLock { + display.shadowRadius = radius + } + } + + override fun glow(glow: Boolean) { + if (!forceGlow.compareAndSet(!glow, glow)) return + entityDataLock.accessToLock { + display.setGlowingTag(display.isCurrentlyGlowing || glow) + } + } + + override fun glowColor(glowColor: Int) { + entityDataLock.accessToLock { + display.glowColorOverride = glowColor + } + } + + override fun billboard(billboard: PlatformBillboard) { + entityDataLock.accessToLock { + display.billboardConstraints = Display.BillboardConstraints.BY_ID.apply(billboard.ordinal) + } + } + + override fun createTransformer(): DisplayTransformer = DisplayTransformerImpl(display) + + override fun invisible(): Boolean = entityDataLock.accessToLock { + display.isInvisible || forceInvisibility.get() || display.itemStack.`is`(Items.AIR) + } + + override fun sendDirtyEntityData(bundler: PacketBundler) { + entityDataLock.accessToLock { + entityData.pack( + clean = true, + itemFilter = { it.isDirty }, + valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } + ) + }?.markVisible(!invisible())?.run { + bundler += ClientboundSetEntityDataPacket(display.id, this) + } + } + + override fun sendEntityData(showItem: Boolean, bundler: PacketBundler) { + entityDataLock.accessToLock { + entityData.pack( + valueFilter = { ITEM_ENTITY_DATA.contains(it.id) } + ) + }?.markVisible(showItem && !invisible())?.run { + bundler += ClientboundSetEntityDataPacket(display.id, this) + } + } + + private fun List>.markVisible(showItem: Boolean) = map { + if (it.id == ITEM_SERIALIZER.id) SynchedEntityData.DataValue( + it.id, + EntityDataSerializers.ITEM_STACK, + if (showItem) display.itemStack else EMPTY_ITEM + ) else it + } + + private val addPacket + get() = ClientboundAddEntityPacket( + display.id, + display.uuid, + pos.x, + pos.y + yOffset, + pos.z, + display.xRot, + display.yRot, + display.type, + 0, + display.deltaMovement, + display.yHeadRot.toDouble() + ) + + private val removePacket = ClientboundRemoveEntitiesPacket(display.id) + + private class DisplayTransformerImpl( + source: ItemDisplay + ) : DisplayTransformer { + private val id = source.id + private val entityData = TransformationData() + private val entityDataLock = SingleLock() + + override fun transform( + duration: Int, + position: Vector3f, + scale: Vector3f, + rotation: Quaternionf, + bundler: AnimationBundler + ) { + entityDataLock.accessToLock { + entityData.transform( + duration, + position, + scale, + rotation + ) + entityData.packDirty(id, bundler) + } + } + + override fun sendTransformation(bundler: PacketBundler) { + entityDataLock.accessToLock { + entityData.pack() + }?.run { + bundler += ClientboundSetEntityDataPacket(id, this) + } + } + } +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelGameProfile.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelGameProfile.kt new file mode 100644 index 000000000..9daff28d3 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelGameProfile.kt @@ -0,0 +1,30 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import com.mojang.authlib.GameProfile +import kr.toxicity.model.api.BetterModel +import kr.toxicity.model.api.profile.ModelProfile +import kr.toxicity.model.api.profile.ModelProfileInfo +import kr.toxicity.model.api.profile.ModelProfileSkin + +internal data class ModelGameProfile( + private val gameProfile: GameProfile +) : ModelProfile { + + private val info = ModelProfileInfo(gameProfile.id, gameProfile.name) + private val skin by lazy { + gameProfile.properties["textures"].firstOrNull()?.let { + BetterModel.platform().profileManager().skin(it.value) + } ?: ModelProfileSkin.EMPTY + } + + override fun info(): ModelProfileInfo = info + + override fun skin(): ModelProfileSkin = skin +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelNametagImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelNametagImpl.kt new file mode 100644 index 000000000..a4e03e876 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ModelNametagImpl.kt @@ -0,0 +1,119 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import com.mojang.math.Transformation +import kr.toxicity.model.api.BetterModel +import kr.toxicity.model.api.bone.BoneMovement +import kr.toxicity.model.api.bone.BonePosition +import kr.toxicity.model.api.bone.RenderedBone +import kr.toxicity.model.api.nms.ModelNametag +import kr.toxicity.model.api.nms.PacketBundler +import kr.toxicity.model.api.platform.PlatformLocation +import kr.toxicity.model.api.platform.PlatformPlayer +import kr.toxicity.model.api.util.EntityUtil +import net.kyori.adventure.text.Component +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket +import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket +import net.minecraft.server.MinecraftServer +import net.minecraft.world.entity.Display +import net.minecraft.world.entity.EntityTypes +import net.minecraft.world.entity.PositionMoveRotation +import net.minecraft.world.phys.Vec3 +import org.joml.Vector3f +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +internal class ModelNametagImpl( + private val bone: RenderedBone +) : ModelNametag { + private companion object { + private val emptyVector = Vector3f() + private val emptyTransformation = Transformation( + Vector3f(-1F / 40F, -0.2F - 1F / 40F, 0F), + null, + null, + null + ) + } + + private val viewedPlayer = ConcurrentHashMap.newKeySet() + private val display = Display.TextDisplay( + EntityTypes.TEXT_DISPLAY, + MinecraftServer.getServer().overworld() + ).apply { + entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 + setTransformation(emptyTransformation) + billboardConstraints = Display.BillboardConstraints.CENTER + } + private val posCache = BoneMovement() + private var alwaysVisible = false + private var location = BetterModel.platform().adapter().zero() + + override fun component(component: Component?) { + display.text = component?.asVanilla() ?: VanillaComponent.empty() + } + + override fun teleport(location: PlatformLocation) { + this.location = location + } + + override fun alwaysVisible(alwaysVisible: Boolean) { + this.alwaysVisible = alwaysVisible + } + + override fun send(player: PlatformPlayer) { + if (display.text == VanillaComponent.empty()) return + val hb = bone.group.hitBoxPoint + val pos = bone.worldPosition(BonePosition(emptyVector, hb, player.uuid()), posCache) + display.moveTo(Vec3( + location.x() + pos.x, + location.y() + pos.y, + location.z() + pos.z + )) + val inPoint = alwaysVisible || EntityUtil.isCustomNameVisible(player.location(), location) + when { + inPoint && viewedPlayer.add(player.uuid()) -> bundlerOfNotNull( + addPacket, + display.entityData.pack()?.let { + ClientboundSetEntityDataPacket(display.id, it) + } + ) + inPoint -> bundlerOfNotNull( + ClientboundEntityPositionSyncPacket(display.id, PositionMoveRotation.of(display), false), + display.entityData.packDirty()?.let { + ClientboundSetEntityDataPacket(display.id, it) + } + ) + viewedPlayer.remove(player.uuid()) -> bundlerOf(removePacket) + else -> null + }?.send(player) + } + + override fun remove(bundler: PacketBundler) { + bundler += removePacket + } + + private val addPacket get() = ClientboundAddEntityPacket( + display.id, + display.uuid, + display.x, + display.y, + display.z, + display.xRot, + display.yRot, + display.type, + 0, + display.deltaMovement, + display.yHeadRot.toDouble() + ) + + private val removePacket get() = ClientboundRemoveEntitiesPacket(display.id) +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/NMSImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/NMSImpl.kt new file mode 100644 index 000000000..ea06ab064 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/NMSImpl.kt @@ -0,0 +1,385 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup +import com.mojang.authlib.GameProfile +import io.netty.channel.ChannelDuplexHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelPromise +import kr.toxicity.model.api.BetterModel +import kr.toxicity.model.api.bone.RenderedBone +import kr.toxicity.model.api.bukkit.BetterModelBukkit +import kr.toxicity.model.api.bukkit.entity.BaseBukkitEntity +import kr.toxicity.model.api.data.blueprint.ModelBoundingBox +import kr.toxicity.model.api.entity.BaseEntity +import kr.toxicity.model.api.entity.BasePlayer +import kr.toxicity.model.api.mount.MountController +import kr.toxicity.model.api.nms.* +import kr.toxicity.model.api.platform.PlatformEntity +import kr.toxicity.model.api.platform.PlatformItemStack +import kr.toxicity.model.api.platform.PlatformLocation +import kr.toxicity.model.api.platform.PlatformPlayer +import kr.toxicity.model.api.player.PlayerSkinParts +import kr.toxicity.model.api.profile.ModelProfile +import kr.toxicity.model.api.tracker.EntityTrackerRegistry +import kr.toxicity.model.api.tracker.TrackerUpdateAction +import kr.toxicity.model.api.util.TransformedItemStack +import net.kyori.adventure.key.Keyed +import net.minecraft.core.component.DataComponents +import net.minecraft.network.Connection +import net.minecraft.network.protocol.Packet +import net.minecraft.network.protocol.game.* +import net.minecraft.network.syncher.EntityDataSerializers +import net.minecraft.network.syncher.SynchedEntityData +import net.minecraft.resources.Identifier +import net.minecraft.server.MinecraftServer +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.network.ServerCommonPacketListenerImpl +import net.minecraft.util.ARGB +import net.minecraft.world.entity.Display +import net.minecraft.world.entity.Display.ItemDisplay +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.EntityTypes +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemDisplayContext +import net.minecraft.world.item.Items +import net.minecraft.world.item.component.CustomModelData +import net.minecraft.world.item.component.DyedItemColor +import net.minecraft.world.level.entity.LevelEntityGetter +import net.minecraft.world.level.entity.LevelEntityGetterAdapter +import net.minecraft.world.level.entity.PersistentEntitySectionManager +import org.bukkit.craftbukkit.CraftWorld +import org.bukkit.craftbukkit.entity.CraftEntity +import org.bukkit.craftbukkit.entity.CraftPlayer +import org.joml.Vector3d +import java.util.* +import java.util.function.Consumer +import java.util.function.IntConsumer + +class NMSImpl : NMS { + + companion object { + private const val INJECT_NAME = "bettermodel_channel_handler" + + //Spigot + private val getGameProfile: (net.minecraft.world.entity.player.Player) -> GameProfile = createAdaptedFieldGetter { it.gameProfile } + private val getConnection: (ServerCommonPacketListenerImpl) -> Connection = createAdaptedFieldGetter { it.connection } + private val spigotChunkAccess = ServerLevel::class.java.fields.firstOrNull { + it.type == PersistentEntitySectionManager::class.java + }?.apply { + isAccessible = true + } + @Suppress("UNCHECKED_CAST") + private val ServerLevel.levelGetter + get(): LevelEntityGetter { + return if (BetterModelBukkit.IS_PAPER) { + `moonrise$getEntityLookup`() + } else { + spigotChunkAccess?.get(this)?.let { + (it as PersistentEntitySectionManager<*>).entityGetter as LevelEntityGetter + } ?: throw RuntimeException("LevelEntityGetter") + } + } + private val getEntityById: (LevelEntityGetter, Int) -> Entity? = if (BetterModelBukkit.IS_PAPER) { g, i -> + (g as EntityLookup)[i] + } else LevelEntityGetterAdapter::class.java.declaredFields.first { + net.minecraft.world.level.entity.EntityLookup::class.java.isAssignableFrom(it.type) + }.let { + it.isAccessible = true + { e, i -> + (it[e] as net.minecraft.world.level.entity.EntityLookup<*>).getEntity(i) as? Entity + } + } + private fun Int.toEntity(level: ServerLevel) = getEntityById(level.levelGetter, this) + //Spigot + private val hitBoxData by lazy { + ItemDisplay(EntityTypes.ITEM_DISPLAY, MinecraftServer.getServer().overworld()).run { + entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 + entityData.nonDefaultValues!! + } + } + } + + override fun hide(channel: PlayerChannelHandler, registry: EntityTrackerRegistry) { + val target = registry.entity().handle() as? Entity ?: return + val list = bundlerOf() + target.entityData.pack( + valueFilter = { it.id == SHARED_FLAG } + )?.let { + list += ClientboundSetEntityDataPacket(target.id, it).toRegistryDataPacket(channel.uuid(), registry) + } + if (target is LivingEntity) { + val packet = if (registry.hideOption(channel.uuid()).equipment) target.toEmptyEquipmentPacket() else target.toEquipmentPacket() + packet?.let { list += it } + } + list.send(channel.player()) + } + + private fun ClientboundSetEntityDataPacket.toRegistryDataPacket(uuid: UUID, registry: EntityTrackerRegistry) = ClientboundSetEntityDataPacket(id, packedItems().map { + if (it.id == SHARED_FLAG) SynchedEntityData.DataValue( + it.id, + EntityDataSerializers.BYTE, + registry.entityFlag(uuid, it.value() as Byte) + ) else it + }) + + inner class PlayerChannelHandlerImpl( + private val player: CraftPlayer + ) : PlayerChannelHandler, ChannelDuplexHandler() { + private val connection = player.handle.connection + private val uuid = player.uniqueId + private val base = adapt(player.wrap()) + + init { + val pipeline = getConnection(connection).channel.pipeline() + pipeline.addBefore(pipeline.first { it.value is Connection }.key, INJECT_NAME, this) + } + + override fun close() { + val channel = getConnection(connection).channel + channel.eventLoop().submit { + channel.pipeline().remove(INJECT_NAME) + } + } + + override fun base(): BasePlayer = base + override fun isModEnabled(): Boolean = (if (BetterModelBukkit.IS_PAPER) player.channels() else player.listeningPluginChannels).contains(ModAnimationBundlerImpl.KEY) + + private val playerModel get() = connection.player.id.toRegistry() + + private fun Int.toPlayerEntity() = toEntity(connection.player.level()) + private fun Entity.toRegistry() = BetterModel.registryOrNull(uuid) + private inline fun Int.toRegistry( + ifHitBox: (Entity) -> Unit = {} + ) = (EntityTrackerRegistry.registry(this) ?: toPlayerEntity()?.let { + if (it is HitBox) ifHitBox(it) + it.toRegistry() + })?.takeIf { + it.isSpawned(player.uniqueId) + } + + override fun sendEntityData(registry: EntityTrackerRegistry) { + val handle = registry.entity().handle() as? Entity ?: return + val list = bundlerOf( + ClientboundSetPassengersPacket(handle) + ) + handle.entityData.pack( + valueFilter = { it.id == SHARED_FLAG } + )?.let { + list += ClientboundSetEntityDataPacket(handle.id, it) + } + if (handle is LivingEntity) handle.toEquipmentPacket()?.let { + list += it + } + list.send(player.wrap()) + } + + private fun Packet.handle(): Packet? { + when (this) { + is ClientboundBundlePacket -> return if (subPackets() is Keyed) this else ClientboundBundlePacket(subPackets().mapNotNull { + it.handle() + }) + is ClientboundAddEntityPacket -> { + val entity = id.toPlayerEntity() ?: return this + if (entity is HitBox) return entity.toFakeAddPacket() + val wrap = entity.bukkitEntity.wrap() + BetterModel.registry(wrap).ifPresent { + wrap.taskLater(1) { + it.spawn(player.wrap()) + } + } + } + is ClientboundRemoveEntitiesPacket -> { + entityIds + .asSequence() + .mapNotNull map@ { + it.toRegistry { + return@map null + } + } + .forEach { + it.remove() + } + } + is ClientboundSetPassengersPacket -> { + vehicle.toRegistry()?.let { + return it.mountPacket(it.entity().handle() as? Entity ?: return this, array = passengers) + } + } + is ClientboundUpdateAttributesPacket if entityId.toPlayerEntity() is HitBox -> return null + is ClientboundSetEntityDataPacket -> id.toRegistry { + return ClientboundSetEntityDataPacket(id, hitBoxData) + }?.let { registry -> + return toRegistryDataPacket(uuid, registry) + } + is ClientboundSetEquipmentPacket -> entity.toRegistry { + return null + }?.let { + if (it.hideOption(uuid).equipment()) (it.entity().handle() as? LivingEntity)?.toEmptyEquipmentPacket()?.let { packet -> + return packet + } + } + is ClientboundRespawnPacket -> playerModel?.let { + bundlerOf(it.mountPacket(connection.player)).send(player.wrap()) + } + is ClientboundContainerSetSlotPacket if isEquipment(connection.player) && playerModel?.hideOption(uuid)?.equipment() == true -> { + return ClientboundContainerSetSlotPacket(containerId, stateId, slot, EMPTY_ITEM) + } + is ClientboundContainerSetContentPacket if containerId == 0 && playerModel?.hideOption(uuid)?.equipment() == true -> { + return ClientboundContainerSetContentPacket( + containerId, + stateId, + items.apply { + PLAYER_EQUIPMENT_SLOT.forEach(IntConsumer { set(it, EMPTY_ITEM) }) + set(connection.player.hotbarSlot, EMPTY_ITEM) + }, + carriedItem + ) + } + } + return this + } + + override fun write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise) { + super.write(ctx, if (msg is Packet<*>) msg.handle() ?: return else msg, promise) + } + + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { + fun EntityTrackerRegistry.updatePlayerLimb() = BetterModel.platform().scheduler().asyncTaskLater(1) { + if (isClosed) return@asyncTaskLater + player.handle.containerMenu.sendAllDataToRemote() + trackers().forEach { tracker -> + tracker.update(TrackerUpdateAction.itemMapping()) { bone -> + !bone.itemMapper.fixed() + } + } + } + when (msg) { + is ServerboundSetCarriedItemPacket -> { + playerModel?.let { registry -> + if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) + if (CONFIG.cancelPlayerModelInventory()) { + connection.send(ClientboundSetHeldSlotPacket(player.inventory.heldItemSlot)) + return + } + registry.updatePlayerLimb() + } + } + is ServerboundPlayerActionPacket -> { + playerModel?.let { registry -> + if (!registry.hideOption(uuid).equipment()) return super.channelRead(ctx, msg) + if (CONFIG.cancelPlayerModelInventory()) return + registry.updatePlayerLimb() + } + } + } + super.channelRead(ctx, msg) + } + + private fun EntityTrackerRegistry.remove() { + remove(player.wrap()) + } + } + + override fun mount(registry: EntityTrackerRegistry, bundler: PacketBundler) { + val entity = registry.entity().handle() + if (entity is Entity) bundler += registry.mountPacket(entity) + } + + private fun EntityTrackerRegistry.mountPacket(entity: Entity, array: IntArray = entity.passengers.filter { + EntityTrackerRegistry.registry(it.uuid) == null + }.map { + it.id + }.toIntArray()): ClientboundSetPassengersPacket { + return useByteBuf { buffer -> + buffer.writeVarInt(entity.id) + buffer.writeVarIntArray(displays() + .mapToInt { + (it as ModelDisplayImpl).display.id + }.toArray() + array) + ClientboundSetPassengersPacket.STREAM_CODEC.decode(buffer) + } + } + + override fun inject(player: PlatformPlayer): PlayerChannelHandlerImpl = PlayerChannelHandlerImpl(player.unwarp() as CraftPlayer) + + override fun createBundler(initialCapacity: Int): PacketBundler = bundlerOf(initialCapacity) + override fun createParallelBundler(threshold: Int): PacketBundler = parallelBundlerOf(threshold) + override fun createModAnimationBuilder(initialCapacity: Int): ModAnimationBundler = ModAnimationBundlerImpl(initialCapacity) + + override fun create(location: PlatformLocation, yOffset: Double, initialConsumer: Consumer): ModelDisplay = ModelDisplayImpl( + Vector3d(location.x(), location.y(), location.z()), + ItemDisplay(EntityTypes.ITEM_DISPLAY, (location.world().unwarp() as CraftWorld).handle).apply { + entityData[Display.DATA_POS_ROT_INTERPOLATION_DURATION_ID] = 3 + billboardConstraints = Display.BillboardConstraints.FIXED + valid = true + yRot = location.yaw() + itemTransform = ItemDisplayContext.FIXED + }, + yOffset + ).apply { + initialConsumer.accept(this) + display.entityData.packDirty() + } + + override fun createNametag(bone: RenderedBone): ModelNametag = ModelNametagImpl(bone) + + override fun tint(itemStack: PlatformItemStack, rgb: Int): PlatformItemStack { + return itemStack.unwarp().asVanilla().apply { + set(DataComponents.DYED_COLOR, DyedItemColor(rgb)) + set(DataComponents.CUSTOM_MODEL_DATA, get(DataComponents.CUSTOM_MODEL_DATA)?.let { + CustomModelData(it.floats, it.flags, it.strings, it.colors + .run { + if (rgb == 0xFFFFFF) this else map { color -> + ARGB.multiply(color, rgb) and 0xFFFFFF + } + } + .ifEmpty { listOf(rgb) }) + }) + }.asBukkit().wrap() + } + + override fun createHitBox(entity: BaseEntity, bone: RenderedBone, boundingBox: ModelBoundingBox, mountController: MountController, listener: HitBoxListener): HitBox? { + val handle = entity.handle() as? Entity ?: return null + return HitBoxImpl( + boundingBox.center(), + bone, + listener, + handle, + mountController + ).craftEntity + } + override fun version(): NMSVersion = NMSVersion.V26_R2 + + override fun adapt(entity: PlatformEntity): BaseBukkitEntity { + val craft = entity.unwarp() as CraftEntity + return BaseEntityImpl(craft) + } + + override fun adapt(player: PlatformPlayer): BasePlayer { + val craft = player.unwarp() as CraftPlayer + return BasePlayerImpl( + craft, + dirtyChecked({ getGameProfile(craft.handle) }, { ModelGameProfile(it) }), + dirtyChecked({ craft.handle.toCustomisation() }, { PlayerSkinParts(it) }) + ) + } + + override fun profile(player: PlatformPlayer): ModelProfile = ModelGameProfile(getGameProfile((player.unwarp() as CraftPlayer).handle)) + + override fun createSkinItem(model: String, floats: List, flags: List, strings: List, colors: List): TransformedItemStack { + return VanillaItemStack(Items.PLAYER_HEAD).run { + set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(floats, flags, strings, colors)) + set(DataComponents.ITEM_MODEL, Identifier.parse(model)) + TransformedItemStack.of(asBukkit().wrap()) + } + } + + override fun isProxyOnlineMode(): Boolean = ONLINE_MODE +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/PacketBundlers.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/PacketBundlers.kt new file mode 100644 index 000000000..1394c8726 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/PacketBundlers.kt @@ -0,0 +1,91 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.nms.PacketBundler +import kr.toxicity.model.api.platform.PlatformPlayer +import net.kyori.adventure.key.Key +import net.kyori.adventure.key.Keyed +import net.minecraft.network.PacketSendListener +import net.minecraft.network.protocol.Packet +import net.minecraft.network.protocol.game.ClientboundBundlePacket +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket +import org.bukkit.craftbukkit.entity.CraftPlayer + +private val KEY = Key.key("bettermodel") + +internal fun bundlerOfNotNull(vararg packets: ClientPacket?) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.filterNotNull().toMutableList()) +internal fun bundlerOf(vararg packets: ClientPacket) = SimpleBundler(if (packets.isEmpty()) arrayListOf() else packets.toMutableList()) +internal fun bundlerOf(size: Int) = SimpleBundler(ArrayList(size)) +internal fun parallelBundlerOf(threshold: Int) = ParallelBundler(threshold) + +internal operator fun PacketBundler.plusAssign(other: ClientPacket) { + when (this) { + is SimpleBundler -> add(other) + is ParallelBundler -> add(other) + else -> throw RuntimeException("unsupported bundler.") + } +} +internal fun Packet<*>.assumeSize() = when (this) { + is ClientboundSetEntityDataPacket -> packedItems.size + is ClientboundSetEquipmentPacket -> slots.size + else -> 1 +} + +internal interface PluginBundlePacketImpl : Iterable, Keyed { + val bundlePacket: ClientboundBundlePacket + fun size(): Int + fun isEmpty(): Boolean + fun add(other: ClientPacket) +} + +internal class SimpleBundler( + private val list: MutableList +) : PacketBundler, PluginBundlePacketImpl { + override val bundlePacket = ClientboundBundlePacket(this) + override fun send(player: PlatformPlayer, onSuccess: Runnable) { + if (isEmpty) return + val connection = (player.unwarp() as CraftPlayer).handle.connection + connection.send(bundlePacket, PacketSendListener.thenRun(onSuccess)) + } + override fun isEmpty(): Boolean = list.isEmpty() + override fun size(): Int = list.size + override fun key(): Key = KEY + override fun iterator(): MutableIterator = list.iterator() + override fun add(other: ClientPacket) { + list += other + } +} + +internal class ParallelBundler( + private val threshold: Int +) : PacketBundler { + private val subBundlers = mutableListOf() + private var sizeAssume = 0 + private val newBundler get() = bundlerOf().apply { + sizeAssume = 0 + subBundlers += this + } + private var selectedBundler = newBundler + override fun send(player: PlatformPlayer, onSuccess: Runnable) { + if (isEmpty) return + val connection = (player.unwarp() as CraftPlayer).handle.connection + subBundlers.forEach { + connection.send(it.bundlePacket) + } + } + override fun isEmpty(): Boolean = selectedBundler.isEmpty() + override fun size(): Int = subBundlers.sumOf(PluginBundlePacketImpl::size) + fun add(other: ClientPacket) { + (if (sizeAssume > threshold) newBundler else selectedBundler) + .apply { selectedBundler = this } + .add(other) + sizeAssume += other.assumeSize() + } +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/PlayerArmorImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/PlayerArmorImpl.kt new file mode 100644 index 000000000..93869b1ed --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/PlayerArmorImpl.kt @@ -0,0 +1,47 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.armor.ArmorItem +import kr.toxicity.model.api.armor.PlayerArmor +import net.minecraft.core.component.DataComponents +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.item.component.DyedItemColor +import net.minecraft.world.item.equipment.EquipmentAssets +import org.bukkit.craftbukkit.entity.CraftPlayer + +internal data class PlayerArmorImpl( + private val player: CraftPlayer +) : PlayerArmor { + + override fun helmet(): ArmorItem? { + return player.handle.getItemBySlot(EquipmentSlot.HEAD).toArmorItem() + } + + override fun leggings(): ArmorItem? { + return player.handle.getItemBySlot(EquipmentSlot.LEGS).toArmorItem() + } + + override fun chestplate(): ArmorItem? { + return player.handle.getItemBySlot(EquipmentSlot.CHEST).toArmorItem() + } + + override fun boots(): ArmorItem? { + return player.handle.getItemBySlot(EquipmentSlot.FEET).toArmorItem() + } + + private fun VanillaItemStack.toArmorItem(): ArmorItem? = get(DataComponents.EQUIPPABLE)?.assetId?.map { + val trim = get(DataComponents.TRIM) + ArmorItem( + get(DataComponents.DYED_COLOR)?.rgb ?: if (it === EquipmentAssets.LEATHER) DyedItemColor.LEATHER_COLOR else 0xFFFFFF, + it.identifier().path, + trim?.pattern?.value()?.assetId?.path, + trim?.material?.value()?.assets?.base?.suffix + ) + }?.orElse(null) +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ProfiledImpl.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ProfiledImpl.kt new file mode 100644 index 000000000..20bd57b5b --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/ProfiledImpl.kt @@ -0,0 +1,24 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import kr.toxicity.model.api.armor.PlayerArmor +import kr.toxicity.model.api.nms.Profiled +import kr.toxicity.model.api.player.PlayerSkinParts +import kr.toxicity.model.api.profile.ModelProfile + +internal class ProfiledImpl( + private val playerArmor: PlayerArmor, + private val modelProfile: () -> ModelProfile, + private val playerSkinParts: () -> PlayerSkinParts +) : Profiled { + + override fun profile(): ModelProfile = modelProfile() + override fun armors(): PlayerArmor = playerArmor + override fun skinParts(): PlayerSkinParts = playerSkinParts() +} diff --git a/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/TypeAliases.kt b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/TypeAliases.kt new file mode 100644 index 000000000..79c9cf974 --- /dev/null +++ b/nms/v26_R2/src/main/kotlin/kr/toxicity/model/bukkit/nms/v26_R2/TypeAliases.kt @@ -0,0 +1,19 @@ +/* + * This source file is part of BetterModel. + * Copyright (c) 2026 toxicity188 + * Licensed under the MIT License. + * See LICENSE.md file for full license text. + */ + +package kr.toxicity.model.bukkit.nms.v26_R2 + +import net.minecraft.network.chat.Component +import net.minecraft.network.protocol.Packet +import net.minecraft.network.protocol.game.ClientGamePacketListener +import net.minecraft.world.item.ItemStack + +internal typealias VanillaItemStack = ItemStack +internal typealias BukkitItemStack = org.bukkit.inventory.ItemStack +internal typealias ClientPacket = Packet +internal typealias VanillaComponent = Component +internal typealias AdventureComponent = net.kyori.adventure.text.Component diff --git a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelLoggerImpl.kt b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelLoggerImpl.kt index a1bc135ab..2e1bfc219 100644 --- a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelLoggerImpl.kt +++ b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelLoggerImpl.kt @@ -18,20 +18,14 @@ class BetterModelLoggerImpl : BetterModelLogger { ComponentLogger.logger(LOGGER.name) } + @Synchronized override fun info(vararg messages: Component) { - synchronized(this) { - for (message in messages) { - logger.info(message) - } - } + messages.forEach(logger::info) } + @Synchronized override fun warn(vararg messages: Component) { - synchronized(this) { - for (message in messages) { - logger.warn(message) - } - } + messages.forEach(logger::warn) } companion object { diff --git a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelNMSImpl.kt b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelNMSImpl.kt index 82df11a3e..1d0fe87d8 100644 --- a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelNMSImpl.kt +++ b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/BetterModelNMSImpl.kt @@ -34,7 +34,7 @@ import net.minecraft.resources.Identifier import net.minecraft.util.ARGB import net.minecraft.world.entity.Display import net.minecraft.world.entity.Entity -import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.EntityTypes import net.minecraft.world.entity.LivingEntity import net.minecraft.world.item.ItemDisplayContext import net.minecraft.world.item.ItemStack @@ -50,7 +50,7 @@ class BetterModelNMSImpl : NMS { yOffset: Double, initialConsumer: Consumer ): ModelDisplay { - val type = EntityType.ITEM_DISPLAY + val type = EntityTypes.ITEM_DISPLAY val level = location.asFabric.level()!! val itemDisplay = Display.ItemDisplay(type, level).apply { @@ -145,7 +145,7 @@ class BetterModelNMSImpl : NMS { ) } - override fun version(): NMSVersion = NMSVersion.V26_R1 + override fun version(): NMSVersion = NMSVersion.V26_R2 override fun adapt(entity: PlatformEntity): BaseEntity = BaseFabricEntityImpl(entity.unwarp()) diff --git a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/Entities.kt b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/Entities.kt index e0029f604..677024a42 100644 --- a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/Entities.kt +++ b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/Entities.kt @@ -21,7 +21,6 @@ import net.minecraft.world.entity.ai.goal.Goal import net.minecraft.world.entity.ai.goal.RangedAttackGoal import net.minecraft.world.entity.ai.goal.RangedBowAttackGoal import net.minecraft.world.entity.ai.goal.RangedCrossbowAttackGoal -import net.minecraft.world.entity.animal.FlyingAnimal import net.minecraft.world.entity.player.Player import org.joml.Vector3f @@ -36,8 +35,7 @@ val Entity.isWalking: Boolean val Entity.isFlying: Boolean get() { - return this is FlyingAnimal && isFlying || - this is Mob && isNoAi || + return this is Mob && isNoAi || this is Player && abilities.flying || this is LivingEntity && isFallFlying } diff --git a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/command/Commands.kt b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/command/Commands.kt index c43c34d3e..4ac97afcb 100644 --- a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/command/Commands.kt +++ b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/command/Commands.kt @@ -30,6 +30,7 @@ import net.minecraft.commands.CommandSourceStack import net.minecraft.core.registries.Registries import net.minecraft.world.entity.EntitySpawnReason import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.EntityTypes import net.minecraft.world.phys.Vec3 import org.incendo.cloud.SenderMapper import org.incendo.cloud.context.CommandContext @@ -205,7 +206,7 @@ private fun spawn(context: CommandContext) { val audience = context.sender() val player = audience.player val model = context.model("model") { return audience.warn("Unable to find this model: $it") } - val type = context.nullable>("type", EntityType.HUSK) + val type = context.nullable>("type", EntityTypes.HUSK) val scale = context.nullable("scale", 1.0) val loc = context.nullable("location") type.spawn( diff --git a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/HitBoxEntityImpl.kt b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/HitBoxEntityImpl.kt index 6a27cbc7c..7ace223d6 100644 --- a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/HitBoxEntityImpl.kt +++ b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/HitBoxEntityImpl.kt @@ -49,7 +49,7 @@ class HitBoxEntityImpl( private val delegate: Entity, private var mountController: MountController ) : - AbstractArmorStand(EntityType.ARMOR_STAND, delegate.level()), + AbstractArmorStand(EntityTypes.ARMOR_STAND, delegate.level()), HitBox { private val posCache = BoneMovement() @@ -182,8 +182,9 @@ class HitBoxEntityImpl( return delegate.remainingFireTicks } - override fun knockback(d: Double, e: Double, f: Double) { - (delegate as? LivingEntity)?.knockback(d, e, f) + override fun knockback(power: Double, xd: Double, zd: Double, source: DamageSource, damage: Float, comesFromEffect: Boolean) { + if (source.entity == delegate) return + (delegate as? LivingEntity)?.knockback(power, xd, zd, source, damage, comesFromEffect) } override fun push(pushingEntity: Entity) { diff --git a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/InteractionEntityImpl.kt b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/InteractionEntityImpl.kt index 977b71795..dc03b7540 100644 --- a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/InteractionEntityImpl.kt +++ b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/InteractionEntityImpl.kt @@ -10,13 +10,13 @@ package kr.toxicity.model.impl.fabric.entity import net.minecraft.world.InteractionHand import net.minecraft.world.InteractionResult import net.minecraft.world.entity.Entity -import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.EntityTypes import net.minecraft.world.entity.Interaction import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.Vec3 class InteractionEntityImpl(val delegate: HitBoxEntityImpl) : - Interaction(EntityType.INTERACTION, delegate.level()) + Interaction(EntityTypes.INTERACTION, delegate.level()) { override fun tick() { delegate.calculateDimensions().let { dimensions -> diff --git a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/ModelNametagImpl.kt b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/ModelNametagImpl.kt index 014c39fa9..831a1f3af 100644 --- a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/ModelNametagImpl.kt +++ b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/ModelNametagImpl.kt @@ -30,7 +30,7 @@ import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket import net.minecraft.world.entity.Display -import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.EntityTypes import net.minecraft.world.entity.PositionMoveRotation import net.minecraft.world.phys.Vec3 import org.joml.Vector3f @@ -52,7 +52,7 @@ class ModelNametagImpl( private val viewedPlayer = ConcurrentHashMap.newKeySet() private val display = Display.TextDisplay( - EntityType.TEXT_DISPLAY, + EntityTypes.TEXT_DISPLAY, BetterModelMod.platform().server().overworld() ).apply { entityData[DisplayAccessor.`bettermodel$getDataPosRotInterpolationDurationId`()] = 3 diff --git a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/PlayerChannelHandlerImpl.kt b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/PlayerChannelHandlerImpl.kt index 0b6022c54..07978d9b3 100644 --- a/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/PlayerChannelHandlerImpl.kt +++ b/platform/fabric/src/main/kotlin/kr/toxicity/model/impl/fabric/entity/PlayerChannelHandlerImpl.kt @@ -31,7 +31,7 @@ import net.minecraft.server.level.ServerLevel import net.minecraft.server.network.ServerPlayerConnection import net.minecraft.world.entity.Display import net.minecraft.world.entity.Entity -import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.EntityTypes import net.minecraft.world.entity.LivingEntity import net.minecraft.world.item.ItemStack import java.util.stream.IntStream @@ -99,7 +99,7 @@ class PlayerChannelHandlerImpl( uuid, x, y, z, xRot, yRot, - EntityType.ITEM_DISPLAY, + EntityTypes.ITEM_DISPLAY, 0, deltaMovement, yHeadRot.toDouble() @@ -234,7 +234,7 @@ class PlayerChannelHandlerImpl( private val hitBoxData by lazy { Display.ItemDisplay( - EntityType.ITEM_DISPLAY, + EntityTypes.ITEM_DISPLAY, (PLATFORM as BetterModelMod).server().overworld() ).run { entityData.set(DisplayAccessor.`bettermodel$getDataPosRotInterpolationDurationId`(), 3) diff --git a/platform/paper/build.gradle.kts b/platform/paper/build.gradle.kts index d68ddf83a..e1e653a0d 100644 --- a/platform/paper/build.gradle.kts +++ b/platform/paper/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { shade(project(":nms:v1_21_R6")) { isTransitive = false } shade(project(":nms:v1_21_R7")) { isTransitive = false } shade(project(":nms:v26_R1")) { isTransitive = false } + shade(project(":nms:v26_R2")) { isTransitive = false } } modrinth { diff --git a/platform/spigot/build.gradle.kts b/platform/spigot/build.gradle.kts index 109c77124..d7e4c8b18 100644 --- a/platform/spigot/build.gradle.kts +++ b/platform/spigot/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { shade(project(":nms:v1_21_R6", configuration = "reobf")) { isTransitive = false } shade(project(":nms:v1_21_R7", configuration = "reobf")) { isTransitive = false } shade(project(":nms:v26_R1")) { isTransitive = false } + shade(project(":nms:v26_R2")) { isTransitive = false } } modrinth { diff --git a/purpur/build.gradle.kts b/purpur/build.gradle.kts index 57c454417..2f6450ad3 100644 --- a/purpur/build.gradle.kts +++ b/purpur/build.gradle.kts @@ -5,5 +5,6 @@ plugins { dependencies { compileOnly(project(":bettermodel-api")) compileOnly(project(":bettermodel-api:bettermodel-bukkit-api")) - compileOnly("org.purpurmc.purpur:purpur-api:${property("minecraft_version")}.build.+") + //TODO compileOnly("org.purpurmc.purpur:purpur-api:${property("minecraft_version")}.build.+") + compileOnly("org.purpurmc.purpur:purpur-api:26.1.2.build.+") } diff --git a/settings.gradle.kts b/settings.gradle.kts index f19bfe09d..8ec23d318 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -63,6 +63,7 @@ include( "nms:v1_21_R6", "nms:v1_21_R7", "nms:v26_R1", + "nms:v26_R2", //test "test-plugin"