diff --git a/.gitignore b/.gitignore index 8089a39..3d8a072 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ .DS_Store **.exe **.bbmodel -**.json +gen/**.json +gen/assets plugin.iml /bbconv /assets diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/BoneAnimation.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/BoneAnimation.kt new file mode 100644 index 0000000..9fc9227 --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/BoneAnimation.kt @@ -0,0 +1,10 @@ +package io.github.solid.resourcepack.bbconv.api.animation.bone + +import org.bukkit.util.Transformation + +/** + * Produces a Transformation based on the current animation context + */ +fun interface BoneAnimation { + fun animate(context: BoneAnimationContext): Transformation +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/BoneAnimationContext.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/BoneAnimationContext.kt new file mode 100644 index 0000000..bce55bb --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/BoneAnimationContext.kt @@ -0,0 +1,17 @@ +package io.github.solid.resourcepack.bbconv.api.animation.bone + +import io.github.solid.resourcepack.bbconv.api.entity.RenderedBone +import io.github.solid.resourcepack.bbconv.api.entity.RenderedEntity + +/** + * Context of an active BoneAnimation. + * This holds the bone itself, the current entity and the + * elapsed time for the current animation and the initial state (transformation property pre mutation) of the bone. + * + * Mutating the transformation property will apply this mutation to the [RenderedBone] + */ +data class BoneAnimationContext( + val entity: RenderedEntity, + val elapsedTime: Float, + val self: RenderedBone, +) diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/GroupedBoneAnimation.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/GroupedBoneAnimation.kt new file mode 100644 index 0000000..59976dc --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/GroupedBoneAnimation.kt @@ -0,0 +1,24 @@ +package io.github.solid.resourcepack.bbconv.api.animation.bone + +import org.bukkit.util.Transformation +import org.joml.Quaternionf +import org.joml.Vector3f +import org.joml.times + +class GroupedBoneAnimation( + private val animations: List +) : BoneAnimation { + override fun animate(context: BoneAnimationContext): Transformation { + var result = Transformation(Vector3f(), Quaternionf(), Vector3f(), Quaternionf()) + animations.forEach { + val transformation = it.animate(context) + result = Transformation( + result.translation.add(transformation.translation), + Quaternionf(transformation.leftRotation).times(result.leftRotation), + result.scale.add(transformation.scale), + Quaternionf() + ) + } + return result + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/KeyFramedBoneAnimation.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/KeyFramedBoneAnimation.kt new file mode 100644 index 0000000..6f001fb --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/bone/KeyFramedBoneAnimation.kt @@ -0,0 +1,24 @@ +package io.github.solid.resourcepack.bbconv.api.animation.bone + +import io.github.solid.resourcepack.bbconv.api.animation.keyframe.Timeline +import io.github.solid.resourcepack.bbconv.config.Animator +import org.bukkit.util.Transformation +import org.joml.Quaternionf +import org.joml.Vector3f + +/** + * Represents a keyframe based bone animation (being read from configs or other non-procedural sources) + */ +class KeyFramedBoneAnimation( + private val animator: Animator +) : BoneAnimation { + override fun animate(context: BoneAnimationContext): Transformation { + val scale = if (animator.scale.size >= 2) Timeline.ofScale(animator.scale) + .interpolate(context.elapsedTime) else Vector3f() + val position = if (animator.position.size >= 2) Timeline.ofPosition(animator.position) + .interpolate(context.elapsedTime) else Vector3f() + val rotation = if (animator.rotation.size >= 2) Timeline.ofRotation(animator.rotation) + .interpolate(context.elapsedTime) else Quaternionf() + return Transformation(position, rotation, scale, Quaternionf()) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimation.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimation.kt new file mode 100644 index 0000000..47ea63e --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimation.kt @@ -0,0 +1,15 @@ +package io.github.solid.resourcepack.bbconv.api.animation.entity + +import io.github.solid.resourcepack.bbconv.api.animation.bone.BoneAnimation +import io.github.solid.resourcepack.bbconv.api.animation.bone.KeyFramedBoneAnimation +import io.github.solid.resourcepack.bbconv.config.Animation + +typealias EntityAnimation = MutableMap + +object EntityAnimations { + fun of(animation: Animation): EntityAnimation { + return animation.animators.associate { animator -> animator.bone to KeyFramedBoneAnimation(animator) } + .toMutableMap() + } +} + diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimationController.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimationController.kt new file mode 100644 index 0000000..ae50ead --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimationController.kt @@ -0,0 +1,169 @@ +package io.github.solid.resourcepack.bbconv.api.animation.entity + +import io.github.solid.resourcepack.bbconv.api.animation.bone.BoneAnimation +import io.github.solid.resourcepack.bbconv.api.animation.bone.BoneAnimationContext +import io.github.solid.resourcepack.bbconv.api.entity.RenderedBone +import io.github.solid.resourcepack.bbconv.api.entity.RenderedEntity +import io.github.solid.resourcepack.bbconv.config.OpenModelConfig +import io.github.solid.resourcepack.bbconv.util.QuaternionMath +import org.bukkit.util.Transformation +import org.joml.Quaternionf +import org.joml.Vector3f + +class EntityAnimationController( + private val entity: RenderedEntity, +) { + val loadedAnimations = AnimationInfos.of(entity.config).toMutableMap() + private val activeAnimations: MutableMap = mutableMapOf() + + fun play(animation: EntityAnimation) { + val type = loadedAnimations.toList().firstOrNull { it.second == animation }?.first ?: return + activeAnimations[type] = 0f + } + + fun play(animation: String) { + val type = loadedAnimations.toList().firstOrNull { it.first.name == animation }?.first ?: return + activeAnimations[type] = 0f + } + + fun get(animation: String): EntityAnimationData? { + return loadedAnimations.toList().firstOrNull { it.first.name == animation }?.first + } + + fun cancel(animation: EntityAnimation) { + val type = loadedAnimations.toList().firstOrNull { it.second == animation }?.first ?: return + activeAnimations.remove(type) + } + + fun cancel(animation: String) { + val type = loadedAnimations.toList().firstOrNull { it.first.name == animation }?.first ?: return + activeAnimations.remove(type) + } + + fun reset() { + activeAnimations.clear() + } + + fun animate(delta: Float) { + val result = mutableMapOf() + activeAnimations.forEach { (type, currentTime) -> + val animation = loadedAnimations[type] ?: return@forEach + var time = currentTime + delta + if (time > type.duration) { + if (!type.looped) { + activeAnimations.remove(type) + return@forEach + } + time = 0f; + } + val mappedBones = bonesToAnimations(entity.rootBones, animation) + mappedBones.forEach { (bone, anim) -> + result.putAll( + partialAnimate( + anim, createContext(bone, time), result[bone] ?: Transformation( + Vector3f(), Quaternionf(), Vector3f(), Quaternionf() + ) + ) + ) + } + activeAnimations[type] = time + } + result.forEach { (bone, transformation) -> + val transformation = appendTransformation(transformation, bone.getInitialTransformation()) + bone.getDisplay().transformation = transformation + } + } + + private fun createContext(bone: RenderedBone, time: Float): BoneAnimationContext { + return BoneAnimationContext( + entity, + time, + bone, + ) + } + + private fun bonesToAnimations( + bones: List, animation: EntityAnimation + ): MutableMap { + val result = bones.filter { animation.keys.contains(it.bone.id) } + .associateWith { bone -> animation.toList().first { it.first == bone.bone.id }.second }.toMutableMap() + bones.forEach { bone -> + result.putAll(bonesToAnimations(bone.children, animation)) + } + return result + } + + private fun partialAnimate( + animation: BoneAnimation, ctx: BoneAnimationContext, transformation: Transformation + ): Map { + val result = appendTransformation(transformation, animation.animate(ctx)) + val returned = mutableMapOf() + returned[ctx.self] = result + returned.putAll(animateChildren(result, ctx.self)) + return returned + } + + private fun animateChildren( + transformation: Transformation, parent: RenderedBone + ): Map { + val result = mutableMapOf() + parent.children.forEach { child -> + val initial = child.getInitialTransformation() + val parentTrans = transformation + + // Calculate rotated local translation + val rotatedTranslation = Vector3f(initial.translation) + parentTrans.leftRotation.transform(rotatedTranslation) + + val deltaTranslation = Vector3f(rotatedTranslation).sub(initial.translation) + + // Rotation delta: how the rotation has changed + val rotatedRotation = Quaternionf(parentTrans.leftRotation).mul(initial.leftRotation) + val deltaRotation = QuaternionMath.delta(initial.leftRotation, rotatedRotation) + + // Scale delta: how the scale has changed + val deltaScale = Vector3f(parentTrans.scale).div(parent.bone.scale).mul(initial.scale) + + val childTransformation = Transformation( + deltaTranslation, + deltaRotation, + deltaScale, + Quaternionf() + ) + + result[child] = childTransformation + result.putAll(animateChildren(childTransformation, child)) + } + return result + } + + private fun appendTransformation(first: Transformation, second: Transformation): Transformation { + return Transformation( + Vector3f(first.translation).add(second.translation), + Quaternionf(first.leftRotation).mul(Quaternionf(second.leftRotation)), + Vector3f(first.scale).add(Vector3f(second.scale)), + Quaternionf() + ) + } +} + +object AnimationInfos { + + private val cache = mutableMapOf>() + + @JvmStatic + fun of(config: OpenModelConfig): Map { + if (cache.containsKey(config)) { + return cache[config]!! + } + val result = config.animations.associate { animation -> + EntityAnimationData.of(animation) to EntityAnimations.of(animation) + } + cache[config] = result + return result + } + + fun clearCache() { + cache.clear() + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimationData.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimationData.kt new file mode 100644 index 0000000..249f577 --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/entity/EntityAnimationData.kt @@ -0,0 +1,25 @@ +package io.github.solid.resourcepack.bbconv.api.animation.entity + +import io.github.solid.resourcepack.bbconv.config.Animation + +data class EntityAnimationData( + val name: String, + /** + * Negative if the duration is indefinite/procedural + */ + val duration: Float = -1f, + val looped: Boolean = false, + val loopDelay: Float = -1f, +) { + companion object { + @JvmStatic + fun of(animation: Animation): EntityAnimationData { + return EntityAnimationData( + name = animation.name, + duration = animation.length.toFloat(), + looped = animation.loop, + loopDelay = animation.loopDelay + ) + } + } +} diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/keyframe/Keyframe.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/keyframe/Keyframe.kt new file mode 100644 index 0000000..db7a6fc --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/keyframe/Keyframe.kt @@ -0,0 +1,8 @@ +package io.github.solid.resourcepack.bbconv.api.animation.keyframe + +import io.github.solid.resourcepack.bbconv.util.Interpolation + +data class Keyframe( + val data: T, + val interpolation: Interpolation, +) \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/keyframe/Timeline.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/keyframe/Timeline.kt new file mode 100644 index 0000000..e365e57 --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/animation/keyframe/Timeline.kt @@ -0,0 +1,75 @@ +package io.github.solid.resourcepack.bbconv.api.animation.keyframe + +import io.github.solid.resourcepack.bbconv.config.PositionKeyframe +import io.github.solid.resourcepack.bbconv.config.RotationKeyframe +import io.github.solid.resourcepack.bbconv.config.ScaleKeyframe +import io.github.solid.resourcepack.bbconv.util.Interpolation +import io.github.solid.resourcepack.bbconv.util.Interpolator +import org.joml.Quaternionf +import org.joml.Vector3f + +typealias TimelineFrames = Map> + +class Timeline(private val frames: TimelineFrames, private val clazz: Class) { + + private val sortedFrames = frames.toSortedMap(compareBy { it }) + + fun interpolate(time: Float): T { + require(sortedFrames.size >= 2) { "Need at least 2 keyframes, has ${sortedFrames.size}" } + val times = sortedFrames.keys.toList() + + if (time <= times.first()) return sortedFrames[times.first()]!!.data + if (time >= times.last()) return sortedFrames[times.last()]!!.data + + val index = times.indexOfLast { it <= time } + val t0 = times.getOrNull(index - 1) + val t1 = times[index] + val t2 = times[index + 1] + val t3 = times.getOrNull(index + 2) + + val k0 = t0?.let { frames[it] } ?: frames[t1]!! + val k1 = frames[t1]!! + val k2 = frames[t2]!! + val k3 = t3?.let { frames[it] } ?: frames[t2]!! + + val localT = ((time - t1) / (t2 - t1)).coerceIn(0f, 1f) + + val interpolator = Interpolator(k1.interpolation, clazz) + return interpolator.interpolate(localT, k0.data, k1.data, k2.data, k3.data) + } + + companion object { + @JvmStatic + fun ofPosition(frames: List): Timeline { + val converted = frames.associate { + it.time to Keyframe( + it.position.toVectorf().div(16f), + Interpolation.fromString(it.interpolation) + ) + } + return Timeline(converted, Vector3f::class.java) + } + + @JvmStatic + fun ofScale(frames: List): Timeline { + val converted = frames.associate { + it.time to Keyframe( + it.scale.toVectorf().div(16f), + Interpolation.fromString(it.interpolation) + ) + } + return Timeline(converted, Vector3f::class.java) + } + + @JvmStatic + fun ofRotation(frames: List): Timeline { + val converted = frames.associate { + it.time to Keyframe( + it.leftRotation.toQuaternionf(), + Interpolation.fromString(it.interpolation) + ) + } + return Timeline(converted, Quaternionf::class.java) + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/entity/RenderedBone.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/entity/RenderedBone.kt new file mode 100644 index 0000000..03a530d --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/entity/RenderedBone.kt @@ -0,0 +1,104 @@ +package io.github.solid.resourcepack.bbconv.api.entity + +import io.github.solid.resourcepack.bbconv.config.Bone +import io.github.solid.resourcepack.bbconv.config.DevFlags +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.entity.EntityType +import org.bukkit.entity.ItemDisplay +import org.bukkit.inventory.ItemStack +import org.bukkit.persistence.PersistentDataType +import org.bukkit.util.Transformation +import org.joml.Quaternionf +import org.joml.Vector3f +import java.util.* + +class RenderedBone( + private val entity: RenderedEntity, + val bone: Bone, + val children: MutableList = mutableListOf(), + private val parent: RenderedBone?, +) { + + private lateinit var display: ItemDisplay + private val initialTransformation = Transformation( + Vector3f(bone.origin[0], bone.origin[1], bone.origin[2]), + if (parent != null) Quaternionf(parent.bone.leftRotation.toQuaternionf()).mul(bone.leftRotation.toQuaternionf()) else bone.leftRotation.toQuaternionf(), + Vector3f(bone.scale), + Quaternionf(), + ) + + fun getInitialTransformation(): Transformation { + return Transformation( + Vector3f(initialTransformation.translation), Quaternionf(initialTransformation.leftRotation), + Vector3f(initialTransformation.scale), Quaternionf() + ) + } + + fun spawn() { + if (::display.isInitialized) { + return + } + display = entity.location.world.spawnEntity(entity.location, EntityType.ITEM_DISPLAY) as ItemDisplay + display.isInvisible = !bone.visible + display.itemDisplayTransform = ItemDisplay.ItemDisplayTransform.FIXED + if(DevFlags.ANIMATION_DEBUG) { + val stack = ItemStack(Material.CYAN_CONCRETE) + display.setItemStack(stack) + }else { + val stack = ItemStack(Material.BONE) + stack.editMeta { + it.itemModel = NamespacedKey.fromString(bone.model.replaceFirst("item/", "")) + } + display.setItemStack(stack) + } + + display.transformation = getInitialTransformation() + display.interpolationDuration = 1 + markDisplay() + return + } + + // Mark our current display to be a certain bone of a certain entity + private fun markDisplay() { + display.persistentDataContainer.set( + NamespacedKey("bbconv", "bone_id"), PersistentDataType.STRING, bone.id + ) + display.persistentDataContainer.set( + NamespacedKey( + "bbconv", "entity_id" + ), PersistentDataType.STRING, entity.id.toString() + ) + } + + fun getDisplay(): ItemDisplay { + return display + } + + companion object { + + /** + * Find the rendered bone of an entity in the world + */ + @JvmStatic + fun find(entity: RenderedEntity, bone: Bone): Optional { + entity.location.world.getNearbyEntitiesByType(ItemDisplay::class.java, entity.location, 20.0) + .forEach { display -> + val boneId = display.persistentDataContainer.get( + NamespacedKey("bbconv", "bone_id"), + PersistentDataType.STRING + ) + val entityId = display.persistentDataContainer.get( + NamespacedKey("bbconv", "entity_id"), + PersistentDataType.STRING + ) + if (entityId == entity.id.toString() && boneId == bone.id) { + val rendered = RenderedBone(entity, bone, parent = null) + rendered.display = display + return Optional.of(rendered) + } + } + return Optional.empty() + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/entity/RenderedEntity.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/entity/RenderedEntity.kt new file mode 100644 index 0000000..bff6e4c --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/api/entity/RenderedEntity.kt @@ -0,0 +1,69 @@ +package io.github.solid.resourcepack.bbconv.api.entity + +import io.github.solid.resourcepack.bbconv.api.animation.entity.EntityAnimationController +import io.github.solid.resourcepack.bbconv.config.Bone +import io.github.solid.resourcepack.bbconv.config.OpenModelConfig +import org.bukkit.Location +import java.util.* + +data class RenderedEntity( + val location: Location, + val config: OpenModelConfig, + val id: UUID = UUID.randomUUID(), +) { + private val renderedBones = mutableMapOf() + val rootBones = mutableListOf() + + private val animationController = EntityAnimationController(this) + + init { + location.pitch = 0f + config.boneTree.forEach { bone -> + val bone = initBone(bone) ?: return@forEach + rootBones.add(bone) + } + if (rootBones.isEmpty()) { + findRootBones(config.boneTree) + } + } + + private fun findRootBones(bones: List) { + var found = false + for (bone in bones) { + bone.children.forEach { + if (renderedBones.containsKey(it.id)) { + rootBones.add(renderedBones[it.id]!!) + found = true + } + } + } + if (!found) { + bones.forEach { bone -> + findRootBones(bone.children) + } + } + } + + private fun initBone(bone: Bone, parent: RenderedBone? = null): RenderedBone? { + var rendered: RenderedBone? = null + if (bone.visible) { + rendered = RenderedBone(this, bone, parent = parent) + parent?.children?.add(rendered) + renderedBones[bone.id] = rendered + } + bone.children.forEach { child -> + initBone(child, rendered) + } + return rendered + } + + fun getAnimationController(): EntityAnimationController { + return animationController + } + + fun spawn() { + renderedBones.values.forEach { it.spawn() } + } + + +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/DevFlags.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/DevFlags.kt new file mode 100644 index 0000000..6d07487 --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/DevFlags.kt @@ -0,0 +1,5 @@ +package io.github.solid.resourcepack.bbconv.config + +object DevFlags { + const val ANIMATION_DEBUG = false +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/io/solidresourcepack/bbconv/plugin/BaseConfig.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/OpenModelFormat.kt similarity index 56% rename from plugin/src/main/kotlin/io/solidresourcepack/bbconv/plugin/BaseConfig.kt rename to api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/OpenModelFormat.kt index fab52eb..7708110 100644 --- a/plugin/src/main/kotlin/io/solidresourcepack/bbconv/plugin/BaseConfig.kt +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/OpenModelFormat.kt @@ -1,6 +1,7 @@ -package io.solidresourcepack.bbconv.plugin +package io.github.solid.resourcepack.bbconv.config import org.joml.Quaternionf +import org.joml.Vector3f import org.spongepowered.configurate.objectmapping.ConfigSerializable import org.spongepowered.configurate.objectmapping.meta.Setting @@ -14,10 +15,8 @@ data class Bone( val origin: List = emptyList(), - @Setting("left_rotation") - val leftRotation: Quaternion = Quaternion(0f, 0f, 0f, 0f), - @Setting("right_rotation") - val rightRotation: Quaternion = Quaternion(0f, 0f, 0f, 0f), + @Setting("left_rotation") val leftRotation: Quaternion = Quaternion(0f, 0f, 0f, 0f), + @Setting("right_rotation") val rightRotation: Quaternion = Quaternion(0f, 0f, 0f, 0f), val visible: Boolean = false, @@ -30,11 +29,9 @@ data class Animation( val length: Double = 0.0, - @Setting("start_delay") - val startDelay: Float = 0f, + @Setting("start_delay") val startDelay: Float = 0f, - @Setting("loop_delay") - val loopDelay: Float = 0f, + @Setting("loop_delay") val loopDelay: Float = 0f, val name: String = "", @@ -52,7 +49,9 @@ data class Animator( val rotation: List = emptyList(), val scale: List = emptyList(), -) +) { + +} @ConfigSerializable data class PositionKeyframe( @@ -67,11 +66,9 @@ data class PositionKeyframe( data class RotationKeyframe( val time: Float = 0f, - @Setting("left_rotation") - val leftRotation: Quaternion = Quaternion(0f, 0f, 0f, 0f), + @Setting("left_rotation") val leftRotation: Quaternion = Quaternion(0f, 0f, 0f, 0f), - @Setting("right_rotation") - val rightRotation: Quaternion = Quaternion(0f, 0f, 0f, 0f), + @Setting("right_rotation") val rightRotation: Quaternion = Quaternion(0f, 0f, 0f, 0f), val interpolation: String = "", ) @@ -85,15 +82,6 @@ data class ScaleKeyframe( val interpolation: String = "", ) -@ConfigSerializable -data class Point( - val x: Float = 0f, - - val y: Float = 0f, - - val z: Float = 0f, -) - @ConfigSerializable data class Vector( val x: Float = 0f, @@ -101,7 +89,11 @@ data class Vector( val y: Float = 0f, val z: Float = 0f, -) +) { + fun toVectorf(): Vector3f { + return Vector3f(x, y, z) + } +} @ConfigSerializable data class Quaternion( @@ -119,11 +111,34 @@ data class Quaternion( } @ConfigSerializable -data class BaseConfig( +data class OpenModelConfig( val name: String = "", - @Setting("bone_tree") - val boneTree: List = emptyList(), + @Setting("bone_tree") val boneTree: List = emptyList(), val animations: List = emptyList() -) +) { + + private lateinit var boneMap: Map + + fun getBoneMap(): Map { + if (::boneMap.isInitialized) { + return boneMap + } + val result = mutableMapOf() + for (bone in boneTree) { + result.putAll(getPartBoneMap(bone)) + } + boneMap = result + return result + } + + private fun getPartBoneMap(bone: Bone): Map { + val result = mutableMapOf() + result[bone.id] = bone + for (child in bone.children) { + result.putAll(getPartBoneMap(child)) + } + return result + } +} diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/OpenModelLoader.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/OpenModelLoader.kt new file mode 100644 index 0000000..dbc5374 --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/config/OpenModelLoader.kt @@ -0,0 +1,15 @@ +package io.github.solid.resourcepack.bbconv.config + +import org.spongepowered.configurate.gson.GsonConfigurationLoader +import java.nio.file.Path + +class OpenModelLoader(private val path: Path) { + fun load(): OpenModelConfig? { + val loader = GsonConfigurationLoader.builder() + .path(path) + .build() + + val node = loader.load() + return node.get(OpenModelConfig::class.java) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/util/Interpolation.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/util/Interpolation.kt new file mode 100644 index 0000000..a340c6f --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/util/Interpolation.kt @@ -0,0 +1,102 @@ +package io.github.solid.resourcepack.bbconv.util + +import org.joml.Quaternionf +import org.joml.Vector3f + +enum class Interpolation { + LINEAR { + override fun interpolate( + t: Float, + functions: InterpolationFunctions, + vararg v: T + ): T { + val first = if (v.size > 2) v[1] else v[0] + val second = if (v.size > 2) v[2] else v[1] + return functions.linear(first, second, t) + } + }, + + CATMULLROM { + override fun interpolate( + t: Float, + functions: InterpolationFunctions, + vararg v: T + ): T { + return functions.catmullRom(v[0], v[1], v[1], v[3], t) + } + }; + + abstract fun interpolate( + t: Float, + functions: InterpolationFunctions, + vararg v: T + ): T + + companion object { + @JvmStatic + fun fromString(interpolation: String): Interpolation { + return valueOf(interpolation.uppercase()) + } + } +} + +class Interpolator( + private val interpolation: Interpolation, + clazz: Class +) { + private val functions: InterpolationFunctions = InterpolationFunctions.of(clazz) + + fun interpolate(t: Float, vararg values: T): T { + return interpolation.interpolate(t, functions, *values) + } +} + +interface InterpolationFunctions { + fun linear(a: T, b: T, t: Float): T + fun catmullRom(p0: T, p1: T, p2: T, p3: T, t: Float): T + + companion object { + fun of(clazz: Class): InterpolationFunctions { + return when (clazz) { + Vector3f::class.java -> VectorInterpolationFunctions as InterpolationFunctions + Quaternionf::class.java -> QuaternionInterpolationFunctions as InterpolationFunctions + else -> throw IllegalArgumentException("Unsupported type: $clazz") + } + } + } +} + +object VectorInterpolationFunctions : InterpolationFunctions { + override fun linear(a: Vector3f, b: Vector3f, t: Float): Vector3f { + return Vector3f().set(a).lerp(b, t) + } + + override fun catmullRom(p0: Vector3f, p1: Vector3f, p2: Vector3f, p3: Vector3f, t: Float): Vector3f { + val t2 = t * t + val t3 = t2 * t + return Vector3f( + 0.5f * ((2f * p1.x) + (-p0.x + p2.x) * t + (2f * p0.x - 5f * p1.x + 4f * p2.x - p3.x) * t2 + (-p0.x + 3f * p1.x - 3f * p2.x + p3.x) * t3), + 0.5f * ((2f * p1.y) + (-p0.y + p2.y) * t + (2f * p0.y - 5f * p1.y + 4f * p2.y - p3.y) * t2 + (-p0.y + 3f * p1.y - 3f * p2.y + p3.y) * t3), + 0.5f * ((2f * p1.z) + (-p0.z + p2.z) * t + (2f * p0.z - 5f * p1.z + 4f * p2.z - p3.z) * t2 + (-p0.z + 3f * p1.z - 3f * p2.z + p3.z) * t3) + ) + } +} + +object QuaternionInterpolationFunctions : InterpolationFunctions { + override fun linear(a: Quaternionf, b: Quaternionf, t: Float): Quaternionf { + return Quaternionf(a).slerp(b, t) + } + + override fun catmullRom( + p0: Quaternionf, + p1: Quaternionf, + p2: Quaternionf, + p3: Quaternionf, + t: Float + ): Quaternionf { + val slerp1 = Quaternionf(p1).slerp(p2, t) + val slerp2 = Quaternionf(p0).slerp(p3, t) + return slerp1.slerp(slerp2, 2f * t * (1f - t)) // not perfect but smoother than pure slerp + } + +} \ No newline at end of file diff --git a/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/util/QuaternionMath.kt b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/util/QuaternionMath.kt new file mode 100644 index 0000000..62bf04b --- /dev/null +++ b/api/src/main/kotlin/io/github/solid/resourcepack/bbconv/util/QuaternionMath.kt @@ -0,0 +1,10 @@ +package io.github.solid.resourcepack.bbconv.util + +import org.joml.Quaternionf + +object QuaternionMath { + fun delta(from: Quaternionf, to: Quaternionf): Quaternionf { + val invFrom = Quaternionf(from).invert() + return Quaternionf(to).mul(invFrom) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..278fb78 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + kotlin("jvm") version "2.1.10" + id("com.gradleup.shadow") version "8.3.3" +} + +group = "io.solid-resourcepack.bbconv" +version = "0.1.0" + +allprojects { + repositories { + mavenCentral() + maven { + name = "papermc" + url = uri("https://repo.papermc.io/repository/maven-public/") + } + } +} + +subprojects { + + apply { + plugin("org.jetbrains.kotlin.jvm") + plugin("com.gradleup.shadow") + } + + dependencies { + testImplementation(kotlin("test")) + implementation("org.spongepowered:configurate-extra-kotlin:4.1.2") + implementation("org.spongepowered:configurate-gson:4.1.2") + compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + } + + tasks.test { + useJUnitPlatform() + } + + kotlin { + jvmToolchain(21) + } +} + diff --git a/plugin/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from plugin/gradle/wrapper/gradle-wrapper.jar rename to gradle/wrapper/gradle-wrapper.jar diff --git a/plugin/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from plugin/gradle/wrapper/gradle-wrapper.properties rename to gradle/wrapper/gradle-wrapper.properties diff --git a/plugin/gradlew b/gradlew old mode 100755 new mode 100644 similarity index 100% rename from plugin/gradlew rename to gradlew diff --git a/plugin/gradlew.bat b/gradlew.bat similarity index 96% rename from plugin/gradlew.bat rename to gradlew.bat index 9b42019..9d21a21 100644 --- a/plugin/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 2037e1a..cdd3036 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -1,31 +1,17 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import dev.s7a.gradle.minecraft.server.tasks.LaunchMinecraftServerTask plugins { - kotlin("jvm") version "2.1.10" - id("com.gradleup.shadow") version "8.3.3" id("dev.s7a.gradle.minecraft.server") version "3.2.1" } -group = "io.solid-resourcepack.bbconv" -version = "0.1.0" - -repositories { - mavenCentral() - maven { - name = "papermc" - url = uri("https://repo.papermc.io/repository/maven-public/") - } -} - dependencies { - testImplementation(kotlin("test")) - implementation("org.spongepowered:configurate-extra-kotlin:4.1.2") - implementation("org.spongepowered:configurate-gson:4.1.2") - compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") + implementation(project(":api")) } -tasks.test { - useJUnitPlatform() +tasks.named("shadowJar", ShadowJar::class) { + mergeServiceFiles() + archiveFileName = "${rootProject.name}.jar" } task("launchMinecraftServer") { @@ -33,11 +19,11 @@ task("launchMinecraftServer") { doFirst { copy { - from(layout.buildDirectory.file("libs/${project.name}-${project.version}-all.jar")) + from(layout.buildDirectory.file("libs/${rootProject.name}.jar")) into(layout.buildDirectory.file("MinecraftServer/plugins")) } } - jarUrl.set(LaunchMinecraftServerTask.JarUrl.Paper("1.21.4")) + jarUrl.set(LaunchMinecraftServerTask.JarUrl.Paper("1.21.5")) agreeEula.set(true) } diff --git a/plugin/src/main/kotlin/io/solidresourcepack/bbconv/plugin/Plugin.kt b/plugin/src/main/kotlin/io/solidresourcepack/bbconv/plugin/Plugin.kt index e7e8572..eacad2b 100644 --- a/plugin/src/main/kotlin/io/solidresourcepack/bbconv/plugin/Plugin.kt +++ b/plugin/src/main/kotlin/io/solidresourcepack/bbconv/plugin/Plugin.kt @@ -1,19 +1,13 @@ package io.solidresourcepack.bbconv.plugin -import org.bukkit.Location -import org.bukkit.Material -import org.bukkit.NamespacedKey +import io.github.solid.resourcepack.bbconv.api.entity.RenderedEntity +import io.github.solid.resourcepack.bbconv.config.OpenModelLoader +import org.bukkit.Bukkit import org.bukkit.command.Command import org.bukkit.command.CommandSender -import org.bukkit.entity.EntityType -import org.bukkit.entity.ItemDisplay import org.bukkit.entity.Player -import org.bukkit.inventory.ItemStack import org.bukkit.plugin.java.JavaPlugin -import org.bukkit.util.Transformation -import org.joml.Quaternionf -import org.joml.Vector3f -import org.spongepowered.configurate.gson.GsonConfigurationLoader +import org.bukkit.scheduler.BukkitTask import java.io.File @@ -23,45 +17,33 @@ class Plugin : JavaPlugin() { this.getCommand("t")?.setExecutor(this) } + private var entity: RenderedEntity? = null + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { val path = File(this.dataFolder, "baseconfig.json").toPath() - val loader = GsonConfigurationLoader.builder() - .path(path) - .build() - - val node = loader.load() - val config = node.get(BaseConfig::class.java) ?: return false - - renderTree(config.boneTree, (sender as Player).location) - - return true - } - - fun renderTree(bones: List, loc: Location) { - for (bone in bones) { - if (bone.visible) { - boneToItemDisplay(bone, loc) + val config = OpenModelLoader(path).load() ?: return false + + if (entity == null) { + entity = RenderedEntity((sender as Player).location, config) + entity!!.spawn() + if (args.isNotEmpty()) { + val type = args[0] + val animation = entity!!.getAnimationController().get(type) ?: return false + entity!!.getAnimationController().play(type) + var elapsed = 0f + var task: BukkitTask? = null + task = Bukkit.getScheduler().runTaskTimer(this, Runnable { + entity!!.getAnimationController().animate(0.05f) + elapsed += 0.05f + if (elapsed > animation.duration) { + task?.cancel() + entity = null + } + }, 5L, 1L) } - renderTree(bone.children, loc) - } - } - - fun boneToItemDisplay(bone: Bone, loc: Location) { - val entity = loc.world.spawnEntity(loc, EntityType.ITEM_DISPLAY) as ItemDisplay - val stack = ItemStack(Material.BONE) - stack.editMeta { - it.itemModel = NamespacedKey.fromString(bone.model.replaceFirst("item/", "")) + return true } - - entity.isInvisible = false - entity.setItemStack(stack) - entity.itemDisplayTransform = ItemDisplay.ItemDisplayTransform.FIXED - entity.transformation = Transformation( - Vector3f(bone.origin[0], bone.origin[1], bone.origin[2]), - bone.leftRotation.toQuaternionf(), - Vector3f(bone.scale), - Quaternionf(), - ) + return true } } \ No newline at end of file diff --git a/plugin/settings.gradle.kts b/settings.gradle.kts similarity index 62% rename from plugin/settings.gradle.kts rename to settings.gradle.kts index 66b44dc..a8a2926 100644 --- a/plugin/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } -rootProject.name = "plugin" +rootProject.name = "bbconv" +include("plugin", "api")